@inglorious/engine
Version:
A JavaScript game engine written with global state, immutability, and pure functions in mind. Have fun(ctional programming) with it!
160 lines (135 loc) • 4.74 kB
JavaScript
import { audio } from "@inglorious/engine/behaviors/audio.js"
import { game } from "@inglorious/engine/behaviors/game.js"
import { createApi } from "@inglorious/store/api.js"
import { createStore } from "@inglorious/store/store.js"
import { augmentType } from "@inglorious/store/types.js"
import { isArray } from "@inglorious/utils/data-structures/array.js"
import { extendWith } from "@inglorious/utils/data-structures/objects.js"
import { isVector } from "@inglorious/utils/math/vector.js"
import { v } from "@inglorious/utils/v.js"
import { coreEvents } from "./core-events.js"
import { disconnectDevTools, initDevTools, sendAction } from "./dev-tools.js"
import { Loops } from "./loops/loops.js"
import { entityPoolMiddleware } from "./middlewares/entity-pool/entity-pool-middleware.js"
import { EntityPools } from "./middlewares/entity-pool/entity-pools.js"
import { applyMiddlewares } from "./middlewares/middlewares.js"
import { multiplayerMiddleware } from "./middlewares/multiplayer-middleware.js"
// Default game configuration
// loop.type specifies the type of loop to use (defaults to "animationFrame").
const DEFAULT_GAME_CONFIG = {
loop: { type: "animationFrame", fps: 60 },
systems: [],
types: {
game: [game()],
audio: [audio()],
},
entities: {
// eslint-disable-next-line no-magic-numbers
game: { type: "game", size: v(800, 600) },
audio: { type: "audio", sounds: {} },
},
}
const ONE_SECOND = 1000 // Number of milliseconds in one second.
/**
* Engine class responsible for managing the game loop, state, and rendering.
*/
export class Engine {
/**
* @param {Object} [gameConfig] - Game-specific configuration.
* @param {Object} [renderer] - UI entity responsible for rendering. It must have a `render` method.
*/
constructor(...gameConfigs) {
this._config = extendWith(merger, DEFAULT_GAME_CONFIG, ...gameConfigs)
// Determine devMode from the entities config.
const devMode = this._config.entities.game?.devMode
this._devMode = devMode
// Add user-defined systems
const systems = [...(this._config.systems ?? [])]
this._store = createStore({ ...this._config, systems })
// Create API layer, with optional methods for debugging
this._api = createApi(this._store)
this._entityPools = new EntityPools()
this._api = applyMiddlewares(entityPoolMiddleware(this._entityPools))(
this._api,
)
this._api.getAllActivePoolEntities = () =>
this._entityPools.getAllActiveEntities()
if (this._devMode) {
this._api.getEntityPoolsStats = () => this._entityPools.getStats()
}
// Apply multiplayer if specified.
const multiplayer = this._config.entities.game?.multiplayer
if (multiplayer) {
this._api = applyMiddlewares(multiplayerMiddleware(multiplayer))(
this._api,
)
}
this._loop = new Loops[this._config.loop.type]()
if (this._devMode) {
initDevTools(this._store)
}
}
async init() {
return Promise.all(
Object.values(this._config.entities).map((entity) => {
const originalType = this._config.types[entity.type]
const type = augmentType(originalType)
return type.init?.(entity, null, this._api)
}),
)
}
/**
* Starts the game engine, initializing the loop and notifying the store.
*/
start() {
this._api.notify("start")
this._loop.start(this, ONE_SECOND / this._config.loop.fps)
}
/**
* Stops the game engine, halting the loop and notifying the store.
*/
stop() {
this._api.notify("stop")
this._store.update(this._api)
this._loop.stop()
}
/**
* Updates the game state.
* @param {number} dt - Delta time since the last update in milliseconds.
*/
update(dt) {
this._api.notify("update", dt)
const processedEvents = this._store.update(this._api)
const state = this._store.getState()
// Check for devMode changes and connect/disconnect dev tools accordingly.
const newDevMode = state.entities.game?.devMode
if (newDevMode !== this._devMode) {
if (newDevMode) {
initDevTools(this._store)
} else {
disconnectDevTools()
}
this._devMode = newDevMode
}
const eventsToLog = processedEvents.filter(
({ type }) => !coreEvents.includes(type),
)
if (eventsToLog.length) {
const action = {
type: eventsToLog.map(({ type }) => type).join("|"),
payload: eventsToLog,
}
sendAction(action, state)
}
}
}
function merger(targetValue, sourceValue) {
if (
isArray(targetValue) &&
!isVector(targetValue) &&
isArray(sourceValue) &&
!isVector(sourceValue)
) {
return [...targetValue, ...sourceValue]
}
}