@inglorious/engine 0.5.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/package.json +2 -2
- package/src/animation/ticker.js +39 -28
- package/src/behaviors/fsm.js +1 -1
- package/src/behaviors/fsm.test.js +3 -3
- package/src/behaviors/physics/bouncy.js +1 -1
- package/src/core/engine.js +36 -22
- 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.js → middlewares/middlewares.js} +3 -7
- package/src/core/{multiplayer.js → middlewares/multiplayer-middleware.js} +1 -1
- package/src/systems/entity-creator.js +30 -0
- package/src/core/loop.js +0 -15
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",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@inglorious/utils": "1.1.0",
|
|
36
|
-
"@inglorious/store": "
|
|
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/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()
|
package/src/core/engine.js
CHANGED
|
@@ -5,9 +5,11 @@ import { extend } from "@inglorious/utils/data-structures/objects.js"
|
|
|
5
5
|
|
|
6
6
|
import { coreEvents } from "./core-events.js"
|
|
7
7
|
import { disconnectDevTools, initDevTools, sendAction } from "./dev-tools.js"
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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"
|
|
11
13
|
|
|
12
14
|
// Default game configuration
|
|
13
15
|
// loop.type specifies the type of loop to use (defaults to "animationFrame").
|
|
@@ -28,15 +30,10 @@ const DEFAULT_GAME_CONFIG = {
|
|
|
28
30
|
|
|
29
31
|
const ONE_SECOND = 1000 // Number of milliseconds in one second.
|
|
30
32
|
|
|
31
|
-
// Delta time for the final update call when stopping the engine.
|
|
32
|
-
const FINAL_UPDATE_DELTA_TIME = 0 // This ensures any pending events (like 'stop') are processed before shutdown.
|
|
33
|
-
|
|
34
33
|
/**
|
|
35
34
|
* Engine class responsible for managing the game loop, state, and rendering.
|
|
36
35
|
*/
|
|
37
36
|
export class Engine {
|
|
38
|
-
_devMode = false
|
|
39
|
-
|
|
40
37
|
/**
|
|
41
38
|
* @param {Object} [gameConfig] - Game-specific configuration.
|
|
42
39
|
* @param {Object} [renderer] - UI entity responsible for rendering. It must have a `render` method.
|
|
@@ -44,30 +41,46 @@ export class Engine {
|
|
|
44
41
|
constructor(gameConfig) {
|
|
45
42
|
this._config = extend(DEFAULT_GAME_CONFIG, gameConfig)
|
|
46
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
|
|
47
49
|
const systems = [...(this._config.systems ?? [])]
|
|
48
50
|
if (this._config.renderer) {
|
|
49
51
|
systems.push(...this._config.renderer.getSystems())
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
this._store = createStore({ ...this._config, systems })
|
|
55
|
+
|
|
56
|
+
// Create API layer, with optional methods for debugging
|
|
57
|
+
this._api = createApi(this._store)
|
|
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
|
+
}
|
|
53
69
|
|
|
54
70
|
// Apply multiplayer if specified.
|
|
55
71
|
const multiplayer = this._config.entities.game?.multiplayer
|
|
56
72
|
if (multiplayer) {
|
|
57
|
-
|
|
73
|
+
this._api = applyMiddlewares(multiplayerMiddleware(multiplayer))(
|
|
74
|
+
this._api,
|
|
75
|
+
)
|
|
58
76
|
}
|
|
59
77
|
|
|
60
|
-
this.
|
|
61
|
-
this._loop = new Loop[this._config.loop.type]()
|
|
62
|
-
this._api = createApi(this._store)
|
|
78
|
+
this._loop = new Loops[this._config.loop.type]()
|
|
63
79
|
|
|
64
80
|
// The renderer might need the engine instance to initialize itself (e.g., to set up DOM events).
|
|
65
81
|
this._config.renderer?.init(this)
|
|
66
82
|
|
|
67
|
-
|
|
68
|
-
const devMode = this._config.entities.game?.devMode
|
|
69
|
-
this._devMode = devMode
|
|
70
|
-
if (devMode) {
|
|
83
|
+
if (this._devMode) {
|
|
71
84
|
initDevTools(this._store)
|
|
72
85
|
}
|
|
73
86
|
}
|
|
@@ -76,7 +89,7 @@ export class Engine {
|
|
|
76
89
|
* Starts the game engine, initializing the loop and notifying the store.
|
|
77
90
|
*/
|
|
78
91
|
start() {
|
|
79
|
-
this.
|
|
92
|
+
this._api.notify("start")
|
|
80
93
|
this._loop.start(this, ONE_SECOND / this._config.loop.fps)
|
|
81
94
|
this.isRunning = true
|
|
82
95
|
}
|
|
@@ -85,8 +98,8 @@ export class Engine {
|
|
|
85
98
|
* Stops the game engine, halting the loop and notifying the store.
|
|
86
99
|
*/
|
|
87
100
|
stop() {
|
|
88
|
-
this.
|
|
89
|
-
this._store.update(
|
|
101
|
+
this._api.notify("stop")
|
|
102
|
+
this._store.update(this._api)
|
|
90
103
|
this._loop.stop()
|
|
91
104
|
this.isRunning = false
|
|
92
105
|
}
|
|
@@ -96,14 +109,15 @@ export class Engine {
|
|
|
96
109
|
* @param {number} dt - Delta time since the last update in milliseconds.
|
|
97
110
|
*/
|
|
98
111
|
update(dt) {
|
|
99
|
-
|
|
112
|
+
this._api.notify("update", dt)
|
|
113
|
+
const processedEvents = this._store.update(this._api)
|
|
100
114
|
const state = this._store.getState()
|
|
101
115
|
|
|
102
116
|
// Check for devMode changes and connect/disconnect dev tools accordingly.
|
|
103
117
|
const newDevMode = state.entities.game?.devMode
|
|
104
118
|
if (newDevMode !== this._devMode) {
|
|
105
119
|
if (newDevMode) {
|
|
106
|
-
initDevTools(this.
|
|
120
|
+
initDevTools(this._api)
|
|
107
121
|
} else {
|
|
108
122
|
disconnectDevTools()
|
|
109
123
|
}
|
|
@@ -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
|
+
}
|
|
@@ -16,9 +16,9 @@ export function applyMiddlewares(...middlewares) {
|
|
|
16
16
|
|
|
17
17
|
// The middleware API that can be passed to each middleware function.
|
|
18
18
|
const api = {
|
|
19
|
+
...store,
|
|
19
20
|
dispatch: (...args) => dispatch(...args),
|
|
20
|
-
|
|
21
|
-
setState: store.setState,
|
|
21
|
+
notify: (type, payload) => dispatch({ type, payload }),
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
// Create a chain of middleware functions.
|
|
@@ -27,10 +27,6 @@ export function applyMiddlewares(...middlewares) {
|
|
|
27
27
|
// Compose the middleware chain to create the final dispatch function.
|
|
28
28
|
dispatch = compose(...chain)(store.dispatch)
|
|
29
29
|
|
|
30
|
-
return
|
|
31
|
-
...store,
|
|
32
|
-
dispatch,
|
|
33
|
-
notify: (type, payload) => dispatch({ type, payload }),
|
|
34
|
-
}
|
|
30
|
+
return api
|
|
35
31
|
}
|
|
36
32
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { extend } from "@inglorious/utils/data-structures/objects.js"
|
|
2
2
|
|
|
3
|
-
import { coreEvents } from "
|
|
3
|
+
import { coreEvents } from "../core-events.js"
|
|
4
4
|
|
|
5
5
|
// A constant for the server's WebSocket URL.
|
|
6
6
|
const DEFAULT_SERVER_URL = `ws://${window.location.hostname}:3000`
|
|
@@ -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
|
-
}
|