Skip to content

Commit

Permalink
refactor: New ECS simplification (#2900)
Browse files Browse the repository at this point in the history
This PR includes the new ECS simplification to remove the "stringly" typed components/systems. 

- New simplified way to query entities `ex.World.query([MyComponentA, MyComponentB])`;
- New way to query for tags on entities `ex.World.queryTags(['A', 'B'])`
- Systems can be added as a constructor to a world, if they are the world will construct and pass a world instance to them
  ```typescript
  world.add(MySystem);
  ...

  class MySystem extends System {
    query: Query;
    constructor(world: World) {
      super()
      this.query = world.query([MyComponent]);
    }

    update(elapsed) {
      for (const entity of this.query.entities) {
        // do stuff
      }
    }
  }
  
  ```

- ECS implementation has been updated to remove the "stringly" typed nature of components & systems
  * For average users of Excalibur folks shouldn't notice any difference
  * For folks leveraging the ECS, Systems/Components no longer have type parameters based on strings. The type itself is used to track changes.
  * `class MySystem extends System<'ex.component'>` becomes `class MySystem extends System`
  * `class MyComponent extends Component<'ex.component'>` becomes `class MyComponent extends Component`
  * `ex.System.update(elapsedMs: number)` is only passed an elapsed time
  • Loading branch information
eonarheim authored Jan 23, 2024
1 parent df59264 commit 8f4d990
Show file tree
Hide file tree
Showing 58 changed files with 1,350 additions and 1,048 deletions.
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,39 @@ This project adheres to [Semantic Versioning](http://semver.org/).

- Remove confusing Graphics Layering from `ex.GraphicsComponent`, recommend we use the `ex.GraphicsGroup` to manage this behavior
* Update `ex.GraphicsGroup` to be consistent and use `offset` instead of `pos` for graphics relative positioning
- ECS implementation has been updated to remove the "stringly" typed nature of components & systems
* For average users of Excalibur folks shouldn't notice any difference
* For folks leveraging the ECS, Systems/Components no longer have type parameters based on strings. The type itself is used to track changes.
* `class MySystem extends System<'ex.component'>` becomes `class MySystem extends System`
* `class MyComponent extends Component<'ex.component'>` becomes `class MyComponent extends Component`
* `ex.System.update(elapsedMs: number)` is only passed an elapsed time
- Prevent people from inadvertently overriding `update()` in `ex.Scene` and `ex.Actor`. This method can still be overridden with the `//@ts-ignore` pragma


### Deprecated

-

### Added

- New simplified way to query entities `ex.World.query([MyComponentA, MyComponentB])`
- New way to query for tags on entities `ex.World.queryTags(['A', 'B'])`
- Systems can be added as a constructor to a world, if they are the world will construct and pass a world instance to them
```typescript
world.add(MySystem);
...

class MySystem extends System {
query: Query;
constructor(world: World) {
super()
this.query = world.query([MyComponent]);
}

update
}

```
- Added `RayCastHit`as part of every raycast not just the physics world query!
* Additionally added the ray distance and the contact normal for the surface
- Added the ability to log a message once to all log levels
Expand Down
19 changes: 12 additions & 7 deletions sandbox/tests/scenepredraw/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

var paddle = new ex.Actor({
x: 150, y: 150,
width: 200, height: 200,
Expand All @@ -24,14 +25,18 @@ class MyScene extends ex.Scene {
}


class CustomDraw extends ex.System<ex.GraphicsComponent> {
public readonly types = ['ex.graphics'] as const;
class CustomDraw extends ex.System {
public readonly systemType = ex.SystemType.Draw;

private _graphicsContext?: ex.ExcaliburGraphicsContext;
private _engine?: ex.Engine;
query: ex.Query<typeof ex.GraphicsComponent>;
constructor(public world: ex.World) {
super();
this.query = this.world.query([ex.GraphicsComponent]);
}

public initialize(scene: ex.Scene): void {
public initialize(world: ex.World, scene: ex.Scene): void {
this._graphicsContext = scene.engine.graphicsContext;
this._engine = scene.engine;
}
Expand All @@ -44,7 +49,7 @@ class CustomDraw extends ex.System<ex.GraphicsComponent> {
this._graphicsContext = this._engine.graphicsContext;
}

public update(entities: ex.Entity[], delta: number) {
public update( delta: number) {
if (this._graphicsContext == null) {
throw new Error("Uninitialized ObjectSystem");
}
Expand All @@ -63,9 +68,9 @@ var game = new ex.Engine({
height: 600,
});

var thescene = new MyScene();
thescene.world.add(new CustomDraw());
game.addScene('test', thescene);
var theScene = new MyScene();
theScene.world.add(CustomDraw);
game.addScene('test', theScene);
game.goToScene('test');

game.add(paddle);
Expand Down
7 changes: 5 additions & 2 deletions src/engine/Actions/ActionQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Action } from './Action';
export class ActionQueue {
private _entity: Entity;
private _actions: Action[] = [];
private _currentAction: Action;
private _currentAction: Action | null = null;
private _completedActions: Action[] = [];
constructor(entity: Entity) {
this._entity = entity;
Expand Down Expand Up @@ -100,7 +100,10 @@ export class ActionQueue {

if (this._currentAction.isComplete(this._entity)) {
this._entity.emit('actioncomplete', new ActionCompleteEvent(this._currentAction, this._entity));
this._completedActions.push(this._actions.shift());
const complete = this._actions.shift();
if (complete) {
this._completedActions.push(complete);
}
}
}
}
Expand Down
58 changes: 35 additions & 23 deletions src/engine/Actions/ActionsComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ import { Action } from './Action';

export interface ActionContextMethods extends Pick<ActionContext, keyof ActionContext> { };

export class ActionsComponent extends Component<'ex.actions'> implements ActionContextMethods {
public readonly type = 'ex.actions';
export class ActionsComponent extends Component implements ActionContextMethods {
dependencies = [TransformComponent, MotionComponent];
private _ctx: ActionContext;
private _ctx: ActionContext | null = null;

onAdd(entity: Entity) {
this._ctx = new ActionContext(entity);
Expand All @@ -25,16 +24,29 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC
this._ctx = null;
}

private _getCtx() {
if (!this._ctx) {
throw new Error('Actions component not attached to an entity, no context available');
}
return this._ctx;
}

/**
* Returns the internal action queue
* @returns action queue
*/
public getQueue(): ActionQueue {
return this._ctx?.getQueue();
if (!this._ctx) {
throw new Error('Actions component not attached to an entity, no queue available');
}
return this._ctx.getQueue();
}

public runAction(action: Action): ActionContext {
return this._ctx?.runAction(action);
if (!this._ctx) {
throw new Error('Actions component not attached to an entity, cannot run action');
}
return this._ctx.runAction(action);
}

/**
Expand Down Expand Up @@ -72,13 +84,13 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC
*/
public easeTo(x: number, y: number, duration: number, easingFcn?: EasingFunction): ActionContext;
public easeTo(...args: any[]): ActionContext {
return this._ctx.easeTo.apply(this._ctx, args);
return this._getCtx().easeTo.apply(this._ctx, args as any);
}

public easeBy(offset: Vector, duration: number, easingFcn?: EasingFunction): ActionContext;
public easeBy(offsetX: number, offsetY: number, duration: number, easingFcn?: EasingFunction): ActionContext;
public easeBy(...args: any[]): ActionContext {
return this._ctx.easeBy.apply(this._ctx, args);
return this._getCtx().easeBy.apply(this._ctx, args as any);
}

/**
Expand All @@ -99,7 +111,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC
*/
public moveTo(x: number, y: number, speed: number): ActionContext;
public moveTo(xOrPos: number | Vector, yOrSpeed: number, speedOrUndefined?: number): ActionContext {
return this._ctx.moveTo.apply(this._ctx, [xOrPos, yOrSpeed, speedOrUndefined]);
return this._getCtx().moveTo.apply(this._ctx, [xOrPos, yOrSpeed, speedOrUndefined] as any);
}

/**
Expand All @@ -118,7 +130,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC
*/
public moveBy(xOffset: number, yOffset: number, speed: number): ActionContext;
public moveBy(xOffsetOrVector: number | Vector, yOffsetOrSpeed: number, speedOrUndefined?: number): ActionContext {
return this._ctx.moveBy.apply(this._ctx, [xOffsetOrVector, yOffsetOrSpeed, speedOrUndefined]);
return this._getCtx().moveBy.apply(this._ctx, [xOffsetOrVector, yOffsetOrSpeed, speedOrUndefined] as any);
}

/**
Expand All @@ -130,7 +142,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC
* @param rotationType The [[RotationType]] to use for this rotation
*/
public rotateTo(angleRadians: number, speed: number, rotationType?: RotationType): ActionContext {
return this._ctx.rotateTo(angleRadians, speed, rotationType);
return this._getCtx().rotateTo(angleRadians, speed, rotationType);
}

/**
Expand All @@ -142,7 +154,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC
* @param rotationType The [[RotationType]] to use for this rotation, default is shortest path
*/
public rotateBy(angleRadiansOffset: number, speed: number, rotationType?: RotationType): ActionContext {
return this._ctx.rotateBy(angleRadiansOffset, speed, rotationType);
return this._getCtx().rotateBy(angleRadiansOffset, speed, rotationType);
}

/**
Expand Down Expand Up @@ -170,7 +182,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC
sizeYOrSpeed: number | Vector,
speedXOrUndefined?: number,
speedYOrUndefined?: number): ActionContext {
return this._ctx.scaleTo.apply(this._ctx, [sizeXOrVector, sizeYOrSpeed, speedXOrUndefined, speedYOrUndefined]);
return this._getCtx().scaleTo.apply(this._ctx, [sizeXOrVector, sizeYOrSpeed, speedXOrUndefined, speedYOrUndefined] as any);
}

/**
Expand All @@ -191,7 +203,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC
*/
public scaleBy(sizeOffsetX: number, sizeOffsetY: number, speed: number): ActionContext;
public scaleBy(sizeOffsetXOrVector: number | Vector, sizeOffsetYOrSpeed: number, speed?: number): ActionContext {
return this._ctx.scaleBy.apply(this._ctx, [sizeOffsetXOrVector, sizeOffsetYOrSpeed, speed]);
return this._getCtx().scaleBy.apply(this._ctx, [sizeOffsetXOrVector, sizeOffsetYOrSpeed, speed] as any);
}

/**
Expand All @@ -204,7 +216,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC
* @param numBlinks The number of times to blink
*/
public blink(timeVisible: number, timeNotVisible: number, numBlinks?: number): ActionContext {
return this._ctx.blink(timeVisible, timeNotVisible, numBlinks);
return this._getCtx().blink(timeVisible, timeNotVisible, numBlinks);
}

/**
Expand All @@ -215,7 +227,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC
* @param time The time it should take to fade the actor (in milliseconds)
*/
public fade(opacity: number, time: number): ActionContext {
return this._ctx.fade(opacity, time);
return this._getCtx().fade(opacity, time);
}

/**
Expand All @@ -225,7 +237,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC
* @param time The amount of time to delay the next action in the queue from executing in milliseconds
*/
public delay(time: number): ActionContext {
return this._ctx.delay(time);
return this._getCtx().delay(time);
}

/**
Expand All @@ -234,7 +246,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC
* action queue after this action will not be executed.
*/
public die(): ActionContext {
return this._ctx.die();
return this._getCtx().die();
}

/**
Expand All @@ -243,7 +255,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC
* action, i.e An actor arrives at a destination after traversing a path
*/
public callMethod(method: () => any): ActionContext {
return this._ctx.callMethod(method);
return this._getCtx().callMethod(method);
}

/**
Expand All @@ -264,7 +276,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC
* will repeat forever
*/
public repeat(repeatBuilder: (repeatContext: ActionContext) => any, times?: number): ActionContext {
return this._ctx.repeat(repeatBuilder, times);
return this._getCtx().repeat(repeatBuilder, times);
}

/**
Expand All @@ -283,7 +295,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC
* @param repeatBuilder The builder to specify the repeatable list of actions
*/
public repeatForever(repeatBuilder: (repeatContext: ActionContext) => any): ActionContext {
return this._ctx.repeatForever(repeatBuilder);
return this._getCtx().repeatForever(repeatBuilder);
}

/**
Expand All @@ -292,7 +304,7 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC
* @param followDistance The distance to maintain when following, if not specified the actor will follow at the current distance.
*/
public follow(entity: Actor, followDistance?: number): ActionContext {
return this._ctx.follow(entity, followDistance);
return this._getCtx().follow(entity, followDistance);
}

/**
Expand All @@ -302,14 +314,14 @@ export class ActionsComponent extends Component<'ex.actions'> implements ActionC
* @param speed The speed in pixels per second to move, if not specified it will match the speed of the other actor
*/
public meet(entity: Actor, speed?: number): ActionContext {
return this._ctx.meet(entity, speed);
return this._getCtx().meet(entity, speed);
}

/**
* Returns a promise that resolves when the current action queue up to now
* is finished.
*/
public toPromise(): Promise<void> {
return this._ctx.toPromise();
return this._getCtx().toPromise();
}
}
31 changes: 15 additions & 16 deletions src/engine/Actions/ActionsSystem.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
import { Entity } from '../EntityComponentSystem';
import { AddedEntity, isAddedSystemEntity, RemovedEntity, System, SystemType } from '../EntityComponentSystem/System';
import { Query, SystemPriority, World } from '../EntityComponentSystem';
import { System, SystemType } from '../EntityComponentSystem/System';
import { ActionsComponent } from './ActionsComponent';


export class ActionsSystem extends System<ActionsComponent> {
public readonly types = ['ex.actions'] as const;
export class ActionsSystem extends System {
systemType = SystemType.Update;
priority = -1;

priority = SystemPriority.Higher;
private _actions: ActionsComponent[] = [];
public notify(entityAddedOrRemoved: AddedEntity | RemovedEntity): void {
if (isAddedSystemEntity(entityAddedOrRemoved)) {
const action = entityAddedOrRemoved.data.get(ActionsComponent);
this._actions.push(action);
} else {
const action = entityAddedOrRemoved.data.get(ActionsComponent);
query: Query<typeof ActionsComponent>;

constructor(public world: World) {
super();
this.query = this.world.query([ActionsComponent]);

this.query.entityAdded$.subscribe(e => this._actions.push(e.get(ActionsComponent)));
this.query.entityRemoved$.subscribe(e => {
const action = e.get(ActionsComponent);
const index = this._actions.indexOf(action);
if (index > -1) {
this._actions.splice(index, 1);
}
}
});
}

update(_entities: Entity[], delta: number): void {
update(delta: number): void {
for (const actions of this._actions) {
actions.update(delta);
}
Expand Down
6 changes: 6 additions & 0 deletions src/engine/Actions/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"strict": true
}
}
2 changes: 1 addition & 1 deletion src/engine/Actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ export class Actor extends Entity implements Eventable, PointerEvents, CanInitia
...config
};

this._setName(name);
this.name = name ?? this.name;
this.anchor = anchor ?? Actor.defaults.anchor.clone();
const tx = new TransformComponent();
this.addComponent(tx);
Expand Down
3 changes: 1 addition & 2 deletions src/engine/Collision/BodyComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ export enum DegreeOfFreedom {
* Body describes all the physical properties pos, vel, acc, rotation, angular velocity for the purpose of
* of physics simulation.
*/
export class BodyComponent extends Component<'ex.body'> implements Clonable<BodyComponent> {
public readonly type = 'ex.body';
export class BodyComponent extends Component implements Clonable<BodyComponent> {
public dependencies = [TransformComponent, MotionComponent];
public static _ID = 0;
public readonly id: Id<'body'> = createId('body', BodyComponent._ID++);
Expand Down
3 changes: 1 addition & 2 deletions src/engine/Collision/ColliderComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ import { Shape } from './Colliders/Shape';
import { EventEmitter } from '../EventEmitter';
import { Actor } from '../Actor';

export class ColliderComponent extends Component<'ex.collider'> {
public readonly type = 'ex.collider';
export class ColliderComponent extends Component {

public events = new EventEmitter();
/**
Expand Down
Loading

0 comments on commit 8f4d990

Please sign in to comment.