From 3ce1de28250df99e0f837ed85f0fb53949f55cef Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Sun, 4 Feb 2024 22:20:22 -0600 Subject: [PATCH] fix: [#2896] Allow component subtypes to work on extend Actors (#2924) 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! ``` --- CHANGELOG.md | 19 ++++ src/engine/Actor.ts | 132 ++++++++++++++++++-------- src/engine/Graphics/Context/shader.ts | 3 + src/engine/Graphics/GraphicsSystem.ts | 16 +++- src/engine/Particles.ts | 4 +- src/spec/MaterialRendererSpec.ts | 10 +- 6 files changed, 133 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f38b916a..069241e66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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! @@ -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 diff --git a/src/engine/Actor.ts b/src/engine/Actor.ts index 864da3528..77f32532d 100644 --- a/src/engine/Actor.ts +++ b/src/engine/Actor.ts @@ -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 @@ -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 */ @@ -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 */ @@ -227,44 +237,32 @@ 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. @@ -272,9 +270,7 @@ export class Actor extends Entity implements Eventable, PointerEvents, CanInitia * 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 @@ -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 @@ -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; } @@ -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 */ @@ -526,7 +543,9 @@ export class Actor extends Entity implements Eventable, PointerEvents, CanInitia z, color, visible, + opacity, anchor, + offset, collisionType, collisionGroup } = { @@ -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 } } @@ -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; } diff --git a/src/engine/Graphics/Context/shader.ts b/src/engine/Graphics/Context/shader.ts index c0715589d..97d29dd60 100644 --- a/src/engine/Graphics/Context/shader.ts +++ b/src/engine/Graphics/Context/shader.ts @@ -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' | diff --git a/src/engine/Graphics/GraphicsSystem.ts b/src/engine/Graphics/GraphicsSystem.ts index 601ef6877..5915850fc 100644 --- a/src/engine/Graphics/GraphicsSystem.ts +++ b/src/engine/Graphics/GraphicsSystem.ts @@ -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) { @@ -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; @@ -184,7 +184,8 @@ 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; @@ -192,9 +193,13 @@ export class GraphicsSystem extends System { 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; @@ -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) { diff --git a/src/engine/Particles.ts b/src/engine/Particles.ts index 6fb3825b9..76463cb15 100644 --- a/src/engine/Particles.ts +++ b/src/engine/Particles.ts @@ -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. diff --git a/src/spec/MaterialRendererSpec.ts b/src/spec/MaterialRendererSpec.ts index 357b6cd25..d49f30774 100644 --- a/src/spec/MaterialRendererSpec.ts +++ b/src/spec/MaterialRendererSpec.ts @@ -54,7 +54,7 @@ describe('A Material', () => { const graphicsContext = new ex.ExcaliburGraphicsContextWebGL({ canvasElement: canvas, backgroundColor: ex.Color.Black, - smoothing: false, + antialiasing: false, snapToPixel: true }); const material = new ex.Material({ @@ -101,7 +101,7 @@ describe('A Material', () => { const context = new ex.ExcaliburGraphicsContextWebGL({ canvasElement: canvas, backgroundColor: ex.Color.ExcaliburBlue, - smoothing: false, + antialiasing: false, snapToPixel: true }); @@ -145,7 +145,7 @@ describe('A Material', () => { const context = new ex.ExcaliburGraphicsContextWebGL({ canvasElement: canvas, backgroundColor: ex.Color.ExcaliburBlue, - smoothing: false, + antialiasing: false, snapToPixel: true }); @@ -320,7 +320,7 @@ describe('A Material', () => { const graphicsContext = new ex.ExcaliburGraphicsContextWebGL({ canvasElement: canvas, backgroundColor: ex.Color.Black, - smoothing: false, + antialiasing: false, snapToPixel: true }); @@ -376,7 +376,7 @@ describe('A Material', () => { const graphicsContext = new ex.ExcaliburGraphicsContextWebGL({ canvasElement: canvas, backgroundColor: ex.Color.Black, - smoothing: false, + antialiasing: false, snapToPixel: true });