@inglorious/engine 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -43
- package/package.json +6 -6
- package/src/animation/ticker.js +39 -28
- package/src/behaviors/camera.js +1 -1
- package/src/behaviors/controls/dynamic/modern.js +2 -2
- package/src/behaviors/controls/dynamic/shooter.js +2 -2
- package/src/behaviors/controls/dynamic/tank.js +2 -2
- package/src/behaviors/controls/kinematic/modern.js +2 -2
- package/src/behaviors/controls/kinematic/shooter.js +2 -2
- package/src/behaviors/controls/kinematic/tank.js +2 -2
- package/src/behaviors/fps.js +1 -1
- package/src/behaviors/fsm.js +2 -2
- package/src/behaviors/fsm.test.js +3 -3
- package/src/behaviors/input/gamepad.js +1 -1
- package/src/behaviors/input/keyboard.js +1 -1
- package/src/behaviors/input/mouse.js +1 -1
- package/src/behaviors/physics/bouncy.js +2 -2
- package/src/behaviors/physics/clamped.js +2 -2
- package/src/behaviors/physics/jumpable.js +2 -2
- package/src/core/core-events.js +16 -0
- package/src/core/dev-tools.js +2 -15
- package/src/core/engine.js +43 -23
- package/src/core/loops/loops.js +15 -0
- package/src/core/middlewares/entity-pool/entity-pool-middleware.js +28 -0
- package/src/core/middlewares/entity-pool/entity-pool.js +38 -0
- package/src/core/middlewares/entity-pool/entity-pools.js +37 -0
- package/src/core/middlewares/middlewares.js +32 -0
- package/src/core/middlewares/multiplayer-middleware.js +91 -0
- package/src/systems/entity-creator.js +30 -0
- package/src/core/loop.js +0 -15
package/README.md
CHANGED
|
@@ -3,74 +3,120 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@inglorious/engine)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
The core orchestrator for the [Inglorious Engine](https://github.com/IngloriousCoderz/inglorious-engine). This package provides a complete game loop, state management, and rendering pipeline in a single, cohesive unit. It is designed to be highly configurable and extensible, allowing you to build games with a functional, data-oriented approach.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
---
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
- **Composable by Design**: Build complex behaviors by composing pure functions and decorators, offering a powerful alternative to inheritance.
|
|
12
|
-
- **Renderer Agnostic**: The engine is headless. You can use any rendering technology you like, from Canvas2D and HTML to React components.
|
|
13
|
-
- **Zero Build Step**: Write plain JavaScript and run it directly in the browser. No complex build configurations to worry about.
|
|
10
|
+
## Core Concepts
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
The `@inglorious/engine` package acts as the central hub that brings together all the engine's components. Its main responsibilities are:
|
|
16
13
|
|
|
17
|
-
The
|
|
14
|
+
1. **Orchestrating the Game Loop**: The `Engine` class manages the game loop, which is responsible for continuously updating the state and rendering the game. The loop can be configured to use different timing mechanisms, such as `animationFrame` or a fixed `fps`.
|
|
18
15
|
|
|
19
|
-
|
|
16
|
+
2. **State Management**: It leverages the `@inglorious/store` package to manage the game's state. It provides methods to start, stop, and update the state manager, processing a queue of events on each frame.
|
|
20
17
|
|
|
21
|
-
|
|
18
|
+
3. **Integrating the Renderer**: The engine is headless by design, but it can be configured with a renderer to display the game. The engine takes a renderer object in its configuration and integrates its systems and logic into the main game loop.
|
|
22
19
|
|
|
23
|
-
|
|
20
|
+
4. **Dev Tools Integration**: The engine automatically connects to a browser's dev tools for debugging and time-travel capabilities if `devMode` is enabled in the game's state.
|
|
24
21
|
|
|
25
|
-
|
|
22
|
+
---
|
|
26
23
|
|
|
27
|
-
|
|
24
|
+
## Installation
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
```bash
|
|
27
|
+
npm install @inglorious/engine
|
|
28
|
+
```
|
|
31
29
|
|
|
32
|
-
|
|
30
|
+
---
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
## API
|
|
35
33
|
|
|
36
|
-
|
|
34
|
+
### `new Engine(gameConfig)`
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
- **Time-Travel Debugging**: Save the state at any frame. You can step backward and forward through state changes to find exactly when a bug was introduced.
|
|
40
|
-
- **Hot-Reloading**: Modify your game's logic and instantly see the results without restarting. The engine can reload the code and re-apply it to the current state, dramatically speeding up iteration.
|
|
41
|
-
- **Simplified Persistence**: Saving and loading a game is as simple as serializing and deserializing a single JSON object.
|
|
42
|
-
- **Simplified Networking**: For multiplayer games, you don't need to synchronize complex objects. You just send small, serializable `event` objects over the network. Each client processes the same event with the same pure event handler, guaranteeing their game states stay in sync.
|
|
36
|
+
Creates a new `Engine` instance.
|
|
43
37
|
|
|
44
|
-
|
|
38
|
+
**Parameters:**
|
|
45
39
|
|
|
46
|
-
|
|
40
|
+
- `gameConfig` (object): The game-specific configuration. It is an object with the following properties:
|
|
41
|
+
- `loop` (object, optional): Configuration for the game loop.
|
|
42
|
+
- `type` (string, optional): The type of loop to use (`animationFrame` or `fixed`). Defaults to `animationFrame`.
|
|
43
|
+
- `fps` (number, optional): The target frames per second. Only used with the `fixed` loop type. Defaults to `60`.
|
|
44
|
+
- `systems` (array, optional): An array of system objects, which define behaviors for the whole state.
|
|
45
|
+
- `types` (object, optional): A map of entity types.
|
|
46
|
+
- `entities` (object, optional): A map of initial entities.
|
|
47
|
+
- `renderer` (object, optional): A renderer entity responsible for drawing the game. It must implement `getSystems()` and `init(engine)` methods.
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
**Returns:**
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
- A new `Engine` instance.
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
### `engine.start()`
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
- It decouples game logic from state updates.
|
|
56
|
-
- It ensures state changes happen at a predictable point in the game loop, preventing race conditions or cascading updates within a single frame.
|
|
57
|
-
- It allows for event batching and provides a solid foundation for networking and time-travel debugging.
|
|
55
|
+
Starts the game loop, triggering the first `update` and `render` calls.
|
|
58
56
|
|
|
59
|
-
|
|
60
|
-
- `update`: Fired on every frame, typically carrying the `deltaTime` since the last frame. This is where you'll put most of your continuous game logic (like movement).
|
|
61
|
-
- `add`: Used to add a new entity to the game state.
|
|
62
|
-
- `remove`: Used to remove an entity from the game state.
|
|
63
|
-
- `morph`: Used to dynamically change the behaviors associated with an entity's type.
|
|
57
|
+
### `engine.stop()`
|
|
64
58
|
|
|
65
|
-
|
|
59
|
+
Halts the game loop and cleans up any resources. This method also processes a final `stop` event to ensure a clean shutdown.
|
|
66
60
|
|
|
67
|
-
|
|
61
|
+
---
|
|
68
62
|
|
|
69
|
-
|
|
70
|
-
- **How it works**: Any event notified from within a handler is simply added to the end of the main event queue. It will be processed in a subsequent pass of the game loop, not immediately. This prevents synchronous, cascading updates within a single frame and makes the flow of logic easier to trace.
|
|
63
|
+
## Basic Usage
|
|
71
64
|
|
|
72
|
-
|
|
65
|
+
Here is a complete example showing how to set up and run a game using the engine.
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<!doctype html>
|
|
69
|
+
<html lang="en">
|
|
70
|
+
|
|
71
|
+
<head>
|
|
72
|
+
|
|
73
|
+
<meta charset="UTF-8" />
|
|
74
|
+
|
|
75
|
+
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
|
76
|
+
|
|
77
|
+
<link rel="stylesheet" href="/style.css" />
|
|
78
|
+
|
|
79
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
<title>Inglorious Engine</title>
|
|
83
|
+
|
|
84
|
+
</head>
|
|
85
|
+
|
|
86
|
+
<body>
|
|
87
|
+
<canvas id="canvas"></canvas>
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
<script type="text/javascript">
|
|
91
|
+
window.process = { env: "development" }
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
<script type="importmap">
|
|
96
|
+
{
|
|
97
|
+
"imports": {
|
|
98
|
+
"immer": "https://unpkg.com/immer@10.1.1/dist/immer.mjs",
|
|
99
|
+
"@inglorious/utils/": "https://unpkg.com/@inglorious%2Futils@1.1.0/",
|
|
100
|
+
"@inglorious/store/": "https://unpkg.com/@inglorious%2Fstore@0.1.0/",
|
|
101
|
+
"@inglorious/engine/": "https://unpkg.com/@inglorious%2Fengine@0.4.0/",
|
|
102
|
+
"@inglorious/renderers/": "https://unpkg.com/@inglorious%2Frenderer-2d@0.2.0/",
|
|
103
|
+
"game": "/game.js"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
<script
|
|
110
|
+
type="module"
|
|
111
|
+
src="https://unpkg.com/@inglorious%2Fengine@0.2.0/src/main.js"
|
|
112
|
+
></script>
|
|
113
|
+
|
|
114
|
+
</body>
|
|
115
|
+
</html>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
73
119
|
|
|
74
120
|
## Contributing
|
|
75
121
|
|
|
76
|
-
We welcome contributions from the community
|
|
122
|
+
We welcome contributions from the community\! Whether you're fixing a bug, adding a feature, or improving the documentation, your help is appreciated. Please read our [Contributing Guidelines](https://github.com/IngloriousCoderz/inglorious-engine/blob/main/CONTRIBUTING.md) for details on how to get started.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A JavaScript game engine written with global state, immutability, and pure functions in mind. Have fun(ctional programming) with it!",
|
|
5
5
|
"author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,20 +20,20 @@
|
|
|
20
20
|
"game-engine"
|
|
21
21
|
],
|
|
22
22
|
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
"./*": "./src/*"
|
|
25
|
+
},
|
|
23
26
|
"files": [
|
|
24
27
|
"src",
|
|
25
28
|
"README.md",
|
|
26
29
|
"LICENSE"
|
|
27
30
|
],
|
|
28
|
-
"exports": {
|
|
29
|
-
"./*": "./src/*"
|
|
30
|
-
},
|
|
31
31
|
"publishConfig": {
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@inglorious/
|
|
36
|
-
"@inglorious/
|
|
35
|
+
"@inglorious/utils": "1.1.0",
|
|
36
|
+
"@inglorious/store": "2.0.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"prettier": "^3.5.3",
|
package/src/animation/ticker.js
CHANGED
|
@@ -2,37 +2,48 @@ const DEFAULT_STATE = "default"
|
|
|
2
2
|
const DEFAULT_VALUE = 0
|
|
3
3
|
const COUNTER_RESET = 0
|
|
4
4
|
|
|
5
|
-
export const Ticker = {
|
|
5
|
+
export const Ticker = {
|
|
6
|
+
/**
|
|
7
|
+
* Ticks a counter and calls a function when a target interval is reached.
|
|
8
|
+
*
|
|
9
|
+
* @param {object} options The options for the tick.
|
|
10
|
+
* @param {object} options.target An object to hold the ticker's internal state.
|
|
11
|
+
* @param {number} options.target.speed The interval in milliseconds for the ticker to "tick".
|
|
12
|
+
* @param {number} options.dt The time elapsed since the last tick.
|
|
13
|
+
* @param {function} options.onTick The function to call when the ticker "ticks".
|
|
14
|
+
* @param {string} [options.state="default"] An optional state to track changes and reset the ticker.
|
|
15
|
+
* @param {number} [options.defaultValue=0] The default value for the target's counter.
|
|
16
|
+
*/
|
|
17
|
+
tick({ target, state = DEFAULT_STATE, dt, onTick, ...options }) {
|
|
18
|
+
const missing = [target == null && "'target'", dt == null && "'dt'"]
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.join(", ")
|
|
21
|
+
if (missing.length) {
|
|
22
|
+
throw new Error(`Ticker.tick is missing mandatory parameters: ${missing}`)
|
|
23
|
+
}
|
|
6
24
|
|
|
7
|
-
|
|
8
|
-
const missing = [target == null && "'target'", dt == null && "'dt'"]
|
|
9
|
-
.filter(Boolean)
|
|
10
|
-
.join(", ")
|
|
11
|
-
if (missing.length) {
|
|
12
|
-
throw new Error(`Ticker.tick is missing mandatory parameters: ${missing}`)
|
|
13
|
-
}
|
|
25
|
+
const { speed, defaultValue = DEFAULT_VALUE } = target
|
|
14
26
|
|
|
15
|
-
|
|
27
|
+
// The `state` property on a sprite is used to declare the desired animation.
|
|
28
|
+
// The Ticker needs its own internal property to track the *current* animation
|
|
29
|
+
// to detect when it changes. We'll use `target.animation`.
|
|
30
|
+
if (state !== target.animation) {
|
|
31
|
+
target.animation = state
|
|
32
|
+
target.counter = COUNTER_RESET
|
|
33
|
+
target.value = defaultValue
|
|
34
|
+
}
|
|
16
35
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// to detect when it changes. We'll use `target.animation`.
|
|
20
|
-
if (state !== target.animation) {
|
|
21
|
-
target.animation = state
|
|
22
|
-
target.counter = COUNTER_RESET
|
|
23
|
-
target.value = defaultValue
|
|
24
|
-
}
|
|
36
|
+
// Always ensure the public `state` property reflects the intended animation for `onTick`.
|
|
37
|
+
target.state = state
|
|
25
38
|
|
|
26
|
-
|
|
27
|
-
|
|
39
|
+
// Ensure properties are initialized on the first run for a given target.
|
|
40
|
+
target.counter ??= COUNTER_RESET
|
|
41
|
+
target.value ??= defaultValue
|
|
28
42
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
target.counter = COUNTER_RESET
|
|
36
|
-
onTick?.(target, dt, options)
|
|
37
|
-
}
|
|
43
|
+
target.counter += dt
|
|
44
|
+
if (target.counter >= speed) {
|
|
45
|
+
target.counter = COUNTER_RESET
|
|
46
|
+
onTick?.(target, dt, options)
|
|
47
|
+
}
|
|
48
|
+
},
|
|
38
49
|
}
|
package/src/behaviors/camera.js
CHANGED
|
@@ -24,8 +24,8 @@ export function modernAcceleration(params) {
|
|
|
24
24
|
"moveUpDown",
|
|
25
25
|
]),
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
type.
|
|
27
|
+
create(entity, event, api) {
|
|
28
|
+
type.create?.(entity, event, api)
|
|
29
29
|
|
|
30
30
|
entity.maxAcceleration ??= params.maxAcceleration
|
|
31
31
|
entity.movement ??= {}
|
|
@@ -33,8 +33,8 @@ export function shooterControls(params) {
|
|
|
33
33
|
"turn",
|
|
34
34
|
]),
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
type.
|
|
36
|
+
create(entity, event, api) {
|
|
37
|
+
type.create?.(entity, event, api)
|
|
38
38
|
|
|
39
39
|
entity.maxSpeed ??= params.maxSpeed
|
|
40
40
|
entity.maxAngularSpeed ??= params.maxAngularSpeed
|
|
@@ -27,8 +27,8 @@ export function tankControls(params) {
|
|
|
27
27
|
"turn",
|
|
28
28
|
]),
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
type.
|
|
30
|
+
create(entity, event, api) {
|
|
31
|
+
type.create?.(entity, event, api)
|
|
32
32
|
|
|
33
33
|
entity.maxSpeed ??= params.maxSpeed
|
|
34
34
|
entity.maxAngularSpeed ??= params.maxAngularSpeed
|
|
@@ -24,8 +24,8 @@ export function modernVelocity(params) {
|
|
|
24
24
|
"moveUpDown",
|
|
25
25
|
]),
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
type.
|
|
27
|
+
create(entity, event, api) {
|
|
28
|
+
type.create?.(entity, event, api)
|
|
29
29
|
|
|
30
30
|
entity.maxSpeed ??= params.maxSpeed
|
|
31
31
|
entity.movement ??= {}
|
|
@@ -32,8 +32,8 @@ export function shooterControls(params) {
|
|
|
32
32
|
"turn",
|
|
33
33
|
]),
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
type.
|
|
35
|
+
create(entity, event, api) {
|
|
36
|
+
type.create?.(entity, event, api)
|
|
37
37
|
|
|
38
38
|
entity.maxSpeed ??= params.maxSpeed
|
|
39
39
|
entity.maxAngularSpeed ??= params.maxAngularSpeed
|
|
@@ -26,8 +26,8 @@ export function tankControls(params) {
|
|
|
26
26
|
"turn",
|
|
27
27
|
]),
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
type.
|
|
29
|
+
create(entity, event, api) {
|
|
30
|
+
type.create?.(entity, event, api)
|
|
31
31
|
|
|
32
32
|
entity.maxSpeed ??= params.maxSpeed
|
|
33
33
|
entity.maxAngularSpeed ??= params.maxAngularSpeed
|
package/src/behaviors/fps.js
CHANGED
package/src/behaviors/fsm.js
CHANGED
|
@@ -9,7 +9,7 @@ test("it should add a finite state machine", () => {
|
|
|
9
9
|
kitty: [
|
|
10
10
|
fsm({
|
|
11
11
|
default: {
|
|
12
|
-
|
|
12
|
+
meow(entity) {
|
|
13
13
|
entity.state = "meowing"
|
|
14
14
|
},
|
|
15
15
|
},
|
|
@@ -40,8 +40,8 @@ test("it should add a finite state machine", () => {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const store = createStore(config)
|
|
43
|
-
store.notify("
|
|
44
|
-
store.notify("
|
|
43
|
+
store.notify("meow")
|
|
44
|
+
store.notify("update")
|
|
45
45
|
store.update()
|
|
46
46
|
|
|
47
47
|
const state = store.getState()
|
|
@@ -11,8 +11,8 @@ export function clamped(params) {
|
|
|
11
11
|
|
|
12
12
|
return (type) =>
|
|
13
13
|
extend(type, {
|
|
14
|
-
|
|
15
|
-
type.
|
|
14
|
+
create(entity, event, api) {
|
|
15
|
+
type.create?.(entity, event, api)
|
|
16
16
|
|
|
17
17
|
entity.collisions ??= {}
|
|
18
18
|
entity.collisions[params.collisionGroup] ??= {}
|
|
@@ -27,8 +27,8 @@ export function jumpable(params) {
|
|
|
27
27
|
|
|
28
28
|
return (type) =>
|
|
29
29
|
extend(type, {
|
|
30
|
-
|
|
31
|
-
type.
|
|
30
|
+
create(entity, event, api) {
|
|
31
|
+
type.create?.(entity, event, api)
|
|
32
32
|
defaults(entity, params)
|
|
33
33
|
entity.jumpsLeft ??= entity.maxJumps
|
|
34
34
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const coreEvents = [
|
|
2
|
+
"create",
|
|
3
|
+
"update",
|
|
4
|
+
"destroy",
|
|
5
|
+
"gamepadAxis",
|
|
6
|
+
"gamepadPress",
|
|
7
|
+
"gamepadRelease",
|
|
8
|
+
"keyboardKeyDown",
|
|
9
|
+
"keyboardKeyUp",
|
|
10
|
+
"inputAxis",
|
|
11
|
+
"inputPress",
|
|
12
|
+
"inputRelease",
|
|
13
|
+
"mouseMove",
|
|
14
|
+
"mouseClick",
|
|
15
|
+
"spriteAnimationEnd",
|
|
16
|
+
]
|
package/src/core/dev-tools.js
CHANGED
|
@@ -1,17 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
"update",
|
|
3
|
-
"gamepadAxis",
|
|
4
|
-
"gamepadPress",
|
|
5
|
-
"gamepadRelease",
|
|
6
|
-
"keyboardKeyDown",
|
|
7
|
-
"keyboardKeyUp",
|
|
8
|
-
"inputAxis",
|
|
9
|
-
"inputPress",
|
|
10
|
-
"inputRelease",
|
|
11
|
-
"mouseMove",
|
|
12
|
-
"mouseClick",
|
|
13
|
-
"spriteAnimationEnd",
|
|
14
|
-
]
|
|
1
|
+
import { coreEvents } from "./core-events.js"
|
|
15
2
|
|
|
16
3
|
const LAST_STATE = 1
|
|
17
4
|
|
|
@@ -30,7 +17,7 @@ export function initDevTools(store) {
|
|
|
30
17
|
|
|
31
18
|
devToolsInstance = window.__REDUX_DEVTOOLS_EXTENSION__.connect({
|
|
32
19
|
name: "Inglorious Engine",
|
|
33
|
-
predicate: (state, action) => !
|
|
20
|
+
predicate: (state, action) => !coreEvents.includes(action.type),
|
|
34
21
|
actionCreators: {
|
|
35
22
|
jump: () => ({ type: "jump", payload: { inputId: "input0" } }),
|
|
36
23
|
},
|
package/src/core/engine.js
CHANGED
|
@@ -3,13 +3,13 @@ import { createApi } from "@inglorious/store/api.js"
|
|
|
3
3
|
import { createStore } from "@inglorious/store/store.js"
|
|
4
4
|
import { extend } from "@inglorious/utils/data-structures/objects.js"
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "./
|
|
12
|
-
import
|
|
6
|
+
import { coreEvents } from "./core-events.js"
|
|
7
|
+
import { disconnectDevTools, initDevTools, sendAction } from "./dev-tools.js"
|
|
8
|
+
import { Loops } from "./loops/loops.js"
|
|
9
|
+
import { entityPoolMiddleware } from "./middlewares/entity-pool/entity-pool-middleware.js"
|
|
10
|
+
import { EntityPools } from "./middlewares/entity-pool/entity-pools.js"
|
|
11
|
+
import { applyMiddlewares } from "./middlewares/middlewares.js"
|
|
12
|
+
import { multiplayerMiddleware } from "./middlewares/multiplayer-middleware.js"
|
|
13
13
|
|
|
14
14
|
// Default game configuration
|
|
15
15
|
// loop.type specifies the type of loop to use (defaults to "animationFrame").
|
|
@@ -30,15 +30,10 @@ const DEFAULT_GAME_CONFIG = {
|
|
|
30
30
|
|
|
31
31
|
const ONE_SECOND = 1000 // Number of milliseconds in one second.
|
|
32
32
|
|
|
33
|
-
// Delta time for the final update call when stopping the engine.
|
|
34
|
-
const FINAL_UPDATE_DELTA_TIME = 0 // This ensures any pending events (like 'stop') are processed before shutdown.
|
|
35
|
-
|
|
36
33
|
/**
|
|
37
34
|
* Engine class responsible for managing the game loop, state, and rendering.
|
|
38
35
|
*/
|
|
39
36
|
export class Engine {
|
|
40
|
-
_devMode = false
|
|
41
|
-
|
|
42
37
|
/**
|
|
43
38
|
* @param {Object} [gameConfig] - Game-specific configuration.
|
|
44
39
|
* @param {Object} [renderer] - UI entity responsible for rendering. It must have a `render` method.
|
|
@@ -46,22 +41,46 @@ export class Engine {
|
|
|
46
41
|
constructor(gameConfig) {
|
|
47
42
|
this._config = extend(DEFAULT_GAME_CONFIG, gameConfig)
|
|
48
43
|
|
|
44
|
+
// Determine devMode from the entities config.
|
|
45
|
+
const devMode = this._config.entities.game?.devMode
|
|
46
|
+
this._devMode = devMode
|
|
47
|
+
|
|
48
|
+
// Add user-defined systems
|
|
49
49
|
const systems = [...(this._config.systems ?? [])]
|
|
50
50
|
if (this._config.renderer) {
|
|
51
51
|
systems.push(...this._config.renderer.getSystems())
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
this._store = createStore({ ...this._config, systems })
|
|
55
|
-
|
|
55
|
+
|
|
56
|
+
// Create API layer, with optional methods for debugging
|
|
56
57
|
this._api = createApi(this._store)
|
|
57
58
|
|
|
59
|
+
this._entityPools = new EntityPools()
|
|
60
|
+
this._api = applyMiddlewares(entityPoolMiddleware(this._entityPools))(
|
|
61
|
+
this._api,
|
|
62
|
+
)
|
|
63
|
+
this._api.getAllActivePoolEntities = () =>
|
|
64
|
+
this._entityPools.getAllActiveEntities()
|
|
65
|
+
|
|
66
|
+
if (this._devMode) {
|
|
67
|
+
this._api.getEntityPoolsStats = () => this._entityPools.getStats()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Apply multiplayer if specified.
|
|
71
|
+
const multiplayer = this._config.entities.game?.multiplayer
|
|
72
|
+
if (multiplayer) {
|
|
73
|
+
this._api = applyMiddlewares(multiplayerMiddleware(multiplayer))(
|
|
74
|
+
this._api,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this._loop = new Loops[this._config.loop.type]()
|
|
79
|
+
|
|
58
80
|
// The renderer might need the engine instance to initialize itself (e.g., to set up DOM events).
|
|
59
81
|
this._config.renderer?.init(this)
|
|
60
82
|
|
|
61
|
-
|
|
62
|
-
const devMode = this._config.entities.game?.devMode
|
|
63
|
-
this._devMode = devMode
|
|
64
|
-
if (devMode) {
|
|
83
|
+
if (this._devMode) {
|
|
65
84
|
initDevTools(this._store)
|
|
66
85
|
}
|
|
67
86
|
}
|
|
@@ -70,7 +89,7 @@ export class Engine {
|
|
|
70
89
|
* Starts the game engine, initializing the loop and notifying the store.
|
|
71
90
|
*/
|
|
72
91
|
start() {
|
|
73
|
-
this.
|
|
92
|
+
this._api.notify("start")
|
|
74
93
|
this._loop.start(this, ONE_SECOND / this._config.loop.fps)
|
|
75
94
|
this.isRunning = true
|
|
76
95
|
}
|
|
@@ -79,8 +98,8 @@ export class Engine {
|
|
|
79
98
|
* Stops the game engine, halting the loop and notifying the store.
|
|
80
99
|
*/
|
|
81
100
|
stop() {
|
|
82
|
-
this.
|
|
83
|
-
this._store.update(
|
|
101
|
+
this._api.notify("stop")
|
|
102
|
+
this._store.update(this._api)
|
|
84
103
|
this._loop.stop()
|
|
85
104
|
this.isRunning = false
|
|
86
105
|
}
|
|
@@ -90,14 +109,15 @@ export class Engine {
|
|
|
90
109
|
* @param {number} dt - Delta time since the last update in milliseconds.
|
|
91
110
|
*/
|
|
92
111
|
update(dt) {
|
|
93
|
-
|
|
112
|
+
this._api.notify("update", dt)
|
|
113
|
+
const processedEvents = this._store.update(this._api)
|
|
94
114
|
const state = this._store.getState()
|
|
95
115
|
|
|
96
116
|
// Check for devMode changes and connect/disconnect dev tools accordingly.
|
|
97
117
|
const newDevMode = state.entities.game?.devMode
|
|
98
118
|
if (newDevMode !== this._devMode) {
|
|
99
119
|
if (newDevMode) {
|
|
100
|
-
initDevTools(this.
|
|
120
|
+
initDevTools(this._api)
|
|
101
121
|
} else {
|
|
102
122
|
disconnectDevTools()
|
|
103
123
|
}
|
|
@@ -105,7 +125,7 @@ export class Engine {
|
|
|
105
125
|
}
|
|
106
126
|
|
|
107
127
|
const eventsToLog = processedEvents.filter(
|
|
108
|
-
({ type }) => !
|
|
128
|
+
({ type }) => !coreEvents.includes(type),
|
|
109
129
|
)
|
|
110
130
|
|
|
111
131
|
if (eventsToLog.length) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AnimationFrameLoop } from "./animation-frame.js"
|
|
2
|
+
import { ElapsedLoop } from "./elapsed.js"
|
|
3
|
+
import { FixedLoop } from "./fixed.js"
|
|
4
|
+
import { FlashLoop } from "./flash.js"
|
|
5
|
+
import { LagLoop } from "./lag.js"
|
|
6
|
+
|
|
7
|
+
// @see https://gameprogrammingpatterns.com/game-loop.html
|
|
8
|
+
|
|
9
|
+
export const Loops = {
|
|
10
|
+
flash: FlashLoop,
|
|
11
|
+
fixed: FixedLoop,
|
|
12
|
+
elapsed: ElapsedLoop,
|
|
13
|
+
lag: LagLoop,
|
|
14
|
+
animationFrame: AnimationFrameLoop,
|
|
15
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function entityPoolMiddleware(pools) {
|
|
2
|
+
return (api) => (next) => (event) => {
|
|
3
|
+
switch (event.type) {
|
|
4
|
+
case "spawn": {
|
|
5
|
+
pools.acquire(event.payload)
|
|
6
|
+
break
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
case "despawn": {
|
|
10
|
+
pools.recycle(event.payload)
|
|
11
|
+
break
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
default: {
|
|
15
|
+
const types = api.getTypes()
|
|
16
|
+
pools._pools.values().forEach((pool) => {
|
|
17
|
+
pool._activeEntities.forEach((entity) => {
|
|
18
|
+
const type = types[entity.type]
|
|
19
|
+
const handle = type[event.type]
|
|
20
|
+
handle?.(entity, event.payload, api)
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return next(event)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const INITIAL_ID = 0
|
|
2
|
+
const NOT_FOUND = -1
|
|
3
|
+
const ITEMS_TO_REMOVE = 1
|
|
4
|
+
|
|
5
|
+
export class EntityPool {
|
|
6
|
+
_activeEntities = []
|
|
7
|
+
_inactiveEntities = []
|
|
8
|
+
_nextId = INITIAL_ID
|
|
9
|
+
|
|
10
|
+
populate(factory, count) {
|
|
11
|
+
for (let i = 0; i < count; i++) {
|
|
12
|
+
this._activeEntities.push(factory())
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
acquire(props) {
|
|
17
|
+
const entity = this._inactiveEntities.pop() || {
|
|
18
|
+
id: `entity-${this._nextId++}`,
|
|
19
|
+
}
|
|
20
|
+
Object.assign(entity, props)
|
|
21
|
+
this._activeEntities.push(entity)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
recycle(entity) {
|
|
25
|
+
const index = this._activeEntities.indexOf(entity)
|
|
26
|
+
if (index !== NOT_FOUND) {
|
|
27
|
+
const [entity] = this._activeEntities.splice(index, ITEMS_TO_REMOVE)
|
|
28
|
+
this._inactiveEntities.push(entity)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getStats() {
|
|
33
|
+
return {
|
|
34
|
+
active: this._activeEntities.length,
|
|
35
|
+
inactive: this._inactiveEntities.length,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { EntityPool } from "./entity-pool"
|
|
2
|
+
|
|
3
|
+
export class EntityPools {
|
|
4
|
+
_pools = new Map()
|
|
5
|
+
|
|
6
|
+
acquire(entity) {
|
|
7
|
+
this.lazyInit(entity)
|
|
8
|
+
this._pools.get(entity.type).acquire(entity)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
recycle(entity) {
|
|
12
|
+
this.lazyInit(entity)
|
|
13
|
+
this._pools.get(entity.type).recycle(entity)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getStats() {
|
|
17
|
+
const stats = {}
|
|
18
|
+
for (const [type, pool] of this._pools.entries()) {
|
|
19
|
+
stats[type] = pool.getStats()
|
|
20
|
+
}
|
|
21
|
+
return stats
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
lazyInit(entity) {
|
|
25
|
+
if (!this._pools.get(entity.type)) {
|
|
26
|
+
this._pools.set(entity.type, new EntityPool())
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getAllActiveEntities() {
|
|
31
|
+
const activeEntities = []
|
|
32
|
+
for (const pool of this._pools.values()) {
|
|
33
|
+
activeEntities.push(...pool._activeEntities)
|
|
34
|
+
}
|
|
35
|
+
return activeEntities
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { compose } from "@inglorious/utils/functions/functions.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Applies a list of middleware functions to a store's dispatch method.
|
|
5
|
+
*
|
|
6
|
+
* @param {...Function} middlewares The middleware functions to apply.
|
|
7
|
+
* @returns {Function} A store enhancer function.
|
|
8
|
+
*/
|
|
9
|
+
export function applyMiddlewares(...middlewares) {
|
|
10
|
+
return (store) => {
|
|
11
|
+
let dispatch = () => {
|
|
12
|
+
throw new Error(
|
|
13
|
+
"Dispatching while constructing your middleware is not allowed.",
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// The middleware API that can be passed to each middleware function.
|
|
18
|
+
const api = {
|
|
19
|
+
...store,
|
|
20
|
+
dispatch: (...args) => dispatch(...args),
|
|
21
|
+
notify: (type, payload) => dispatch({ type, payload }),
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Create a chain of middleware functions.
|
|
25
|
+
const chain = middlewares.map((middleware) => middleware(api))
|
|
26
|
+
|
|
27
|
+
// Compose the middleware chain to create the final dispatch function.
|
|
28
|
+
dispatch = compose(...chain)(store.dispatch)
|
|
29
|
+
|
|
30
|
+
return api
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { extend } from "@inglorious/utils/data-structures/objects.js"
|
|
2
|
+
|
|
3
|
+
import { coreEvents } from "../core-events.js"
|
|
4
|
+
|
|
5
|
+
// A constant for the server's WebSocket URL.
|
|
6
|
+
const DEFAULT_SERVER_URL = `ws://${window.location.hostname}:3000`
|
|
7
|
+
const DEFAULT_RECONNECTION_DELAY = 1000
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Creates and returns the multiplayer middleware.
|
|
11
|
+
* @returns {Function} The middleware function.
|
|
12
|
+
*/
|
|
13
|
+
export function multiplayerMiddleware(config = {}) {
|
|
14
|
+
const serverUrl = config.serverUrl ?? DEFAULT_SERVER_URL
|
|
15
|
+
const reconnectionDelay =
|
|
16
|
+
config.reconnectionDelay ?? DEFAULT_RECONNECTION_DELAY
|
|
17
|
+
|
|
18
|
+
let ws = null
|
|
19
|
+
const localQueue = []
|
|
20
|
+
|
|
21
|
+
// The middleware function that will be applied to the store.
|
|
22
|
+
return (api) => (next) => (event) => {
|
|
23
|
+
if (coreEvents.includes(event.type)) {
|
|
24
|
+
return next(event)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Establish the connection on the first event.
|
|
28
|
+
if (!ws) {
|
|
29
|
+
establishConnection(api)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Only send the event to the server if it didn't come from the server.
|
|
33
|
+
if (!event.fromServer) {
|
|
34
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
35
|
+
// If the connection is open, send the event immediately.
|
|
36
|
+
ws.send(JSON.stringify(event))
|
|
37
|
+
} else {
|
|
38
|
+
// If the connection is not open, queue the event for later.
|
|
39
|
+
localQueue.push(event)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Pass the event to the next middleware in the chain,
|
|
44
|
+
// which is eventually the store's original dispatch function.
|
|
45
|
+
return next(event)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Attempts to establish a WebSocket connection to the server.
|
|
50
|
+
*/
|
|
51
|
+
function establishConnection(api) {
|
|
52
|
+
// If a connection already exists, close it first.
|
|
53
|
+
if (ws) {
|
|
54
|
+
ws.close()
|
|
55
|
+
}
|
|
56
|
+
ws = new WebSocket(serverUrl)
|
|
57
|
+
|
|
58
|
+
// =====================================================================
|
|
59
|
+
// WebSocket Event Handlers
|
|
60
|
+
// =====================================================================
|
|
61
|
+
|
|
62
|
+
ws.onopen = () => {
|
|
63
|
+
// Send any queued events to the server.
|
|
64
|
+
while (localQueue.length) {
|
|
65
|
+
ws.send(JSON.stringify(localQueue.shift()))
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
ws.onmessage = (event) => {
|
|
70
|
+
const serverEvent = JSON.parse(event.data)
|
|
71
|
+
|
|
72
|
+
if (serverEvent.type === "initialState") {
|
|
73
|
+
// Merge the server's initial state with the client's local state.
|
|
74
|
+
const nextState = extend(api.getState(), serverEvent.payload)
|
|
75
|
+
api.setState(nextState)
|
|
76
|
+
} else {
|
|
77
|
+
// Dispatch the event to the local store to update the client's state.
|
|
78
|
+
api.dispatch({ ...serverEvent, fromServer: true })
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
ws.onclose = () => {
|
|
83
|
+
// Attempt to reconnect after a short delay.
|
|
84
|
+
setTimeout(() => establishConnection(api), reconnectionDelay)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
ws.onerror = () => {
|
|
88
|
+
ws.close() // The 'onclose' handler will trigger the reconnect.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Ticker } from "@inglorious/engine/animation/ticker.js"
|
|
2
|
+
|
|
3
|
+
const DEFAULT_SPAWN_INTERVAL = 1 // Time in seconds between spawning entities.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A system responsible for creating new entities, like asteroids, at regular intervals.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} params Configuration parameters for the system.
|
|
9
|
+
* @param {number} [params.spawnInterval=1] The time in seconds between spawning entities.
|
|
10
|
+
* @param {function} params.factory A function that returns the properties for a new entity.
|
|
11
|
+
* @returns {object} The configured entity creator system.
|
|
12
|
+
*/
|
|
13
|
+
export function entityCreator(params = {}) {
|
|
14
|
+
const spawnInterval = params.spawnInterval ?? DEFAULT_SPAWN_INTERVAL
|
|
15
|
+
const factory = params.factory
|
|
16
|
+
|
|
17
|
+
const ticker = { speed: spawnInterval }
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
update(state, dt, api) {
|
|
21
|
+
Ticker.tick({
|
|
22
|
+
target: ticker,
|
|
23
|
+
dt,
|
|
24
|
+
onTick: () => {
|
|
25
|
+
api.notify("add", factory())
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/core/loop.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { AnimationFrameLoop } from "./loops/animation-frame.js"
|
|
2
|
-
import { ElapsedLoop } from "./loops/elapsed.js"
|
|
3
|
-
import { FixedLoop } from "./loops/fixed.js"
|
|
4
|
-
import { FlashLoop } from "./loops/flash.js"
|
|
5
|
-
import { LagLoop } from "./loops/lag.js"
|
|
6
|
-
|
|
7
|
-
// @see https://gameprogrammingpatterns.com/game-loop.html
|
|
8
|
-
|
|
9
|
-
export default {
|
|
10
|
-
flash: FlashLoop,
|
|
11
|
-
fixed: FixedLoop,
|
|
12
|
-
elapsed: ElapsedLoop,
|
|
13
|
-
lag: LagLoop,
|
|
14
|
-
animationFrame: AnimationFrameLoop,
|
|
15
|
-
}
|