0g
Advanced tools
Comparing version 0.3.0 to 0.3.1
@@ -32,3 +32,3 @@ import { ComponentPool } from './ComponentPool.js'; | ||
this.onComponentChanged = (component) => { | ||
this.game.enqueueOperation({ | ||
this.game.enqueuePhaseOperation({ | ||
op: 'markChanged', | ||
@@ -35,0 +35,0 @@ componentId: component.$.id, |
@@ -13,3 +13,2 @@ import { QueryManager } from './QueryManager.js'; | ||
import { type AssetLoaders, type BaseShape, type Globals } from './index.js'; | ||
import { EventSubscriber } from '@a-type/utils'; | ||
import { ComponentHandle } from './Component2.js'; | ||
@@ -21,5 +20,3 @@ export type GameConstants = { | ||
export type GameEvents = { | ||
preStep(): any; | ||
step(): any; | ||
postStep(): any; | ||
[phase: `phase:${string}`]: any; | ||
stepComplete(): any; | ||
@@ -29,7 +26,9 @@ preApplyOperations(): any; | ||
}; | ||
export declare class Game extends EventSubscriber<GameEvents> { | ||
export declare class Game { | ||
private events; | ||
private _queryManager; | ||
private _entityIds; | ||
private _archetypeManager; | ||
private _operationQueue; | ||
private _stepOperationQueue; | ||
private _phaseOperationQueue; | ||
private _componentManager; | ||
@@ -45,5 +44,6 @@ private _globals; | ||
private _constants; | ||
constructor({ assetLoaders, ignoreSystemsWarning, }?: { | ||
constructor({ assetLoaders, ignoreSystemsWarning, phases, }?: { | ||
assetLoaders?: AssetLoaders; | ||
ignoreSystemsWarning?: boolean; | ||
phases?: string[]; | ||
}); | ||
@@ -60,2 +60,3 @@ get entityIds(): IdManager; | ||
get entityPool(): ObjectPool<Entity<ComponentHandle, any>>; | ||
subscribe: <K extends keyof GameEvents>(event: K, listener: GameEvents[K]) => () => void; | ||
/** | ||
@@ -92,6 +93,8 @@ * Allocates a new entity id and enqueues an operation to create the entity at the next opportunity. | ||
step: (delta: number) => void; | ||
enqueueOperation: (operation: Operation) => void; | ||
enqueuePhaseOperation: (operation: Operation) => void; | ||
enqueueStepOperation: (operation: Operation) => void; | ||
private destroyEntity; | ||
private flushOperations; | ||
private flushPhaseOperations; | ||
private flushStepOperations; | ||
private applyOperation; | ||
} |
@@ -12,11 +12,13 @@ import { QueryManager } from './QueryManager.js'; | ||
import { allSystems } from './System.js'; | ||
export class Game extends EventSubscriber { | ||
constructor({ assetLoaders = {}, ignoreSystemsWarning, } = {}) { | ||
super(); | ||
export class Game { | ||
constructor({ assetLoaders = {}, ignoreSystemsWarning, phases, } = {}) { | ||
this.events = new EventSubscriber(); | ||
this._entityIds = new IdManager((...msgs) => console.debug('Entity IDs:', ...msgs)); | ||
this._operationQueue = []; | ||
// operations applied every step | ||
this._stepOperationQueue = []; | ||
// operations applied every phase | ||
this._phaseOperationQueue = []; | ||
this._globals = new Resources(); | ||
this._entityPool = new ObjectPool(() => new Entity(), (e) => e.reset()); | ||
this._removedList = new RemovedList(); | ||
// TODO: configurable? | ||
this._phases = ['preStep', 'step', 'postStep']; | ||
@@ -29,2 +31,8 @@ this._delta = 0; | ||
}; | ||
this.subscribe = (event, listener) => { | ||
if (event.startsWith('phase:') && !this._phases.includes(event.slice(6))) { | ||
throw new Error(`Unknown phase: ${event.slice(6)}. Known phases: ${this._phases.join(', ')}. Add this phase to your phases array in the Game constructor if you want to use it.`); | ||
} | ||
return this.events.subscribe(event, listener); | ||
}; | ||
/** | ||
@@ -35,3 +43,3 @@ * Allocates a new entity id and enqueues an operation to create the entity at the next opportunity. | ||
const id = this.entityIds.get(); | ||
this._operationQueue.push({ | ||
this.enqueueStepOperation({ | ||
op: 'createEntity', | ||
@@ -46,3 +54,3 @@ entityId: id, | ||
this.destroy = (id) => { | ||
this._operationQueue.push({ | ||
this.enqueueStepOperation({ | ||
op: 'removeEntity', | ||
@@ -57,3 +65,3 @@ entityId: id, | ||
const entityId = typeof entity === 'number' ? entity : entity.id; | ||
this._operationQueue.push({ | ||
this.enqueueStepOperation({ | ||
op: 'addComponent', | ||
@@ -70,3 +78,3 @@ entityId, | ||
const entityId = typeof entity === 'number' ? entity : entity.id; | ||
this._operationQueue.push({ | ||
this.enqueueStepOperation({ | ||
op: 'removeComponent', | ||
@@ -109,14 +117,18 @@ entityId, | ||
this._delta = delta; | ||
this._phases.forEach((phase) => { | ||
this.emit(phase); | ||
}); | ||
this.emit('destroyEntities'); | ||
for (const phase of this._phases) { | ||
this.events.emit(`phase:${phase}`); | ||
this.flushPhaseOperations(); | ||
} | ||
this.events.emit('destroyEntities'); | ||
this._removedList.flush(this.destroyEntity); | ||
this.emit('preApplyOperations'); | ||
this.flushOperations(); | ||
this.emit('stepComplete'); | ||
this.events.emit('preApplyOperations'); | ||
this.flushStepOperations(); | ||
this.events.emit('stepComplete'); | ||
}; | ||
this.enqueueOperation = (operation) => { | ||
this._operationQueue.push(operation); | ||
this.enqueuePhaseOperation = (operation) => { | ||
this._phaseOperationQueue.push(operation); | ||
}; | ||
this.enqueueStepOperation = (operation) => { | ||
this._stepOperationQueue.push(operation); | ||
}; | ||
this.destroyEntity = (entity) => { | ||
@@ -129,7 +141,12 @@ entity.components.forEach((instance) => { | ||
}; | ||
this.flushOperations = () => { | ||
while (this._operationQueue.length) { | ||
this.applyOperation(this._operationQueue.shift()); | ||
this.flushPhaseOperations = () => { | ||
while (this._phaseOperationQueue.length) { | ||
this.applyOperation(this._phaseOperationQueue.shift()); | ||
} | ||
}; | ||
this.flushStepOperations = () => { | ||
while (this._stepOperationQueue.length) { | ||
this.applyOperation(this._stepOperationQueue.shift()); | ||
} | ||
}; | ||
this.applyOperation = (operation) => { | ||
@@ -168,2 +185,3 @@ let instance; | ||
}; | ||
this._phases = phases !== null && phases !== void 0 ? phases : this._phases; | ||
this._componentManager = new ComponentManager(this); | ||
@@ -170,0 +188,0 @@ this._assets = new Assets(assetLoaders); |
@@ -6,2 +6,4 @@ export type KeyboardKey = string; | ||
private keysUp; | ||
private _blockBrowserShortcuts; | ||
set blockBrowserShortcuts(value: boolean); | ||
constructor(); | ||
@@ -8,0 +10,0 @@ private handleKeyDown; |
export class Keyboard { | ||
set blockBrowserShortcuts(value) { | ||
this._blockBrowserShortcuts = value; | ||
} | ||
constructor() { | ||
@@ -6,4 +9,11 @@ this.keysPressed = new Set(); | ||
this.keysUp = new Set(); | ||
this._blockBrowserShortcuts = false; | ||
this.handleKeyDown = (ev) => { | ||
if (ev.target === document.body && ev.key !== 'F5' && ev.key !== 'F12') { | ||
if (ev.target === document.body && | ||
(this._blockBrowserShortcuts || | ||
// allow F12 | ||
(ev.key !== 'F12' && | ||
// allow refresh shortcuts | ||
ev.key !== 'F5' && | ||
!(ev.key === 'r' && (ev.ctrlKey || ev.metaKey))))) { | ||
ev.preventDefault(); | ||
@@ -10,0 +20,0 @@ } |
@@ -15,2 +15,3 @@ import { ArchetypeManager } from './ArchetypeManager.js'; | ||
describe('Query', () => { | ||
let events; | ||
let game = null; | ||
@@ -37,3 +38,4 @@ // bootstrapping | ||
}); | ||
game = new EventSubscriber(); | ||
events = new EventSubscriber(); | ||
game = events; | ||
game.archetypeManager = archetypeManager; | ||
@@ -103,3 +105,3 @@ game.entityPool = { | ||
// reset frame tracking | ||
game.emit('preApplyOperations'); | ||
events.emit('preApplyOperations'); | ||
onAdded.mockClear(); | ||
@@ -111,3 +113,3 @@ expect(query.entities).toEqual([withA, withAB, withAD]); | ||
game.archetypeManager.addComponent(withC, ComponentA.create()); | ||
game.emit('stepComplete'); | ||
events.emit('stepComplete'); | ||
expect(query.entities).toEqual([withA, withAB, withAD, withC]); | ||
@@ -117,3 +119,3 @@ expect(query.addedIds).toEqual([withC]); | ||
expect(onAdded).toHaveBeenCalledTimes(1); | ||
game.emit('preApplyOperations'); | ||
events.emit('preApplyOperations'); | ||
onAdded.mockClear(); | ||
@@ -125,3 +127,3 @@ expect(query.entities).toEqual([withA, withAB, withAD, withC]); | ||
game.archetypeManager.removeComponent(withAD, ComponentA.id); | ||
game.emit('stepComplete'); | ||
events.emit('stepComplete'); | ||
expect(query.entities).toEqual([withA, withAB, withC]); | ||
@@ -131,3 +133,3 @@ expect(query.addedIds).toEqual([]); | ||
expect(onRemoved).toHaveBeenCalledTimes(1); | ||
game.emit('preApplyOperations'); | ||
events.emit('preApplyOperations'); | ||
onAdded.mockClear(); | ||
@@ -139,3 +141,3 @@ expect(query.entities).toEqual([withA, withAB, withC]); | ||
game.archetypeManager.addComponent(withA, ComponentC.create()); | ||
game.emit('stepComplete'); | ||
events.emit('stepComplete'); | ||
expect(query.entities).toEqual([withAB, withC, withA]); | ||
@@ -155,3 +157,3 @@ expect(query.addedIds).toEqual([]); | ||
query.subscribe('entityRemoved', onRemoved); | ||
game.emit('preApplyOperations'); | ||
events.emit('preApplyOperations'); | ||
}); | ||
@@ -162,3 +164,3 @@ it('emits entityAdded events when an entity is added to matching Archetype', () => { | ||
addEntity(202, [ComponentD.create()]); | ||
game.emit('stepComplete'); | ||
events.emit('stepComplete'); | ||
expect(onAdded).toHaveBeenCalledTimes(2); | ||
@@ -170,3 +172,3 @@ expect(onAdded).toHaveBeenNthCalledWith(1, 200); | ||
game.archetypeManager.removeComponent(withAB, ComponentA.id); | ||
game.emit('stepComplete'); | ||
events.emit('stepComplete'); | ||
expect(onRemoved).toHaveBeenCalledWith(withAB); | ||
@@ -173,0 +175,0 @@ expect(onAdded).not.toHaveBeenCalled(); |
@@ -5,4 +5,9 @@ import { Game } from './Game.js'; | ||
export declare const allSystems: ((game: Game) => void | (() => void))[]; | ||
export declare function system<Filter extends QueryComponentFilter>(filter: Filter, run: (entity: EntityImpostorFor<Filter>, game: Game) => void, phase?: 'step' | 'preStep' | 'postStep'): (game: Game) => () => void; | ||
type SystemRunner<Filter extends QueryComponentFilter, Result> = (entity: EntityImpostorFor<Filter>, game: Game, previousResult: Result) => Result; | ||
export declare function system<Filter extends QueryComponentFilter, Result = void>(filter: Filter, run: SystemRunner<Filter, Result>, { phase, initialResult, }?: { | ||
phase?: 'step' | 'preStep' | 'postStep' | (string & {}); | ||
initialResult?: Result; | ||
}): (game: Game) => () => void; | ||
/** @deprecated - use system */ | ||
export declare const makeSystem: typeof system; | ||
export {}; |
export const allSystems = new Array(); | ||
export function system(filter, run, phase = 'step') { | ||
export function system(filter, run, { phase = 'step', initialResult = undefined, } = {}) { | ||
function sys(game) { | ||
const query = game.queryManager.create(filter); | ||
let result; | ||
const entityResults = new WeakMap(); | ||
function onPhase() { | ||
var _a; | ||
let ent; | ||
for (ent of query) { | ||
run(ent, game); | ||
result = run(ent, game, (_a = entityResults.get(ent)) !== null && _a !== void 0 ? _a : initialResult); | ||
entityResults.set(ent, result); | ||
} | ||
} | ||
return game.subscribe(phase, onPhase); | ||
return game.subscribe(`phase:${phase}`, onPhase); | ||
} | ||
@@ -13,0 +17,0 @@ allSystems.push(sys); |
{ | ||
"name": "0g", | ||
"version": "0.3.0", | ||
"version": "0.3.1", | ||
"description": "", | ||
@@ -5,0 +5,0 @@ "type": "module", |
@@ -60,3 +60,3 @@ import { ComponentPool } from './ComponentPool.js'; | ||
private onComponentChanged = (component: ComponentInstanceInternal) => { | ||
this.game.enqueueOperation({ | ||
this.game.enqueuePhaseOperation({ | ||
op: 'markChanged', | ||
@@ -63,0 +63,0 @@ componentId: component.$.id, |
@@ -29,5 +29,3 @@ import { QueryManager } from './QueryManager.js'; | ||
export type GameEvents = { | ||
preStep(): any; | ||
step(): any; | ||
postStep(): any; | ||
[phase: `phase:${string}`]: any; | ||
stepComplete(): any; | ||
@@ -38,3 +36,4 @@ preApplyOperations(): any; | ||
export class Game extends EventSubscriber<GameEvents> { | ||
export class Game { | ||
private events = new EventSubscriber<GameEvents>(); | ||
private _queryManager: QueryManager; | ||
@@ -45,3 +44,6 @@ private _entityIds = new IdManager((...msgs) => | ||
private _archetypeManager: ArchetypeManager; | ||
private _operationQueue: OperationQueue = []; | ||
// operations applied every step | ||
private _stepOperationQueue: OperationQueue = []; | ||
// operations applied every phase | ||
private _phaseOperationQueue: OperationQueue = []; | ||
private _componentManager: ComponentManager; | ||
@@ -57,4 +59,3 @@ private _globals = new Resources<Globals>(); | ||
// TODO: configurable? | ||
private _phases = ['preStep', 'step', 'postStep'] as const; | ||
private _phases = ['preStep', 'step', 'postStep']; | ||
@@ -72,4 +73,9 @@ private _delta = 0; | ||
ignoreSystemsWarning, | ||
}: { assetLoaders?: AssetLoaders; ignoreSystemsWarning?: boolean } = {}) { | ||
super(); | ||
phases, | ||
}: { | ||
assetLoaders?: AssetLoaders; | ||
ignoreSystemsWarning?: boolean; | ||
phases?: string[]; | ||
} = {}) { | ||
this._phases = phases ?? this._phases; | ||
this._componentManager = new ComponentManager(this); | ||
@@ -122,2 +128,14 @@ this._assets = new Assets(assetLoaders); | ||
subscribe = <K extends keyof GameEvents>( | ||
event: K, | ||
listener: GameEvents[K], | ||
) => { | ||
if (event.startsWith('phase:') && !this._phases.includes(event.slice(6))) { | ||
throw new Error( | ||
`Unknown phase: ${event.slice(6)}. Known phases: ${this._phases.join(', ')}. Add this phase to your phases array in the Game constructor if you want to use it.`, | ||
); | ||
} | ||
return this.events.subscribe(event, listener); | ||
}; | ||
/** | ||
@@ -128,3 +146,3 @@ * Allocates a new entity id and enqueues an operation to create the entity at the next opportunity. | ||
const id = this.entityIds.get(); | ||
this._operationQueue.push({ | ||
this.enqueueStepOperation({ | ||
op: 'createEntity', | ||
@@ -140,3 +158,3 @@ entityId: id, | ||
destroy = (id: number) => { | ||
this._operationQueue.push({ | ||
this.enqueueStepOperation({ | ||
op: 'removeEntity', | ||
@@ -156,3 +174,3 @@ entityId: id, | ||
const entityId = typeof entity === 'number' ? entity : entity.id; | ||
this._operationQueue.push({ | ||
this.enqueueStepOperation({ | ||
op: 'addComponent', | ||
@@ -170,3 +188,3 @@ entityId, | ||
const entityId = typeof entity === 'number' ? entity : entity.id; | ||
this._operationQueue.push({ | ||
this.enqueueStepOperation({ | ||
op: 'removeComponent', | ||
@@ -222,16 +240,21 @@ entityId, | ||
this._delta = delta; | ||
this._phases.forEach((phase) => { | ||
this.emit(phase); | ||
}); | ||
this.emit('destroyEntities'); | ||
for (const phase of this._phases) { | ||
this.events.emit(`phase:${phase}`); | ||
this.flushPhaseOperations(); | ||
} | ||
this.events.emit('destroyEntities'); | ||
this._removedList.flush(this.destroyEntity); | ||
this.emit('preApplyOperations'); | ||
this.flushOperations(); | ||
this.emit('stepComplete'); | ||
this.events.emit('preApplyOperations'); | ||
this.flushStepOperations(); | ||
this.events.emit('stepComplete'); | ||
}; | ||
enqueueOperation = (operation: Operation) => { | ||
this._operationQueue.push(operation); | ||
enqueuePhaseOperation = (operation: Operation) => { | ||
this._phaseOperationQueue.push(operation); | ||
}; | ||
enqueueStepOperation = (operation: Operation) => { | ||
this._stepOperationQueue.push(operation); | ||
}; | ||
private destroyEntity = (entity: Entity) => { | ||
@@ -244,8 +267,14 @@ entity.components.forEach((instance) => { | ||
private flushOperations = () => { | ||
while (this._operationQueue.length) { | ||
this.applyOperation(this._operationQueue.shift()!); | ||
private flushPhaseOperations = () => { | ||
while (this._phaseOperationQueue.length) { | ||
this.applyOperation(this._phaseOperationQueue.shift()!); | ||
} | ||
}; | ||
private flushStepOperations = () => { | ||
while (this._stepOperationQueue.length) { | ||
this.applyOperation(this._stepOperationQueue.shift()!); | ||
} | ||
}; | ||
private applyOperation = (operation: Operation) => { | ||
@@ -252,0 +281,0 @@ let instance: ComponentInstanceInternal | undefined; |
@@ -8,2 +8,8 @@ export type KeyboardKey = string; | ||
private _blockBrowserShortcuts = false; | ||
set blockBrowserShortcuts(value: boolean) { | ||
this._blockBrowserShortcuts = value; | ||
} | ||
constructor() { | ||
@@ -15,3 +21,11 @@ window.addEventListener('keydown', this.handleKeyDown); | ||
private handleKeyDown = (ev: KeyboardEvent) => { | ||
if (ev.target === document.body && ev.key !== 'F5' && ev.key !== 'F12') { | ||
if ( | ||
ev.target === document.body && | ||
(this._blockBrowserShortcuts || | ||
// allow F12 | ||
(ev.key !== 'F12' && | ||
// allow refresh shortcuts | ||
ev.key !== 'F5' && | ||
!(ev.key === 'r' && (ev.ctrlKey || ev.metaKey)))) | ||
) { | ||
ev.preventDefault(); | ||
@@ -18,0 +32,0 @@ } |
@@ -24,2 +24,3 @@ import { ArchetypeManager } from './ArchetypeManager.js'; | ||
describe('Query', () => { | ||
let events: EventSubscriber<any>; | ||
let game: Game = null as any; | ||
@@ -48,3 +49,4 @@ | ||
} as any); | ||
game = new EventSubscriber() as any; | ||
events = new EventSubscriber(); | ||
game = events as any; | ||
(game as any).archetypeManager = archetypeManager; | ||
@@ -121,3 +123,3 @@ (game as any).entityPool = { | ||
// reset frame tracking | ||
game.emit('preApplyOperations'); | ||
events.emit('preApplyOperations'); | ||
onAdded.mockClear(); | ||
@@ -131,3 +133,3 @@ | ||
game.archetypeManager.addComponent(withC, ComponentA.create()); | ||
game.emit('stepComplete'); | ||
events.emit('stepComplete'); | ||
@@ -139,3 +141,3 @@ expect(query.entities).toEqual([withA, withAB, withAD, withC]); | ||
game.emit('preApplyOperations'); | ||
events.emit('preApplyOperations'); | ||
onAdded.mockClear(); | ||
@@ -149,3 +151,3 @@ | ||
game.archetypeManager.removeComponent(withAD, ComponentA.id); | ||
game.emit('stepComplete'); | ||
events.emit('stepComplete'); | ||
@@ -157,3 +159,3 @@ expect(query.entities).toEqual([withA, withAB, withC]); | ||
game.emit('preApplyOperations'); | ||
events.emit('preApplyOperations'); | ||
onAdded.mockClear(); | ||
@@ -167,3 +169,3 @@ | ||
game.archetypeManager.addComponent(withA, ComponentC.create()); | ||
game.emit('stepComplete'); | ||
events.emit('stepComplete'); | ||
@@ -186,3 +188,3 @@ expect(query.entities).toEqual([withAB, withC, withA]); | ||
query.subscribe('entityRemoved', onRemoved); | ||
game.emit('preApplyOperations'); | ||
events.emit('preApplyOperations'); | ||
}); | ||
@@ -194,3 +196,3 @@ | ||
addEntity(202, [ComponentD.create()]); | ||
game.emit('stepComplete'); | ||
events.emit('stepComplete'); | ||
expect(onAdded).toHaveBeenCalledTimes(2); | ||
@@ -203,3 +205,3 @@ expect(onAdded).toHaveBeenNthCalledWith(1, 200); | ||
game.archetypeManager.removeComponent(withAB, ComponentA.id); | ||
game.emit('stepComplete'); | ||
events.emit('stepComplete'); | ||
expect(onRemoved).toHaveBeenCalledWith(withAB); | ||
@@ -206,0 +208,0 @@ expect(onAdded).not.toHaveBeenCalled(); |
@@ -7,9 +7,25 @@ import { Game } from './Game.js'; | ||
export function system<Filter extends QueryComponentFilter>( | ||
type SystemRunner<Filter extends QueryComponentFilter, Result> = ( | ||
entity: EntityImpostorFor<Filter>, | ||
game: Game, | ||
previousResult: Result, | ||
) => Result; | ||
export function system<Filter extends QueryComponentFilter, Result = void>( | ||
filter: Filter, | ||
run: (entity: EntityImpostorFor<Filter>, game: Game) => void, | ||
phase: 'step' | 'preStep' | 'postStep' = 'step', | ||
run: SystemRunner<Filter, Result>, | ||
{ | ||
phase = 'step', | ||
initialResult = undefined, | ||
}: { | ||
// TS trick to show the default value in the signature | ||
// but still allow any string | ||
phase?: 'step' | 'preStep' | 'postStep' | (string & {}); | ||
initialResult?: Result; | ||
} = {}, | ||
) { | ||
function sys(game: Game) { | ||
const query = game.queryManager.create(filter); | ||
let result: Result; | ||
const entityResults = new WeakMap<EntityImpostorFor<Filter>, Result>(); | ||
@@ -19,7 +35,8 @@ function onPhase() { | ||
for (ent of query) { | ||
run(ent, game); | ||
result = run(ent, game, entityResults.get(ent) ?? initialResult!); | ||
entityResults.set(ent, result); | ||
} | ||
} | ||
return game.subscribe(phase, onPhase); | ||
return game.subscribe(`phase:${phase}`, onPhase); | ||
} | ||
@@ -26,0 +43,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
254725
4790