Skip to content

Commit

Permalink
fix: [#2896] Allow component subtypes to work on extend Actors (#2924)
Browse files Browse the repository at this point in the history
Closes #2896

- Fixed issue where Actor built in components could not be extended because of the way the Actor based type was built.
  - Actors now use instance properties for built-ins instead of getters
  - With the ECS refactor you can now subtype built-in `Components` and `.get(Builtin)` will return the correct subtype.
  ```typescript
  class MyBodyComponent extends ex.BodyComponent {}

  class MyActor extends ex.Actor {
      constructor() {
        super({})
        this.removeComponent(ex.BodyComponent);
        this.addComponent(new MyBodyComponent())
      }
  }

  const myActor = new MyActor();
  const myBody = myActor.get(ex.BodyComponent); // Returns the new MyBodyComponent subtype!
  ```
  • Loading branch information
eonarheim authored Feb 5, 2024
1 parent b95b4c5 commit 3ce1de2
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 51 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Added

- Added Graphics `opacity` on the Actor constructor `new ex.Actor({opacity: .5})`
- Added Graphics pixel `offset` on the Actor constructor `new ex.Actor({offset: ex.vec(-15, -15)})`
- Added new `new ex.Engine({uvPadding: .25})` option to allow users using texture atlases in their sprite sheets to configure this to avoid texture bleed. This can happen if you're sampling from images meant for pixel art
- Added new antialias settings for pixel art! This allows for smooth subpixel rendering of pixel art without shimmer/fat-pixel artifacts.
- Use `new ex.Engine({pixelArt: true})` to opt in to all the right defaults to make this work!
Expand Down Expand Up @@ -171,6 +173,23 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Fixed

- Fixed issue where Actor built in components could not be extended because of the way the Actor based type was built.
- Actors now use instance properties for built-ins instead of getters
- With the ECS refactor you can now subtype built-in `Components` and `.get(Builtin)` will return the correct subtype.
```typescript
class MyBodyComponent extends ex.BodyComponent {}

class MyActor extends ex.Actor {
constructor() {
super({})
this.removeComponent(ex.BodyComponent);
this.addComponent(new MyBodyComponent())
}
}

const myActor = new MyActor();
const myBody = myActor.get(ex.BodyComponent); // Returns the new MyBodyComponent subtype!
```
- Fixed issue with `snapToPixel` where the `ex.Camera` was not snapping correctly
- Fixed issue where using CSS transforms on the canvas confused Excalibur pointers
- Fixed issue with *AndFill suffixed [[DisplayModes]]s where content area offset was not accounted for in world space
Expand Down
132 changes: 93 additions & 39 deletions src/engine/Actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { Raster } from './Graphics/Raster';
import { Text } from './Graphics/Text';
import { CoordPlane } from './Math/coord-plane';
import { EventEmitter, EventKey, Handler, Subscription } from './EventEmitter';
import { Component } from './EntityComponentSystem';

/**
* Type guard for checking if something is an Actor
Expand Down Expand Up @@ -120,6 +121,11 @@ export interface ActorArgs {
* If a width/height or a radius was set a default graphic will be added
*/
color?: Color;
/**
* Optionally set the color of an actor, only used if no graphics are present
* If a width/height or a radius was set a default graphic will be added
*/
opacity?: number;
/**
* Optionally set the visibility of the actor
*/
Expand All @@ -128,6 +134,10 @@ export interface ActorArgs {
* Optionally set the anchor for graphics in the actor
*/
anchor?: Vector;
/**
* Optionally set the anchor for graphics in the actor
*/
offset?: Vector;
/**
* Optionally set the collision type
*/
Expand Down Expand Up @@ -227,54 +237,40 @@ export class Actor extends Entity implements Eventable, PointerEvents, CanInitia
* The physics body the is associated with this actor. The body is the container for all physical properties, like position, velocity,
* acceleration, mass, inertia, etc.
*/
public get body(): BodyComponent {
return this.get(BodyComponent);
}
public body: BodyComponent;

/**
* Access the Actor's built in [[TransformComponent]]
*/
public get transform(): TransformComponent {
return this.get(TransformComponent);
}
public transform: TransformComponent;

/**
* Access the Actor's built in [[MotionComponent]]
*/
public get motion(): MotionComponent {
return this.get(MotionComponent);
}
public motion: MotionComponent;

/**
* Access to the Actor's built in [[GraphicsComponent]]
*/
public get graphics(): GraphicsComponent {
return this.get(GraphicsComponent);
}
public graphics: GraphicsComponent;

/**
* Access to the Actor's built in [[ColliderComponent]]
*/
public get collider(): ColliderComponent {
return this.get(ColliderComponent);
}
public collider: ColliderComponent;

/**
* Access to the Actor's built in [[PointerComponent]] config
*/
public get pointer(): PointerComponent {
return this.get(PointerComponent);
}
public pointer: PointerComponent;

/**
* Useful for quickly scripting actor behavior, like moving to a place, patrolling back and forth, blinking, etc.
*
* Access to the Actor's built in [[ActionsComponent]] which forwards to the
* [[ActionContext|Action context]] of the actor.
*/
public get actions(): ActionsComponent {
return this.get(ActionsComponent);
}
public actions: ActionsComponent;

/**
* Gets the position vector of the actor in pixels
Expand Down Expand Up @@ -397,6 +393,7 @@ export class Actor extends Entity implements Eventable, PointerEvents, CanInitia
this.get(TransformComponent).scale = scale;
}

private _anchor: Vector = watch(Vector.Half, (v) => this._handleAnchorChange(v));
/**
* The anchor to apply all actor related transformations like rotation,
* translation, and scaling. By default the anchor is in the center of
Expand All @@ -408,7 +405,6 @@ export class Actor extends Entity implements Eventable, PointerEvents, CanInitia
* values between 0 and 1. For example, anchoring to the top-left would be
* `Actor.anchor.setTo(0, 0)` and top-right would be `Actor.anchor.setTo(0, 1)`.
*/
private _anchor: Vector = watch(Vector.Half, (v) => this._handleAnchorChange(v));
public get anchor(): Vector {
return this._anchor;
}
Expand All @@ -424,6 +420,27 @@ export class Actor extends Entity implements Eventable, PointerEvents, CanInitia
}
}

private _offset: Vector = watch(Vector.Zero, (v) => this._handleOffsetChange(v));
/**
* The offset in pixels to apply to all actor graphics
*
* Default offset of (0, 0)
*/
public get offset(): Vector {
return this._offset;
}

public set offset(vec: Vector) {
this._offset = watch(vec, (v) => this._handleOffsetChange(v));
this._handleOffsetChange(vec);
}

private _handleOffsetChange(v: Vector) {
if (this.graphics) {
this.graphics.offset = v;
}
}

/**
* Indicates whether the actor is physically in the viewport
*/
Expand Down Expand Up @@ -526,7 +543,9 @@ export class Actor extends Entity implements Eventable, PointerEvents, CanInitia
z,
color,
visible,
opacity,
anchor,
offset,
collisionType,
collisionGroup
} = {
Expand All @@ -535,41 +554,54 @@ export class Actor extends Entity implements Eventable, PointerEvents, CanInitia

this.name = name ?? this.name;
this.anchor = anchor ?? Actor.defaults.anchor.clone();
const tx = new TransformComponent();
this.addComponent(tx);
this.offset = offset ?? Vector.Zero;
this.transform = new TransformComponent();
this.addComponent(this.transform);
this.pos = pos ?? vec(x ?? 0, y ?? 0);
this.rotation = rotation ?? 0;
this.scale = scale ?? vec(1, 1);
this.z = z ?? 0;
tx.coordPlane = coordPlane ?? CoordPlane.World;
this.transform.coordPlane = coordPlane ?? CoordPlane.World;

this.addComponent(new PointerComponent);
this.pointer = new PointerComponent;
this.addComponent(this.pointer);

this.addComponent(new GraphicsComponent({
anchor: this.anchor
}));
this.addComponent(new MotionComponent());
this.graphics = new GraphicsComponent({
anchor: this.anchor,
offset: this.offset,
opacity: opacity
});
this.addComponent(this.graphics);

this.motion = new MotionComponent;
this.addComponent(this.motion);
this.vel = vel ?? Vector.Zero;
this.acc = acc ?? Vector.Zero;
this.angularVelocity = angularVelocity ?? 0;

this.addComponent(new ActionsComponent());
this.actions = new ActionsComponent;
this.addComponent(this.actions);

this.addComponent(new BodyComponent());
this.body = new BodyComponent;
this.addComponent(this.body);
this.body.collisionType = collisionType ?? CollisionType.Passive;
if (collisionGroup) {
this.body.group = collisionGroup;
}

if (collider) {
this.addComponent(new ColliderComponent(collider));
this.collider = new ColliderComponent(collider);
this.addComponent(this.collider);
} else if (radius) {
this.addComponent(new ColliderComponent(Shape.Circle(radius)));
this.collider = new ColliderComponent(Shape.Circle(radius));
this.addComponent(this.collider);
} else {
if (width > 0 && height > 0) {
this.addComponent(new ColliderComponent(Shape.Box(width, height, this.anchor)));
this.collider = new ColliderComponent(Shape.Box(width, height, this.anchor));
this.addComponent(this.collider);
} else {
this.addComponent(new ColliderComponent()); // no collider
this.collider = new ColliderComponent();
this.addComponent(this.collider); // no collider
}
}

Expand Down Expand Up @@ -599,15 +631,37 @@ export class Actor extends Entity implements Eventable, PointerEvents, CanInitia
public clone(): Actor {
const clone = new Actor({
color: this.color.clone(),
anchor: this.anchor.clone()
anchor: this.anchor.clone(),
offset: this.offset.clone()
});
clone.clearComponents();
clone.processComponentRemoval();

// Clone the current actors components
// Clone builtins, order is important, same as ctor
clone.addComponent(clone.transform = this.transform.clone() as TransformComponent, true);
clone.addComponent(clone.pointer = this.pointer.clone() as PointerComponent, true);
clone.addComponent(clone.graphics = this.graphics.clone() as GraphicsComponent, true);
clone.addComponent(clone.motion = this.motion.clone() as MotionComponent, true);
clone.addComponent(clone.actions = this.actions.clone() as ActionsComponent, true);
clone.addComponent(clone.body = this.body.clone() as BodyComponent, true);
clone.addComponent(clone.collider = this.collider.clone() as ColliderComponent, true);

const builtInComponents: Component[] = [
this.transform,
this.pointer,
this.graphics,
this.motion,
this.actions,
this.body,
this.collider
];

// Clone non-builtin the current actors components
const components = this.getComponents();
for (const c of components) {
clone.addComponent(c.clone(), true);
if (!builtInComponents.includes(c)) {
clone.addComponent(c.clone(), true);
}
}
return clone;
}
Expand Down
3 changes: 3 additions & 0 deletions src/engine/Graphics/Context/shader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { Color, Logger, Vector } from '../..';
import { Matrix } from '../../Math/matrix';
import { getAttributeComponentSize, getAttributePointerType } from './webgl-util';

/**
* List of the possible glsl uniform types
*/
export type UniformTypeNames =
'uniform1f' |
'uniform1i' |
Expand Down
16 changes: 11 additions & 5 deletions src/engine/Graphics/GraphicsSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export class GraphicsSystem extends System {
this._graphicsContext.opacity *= graphics.opacity * particleOpacity;

// Draw the graphics component
this._drawGraphicsComponent(graphics);
this._drawGraphicsComponent(graphics, transform);

// Optionally run the onPostDraw graphics lifecycle draw
if (graphics.onPostDraw) {
Expand All @@ -173,7 +173,7 @@ export class GraphicsSystem extends System {
this._graphicsContext.restore();
}

private _drawGraphicsComponent(graphicsComponent: GraphicsComponent) {
private _drawGraphicsComponent(graphicsComponent: GraphicsComponent, transformComponent: TransformComponent) {
if (graphicsComponent.visible) {
const flipHorizontal = graphicsComponent.flipHorizontal;
const flipVertical = graphicsComponent.flipVertical;
Expand All @@ -184,17 +184,22 @@ export class GraphicsSystem extends System {
if (graphic) {
let anchor = graphicsComponent.anchor;
let offset = graphicsComponent.offset;

let scaleX = 1;
let scaleY = 1;
// handle specific overrides
if (options?.anchor) {
anchor = options.anchor;
}
if (options?.offset) {
offset = options.offset;
}
const globalScale = transformComponent.globalScale;
scaleX *= graphic.scale.x * globalScale.x;
scaleY *= graphic.scale.y * globalScale.y;

// See https://github.com/excaliburjs/Excalibur/pull/619 for discussion on this formula
const offsetX = -graphic.width * anchor.x + offset.x;
const offsetY = -graphic.height * anchor.y + offset.y;
const offsetX = -graphic.width * anchor.x + offset.x * scaleX;
const offsetY = -graphic.height * anchor.y + offset.y * scaleY;

const oldFlipHorizontal = graphic.flipHorizontal;
const oldFlipVertical = graphic.flipVertical;
Expand All @@ -214,6 +219,7 @@ export class GraphicsSystem extends System {
graphic.flipVertical = oldFlipVertical;
}

// TODO move debug code out?
if (this._engine?.isDebug && this._engine.debug.graphics.showBounds) {
const offset = vec(offsetX, offsetY);
if (graphic instanceof GraphicsGroup) {
Expand Down
4 changes: 2 additions & 2 deletions src/engine/Particles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,13 +360,13 @@ export class ParticleEmitter extends Actor {
* Gets the opacity of each particle from 0 to 1.0
*/
public get opacity(): number {
return super.graphics.opacity;
return this.graphics.opacity;
}
/**
* Gets the opacity of each particle from 0 to 1.0
*/
public set opacity(opacity: number) {
super.graphics.opacity = opacity;
this.graphics.opacity = opacity;
}
/**
* Gets or sets the fade flag which causes particles to gradually fade out over the course of their life.
Expand Down
Loading

0 comments on commit 3ce1de2

Please sign in to comment.