Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: [#2407] Repair ex.Actor.clone() #2414

Merged
merged 9 commits into from
Feb 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ are returned

### Fixed

- Fixed issue where `Actor.clone()` and `Entity.clone()` crashed.
- Fixed issue where zero mtv collisions cause erroneous precollision events to be fired in the `ArcadeSolver` and `RealisticSolver`
- Fixed issue where calling `.kill()` on a child entity would not remove it from the parent `Entity`
- Fixed issue where calling `.removeAllChildren()` would not remove all the children from the parent `Entity`
Expand Down
16 changes: 16 additions & 0 deletions src/engine/Actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,22 @@ export class Actor extends Entity implements Eventable, PointerEvents, CanInitia
}
}

public clone(): Actor {
const clone = new Actor({
color: this.color.clone(),
anchor: this.anchor.clone()
});
clone.clearComponents();
clone.processComponentRemoval();

// Clone the current actors components
const components = this.getComponents();
for (const c of components) {
clone.addComponent(c.clone(), true);
}
return clone;
}

/**
* `onInitialize` is called before the first update of the actor. This method is meant to be
* overridden. This is where initialization of child actors should take place.
Expand Down
5 changes: 5 additions & 0 deletions src/engine/Collision/BodyComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,4 +431,9 @@ export class BodyComponent extends Component<'ex.body'> implements Clonable<Body
this.oldVel.setTo(this.vel.x, this.vel.y);
this.oldAcc.setTo(this.acc.x, this.acc.y);
}

public clone(): BodyComponent {
const component = super.clone() as BodyComponent;
return component;
}
}
16 changes: 9 additions & 7 deletions src/engine/Collision/Colliders/PolygonCollider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,13 +401,15 @@ export class PolygonCollider extends Collider {
* @param transform
*/
public update(transform: Transform): void {
this._transform = transform;
this._transformedPointsDirty = true;
this._sidesDirty = true;
// This change means an update must be performed in order for geometry to update
const globalMat = transform.matrix ?? this._globalMatrix;
globalMat.clone(this._globalMatrix);
this._globalMatrix.translate(this.offset.x, this.offset.y);
if (transform) {
this._transform = transform;
this._transformedPointsDirty = true;
this._sidesDirty = true;
// This change means an update must be performed in order for geometry to update
const globalMat = transform.matrix ?? this._globalMatrix;
globalMat.clone(this._globalMatrix);
this._globalMatrix.translate(this.offset.x, this.offset.y);
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/engine/EntityComponentSystem/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export abstract class Component<TypeName extends string = string> {
/**
* Clones any properties on this component, if that property value has a `clone()` method it will be called
*/
clone(): this {
clone(): Component {
const newComponent = new (this.constructor as any)();
for (const prop in this) {
if (this.hasOwnProperty(prop)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,11 @@ export class TransformComponent extends Component<'ex.transform'> {
apply(v: Vector) {
return this._transform.apply(v);
}

clone(): TransformComponent {
const component = new TransformComponent();
component._transform = this._transform.clone();
component._z = this._z;
return component;
}
}
7 changes: 7 additions & 0 deletions src/engine/EntityComponentSystem/Entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,13 @@ export class Entity extends Class implements OnInitialize, OnPreUpdate, OnPostUp
return this as any;
}

public clearComponents() {
const components = this.getComponents();
for (const c of components) {
this.removeComponent(c);
}
}

private _removeComponentByType(type: string) {
if (this.has(type)) {
const component = this.get(type);
Expand Down
33 changes: 33 additions & 0 deletions src/engine/Graphics/GraphicsComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,12 @@ export class GraphicsLayer {
public get currentKeys(): string {
return this.name ?? 'anonymous';
}

public clone(graphicsComponent: GraphicsComponent): GraphicsLayer {
const layer = new GraphicsLayer({...this._options}, graphicsComponent);
layer.graphics = [...this.graphics.map(g => ({graphic: g.graphic, options: {...g.options}}))];
return layer;
}
}

export class GraphicsLayers {
Expand Down Expand Up @@ -233,6 +239,18 @@ export class GraphicsLayers {
private _getLayer(name: string): GraphicsLayer | undefined {
return this._layerMap[name];
}

public clone(graphicsComponent: GraphicsComponent): GraphicsLayers {
const layers = new GraphicsLayers(graphicsComponent);
layers._layerMap = {};
layers._layers = [];
layers.default = this.default.clone(graphicsComponent);
layers._maybeAddLayer(layers.default);
// Remove the default layer out of the clone
const clonedLayers = this._layers.filter(l => l.name !== 'default').map(l => l.clone(graphicsComponent));
clonedLayers.forEach(layer => layers._maybeAddLayer(layer));
return layers;
}
}

/**
Expand Down Expand Up @@ -433,4 +451,19 @@ export class GraphicsComponent extends Component<'ex.graphics'> {
}
}
}

public clone(): GraphicsComponent {
const graphics = new GraphicsComponent();
graphics._graphics = { ...this._graphics };
graphics.offset = this.offset.clone();
graphics.opacity = this.opacity;
graphics.anchor = this.anchor.clone();
graphics.copyGraphics = this.copyGraphics;
graphics.onPreDraw = this.onPreDraw;
graphics.onPostDraw = this.onPostDraw;
graphics.visible = this.visible;
graphics.layers = this.layers.clone(graphics);

return graphics;
}
}
1 change: 0 additions & 1 deletion src/engine/Graphics/SpriteFont.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,6 @@ export class SpriteFont extends Graphic implements FontRenderer {
private _cachedLines: string[];
private _cachedRenderWidth: number;
private _getLinesFromText(text: string, maxWidth?: number) {
Logger.getInstance().info(this.spacing);
if (this._cachedText === text && this._cachedRenderWidth === maxWidth) {
return this._cachedLines;
}
Expand Down
1 change: 1 addition & 0 deletions src/engine/Math/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,5 +225,6 @@ export class Transform {
target._rotation = this._rotation;
this._scale.clone(target._scale);
target.flagDirty();
return target;
}
}
35 changes: 35 additions & 0 deletions src/spec/ActorSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,41 @@ describe('A game actor', () => {
expect(actor2.pos.y).toBe(5);
});

it('can be cloned', () => {
const original = new ex.Actor({
width: 10,
height: 100,
anchor: ex.vec(0, 1),
color: ex.Color.Azure
});
original.pos = ex.vec(10, 20);
original.vel = ex.vec(30, 30);

const sut = original.clone();

expect(sut.get(ex.TransformComponent)).not.toBe(original.get(ex.TransformComponent));
expect(sut.get(ex.MotionComponent)).not.toBe(original.get(ex.MotionComponent));
expect(sut.get(ex.ActionsComponent)).not.toBe(original.get(ex.ActionsComponent));
expect(sut.get(ex.PointerComponent)).not.toBe(original.get(ex.PointerComponent));
expect(sut.get(ex.BodyComponent)).not.toBe(original.get(ex.BodyComponent));
expect(sut.get(ex.ColliderComponent)).not.toBe(original.get(ex.ColliderComponent));
expect(sut.get(ex.GraphicsComponent)).not.toBe(original.get(ex.GraphicsComponent));

// New refs
expect(sut).not.toBe(original);
expect(sut.id).not.toBe(original.id);
expect(sut.color).not.toBe(original.color);
expect(sut.anchor).not.toBe(original.anchor);

// Same values
expect(sut.pos).toBeVector(original.pos);
expect(sut.vel).toBeVector(original.vel);
expect(sut.width).toBe(original.width);
expect(sut.height).toBe(original.height);
expect(sut.anchor).toEqual(original.anchor);
expect(sut.color).toEqual(original.color);
});

it('should have default properties set', () => {
const actor = new ex.Actor();

Expand Down
49 changes: 48 additions & 1 deletion src/spec/BodyComponentSpec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as ex from '@excalibur';
import { BodyComponent } from '@excalibur';
import { BodyComponent, CollisionGroup, CollisionType } from '@excalibur';
import { ExcaliburMatchers } from 'excalibur-jasmine';

describe('A body component', () => {
Expand Down Expand Up @@ -49,4 +49,51 @@ describe('A body component', () => {
expect(body.globalPos).toBeVector(ex.vec(110, 110));
});

it('can be cloned', () => {
const body = new ex.BodyComponent();
const owner = new ex.Entity([body]);
body.collisionType = ex.CollisionType.Fixed;
body.group = ex.CollisionGroupManager.create('somegroup');
body.mass = 100;
body.canSleep = true;
body.bounciness = 1;
body.friction = .5;
body.useGravity = false;
body.limitDegreeOfFreedom.push(ex.DegreeOfFreedom.Rotation);
body.vel = ex.vec(1, 2);
body.acc = ex.vec(3, 4);
body.scaleFactor = ex.vec(5, 6);
body.angularVelocity = 7;
body.torque = 8;

const clone = owner.clone();

const sut = clone.get(ex.BodyComponent);

// Should be same value
expect(sut.vel).toBeVector(body.vel);
expect(sut.acc).toBeVector(body.acc);
expect(sut.scaleFactor).toBeVector(body.scaleFactor);
expect(sut.angularVelocity).toBe(body.angularVelocity);
expect(sut.torque).toBe(body.torque);
expect(sut.inertia).toBe(body.inertia);
expect(sut.collisionType).toEqual(body.collisionType);
expect(sut.group).toEqual(body.group);
expect(sut.mass).toEqual(body.mass);
expect(sut.canSleep).toEqual(body.canSleep);
expect(sut.bounciness).toEqual(body.bounciness);
expect(sut.friction).toEqual(body.friction);
expect(sut.useGravity).toEqual(body.useGravity);
expect(sut.limitDegreeOfFreedom).toEqual(body.limitDegreeOfFreedom);

// Should be new refs
expect(sut).not.toBe(body);
expect(sut.vel).not.toBe(body.vel);
expect(sut.acc).not.toBe(body.acc);
expect(sut.scaleFactor).not.toBe(body.scaleFactor);

// Should have a new owner
expect(sut.owner).toBe(clone);
});

});
19 changes: 19 additions & 0 deletions src/spec/ColliderComponentSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,25 @@ describe('A ColliderComponent', () => {
expect(contacts.length).toBe(1);
});

it('can be cloned', () => {
const collider = new ex.ColliderComponent(ex.Shape.Circle(50));
const owner = new ex.Entity([collider]);

const clone = owner.clone();

const sut = clone.get(ex.ColliderComponent);

// Should be same value
expect(sut.get().bounds).toEqual(collider.get().bounds);
expect(sut.bounds).toEqual(collider.bounds);

// Should be new refs
expect(sut).not.toBe(collider);

// Should have a new owner
expect(sut.owner).toBe(clone);
});

it('can handle composite components', () => {
const compCollider = new ex.CompositeCollider([ex.Shape.Circle(50), ex.Shape.Box(200, 10)]);

Expand Down
49 changes: 49 additions & 0 deletions src/spec/GraphicsComponentSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,55 @@ describe('A Graphics ECS Component', () => {
expect(sut.graphics).toEqual({});
});

it('can be cloned', () => {
const graphics = new ex.GraphicsComponent();
const owner = new ex.Entity([graphics]);
const rect = new ex.Rectangle({
width: 40,
height: 40,
color: ex.Color.Red
});
const rect2 = new ex.Rectangle({
width: 40,
height: 40,
color: ex.Color.Blue
});
graphics.anchor = ex.vec(0, 0);
graphics.offset = ex.vec(1, 1);
graphics.opacity = .2;
graphics.visible = false;
graphics.copyGraphics = true;
graphics.onPreDraw = () => { /* do nothing */ };
graphics.onPostDraw = () => { /* do nothing */};
graphics.use(rect);
graphics.layers.create({name: 'background', order: -1}).use(rect2);

const clone = owner.clone();

const sut = clone.get(ex.GraphicsComponent);

// Should be same value
expect(sut.anchor).toBeVector(graphics.anchor);
expect(sut.offset).toBeVector(graphics.offset);
expect(sut.opacity).toEqual(graphics.opacity);
expect(sut.visible).toEqual(graphics.visible);
expect(sut.copyGraphics).toEqual(graphics.copyGraphics);
expect(sut.onPreDraw).toBe(sut.onPreDraw);
expect(sut.onPostDraw).toBe(sut.onPostDraw);
expect(sut.layers.get().length).toEqual(graphics.layers.get().length);
expect(sut.layers.get('background').graphics).toEqual(graphics.layers.get('background').graphics);

// Should be new refs
expect(sut).not.toBe(graphics);
expect(sut.offset).not.toBe(graphics.offset);
expect(sut.anchor).not.toBe(graphics.anchor);
expect(sut.layers.get()).not.toBe(graphics.layers.get());
expect(sut.layers.get('background').graphics).not.toBe(graphics.layers.get('background').graphics);

// Should have a new owner
expect(sut.owner).toBe(clone);
});

it('can be constructed with optional params', () => {
const rect = new ex.Rectangle({
width: 40,
Expand Down
40 changes: 40 additions & 0 deletions src/spec/MotionComponentSpec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as ex from '@excalibur';
import { ExcaliburMatchers } from 'excalibur-jasmine';

describe('A Motion Component', () => {
beforeAll(() => {
jasmine.addMatchers(ExcaliburMatchers);
});

it('can be cloned', () => {
const motion = new ex.MotionComponent();
const owner = new ex.Entity([motion]);
motion.vel = ex.vec(1, 2);
motion.acc = ex.vec(3, 4);
motion.scaleFactor = ex.vec(5, 6);
motion.angularVelocity = 7;
motion.torque = 8;
motion.inertia = 9;

const clone = owner.clone();

const sut = clone.get(ex.MotionComponent);

// Should be same value
expect(sut.vel).toBeVector(motion.vel);
expect(sut.acc).toBeVector(motion.acc);
expect(sut.scaleFactor).toBeVector(motion.scaleFactor);
expect(sut.angularVelocity).toBe(motion.angularVelocity);
expect(sut.torque).toBe(motion.torque);
expect(sut.inertia).toBe(motion.inertia);

// Should be new refs
expect(sut).not.toBe(motion);
expect(sut.vel).not.toBe(motion.vel);
expect(sut.acc).not.toBe(motion.acc);
expect(sut.scaleFactor).not.toBe(motion.scaleFactor);

// Should have a new owner
expect(sut.owner).toBe(clone);
});
});
Loading