diff --git a/examples/simpleCollision/index.html b/examples/simpleCollision/index.html index 68c3317a..76e69494 100644 --- a/examples/simpleCollision/index.html +++ b/examples/simpleCollision/index.html @@ -13,48 +13,44 @@ const physx = new SparkEngine.Physx(); const physicsSystem = new SparkEngine.PhysicsSystem(physx); - const firstGameObject = new SparkEngine.GameObject(); - firstGameObject.transform.size = { width: 20, height: 150 }; - firstGameObject.material.diffuseColor = new SparkEngine.Rgb(255, 0, 0); - const firstGameObjectBB = new SparkEngine.BoundingBoxComponent(); - firstGameObject.addComponent(firstGameObjectBB); - firstGameObjectBB.aabb = { ...firstGameObject.transform.position, ...firstGameObject.transform.size } - - const secondGameObject = new SparkEngine.GameObject(); - secondGameObject.transform.size = { width: 20, height: 150 }; - secondGameObject.transform.position = { x: 280, y: 0 }; - secondGameObject.material.diffuseColor = new SparkEngine.Rgb(0, 0, 255); - - secondGameObjectBB = new SparkEngine.BoundingBoxComponent(); - secondGameObject.addComponent(secondGameObjectBB); - secondGameObjectBB.aabb = { ...secondGameObject.transform.position, ...secondGameObject.transform.size } + const leftWall = new SparkEngine.StaticObject(); + leftWall.transform.size = { width: 20, height: 150 }; + leftWall.material.diffuseColor = new SparkEngine.Rgb(255, 0, 0); + leftWall.boundingBox.matchContainerTransform = true; + + const rightWall = new SparkEngine.StaticObject(); + rightWall.transform.size = { width: 20, height: 150 }; + rightWall.transform.position = { x: 280, y: 0 }; + rightWall.material.diffuseColor = new SparkEngine.Rgb(0, 0, 255); + rightWall.boundingBox.matchContainerTransform = true; const pingPongBall = new SparkEngine.GameObject(); pingPongBall.transform.size = { width: 20, height: 20 }; pingPongBall.transform.position = { x: 140, y: 70 }; pingPongBall.material.diffuseColor = new SparkEngine.Rgb(0, 255, 0); - const pingPongBoundingBox = new SparkEngine.BoundingBoxComponent() - pingPongBall.addComponent(pingPongBoundingBox); - pingPongBoundingBox.aabb = { ...pingPongBall.transform.position, ...pingPongBall.transform.size } - renderSystem.registerComponent(firstGameObject.shape); - renderSystem.registerComponent(secondGameObject.shape); + const pingPongBB = new SparkEngine.BoundingBoxComponent() + pingPongBB.matchContainerTransform = true; + + pingPongBall.addComponent(pingPongBB); + + renderSystem.registerComponent(leftWall.shape); + renderSystem.registerComponent(rightWall.shape); renderSystem.registerComponent(pingPongBall.shape); - physicsSystem.registerComponent(firstGameObjectBB); - physicsSystem.registerComponent(secondGameObjectBB); - physicsSystem.registerComponent(pingPongBoundingBox); + physicsSystem.registerComponent(leftWall.boundingBox); + physicsSystem.registerComponent(rightWall.boundingBox); + physicsSystem.registerComponent(pingPongBB); let velocity = 5; - pingPongBoundingBox.onCollisionCb = () => { + pingPongBB.onCollisionCb = () => { velocity *= -1; pingPongBall.transform.position.x += velocity; } const drawFrame = () => { pingPongBall.transform.position.x += velocity; - pingPongBoundingBox.aabb.x = pingPongBall.transform.position.x; physicsSystem.update(); physx.simulate(); diff --git a/src/ecs/components/BoundingBoxComponent.ts b/src/ecs/components/BoundingBoxComponent.ts index d02796b7..baea0251 100644 --- a/src/ecs/components/BoundingBoxComponent.ts +++ b/src/ecs/components/BoundingBoxComponent.ts @@ -1,6 +1,7 @@ import { Type } from "../../core"; import { PhysicsObject, Physx } from "../../physx"; import { BaseComponent } from "./BaseComponent"; +import { TransformComponent } from "./TransformComponent"; import { CollisionCallback, ICollidableComponent } from "./interfaces"; interface AABB { @@ -12,11 +13,19 @@ interface AABB { @Type('BoundingBoxComponent') export class BoundingBoxComponent extends BaseComponent implements ICollidableComponent { - public onCollisionCb: CollisionCallback | undefined; private defaultAABB: AABB = { x: 0, y: 0, width: 0, height: 0 }; + public onCollisionCb: CollisionCallback | undefined; + public matchContainerTransform: boolean = false; + public get aabb(): AABB { - return this.defaultAABB; + + const container = this.getContainer(); + const containerTransform = container ? container.getComponent('TransformComponent') : undefined; + + return this.matchContainerTransform && containerTransform + ? { ...containerTransform.position, ...containerTransform.size } + : this.defaultAABB } public set aabb(value: AABB) { diff --git a/src/ecs/entities/StaticObject.ts b/src/ecs/entities/StaticObject.ts new file mode 100644 index 00000000..fa01dc86 --- /dev/null +++ b/src/ecs/entities/StaticObject.ts @@ -0,0 +1,12 @@ +import { BoundingBoxComponent } from "../components"; +import { GameObject } from "./GameObject"; + +export class StaticObject extends GameObject { + public boundingBox = new BoundingBoxComponent(); + + constructor() { + super(); + + this.addComponent(this.boundingBox); + } +} \ No newline at end of file diff --git a/src/ecs/entities/index.ts b/src/ecs/entities/index.ts index 7084a62f..42c66002 100644 --- a/src/ecs/entities/index.ts +++ b/src/ecs/entities/index.ts @@ -1,3 +1,4 @@ export * from './BaseEntity'; export * from './IEntity'; -export * from './GameObject'; \ No newline at end of file +export * from './GameObject'; +export * from './StaticObject'; \ No newline at end of file diff --git a/test/unit/ecs/components/BoundingBoxComponent.test.ts b/test/unit/ecs/components/BoundingBoxComponent.test.ts index a662d3d8..7a41b9bc 100644 --- a/test/unit/ecs/components/BoundingBoxComponent.test.ts +++ b/test/unit/ecs/components/BoundingBoxComponent.test.ts @@ -1,4 +1,4 @@ -import { BoundingBoxComponent, ICollidableComponent, Physx } from "../../../../src"; +import { BaseEntity, BoundingBoxComponent, GameObject, ICollidableComponent, Physx } from "../../../../src"; describe('ecs/components/BoundingBoxComponent', () => { let bbComponent = new BoundingBoxComponent(); @@ -9,6 +9,69 @@ describe('ecs/components/BoundingBoxComponent', () => { physx = new Physx(); }) + describe('get aabb()', () => { + it.each([ + { + aabb: { x: 0, y: 0, width: 0, height: 0 }, + expected: { x: 0, y: 0, width: 0, height: 0 } + }, + { + aabb: { x: 5, y: 10, width: 15, height: 5 }, + expected: { x: 5, y: 10, width: 15, height: 5 } + } + ])('Should return the default aabb by default', (test) => { + bbComponent.aabb = test.aabb; + expect(bbComponent.aabb).toEqual(test.expected); + }) + + it('Should return the container entity AABB if .matchContainerTransform is true', () => { + const parentEntity = new GameObject(); + parentEntity.addComponent(bbComponent); + + bbComponent.matchContainerTransform = true; + + parentEntity.transform.position = { x: 15, y: 25 }; + parentEntity.transform.size = { width: 10, height: 10 }; + + expect(bbComponent.aabb).toEqual({ + x: 15, y: 25, width: 10, height: 10 + }) + }); + + it('Should return the default AABB if .matchContainerTransform is true but parent container is null', () => { + bbComponent.matchContainerTransform = true; + bbComponent.aabb.x = 555; + + expect(bbComponent.aabb).toEqual({ + x: 555, y: 0, width: 0, height: 0 + }) + }); + + it('Should return the default AABB if .matchContainerTransform is false', () => { + const parentEntity = new GameObject(); + parentEntity.addComponent(bbComponent); + + bbComponent.matchContainerTransform = false; + bbComponent.aabb.x = 555; + + expect(bbComponent.aabb).toEqual({ + x: 555, y: 0, width: 0, height: 0 + }) + }) + + it('Should return the default AABB if .matchContainerTransform is true but parent container transform is null', () => { + const parentEntity = new BaseEntity(); + parentEntity.addComponent(bbComponent); + + bbComponent.matchContainerTransform = true; + bbComponent.aabb.x = 555; + + expect(bbComponent.aabb).toEqual({ + x: 555, y: 0, width: 0, height: 0 + }) + }) + }) + describe('.update()', () => { it.each([{ @@ -24,15 +87,15 @@ describe('ecs/components/BoundingBoxComponent', () => { expect(physx.physicalWorld).toEqual(expect.arrayContaining([{ object: { aabb: test.expected - }, + }, onCollisionCallback: expect.any(Function) }])); }); it('Should trigger the onCollision callback when a collision is registered', () => { - const cbBBComponentA = jest.fn(() => {}); - const cbBBComponentB = jest.fn(() => {}); + const cbBBComponentA = jest.fn(() => { }); + const cbBBComponentB = jest.fn(() => { }); bbComponent.aabb.x = 5; bbComponent.aabb.y = 5; @@ -53,5 +116,5 @@ describe('ecs/components/BoundingBoxComponent', () => { expect(cbBBComponentA).toHaveBeenCalled(); expect(cbBBComponentB).toHaveBeenCalled(); }) - }) + }); }) \ No newline at end of file diff --git a/test/unit/ecs/entities/StaticObject.test.ts b/test/unit/ecs/entities/StaticObject.test.ts new file mode 100644 index 00000000..3dce1a2b --- /dev/null +++ b/test/unit/ecs/entities/StaticObject.test.ts @@ -0,0 +1,25 @@ +import { BoundingBoxComponent, GameObject, StaticObject } from "../../../../src" + +describe('ecs/entities/StaticObject', () => { + let staticObject = new StaticObject(); + + afterEach(() => { + staticObject = new StaticObject(); + }) + + describe('.constructor()', () => { + it('Should extend GameObject class', () => { + expect(staticObject).toBeInstanceOf(GameObject); + }) + + it('Should have a bounding box by default', () => { + expect(staticObject.boundingBox).toEqual(expect.objectContaining({ + aabb: { x: 0, y: 0, width: 0, height: 0 }, + })); + }) + + it('Should register this entity as a container of its default components', () => { + expect(staticObject.boundingBox.getContainer()).toBe(staticObject); + }) + }) +}) \ No newline at end of file