├── .eslintrc.js
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── RELEASE_NOTES.md
├── docs
├── A_category_introduction.md
├── B_category_overview.md
├── C_category_sync.md
├── D_category_tutorials.md
├── E_category_extras.md
├── MyFirstGame.md
├── articleSettings.json
├── choosing_a_physics_engine.md
├── furtherreading.md
├── guide_clientengine.md
├── guide_gameengine.md
├── guide_gameworld.md
├── guide_renderer.md
├── guide_serialization.md
├── guide_serverengine.md
├── guide_syncextrapolation.md
├── guide_syncinterpolation.md
├── guide_tuningdebugging.md
├── introduction.md
├── introduction_community.md
├── introduction_faq.md
├── introduction_prologue.md
├── introduction_roadmap.md
├── overview_architecture.md
└── spaceships.md
├── jsdoc.conf.json
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
├── ClientEngine.ts
├── GameEngine.ts
├── GameWorld.ts
├── ServerEngine.ts
├── Synchronizer.ts
├── controls
│ └── KeyboardControls.ts
├── game
│ └── Timer.ts
├── lib
│ ├── MathUtils.ts
│ ├── Scheduler.ts
│ ├── Trace.ts
│ ├── Utils.ts
│ └── lib.ts
├── network
│ ├── NetworkMonitor.ts
│ ├── NetworkTransmitter.ts
│ └── NetworkedEventCollection.ts
├── package
│ ├── allExports.ts
│ ├── clientExports.ts
│ └── serverExports.ts
├── physics
│ ├── CannonPhysicsEngine.ts
│ ├── P2PhysicsEngine.ts
│ ├── PhysicsEngine.ts
│ ├── SimplePhysics
│ │ ├── BruteForceCollisionDetection.ts
│ │ ├── CollisionDetection.ts
│ │ ├── HSHG.ts
│ │ └── HSHGCollisionDetection.ts
│ └── SimplePhysicsEngine.ts
├── render
│ ├── AFrameRenderer.ts
│ ├── Renderer.ts
│ ├── ThreeRenderer.ts
│ ├── aframe
│ │ └── system.ts
│ └── pixi
│ │ ├── PixiRenderableComponent.ts
│ │ └── PixiRenderer.ts
├── serialize
│ ├── BaseTypes.ts
│ ├── DynamicObject.ts
│ ├── GameComponent.ts
│ ├── GameObject.ts
│ ├── NetScheme.ts
│ ├── PhysicalObject2D.ts
│ ├── PhysicalObject3D.ts
│ ├── Quaternion.ts
│ ├── Serializable.ts
│ ├── Serializer.ts
│ ├── ThreeVector.ts
│ └── TwoVector.ts
└── syncStrategies
│ ├── ExtrapolateStrategy.ts
│ ├── FrameSyncStrategy.ts
│ ├── InterpolateStrategy.ts
│ └── SyncStrategy.ts
├── test
├── EndToEnd
│ ├── multiplayer.js
│ └── testGame
│ │ ├── client.js
│ │ ├── server.js
│ │ └── src
│ │ ├── client
│ │ ├── MyClientEngine.js
│ │ └── MyRenderer.js
│ │ ├── common
│ │ ├── MyGameEngine.js
│ │ └── PlayerAvatar.js
│ │ └── server
│ │ └── MyServerEngine.js
├── SimplePhysics
│ ├── HSHG.js
│ └── HSHGGameEngine.js
└── serializer
│ ├── list.js
│ ├── primitives.js
│ └── string.js
├── tsconfig.json
└── utils
├── lanceInfo.js
├── npm-install.sh
└── showMovement.js
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "extends": "google",
3 | "env": {
4 | "browser": true,
5 | "node": true,
6 | "mocha": true
7 | },
8 | "installedESLint": true,
9 | "parserOptions": {
10 | "ecmaVersion": 6
11 | },
12 | "rules": {
13 | "arrow-parens": "off",
14 | "brace-style": ["error", "1tbs", { "allowSingleLine": true }],
15 | "comma-dangle": "off",
16 | "quotes": ["error", "single"],
17 | "guard-for-in": "off",
18 | "indent": ["error", 4],
19 | "linebreak-style": ["error", "unix"],
20 | "max-len": "off",
21 | "max-statements-per-line": ["error", { "max": 2 }],
22 | "no-alert": "off",
23 | "no-console": "off",
24 | "no-warning-comments": "off",
25 | "object-curly-spacing": ["error", "always"],
26 | "padded-blocks": "off",
27 | "require-jsdoc": "off"
28 | }
29 |
30 | };
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 |
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 | *.trace
9 |
10 | # Runtime data
11 | pids
12 | *.pid
13 | *.seed
14 |
15 | # transpiled code
16 | # es5
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 |
24 | # nyc test coverage
25 | .nyc_output
26 |
27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
28 | .grunt
29 |
30 | # node-waf configuration
31 | .lock-wscript
32 |
33 | # Compiled binary addons (http://nodejs.org/api/addons.html)
34 | build/Release
35 |
36 | # Dependency directories
37 | node_modules
38 | jspm_packages
39 |
40 | # Optional npm cache directory
41 | .npm
42 |
43 | # Optional REPL history
44 | .node_repl_history
45 |
46 | #jetbrains project files
47 | .idea
48 |
49 | # jsdoc output
50 | docs_out
51 |
52 | # yarn files
53 | yarn.lock
54 |
55 |
56 | deploy_key
57 | docmeta_temp
58 | docmeta_temp_deploy
59 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "6"
4 | env:
5 | global:
6 | - ENCRYPTION_LABEL: "d436d79ac9c9"
7 | - COMMIT_AUTHOR_EMAIL: "opherv@gmail.com"
8 | after_script:
9 | - bash ./deploydocs.sh
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # [Lance](https://lance-gg.github.io/) is a real-time multiplayer game server
4 |
5 | It provides an extendible Node.JS based server, on which game logic runs, as well as a client-side library
6 | which synchronizes the client's game state with the server game state. In order
7 | to provide a smooth visual experience for each connected client, Lance implements
8 | efficient networking methods, position interpolation and extrapolation, user input
9 | coordination, shadow objects, physics and pseudo-physical movement, automatic
10 | handling of network spikes.
11 |
12 | Lance aims to optimize the player's visual experience, while providing
13 | a simple development model which is highly configurable and easy to analyze
14 | and debug.
15 |
16 | ## Features:
17 |
18 | * Focus on writing your game. Lance takes care of the netcode
19 | * Can support any type of game or genre
20 | * Optimized networking
21 | * TCP via websockets
22 | * Communication is packed and serialized into binary
23 | * Automatic handling of network spikes with step correction
24 | * Intelligent sync strategies for lag handling
25 | * Extrapolation (client side prediction) with step re-enactment or:
26 | * Interpolation for optimal object motion
27 | * Tools for debugging and tracing
28 |
29 | More features in the pipeline:
30 |
31 | * UDP via WebRTC
32 | * Full-stack testing suite
33 | * Replay saving
34 | * More physics engines
35 |
36 | ## That's so neat! Where do I start?
37 |
38 | The official [Lance documentation](https://lance-gg.github.io/docs_out/index.html) contains articles on theory and rationale, as well as the structure and architecture of the project.
39 |
40 | ## Something went wrong! I need help!
41 |
42 | If you're not exactly sure how to do something, [Stack Overflow](http://stackoverflow.com/questions/tagged/lance) is your friend.
43 |
44 | If you've encountered a bug and it's not already in the [issues page](https://github.com/lance-gg/lance/issues), open a new issue.
45 |
46 |
--------------------------------------------------------------------------------
/RELEASE_NOTES.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Release Notes for Lance
4 |
5 | ## Release 5.0.1 - May 2024
6 |
7 | * TypeScript Support
8 | * major version upgrades on dependencies - fixing npm audit
9 | * move to newer socket.io
10 |
11 |
12 | ## Release 3.0.0 - July 2018
13 |
14 | * 2D Physics Support via P2
15 | * New sample game Asteroids
16 | * Interpolation mode
17 | * Common bending code
18 |
19 | ## Release 2.0.0 - February 2018
20 |
21 | ### New Features
22 |
23 | * new netscheme data type: *STRING*. Will only be broadcast if it changed since last broadcast.
24 | * PhysicsEngine no longer initialized in two places. It is initialized in the GameEngine
25 | * Implemented HSHG collision detection for SimplePhysics
26 | * Implemented ClientEngine standaloneMode for network-less testing of game engines
27 | * New KeyboardControls class to help with sending key-based input
28 | * Moved to es6 modules on all games
29 | * ES6 Modules: Modules are imported using the "import from" construct. For example import GameEngine from 'lance/GameEngine' instead of const GameEngine = require(...)
30 | * ES6 Modules: Games must configure webpack.config.js. See sample game
31 | * ES6 Modules: Babel must be configured in order to run es6 modules on node server-side, by creating a .babelrc file. See sample game
32 | * Renderer-controlled game loop support: the physics engine is asked to step forwards by dt, where dt is the time between current frame render and last frame render
33 | * full-sync support for updating all game objects when new player connects
34 | * Renderer refactored as a singleton, and instantiated only on client
35 | * Render objects and Physics objects are now sub-objects of the GameObject
36 |
37 | ### Breaking Changes
38 |
39 | * All classes are now in ES6 format instead of CommonJS
40 | * `PhysicsEngine` should no longer be instantiated in the Server `main.js` and in the client entry point. Rather, it should be instantiated in the `GameEngine` subclass for your game.
41 | * `PhysicsEngine` constructor now does all initialization. Use of `init` function is deprecated.
42 | * `GameEngine` step method cannot be called without passing the `isReenact` argument. Games which override the `step` method must pass this argument when calling the super method.
43 | * `GameObject` New `onRemoveFromWorld` to mirror `onAddToWorld`
44 | * Objects are now instantiated with a reference to the gameEngine, and get and ID automatically
45 | * Method `isOwnedByPlayer` moved from `clientEngine` to `GameEngine`, and the `clientEngine` now sets the `playerId` in the `gameEngine`. `GameObject` constructor is therefore: constructor(gameEngine, options, props) and must call the super constructor correspondingly
46 | * The `GameWorld.getPlayerObject()` method has been removed, you can get the player objects using the `GameWorld.query()` method, passing a `playerId` attribute.
47 | * constructors of `DynamicObject` and `PhysicalObject` have changed to the following: gameEngine, options, and props.
48 |
49 | ## Release 1.0.1
50 |
51 | ### Breaking Changes
52 |
53 | 1. Event `preInput` was renamed to `processInput`, `client__processInput`, `server__processInput`. `postInput`. This is a breaking change but no one actually used these events.
54 |
55 | ## Release 1.0.0 - March 2017
56 |
57 | ### New Features
58 |
59 | * Server only sends updates for objects which changed
60 | * **A-Frame** support
61 | * **Cannon.js** support
62 | * **3D** support
63 |
64 |
65 | ### Breaking Changes
66 |
67 | Note that this is a major release update, which breaks current
68 | games. To upgrade to this latest major **release 1.0.0**, you will need
69 | to apply the following changes:
70 |
71 | 1. Option `frameRate` has been renamed to `stepRate`.
72 | 2. `ClientEngine` constructor receives a third argument, a `Renderer` class. Not a `Renderer` instance but the class itself.
73 | 3. Class `Point` has been removed. Replaced by `TwoVector` and `ThreeVector`.
74 | 4. `DynamicObject` no longer has attributes `x`, `y`, `velX`, `velY`. instead it has attributes `position` and `velocity` which are instances of `TwoVector`. The `DynamicObject` constructor also has changed to receive arguments `position` and `velocity`.
75 | 5. The concept of bending has been redefined so that 0.0 means no bending
76 | towards the server value, and 1.0 means total bending towards the server
77 | value until the next server update.
78 |
--------------------------------------------------------------------------------
/docs/A_category_introduction.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lance-gg/lance/fd9bc5dce93f59684acc0c862a3a7849b993f65a/docs/A_category_introduction.md
--------------------------------------------------------------------------------
/docs/B_category_overview.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lance-gg/lance/fd9bc5dce93f59684acc0c862a3a7849b993f65a/docs/B_category_overview.md
--------------------------------------------------------------------------------
/docs/C_category_sync.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lance-gg/lance/fd9bc5dce93f59684acc0c862a3a7849b993f65a/docs/C_category_sync.md
--------------------------------------------------------------------------------
/docs/D_category_tutorials.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lance-gg/lance/fd9bc5dce93f59684acc0c862a3a7849b993f65a/docs/D_category_tutorials.md
--------------------------------------------------------------------------------
/docs/E_category_extras.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lance-gg/lance/fd9bc5dce93f59684acc0c862a3a7849b993f65a/docs/E_category_extras.md
--------------------------------------------------------------------------------
/docs/articleSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "A_category_introduction": {
3 | "title": "Introduction",
4 | "order": 1,
5 | "children": {
6 | "introduction": {
7 | "title": "Introduction"
8 | },
9 | "introduction_prologue": {
10 | "title": "Prologue"
11 | },
12 | "introduction_community": {
13 | "title": "Community"
14 | },
15 | "introduction_roadmap": {
16 | "title": "Development Roadmap"
17 | },
18 | "introduction_faq": {
19 | "title": "Frequently Asked Questions"
20 | }
21 | }
22 | },
23 | "B_category_overview": {
24 | "title": "Overview",
25 | "order": 2,
26 | "children": {
27 | "overview_architecture":{
28 | "title": "Architecture of a Multiplayer Game"
29 | },
30 | "choosing_a_physics_engine":{
31 | "title": "Choosing a Physics Engine"
32 | },
33 | "guide_gameengine":{
34 | "title": "Game Engine"
35 | },
36 | "guide_serverengine": {
37 | "title": "Server Engine"
38 | },
39 | "guide_clientengine": {
40 | "title": "Client Engine"
41 | },
42 | "guide_renderer": {
43 | "title": "Renderer"
44 | },
45 | "guide_gameworld": {
46 | "title": "Game World and Game Objects"
47 | },
48 | "guide_serialization": {
49 | "title": "Serialization and Communication"
50 | }
51 | }
52 | },
53 | "C_category_sync":{
54 | "title": "Synchronization Methods",
55 | "order": 30,
56 | "children": {
57 | "guide_syncinterpolation": {
58 | "title": "Interpolation"
59 | },
60 | "guide_syncextrapolation": {
61 | "title": "Extrapolation"
62 | }
63 | }
64 | },
65 | "D_category_tutorials": {
66 | "title": "Tutorials",
67 | "order": 50,
68 | "children": {
69 | "MyFirstGame":{
70 | "title": "My First Game: Pong"
71 | },
72 | "spaceships": {
73 | "title": "Spaaace"
74 | }
75 | }
76 | },
77 | "E_category_extras": {
78 | "title": "Extras",
79 | "order": 200,
80 | "children": {
81 | "guide_tuningdebugging": {
82 | "title": "Fine Tuning and Debugging",
83 | "order": 80
84 | },
85 | "furtherreading": {
86 | "title": "Further Reading",
87 | "order": 100
88 | }
89 | }
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/docs/choosing_a_physics_engine.md:
--------------------------------------------------------------------------------
1 |
2 | There are many different kinds of multiplayer games, and different games have different approaches to physics. Lance supports three basic physics models.
3 |
4 | * **2D Simple Physics**: A pseudo-physics arcade mode, which only tracks position, velocity, and angle for each object.
5 | * **2D Physics Engine**: A true physics engine mode, based on the engine [P2](https://github.com/schteppe/p2.js).
6 | * **3D Physics Engine**: A true 3D physics engine mode, based on the engine [Cannon](https://github.com/schteppe/cannon.js/).
7 |
8 | ## 2D Simple Physics
9 |
10 | This is probably the most common mode, and is appropriate for 2D games which don't need a real physics engine. Consider arcade games, fighting/brawler games, dungeon room games, platform, shooter, RPG, RTS, and runners. In all these cases, the game code doesn't require true physics. Instead, physical aspects such as position, velocity, and collisions are easily implemented using simple game logic. Even jumping from one platform to the next can be implemented as a sequence of height changes.
11 |
12 | In fact, many of these games are more fun to play using simplified (or pseudo) physics as they become more deterministic and predictable to the player.
13 |
14 | In order to use simple physics in Lance, you will need to initialize a `SimplePhysicsEngine` instance in the `GameEngine` constructor. In addition, all game object classes must extend the `DynamicObject` class.
15 |
16 | For example:
17 | ```javascript
18 | export default class Simple2DGameEngine extends GameEngine {
19 |
20 | constructor(options) {
21 | super(options);
22 | this.physicsEngine = new SimplePhysicsEngine({
23 | gameEngine: this,
24 | collisions: {
25 | type: 'brute'
26 | }
27 | });
28 | }
29 | }
30 | ```
31 |
32 | See the sample game [Spaaace](https://github.com/lance-gg/spaaace) to see how this is done.
33 |
34 | ## 2D Real Physics
35 |
36 | Games which require true physics in 2D can use this mode. In order to use real physics in 2D, you will need to initialize an instance of `P2PhysicsEngine` in the `GameEngine` constructor. The underlying physics engine used in this case is P2. A very lightweight 2D physics engine. All game object classes must extend the `PhysicalObject2D` base class.
37 |
38 | See the sample game [Asteroids](https://github.com/lance-gg/asteroids) to see how this is done. Also, you may want to compare Spaaace and Asteroids to see how the game experience changes from pseudo-physics to real physics.
39 |
40 |
41 | ## 3D Real Physics
42 |
43 | 3D physics games are not common. In fact, many games which appear to be 3D physics games are in fact 2.5D pseudo-physics games. In those cases, the map is really a two dimensional map, but the objects are allowed to also have a height. The lance 3D physics mode uses the native javascript Cannon physics engine, written by Schteppe.
44 |
45 | To use 3D real physics, initialize an instance of `P2PhysicsEngine` in the `GameEngine` constructor. All game object classes must extend the `PhysicalObject3D` base class.
46 |
47 | See [SprocketLeague](https://github.com/lance-gg/sprocketleague) for a sample implementation.
48 |
49 | Next: {@tutorial guide_gameengine}
50 |
--------------------------------------------------------------------------------
/docs/furtherreading.md:
--------------------------------------------------------------------------------
1 | The field of multiplayer game networking is vast, spans many years, game genres and software architectures. Here is a partial list of sources we've used while developing Lance.
2 |
3 |
4 | * **[Multiplayer Game Programming: Architecting Networked Game](https://www.amazon.com/Multiplayer-Game-Programming-Architecting-Networked-ebook/dp/B0189RXWJQ)** by Josh Glazer & Sanjay Madhav
5 | A very comprehensive book on all things multiplayer
6 |
7 | * **[Fast-Paced Multiplayer](http://www.gabrielgambetta.com/fast_paced_multiplayer.html)** by Gabriel Gambetta
8 | Describes client-side prediction and interpolation in depth
9 |
10 | * **[Gaffer on Games](http://gafferongames.com/networking-for-game-programmers/)** by Glenn Fiedler
11 | A series on Game Networking and a series on Networked Physics.
12 |
13 | * **[Synchronous RTS Engines and a Tale of Desyncs](https://blog.forrestthewoods.com/synchronous-rts-engines-and-a-tale-of-desyncs-9d8c3e48b2be#.tg3rrpjpr)** by Forrest Smith
14 | Articles discussing the multiplayer architecture of the RTS title *Supreme Commander*
15 |
16 | * **[Source Multiplayer Networking](https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking)** by Valve
17 | A thorough description of Valve's Source Engine multiplayer architecture
18 |
19 | * **[Timelines: simplifying the programming of lag compensation for the next generation of networked games
20 | Authors
21 | ](http://link.springer.com/article/10.1007/s00530-012-0271-3#Sec17)** by Cheryl Savery, T. C. Nicholas Graham
22 | In depth discussion of a timeline model for multiplayer games
23 |
--------------------------------------------------------------------------------
/docs/guide_clientengine.md:
--------------------------------------------------------------------------------
1 | The Client Engine must run the game loop, just like the server engine. However it also needs to handle game syncs which arrive from the server. Also, the client engine executes a render loop, which can run at a different rate from the game loop. The render loop frequency is determined by the display hardware available, and so it may be faster or slower than the game loop frequency.
2 |
3 | A deeper exploration of the game world synchronization algorithms is addressed in separate chapters. The client will let the synchronization method take care of updating the client's game world in a way which appears visually smooth to the player.
4 |
5 | The client collects the player's inputs, such as mouse clicks, keyboard presses, and submits them to the server by calling `ClientEngine::sendInput()` method.
6 |
7 | Additionally, the client engine handles the following functions:
8 |
9 | 1. In the case of extrapolation mode, the client will not wait for the server to apply the inputs. Instead, it will apply the inputs locall as well, with a delay if so configured
10 |
11 | 2. Transmit user inputs to server
12 |
13 | 3. Handle changes in network delay, and step drift. If the server runs faster (or slower) than the client, then the server is executing steps faster (or slower) than the
14 | client. The client engine handles these situations by skipping steps or hurrying steps as required. Step adjustment is also necessary to recover from a spike in network traffic.
15 |
16 | The Client Engine has many options which must be chosen carefully. There is an option to auto-connect. If this is not set, then the client should at some point call the `connect()` method explicitly.
17 | Another important option is the sync-options. Here the client specifies the desired synchronization method and its options. The available synchronization methods and their options will be described in a separate chapter: {@tutorial guide_synchronization_methods}.
18 |
19 | See the {@link ClientEngine} implementation in the API Reference.
20 |
21 | Next: {@tutorial guide_renderer}
22 |
--------------------------------------------------------------------------------
/docs/guide_gameengine.md:
--------------------------------------------------------------------------------
1 | The Game Engine contains the core game logic. It understands user inputs, and applies them to the game. It will fire missiles, hit aliens, boost spaceships, and apply magic spells. The primary game engine method is the step method, which progresses the game from step *N* to step *N+1*, given the inputs that arrived since step *N-1*.
2 |
3 | When the game engine step has completed, each game object must have it’s new current position updated, along with health, power, and any other game attributes your game derives.
4 |
5 | It is important to take note of the following architectural requirements:
6 |
7 | 1. The Game Engine may include some functions which will only run on the server, and these functions should be called directly by the server engine when necessary. For example, there may be a function to handle the score reporting. It is important to isolate server-only logic in a clear way.
8 |
9 | 2. On clients which use extrapolated synchronization, a given game step is often re-enacted following the arrival of a sync from the server. These "re-enactment" steps are flagged, and the game engine must ensure that the its step method does not have any side-effects when the "re-enactment" flag is set.
10 |
11 | Lastly, the game engine also provides a useful event emitter which reports on the progress of the game. A typical game will use these events to trigger any required logic with specific timing. Events include pre-step, post-step, player-joined, object-added, etc. Refer to the Events section of the API Reference for a complete list.
12 |
13 | See the {@link GameEngine} implementation in the API Reference.
14 |
15 | Next: {@tutorial guide_serverengine}
16 |
--------------------------------------------------------------------------------
/docs/guide_gameworld.md:
--------------------------------------------------------------------------------
1 | The Game Engine includes a reference to the Game World, which consists of the game state, and a collection of Game Objects. The Game World is essentially the data which is liable to change from one game step to the next, and whose changes must be sent on every sync to every client.
2 |
3 | The game objects, as implemented in Lance, are instances of game object base classes. There are two supported base object classes: `DynamicObject` and `PhysicalObject`. DynamicObject is used for 2D games, with simplified physics. PhysicalObject is used for 3D games, with full-featured physics engine. For example, in a 2D game, all the game objects will be implemented as sub-classes of DynamicObject. These objects are serializable, and can be sent over a network.
4 |
5 | Take for example a game of 2D Breakout. The implementation will have three game object classes: the Paddle, the Ball, and the Brick. The game objects will include one instance of the Paddle class, one instance of a Ball class, and multiple instances of the Brick class. (That much is true anyways of the simple version of the game, where there is only a single ball, and a single paddle, and no shooting). The Paddle, the Ball, and the Brick are subclasses of DynamicObject.
6 |
7 | A game object sub-class will define its "netscheme", which is a dictionary of networked attributes for this object. The attributes listed in the netscheme will be those exact attributes which will be serialized by the server and broadcast to all clients on every sync. The DynamicObject base class only implements positional attributes, so additional attributes such as power, energy, or health, must be specified in the netscheme.
8 |
9 | Notes for the game developer:
10 |
11 | 1. PhysicalObject subclasses are "physical". In this case, each object instance has a velocity, orientation, and continuous-ranged position (i.e. the position is a floating-point value).
12 |
13 | 2. DynamicObject sub-classes may optionally be "pseudo-physical". In this case, the object’s position is a discrete value (i.e. an integer, or low-precision decimal), determined precisely by the count of movement inputs.
14 |
15 | 3. Some game objects may be user-controlled by a client, in that same client’s engine. These objects may not need to be synchronized to server sync data, since the client information is always up-to-date. We refer to these objects as "player-controlled objects".
16 |
17 | 4. Some game objects can be teleported (position translation) and others cannot.
18 |
19 | 5. Some game objects can have their velocity change suddenly and others cannot.
20 |
21 | It is important for a game developer to understand exactly what types of objects are being used and what properties they have, in order to properly configure the synchronization.
22 |
23 | ### Game Object Sub-Assets
24 | The game objects can keep references to one or more renderable assets which are managed by the Renderer. Also, a game object can keep references to one or more physical assets (bodies & particles) which are managed by a physics engine. For example, you may have a car game object. The car object keeps references to renderable assets such as the car's chassis, antenna, and wheels. In addition, the car object can keep reference to a single body in the physics object: a simple brick-shape.
25 |
26 | See the {@link DynamicObject} implementation in the API Reference.
27 |
28 | Next: {@tutorial guide_serialization}
29 |
--------------------------------------------------------------------------------
/docs/guide_renderer.md:
--------------------------------------------------------------------------------
1 | The Renderer is a fully user-implemented component. In the architecture of an Lance multiplayer game, the renderer must render frames at the rate of the render-loop, as defined by the browser. Lance provides a full description of the game state in a list of game objects. The renderer must then scan the game objects and render them, based on position attributes, and any other object attributes which are prescribed by the game.
2 |
3 | See the {@link Renderer} implementation in the API Reference.
4 |
5 | Next: {@tutorial guide_gameworld}
6 |
--------------------------------------------------------------------------------
/docs/guide_serialization.md:
--------------------------------------------------------------------------------
1 | As mentioned in previous chapters, the game world and its associated game objects must be packed on the server side, serialized, and broadcast to all clients at regular intervals. These game world update are called syncs.
2 |
3 | Serialization requires that each game object define those attributes which must be serialized on every sync. This attribute list is known as the game object’s netscheme. The larger a netscheme is, the more data will need to be transferred on every sync. Therefore care should be taken to minimize the amount of data in the netscheme.
4 |
5 | The serialization process allows for a hierarchy of objects. This means that the netscheme of one object may include another object which has its own netscheme.
6 |
7 | Synchronizations of the game state occurs at regular intervals, which are less frequent than the game steps. For example there may be 1 sync for every 10 steps. However
8 | this can be problematic for game events which occur at a very specific step. For example, a sync that describes the game evolution from step *N* to step *N+10* may
9 | need to report that a projectile was fired at step *N+5*. In order to handle this requirement the game must create "atomic" events.
10 |
11 | Next: {@tutorial guide_syncinterpolation}
12 |
--------------------------------------------------------------------------------
/docs/guide_serverengine.md:
--------------------------------------------------------------------------------
1 | The Server Engine is a relatively simple component. As mentioned in the introduction, the server engine must do the following: (1) it must initialize the game, (2) it must accept connections from players over a socket, (3) it must execute the game loop, by calling the game engine’s `step()` method at a fixed interval, (4) it must apply the player inputs from all connections, and (5) it must broadcast regular updates to all the clients at a fixed interval.
2 |
3 | The game developer will extend this logic, by adding functions that log gameplay statistics, registering user activity, wins, losses, achievements. One of the extended methods is `GameEngine::processInput()` which applies a player's input to change the game state.
4 |
5 | Care must be taken on the following point: the server engine should not be used to contain game logic - that belongs in the game engine class.
6 |
7 | See the {@link ServerEngine} implementation in the API Reference.
8 |
9 | Next: {@tutorial guide_clientengine}
10 |
--------------------------------------------------------------------------------
/docs/guide_syncextrapolation.md:
--------------------------------------------------------------------------------
1 | A different approach to synchronization is to extrapolate the game’s progress on the client, in the absence of sync data from the server. Once sync data does arrive from the server, a client must reconcile between the game state predicted on the client, and the game state that actually occurred on the server.
2 |
3 | 
4 |
5 | In the diagram above, while the server is playing out step 1026, the clients are simultaneously rendering step 1030, which didn’t happen yet on the server. This implies that the clients used extrapolation to predict the future.
6 |
7 | Extrapolation is tunable, just like interpolation, by setting the number of steps that a client should be ahead of the server. Ideally, the client is just a little bit ahead, just enough so that when sync data arrives, the highest step information in the sync matches the current step played out on the client.
8 |
9 | An input which is sent by a client may arrive before the server has started playing the corresponding step. In this case the Server can be configured to wait for the input’s step. Since the server will process the input at a later step, the client may want to impose an artificial delay before actually enacting the input locally. This is another tunable parameter.
10 |
11 | When a sync arrives at the client, the client must reconcile the predicted game state with the actual (server) game state. This is performed as follows:
12 |
13 | 1. Client is at step N.
14 |
15 | 2. Roll back the state to the step described by the server in the sync, step M.
16 |
17 | 3. Re-enact all the steps between M and N by invoking the game engine’s step() method.
18 |
19 | 4. The game objects positions have shifted from the predicted value. Revert back to the client’s expected position, but remember the required delta between the client and the server.
20 |
21 | 5. Apply the delta so that the object on the client gradually bends towards the correct position as indicated by the server.
22 |
23 | 1. Not all the delta is applied. A "bending factor" must be specified, to indicate how much of the delta should be applied.
24 |
25 | 2. The delta is not applied in a single step. Rather, split the correction into incremental corrections that are applied until the next sync data arrives.
26 |
27 | Notes for the game developer: Multiple complications arise from the extrapolation method.
28 |
29 | 1. **Re-enactment**. Extrapolation re-enacts the game steps, and sometimes it may re-enact many steps. This means that the game engine must be capable of stepping through the same step multiple times. These are known as re-enactment steps. The step() method is passed an argument so that it can know when a step is a re-enactment step.
30 |
31 | 2. **Bending**. The client’s objects positions gradually bend towards the server’s positions. Velocity can also bend. The game designer must choose bending values carefully. Objects whose position can change suddenly (teleporting objects) will not bend well. Objects whose velocity can change suddenly (impulse) should not have velocity bending.
32 |
33 | 3. **Shadow Objects**. The client may create an object which does not yet exist on the server. For example, if a ship fires a missile, the client must render a missile, even though the missile object is yet to exist on the server. Until the true missile object will be created on the server, the client will model the missile with a shadow object.
34 |
35 | To select this synchronization method, the Client Engine syncOptions must be set to "extrapolate". The option "localObjBending" describes the amount of bending (0.0 to 1.0) for local objects, and "remoteObjBending" describes the amount of bending (0.0 to 1.0) for remote objects. See the client engine options as described in the API Reference: {@link ClientEngine}.
36 |
37 | Next: {@tutorial tutorials}
38 |
--------------------------------------------------------------------------------
/docs/guide_syncinterpolation.md:
--------------------------------------------------------------------------------
1 | One way to synchronize the server and the clients is known as Interpolation. In this method, the clients render steps which previously played out on the server. The diagram below shows a typical sequence in an interpolated game. Time is advancing downwards. At the time when the server is playing out step 1026, the players are rendering the older step 1010.
2 |
3 | 
4 |
5 | This approach has an important advantage, and an important disadvantage. The advantage is that each player has enough information available about the future in order to render visually smooth object motion. If a sync arrived at step 1010, and another at step 1020, then the object positions at the intermediate steps can be interpolated. The resulting visuals are smooth.
6 |
7 | The disadvantage of this approach is that when a player input arrives on a client, the consequences of this input will only kick in at a later time, potentially causing noticeable lag between a user’s input and the visual response. In the diagram above, an input that Player A submitted while Client A was at step 1010, will only get a visual response when Player A starts rendering objects at step 1029.
8 |
9 | The time delay between the server and the clients is a tunable parameter. Ideally one would choose the smallest delay possible such that there is still enough sync data to interpolate to. A temporary spike in network delays will cause the client to stop rendering and wait for new data. In order to recover from such spikes, the client should attempt to reduce the delay gradually.
10 |
11 | Notes for the game developer:
12 |
13 | 1. Not all game progress can be interpolated. Bouncing off walls, shooting, etc. are "atomic" steps which are not interpolatable.
14 |
15 | 2. Actions which are atomic must be explicitly marked as such, otherwise the game visuals will not make sense.
16 |
17 | 3. The delay in input response may mean that a given game is not a good match for interpolated synchronization. In that case, extrapolation should be considered. However, there are techniques which can make interpolation work nonetheless. Real-world input delay is about 200ms. There may be visual tricks that make this delay acceptable. For example if the input boosts a rocket, the game might show a visual boost light up before the rocket actually starts accelerating.
18 |
19 | 4. The game engine may not need to run on the client at all, if all the information required for rendering is available in the server sync.
20 |
21 | Next: {@tutorial guide_syncextrapolation}
22 |
--------------------------------------------------------------------------------
/docs/guide_tuningdebugging.md:
--------------------------------------------------------------------------------
1 | In the prologue of this documentation, the complexity of multiplayer network games becomes evident. But that
2 | is small potatoes compared to debugging multiplayer network games. The debugging of a multiplayer game
3 | is made difficult by the following reasons:
4 |
5 | 1. A multiplayer network game consists of several non-symmetric hosts running non-symmetric software, separated by non-symmetric networks.
6 | 2. The problem may be very hard to reproduce, and exists only under certain race conditions.
7 | 3. The problem may be in the game mechanics as implemented in the game engine. For example, in the game of Pong, one might forget to check if the left paddle was hit *before* one checks if the left wall was hit (true story).
8 | 4. Then again, the problem may be in the configuration of the synchronization. For example, in the game of Pong, one might forget to disable velocity interpolation, and when the ball's velocity changes direction, the velocity interpolation causes strange visual effects (also, a true story).
9 | 5. Then again, the problem may be due to network spikes.
10 | 6. And finally, there is always the possibility that the game in question is not a good match for the architecture presented in this
11 | guide, and some of the assumptions in the game may need to be revisited in order to make the multiplayer experience viable with the proposed architecture.
12 |
13 | In all the cases above, the game developer sees the same symptom: an object jumps around strangely on the screen!
14 |
15 | At first this can be *very* frustrating, because the game developer does not know where to start. This document
16 | was written to help with the process of debugging multiplayer networked games using an organized process which
17 | help to narrow down the problem efficiently.
18 |
19 | ## The basic debugging process
20 |
21 | The following steps define an efficient debugging process:
22 |
23 | 1. Is the problem also on the Server, or just when the client tries to render the visuals?
24 | 1. If the problem is on the server, use server traces to see how the objects states change at each step.
25 | 2. If the problem is only on the client, is the problem related to Synchronization?
26 | 2. Is the problem caused by unusual network conditions?
27 |
28 |
29 | ## Server or Client
30 |
31 | The problem is always first observed on the client. To determine if the problem
32 | exists on the server as well, the easiest approach is to use the Reflect synchronization
33 | strategy. The Reflect method is not really a synchronization strategy but rather a degenerate
34 | synchronization method which simply shows the server sync data as it arrives.
35 |
36 | To run a game in reflect mode, simply use the URL query string option:
37 |
38 | ```
39 | http://127.0.0.1:3000/?sync=reflect
40 | ```
41 |
42 | The problem can also occur in such a way that it usually happens in between syncs.
43 | In this case, one can configure the server to increase the update rate so that
44 | it approaches the client's render rate.
45 |
46 | The ServerEngine has options to set the step rate and the sync update rate:
47 | ```javascript
48 | const serverEngine = new MyServerEngine(io, gameEngine, {
49 | updateRate: 6,
50 | stepRate: 60,
51 | });
52 | ```
53 |
54 | Lastly, an important technique is to test the game on a local network (i.e. run server
55 | and clients on same host) to make sure that network delay is not a factor.
56 |
57 | ## Check Server Traces
58 |
59 | To enable server traces, set the trace level when the game engine is created:
60 | ```javascript
61 | const gameEngine = new MyGameEngine({ traceLevel: 1 });
62 | ```
63 |
64 | See the Trace class for trace constants. The trace output file is called `server.trace`
65 | and it will show trace data which looks as follows:
66 |
67 | ```
68 | [2016-12-04T22:16:15.136Z]719>game engine processing input[10052] from playerId 1
69 | [2016-12-04T22:16:15.153Z]720>game engine processing input[10053] from playerId 1
70 | [2016-12-04T22:16:15.153Z]720>========== sending world update 720 ==========
71 | [2016-12-04T22:16:15.170Z]721>game engine processing input[10054] from playerId 1
72 | [2016-12-04T22:16:15.170Z]721>========== destroying object DynamicObject[2] position(273.854, 304.23, NaN) velocity(-4.529, -2.119, NaN) angle205 ==========
73 | ```
74 |
75 | Each line starts with a timestamp, and a step number. The example above shows
76 | steps 719-720. At the lowest trace level, the service will show each object's
77 | attributes, including position, at the end of every step.
78 |
79 | To make debugging easier, you may want to implement your object's `toString()` method,
80 | to show a useful representation of the object. This will be *very useful* in the long run.
81 |
82 | ## Check Client Traces
83 |
84 | Enabling client traces can be done from the URL's query string, using the *traceLevel*
85 | parameter. Here is an example:
86 | ```
87 | http://127.0.0.1:3000/?sync=reflect&traceLevel=0
88 | ```
89 |
90 | Since there are multiple clients, there are also multiple trace files, one for each client. The trace files are named `client..trace`.
91 |
92 | ### Client Interpolation
93 |
94 | Note that in interpolation, the client actually plays a step which is older than the current
95 | step. Ensure you check the trace to see which step the interpolation is targeting.
96 | Interpolation runs the client's game engine in passive mode, meaning that the game engine
97 | on the client will not execute the physics steps.
98 |
99 | ### Client Extrapolation
100 |
101 | For extrapolation synchronization, the client will re-enact the required history of steps
102 | each time a new sync is received. This is traced in low-level detail. To debug
103 | problems related to bending, look at the position attributes of the relevant object
104 | just before the sync was received, and the result of the re-enactment before bending
105 | was applied, and the final object position after bending was applied.
106 |
--------------------------------------------------------------------------------
/docs/introduction.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/lance-gg/lance) [](http://inch-ci.org/github/lance-gg/lance)
2 | [](http://slack.lance.gg)
3 |
4 |
5 |
6 | # [Lance](http://lance.gg) is a real-time multiplayer game server
7 |
8 | Lance provides an extendible Node.JS based server, on which game logic runs, as well as a client-side library
9 | which synchronizes the client's game state with the server game state. In order
10 | to provide a smooth visual experience for each connected client, Lance implements
11 | efficient networking methods, position interpolation and extrapolation, user input
12 | coordination, shadow objects, physics and pseudo-physical movement, automatic
13 | handling of network spikes.
14 |
15 | Lance aims to optimize the player's visual experience, while providing
16 | a simple development model which is highly configurable and easy to analyze
17 | and debug.
18 |
19 | ## See it in action
20 | Check out the official demo: [Spaaace](http://spaaace.herokuapp.com)
21 |
22 | 
23 |
24 |
25 |
26 | ## Features:
27 |
28 | * Focus on writing your game. Lance takes care of the netcode
29 | * Can support any type of game or genre
30 | * Optimized networking
31 | * TCP via websockets
32 | * Communication is packed and serialized into binary
33 | * Automatic handling of network spikes with step correction
34 | * Intelligent synchronization strategies for lag handling
35 | * Extrapolation (client side prediction) with step re-enactment, or:
36 | * Interpolation for optimal object motion
37 | * Tools for debugging and tracing
38 |
39 | ## That's so neat! Where do I start?
40 |
41 | To get a bird's eye view on what's Lance all about and how to use it to make a multiplayer game, we recommend reading the guide: {@tutorial overview_architecture}
42 |
43 | If you're more of the learning-by-doing type, you can start with the first tutorial, {@tutorial MyFirstGame} which contains step-by-step instructions on how to implement a networked version of this classic game.
44 |
45 | ## Join the fun!
46 |
47 | We are working hard to build an active, positive and inclusive {@tutorial introduction_community}
48 |
49 | ## Need help?
50 |
51 | If you're not exactly sure how to do something, [Stack Overflow](http://stackoverflow.com/questions/tagged/lance) is your friend.
52 |
53 | If you've encountered a bug and it's not already in the [issues page](https://github.com/lance-gg/lance/issues), open a new issue.
54 |
55 |
56 | ## Built something cool with Lance?
57 |
58 | Please [Let us know](http://www.twitter.com/opherv)! We'd love to play it, and feature it on the [Lance homepage](http://lance.gg).
59 |
--------------------------------------------------------------------------------
/docs/introduction_community.md:
--------------------------------------------------------------------------------
1 | We make great efforts to maintain an active, positive, friendly and inclusive community around Lance and game development.
2 |
3 | ### Starting out
4 |
5 | To get a bird's eye view on what's Lance all about and how to use it to make a multiplayer game, we recommend reading the guide: {@tutorial architecture}
6 |
7 | If you're more of the learning-by-doing type, you can start with the first tutorial, {@tutorial MyFirstGame} which contains step-by-step instructions on how to implement a networked version of this classic game.
8 |
9 | ### Joining the discussion
10 | You're welcome to join the [Lance Slack](http://slack.lance.gg) to hang out and engage in active discussion with the project maintainers and fellow game developers.
11 |
12 | ### Keeping in touch
13 |
14 | Follow [LanceGG_](http://twitter.com/LanceGG_) on Twitter to hear the latest and see what others are doing with Lance
15 |
16 | ### Getting help
17 | Feel free to ask anything on [Stack Overflow](http://stackoverflow.com/questions/tagged/lance). We keep an eye out for questions!
18 |
19 | ### Helping out
20 |
21 | Lance is an open source project, and as such we welcome any contribution.
22 |
23 | * Spreading the word, posting or tweeting about Lance in social media using the (https://twitter.com/lancegg_)[@lancegg_ tag]. The larger the community, the more fun we have and the faster development can move!
24 |
25 | * Improving the documentation: fixing typos, expanding, clarifying or writing new articles
26 |
27 | * Fixing bugs, or adding new functionality. A good place to start would be issues on Github with a status of **["PR Welcome"](https://github.com/lance-gg/lance/issues?q=is%3Aissue+is%3Aopen+label%3A%22PR+welcome%22)**. Make sure to open pull requests against the develop branch.
28 |
--------------------------------------------------------------------------------
/docs/introduction_faq.md:
--------------------------------------------------------------------------------
1 |
2 | ## Lance Basics
3 |
4 | This list of questions is derived mostly from recurring questions and discussions on our slack group. The list is meant as a high-level starting point, but you are encouraged to join in the conversation. Auto-join the [Lance Slack group with this link](http://slack.lance.gg).
5 |
6 | ### 1. What is Lance?
7 |
8 | Lance is an open-source project which makes it easier to write multiplayer physics networked games in JavaScript. Lance implements some components of the multiplayer game, so that a game developer can focus on the game itself. The components which Lance implements are networking, client-side prediction of object positions, extrapolation, 3D/2D networked physics, and more.
9 |
10 | ### 2. What is the basic structure of an Lance game?
11 | Lance uses a client-server model. There is a single authoritative server, and multiple clients in each networked game. A game based on Lance will have server code, client code, and common code. The game logic is common code, and can run on the authoritative server as well as on every client.
12 |
13 | ### 3. What is client-side prediction?
14 | Client-side prediction is the logic which runs on every client, and extrapolates the state of the game. In the Lance implementation, client-side prediction uses physical attributes such as position, velocity, orientation, and angular velocity to predict the game state on each client. This provides a smooth playing experience for each client, hiding the side-effects of network lag and network spikes.
15 |
16 | ### 4. What is bending?
17 | Bending is the process of gradually correcting the positions and orientations of game objects on the client, which have drifted from their authoritative positions and orientations on the server. Bending is applied incrementally on each renderer draw event.
18 |
19 | ## Lance Usage
20 |
21 | ### 1. Does Lance scale?
22 | The answer to this question highly depends on the game type. For games where a few players join separate rooms or “zones” within the game, an Lance server can be used for each room. This is the simple case.
23 |
24 | In the case of large worlds, where hundreds of players join a single space, Lance needs to separate the single space into subspaces such that each Lance server is responsible for one of the subspaces. This feature has not been implemented yet. If you’re interested in this feature please upvote the following [feature request](https://github.com/lance-gg/lance/issues/30).
25 |
26 | ### 2. Does Lance preserve state?
27 | Since Lance is designed for real-time calculation of position, running typically at 60 steps per second, state-preservation is not a primary concern. However, this has been requested in the past. If you’re interested in this feature please upvote the following [feature request](https://github.com/lance-gg/lance/issues/31).
28 |
29 | ### 3. How do I implement chat services with Lance?
30 | It’s preferred that non-real time communication is handled by a different server than the one which runs the game state, therefore chat services are outside the scope of Lance. The preferred approach is to use an existing chat service together with Lance in your game. There is a feature request to provide a sample game which demonstrates Lance working together with a third-party chat service. If you’re interested in this feature please upvote the following [feature request](https://github.com/lance-gg/lance/issues/32).
31 |
32 | ### 4. What is in the Lance Roadmap?
33 | See the Roadmap: {@tutorial introduction_roadmap}. Some of the major planned features include UDP support, and Visual Debugging.
34 |
35 | ## Lance Packaging / Transpiling
36 |
37 | ### 1. Can I use Lance with babel?
38 | Yes! Lance exports es6 code, so you will need to include it in your babel configuration so that it gets transpiled with the rest of your code. This can be done using the include option in your .babelrc or webpack.config file. See the sample games provided in the Github Lance-gg organization.
39 |
--------------------------------------------------------------------------------
/docs/introduction_prologue.md:
--------------------------------------------------------------------------------
1 | ## ...or why making multiplayer games is so hard
2 |
3 | If you've ever made a single-player video game, you already know that regardless of the platform, genre or mechanic - developing a game is a huge undertaking. There are so many challenges to overcome including narrative, game design, graphics, sound, AI, performance and many others. Even if you get all of those right - the game might still not be fun. A well-executed game that's no fun is not even worth playing, despite all the effort put into it.
4 |
5 | Making a multiplayer game is even harder than a single-player one. It takes all the difficulties already encountered while developing a single-player game and amplifies them by orders of magnitude.
6 |
7 | Your major obstacle? The speed of light.
8 |
9 | Well, not exactly, but the speed of light is THE upper bound for the fastest speed anything can physically move at, and that includes data between computers. Despite the contrary belief, the speed of data isn't instantaneous.
10 |
11 | Let's say a game server is in Melbourne, Australia, and you, the player, is connecting from Tel Aviv, Israel. The distance between the two cities is 13,759 Kilometers, or 13,759,000 meters.
12 | The speed of light is 299,792,458 meters per second. Assume for a second that there are no computers, networks or routers in the way - and that data moves at the speed of light. An update travelling from the server in Australia to the in Israel will take 45 milliseconds to reach its destination. So if you hit a key to move your car in-game, that command gets sent to the server in Melbourne and than the result of that command is sent back to your computer in Tel Aviv. The roundtrip alone is 90ms! That is without any other delays generated by network, hardware or software.
13 |
14 | 
15 |
16 |
17 | What does this mean though? It means that lag, or latency, is not a "bug" or a "network condition". It is a constant, ever-present and inherent to the system. A player can never know the true current state of the game, because it always gets updates in delay. The server can't know the true state of the game, because all input from clients will arrive delayed.
18 |
19 | #### Real-time multiplayer games aren't actually real-time. They only offer the *illusion* that everyone is playing the same game, in the same world, at the same time.
20 |
21 | Dealing with all the issues associated from networking games is a daunting task that requires knowledge, persistence and constant vigilance.
22 |
23 | But you're a game designer - not a network engineer and that's exactly why we built Lance. We want to free game developers to develop games, not network synchronisation code. We aim to build a thriving community around Lance and hope to see many more multiplayer games out there that truly connect people. That's what multiplayer games are all about
24 |
25 | Opher & Gary
26 |
27 |
28 |
29 |
30 | Next: {@tutorial overview_architecture}
31 |
--------------------------------------------------------------------------------
/docs/introduction_roadmap.md:
--------------------------------------------------------------------------------
1 |
2 | ## Current Release
3 |
4 | ### r4.0.0 - *Noether* - May 2019
5 |
6 | Release 4.0.0 is now available for npm-installing. It provides mostly infrastructure improvements, which were required for some basic feature implementation.
7 |
8 | * adds rollup-js for native modules. This will allow tree-shaking on the game side, making the client code smaller. It also enables TypeScript exports, which is the most popular feature request.
9 | * one-page games. There are several one-page game examples in the examples repo [tinygames](https://github.com/lance-gg/tinygames).
10 | * support for rooms (game object name-spacing). Using new methods in the ServerEngine: `createRoom()`, `assignObjectToRoom()`, and `assignPlayerToRoom()`
11 | * upgrade to babel 7.0
12 |
13 | ## Future Releases
14 |
15 | All releases listed here, along with their planned release dates and their listed contents, are a statement of intentions, and are provided with no guarantee whatsoever. Lance is an open-source project and as such depends on the available time of its developers. The roadmap is subject to change at any time.
16 |
17 | ### r5.0.0 - *Maxwell* - December 2019
18 |
19 | ## Roadmap Candidates
20 |
21 | There are many candidates, please upvote your favourite at the corresponding github issue-request.
22 |
23 | * entity-component-system redesign
24 | * typescript
25 | * UDP via WebRTC
26 | * Electron support
27 | * MatterJS support
28 | * Automated Cloud Deploy system for Lance game servers
29 | * Proper testing framework
30 | * Better debug tools:
31 | * Parses recorded logs
32 | * shows GUI that allow scrubbing through time to see values over time of client state, server state, interpolated/extrapolated state
33 |
34 | ## Past Releases
35 |
36 | ### r3.0.0 - *Majorana* - July 2018
37 | * 2D engine support - P2
38 | * New sample game Asteroids
39 | * Interpolation mode
40 | * Generic bending code
41 |
42 | ### r2.0.1 - *Spinor* - February 2018
43 | * ES6 Modules support
44 | * Renderer-controller game loop. The game step delta is tuned to the render draw time
45 | * Full-sync support, providing full data sync to new connections
46 | * Game Object re-architecture: Renderer objects and Physics objects are sub-objects of the Game Object
47 | * New KeyboardControls class
48 | * Smart sync, syncing only changed objects
49 |
50 | ### r1.0.1 - *Tensor* - March 2017
51 | * Full 3D support
52 | * Pluggable Physics Engine support: cannon.js
53 | * Demonstrate A-Frame support
54 | * Refactor: game objects contain render and physics sub-objects
55 |
56 | ### r0.9.1 - “Incheon Phase 2” External Beta Release - January 2017
57 |
58 | * Games: Pong, Spaaace
59 | * Sync Strategies: Extrapolation, Interpolation
60 | * Complete Documentation
61 | * Spaaace - Online Desktop/Mobile Live Demo
62 | * Refactor event names (remove dot) to make compatible with jsdoc
63 |
64 | ### r0.2.0 - “Incheon Phase 1” Internal Release - December 2016
65 |
66 | * Games: Pong
67 | * Sync Strategies: Extrapolation, Interpolation
68 | * Refactor Renderer
69 | * Lance.gg web site
70 | * Boilerplate Game Repository
71 | * Documentation started. Tutorials: MyFirstGame, Spaceships
72 |
73 |
74 | ### r0.1.0 - "Incheon" October 2016
75 |
76 | * First working model
77 | * Games: Spaaace, Sumo
78 | * Sync Strategies: ServerSync, Interpolation
79 |
--------------------------------------------------------------------------------
/docs/overview_architecture.md:
--------------------------------------------------------------------------------
1 |
2 | The architecture of a multiplayer game must meet the requirements and challenges presented in the {@tutorial prologue} "why is making a multiplayer game so hard". The architecture cannot wish away these facts: that network delays exist, the delay durations are not consistent or predictable, that players can suddenly disconnect, or that client code cannot be trusted.
3 |
4 | These requirements lead to some basic architectural principles:
5 |
6 | 1. Each game instance will have a single server where the actual game progress and decisions are played out.
7 |
8 | 2. A client will always be out-of-sync with the server, and will need to be able to adjust itself to server decisions.
9 |
10 | 3. In order to provide a smooth playing experience, a client will need to implement either some predictive calculation (extrapolation), or otherwise present a somewhat out-of-date state to the end user (interpolation).
11 |
12 | The main components of a networked game are:
13 |
14 | * The **clients**. Represented in Lance by multiple instances of the **ClientEngine** class. Clients collect inputs from the player and send them to the server.
15 |
16 | * The **server**. Represented in Lance by a singleton instance of the **ServerEngine** class. The server handles the user inputs, and sends updates to all clients.
17 |
18 | * The **game logic**. Represented in Lance by the **GameEngine** class.
19 |
20 | * The **game world**, which includes multiple **game objects**. The Lance **GameObject** is the base class for all kinds of game objects.
21 |
22 | * The **renderer**. A component which draws the game visuals on every iteration of the render loop. In Lance this is represented by the **Renderer** class.
23 |
24 | * **Synchronization**. Lance provides several ways to synchronize between the server and the clients. The game developer must configure which synchronization method works best for their specific game and use case.
25 |
26 | As you develop your game, you will need to implement your own extensions (sub-classes) of the classes above. The core game logic will be implemented in your extension of GameEngine.
27 |
28 | The following diagram shows how these components connect in the overall architecture:
29 |
30 | 
31 |
32 | ## The Game as a Sequence of Steps
33 |
34 | The basic flow of a game can be seen as a sequence of *game steps*. This is a basic concept which is true
35 | for game development generally, and the concept works well for networked games as well. During a single step, the
36 | game progresses from time *T* to time *T + δt*. The game engine will have to determine the state of the game
37 | at time *T + δt* by applying physics, taking account of new user inputs, and applying the game mechanics logic.
38 |
39 | In the context of multiplayer, networked games, the steps will be executed both on the server and the client. Each step is numbered, and depending on the synchronization strategy, clients may be executing a given step before the corresponding server information has arrived at the client (i.e. extrapolation) or after (i.e. interpolation). Ideally, a given step *N* represents the same point in game play on both the server and the client.
40 |
41 | The core game logic is implemented in the game engine, so a game step is simply a call to the game engine’s `GameEngine::step()` method.
42 |
43 | ## Server Flow
44 |
45 | The server logic is implemented by the server engine, which must do the following: (1) it must initialize the game, (2) it must accept connections from players over a socket, (3) it must execute the game loop, by calling the game engine’s `GameEngine::step()` method at a fixed interval, and (4) it must broadcast regular updates to all the clients at a fixed interval.
46 |
47 | The server engine schedules a step function to be called at a regular interval. The flow is:
48 |
49 | * ServerEngine - *start of a single server step*
50 |
51 | * GameEngine - read and process any inputs that arrived from clients since the previous step. The inputs are handled by the `GameEngine::processInput()` method.
52 |
53 | * GameEngine - *start of a single game step*
54 |
55 | * PhysicsEngine - handle physics step
56 |
57 | * If it is time to broadcast a new sync
58 |
59 | * for each player: transmit a "world update"
60 |
61 | ## Client Flow
62 |
63 | The client flow is more complicated than the server, for two reasons. First it must listen to syncs which have arrived from the server, and reconcile the data with its own game state. Second, it must invoke the renderer to draw the game state.
64 |
65 | * ClientEngine - *start of a single client step*
66 |
67 | * check inbound messages / syncs
68 |
69 | * capture user inputs that have occurred since previous step. Inputs are sent to the server by calling the method `ClientEngine::sendInput()`.
70 |
71 | * transmit user inputs to server
72 |
73 | * apply user inputs locally
74 |
75 |
76 |
77 | * ClientEngine - *start of a single render step*
78 |
79 | * Renderer - draw event
80 |
81 | * GameEngine - *start of a single game step* - may need to be executed zero or more times, depending on the number of steps which should have taken place since the last render draw event
82 |
83 | * PhysicsEngine - handle physics step
84 |
85 | Next: {@tutorial choosing_a_physics_engine}
86 |
--------------------------------------------------------------------------------
/jsdoc.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "opts": {
3 | "destination": "./docs_out/",
4 | "encoding": "utf8",
5 | "readme": "docs/introduction.md",
6 | "template": "node_modules/lance-docs-template",
7 | "tutorials": "./docs"
8 | },
9 | "sourceType": "module",
10 | "source": {
11 | "include": [
12 | "src/ServerEngine.js",
13 | "src/ClientEngine.js",
14 | "src/GameEngine.js",
15 | "src/GameWorld.js",
16 | "src/controls/KeyboardControls.js",
17 | "src/physics/SimplePhysicsEngine.js",
18 | "src/physics/CannonPhysicsEngine.js",
19 | "src/serialize/BaseTypes.js",
20 | "src/serialize/GameObject.js",
21 | "src/serialize/DynamicObject.js",
22 | "src/serialize/Serializer.js",
23 | "src/serialize/Serializable.js",
24 | "src/serialize/PhysicalObject2D.js",
25 | "src/serialize/PhysicalObject3D.js",
26 | "src/serialize/TwoVector.js",
27 | "src/serialize/ThreeVector.js",
28 | "src/serialize/Quaternion.js",
29 | "src/render/Renderer.js",
30 | "src/render/AFrameRenderer.js",
31 | "src/lib/Trace.js"
32 | ]
33 | },
34 | "plugins": [
35 | "plugins/markdown"
36 | ],
37 | "templates": {
38 | "systemName" : "Lance",
39 | "copyright" : "Lance Copyright © 2017 The contributors to the Lance project.",
40 | "includeDate" : true,
41 | "navType" : "vertical",
42 | "theme" : "spacelab",
43 | "linenums" : false,
44 | "collapseSymbols" : true,
45 | "inverseNav" : false,
46 | "outputSourceFiles" : false,
47 | "outputSourcePath" : false,
48 | "dateFormat" : "ddd MMM Do YYYY",
49 | "syntaxTheme" : "default",
50 | "sort" : false,
51 | "search" : true
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lance-gg",
3 | "version": "5.0.2",
4 | "description": "A Node.js based real-time multiplayer game server",
5 | "type": "module",
6 | "main": "dist/server/lance-gg.js",
7 | "browser": "dist/client/lance-gg.js",
8 | "types": "dist/types/lance-gg.d.ts",
9 | "scripts": {
10 | "docs": "jsdoc -c jsdoc.conf.json",
11 | "build": "rollup --config rollup.config.js",
12 | "test-all": "mocha test/EndToEnd/",
13 | "test-serializer": "mocha ./test/serializer/",
14 | "test": "mocha ./test/serializer/"
15 | },
16 | "keywords": [
17 | "pixijs",
18 | "pixi",
19 | "socket",
20 | "canvas",
21 | "WebGL",
22 | "HTML5",
23 | "AFrame",
24 | "physics",
25 | "engine",
26 | "game",
27 | "realtime",
28 | "multiplayer"
29 | ],
30 | "repository": {
31 | "type": "git",
32 | "url": "git://github.com/lance-gg/lance.git"
33 | },
34 | "dependencies": {
35 | "@types/cannon": "^0.1.12",
36 | "@types/event-emitter": "^0.3.5",
37 | "@types/p2": "^0.7.44",
38 | "@types/three": "^0.163.0",
39 | "bufferutil": "^4.0.1",
40 | "cannon": "^0.6.2",
41 | "event-emitter": "^0.3.5",
42 | "jsdoc": "^4.0.2",
43 | "mkdirp": "^0.5.1",
44 | "p2": "^0.7.1",
45 | "socket.io": "^4.7.5",
46 | "socket.io-client": "^4.7.5",
47 | "tslib": "^2.6.2",
48 | "typescript": "next",
49 | "utf-8-validate": "^5.0.2"
50 | },
51 | "files": [
52 | "src",
53 | "dist"
54 | ],
55 | "author": "Opher Vishnia",
56 | "contributors": [
57 | {
58 | "name": "Opher Vishnia"
59 | },
60 | {
61 | "name": "Gary Weiss"
62 | }
63 | ],
64 | "license": "Apache-2.0",
65 | "bugs": {
66 | "url": "https://github.com/lance-gg/lance/issues"
67 | },
68 | "homepage": "https://github.com/lance-gg/lance#readme",
69 | "devDependencies": {
70 | "@rollup/plugin-commonjs": "^25.0.7",
71 | "@rollup/plugin-json": "^6.1.0",
72 | "@rollup/plugin-node-resolve": "^15.2.3",
73 | "@rollup/plugin-typescript": "^11.1.6",
74 | "chai": "^3.5.0",
75 | "eslint": "^8.28.0",
76 | "eslint-config-google": "^0.6.0",
77 | "express": "^4.14.0",
78 | "lance-docs-template": "github:lance-gg/lance-docs-template#semver:^1.0.1",
79 | "mocha": "^10.1.0",
80 | "query-string": "^4.3.1",
81 | "rollup": "^4.14.2",
82 | "rollup-plugin-dts": "^6.1.0",
83 | "should": "^11.0.0"
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import typescript from '@rollup/plugin-typescript';
2 | import pjson from '@rollup/plugin-json';
3 | import commonjs from '@rollup/plugin-commonjs';
4 | import resolve from '@rollup/plugin-node-resolve';
5 | import dts from 'rollup-plugin-dts';
6 |
7 | export default [
8 | {
9 | // server build non-transpiled with es2015 modules
10 | input: 'src/package/serverExports.ts',
11 | external: ['fs', 'bufferutil', 'utf-8-validate'],
12 | output: { file: 'dist/server/lance-gg.js', format: 'esm', name: 'Server' },
13 | plugins: [resolve(), typescript(), pjson(), commonjs({ include: 'node_modules/**' })]
14 | },
15 | {
16 | // client build non-transpiled with es2015 modules
17 | input: 'src/package/clientExports.ts',
18 | output: { file: 'dist/client/lance-gg.js', format: 'esm', name: 'Client' },
19 | plugins: [resolve({ browser: true, preferBuiltins: false }), typescript(), pjson(), commonjs({ include: 'node_modules/**' })]
20 | },
21 | {
22 | // types only
23 | input: ['src/package/allExports.ts'],
24 | output: { file: "dist/types/lance-gg.d.ts", format: "es" },
25 | plugins: [dts()]
26 | }
27 | ]
28 |
--------------------------------------------------------------------------------
/src/GameWorld.ts:
--------------------------------------------------------------------------------
1 | import { GameObject } from "./serialize/GameObject.js";
2 |
3 | interface ObjectQuery {
4 | id?: number;
5 | playerId?: number;
6 | instanceType?: typeof GameObject;
7 | components?: string[];
8 | returnSingle?: boolean;
9 | }
10 |
11 | /**
12 | * This class implements a singleton game world instance, created by Lance.
13 | * It represents an instance of the game world, and includes all the game objects.
14 | * It is the state of the game.
15 | */
16 | class GameWorld {
17 |
18 | public objects: { [key: number]: GameObject }
19 | public stepCount: number;
20 | public playerCount: number;
21 | public idCount: number;
22 |
23 | /**
24 | * Constructor of the World instance. Invoked by Lance on startup.
25 | *
26 | * @hideconstructor
27 | */
28 | constructor() {
29 | this.stepCount = 0;
30 | this.objects = {};
31 | this.playerCount = 0;
32 | this.idCount = 0;
33 | }
34 |
35 | /**
36 | * Gets a new, fresh and unused id that can be used for a new object
37 | * @private
38 | * @return {Number} the new id
39 | */
40 | getNewId(): number {
41 | let possibleId = this.idCount;
42 |
43 | // find a free id
44 | while (possibleId in this.objects)
45 | possibleId++;
46 |
47 | this.idCount = possibleId + 1;
48 | return possibleId;
49 | }
50 |
51 | queryOneObject(query: ObjectQuery): GameObject | null {
52 | let objs = this.queryObjects(query);
53 | return objs.length > 0 ? objs[0] : null;
54 | }
55 |
56 | /**
57 | * Returns all the game world objects which match a criteria
58 | * @param {Object} query The query object
59 | * @param {Object} [query.id] object id
60 | * @param {Object} [query.playerId] player id
61 | * @param {Class} [query.instanceType] matches whether `object instanceof instanceType`
62 | * @param {Array} [query.components] An array of component names
63 | * @return {Array} All game objects which match all the query parameters
64 | */
65 | queryObjects(query: ObjectQuery): GameObject[] {
66 | let queriedObjects: GameObject[] = [];
67 |
68 | // todo this is currently a somewhat inefficient implementation for API testing purposes.
69 | // It should be implemented with cached dictionaries like in nano-ecs
70 | this.forEachObject((id: number, object: GameObject) => {
71 | let conditions: boolean[] = [];
72 |
73 | // object id condition
74 | conditions.push(!('id' in query) || query.id !== null && object.id === query.id);
75 |
76 | // player id condition
77 | conditions.push(!('playerId' in query) || query.playerId !== null && object.playerId === query.playerId);
78 |
79 | // instance type conditio
80 | conditions.push(!('instanceType' in query) || query.instanceType !== null && object instanceof query.instanceType);
81 |
82 | // components conditions
83 | if ('components' in query) {
84 | query.components.forEach(componentClass => {
85 | conditions.push(object.hasComponent(componentClass));
86 | });
87 | }
88 |
89 | // all conditions are true, object is qualified for the query
90 | if (conditions.every(value => value)) {
91 | queriedObjects.push(object);
92 | if (query.returnSingle) return false;
93 | }
94 | });
95 |
96 | return queriedObjects;
97 | }
98 |
99 | /**
100 | * Returns The first game object encountered which matches a criteria.
101 | * Syntactic sugar for {@link queryObjects} with `returnSingle: true`
102 | * @param {Object} query See queryObjects
103 | * @return {Object} The game object, if found
104 | */
105 | queryObject(query) {
106 | return this.queryObjects(Object.assign(query, {
107 | returnSingle: true
108 | }));
109 | }
110 |
111 | /**
112 | * Add an object to the game world
113 | * @private
114 | * @param {Object} object object to add
115 | */
116 | addObject(object) {
117 | this.objects[object.id] = object;
118 | }
119 |
120 | /**
121 | * Remove an object from the game world
122 | * @private
123 | * @param {number} id id of the object to remove
124 | */
125 | removeObject(id) {
126 | delete this.objects[id];
127 | }
128 |
129 | /**
130 | * World object iterator.
131 | * Invoke callback(objId, obj) for each object
132 | *
133 | * @param {function} callback function receives id and object. If callback returns false, the iteration will cease
134 | */
135 | forEachObject(callback) {
136 | for (let id of Object.keys(this.objects)) {
137 | let returnValue = callback(id, this.objects[id]); // TODO: the key should be Number(id)
138 | if (returnValue === false) break;
139 | }
140 | }
141 |
142 | }
143 |
144 | export { GameWorld, ObjectQuery }
145 |
--------------------------------------------------------------------------------
/src/Synchronizer.ts:
--------------------------------------------------------------------------------
1 | import { ClientEngine } from './ClientEngine.js';
2 | import { SyncStrategy, SyncStrategyOptions } from './syncStrategies/SyncStrategy.js';
3 |
4 | class Synchronizer {
5 |
6 | public syncStrategy: SyncStrategy;
7 | private clientEngine: ClientEngine;
8 |
9 | // create a synchronizer instance
10 | constructor(clientEngine: ClientEngine, syncStrategy: SyncStrategy) {
11 | this.clientEngine = clientEngine;
12 | this.syncStrategy = syncStrategy;
13 | }
14 | }
15 |
16 | export { Synchronizer}
--------------------------------------------------------------------------------
/src/controls/KeyboardControls.ts:
--------------------------------------------------------------------------------
1 | import { ClientEngine } from '../ClientEngine.js'
2 | import { GameEngine } from '../GameEngine.js';
3 |
4 | type KeyOptions = {
5 | repeat: boolean
6 | }
7 |
8 | type KeyAction = {
9 | actionName: string,
10 | options: KeyOptions,
11 | parameters: any
12 | }
13 |
14 | type KeyState = {
15 | isDown: boolean,
16 | count: number
17 | }
18 |
19 | /**
20 | * This class allows easy usage of device keyboard controls. Use the method {@link KeyboardControls#bindKey} to
21 | * generate events whenever a key is pressed.
22 | *
23 | * @example
24 | * // in the ClientEngine constructor
25 | * this.controls = new KeyboardControls(this);
26 | * this.controls.bindKey('left', 'left', { repeat: true } );
27 | * this.controls.bindKey('right', 'right', { repeat: true } );
28 | * this.controls.bindKey('space', 'space');
29 | *
30 | */
31 | class KeyboardControls {
32 |
33 | private clientEngine: ClientEngine;
34 | private gameEngine: GameEngine;
35 | private boundKeys: { [key: string]: KeyAction };
36 | private keyStates: { [key: string]: KeyState };
37 | private lastKeyPressed: string | null;
38 |
39 | constructor(clientEngine: ClientEngine) {
40 |
41 | this.clientEngine = clientEngine;
42 | this.gameEngine = clientEngine.gameEngine;
43 |
44 | this.setupListeners();
45 |
46 | // keep a reference for key press state
47 | this.keyStates = {};
48 |
49 | // a list of bound keys and their corresponding actions
50 | this.boundKeys = {};
51 |
52 | this.gameEngine.on('client__preStep', () => {
53 | for (let keyName of Object.keys(this.boundKeys)) {
54 | if (this.keyStates[keyName] && this.keyStates[keyName].isDown) {
55 |
56 | // handle repeat press
57 | if (this.boundKeys[keyName].options.repeat || this.keyStates[keyName].count == 0) {
58 |
59 | // callback to get live parameters if function
60 | let parameters = this.boundKeys[keyName].parameters;
61 | if (typeof parameters === "function") {
62 | parameters = parameters();
63 | }
64 |
65 | // todo movement is probably redundant
66 | let inputOptions = Object.assign({
67 | movement: true
68 | }, parameters || {});
69 | this.clientEngine.sendInput(this.boundKeys[keyName].actionName, inputOptions);
70 | this.keyStates[keyName].count++;
71 | }
72 | }
73 | }
74 | });
75 | }
76 |
77 | setupListeners() {
78 | document.addEventListener('keydown', (e) => { this.onKeyChange(e, true);});
79 | document.addEventListener('keyup', (e) => { this.onKeyChange(e, false);});
80 | }
81 |
82 | /**
83 | * Bind a keyboard key to a Lance client event. Each time the key is pressed,
84 | * an event will be transmitted by the client engine, using {@link ClientEngine#sendInput},
85 | * and the specified event name.
86 | *
87 | * Common key names: up, down, left, right, enter, shift, ctrl, alt,
88 | * escape, space, page up, page down, end, home, 0..9, a..z, A..Z.
89 | * For a full list, please check the source link above.
90 | *
91 | * @param {String} keys - keyboard key (or array of keys) which will cause the event.
92 | * @param {String} actionName - the event name
93 | * @param {Object} options - options object
94 | * @param {Boolean} options.repeat - if set to true, an event continues to be sent on each game step, while the key is pressed
95 | * @param {Object/Function} parameters - parameters (or function to get parameters) to be sent to
96 | * the server with sendInput as the inputOptions
97 | */
98 | bindKey(keys: string | string[], actionName: string, options?: KeyOptions, parameters?: any) {
99 | if (!Array.isArray(keys)) keys = [keys];
100 |
101 | let keyOptions = Object.assign({
102 | repeat: false
103 | }, options);
104 |
105 | keys.forEach(keyName => {
106 | this.boundKeys[keyName] = { actionName, options: keyOptions, parameters: parameters };
107 | });
108 | }
109 |
110 | // todo implement unbindKey
111 | onKeyChange(e: KeyboardEvent, isDown: boolean) {
112 | if (e.code && this.boundKeys[e.code]) {
113 | if (this.keyStates[e.code] == null) {
114 | this.keyStates[e.code] = { isDown: false, count: 0 };
115 | }
116 | this.keyStates[e.code].isDown = isDown;
117 |
118 | // key up, reset press count
119 | if (!isDown) this.keyStates[e.code].count = 0;
120 |
121 | // keep reference to the last key pressed to avoid duplicates
122 | this.lastKeyPressed = isDown ? e.code : null;
123 |
124 | e.preventDefault();
125 | }
126 | }
127 | }
128 |
129 | export { KeyboardControls };
130 |
--------------------------------------------------------------------------------
/src/game/Timer.ts:
--------------------------------------------------------------------------------
1 |
2 | type TimerCallback = (args: any) => void;
3 |
4 | // TODO: needs documentation
5 | // I think the API could be simpler
6 | // - Timer.run(waitSteps, cb)
7 | // - Timer.repeat(waitSteps, count, cb) // count=null=>forever
8 | // - Timer.cancel(cb)
9 | class Timer {
10 | public currentTime: number;
11 | public idCounter: number;
12 | private isActive: boolean;
13 | private events: { [key: number]: TimerEvent }
14 |
15 | constructor() {
16 | this.currentTime = 0;
17 | this.isActive = false;
18 | this.idCounter = 0;
19 | this.events = {};
20 | }
21 |
22 | play() {
23 | this.isActive = true;
24 | }
25 |
26 | tick() {
27 | let event;
28 | let eventId;
29 |
30 | if (this.isActive) {
31 | this.currentTime++;
32 |
33 | for (eventId in this.events) {
34 | event = this.events[eventId];
35 | if (event) {
36 |
37 | if (event.type == 'repeat') {
38 | if ((this.currentTime - event.startOffset) % event.time == 0) {
39 | event.callback.apply(event.thisContext, event.args);
40 | }
41 | }
42 | if (event.type == 'single') {
43 | if ((this.currentTime - event.startOffset) % event.time == 0) {
44 | event.callback.apply(event.thisContext, event.args);
45 | event.destroy();
46 | }
47 | }
48 | }
49 | }
50 | }
51 | }
52 |
53 | destroyEvent(eventId: number) {
54 | delete this.events[eventId];
55 | }
56 |
57 | loop(time: number, callback: TimerCallback) {
58 | let timerEvent = new TimerEvent(this,
59 | 'repeat',
60 | time,
61 | callback
62 | );
63 |
64 | this.events[timerEvent.id] = timerEvent;
65 |
66 | return timerEvent;
67 | }
68 |
69 | add(time: number, callback: TimerCallback, thisContext: any, args: any): TimerEvent {
70 | let timerEvent = new TimerEvent(this,
71 | 'single',
72 | time,
73 | callback,
74 | thisContext,
75 | args
76 | );
77 |
78 | this.events[timerEvent.id] = timerEvent;
79 | return timerEvent;
80 | }
81 |
82 | // todo implement timer delete all events
83 |
84 | destroy(id: number) {
85 | delete this.events[id];
86 | }
87 | }
88 |
89 | // timer event
90 | class TimerEvent {
91 | public id: number;
92 | private timer: Timer;
93 | private type: TimerEventType;
94 | private time: number;
95 | private callback: (args: any) => void;
96 | private startOffset: number;
97 | private thisContext: any;
98 | private args: any;
99 | public destroy: () => void;
100 |
101 | constructor(timer: Timer, type: TimerEventType, time: number, callback, thisContext: any = null, args: any = null) {
102 | this.id = ++timer.idCounter;
103 | this.timer = timer;
104 | this.type = type;
105 | this.time = time;
106 | this.callback = callback;
107 | this.startOffset = timer.currentTime;
108 | this.thisContext = thisContext;
109 | this.args = args;
110 |
111 | this.destroy = function() {
112 | this.timer.destroy(this.id);
113 | };
114 | }
115 | }
116 |
117 |
118 | type TimerEventType = 'repeat' | 'single';
119 |
120 | export { Timer, TimerCallback }
121 |
--------------------------------------------------------------------------------
/src/lib/MathUtils.ts:
--------------------------------------------------------------------------------
1 | class MathUtils {
2 |
3 | // interpolate from start to end, advancing "percent" of the way
4 | static interpolate(start: number, end: number, percent: number): number {
5 | return (end - start) * percent + start;
6 | }
7 |
8 | // interpolate from start to end, advancing "percent" of the way
9 | //
10 | // returns just the delta. i.e. the value that must be added to the start value
11 | static interpolateDelta(start: number, end: number, percent: number): number {
12 | return (end - start) * percent;
13 | }
14 |
15 | // interpolate from start to end, advancing "percent" of the way
16 | // and noting that the dimension wraps around {x >= wrapMin, x < wrapMax}
17 | //
18 | // returns just the delta. i.e. the value that must be added to the start value
19 | static interpolateDeltaWithWrapping(start: number, end: number, percent: number, wrapMin: number, wrapMax: number): number {
20 | let wrapTest = wrapMax - wrapMin;
21 | if (start - end > wrapTest / 2) end += wrapTest;
22 | else if (end - start > wrapTest / 2) start += wrapTest;
23 | if (Math.abs(start - end) > wrapTest / 3) {
24 | console.log('wrap interpolation is close to limit. Not sure which edge to wrap to.');
25 | }
26 | return (end - start) * percent;
27 | }
28 |
29 | static interpolateWithWrapping(start: number, end: number, percent: number, wrapMin: number, wrapMax: number): number {
30 | let interpolatedVal = start + this.interpolateDeltaWithWrapping(start, end, percent, wrapMin, wrapMax);
31 | let wrapLength = wrapMax - wrapMin;
32 | if (interpolatedVal >= wrapLength) interpolatedVal -= wrapLength;
33 | if (interpolatedVal < 0) interpolatedVal += wrapLength;
34 | return interpolatedVal;
35 | }
36 | }
37 |
38 | export { MathUtils }
--------------------------------------------------------------------------------
/src/lib/Scheduler.ts:
--------------------------------------------------------------------------------
1 | import EventEmitter from 'event-emitter';
2 |
3 | const SIXTY_PER_SEC = 1000 / 60;
4 | const LOOP_SLOW_THRESH = 0.3;
5 | const LOOP_SLOW_COUNT = 10;
6 |
7 | type SchedulerOptions = {
8 | tick: () => void,
9 | period: number,
10 | delay: number
11 | }
12 |
13 | /**
14 | * Scheduler class
15 | *
16 | */
17 | class Scheduler {
18 |
19 | private options: SchedulerOptions;
20 | private nextExecTime: number;
21 | private requestedDelay: number;
22 | private delayCounter: number;
23 | public emit: (event: string, arg?: any) => void;
24 | public on: (event: string, handler: any) => void;
25 | public once: (event: string, handler: any) => void;
26 |
27 | /**
28 | * schedule a function to be called
29 | *
30 | * @param {Object} options the options
31 | * @param {Function} options.tick the function to be called
32 | * @param {Number} options.period number of milliseconds between each invocation, not including the function's execution time
33 | * @param {Number} options.delay number of milliseconds to add when delaying or hurrying the execution
34 | */
35 | constructor(options: SchedulerOptions) {
36 | this.options = Object.assign({
37 | tick: null,
38 | period: SIXTY_PER_SEC,
39 | delay: SIXTY_PER_SEC / 3
40 | }, options);
41 | this.nextExecTime = 0;
42 | this.requestedDelay = 0;
43 | this.delayCounter = 0;
44 |
45 | // mixin for EventEmitter
46 | let eventEmitter = EventEmitter();
47 | this.on = eventEmitter.on;
48 | this.once = eventEmitter.once;
49 | this.emit = eventEmitter.emit;
50 | }
51 |
52 | // in same cases, setTimeout is ignored by the browser,
53 | // this is known to happen during the first 100ms of a touch event
54 | // on android chrome. Double-check the game loop using requestAnimationFrame
55 | nextTickChecker() {
56 | let currentTime = (new Date()).getTime();
57 | if (currentTime > this.nextExecTime) {
58 | this.delayCounter++;
59 | this.callTick();
60 | this.nextExecTime = currentTime + this.options.period;
61 | }
62 | window.requestAnimationFrame(this.nextTickChecker.bind(this));
63 | }
64 |
65 | nextTick() {
66 | let stepStartTime = (new Date()).getTime();
67 | if (stepStartTime > this.nextExecTime + this.options.period * LOOP_SLOW_THRESH) {
68 | this.delayCounter++;
69 | } else
70 | this.delayCounter = 0;
71 |
72 | this.callTick();
73 | this.nextExecTime = stepStartTime + this.options.period + this.requestedDelay;
74 | this.requestedDelay = 0;
75 | setTimeout(this.nextTick.bind(this), this.nextExecTime - (new Date()).getTime());
76 | }
77 |
78 | callTick() {
79 | if (this.delayCounter >= LOOP_SLOW_COUNT) {
80 | this.emit('loopRunningSlow');
81 | this.delayCounter = 0;
82 | }
83 | this.options.tick();
84 | }
85 |
86 | /**
87 | * start the schedule
88 | * @return {Scheduler} returns this scheduler instance
89 | */
90 | start() {
91 | setTimeout(this.nextTick.bind(this));
92 | if (typeof window === 'object' && typeof window.requestAnimationFrame === 'function')
93 | window.requestAnimationFrame(this.nextTickChecker.bind(this));
94 | return this;
95 | }
96 |
97 | /**
98 | * delay next execution
99 | */
100 | delayTick() {
101 | this.requestedDelay += this.options.delay;
102 | }
103 |
104 | /**
105 | * hurry the next execution
106 | */
107 | hurryTick() {
108 | this.requestedDelay -= this.options.delay;
109 | }
110 | }
111 |
112 | export { Scheduler, SchedulerOptions }
--------------------------------------------------------------------------------
/src/lib/Trace.ts:
--------------------------------------------------------------------------------
1 | type TraceOptions = {
2 | traceLevel: number
3 | }
4 |
5 | type StepDesc = 'initializing' | number;
6 |
7 | type TraceEntry = {
8 | data: string,
9 | level: number,
10 | step: StepDesc,
11 | time: Date
12 | }
13 |
14 | type TraceDataCollector = () => string;
15 |
16 | /**
17 | * Tracing Services.
18 | * Use the trace functions to trace game state. Turn on tracing by
19 | * specifying the minimum trace level which should be recorded. For
20 | * example, setting traceLevel to Trace.TRACE_INFO will cause info,
21 | * warn, and error traces to be recorded.
22 | */
23 | class Trace {
24 | private options: TraceOptions;
25 | private traceBuffer: TraceEntry[];
26 | private step: StepDesc;
27 | public error: (tdc: TraceDataCollector) => void;
28 | public warn: (tdc: TraceDataCollector) => void;
29 | public info: (tdc: TraceDataCollector) => void;
30 | public debug: (tdc: TraceDataCollector) => void;
31 |
32 | constructor(options: TraceOptions) {
33 |
34 | this.options = Object.assign({
35 | traceLevel: Trace.TRACE_DEBUG
36 | }, options);
37 |
38 | this.traceBuffer = [];
39 | this.step = 'initializing';
40 |
41 | // syntactic sugar functions
42 | this.error = this.trace.bind(this, Trace.TRACE_ERROR);
43 | this.warn = this.trace.bind(this, Trace.TRACE_WARN);
44 | this.info = this.trace.bind(this, Trace.TRACE_INFO);
45 | this.debug = this.trace.bind(this, Trace.TRACE_DEBUG);
46 | this.trace = this.trace.bind(this, Trace.TRACE_ALL);
47 | }
48 |
49 | /**
50 | * Include all trace levels.
51 | * @memberof Trace
52 | * @member {Number} TRACE_ALL
53 | */
54 | static get TRACE_ALL() { return 0; }
55 |
56 | /**
57 | * Include debug traces and higher.
58 | * @memberof Trace
59 | * @member {Number} TRACE_DEBUG
60 | */
61 | static get TRACE_DEBUG() { return 1; }
62 |
63 | /**
64 | * Include info traces and higher.
65 | * @memberof Trace
66 | * @member {Number} TRACE_INFO
67 | */
68 | static get TRACE_INFO() { return 2; }
69 |
70 | /**
71 | * Include warn traces and higher.
72 | * @memberof Trace
73 | * @member {Number} TRACE_WARN
74 | */
75 | static get TRACE_WARN() { return 3; }
76 |
77 | /**
78 | * Include error traces and higher.
79 | * @memberof Trace
80 | * @member {Number} TRACE_ERROR
81 | */
82 | static get TRACE_ERROR() { return 4; }
83 |
84 | /**
85 | * Disable all tracing.
86 | * @memberof Trace
87 | * @member {Number} TRACE_NONE
88 | */
89 | static get TRACE_NONE() { return 1000; }
90 |
91 | trace(level: number, dataCB: TraceDataCollector) {
92 |
93 | if (level < this.options.traceLevel)
94 | return;
95 |
96 | this.traceBuffer.push({ data: dataCB(), level, step: this.step, time: new Date() });
97 | }
98 |
99 | rotate() {
100 | let buffer = this.traceBuffer;
101 | this.traceBuffer = [];
102 | return buffer;
103 | }
104 |
105 | get length(): number {
106 | return this.traceBuffer.length;
107 | }
108 |
109 | setStep(s: StepDesc) {
110 | this.step = s;
111 | }
112 | }
113 |
114 | export default Trace;
115 |
--------------------------------------------------------------------------------
/src/lib/Utils.ts:
--------------------------------------------------------------------------------
1 | export default class Utils {
2 |
3 | static hashStr(str: string, bits = 8): number {
4 | let hash = 5381;
5 | let i = str.length;
6 |
7 | while (i) {
8 | hash = (hash * 33) ^ str.charCodeAt(--i);
9 | }
10 | hash = hash >>> 0;
11 | hash = hash % (Math.pow(2, bits) - 1);
12 |
13 | // JavaScript does bitwise operations (like XOR, above) on 32-bit signed
14 | // integers. Since we want the results to be always positive, convert the
15 | // signed int to an unsigned by doing an unsigned bitshift. */
16 | return hash;
17 | }
18 |
19 | static arrayBuffersEqual(buf1: ArrayBuffer, buf2: ArrayBuffer): boolean {
20 | if (buf1.byteLength !== buf2.byteLength) return false;
21 | let dv1 = new Int8Array(buf1);
22 | let dv2 = new Int8Array(buf2);
23 | for (let i = 0; i !== buf1.byteLength; i++) {
24 | if (dv1[i] !== dv2[i]) return false;
25 | }
26 | return true;
27 | }
28 |
29 | static httpGetPromise(url: string): Promise {
30 | return new Promise((resolve, reject) => {
31 | let req = new XMLHttpRequest();
32 | req.open('GET', url, true);
33 | req.onload = () => {
34 | if (req.status >= 200 && req.status < 400) resolve(JSON.parse(req.responseText));
35 | else reject();
36 | };
37 | req.onerror = () => {};
38 | req.send();
39 | });
40 | }
41 | }
--------------------------------------------------------------------------------
/src/lib/lib.ts:
--------------------------------------------------------------------------------
1 | import Trace from './Trace.js';
2 |
3 | export default {
4 | Trace
5 | };
6 |
--------------------------------------------------------------------------------
/src/network/NetworkMonitor.ts:
--------------------------------------------------------------------------------
1 | import EventEmitter from 'event-emitter';
2 | import http from 'http';
3 | import { ClientEngine } from '../ClientEngine.js';
4 | import { Socket } from 'socket.io-client';
5 |
6 | /**
7 | * Measures network performance between the client and the server
8 | * Represents both the client and server portions of NetworkMonitor
9 | */
10 | export default class NetworkMonitor {
11 |
12 | public emit: (event: string, arg?: any) => void;
13 | public on: (event: string, handler: any) => void;
14 | public once: (event: string, handler: any) => void;
15 | private queryIdCounter: number;
16 | private RTTQueries: { [key: number]: number};
17 | private movingRTTAverage: number;
18 | private movingRTTAverageFrame: number[];
19 | private movingFPSAverageSize: number;
20 | private clientEngine: ClientEngine;
21 |
22 | constructor() {
23 |
24 | // mixin for EventEmitter
25 | let eventEmitter = EventEmitter();
26 | this.on = eventEmitter.on;
27 | this.once = eventEmitter.once;
28 | this.emit = eventEmitter.emit;
29 | }
30 |
31 | // client
32 | registerClient(clientEngine: ClientEngine) {
33 | this.queryIdCounter = 0;
34 | this.RTTQueries = {};
35 |
36 | this.movingRTTAverage = 0;
37 | this.movingRTTAverageFrame = [];
38 | this.movingFPSAverageSize = clientEngine.options.healthCheckRTTSample;
39 | this.clientEngine = clientEngine;
40 | clientEngine.socket.on('RTTResponse', this.onReceivedRTTQuery.bind(this));
41 | setInterval(this.sendRTTQuery.bind(this), clientEngine.options.healthCheckInterval);
42 | }
43 |
44 | sendRTTQuery() {
45 | // todo implement cleanup of older timestamp
46 | this.RTTQueries[this.queryIdCounter] = new Date().getTime();
47 | this.clientEngine.socket.emit('RTTQuery', this.queryIdCounter);
48 | this.queryIdCounter++;
49 | }
50 |
51 | onReceivedRTTQuery(queryId: number) {
52 | let RTT = (new Date().getTime()) - this.RTTQueries[queryId];
53 |
54 | this.movingRTTAverageFrame.push(RTT);
55 | if (this.movingRTTAverageFrame.length > this.movingFPSAverageSize) {
56 | this.movingRTTAverageFrame.shift();
57 | }
58 | this.movingRTTAverage = this.movingRTTAverageFrame.reduce((a, b) => a + b) / this.movingRTTAverageFrame.length;
59 | this.emit('RTTUpdate', {
60 | RTT: RTT,
61 | RTTAverage: this.movingRTTAverage
62 | });
63 | }
64 |
65 | // server
66 | registerPlayerOnServer(socket: Socket) {
67 | socket.on('RTTQuery', this.respondToRTTQuery.bind(this, socket));
68 | }
69 |
70 | respondToRTTQuery(socket: Socket, queryId: number) {
71 | socket.emit('RTTResponse', queryId);
72 | }
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/src/network/NetworkTransmitter.ts:
--------------------------------------------------------------------------------
1 | import BaseTypes from '../serialize/BaseTypes.js';
2 | import NetworkedEventCollection from './NetworkedEventCollection.js';
3 | import Serializer from '../serialize/Serializer.js';
4 | import Serializable from '../serialize/Serializable.js';
5 |
6 |
7 | class ObjectUpdate extends Serializable {
8 | stepCount: number;
9 | objectInstance: Serializable;
10 |
11 | netScheme() {
12 | return {
13 | stepCount: { type: BaseTypes.Int32 },
14 | objectInstance: { type: BaseTypes.ClassInstance }
15 | };
16 | }
17 | constructor(stepCount: number, objectInstance: Serializable) {
18 | super();
19 | this.stepCount = stepCount;
20 | this.objectInstance = objectInstance;
21 | }
22 | }
23 |
24 | class ObjectCreate extends Serializable {
25 | stepCount: number;
26 | objectInstance: Serializable;
27 |
28 | netScheme() {
29 | return {
30 | stepCount: { type: BaseTypes.Int32 },
31 | objectInstance: { type: BaseTypes.ClassInstance }
32 | }
33 | }
34 | constructor(stepCount: number, objectInstance: Serializable) {
35 | super();
36 | this.stepCount = stepCount;
37 | this.objectInstance = objectInstance;
38 | }
39 | }
40 |
41 | class ObjectDestroy extends Serializable {
42 | stepCount: number;
43 | objectInstance: Serializable;
44 |
45 | netScheme() {
46 | return {
47 | stepCount: { type: BaseTypes.Int32 },
48 | objectInstance: { type: BaseTypes.ClassInstance }
49 | }
50 | }
51 | constructor(stepCount: number, objectInstance: Serializable) {
52 | super();
53 | this.stepCount = stepCount;
54 | this.objectInstance = objectInstance;
55 | }
56 | }
57 |
58 | class SyncHeader extends Serializable {
59 | stepCount: number;
60 | fullUpdate: number;
61 | netScheme() {
62 | return {
63 | stepCount: { type: BaseTypes.Int32 },
64 | fullUpdate: { type: BaseTypes.Int8 }
65 | }
66 | }
67 | constructor(stepCount: number, fullUpdate: number) {
68 | super();
69 | this.stepCount = stepCount;
70 | this.fullUpdate = fullUpdate;
71 | }
72 | }
73 |
74 | export default class NetworkTransmitter {
75 |
76 | private serializer: Serializer;
77 | private networkedEventCollection: NetworkedEventCollection;
78 |
79 |
80 | constructor(serializer: Serializer) {
81 | this.serializer = serializer;
82 | this.serializer.registerClass(NetworkedEventCollection);
83 | this.serializer.registerClass(ObjectUpdate);
84 | this.serializer.registerClass(ObjectCreate);
85 | this.serializer.registerClass(ObjectDestroy);
86 | this.serializer.registerClass(SyncHeader);
87 | this.networkedEventCollection = new NetworkedEventCollection([]);
88 | }
89 |
90 | sendUpdate(stepCount: number, obj: Serializable) {
91 | this.networkedEventCollection.events.push(new ObjectUpdate(stepCount, obj));
92 | }
93 |
94 | sendCreate(stepCount: number, obj: Serializable) {
95 | this.networkedEventCollection.events.push(new ObjectCreate(stepCount, obj));
96 | }
97 |
98 | sendDestroy(stepCount: number, obj: Serializable) {
99 | this.networkedEventCollection.events.push(new ObjectDestroy(stepCount, obj));
100 | }
101 |
102 | syncHeader(stepCount: number, fullUpdate: number) {
103 | this.networkedEventCollection.events.push(new SyncHeader(stepCount, fullUpdate));
104 | }
105 |
106 |
107 | serializePayload() {
108 | if (this.networkedEventCollection.events.length === 0)
109 | return null;
110 |
111 | let dataBuffer = this.networkedEventCollection.serialize(this.serializer, { dry: false, dataBuffer: null, bufferOffset: 0 });
112 |
113 | return dataBuffer;
114 | }
115 |
116 | deserializePayload(payload) {
117 | return this.serializer.deserialize(payload.dataBuffer, 0).obj;
118 | }
119 |
120 | clearPayload() {
121 | this.networkedEventCollection.events = [];
122 | }
123 |
124 | // TODO: there must be a better way than this
125 | static getNetworkEvent(event: Serializable): string {
126 | if (event instanceof ObjectUpdate) return 'objectUpdate';
127 | else if (event instanceof ObjectCreate) return 'objectCreate';
128 | else if (event instanceof ObjectDestroy) return 'objectDestroy';
129 | else if (event instanceof SyncHeader) return 'syncHeader';
130 | return 'unknown'; // raise an error here
131 | }
132 |
133 | }
134 |
--------------------------------------------------------------------------------
/src/network/NetworkedEventCollection.ts:
--------------------------------------------------------------------------------
1 | import BaseTypes from '../serialize/BaseTypes.js';
2 | import Serializable from '../serialize/Serializable.js';
3 |
4 | /**
5 | * Defines a collection of NetworkEvents to be transmitted over the wire
6 | */
7 | export default class NetworkedEventCollection extends Serializable {
8 | public events: Serializable[];
9 |
10 | netScheme() {
11 | return {
12 | events: {
13 | type: BaseTypes.List,
14 | itemType: BaseTypes.ClassInstance
15 | },
16 | };
17 | }
18 |
19 | constructor(events: Serializable[]) {
20 | super();
21 | this.events = events;
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/src/package/allExports.ts:
--------------------------------------------------------------------------------
1 | import { GameEngine, GameEngineOptions, InputDesc, PreStepDesc } from '../GameEngine.js';
2 | import { GameWorld } from '../GameWorld.js';
3 | import { P2PhysicsEngine } from '../physics/P2PhysicsEngine.js';
4 | import { SimplePhysicsEngine, SimplePhysicsEngineOptions } from '../physics/SimplePhysicsEngine.js';
5 | import { CannonPhysicsEngine } from '../physics/CannonPhysicsEngine.js';
6 | import BaseTypes from '../serialize/BaseTypes.js';
7 | import { TwoVector } from '../serialize/TwoVector.js';
8 | import { ThreeVector } from '../serialize/ThreeVector.js';
9 | import Quaternion from '../serialize/Quaternion.js';
10 | import { GameObject } from '../serialize/GameObject.js';
11 | import DynamicObject from '../serialize/DynamicObject.js';
12 | import { PhysicalObject2D } from '../serialize/PhysicalObject2D.js';
13 | import { PhysicalObject3D } from '../serialize/PhysicalObject3D.js';
14 | import { ServerEngine, ServerEngineOptions } from '../ServerEngine.js';
15 | import { ClientEngineOptions, ClientEngine } from '../ClientEngine.js';
16 | import { KeyboardControls } from '../controls/KeyboardControls.js';
17 | import Renderer from '../render/Renderer.js';
18 | import AFrameRenderer from '../render/AFrameRenderer.js';
19 | import { SyncStrategy, SyncStrategyOptions } from '../syncStrategies/SyncStrategy.js';
20 | import { ExtrapolateStrategy, ExtrapolateSyncStrategyOptions } from '../syncStrategies/ExtrapolateStrategy.js';
21 | import Lib from '../lib/lib.js';
22 | import Serializer from '../serialize/Serializer.js';
23 | import { BruteForceCollisionDetectionOptions } from '../physics/SimplePhysics/BruteForceCollisionDetection.js';
24 | import { HSHGCollisionDetectionOptions } from '../physics/SimplePhysics/HSHGCollisionDetection.js';
25 | import { FrameSyncStrategy } from '../syncStrategies/FrameSyncStrategy.js';
26 |
27 | export {
28 | GameEngine, GameEngineOptions,
29 | GameWorld,
30 | InputDesc,
31 | PreStepDesc,
32 | Serializer,
33 | P2PhysicsEngine,
34 | BruteForceCollisionDetectionOptions, HSHGCollisionDetectionOptions,
35 | SimplePhysicsEngine, SimplePhysicsEngineOptions,
36 | CannonPhysicsEngine,
37 | BaseTypes,
38 | TwoVector,
39 | ThreeVector,
40 | Quaternion,
41 | GameObject,
42 | DynamicObject,
43 | PhysicalObject2D,
44 | PhysicalObject3D,
45 | ServerEngine, ServerEngineOptions,
46 | ClientEngine, ClientEngineOptions,
47 | KeyboardControls,
48 | Renderer,
49 | AFrameRenderer,
50 | SyncStrategy, SyncStrategyOptions,
51 | ExtrapolateStrategy, ExtrapolateSyncStrategyOptions,
52 | FrameSyncStrategy,
53 | Lib,
54 | }
55 |
--------------------------------------------------------------------------------
/src/package/clientExports.ts:
--------------------------------------------------------------------------------
1 | import { GameEngine, InputDesc, PreStepDesc } from "../GameEngine.js";
2 | import { GameWorld } from "../GameWorld.js";
3 | import { CannonPhysicsEngine } from "../physics/CannonPhysicsEngine.js";
4 | import { P2PhysicsEngine } from "../physics/P2PhysicsEngine.js";
5 | import { BruteForceCollisionDetectionOptions } from "../physics/SimplePhysics/BruteForceCollisionDetection.js";
6 | import { HSHGCollisionDetectionOptions } from "../physics/SimplePhysics/HSHGCollisionDetection.js";
7 | import { SimplePhysicsEngine, SimplePhysicsEngineOptions } from "../physics/SimplePhysicsEngine.js";
8 | import BaseTypes from "../serialize/BaseTypes.js";
9 | import DynamicObject from "../serialize/DynamicObject.js";
10 | import { GameObject } from "../serialize/GameObject.js";
11 | import { PhysicalObject2D } from "../serialize/PhysicalObject2D.js";
12 | import { PhysicalObject3D } from "../serialize/PhysicalObject3D.js";
13 | import Quaternion from "../serialize/Quaternion.js";
14 | import Serializer from "../serialize/Serializer.js";
15 | import { ThreeVector } from "../serialize/ThreeVector.js";
16 | import { TwoVector } from "../serialize/TwoVector.js";
17 | import Lib from '../lib/lib.js';
18 | import { ClientEngine, ClientEngineOptions } from "../ClientEngine.js";
19 | import { KeyboardControls } from "../controls/KeyboardControls.js";
20 | import Renderer from "../render/Renderer.js";
21 | import AFrameRenderer from "../render/AFrameRenderer.js";
22 | import { SyncStrategy, SyncStrategyOptions } from "../syncStrategies/SyncStrategy.js";
23 | import { ExtrapolateStrategy, ExtrapolateSyncStrategyOptions } from "../syncStrategies/ExtrapolateStrategy.js";
24 | import { FrameSyncStrategy } from "../syncStrategies/FrameSyncStrategy.js";
25 |
26 |
27 | export {
28 | GameEngine,
29 | GameWorld,
30 | InputDesc,
31 | PreStepDesc,
32 | Serializer,
33 | P2PhysicsEngine,
34 | BruteForceCollisionDetectionOptions,
35 | HSHGCollisionDetectionOptions,
36 | SimplePhysicsEngine, SimplePhysicsEngineOptions,
37 | CannonPhysicsEngine,
38 | BaseTypes,
39 | TwoVector,
40 | ThreeVector,
41 | Quaternion,
42 | GameObject,
43 | DynamicObject,
44 | PhysicalObject2D,
45 | PhysicalObject3D,
46 | Lib,
47 | ClientEngineOptions,
48 | ClientEngine,
49 | KeyboardControls,
50 | Renderer,
51 | AFrameRenderer,
52 | SyncStrategy, SyncStrategyOptions,
53 | ExtrapolateStrategy, ExtrapolateSyncStrategyOptions,
54 | FrameSyncStrategy
55 | };
56 |
--------------------------------------------------------------------------------
/src/package/serverExports.ts:
--------------------------------------------------------------------------------
1 | import { GameEngine, GameEngineOptions, InputDesc, PreStepDesc } from '../GameEngine.js';
2 | import { GameWorld } from '../GameWorld.js';
3 | import { P2PhysicsEngine } from '../physics/P2PhysicsEngine.js';
4 | import { SimplePhysicsEngine, SimplePhysicsEngineOptions } from '../physics/SimplePhysicsEngine.js';
5 | import { CannonPhysicsEngine } from '../physics/CannonPhysicsEngine.js';
6 | import BaseTypes from '../serialize/BaseTypes.js';
7 | import { TwoVector } from '../serialize/TwoVector.js';
8 | import { ThreeVector } from '../serialize/ThreeVector.js';
9 | import Quaternion from '../serialize/Quaternion.js';
10 | import { GameObject } from '../serialize/GameObject.js';
11 | import DynamicObject from '../serialize/DynamicObject.js';
12 | import { PhysicalObject2D } from '../serialize/PhysicalObject2D.js';
13 | import { PhysicalObject3D } from '../serialize/PhysicalObject3D.js';
14 | import { ServerEngine, ServerEngineOptions } from '../ServerEngine.js';
15 | import Renderer from '../render/Renderer.js';
16 | import Lib from '../lib/lib.js';
17 | import Serializer from '../serialize/Serializer.js';
18 | // TODO: remove Renderer from server exports.
19 | // some games (Brawler) have a renderer which needs to be notified on object create,
20 | // and instead of just listening to the object-create event, they are invoked by the
21 | // object itself on creation
22 |
23 | export {
24 | GameEngine, GameEngineOptions,
25 | GameWorld,
26 | Serializer,
27 | Renderer,
28 | InputDesc,
29 | PreStepDesc,
30 | P2PhysicsEngine,
31 | SimplePhysicsEngine, SimplePhysicsEngineOptions,
32 | CannonPhysicsEngine,
33 | BaseTypes,
34 | TwoVector,
35 | ThreeVector,
36 | Quaternion,
37 | GameObject,
38 | DynamicObject,
39 | PhysicalObject2D,
40 | PhysicalObject3D,
41 | ServerEngine, ServerEngineOptions,
42 | Lib,
43 | }
44 |
--------------------------------------------------------------------------------
/src/physics/CannonPhysicsEngine.ts:
--------------------------------------------------------------------------------
1 | import { GameObject } from '../serialize/GameObject.js';
2 | import { PhysicsEngine, PhysicsEngineOptions } from './PhysicsEngine.js';
3 | import * as Cannon from 'cannon';
4 |
5 | interface CannonPhysicsEngineOptions extends PhysicsEngineOptions {
6 | dt: number
7 | }
8 |
9 | /**
10 | * CannonPhysicsEngine is a three-dimensional lightweight physics engine
11 | */
12 | class CannonPhysicsEngine extends PhysicsEngine {
13 |
14 | private cannonPhysicsEngineOptions: CannonPhysicsEngineOptions;
15 |
16 | constructor(options: CannonPhysicsEngineOptions) {
17 | super(options);
18 |
19 | this.cannonPhysicsEngineOptions = options;
20 | this.cannonPhysicsEngineOptions.dt = this.cannonPhysicsEngineOptions.dt || (1 / 60);
21 | let world = this.world = new Cannon.World();
22 | world.quatNormalizeSkip = 0;
23 | world.quatNormalizeFast = false;
24 | world.gravity.set(0, -10, 0);
25 | world.broadphase = new Cannon.NaiveBroadphase();
26 | }
27 |
28 | // entry point for a single step of the Simple Physics
29 | step(dt: number, objectFilter: (o: GameObject) => boolean): void {
30 | this.world.step(dt || this.cannonPhysicsEngineOptions.dt);
31 | }
32 |
33 | addSphere(radius: number, mass: number): Cannon.Body {
34 | let shape = new Cannon.Sphere(radius);
35 | let body = new Cannon.Body({ mass, shape });
36 | body.position.set(0, 0, 0);
37 | this.world.addBody(body);
38 | return body;
39 | }
40 |
41 | addBox(x: number, y: number, z: number, mass: number, friction: number): Cannon.Body {
42 | let shape = new Cannon.Box(new Cannon.Vec3(x, y, z));
43 | let options: Cannon.IBodyOptions = { mass, shape };
44 | if (friction !== undefined)
45 | options.material = new Cannon.Material('material');
46 | let body = new Cannon.Body(options);
47 | body.position.set(0, 0, 0);
48 | this.world.addBody(body);
49 | return body;
50 | }
51 |
52 | addCylinder(radiusTop: number, radiusBottom: number, height: number, numSegments: number, mass: number): Cannon.Body {
53 | let shape = new Cannon.Cylinder(radiusTop, radiusBottom, height, numSegments);
54 | let body = new Cannon.Body({ mass, shape });
55 | this.world.addBody(body);
56 | return body;
57 | }
58 |
59 | removeObject(obj: Cannon.Body): void {
60 | this.world.remove(obj);
61 | }
62 | }
63 |
64 | export { CannonPhysicsEngine, CannonPhysicsEngineOptions }
65 |
--------------------------------------------------------------------------------
/src/physics/P2PhysicsEngine.ts:
--------------------------------------------------------------------------------
1 | import { GameObject } from '../serialize/GameObject.js';
2 | import { PhysicsEngine, PhysicsEngineOptions } from './PhysicsEngine.js';
3 | import P2, { BodyOptions, CircleOptions } from 'p2';
4 |
5 | interface P2PhysicsEngineOptions extends PhysicsEngineOptions {
6 | dt?: number
7 | }
8 |
9 | /**
10 | * P2PhysicsEngine is a three-dimensional lightweight physics engine
11 | */
12 | class P2PhysicsEngine extends PhysicsEngine {
13 | private p2PhysicsEngineOptions: P2PhysicsEngineOptions;
14 |
15 | constructor(options: P2PhysicsEngineOptions) {
16 | super(options);
17 | this.p2PhysicsEngineOptions = options;
18 | this.p2PhysicsEngineOptions.dt = this.p2PhysicsEngineOptions.dt || (1 / 60);
19 | this.world = new P2.World({ gravity: [0, 0] });
20 | }
21 |
22 | // entry point for a single step of the P2 Physics
23 | step(dt: number, objectFilter: (o: GameObject) => boolean): void {
24 | this.world.step(dt || this.p2PhysicsEngineOptions.dt);
25 | }
26 |
27 | // add a circle
28 | addCircle(circleOptions: CircleOptions, bodyOptions: BodyOptions): P2.Body {
29 |
30 | // create a body, add shape, add to world
31 | let body = new P2.Body(bodyOptions);
32 | body.addShape(new P2.Circle(circleOptions));
33 | this.world.addBody(body);
34 |
35 | return body;
36 | }
37 |
38 | addBox(width: number, height: number, mass: number) {
39 |
40 | // create a body, add shape, add to world
41 | let body = new P2.Body({ mass, position: [0, 0] });
42 | body.addShape(new P2.Box({ width, height }));
43 | this.world.addBody(body);
44 |
45 | return body;
46 | }
47 |
48 | removeObject(obj: P2.Body) {
49 | this.world.removeBody(obj);
50 | }
51 | }
52 |
53 | export { P2PhysicsEngine, P2PhysicsEngineOptions }
54 |
--------------------------------------------------------------------------------
/src/physics/PhysicsEngine.ts:
--------------------------------------------------------------------------------
1 | import { GameEngine } from '../GameEngine.js';
2 | import { GameObject } from "../serialize/GameObject.js";
3 |
4 | // The base Physics Engine class defines the expected interface
5 | // for all physics engines
6 |
7 | interface PhysicsEngineOptions {
8 | gameEngine: GameEngine;
9 | }
10 |
11 | class PhysicsEngine {
12 |
13 | private options: PhysicsEngineOptions;
14 | protected gameEngine: GameEngine;
15 | public world?: any;
16 |
17 | constructor(options: PhysicsEngineOptions) {
18 | this.options = options;
19 | this.gameEngine = options.gameEngine;
20 |
21 | if (!options.gameEngine) {
22 | console.warn('Physics engine initialized without gameEngine!');
23 | }
24 | }
25 |
26 | /**
27 | * A single Physics step.
28 | *
29 | * @param {Number} dt - time elapsed since last step
30 | * @param {Function} objectFilter - a test function which filters which objects should move
31 | */
32 | step(dt: number, objectFilter: (o: GameObject) => boolean) {}
33 |
34 | }
35 |
36 | export { PhysicsEngine, PhysicsEngineOptions }
--------------------------------------------------------------------------------
/src/physics/SimplePhysics/BruteForceCollisionDetection.ts:
--------------------------------------------------------------------------------
1 | import { GameEngine } from '../../GameEngine.js';
2 | import DynamicObject from '../../serialize/DynamicObject.js';
3 | import { TwoVector } from '../../serialize/TwoVector.js';
4 | import { CollisionDetection, CollisionDetectionOptions } from './CollisionDetection.js';
5 | let differenceVector = new TwoVector(0, 0);
6 |
7 | interface BruteForceCollisionDetectionOptions extends CollisionDetectionOptions {
8 | autoResolve: boolean,
9 | collisionDistance: number
10 | }
11 |
12 | // The collision detection of SimplePhysicsEngine is a brute-force approach
13 | class BruteForceCollisionDetection implements CollisionDetection {
14 |
15 | private options: BruteForceCollisionDetectionOptions;
16 | private gameEngine: GameEngine;
17 | private collisionPairs: { [key: string]: boolean }
18 |
19 | constructor(options: BruteForceCollisionDetectionOptions) {
20 | this.options = Object.assign({
21 | autoResolve: true
22 | }, options);
23 | this.collisionPairs = {};
24 | }
25 |
26 | // TODO: why do we need init as well as constructor?
27 | // TODO: HSHGCollisionDetectionOptions is uselessly passed twice, both on constructor and on init (below)
28 | init(options: BruteForceCollisionDetectionOptions) {
29 | this.gameEngine = options.gameEngine;
30 | }
31 |
32 | findCollision(o1: DynamicObject, o2: DynamicObject) {
33 |
34 | // static objects don't collide
35 | if (o1.isStatic && o2.isStatic)
36 | return false;
37 |
38 | // allow a collision checker function
39 | if (typeof o1.collidesWith === 'function') {
40 | if (!o1.collidesWith(o2))
41 | return false;
42 | }
43 |
44 | // radius-based collision
45 | if (this.options.collisionDistance) {
46 | differenceVector.copy(o1.position).subtract(o2.position);
47 | return differenceVector.length() < this.options.collisionDistance;
48 | }
49 |
50 | // check for no-collision first
51 | let o1Box = getBox(o1);
52 | let o2Box = getBox(o2);
53 | if (o1Box.xMin > o2Box.xMax ||
54 | o1Box.yMin > o2Box.yMax ||
55 | o2Box.xMin > o1Box.xMax ||
56 | o2Box.yMin > o1Box.yMax)
57 | return false;
58 |
59 | if (!this.options.autoResolve)
60 | return true;
61 |
62 | // need to auto-resolve
63 | let shiftY1 = o2Box.yMax - o1Box.yMin;
64 | let shiftY2 = o1Box.yMax - o2Box.yMin;
65 | let shiftX1 = o2Box.xMax - o1Box.xMin;
66 | let shiftX2 = o1Box.xMax - o2Box.xMin;
67 | let smallestYShift = Math.min(Math.abs(shiftY1), Math.abs(shiftY2));
68 | let smallestXShift = Math.min(Math.abs(shiftX1), Math.abs(shiftX2));
69 |
70 | // choose to apply the smallest shift which solves the collision
71 | if (smallestYShift < smallestXShift) {
72 | if (o1Box.yMin > o2Box.yMin && o1Box.yMin < o2Box.yMax) {
73 | if (o2.isStatic) o1.position.y += shiftY1;
74 | else if (o1.isStatic) o2.position.y -= shiftY1;
75 | else {
76 | o1.position.y += shiftY1 / 2;
77 | o2.position.y -= shiftY1 / 2;
78 | }
79 | } else if (o1Box.yMax > o2Box.yMin && o1Box.yMax < o2Box.yMax) {
80 | if (o2.isStatic) o1.position.y -= shiftY2;
81 | else if (o1.isStatic) o2.position.y += shiftY2;
82 | else {
83 | o1.position.y -= shiftY2 / 2;
84 | o2.position.y += shiftY2 / 2;
85 | }
86 | }
87 | o1.velocity.y = 0;
88 | o2.velocity.y = 0;
89 | } else {
90 | if (o1Box.xMin > o2Box.xMin && o1Box.xMin < o2Box.xMax) {
91 | if (o2.isStatic) o1.position.x += shiftX1;
92 | else if (o1.isStatic) o2.position.x -= shiftX1;
93 | else {
94 | o1.position.x += shiftX1 / 2;
95 | o2.position.x -= shiftX1 / 2;
96 | }
97 | } else if (o1Box.xMax > o2Box.xMin && o1Box.xMax < o2Box.xMax) {
98 | if (o2.isStatic) o1.position.x -= shiftX2;
99 | else if (o1.isStatic) o2.position.x += shiftX2;
100 | else {
101 | o1.position.x -= shiftX2 / 2;
102 | o2.position.x += shiftX2 / 2;
103 | }
104 | }
105 | o1.velocity.x = 0;
106 | o2.velocity.x = 0;
107 | }
108 |
109 | return true;
110 | }
111 |
112 | // check if pair (id1, id2) have collided
113 | checkPair(id1: string, id2: string) {
114 | let objects = this.gameEngine.world.objects;
115 | let o1 = objects[id1];
116 | let o2 = objects[id2];
117 |
118 | // make sure that objects actually exist. might have been destroyed
119 | if (!o1 || !o2) return;
120 | let pairId = [id1, id2].join(',');
121 |
122 | if (this.findCollision( o1, o2)) {
123 | if (!(pairId in this.collisionPairs)) {
124 | this.collisionPairs[pairId] = true;
125 | this.gameEngine.emit('collisionStart', { o1, o2 });
126 | }
127 | } else if (pairId in this.collisionPairs) {
128 | this.gameEngine.emit('collisionStop', { o1, o2 });
129 | delete this.collisionPairs[pairId];
130 | }
131 | }
132 |
133 | // detect by checking all pairs
134 | detect() {
135 | let objects = this.gameEngine.world.objects;
136 | let keys = Object.keys(objects);
137 |
138 | // delete non existant object pairs
139 | for (let pairId in this.collisionPairs)
140 | if (this.collisionPairs.hasOwnProperty(pairId))
141 | if (keys.indexOf(pairId.split(',')[0]) === -1 || keys.indexOf(pairId.split(',')[1]) === -1)
142 | delete this.collisionPairs[pairId];
143 |
144 | // check all pairs
145 | for (let k1 of keys)
146 | for (let k2 of keys)
147 | if (k2 > k1) this.checkPair(k1, k2);
148 | }
149 | }
150 |
151 | // get bounding box of object o
152 | function getBox(o: DynamicObject) {
153 | return {
154 | xMin: o.position.x,
155 | xMax: o.position.x + o.width,
156 | yMin: o.position.y,
157 | yMax: o.position.y + o.height
158 | };
159 | }
160 |
161 | export { BruteForceCollisionDetection, BruteForceCollisionDetectionOptions }
--------------------------------------------------------------------------------
/src/physics/SimplePhysics/CollisionDetection.ts:
--------------------------------------------------------------------------------
1 | import { GameEngine } from '../../GameEngine.js';
2 |
3 | interface CollisionDetectionOptions {
4 | gameEngine: GameEngine
5 | }
6 |
7 | interface CollisionDetection {
8 | init(options: CollisionDetectionOptions);
9 | detect();
10 | }
11 |
12 | export { CollisionDetection, CollisionDetectionOptions }
--------------------------------------------------------------------------------
/src/physics/SimplePhysics/HSHGCollisionDetection.ts:
--------------------------------------------------------------------------------
1 | import { GameEngine } from '../../GameEngine.js';
2 | import { CollisionDetection, CollisionDetectionOptions } from './CollisionDetection.js';
3 | import { HSHGDetector } from './HSHG.js';
4 |
5 | interface HSHGCollisionDetectionOptions extends CollisionDetectionOptions {
6 | gameEngine: GameEngine,
7 | COLLISION_DISTANCE?: number
8 | }
9 |
10 | type PairsDict = {
11 | [key: string]: { o1: any, o2: any }
12 | }
13 |
14 | interface CollisionObject {
15 | id: number
16 | }
17 |
18 | // Collision detection based on Hierarchical Spatial Hash Grid
19 | // uses this implementation https://gist.github.com/kirbysayshi/1760774
20 | class HSHGCollisionDetection implements CollisionDetection {
21 |
22 | private options: HSHGCollisionDetectionOptions;
23 | private gameEngine: GameEngine;
24 | private grid: HSHGDetector;
25 | private previousCollisionPairs: PairsDict;
26 | private stepCollidingPairs: PairsDict;
27 |
28 | // TODO: HSHGCollisionDetectionOptions is uselessly passed twice, both on constructor and on init (below)
29 | constructor(options: HSHGCollisionDetectionOptions) {
30 | this.options = Object.assign({ COLLISION_DISTANCE: 28 }, options);
31 | }
32 |
33 | init(options: HSHGCollisionDetectionOptions) {
34 | this.gameEngine = options.gameEngine;
35 | this.grid = new HSHGDetector();
36 | this.previousCollisionPairs = {};
37 | this.stepCollidingPairs = {};
38 |
39 | this.gameEngine.on('objectAdded', obj => {
40 | // add the gameEngine obj the the spatial grid
41 | this.grid.addObject(obj);
42 | });
43 |
44 | this.gameEngine.on('objectDestroyed', obj => {
45 | // add the gameEngine obj the the spatial grid
46 | this.grid.removeObject(obj);
47 | });
48 | }
49 |
50 | detect() {
51 | this.grid.update();
52 | this.stepCollidingPairs = this.grid.queryForCollisionPairs().reduce((accumulator, currentValue, i) => {
53 | let pairId = getArrayPairId(currentValue);
54 | accumulator[pairId] = { o1: currentValue[0], o2: currentValue[1] };
55 | return accumulator;
56 | }, {});
57 |
58 | for (let pairId of Object.keys(this.previousCollisionPairs)) {
59 | let pairObj = this.previousCollisionPairs[pairId];
60 |
61 | // existed in previous pairs, but not during this step: this pair stopped colliding
62 | if (pairId in this.stepCollidingPairs === false) {
63 | this.gameEngine.emit('collisionStop', pairObj);
64 | }
65 | }
66 |
67 | for (let pairId of Object.keys(this.stepCollidingPairs)) {
68 | let pairObj = this.stepCollidingPairs[pairId];
69 |
70 | // didn't exist in previous pairs, but exists now: this is a new colliding pair
71 | if (pairId in this.previousCollisionPairs === false) {
72 | this.gameEngine.emit('collisionStart', pairObj);
73 | }
74 | }
75 |
76 | this.previousCollisionPairs = this.stepCollidingPairs;
77 | }
78 |
79 | /**
80 | * checks wheter two objects are currently colliding
81 | * @param {Object} o1 first object
82 | * @param {Object} o2 second object
83 | * @return {boolean} are the two objects colliding?
84 | */
85 | areObjectsColliding(o1, o2) {
86 | return getArrayPairId([o1, o2]) in this.stepCollidingPairs;
87 | }
88 |
89 | }
90 |
91 | function getArrayPairId(arrayPair: CollisionObject[]): string {
92 | // make sure to get the same id regardless of object order
93 | // slice(0) is used as a fast mechanism to create a shallow copy
94 | let sortedArrayPair = arrayPair.slice(0).sort();
95 | return sortedArrayPair[0].id + '-' + sortedArrayPair[1].id;
96 | }
97 |
98 | export { HSHGCollisionDetection, HSHGCollisionDetectionOptions }
99 |
--------------------------------------------------------------------------------
/src/physics/SimplePhysicsEngine.ts:
--------------------------------------------------------------------------------
1 | import { PhysicsEngine, PhysicsEngineOptions } from './PhysicsEngine.js';
2 | import { TwoVector } from '../serialize/TwoVector.js';
3 | import { HSHGCollisionDetection, HSHGCollisionDetectionOptions } from './SimplePhysics/HSHGCollisionDetection.js';
4 | import { BruteForceCollisionDetection, BruteForceCollisionDetectionOptions } from './SimplePhysics/BruteForceCollisionDetection.js';
5 | import { CollisionDetection } from './SimplePhysics/CollisionDetection.js';
6 | import { GameEngine } from '../GameEngine.js';
7 | import { GameObject } from '../serialize/GameObject.js';
8 | import DynamicObject from '../serialize/DynamicObject.js';
9 |
10 | let dv = new TwoVector(0, 0);
11 | let dx = new TwoVector(0, 0);
12 |
13 | interface SimplePhysicsEngineOptions extends PhysicsEngineOptions {
14 | collisionsType?: "HSHG" | "bruteForce",
15 | collisions?: HSHGCollisionDetectionOptions | BruteForceCollisionDetectionOptions,
16 | gravity?: TwoVector,
17 | }
18 | /**
19 | * SimplePhysicsEngine is a pseudo-physics engine which works with
20 | * objects of class DynamicObject.
21 | * The Simple Physics Engine is a "fake" physics engine, which is more
22 | * appropriate for arcade games, and it is sometimes referred to as "arcade"
23 | * physics. For example if a character is standing at the edge of a platform,
24 | * with only one foot on the platform, it won't fall over. This is a desired
25 | * game behaviour in platformer games.
26 | */
27 | class SimplePhysicsEngine extends PhysicsEngine {
28 |
29 | private collisionDetection: CollisionDetection;
30 | private gravity: TwoVector;
31 |
32 | /**
33 | * Creates an instance of the Simple Physics Engine.
34 | * @param {Object} options - physics options
35 | * @param {Object} options.collisions - collision options
36 | * @param {String} options.collisions.type - can be set to "HSHG" or "bruteForce". Default is Brute-Force collision detection.
37 | * @param {Number} options.collisions.collisionDistance - for brute force, this can be set for a simple distance-based (radius) collision detection.
38 | * @param {Boolean} options.collisions.autoResolve - for brute force collision, colliding objects should be moved apart
39 | * @param {TwoVector} options.gravity - TwoVector instance which describes gravity, which will be added to the velocity of all objects at every step. For example TwoVector(0, -0.01)
40 | */
41 | constructor(options: SimplePhysicsEngineOptions) {
42 | super(options);
43 |
44 |
45 | // todo does this mean both modules always get loaded?
46 | if (options.collisions && options.collisionsType === 'HSHG') {
47 | this.collisionDetection = new HSHGCollisionDetection(options.collisions);
48 | } else {
49 | this.collisionDetection = new BruteForceCollisionDetection( options.collisions);
50 | }
51 |
52 | /**
53 | * The actor's name.
54 | * @memberof SimplePhysicsEngine
55 | * @member {TwoVector} gravity affecting all objects
56 | */
57 | this.gravity = new TwoVector(0, 0);
58 |
59 | if (options.gravity)
60 | this.gravity.copy(options.gravity);
61 |
62 | let collisionOptions = Object.assign({ gameEngine: this.gameEngine }, options.collisions);
63 | this.collisionDetection.init(collisionOptions);
64 | }
65 |
66 | // a single object advances, based on:
67 | // isRotatingRight, isRotatingLeft, isAccelerating, current velocity
68 | // wrap-around the world if necessary
69 | objectStep(o: DynamicObject, dt: number) {
70 |
71 | // calculate factor
72 | if (dt === 0)
73 | return;
74 |
75 | if (dt)
76 | dt /= (1 / 60);
77 | else
78 | dt = 1;
79 |
80 | // TODO: worldsettings is a hack. Find all places which use it in all games
81 | // and come up with a better solution. for example an option sent to the physics Engine
82 | // with a "worldWrap:true" options
83 | // replace with a "worldBounds" parameter to the PhysicsEngine constructor
84 |
85 | let worldSettings = this.gameEngine.worldSettings;
86 |
87 | // TODO: remove this code in version 4: these attributes are deprecated
88 | if (o.isRotatingRight) { o.angle += o.rotationSpeed; }
89 | if (o.isRotatingLeft) { o.angle -= o.rotationSpeed; }
90 |
91 | // TODO: remove this code in version 4: these attributes are deprecated
92 | if (o.angle >= 360) { o.angle -= 360; }
93 | if (o.angle < 0) { o.angle += 360; }
94 |
95 | // TODO: remove this code in version 4: these attributes are deprecated
96 | if (o.isAccelerating) {
97 | let rad = o.angle * (Math.PI / 180);
98 | dv.set(Math.cos(rad), Math.sin(rad)).multiplyScalar(o.acceleration).multiplyScalar(dt);
99 | o.velocity.add(dv);
100 | }
101 |
102 | // apply gravity
103 | if (!o.isStatic) o.velocity.add(this.gravity);
104 |
105 | let velMagnitude = o.velocity.length();
106 | if ((o.maxSpeed !== null) && (velMagnitude > o.maxSpeed)) {
107 | o.velocity.multiplyScalar(o.maxSpeed / velMagnitude);
108 | }
109 |
110 | o.isAccelerating = false;
111 | o.isRotatingLeft = false;
112 | o.isRotatingRight = false;
113 |
114 | dx.copy(o.velocity).multiplyScalar(dt);
115 | o.position.add(dx);
116 |
117 | o.velocity.multiply(o.friction);
118 |
119 | // wrap around the world edges
120 | if (worldSettings.worldWrap) {
121 | if (o.position.x >= worldSettings.width) { o.position.x -= worldSettings.width; }
122 | if (o.position.y >= worldSettings.height) { o.position.y -= worldSettings.height; }
123 | if (o.position.x < 0) { o.position.x += worldSettings.width; }
124 | if (o.position.y < 0) { o.position.y += worldSettings.height; }
125 | }
126 | }
127 |
128 | // entry point for a single step of the Simple Physics
129 | step(dt: number, objectFilter: (o: GameObject) => boolean): void {
130 |
131 | // each object should advance
132 | let objects = this.gameEngine.world.objects;
133 | for (let objId of Object.keys(objects)) {
134 |
135 | // shadow objects are not re-enacted
136 | let ob = objects[objId];
137 | if (!objectFilter(ob))
138 | continue;
139 |
140 | // run the object step
141 | this.objectStep(ob, dt);
142 | }
143 |
144 | // emit event on collision
145 | this.collisionDetection.detect();
146 | }
147 | }
148 |
149 | export { SimplePhysicsEngine, SimplePhysicsEngineOptions };
150 |
--------------------------------------------------------------------------------
/src/render/AFrameRenderer.ts:
--------------------------------------------------------------------------------
1 | /* globals AFRAME */
2 |
3 | import { GameEngine } from '../GameEngine.js';
4 | import Renderer from './Renderer.js';
5 | import networkedPhysics from './aframe/system.js';
6 |
7 | declare global {
8 | let AFRAME: any;
9 | }
10 |
11 | /**
12 | * The A-Frame Renderer
13 | */
14 | class AFrameRenderer extends Renderer {
15 |
16 | protected scene: any;
17 |
18 | /**
19 | * Constructor of the Renderer singleton.
20 | * @param {GameEngine} gameEngine - Reference to the GameEngine instance.
21 | * @param {ClientEngine} clientEngine - Reference to the ClientEngine instance.
22 | */
23 | constructor(gameEngine: GameEngine) {
24 | super(gameEngine);
25 |
26 | // set up the networkedPhysics as an A-Frame system
27 | networkedPhysics.setGlobals(gameEngine, this);
28 | AFRAME.registerSystem('networked-physics', networkedPhysics);
29 | }
30 |
31 | reportSlowFrameRate() {
32 | this.gameEngine.emit('client__slowFrameRate');
33 | }
34 |
35 | /**
36 | * Initialize the renderer.
37 | * @return {Promise} Resolves when renderer is ready.
38 | */
39 | init() {
40 |
41 | let p = super.init();
42 |
43 | let sceneElArray = document.getElementsByTagName('a-scene');
44 | if (sceneElArray.length !== 1) {
45 | throw new Error('A-Frame scene element not found');
46 | }
47 | this.scene = sceneElArray[0];
48 |
49 | this.gameEngine.on('objectRemoved', (o) => {
50 | o.renderObj.remove();
51 | });
52 |
53 | return p; // eslint-disable-line new-cap
54 | }
55 |
56 | /**
57 | * In AFrame, we set the draw method (which is called at requestAnimationFrame)
58 | * to a NO-OP. See tick() instead
59 | */
60 | draw() {}
61 |
62 | tick(t: number, dt: number) {
63 | super.draw(t, dt);
64 | }
65 |
66 | }
67 |
68 | export default AFrameRenderer;
69 |
--------------------------------------------------------------------------------
/src/render/Renderer.ts:
--------------------------------------------------------------------------------
1 | import { GameEngine } from '../GameEngine.js';
2 | import { ClientEngine } from '../ClientEngine.js';
3 | let singleton: Renderer;
4 |
5 | const TIME_RESET_THRESHOLD = 100;
6 |
7 | declare global {
8 | let THREE: any;
9 | }
10 |
11 | /**
12 | * The Renderer is the component which must *draw* the game on the client.
13 | * It will be instantiated once on each client, and must implement the draw
14 | * method. The draw method will be invoked on every iteration of the browser's
15 | * render loop.
16 | */
17 | class Renderer {
18 |
19 | protected gameEngine: GameEngine;
20 | public clientEngine: ClientEngine;
21 | private doReset: boolean;
22 |
23 | static getInstance() {
24 | return singleton;
25 | }
26 |
27 | /**
28 | * Constructor of the Renderer singleton.
29 | * @param {GameEngine} gameEngine - Reference to the GameEngine instance.
30 | */
31 | constructor(gameEngine) {
32 | this.gameEngine = gameEngine;
33 | this.gameEngine.on('client__stepReset', () => { this.doReset = true; });
34 | gameEngine.on('objectAdded', this.addObject.bind(this));
35 | gameEngine.on('objectDestroyed', this.removeObject.bind(this));
36 |
37 | // the singleton renderer has been created
38 | singleton = this;
39 | }
40 |
41 | /**
42 | * Initialize the renderer.
43 | * @return {Promise} Resolves when renderer is ready.
44 | */
45 | init(): Promise {
46 | if ((typeof window === 'undefined') || !document) {
47 | console.log('renderer invoked on server side.');
48 | }
49 | this.gameEngine.emit('client__rendererReady');
50 | return Promise.resolve(); // eslint-disable-line new-cap
51 | }
52 |
53 | reportSlowFrameRate() {
54 | this.gameEngine.emit('client__slowFrameRate');
55 | }
56 |
57 | // TODO: t and dt args are not always used (see PixiRenderer) so find a cleaner solution
58 | /**
59 | * The main draw function. This method is called at high frequency,
60 | * at the rate of the render loop. Typically this is 60Hz, in WebVR 90Hz.
61 | * If the client engine has been configured to render-schedule, then this
62 | * method must call the ClientEngine.js's step method.
63 | *
64 | * @param {Number} t - current time (only required in render-schedule mode)
65 | * @param {Number} dt - time elapsed since last draw
66 | */
67 | draw(t: number, dt: number) {
68 | this.gameEngine.emit('client__draw');
69 |
70 | if (this.clientEngine.options.scheduler === 'render-schedule')
71 | this.runClientStep(t);
72 | }
73 |
74 | /**
75 | * The main draw function. This method is called at high frequency,
76 | * at the rate of the render loop. Typically this is 60Hz, in WebVR 90Hz.
77 | *
78 | * @param {Number} t - current time
79 | * @param {Number} dt - time elapsed since last draw
80 | */
81 | runClientStep(t: number) {
82 | let p = this.clientEngine.options.stepPeriod;
83 | let dt = 0;
84 |
85 | // reset step time if we passed a threshold
86 | if (this.doReset || t > this.clientEngine.lastStepTime + TIME_RESET_THRESHOLD) {
87 | this.doReset = false;
88 | this.clientEngine.lastStepTime = t - p / 2;
89 | this.clientEngine.correction = p / 2;
90 | }
91 |
92 | // catch-up missed steps
93 | while (t > this.clientEngine.lastStepTime + p) {
94 | this.clientEngine.step(this.clientEngine.lastStepTime + p, p + this.clientEngine.correction);
95 | this.clientEngine.lastStepTime += p;
96 | this.clientEngine.correction = 0;
97 | }
98 |
99 | // if not ready for a real step yet, return
100 | // this might happen after catch up above
101 | if (t < this.clientEngine.lastStepTime) {
102 | dt = t - this.clientEngine.lastStepTime + this.clientEngine.correction;
103 | if (dt < 0) dt = 0;
104 | this.clientEngine.correction = this.clientEngine.lastStepTime - t;
105 | this.clientEngine.step(t, dt, true);
106 | return;
107 | }
108 |
109 | // render-controlled step
110 | dt = t - this.clientEngine.lastStepTime + this.clientEngine.correction;
111 | this.clientEngine.lastStepTime += p;
112 | this.clientEngine.correction = this.clientEngine.lastStepTime - t;
113 | this.clientEngine.step(t, dt);
114 | }
115 |
116 | /**
117 | * Handle the addition of a new object to the world.
118 | * @param {Object} obj - The object to be added.
119 | */
120 | addObject(obj) {}
121 |
122 | /**
123 | * Handle the removal of an old object from the world.
124 | * @param {Object} obj - The object to be removed.
125 | */
126 | removeObject(obj) {}
127 |
128 | /**
129 | * Called when clientEngine has stopped, time to clean up
130 | */
131 | stop() {}
132 | }
133 |
134 | export default Renderer;
135 |
--------------------------------------------------------------------------------
/src/render/ThreeRenderer.ts:
--------------------------------------------------------------------------------
1 | /* global THREE */
2 | import Renderer from './Renderer.js';
3 |
4 |
5 | // TODO: I have mixed feelings about this class. It doesn't actually provide
6 | // anything useful. I assume each game will write their own renderer even in THREE.
7 | // we can make it store a list of objects, and provide a Raycaster, and send events.
8 | // But it hijacks the creation of the scene and the THREE.renderer. It doesn't make
9 | // sense to me that the camera and lights are in the derived class, but the scene and
10 | // renderer are in the base class. seems like inheritance-abuse.
11 | export default class ThreeRenderer extends Renderer {
12 | private scene: any;
13 | private camera: any;
14 | private renderer: any;
15 | private raycaster: any;
16 | private THREE: any;
17 |
18 | // constructor
19 | constructor(gameEngine) {
20 | super(gameEngine);
21 | this.scene = null;
22 | this.camera = null;
23 | this.renderer = null;
24 | }
25 |
26 | // setup the 3D scene
27 | init() {
28 |
29 | super.init();
30 |
31 | // setup the scene
32 | this.scene = new THREE.Scene();
33 |
34 | // setup the renderer and add the canvas to the body
35 | this.renderer = new THREE.WebGLRenderer({
36 | antialias: true
37 | });
38 | document.getElementById('viewport')?.appendChild(this.renderer.domElement);
39 |
40 | // a local raycaster
41 | this.raycaster = new THREE.Raycaster();
42 |
43 | // TODO: is this still needed?
44 | this.THREE = THREE;
45 |
46 | return Promise.resolve();
47 | }
48 |
49 | // single step
50 | draw() {
51 | this.renderer.render(this.scene, this.camera);
52 | }
53 |
54 | // add one object
55 | addObject(id) {
56 | // this.scene.add(sphere);
57 | // return sphere;
58 | }
59 |
60 | removeObject(o) {
61 | this.scene.remove(o);
62 | }
63 | }
--------------------------------------------------------------------------------
/src/render/aframe/system.ts:
--------------------------------------------------------------------------------
1 | /* global THREE */
2 | const FRAME_HISTORY_SIZE = 20;
3 | const MAX_SLOW_FRAMES = 10;
4 |
5 |
6 | export default {
7 | schema: {
8 | traceLevel: { default: 4 }
9 | },
10 |
11 | init: function() {
12 |
13 | // TODO: Sometimes an object is "simple". For example it uses
14 | // existing AFrame assets (an OBJ file and a material)
15 | // in this case, we can auto-generate the DOM element,
16 | // setting the quaternion, position, material, game-object-id
17 | // and obj-model. Same goes for objects which use primitive
18 | // geometric objects. Remember to also remove them.
19 | this.CAMERA_OFFSET_VEC = new THREE.Vector3(0, 5, -10);
20 | this.frameRateHistory = [];
21 | for (let i = 0; i < FRAME_HISTORY_SIZE; i++)
22 | this.frameRateHistory.push(false);
23 | this.frameRateTest = (1000 / 60) * 1.2;
24 |
25 | // capture the chase camera if available
26 | let chaseCameras = document.getElementsByClassName('chaseCamera');
27 | if (chaseCameras)
28 | this.cameraEl = chaseCameras[0];
29 | },
30 |
31 | tick: function(t, dt) {
32 | if (!this.gameEngine)
33 | return;
34 | this.renderer.tick(t, dt);
35 |
36 | let frh = this.frameRateHistory;
37 | frh.push(dt > this.frameRateTest);
38 | frh.shift();
39 | const slowFrames = frh.filter(x => x);
40 | if (slowFrames.length > MAX_SLOW_FRAMES) {
41 | this.frameRateHistory = frh.map(x => false);
42 | this.renderer.reportSlowFrameRate();
43 | }
44 |
45 | // for each object in the world, update the a-frame element
46 | this.gameEngine.world.forEachObject((id, o) => {
47 | let el = o.renderEl;
48 | if (el) {
49 | let q = o.quaternion;
50 | let p = o.position;
51 | el.setAttribute('position', `${p.x} ${p.y} ${p.z}`);
52 | el.object3D.quaternion.set(q.x, q.y, q.z, q.w);
53 |
54 | // if a chase camera is configured, update it
55 | if (this.cameraEl && this.gameEngine.playerId === o.playerId) {
56 | let camera = this.cameraEl.object3D.children[0];
57 | let relativeCameraOffset = this.CAMERA_OFFSET_VEC.clone();
58 | let cameraOffset = relativeCameraOffset.applyMatrix4(o.renderEl.object3D.matrixWorld);
59 | camera.position.copy(cameraOffset);
60 | camera.lookAt(o.renderEl.object3D.position);
61 | }
62 |
63 | }
64 | });
65 | },
66 |
67 | // NOTE: webpack generated incorrect code if you use arrow notation below
68 | // it sets "this" to "undefined"
69 | setGlobals: function(gameEngine, renderer) {
70 | this.gameEngine = gameEngine;
71 | this.renderer = renderer;
72 | }
73 | };
74 |
--------------------------------------------------------------------------------
/src/render/pixi/PixiRenderableComponent.ts:
--------------------------------------------------------------------------------
1 | import GameComponent from '../../serialize/GameComponent.js';
2 |
3 | declare global {
4 | let PIXI: any;
5 | }
6 |
7 | type PixiRenderableComponentOptions = {
8 | assetName: string,
9 | spriteURL: string,
10 | width: number,
11 | height: number,
12 | onRenderableCreated: (sprite: any, pixiRC: PixiRenderableComponent) => any,
13 | onRender: () => void
14 | }
15 |
16 | export default class PixiRenderableComponent extends GameComponent {
17 |
18 | private options: PixiRenderableComponentOptions;
19 |
20 | constructor(options: PixiRenderableComponentOptions) {
21 | super();
22 | this.options = options;
23 | }
24 |
25 | /**
26 | * Initial creation of the Pixi renderable
27 | * @returns A pixi container/sprite
28 | */
29 | createRenderable() {
30 | let sprite;
31 | if (this.options.assetName) {
32 | sprite = new PIXI.Sprite(PIXI.loader.resources[this.options.assetName].texture);
33 | } else if (this.options.spriteURL) {
34 | sprite = new PIXI.Sprite.fromImage(this.options.spriteURL);
35 | }
36 |
37 | if (this.options.width) {
38 | sprite.width = this.options.width;
39 | }
40 |
41 | if (this.options.height) {
42 | sprite.height = this.options.height;
43 | }
44 |
45 | if (this.options.onRenderableCreated) {
46 | sprite = this.options.onRenderableCreated(sprite, this);
47 | }
48 |
49 | return sprite;
50 | }
51 |
52 | /**
53 | * This method gets executed on every render step
54 | * Note - this should only include rendering logic and not game logic
55 | */
56 | render() {
57 | if (this.options.onRender) {
58 | this.options.onRender();
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/render/pixi/PixiRenderer.ts:
--------------------------------------------------------------------------------
1 | import Renderer from '../Renderer.js';
2 | import PixiRenderableComponent from './PixiRenderableComponent.js';
3 |
4 |
5 | type SpriteMap = {
6 | [key: string]: any;
7 | }
8 | /**
9 | * Pixi Renderer
10 | */
11 | export default class PixiRenderer extends Renderer {
12 |
13 | private isReady: boolean;
14 | private viewportWidth: number;
15 | private viewportHeight: number;
16 | private stage: any;
17 | private layers: any;
18 | private renderer: any;
19 | private sprites: SpriteMap;
20 | private initPromise: Promise;
21 |
22 | /**
23 | * Returns a dictionary of image assets and their paths
24 | * E.G. {
25 | ship: 'assets/ship.png',
26 | missile: 'assets/missile.png',
27 | }
28 | * @returns {{}}
29 | * @constructor
30 | */
31 | get ASSETPATHS() {
32 | return {};
33 | }
34 |
35 | constructor(gameEngine) {
36 | super(gameEngine);
37 | this.sprites = {};
38 | this.isReady = false;
39 | }
40 |
41 | init() {
42 | // prevent calling init twice
43 | if (this.initPromise) return this.initPromise;
44 |
45 | this.viewportWidth = window.innerWidth;
46 | this.viewportHeight = window.innerHeight;
47 | this.stage = new PIXI.Container();
48 |
49 | // default layers
50 | this.layers = {
51 | base: new PIXI.Container()
52 | };
53 |
54 | this.stage.addChild(this.layers.base);
55 |
56 | if (document.readyState === 'complete' || document.readyState === 'interactive') {
57 | this.onDOMLoaded();
58 | } else {
59 | document.addEventListener('DOMContentLoaded', ()=>{
60 | this.onDOMLoaded();
61 | });
62 | }
63 |
64 | this.initPromise = new Promise((resolve, reject)=>{
65 | let onLoadComplete = () => {
66 | this.isReady = true;
67 | resolve();
68 | };
69 |
70 | let resourceList = Object.keys(this.ASSETPATHS).map( x => {
71 | return {
72 | name: x,
73 | url: this.ASSETPATHS[x]
74 | };
75 | });
76 |
77 | // make sure there are actual resources in the queue
78 | if (resourceList.length > 0)
79 | PIXI.loader.add(resourceList).load(onLoadComplete);
80 | else
81 | onLoadComplete();
82 | });
83 |
84 | return this.initPromise;
85 | }
86 |
87 | onDOMLoaded() {
88 | this.renderer = PIXI.autoDetectRenderer(this.viewportWidth, this.viewportHeight);
89 | document.body.querySelector('.pixiContainer')?.appendChild(this.renderer.view);
90 | }
91 |
92 | draw() {
93 | super.draw(0, 0);
94 |
95 | if (!this.isReady) return; // assets might not have been loaded yet
96 |
97 | for (let objId of Object.keys(this.sprites)) {
98 | let objData = this.gameEngine.world.objects[objId];
99 | let sprite = this.sprites[objId];
100 |
101 | if (objData) {
102 | sprite.x = objData.position.x;
103 | sprite.y = objData.position.y;
104 | sprite.rotation = this.gameEngine.world.objects[objId].angle * Math.PI/180;
105 | }
106 | }
107 |
108 | this.renderer.render(this.stage);
109 | }
110 |
111 | addObject(obj) {
112 | if (obj.hasComponent(PixiRenderableComponent)){
113 | let renderable = obj.getComponent(PixiRenderableComponent);
114 | let sprite = this.sprites[obj.id] = renderable.createRenderable();
115 | sprite.anchor.set(0.5, 0.5);
116 | sprite.position.set(obj.position.x, obj.position.y);
117 | this.layers.base.addChild(sprite);
118 | }
119 | }
120 |
121 | removeObject(obj) {
122 | if (obj.hasComponent(PixiRenderableComponent)){
123 | let sprite = this.sprites[obj.id];
124 | if (sprite) {
125 | this.sprites[obj.id].destroy();
126 | delete this.sprites[obj.id];
127 | }
128 | }
129 | }
130 |
131 | }
132 |
--------------------------------------------------------------------------------
/src/serialize/BaseTypes.ts:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * The BaseTypes class defines the base types used in Lance.
4 | * These are the types which can be used to define an object's netscheme attributes,
5 | * which can be serialized by lance.
6 | * @example
7 | * netScheme() {
8 | * return {
9 | * strength: { type: BaseTypes.TYPES.FLOAT32 },
10 | * shield: { type: BaseTypes.TYPES.INT8 },
11 | * name: { type: BaseTypes.String },
12 | * backpack: { type: BaseTypes.ClassInstance },
13 | * coins: {
14 | * type: BaseTypes.List,
15 | * itemType: BaseTypes.TYPES.UINT8
16 | * }
17 | * };
18 | * }
19 | */
20 | enum BaseTypes {
21 | Float32 = 'FLOAT32',
22 | Int32 = 'INT32',
23 | Int16 = 'INT16',
24 | Int8 = 'INT8',
25 | UInt8 = 'UINT8',
26 | String = 'STRING',
27 | ClassInstance = 'CLASSINSTANCE',
28 | List = 'List'
29 | }
30 |
31 |
32 | // static TYPES: 'FLOAT32' | 'INT32' | 'INT16' | 'INT8' | 'UINT8' | 'STRING' | 'CLASSINSTANCE' | 'LIST';
33 |
34 |
35 | /**
36 | * @type {object}
37 | * @property {string} FLOAT32 Seriablizable float
38 | * @property {string} INT32 Seriablizable 32-bit integer
39 | * @property {string} INT16 Seriablizable 16-bit integer
40 | * @property {string} INT8 Seriablizable 8-bit integer
41 | * @property {string} UINT8 Seriablizable unsigned 8-bit integer
42 | * @property {string} STRING Seriablizable string
43 | * @property {string} CLASSINSTANCE Seriablizable class. Make sure you register all the classes included in this way.
44 | * @property {string} LIST Seriablizable list. In the netScheme definition, if an attribute is defined as a list, the itemType should also be defined.
45 | */
46 |
47 |
48 | export default BaseTypes;
49 |
--------------------------------------------------------------------------------
/src/serialize/GameComponent.ts:
--------------------------------------------------------------------------------
1 | import { GameObject } from "./GameObject.js";
2 |
3 | export default class GameComponent {
4 | private parentObject: GameObject | null;
5 |
6 | constructor() {
7 | /**
8 | * the gameObject this component is attached to. This gets set in the addComponent method
9 | * @member {GameObject}
10 | */
11 | this.parentObject = null;
12 | }
13 |
14 | static get componentName() {
15 | return this.constructor.name;
16 | }
17 |
18 | netScheme() {
19 | return null;
20 | }
21 | }
--------------------------------------------------------------------------------
/src/serialize/GameObject.ts:
--------------------------------------------------------------------------------
1 | import Serializable from './Serializable.js';
2 | import BaseTypes from './BaseTypes.js';
3 | import { GameEngine } from '../GameEngine.js';
4 |
5 | type GameObjectOptions = { id?: number }
6 | type GameObjectProps = { playerId?: number }
7 |
8 | /**
9 | * GameObject is the base class of all game objects.
10 | * It is created only for the purpose of clearly defining the game
11 | * object interface.
12 | * Game developers will use one of the subclasses such as DynamicObject,
13 | * or PhysicalObject.
14 | */
15 | class GameObject extends Serializable {
16 |
17 | public id: number;
18 | protected gameEngine: GameEngine;
19 | public playerId: number;
20 | private components: { [key: string]: any }
21 | private savedCopy: GameObject | null;
22 | public refreshRenderObject?: () => void;
23 |
24 | netScheme() {
25 | return {
26 | id: { type: BaseTypes.Float32 },
27 | playerId: { type: BaseTypes.Int16 }
28 | };
29 | }
30 |
31 | /**
32 | * Creates an instance of a game object.
33 | * @param {GameEngine} gameEngine - the gameEngine this object will be used in
34 | * @param {Number} id - if set, the new instantiated object will be set to this id instead of being generated a new one. Use with caution!
35 | * @param {Object} props - additional properties for creation
36 | * @param {Number} props.playerId - the playerId value of the player who owns this object
37 | */
38 | constructor(gameEngine: GameEngine, options: GameObjectOptions, props: GameObjectProps) {
39 | super();
40 | /**
41 | * The gameEngine this object will be used in
42 | * @member {GameEngine}
43 | */
44 | this.gameEngine = gameEngine;
45 |
46 | /**
47 | * ID of this object's instance.
48 | * There are three cases of instance creation which can occur:
49 | * 1. In the normal case, the constructor is asked to assign an ID which is unique
50 | * across the entire game world, including the server and all the clients.
51 | * 2. In extrapolation mode, the client may have an object instance which does not
52 | * yet exist on the server, these objects are known as shadow objects. Their IDs must
53 | * be allocated from a different range.
54 | * 3. Also, temporary objects are created on the client side each time a sync is received.
55 | * These are used for interpolation purposes and as bending targets of position, velocity,
56 | * angular velocity, and orientation. In this case the id will be set to null.
57 | * @member {Number}
58 | */
59 | if (options && options.id)
60 | this.id = options.id;
61 | else
62 | this.id = this.gameEngine.world.getNewId();
63 |
64 | /**
65 | * playerId of player who created this object
66 | * @member {Number}
67 | */
68 | this.playerId = (props && props.playerId) ? props.playerId : 0;
69 |
70 | this.components = {};
71 | }
72 |
73 | /**
74 | * Called after the object is added to to the game world.
75 | * This is the right place to add renderer sub-objects, physics sub-objects
76 | * and any other resources that should be created
77 | * @param {GameEngine} gameEngine the game engine
78 | */
79 | onAddToWorld(gameEngine: GameEngine) {}
80 |
81 | /**
82 | * Called after the object is removed from game-world.
83 | * This is where renderer sub-objects and any other resources should be freed
84 | * @param {GameEngine} gameEngine the game engine
85 | */
86 | onRemoveFromWorld(gameEngine: GameEngine) {}
87 |
88 | /**
89 | * Formatted textual description of the game object.
90 | * @return {String} description - a string description
91 | */
92 | toString() {
93 | return `game-object[${this.id}]`;
94 | }
95 |
96 | /**
97 | * Formatted textual description of the game object's current bending properties.
98 | * @return {String} description - a string description
99 | */
100 | bendingToString() {
101 | return 'no bending';
102 | }
103 |
104 | saveState(other?: GameObject) {
105 | this.savedCopy = (new ( this.constructor)(this.gameEngine, { id: null }));
106 | this.savedCopy?.syncTo(other ? other : this);
107 | }
108 | /**
109 | * Bending is defined as the amount of error correction that will be applied
110 | * on the client side to a given object's physical attributes, incrementally,
111 | * by the time the next server broadcast is expected to arrive.
112 | *
113 | * When this percentage is 0.0, the client always ignores the server object's value.
114 | * When this percentage is 1.0, the server object's attributes will be applied in full.
115 | *
116 | * The GameObject bending attribute is implemented as a getter, and can provide
117 | * distinct values for position, velocity, angle, and angularVelocity.
118 | * And in each case, you can also provide overrides for local objects,
119 | * these attributes will be called, respectively, positionLocal, velocityLocal,
120 | * angleLocal, angularVelocityLocal.
121 | *
122 | * @example
123 | * get bending() {
124 | * return {
125 | * position: { percent: 1.0, min: 0.0 },
126 | * velocity: { percent: 0.0, min: 0.0 },
127 | * angularVelocity: { percent: 0.0 },
128 | * angleLocal: { percent: 1.0 }
129 | * }
130 | * };
131 | *
132 | * @memberof GameObject
133 | * @member {Object} bending
134 | */
135 | get bending() : any {
136 | return {};
137 | }
138 |
139 | // TODO:
140 | // rather than pass worldSettings on each bend, they could
141 | // be passed in on the constructor just once.
142 | bendToCurrentState(bending: number, worldSettings: any, isLocal: boolean, bendingIncrements: number) {
143 | if (this.savedCopy) {
144 | this.bendToCurrent(this.savedCopy, bending, worldSettings, isLocal, bendingIncrements);
145 | }
146 | this.savedCopy = null;
147 | }
148 |
149 | bendToCurrent(original: GameObject, bending: number, worldSettings: any, isLocal: boolean, bendingIncrements: number) {
150 | }
151 |
152 | /**
153 | * synchronize this object to the state of an other object, by copying all the netscheme variables.
154 | * This is used by the synchronizer to create temporary objects, and must be implemented by all sub-classes as well.
155 | * @param {GameObject} other the other object to synchronize to
156 | */
157 | syncTo(other: GameObject) {
158 | super.syncTo(other);
159 | this.playerId = other.playerId;
160 | }
161 |
162 | // copy physical attributes to physics sub-object
163 | refreshToPhysics() {}
164 |
165 | // copy physical attributes from physics sub-object
166 | refreshFromPhysics() {}
167 |
168 | // apply a single bending increment
169 | applyIncrementalBending(stepDesc: any) { }
170 |
171 | // clean up resources
172 | destroy() {}
173 |
174 | addComponent(componentInstance: any) {
175 | componentInstance.parentObject = this;
176 | this.components[componentInstance.constructor.name] = componentInstance;
177 |
178 | // a gameEngine might not exist if this class is instantiated by the serializer
179 | if (this.gameEngine) {
180 | this.gameEngine.emit('componentAdded', this, componentInstance);
181 | }
182 | }
183 |
184 | removeComponent(componentName) {
185 | // todo cleanup of the component ?
186 | delete this.components[componentName];
187 |
188 | // a gameEngine might not exist if this class is instantiated by the serializer
189 | if (this.gameEngine) {
190 | this.gameEngine.emit('componentRemoved', this, componentName);
191 | }
192 | }
193 |
194 | /**
195 | * Check whether this game object has a certain component
196 | * @param {Object} componentClass the comp
197 | * @return {Boolean} true if the gameObject contains this component
198 | */
199 | hasComponent(componentClass) {
200 | return componentClass.name in this.components;
201 | }
202 |
203 | getComponent(componentClass) {
204 | return this.components[componentClass.name];
205 | }
206 |
207 | }
208 |
209 | export { GameObject, GameObjectOptions, GameObjectProps };
210 |
--------------------------------------------------------------------------------
/src/serialize/NetScheme.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The netScheme type defines the base types used in Lance.
3 | * These are the types which can be used to define an object's netscheme attributes,
4 | * which can be serialized by lance.
5 | * @example
6 | * netScheme() {
7 | * return {
8 | * strength: { type: BaseTypes.TYPES.FLOAT32 },
9 | * shield: { type: BaseTypes.TYPES.INT8 },
10 | * name: { type: BaseTypes.String },
11 | * backpack: { type: BaseTypes.ClassInstance },
12 | * coins: {
13 | * type: BaseTypes.List,
14 | * itemType: BaseTypes.TYPES.UINT8
15 | * }
16 | * };
17 | * }
18 | */
19 | type NetScheme = {
20 | [key: string] : { 'type': string, 'itemType'?: string }
21 | }
22 |
23 | export { NetScheme }
24 |
--------------------------------------------------------------------------------
/src/serialize/Quaternion.ts:
--------------------------------------------------------------------------------
1 | import Serializable from './Serializable.js';
2 | import BaseTypes from './BaseTypes.js';
3 | import { ThreeVector } from './ThreeVector.js';
4 |
5 | const SHOW_AS_AXIS_ANGLE = true;
6 | const MAX_DEL_THETA = 0.2;
7 |
8 | /**
9 | * A Quaternion is a geometric object which can be used to
10 | * represent a three-dimensional rotation.
11 | */
12 | class Quaternion extends Serializable {
13 | private w: number;
14 | private x: number;
15 | private y: number;
16 | private z: number;
17 |
18 | netScheme() {
19 | return {
20 | w: { type: BaseTypes.Float32 },
21 | x: { type: BaseTypes.Float32 },
22 | y: { type: BaseTypes.Float32 },
23 | z: { type: BaseTypes.Float32 }
24 | };
25 | }
26 |
27 | /**
28 | * Creates an instance of a Quaternion.
29 | * @param {Number} w - first value
30 | * @param {Number} x - second value
31 | * @param {Number} y - third value
32 | * @param {Number} z - fourth value
33 | * @return {Quaternion} v - the new Quaternion
34 | */
35 | constructor(w: number, x: number, y: number, z: number) {
36 | super();
37 | this.w = w;
38 | this.x = x;
39 | this.y = y;
40 | this.z = z;
41 |
42 | return this;
43 | }
44 |
45 | /**
46 | * Formatted textual description of the Quaternion.
47 | * @return {String} description
48 | */
49 | toString(): string {
50 | function round3(x) { return Math.round(x * 1000) / 1000; }
51 | if (SHOW_AS_AXIS_ANGLE) {
52 | let axisAngle = this.toAxisAngle();
53 | return `[${round3(axisAngle.angle)},${axisAngle.axis.toString()}]`;
54 | }
55 | return `[${round3(this.w)}, ${round3(this.x)}, ${round3(this.y)}, ${round3(this.z)}]`;
56 | }
57 |
58 | /**
59 | * copy values from another quaternion into this quaternion
60 | *
61 | * @param {Quaternion} sourceObj the quaternion to copy from
62 | * @return {Quaternion} returns self
63 | */
64 | copy(sourceObj: Quaternion): this {
65 | this.set(sourceObj.w, sourceObj.x, sourceObj.y, sourceObj.z);
66 | return this;
67 | }
68 |
69 | /**
70 | * set quaternion values
71 | *
72 | * @param {Number} w w-value
73 | * @param {Number} x x-value
74 | * @param {Number} y y-value
75 | * @param {Number} z z-value
76 | * @return {Quaternion} returns self
77 | */
78 | set(w: number, x: number, y: number, z: number): this {
79 | this.w = w;
80 | this.x = x;
81 | this.y = y;
82 | this.z = z;
83 |
84 | return this;
85 | }
86 |
87 | /**
88 | * return an axis-angle representation of this quaternion
89 | *
90 | * @return {Object} contains two attributes: axis (ThreeVector) and angle.
91 | */
92 | toAxisAngle() : { axis: ThreeVector, angle: number} {
93 |
94 | // assuming quaternion normalised then w is less than 1, so term always positive.
95 | let axis = new ThreeVector(1, 0, 0);
96 | this.normalize();
97 | let angle = 2 * Math.acos(this.w);
98 | let s = Math.sqrt(1 - this.w * this.w);
99 | if (s > 0.001) {
100 | let divS = 1 / s;
101 | axis.x = this.x * divS;
102 | axis.y = this.y * divS;
103 | axis.z = this.z * divS;
104 | }
105 | if (s > Math.PI) {
106 | s -= 2 * Math.PI;
107 | }
108 | return { axis, angle };
109 | }
110 |
111 | normalize(): this {
112 | let l = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w);
113 | if (l === 0) {
114 | this.x = 0;
115 | this.y = 0;
116 | this.z = 0;
117 | this.w = 0;
118 | } else {
119 | l = 1 / l;
120 | this.x *= l;
121 | this.y *= l;
122 | this.z *= l;
123 | this.w *= l;
124 | }
125 |
126 | return this;
127 | }
128 |
129 | /**
130 | * set the values of this quaternion from an axis/angle representation
131 | *
132 | * @param {ThreeVector} axis The axis
133 | * @param {Number} angle angle in radians
134 | * @return {Quaternion} returns self
135 | */
136 | setFromAxisAngle(axis: ThreeVector, angle: number): this {
137 |
138 | if (angle < 0)
139 | angle += Math.PI * 2;
140 | let halfAngle = angle * 0.5;
141 | let s = Math.sin(halfAngle);
142 | this.x = axis.x * s;
143 | this.y = axis.y * s;
144 | this.z = axis.z * s;
145 | this.w = Math.cos(halfAngle);
146 |
147 | return this;
148 | }
149 |
150 | /**
151 | * conjugate the quaternion, in-place
152 | *
153 | * @return {Quaternion} returns self
154 | */
155 | conjugate(): this {
156 | this.x *= -1;
157 | this.y *= -1;
158 | this.z *= -1;
159 | return this;
160 | }
161 |
162 | /* eslint-disable */
163 | /**
164 | * multiply this quaternion by another, in-place
165 | *
166 | * @param {Quaternion} other The other quaternion
167 | * @return {Quaternion} returns self
168 | */
169 | multiply(other: Quaternion): this {
170 | let aw = this.w, ax = this.x, ay = this.y, az = this.z;
171 | let bw = other.w, bx = other.x, by = other.y, bz = other.z;
172 |
173 | this.x = ax * bw + aw * bx + ay * bz - az * by;
174 | this.y = ay * bw + aw * by + az * bx - ax * bz;
175 | this.z = az * bw + aw * bz + ax * by - ay * bx;
176 | this.w = aw * bw - ax * bx - ay * by - az * bz;
177 |
178 | return this;
179 | }
180 | /* eslint-enable */
181 |
182 | /* eslint-disable */
183 | /**
184 | * Apply in-place slerp (spherical linear interpolation) to this quaternion,
185 | * towards another quaternion.
186 | *
187 | * @param {Quaternion} target The target quaternion
188 | * @param {Number} bending The percentage to interpolate
189 | * @return {Quaternion} returns self
190 | */
191 | slerp(target: Quaternion, bending: number): this {
192 |
193 | if (bending <= 0) return this;
194 | if (bending >= 1) return this.copy(target);
195 |
196 | let aw = this.w, ax = this.x, ay = this.y, az = this.z;
197 | let bw = target.w, bx = target.x, by = target.y, bz = target.z;
198 |
199 | let cosHalfTheta = aw*bw + ax*bx + ay*by + az*bz;
200 | if (cosHalfTheta < 0) {
201 | this.set(-bw, -bx, -by, -bz);
202 | cosHalfTheta = -cosHalfTheta;
203 | } else {
204 | this.copy(target);
205 | }
206 |
207 | if (cosHalfTheta >= 1.0) {
208 | this.set(aw, ax, ay, az);
209 | return this;
210 | }
211 |
212 | let sqrSinHalfTheta = 1.0 - cosHalfTheta*cosHalfTheta;
213 | if (sqrSinHalfTheta < Number.EPSILON) {
214 | let s = 1 - bending;
215 | this.set(s*aw + bending*this.w, s*ax + bending*this.x, s*ay + bending*this.y, s*az + bending*this.z);
216 | return this.normalize();
217 | }
218 |
219 | let sinHalfTheta = Math.sqrt(sqrSinHalfTheta);
220 | let halfTheta = Math.atan2(sinHalfTheta, cosHalfTheta);
221 | let delTheta = bending * halfTheta;
222 | if (Math.abs(delTheta) > MAX_DEL_THETA)
223 | delTheta = MAX_DEL_THETA * Math.sign(delTheta);
224 | let ratioA = Math.sin(halfTheta - delTheta)/sinHalfTheta;
225 | let ratioB = Math.sin(delTheta)/sinHalfTheta;
226 | this.set(aw*ratioA + this.w*ratioB,
227 | ax*ratioA + this.x*ratioB,
228 | ay*ratioA + this.y*ratioB,
229 | az*ratioA + this.z*ratioB);
230 | return this;
231 | }
232 | /* eslint-enable */
233 | }
234 |
235 | export default Quaternion;
236 |
--------------------------------------------------------------------------------
/src/serialize/Serializable.ts:
--------------------------------------------------------------------------------
1 | import Utils from '../lib/Utils.js';
2 | import BaseTypes from './BaseTypes.js';
3 | import { NetScheme } from './NetScheme.js';
4 | import Serializer from './Serializer.js';
5 |
6 |
7 | type SerializableOptions = {
8 | dataBuffer: any,
9 | bufferOffset: number,
10 | dry: boolean
11 | }
12 |
13 | type SerializedObj = {
14 | dataBuffer: ArrayBuffer,
15 | bufferOffset: number
16 | }
17 |
18 | class Serializable {
19 |
20 | public classId: number;
21 |
22 | constructor() {}
23 |
24 | netScheme(): NetScheme {
25 | return {};
26 | }
27 |
28 | /**
29 | * Class can be serialized using either:
30 | * - a class based netScheme
31 | * - an instance based netScheme
32 | * - completely dynamically (not implemented yet)
33 | *
34 | * @param {Object} serializer - Serializer instance
35 | * @param {Object} [options] - Options object
36 | * @param {Object} options.dataBuffer [optional] - Data buffer to write to. If null a new data buffer will be created
37 | * @param {Number} options.bufferOffset [optional] - The buffer data offset to start writing at. Default: 0
38 | * @param {Boolean} options.dry [optional] - Does not actually write to the buffer (useful to gather serializeable size)
39 | * @return {Object} the serialized object. Contains attributes: dataBuffer - buffer which contains the serialized data; bufferOffset - offset where the serialized data starts.
40 | */
41 | serialize(serializer: Serializer, options: SerializableOptions): SerializedObj {
42 |
43 | options = Object.assign({
44 | bufferOffset: 0
45 | }, options);
46 |
47 | let netScheme;
48 | let dataBuffer;
49 | let dataView;
50 | let classId = 0;
51 | let bufferOffset = options.bufferOffset;
52 | let localBufferOffset = 0; // used for counting the bufferOffset
53 |
54 | // instance classId
55 | // TODO: when is this.classId ever populated?
56 | if (this.classId) {
57 | classId = this.classId;
58 | } else {
59 | classId = Utils.hashStr(this.constructor.name);
60 | }
61 |
62 | netScheme = this.netScheme();
63 |
64 | // TODO: currently we serialize every node twice, once to calculate the size
65 | // of the buffers and once to write them out. This can be reduced to
66 | // a single pass by starting with a large (and static) ArrayBuffer and
67 | // recursively building it up.
68 | // buffer has one Uint8Array for class id, then payload
69 | if (options.dataBuffer == null && options.dry != true) {
70 | let bufferSize = this.serialize(serializer, { dry: true, dataBuffer: null, bufferOffset: 0 }).bufferOffset;
71 | dataBuffer = new ArrayBuffer(bufferSize);
72 | } else {
73 | dataBuffer = options.dataBuffer;
74 | }
75 |
76 | if (options.dry != true) {
77 | dataView = new DataView(dataBuffer);
78 | // first set the id of the class, so that the deserializer can fetch information about it
79 | dataView.setUint8(bufferOffset + localBufferOffset, classId);
80 | }
81 |
82 | // advance the offset counter
83 | localBufferOffset += Uint8Array.BYTES_PER_ELEMENT;
84 |
85 | if (netScheme) {
86 | for (let property of Object.keys(netScheme).sort()) {
87 |
88 | // write the property to buffer
89 | if (options.dry != true) {
90 | serializer.writeDataView(dataView, this[property], bufferOffset + localBufferOffset, netScheme[property]);
91 | }
92 |
93 | if (netScheme[property].type === BaseTypes.String) {
94 | // derive the size of the string
95 | localBufferOffset += Uint16Array.BYTES_PER_ELEMENT;
96 | if (this[property] !== null && this[property] !== undefined)
97 | localBufferOffset += this[property].length * Uint16Array.BYTES_PER_ELEMENT;
98 | } else if (netScheme[property].type === BaseTypes.ClassInstance) {
99 | // derive the size of the included class
100 | let objectInstanceBufferOffset = this[property].serialize(serializer, { dry: true }).bufferOffset;
101 | localBufferOffset += objectInstanceBufferOffset;
102 | } else if (netScheme[property].type === BaseTypes.List) {
103 | // derive the size of the list
104 | // list starts with number of elements
105 | localBufferOffset += Uint16Array.BYTES_PER_ELEMENT;
106 |
107 | for (let item of this[property]) {
108 | // todo inelegant, currently doesn't support list of lists
109 | if (netScheme[property].itemType === BaseTypes.ClassInstance) {
110 | let listBufferOffset = item.serialize(serializer, { dry: true }).bufferOffset;
111 | localBufferOffset += listBufferOffset;
112 | } else if (netScheme[property].itemType === BaseTypes.String) {
113 | // size includes string length plus double-byte characters
114 | localBufferOffset += Uint16Array.BYTES_PER_ELEMENT * (1 + item.length);
115 | } else {
116 | localBufferOffset += serializer.getTypeByteSize(netScheme[property].itemType);
117 | }
118 | }
119 | } else {
120 | // advance offset
121 | localBufferOffset += serializer.getTypeByteSize(netScheme[property].type);
122 | }
123 |
124 | }
125 | } else {
126 | // TODO no netScheme, dynamic class
127 | }
128 |
129 | return { dataBuffer, bufferOffset: localBufferOffset };
130 | }
131 |
132 | // build a clone of this object with pruned strings (if necessary)
133 | prunedStringsClone(serializer: Serializer, prevObject: Serializable) {
134 |
135 | if (!prevObject) return this;
136 | prevObject = serializer.deserialize(prevObject).obj;
137 |
138 | // get list of string properties which changed
139 | // @ts-ignore
140 | let netScheme = this.netScheme();
141 | let isString = p => netScheme[p].type === BaseTypes.String;
142 | let hasChanged = p => prevObject[p] !== this[p];
143 | let changedStrings = Object.keys(netScheme).filter(isString).filter(hasChanged);
144 | if (changedStrings.length == 0) return this;
145 |
146 | // build a clone with pruned strings
147 | let objectClass = serializer.registeredClasses[this.classId];
148 | let prunedCopy = new objectClass(null, { id: null });
149 | // let prunedCopy = new this.constructor(null, { id: null });
150 | for (let p of Object.keys(netScheme))
151 | prunedCopy[p] = changedStrings.indexOf(p) < 0 ? this[p] : null;
152 |
153 | return prunedCopy;
154 | }
155 |
156 | syncTo(other: Serializable) {
157 | let netScheme = this.netScheme();
158 | for (let p of Object.keys(netScheme)) {
159 |
160 | // ignore classes and lists
161 | if (netScheme[p].type === BaseTypes.List || netScheme[p].type === BaseTypes.ClassInstance)
162 | continue;
163 |
164 | // strings might be pruned
165 | if (netScheme[p].type === BaseTypes.String) {
166 | if (typeof other[p] === 'string') this[p] = other[p];
167 | continue;
168 | }
169 |
170 | // all other values are copied
171 | this[p] = other[p];
172 | }
173 | }
174 |
175 | }
176 |
177 | export default Serializable;
178 |
--------------------------------------------------------------------------------
/src/serialize/ThreeVector.ts:
--------------------------------------------------------------------------------
1 | import Serializable from './Serializable.js';
2 | import BaseTypes from './BaseTypes.js';
3 | import { TwoVectorBendingOptions } from './TwoVector.js';
4 |
5 | interface ThreeVectorBendingOptions extends TwoVectorBendingOptions {};
6 |
7 | /**
8 | * A ThreeVector is a geometric object which is completely described
9 | * by three values.
10 | */
11 | class ThreeVector extends Serializable {
12 |
13 | public x: number;
14 | public y: number;
15 | public z: number;
16 |
17 | netScheme() {
18 | return {
19 | x: { type: BaseTypes.Float32 },
20 | y: { type: BaseTypes.Float32 },
21 | z: { type: BaseTypes.Float32 }
22 | };
23 | }
24 |
25 | /**
26 | * Creates an instance of a ThreeVector.
27 | * @param {Number} x - first value
28 | * @param {Number} y - second value
29 | * @param {Number} z - second value
30 | * @return {ThreeVector} v - the new ThreeVector
31 | */
32 | constructor(x: number, y: number, z: number) {
33 | super();
34 | this.x = x;
35 | this.y = y;
36 | this.z = z;
37 |
38 | return this;
39 | }
40 |
41 | /**
42 | * Formatted textual description of the ThreeVector.
43 | * @return {String} description
44 | */
45 | toString(): string {
46 | function round3(x) { return Math.round(x * 1000) / 1000; }
47 | return `[${round3(this.x)}, ${round3(this.y)}, ${round3(this.z)}]`;
48 | }
49 |
50 | /**
51 | * Multiply this ThreeVector by a scalar
52 | *
53 | * @param {Number} s the scale
54 | * @return {ThreeVector} returns self
55 | */
56 | multiplyScalar(s: number): ThreeVector {
57 | this.x *= s;
58 | this.y *= s;
59 | this.z *= s;
60 | return this;
61 | }
62 |
63 | /**
64 | * Get vector length
65 | *
66 | * @return {Number} length of this vector
67 | */
68 | length(): number {
69 | return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
70 | }
71 |
72 | /**
73 | * Add other vector to this vector
74 | *
75 | * @param {ThreeVector} other the other vector
76 | * @return {ThreeVector} returns self
77 | */
78 | add(other: ThreeVector): ThreeVector {
79 | this.x += other.x;
80 | this.y += other.y;
81 | this.z += other.z;
82 | return this;
83 | }
84 |
85 | /**
86 | * Subtract other vector from this vector
87 | *
88 | * @param {ThreeVector} other the other vector
89 | * @return {ThreeVector} returns self
90 | */
91 | subtract(other: ThreeVector): ThreeVector {
92 | this.x -= other.x;
93 | this.y -= other.y;
94 | this.z -= other.z;
95 | return this;
96 | }
97 |
98 | /**
99 | * Normalize this vector, in-place
100 | *
101 | * @return {ThreeVector} returns self
102 | */
103 | normalize(): ThreeVector {
104 | this.multiplyScalar(1 / this.length());
105 | return this;
106 | }
107 |
108 | /**
109 | * Copy values from another ThreeVector into this ThreeVector
110 | *
111 | * @param {ThreeVector} sourceObj the other vector
112 | * @return {ThreeVector} returns self
113 | */
114 | copy(sourceObj: ThreeVector): ThreeVector {
115 | this.x = sourceObj.x;
116 | this.y = sourceObj.y;
117 | this.z = sourceObj.z;
118 | return this;
119 | }
120 |
121 | /**
122 | * Set ThreeVector values
123 | *
124 | * @param {Number} x x-value
125 | * @param {Number} y y-value
126 | * @param {Number} z z-value
127 | * @return {ThreeVector} returns self
128 | */
129 | set(x: number, y: number, z: number): ThreeVector {
130 | this.x = x;
131 | this.y = y;
132 | this.z = z;
133 | return this;
134 | }
135 |
136 | /**
137 | * Create a clone of this vector
138 | *
139 | * @return {ThreeVector} returns clone
140 | */
141 | clone(): ThreeVector {
142 | return new ThreeVector(this.x, this.y, this.z);
143 | }
144 |
145 | /**
146 | * Apply in-place lerp (linear interpolation) to this ThreeVector
147 | * towards another ThreeVector
148 | * @param {ThreeVector} target the target vector
149 | * @param {Number} p The percentage to interpolate
150 | * @return {ThreeVector} returns self
151 | */
152 | lerp(target: ThreeVector, p: number): ThreeVector {
153 | this.x += (target.x - this.x) * p;
154 | this.y += (target.y - this.y) * p;
155 | this.z += (target.z - this.z) * p;
156 | return this;
157 | }
158 |
159 | /**
160 | * Get bending Delta Vector
161 | * towards another ThreeVector
162 | * @param {ThreeVector} target the target vector
163 | * @param {Object} options bending options
164 | * @param {Number} options.increments number of increments
165 | * @param {Number} options.percent The percentage to bend
166 | * @param {Number} options.min No less than this value
167 | * @param {Number} options.max No more than this value
168 | * @return {ThreeVector} returns new Incremental Vector
169 | */
170 | getBendingDelta(target: ThreeVector, options: ThreeVectorBendingOptions): ThreeVector {
171 | let increment = target.clone();
172 | increment.subtract(this);
173 | increment.multiplyScalar(options.percent);
174 |
175 | // check for max case
176 | if ((options.max && increment.length() > options.max) ||
177 | (options.max && increment.length() < options.min)) {
178 | return new ThreeVector(0, 0, 0);
179 | }
180 |
181 | // divide into increments
182 | increment.multiplyScalar(1 / options.increments);
183 |
184 | return increment;
185 | }
186 | }
187 |
188 | export { ThreeVector, ThreeVectorBendingOptions };
189 |
--------------------------------------------------------------------------------
/src/serialize/TwoVector.ts:
--------------------------------------------------------------------------------
1 | import Serializable from './Serializable.js';
2 | import BaseTypes from './BaseTypes.js';
3 |
4 | interface TwoVectorBendingOptions {
5 | increments: number;
6 | percent: number;
7 | min: number;
8 | max: number;
9 | }
10 |
11 | /**
12 | * A TwoVector is a geometric object which is completely described
13 | * by two values.
14 | */
15 | class TwoVector extends Serializable {
16 | public x: number; // TODO: instead of public, have it getter (readonly)
17 | public y: number;
18 |
19 | netScheme() {
20 | return {
21 | x: { type: BaseTypes.Float32 },
22 | y: { type: BaseTypes.Float32 }
23 | };
24 | }
25 |
26 | /**
27 | * Creates an instance of a TwoVector.
28 | * @param {Number} x - first value
29 | * @param {Number} y - second value
30 | * @return {TwoVector} v - the new TwoVector
31 | */
32 | constructor(x: number, y: number) {
33 | super();
34 | this.x = x;
35 | this.y = y;
36 |
37 | return this;
38 | }
39 |
40 | /**
41 | * Formatted textual description of the TwoVector.
42 | * @return {String} description
43 | */
44 | toString(): string {
45 | function round3(x) { return Math.round(x * 1000) / 1000; }
46 | return `[${round3(this.x)}, ${round3(this.y)}]`;
47 | }
48 |
49 | /**
50 | * Set TwoVector values
51 | *
52 | * @param {Number} x x-value
53 | * @param {Number} y y-value
54 | * @return {TwoVector} returns self
55 | */
56 | set(x: number, y: number): this {
57 | this.x = x;
58 | this.y = y;
59 | return this;
60 | }
61 |
62 | multiply(other: TwoVector): this {
63 | this.x *= other.x;
64 | this.y *= other.y;
65 |
66 | return this;
67 | }
68 |
69 | /**
70 | * Multiply this TwoVector by a scalar
71 | *
72 | * @param {Number} s the scale
73 | * @return {TwoVector} returns self
74 | */
75 | multiplyScalar(s: number): this {
76 | this.x *= s;
77 | this.y *= s;
78 |
79 | return this;
80 | }
81 |
82 | /**
83 | * Add other vector to this vector
84 | *
85 | * @param {TwoVector} other the other vector
86 | * @return {TwoVector} returns self
87 | */
88 | add(other: TwoVector): this {
89 | this.x += other.x;
90 | this.y += other.y;
91 |
92 | return this;
93 | }
94 |
95 | /**
96 | * Subtract other vector to this vector
97 | *
98 | * @param {TwoVector} other the other vector
99 | * @return {TwoVector} returns self
100 | */
101 | subtract(other: TwoVector): this {
102 | this.x -= other.x;
103 | this.y -= other.y;
104 |
105 | return this;
106 | }
107 |
108 | /**
109 | * Get vector length
110 | *
111 | * @return {Number} length of this vector
112 | */
113 | length(): number {
114 | return Math.sqrt(this.x * this.x + this.y * this.y);
115 | }
116 |
117 | /**
118 | * Normalize this vector, in-place
119 | *
120 | * @return {TwoVector} returns self
121 | */
122 | normalize(): this {
123 | this.multiplyScalar(1 / this.length());
124 | return this;
125 | }
126 |
127 | /**
128 | * Copy values from another TwoVector into this TwoVector
129 | *
130 | * @param {TwoVector} sourceObj the other vector
131 | * @return {TwoVector} returns self
132 | */
133 | copy(sourceObj: TwoVector): this {
134 | this.x = sourceObj.x;
135 | this.y = sourceObj.y;
136 |
137 | return this;
138 | }
139 |
140 | /**
141 | * Create a clone of this vector
142 | *
143 | * @return {TwoVector} returns clone
144 | */
145 | clone(): TwoVector {
146 | return new TwoVector(this.x, this.y);
147 | }
148 |
149 | /**
150 | * Apply in-place lerp (linear interpolation) to this TwoVector
151 | * towards another TwoVector
152 | * @param {TwoVector} target the target vector
153 | * @param {Number} p The percentage to interpolate
154 | * @return {TwoVector} returns self
155 | */
156 | lerp(target: TwoVector, p: number): this {
157 | this.x += (target.x - this.x) * p;
158 | this.y += (target.y - this.y) * p;
159 |
160 | return this;
161 | }
162 |
163 | /**
164 | * Get bending Delta Vector
165 | * towards another TwoVector
166 | * @param {TwoVector} target the target vector
167 | * @param {Object} options bending options
168 | * @param {Number} options.increments number of increments
169 | * @param {Number} options.percent The percentage to bend
170 | * @param {Number} options.min No less than this value
171 | * @param {Number} options.max No more than this value
172 | * @return {TwoVector} returns new Incremental Vector
173 | */
174 | getBendingDelta(target: TwoVector, options: TwoVectorBendingOptions): TwoVector {
175 | let increment = target.clone();
176 | increment.subtract(this);
177 | increment.multiplyScalar(options.percent);
178 |
179 | // check for max case
180 | if (((typeof options.max === 'number') && increment.length() > options.max) ||
181 | ((typeof options.min === 'number') && increment.length() < options.min)) {
182 | return new TwoVector(0, 0);
183 | }
184 |
185 | // divide into increments
186 | increment.multiplyScalar(1 / options.increments);
187 |
188 | return increment;
189 | }
190 | }
191 |
192 | export { TwoVector, TwoVectorBendingOptions };
193 |
--------------------------------------------------------------------------------
/src/syncStrategies/FrameSyncStrategy.ts:
--------------------------------------------------------------------------------
1 | import { ClientEngine } from '../ClientEngine.js';
2 | import { GameWorld } from '../GameWorld.js';
3 | import NetworkTransmitter from '../network/NetworkTransmitter.js';
4 | import { Sync, SyncStrategy, SyncStrategyOptions } from './SyncStrategy.js';
5 |
6 |
7 | const defaults = {
8 | worldBufferLength: 60,
9 | clientStepLag: 0
10 | };
11 |
12 | class FrameSyncStrategy extends SyncStrategy {
13 |
14 | constructor(options: SyncStrategyOptions) {
15 | super(options);
16 | }
17 |
18 | // apply a new sync
19 | applySync(sync: Sync, required: boolean) {
20 |
21 | this.needFirstSync = false;
22 | this.gameEngine.trace.debug(() => 'framySync applying sync');
23 | let world = this.gameEngine.world;
24 |
25 | for (let ids of Object.keys(sync.syncObjects)) {
26 | let ev = sync.syncObjects[ids][0];
27 | let curObj = world.objects[ev.objectInstance.id];
28 | if (curObj) {
29 | curObj.syncTo(ev.objectInstance);
30 | } else {
31 | this.addNewObject(ev.objectInstance.id, ev.objectInstance);
32 | }
33 | }
34 |
35 | // destroy objects
36 | for (let objIdStr of Object.keys(world.objects)) {
37 |
38 | let objId = Number(objIdStr);
39 | let objEvents = sync.syncObjects[objId];
40 |
41 | // if this was a full sync, and we did not get a corresponding object,
42 | // remove the local object
43 | if (sync.fullUpdate && !objEvents && objId < this.gameEngine.options.clientIDSpace) {
44 | this.gameEngine.removeObjectFromWorld(objId);
45 | continue;
46 | }
47 |
48 | if (!objEvents || objId >= this.gameEngine.options.clientIDSpace)
49 | continue;
50 |
51 | // if we got an objectDestroy event, destroy the object
52 | objEvents.forEach((e) => {
53 | if (NetworkTransmitter.getNetworkEvent(e) == 'objectDestroy') this.gameEngine.removeObjectFromWorld(objId);
54 | });
55 | }
56 |
57 | return SyncStrategy.SYNC_APPLIED;
58 | }
59 |
60 | }
61 |
62 | export { FrameSyncStrategy }
63 |
--------------------------------------------------------------------------------
/src/syncStrategies/InterpolateStrategy.ts:
--------------------------------------------------------------------------------
1 | import { ClientEngine } from '../ClientEngine.js';
2 | import { GameWorld } from '../GameWorld.js';
3 | import NetworkTransmitter from '../network/NetworkTransmitter.js';
4 | import { Sync, SyncStrategy, SyncStrategyOptions } from './SyncStrategy.js';
5 |
6 | const defaults = {
7 | clientStepHold: 6,
8 | localObjBending: 1.0, // amount of bending towards position of sync object
9 | remoteObjBending: 1.0, // amount of bending towards position of sync object
10 | bendingIncrements: 6, // the bending should be applied increments (how many steps for entire bend)
11 | reflect: false
12 | };
13 |
14 | interface InterpolateSyncStrategyOptions extends SyncStrategyOptions {
15 | localObjBending: number;
16 | remoteObjBending: number;
17 | bendingIncrements: number;
18 | }
19 |
20 | class InterpolateStrategy extends SyncStrategy {
21 |
22 | static STEP_DRIFT_THRESHOLDS = {
23 | onServerSync: { MAX_LEAD: -8, MAX_LAG: 16 }, // max step lead/lag allowed after every server sync
24 | onEveryStep: { MAX_LEAD: -4, MAX_LAG: 24 }, // max step lead/lag allowed at every step
25 | clientReset: 40 // if we are behind this many steps, just reset the step counter
26 | };
27 |
28 | private interpolateOptions: InterpolateSyncStrategyOptions;
29 |
30 | constructor(interpolateOptions: InterpolateSyncStrategyOptions) {
31 |
32 | super(interpolateOptions);
33 | this.interpolateOptions = Object.assign({}, defaults, interpolateOptions);
34 |
35 |
36 | this.gameEngine.ignoreInputs = true; // client side engine ignores inputs
37 | this.gameEngine.ignorePhysics = true; // client side engine ignores physics
38 | }
39 |
40 | // apply a new sync
41 | applySync(sync: Sync, required: boolean): string {
42 |
43 | // if sync is in the past we cannot interpolate to it
44 | if (!required && sync.stepCount <= this.gameEngine.world.stepCount) {
45 | return SyncStrategy.SYNC_APPLIED;
46 | }
47 |
48 | this.gameEngine.trace.debug(() => 'interpolate applying sync');
49 | //
50 | // scan all the objects in the sync
51 | //
52 | // 1. if the object exists locally, sync to the server object
53 | // 2. if the object is new, just create it
54 | //
55 | this.needFirstSync = false;
56 | let world: GameWorld = this.gameEngine.world;
57 | for (let ids of Object.keys(sync.syncObjects)) {
58 |
59 | // TODO: we are currently taking only the first event out of
60 | // the events that may have arrived for this object
61 | let ev = sync.syncObjects[ids][0];
62 | let curObj = world.objects[ev.objectInstance.id];
63 |
64 | if (curObj) {
65 |
66 | // case 1: this object already exists locally
67 | this.gameEngine.trace.trace(() => `object before syncTo: ${curObj.toString()}`);
68 | curObj.saveState();
69 | curObj.syncTo(ev.objectInstance);
70 | this.gameEngine.trace.trace(() => `object after syncTo: ${curObj.toString()} synced to step[${ev.stepCount}]`);
71 |
72 | } else {
73 |
74 | // case 2: object does not exist. create it now
75 | this.addNewObject(ev.objectInstance.id, ev.objectInstance);
76 | }
77 | }
78 |
79 | //
80 | // bend back to original state
81 | //
82 | for (let objId of Object.keys(world.objects)) {
83 |
84 | let obj = world.objects[objId];
85 | let isLocal = (obj.playerId == this.gameEngine.playerId); // eslint-disable-line eqeqeq
86 | let bending = isLocal ? this.interpolateOptions.localObjBending : this.interpolateOptions.remoteObjBending;
87 | obj.bendToCurrentState(bending, this.gameEngine.worldSettings, isLocal, this.interpolateOptions.bendingIncrements);
88 | if (typeof obj.refreshRenderObject === 'function')
89 | obj.refreshRenderObject();
90 | this.gameEngine.trace.trace(() => `object[${objId}] ${obj.bendingToString()}`);
91 | }
92 |
93 | // destroy objects
94 | // TODO: use world.forEachObject((id, ob) => {});
95 | // TODO: identical code is in InterpolateStrategy
96 |
97 | for (let objIdStr of Object.keys(world.objects)) {
98 |
99 | let objId = Number(objIdStr);
100 | let objEvents = sync.syncObjects[objId];
101 |
102 | // if this was a full sync, and we did not get a corresponding object,
103 | // remove the local object
104 | if (sync.fullUpdate && !objEvents && objId < this.gameEngine.options.clientIDSpace) {
105 | this.gameEngine.removeObjectFromWorld(objId);
106 | continue;
107 | }
108 |
109 | if (!objEvents || objId >= this.gameEngine.options.clientIDSpace)
110 | continue;
111 |
112 | // if we got an objectDestroy event, destroy the object
113 | objEvents.forEach((e) => {
114 | if (NetworkTransmitter.getNetworkEvent(e) == 'objectDestroy') this.gameEngine.removeObjectFromWorld(objId);
115 | });
116 | }
117 |
118 | return SyncStrategy.SYNC_APPLIED;
119 | }
120 | }
121 |
122 | export { InterpolateStrategy, InterpolateSyncStrategyOptions }
--------------------------------------------------------------------------------
/src/syncStrategies/SyncStrategy.ts:
--------------------------------------------------------------------------------
1 | import { ClientEngine } from '../ClientEngine.js';
2 | import { GameEngine } from '../GameEngine.js';
3 | import NetworkTransmitter from '../network/NetworkTransmitter.js';
4 | import Serializable from '../serialize/Serializable.js';
5 |
6 | interface SyncStrategyOptions {}
7 |
8 | type Sync = {
9 | stepCount: number,
10 | fullUpdate: boolean,
11 | required: boolean,
12 | syncObjects: { [key: number]: Serializable[] }, // all events in the sync indexed by the id of the object involved
13 | syncSteps: { [key: number]: { [key: string]: Serializable[] } } // all events in the sync indexed by the step on which they occurred
14 | };
15 |
16 | class SyncStrategy {
17 |
18 | protected clientEngine: ClientEngine;
19 | protected gameEngine: GameEngine;
20 | protected needFirstSync: boolean;
21 | private requiredSyncs: Sync[];
22 | protected lastSync: Sync | null;
23 | private options: SyncStrategyOptions;
24 | static SYNC_APPLIED = 'SYNC_APPLIED';
25 | static STEP_DRIFT_THRESHOLDS = {
26 | onServerSync: { MAX_LEAD: 1, MAX_LAG: 3 }, // max step lead/lag allowed after every server sync
27 | onEveryStep: { MAX_LEAD: 7, MAX_LAG: 8 }, // max step lead/lag allowed at every step
28 | clientReset: 20 // if we are behind this many steps, just reset the step counter
29 | };
30 |
31 | constructor(inputOptions: SyncStrategyOptions) {
32 | this.needFirstSync = true;
33 | this.options = Object.assign({}, inputOptions);
34 | this.requiredSyncs = [];
35 | }
36 |
37 | initClient(clientEngine: ClientEngine) {
38 | this.clientEngine = clientEngine;
39 | this.gameEngine = clientEngine.gameEngine;
40 | this.gameEngine.on('client__postStep', this.syncStep.bind(this));
41 | this.gameEngine.on('client__syncReceived', this.collectSync.bind(this));
42 | }
43 |
44 | // collect a sync and its events
45 | // maintain a "lastSync" member which describes the last sync we received from
46 | // the server. the lastSync object contains:
47 | // - syncObjects: all events in the sync indexed by the id of the object involved
48 | // - syncSteps: all events in the sync indexed by the step on which they occurred
49 | // - objCount
50 | // - eventCount
51 | // - stepCount
52 | collectSync(e) {
53 |
54 | // on first connect we need to wait for a full world update
55 | if (this.needFirstSync) {
56 | if (!e.fullUpdate)
57 | return;
58 | } else {
59 |
60 | // TODO: there is a problem below in the case where the client is 10 steps behind the server,
61 | // and the syncs that arrive are always in the future and never get processed. To address this
62 | // we may need to store more than one sync.
63 |
64 | // ignore syncs which are older than the latest
65 | if (this.lastSync && this.lastSync.stepCount && this.lastSync.stepCount > e.stepCount)
66 | return;
67 | }
68 |
69 | // before we overwrite the last sync, check if it was a required sync
70 | // syncs that create or delete objects are saved because they must be applied.
71 | if (this.lastSync && this.lastSync.required) {
72 | this.requiredSyncs.push(this.lastSync);
73 | }
74 |
75 | // build new sync object
76 | let lastSync: Sync = this.lastSync = {
77 | stepCount: e.stepCount,
78 | fullUpdate: e.fullUpdate,
79 | required: false,
80 | syncObjects: {},
81 | syncSteps: {}
82 | };
83 |
84 | e.syncEvents.forEach(sEvent => {
85 |
86 | // keep a reference of events by object id
87 | if (sEvent.objectInstance) {
88 | let objectId = sEvent.objectInstance.id;
89 | if (!lastSync.syncObjects[objectId]) lastSync.syncObjects[objectId] = [];
90 | lastSync.syncObjects[objectId].push(sEvent);
91 | }
92 |
93 | // keep a reference of events by step
94 | let stepCount = sEvent.stepCount;
95 | let eventName = NetworkTransmitter.getNetworkEvent(sEvent);
96 | if (eventName === 'objectDestroy' || eventName === 'objectCreate')
97 | lastSync.required = true;
98 |
99 | if (!lastSync.syncSteps[stepCount]) lastSync.syncSteps[stepCount] = {};
100 | if (!lastSync.syncSteps[stepCount][eventName]) lastSync.syncSteps[stepCount][eventName] = [];
101 | lastSync.syncSteps[stepCount][eventName].push(sEvent);
102 | });
103 |
104 | let eventCount = e.syncEvents.length;
105 | let objCount = (Object.keys(lastSync.syncObjects)).length;
106 | let stepCount = (Object.keys(lastSync.syncSteps)).length;
107 | this.gameEngine.trace.debug(() => `sync contains ${objCount} objects ${eventCount} events ${stepCount} steps`);
108 | }
109 |
110 | // add an object to our world
111 | addNewObject(objId, newObj) {
112 |
113 | let curObj = new newObj.constructor(this.gameEngine, {
114 | id: objId
115 | });
116 |
117 | // enforce object implementations of syncTo
118 | if (!curObj.__proto__.hasOwnProperty('syncTo')) {
119 | throw `GameObject of type ${curObj.class} does not implement the syncTo() method, which must copy the netscheme`;
120 | }
121 |
122 | curObj.syncTo(newObj);
123 | this.gameEngine.addObjectToWorld(curObj);
124 | if (this.clientEngine.options.verbose)
125 | console.log(`adding new object ${curObj}`);
126 |
127 | return curObj;
128 | }
129 |
130 | applySync(sync: Sync, required: boolean): string {
131 | return SyncStrategy.SYNC_APPLIED;
132 | }
133 |
134 | // sync to step, by applying bending, and applying the latest sync
135 | syncStep(stepDesc) {
136 |
137 | // apply incremental bending
138 | this.gameEngine.world.forEachObject((id, o) => {
139 | if (typeof o.applyIncrementalBending === 'function') {
140 | o.applyIncrementalBending(stepDesc);
141 | o.refreshToPhysics();
142 | }
143 | });
144 |
145 | // apply all pending required syncs
146 | while (this.requiredSyncs.length) {
147 |
148 | let requiredStep = this.requiredSyncs[0].stepCount;
149 |
150 | // if we haven't reached the corresponding step, it's too soon to apply syncs
151 | if (requiredStep > this.gameEngine.world.stepCount)
152 | return;
153 |
154 | this.gameEngine.trace.trace(() => `applying a required sync ${requiredStep}`);
155 | this.applySync(this.requiredSyncs[0], true);
156 | this.requiredSyncs.shift();
157 | }
158 |
159 | // apply the sync and delete it on success
160 | if (this.lastSync) {
161 | let rc = this.applySync(this.lastSync, false);
162 | if (rc === SyncStrategy.SYNC_APPLIED) // TODO: replace above return with a boolean
163 | this.lastSync = null;
164 | }
165 | }
166 | }
167 |
168 | export { SyncStrategy, SyncStrategyOptions, Sync }
--------------------------------------------------------------------------------
/test/EndToEnd/multiplayer.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const assert = require('chai').assert;
4 | const incheon = require('../../');
5 | const testGameServer = require('./testGame/server');
6 | const testGameClient = require('./testGame/client');
7 |
8 | let state = {
9 | server: null,
10 | clients: [],
11 | numClients: 0
12 | };
13 |
14 | // set all clients in a certain direction
15 | function setDirection(direction, value) {
16 | state.clients.forEach(c => {
17 | c.clientEngine.pressedKeys[direction] = value;
18 | });
19 | }
20 |
21 | describe('multiplayer-game', function() {
22 |
23 | it('start server', function(done) {
24 | let s = state.server = testGameServer.start();
25 | assert.instanceOf(s.gameEngine, incheon.GameEngine);
26 | assert.instanceOf(s.serverEngine, incheon.ServerEngine);
27 | assert.instanceOf(s.physicsEngine, incheon.physics.PhysicsEngine);
28 | done();
29 | });
30 |
31 | it('start five clients', function(done) {
32 | while (state.numClients < 5) {
33 | let c = state.clients[state.numClients++] = testGameClient.start();
34 | assert.instanceOf(c.gameEngine, incheon.GameEngine);
35 | assert.instanceOf(c.clientEngine, incheon.ClientEngine);
36 | assert.instanceOf(c.physicsEngine, incheon.physics.PhysicsEngine);
37 | }
38 | done();
39 | });
40 |
41 | it('everybody go up', function(done) {
42 | this.timeout(10000);
43 | setDirection('up', true);
44 | setTimeout(() => {
45 | setDirection('up', false);
46 | setDirection('down', true);
47 | setTimeout(() => {
48 | setDirection('down', false);
49 | done();
50 | }, 4000);
51 | }, 4000);
52 | });
53 |
54 | });
55 |
--------------------------------------------------------------------------------
/test/EndToEnd/testGame/client.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const incheon = require('../../../');
4 | const MyClientEngine = require('./src/client/MyClientEngine.js');
5 | const MyGameEngine = require('./src/common/MyGameEngine');
6 | const SimplePhysicsEngine = incheon.physics.SimplePhysicsEngine;
7 | const search = (typeof location === 'undefined') ? {} : location.search;
8 | const qsOptions = require('query-string').parse(search);
9 |
10 | // default options, overwritten by query-string options
11 | // is sent to both game engine and client engine
12 | const defaults = {
13 | traceLevel: 1,
14 | delayInputCount: 3,
15 | clientIDSpace: 1000000,
16 | syncOptions: {
17 | sync: qsOptions.sync || 'extrapolate',
18 | localObjBending: 0.0,
19 | remoteObjBending: 0.8,
20 | bendingIncrements: 6
21 | }
22 | };
23 | let options = Object.assign(defaults, qsOptions);
24 |
25 | // create a client engine and a game engine
26 | const physicsEngine = new SimplePhysicsEngine();
27 | const gameOptions = Object.assign({ physicsEngine, traceLevel: 0 }, options);
28 | const gameEngine = new MyGameEngine(gameOptions);
29 | const clientEngine = new MyClientEngine(gameEngine, options);
30 |
31 | function start() {
32 | clientEngine.start();
33 | return { clientEngine, gameEngine, gameOptions, physicsEngine };
34 | }
35 |
36 | // in a browser environment we auto-start
37 | // in a node environment we return a start method
38 | if (typeof document === 'undefined') {
39 | module.exports = { start };
40 | } else {
41 | document.addEventListener('DOMContentLoaded', start);
42 | }
43 |
--------------------------------------------------------------------------------
/test/EndToEnd/testGame/server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const express = require('express');
4 | const socketIO = require('socket.io');
5 | const path = require('path');
6 | const incheon = require('../../../');
7 |
8 | const PORT = process.env.PORT || 3000;
9 | const INDEX = path.join(__dirname, './index.html');
10 |
11 | // define routes and socket
12 | const server = express();
13 | server.get('/', function(req, res) { res.sendFile(INDEX); });
14 | server.use('/', express.static(path.join(__dirname, '.')));
15 | let requestHandler = server.listen(PORT, () => console.log(`Listening on ${PORT}`));
16 | const io = socketIO(requestHandler);
17 |
18 | // Game Server
19 | const MyServerEngine = require(path.join(__dirname, 'src/server/MyServerEngine.js'));
20 | const MyGameEngine = require(path.join(__dirname, 'src/common/MyGameEngine.js'));
21 | const SimplePhysicsEngine = incheon.physics.SimplePhysicsEngine;
22 |
23 | // Game Instances
24 | const physicsEngine = new SimplePhysicsEngine();
25 | const gameEngine = new MyGameEngine({ physicsEngine, traceLevel: 0 });
26 | const serverEngine = new MyServerEngine(io, gameEngine, { debug: {}, updateRate: 6 });
27 |
28 | // start the game
29 | function start() {
30 | serverEngine.start();
31 | return { gameEngine, serverEngine, physicsEngine };
32 | }
33 |
34 | module.exports = { start };
35 |
--------------------------------------------------------------------------------
/test/EndToEnd/testGame/src/client/MyClientEngine.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const incheon = require('../../../../../');
4 | const ClientEngine = incheon.ClientEngine;
5 | const MyRenderer = require('../client/MyRenderer');
6 |
7 | class MyClientEngine extends ClientEngine {
8 |
9 | constructor(gameEngine, options) {
10 | super(gameEngine, options);
11 |
12 | // initialize renderer
13 | this.renderer = new MyRenderer(gameEngine);
14 | this.serializer.registerClass(require('../common/PlayerAvatar'));
15 | this.gameEngine.on('client.preStep', this.preStep.bind(this));
16 |
17 | // keep a reference for key press state
18 | this.pressedKeys = {
19 | down: false,
20 | up: false,
21 | left: false,
22 | right: false,
23 | space: false
24 | };
25 | }
26 |
27 | // our pre-step is to process all inputs
28 | preStep() {
29 |
30 | if (this.pressedKeys.up) {
31 | this.sendInput('up', { movement: true });
32 | }
33 |
34 | if (this.pressedKeys.down) {
35 | this.sendInput('down', { movement: true });
36 | }
37 |
38 | if (this.pressedKeys.left) {
39 | this.sendInput('left', { movement: true });
40 | }
41 |
42 | if (this.pressedKeys.right) {
43 | this.sendInput('right', { movement: true });
44 | }
45 |
46 | if (this.pressedKeys.space) {
47 | this.sendInput('space', { movement: true });
48 | }
49 | }
50 |
51 | }
52 |
53 | module.exports = MyClientEngine;
54 |
--------------------------------------------------------------------------------
/test/EndToEnd/testGame/src/client/MyRenderer.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const incheon = require('../../../../../');
4 | const Renderer = incheon.render.Renderer;
5 |
6 | class MyRenderer extends Renderer {
7 |
8 | constructor(gameEngine, clientEngine) {
9 | super(gameEngine, clientEngine);
10 | this.sprites = {};
11 | }
12 |
13 | draw() {
14 | super.draw();
15 | }
16 |
17 | addObject(objData, options) {
18 | let sprite = {};
19 |
20 | // add this object to the renderer:
21 | // if (objData instanceof PlayerAvatar) {
22 | // ...
23 | // }
24 |
25 | Object.assign(sprite, options);
26 | this.sprites[objData.id] = sprite;
27 |
28 | return sprite;
29 | }
30 |
31 | removeObject(obj) {
32 | obj.destroy();
33 | delete this.sprites[obj.id];
34 | }
35 |
36 | }
37 |
38 | module.exports = MyRenderer;
39 |
--------------------------------------------------------------------------------
/test/EndToEnd/testGame/src/common/MyGameEngine.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const incheon = require('../../../../../');
3 | const GameEngine = incheon.GameEngine;
4 |
5 | class MyGameEngine extends GameEngine {
6 |
7 | constructor(options) {
8 | super(options);
9 | }
10 |
11 | start() {
12 |
13 | super.start();
14 |
15 | this.worldSettings = {
16 | width: 400,
17 | height: 400
18 | };
19 | }
20 |
21 | processInput(inputData, playerId) {
22 |
23 | super.processInput(inputData, playerId);
24 |
25 | // get the player tied to the player socket
26 | let player;
27 | for (let objId in this.world.objects) {
28 | if (this.world.objects[objId].playerId == playerId) {
29 | player = this.world.objects[objId];
30 | break;
31 | }
32 | }
33 |
34 | if (player) {
35 | console.log(`player ${playerId} pressed ${inputData.input}`);
36 | if (inputData.input === 'up') {
37 | player.isMovingUp = true;
38 | } else if (inputData.input === 'down') {
39 | player.isMovingDown = true;
40 | } else if (inputData.input === 'right') {
41 | player.isRotatingRight = true;
42 | } else if (inputData.input === 'left') {
43 | player.isRotatingLeft = true;
44 | } else if (inputData.input === 'space') {
45 | this.fire(player, inputData.messageIndex);
46 | this.emit('fire');
47 | }
48 | }
49 | }
50 | }
51 |
52 | module.exports = MyGameEngine;
53 |
--------------------------------------------------------------------------------
/test/EndToEnd/testGame/src/common/PlayerAvatar.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const incheon = require('../../../../../');
3 | const DynamicObject = incheon.serialize.DynamicObject;
4 |
5 | class PlayerAvatar extends DynamicObject {
6 |
7 | netScheme() {
8 | return Object.assign({}, super.netScheme);
9 | }
10 |
11 | constructor(id, x, y) {
12 | super(id, x, y);
13 | this.class = PlayerAvatar;
14 | }
15 | }
16 |
17 | module.exports = PlayerAvatar;
18 |
--------------------------------------------------------------------------------
/test/EndToEnd/testGame/src/server/MyServerEngine.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const incheon = require('../../../../../');
3 | const ServerEngine = incheon.ServerEngine;
4 |
5 | class MyServerEngine extends ServerEngine {
6 |
7 | constructor(io, gameEngine, inputOptions) {
8 | super(io, gameEngine, inputOptions);
9 | this.serializer.registerClass(require('../common/PlayerAvatar'));
10 | }
11 |
12 | start() {
13 | super.start();
14 | }
15 |
16 | onPlayerConnected(socket) {
17 | super.onPlayerConnected(socket);
18 | }
19 |
20 | onPlayerDisconnected(socketId, playerId) {
21 | super.onPlayerDisconnected(socketId, playerId);
22 |
23 | delete this.gameEngine.world.objects[playerId];
24 | }
25 | }
26 |
27 | module.exports = MyServerEngine;
28 |
--------------------------------------------------------------------------------
/test/SimplePhysics/HSHG.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const should = require('should');
4 | const BaseTypes = require('../../src/serialize/BaseTypes');
5 | const DynamicObject = require('../../src/serialize/DynamicObject');
6 | const HSHG = require('../../src/physics/SimplePhysics/HSHG');
7 |
8 | class TestObject extends DynamicObject {
9 |
10 | static get netScheme(){
11 | return {
12 | height: BaseTypes.TYPES.UINT16,
13 | width: BaseTypes.TYPES.UINT16
14 | };
15 | }
16 |
17 | constructor(){
18 | super();
19 | this.width = 100;
20 | this.height = 100;
21 | };
22 | }
23 |
24 | let grid = new HSHG();
25 |
26 | let obj1 = new TestObject(1);
27 | let obj2 = new TestObject(2);
28 |
29 | grid.addObject(obj1);
30 | grid.addObject(obj2);
31 |
32 | describe('HSHG collision detection', function() {
33 |
34 | it('No collision 1', function() {
35 | obj1.position.x = 0;
36 | obj2.position.x = 101;
37 | grid.update();
38 | let collisionPairs = grid.queryForCollisionPairs();
39 | collisionPairs.length.should.equal(0);
40 | });
41 |
42 | it('Partial overlap Collision', function() {
43 | obj1.position.x = 0;
44 | obj2.position.x = 50;
45 | grid.update();
46 | let collisionPairs = grid.queryForCollisionPairs();
47 | collisionPairs.length.should.equal(1);
48 | });
49 |
50 | it('No collision 2', function() {
51 | obj1.position.x = 0;
52 | obj2.position.x = 101;
53 | grid.update();
54 | let collisionPairs = grid.queryForCollisionPairs();
55 | collisionPairs.length.should.equal(0);
56 | });
57 |
58 |
59 | it('Full overlap collision', function() {
60 | obj1.position.x = 0;
61 | obj2.position.x = 0;
62 | obj2.position.width = 50;
63 | obj2.position.height = 50;
64 | grid.update();
65 | let collisionPairs = grid.queryForCollisionPairs();
66 | collisionPairs.length.should.equal(1);
67 | });
68 |
69 | });
70 |
--------------------------------------------------------------------------------
/test/SimplePhysics/HSHGGameEngine.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const should = require('should');
4 | const BaseTypes = require('../../src/serialize/BaseTypes');
5 | const DynamicObject = require('../../src/serialize/DynamicObject');
6 | const GameEngine = require('../../src/GameEngine');
7 | const SimplePhysicsEngine = require('../../src/physics/SimplePhysicsEngine');
8 |
9 | class TestObject extends DynamicObject {
10 |
11 | static get netScheme(){
12 | return Object.assign({
13 | height: BaseTypes.TYPES.UINT16,
14 | width: BaseTypes.TYPES.UINT16
15 | }, super.netScheme);
16 | }
17 |
18 | constructor(id){
19 | super(id);
20 | this.width = 10;
21 | this.height = 10;
22 | };
23 | }
24 |
25 | let gameEngine = new GameEngine();
26 | gameEngine.physicsEngine = new SimplePhysicsEngine({
27 | gameEngine: gameEngine,
28 | collisionOptions: {
29 | type: 'HSHG'
30 | }
31 | });
32 |
33 | gameEngine.start();
34 |
35 | let obj1 = new TestObject(1);
36 | let obj2 = new TestObject(2);
37 | gameEngine.addObjectToWorld(obj1);
38 | gameEngine.addObjectToWorld(obj2);
39 |
40 | describe('HSHG Game Engine collision detection', function() {
41 | obj1.position.x = 5;
42 | obj2.position.x = 20;
43 | obj2.velocity.x = -5;
44 | console.log(obj1.position, obj2.position);
45 |
46 |
47 | it('Step 0 - No collision ', function() {
48 | // console.log(obj1.position, obj2.position);
49 | gameEngine.physicsEngine.collisionDetection.areObjectsColliding(obj1, obj2).should.equal(false);
50 | });
51 |
52 | it('Step 1 - collision ', function() {
53 | gameEngine.once('collisionStart', pairObj => {
54 | gameEngine.world.stepCount.should.equal(1);
55 | });
56 | gameEngine.step(false);
57 | console.log(obj1.position, obj2.position);
58 | gameEngine.physicsEngine.collisionDetection.areObjectsColliding(obj1, obj2).should.equal(true);
59 | });
60 |
61 | it('Step 2 - collision ', function() {
62 | gameEngine.step(false);
63 | console.log(obj1.position, obj2.position);
64 | gameEngine.physicsEngine.collisionDetection.areObjectsColliding(obj1, obj2).should.equal(true);
65 | });
66 |
67 |
68 | it('Step 3 - collision ', function() {
69 | gameEngine.step(false);
70 | console.log(obj1.position, obj2.position);
71 | gameEngine.physicsEngine.collisionDetection.areObjectsColliding(obj1, obj2).should.equal(true);
72 | });
73 |
74 | it('Step 4 - collision ', function() {
75 | gameEngine.step(false);
76 | console.log(obj1.position, obj2.position);
77 | gameEngine.physicsEngine.collisionDetection.areObjectsColliding(obj1, obj2).should.equal(true);
78 | });
79 |
80 | it('Step 5 - collision ', function() {
81 | gameEngine.step(false);
82 | console.log(obj1.position, obj2.position);
83 | gameEngine.physicsEngine.collisionDetection.areObjectsColliding(obj1, obj2).should.equal(true);
84 | });
85 |
86 | it('Step 6 - no collision ', function() {
87 | gameEngine.once('collisionStop', pairObj => {
88 | gameEngine.world.stepCount.should.equal(6);
89 | });
90 | console.log(obj1.position, obj2.position);
91 | gameEngine.step(false);
92 | gameEngine.physicsEngine.collisionDetection.areObjectsColliding(obj1, obj2).should.equal(false);
93 | });
94 |
95 | });
96 |
--------------------------------------------------------------------------------
/test/serializer/list.js:
--------------------------------------------------------------------------------
1 | // Serializer must be loaded first before Serializable because of circular deps
2 | import Serializer from '../../src/serialize/Serializer.js';
3 | import Serializable from '../../src/serialize/Serializable.js';
4 | import BaseTypes from '../../src/serialize/BaseTypes.js';
5 |
6 | class TestObject extends Serializable {
7 |
8 | static get netScheme() {
9 | return {
10 | playerAges: {
11 | type: BaseTypes.List,
12 | itemType: BaseTypes.TYPES.UINT8
13 | },
14 | };
15 | }
16 |
17 | constructor(playerAges) {
18 | super();
19 | this.playerAges = playerAges;
20 | }
21 | }
22 |
23 | var serializer = new Serializer();
24 |
25 | var testObject = new TestObject([1, 2, 3]);
26 | serializer.registerClass(TestObject);
27 | testObject.class = TestObject;
28 |
29 | describe('List serialization/deserialization', function() {
30 | let serializedTestObject = null;
31 | let deserializedTestObject = null;
32 |
33 | describe('primitives', function() {
34 |
35 | it('Serialize list', function() {
36 | serializedTestObject = testObject.serialize(serializer);
37 |
38 | });
39 |
40 | it('Deserialize list', function() {
41 | deserializedTestObject = serializer.deserialize(serializedTestObject.dataBuffer);
42 | deserializedTestObject.byteOffset.should.equal(6);
43 | deserializedTestObject.obj.playerAges.should.be.instanceof(Array).and.have.lengthOf(3);
44 | deserializedTestObject.obj.playerAges[0].should.equal(1);
45 | deserializedTestObject.obj.playerAges[1].should.equal(2);
46 | deserializedTestObject.obj.playerAges[2].should.equal(3);
47 | });
48 |
49 | });
50 |
51 | });
52 |
--------------------------------------------------------------------------------
/test/serializer/primitives.js:
--------------------------------------------------------------------------------
1 | import should from 'should';
2 |
3 | import Serializer from '../../src/serialize/Serializer.js';
4 | import Serializable from '../../src/serialize/Serializable.js';
5 | import BaseTypes from '../../src/serialize/BaseTypes.js';
6 |
7 | class TestObject extends Serializable {
8 |
9 | static get netScheme(){
10 | return {
11 | float32: { type: BaseTypes.TYPES.FLOAT32 },
12 | int32: { type: BaseTypes.TYPES.INT32 },
13 | int16: { type: BaseTypes.TYPES.INT16 },
14 | int8: { type: BaseTypes.TYPES.INT8 },
15 | uint8: { type: BaseTypes.TYPES.UINT8 }
16 | }
17 | }
18 |
19 | constructor(data){
20 | super();
21 | if (data) {
22 | this.float32 = data.float32;
23 | this.int32 = data.int32;
24 | this.int16 = data.int16;
25 | this.int8 = data.int8;
26 | this.uint8 = data.uint8;
27 | }
28 | };
29 | }
30 |
31 | var serializer = new Serializer();
32 |
33 | var testObject = new TestObject({
34 | float32: 1/3,
35 | int32: 2147483646,
36 | int16: 32766,
37 | int8: 126,
38 | uint8: 254
39 | });
40 | serializer.registerClass(TestObject);
41 | testObject.class = TestObject;
42 |
43 | describe('Object with primitives serialization/deserialization', function() {
44 | let serializedTestObject, deserializedTestObject;
45 |
46 | it('Serialize object', function () {
47 | serializedTestObject = testObject.serialize(serializer);
48 |
49 | });
50 | it('Deserialize object', function () {
51 | deserializedTestObject = serializer.deserialize(serializedTestObject.dataBuffer);
52 | deserializedTestObject.byteOffset.should.equal(1 + 4 + 4 + 2 + 1 + 1);
53 | //float precision is capped
54 | (deserializedTestObject.obj.float32 - testObject.float32).should.be.lessThan(0.00000001);
55 | deserializedTestObject.obj.int32.should.be.equal(testObject.int32);
56 | deserializedTestObject.obj.int16.should.be.equal(testObject.int16);
57 | deserializedTestObject.obj.int8.should.be.equal(testObject.int8);
58 | deserializedTestObject.obj.uint8.should.be.equal(testObject.uint8);
59 | });
60 |
61 | });
62 |
--------------------------------------------------------------------------------
/test/serializer/string.js:
--------------------------------------------------------------------------------
1 | import should from 'should';
2 |
3 | import BaseTypes from '../../src/serialize/BaseTypes.js';
4 | import Serializer from '../../src/serialize/Serializer.js';
5 | import Serializable from '../../src/serialize/Serializable.js';
6 |
7 | class TestObject extends Serializable {
8 |
9 | static get netScheme() {
10 | return {
11 | helloString: { type: BaseTypes.String },
12 | color: { type: BaseTypes.String }
13 | };
14 | }
15 |
16 | constructor(helloString) {
17 | super();
18 | this.helloString = helloString;
19 | this.color = 'RED';
20 | }
21 | }
22 |
23 | var serializer = new Serializer();
24 |
25 | serializer.registerClass(TestObject);
26 | let testObjects = [new TestObject('hello'), new TestObject('hello'), new TestObject('goodbye')];
27 | testObjects.map(t => {
28 | t.class = TestObject;
29 | return null;
30 | });
31 |
32 | describe('List serialization/deserialization', function() {
33 | let serializedTestObjects;
34 | let deserializedTestObjects;
35 |
36 | describe('primitives', function() {
37 |
38 | it('Serialize object with string', function() {
39 | serializedTestObjects = testObjects.map(t => { return t.serialize(serializer); });
40 | });
41 |
42 | it('Deserialize list', function() {
43 | deserializedTestObjects = serializedTestObjects.map(s => { return serializer.deserialize(s.dataBuffer); });
44 | deserializedTestObjects[0].obj.helloString.should.equal('hello');
45 | deserializedTestObjects[1].obj.helloString.should.equal('hello');
46 | deserializedTestObjects[2].obj.helloString.should.equal('goodbye');
47 | });
48 |
49 | });
50 |
51 | });
52 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2015",
4 | "module": "node16",
5 | "removeComments": true,
6 | "preserveConstEnums": true,
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["src/**/*"],
10 | "exclude": ["**/*.spec.ts"]
11 | }
--------------------------------------------------------------------------------
/utils/lanceInfo.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const express = require('express');
3 |
4 | const PORT = process.env.PORT || 2000;
5 |
6 | // define routes and socket
7 | const app = express();
8 | let data = {};
9 | app.get('/:className', (req, res) => {
10 | res.send('ok');
11 | let className = req.params.className;
12 | if (typeof className === 'string' && className.length < 128) {
13 | data[className] = data[className] || 0;
14 | data[className]++;
15 | }
16 | });
17 | app.listen(PORT, () => console.log(`LanceInfo listening on ${PORT}`));
18 |
19 | let syncData = () => {
20 | const d = new Date();
21 | fs.writeFile(`lanceInfo-${d.getFullYear()}-${d.getMonth() + 1}.json`, JSON.stringify(data), () => {});
22 | setTimeout(syncData, 60 * 1000);
23 | };
24 |
25 | syncData();
26 |
--------------------------------------------------------------------------------
/utils/npm-install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | source /home/ec2-user/.bash_profile
3 | mkdir -p /var/games/lance
4 | cd /var/games/lance
5 | npm install
6 | npm run docs
7 |
8 |
9 | # upload static files to s3
10 | cd /var/games/lance/docs_out && aws s3 sync --acl public-read --delete . s3://docs.lance.gg
11 |
12 | # invalidate CDN
13 | aws configure set preview.cloudfront true && aws cloudfront create-invalidation --distribution-id EZZ6SM0QWM7CC --paths "/*"
14 |
--------------------------------------------------------------------------------
/utils/showMovement.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const process = require('process');
3 |
4 | //
5 | // this little utility shows the deltas between positions, quaternions, and time on each step
6 | //
7 | let FILENAME = 'server.trace';
8 | let states = {};
9 |
10 | if (process.argv.length === 3) FILENAME = process.argv[2];
11 | let fin = fs.readFileSync(FILENAME);
12 | let lines = fin.toString().split('\n');
13 | for (let l of lines) {
14 | if (l.indexOf('after step') < 0) continue;
15 |
16 | // position match: Pos[0, -15.4, 0]
17 | let p = l.match(/Pos\[([0-9.-]*), ([0-9.-]*), ([0-9.-]*)\]/);
18 |
19 | // quaternion match: Dir[0.34, [0, 1, 0]]
20 | let q = l.match(/Dir\[([0-9.-]*),[([0-9.-]*), ([0-9.-]*), ([0-9.-]*)\]\]/);
21 |
22 | // match: 2017-06-01T14:25:24.197Z
23 | let ts = l.match(/^\[([0-9\-T:.Z]*)\]/);
24 | let t = new Date(ts[1]);
25 |
26 | let step = l.match(/([0-9]*>)/);
27 | let parts = l.split(' ');
28 | let objname = parts[4];
29 | let old = states[objname];
30 | if (old) {
31 | let deltaP = `(${Number(p[1]) - Number(old.p[1])},${Number(p[2]) - Number(old.p[2])},${Number(p[3]) - Number(old.p[3])})`;
32 | let deltaQ = `(${Number(q[1]) - Number(old.q[1])},${Number(q[2]) - Number(old.q[2])},${Number(q[3]) - Number(old.q[3])},${Number(q[4]) - Number(old.q[4])})`;
33 | console.log(`step ${step[1]} dt=${t - old.t} object ${objname} moved ${deltaP} rotated ${deltaQ}`);
34 | }
35 | states[objname] = { p, q, t };
36 | }
37 |
--------------------------------------------------------------------------------