From ce1c19d86b281d6c024f6e846fb809958facf9ce Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Tue, 28 May 2019 17:34:27 -0500 Subject: [PATCH] [#1119] Decouple Actor from Collision System (#1125) Closes #1119 ## Changes: - New `ex.Collider` type which is the container for all collision related behavior and state. Actor is now extracted from collision. - Added interface `Clonable` to indicate if an object contains a clone method - Added interface `Eventable` to indicated if an object can emit and receive events - `ex.Vector.scale` now also works with vector input - `ex.BoundingBox.fromDimension(width: number, height: number)` can generate a bounding box from a width and height - `ex.BoundingBox.translate(pos: Vector)` will create a new bounding box shifted by `pos` - `ex.BoundingBox.scale(scale: Vector)` will create a new bounding box scaled by `scale` - Added `isActor()` and `isCollider()` type guards - Changed collision system to remove actor coupling, in addition `ex.Collider` is a new type that encapsulates all collision behavior. Use `ex.Actor.body.collider` to interact with collisions in Excalibur ([#1119](https://github.com/excaliburjs/Excalibur/issues/1119)) - Add new `ex.Collider` type that is the housing for all collision related code - The source of truth for `ex.CollisionType` is now on collider, with a convenience getter on actor - The collision system now operates on `ex.Collider`'s not `ex.Actor`'s - `ex.CollisionType` has been moved to a separate file outside of `Actor` - CollisionType is switched to a string enum, style guide also updated - `ex.CollisionPair` now operates on a pair of `ex.Colliders`'s instead of `ex.Actors`'s - `ex.CollisionContact` now operates on a pair of `ex.Collider`'s instead of `ex.Actors`'s - `ex.Body` has been modified to house all the physical position/transform information - Integration has been moved from actor to `Body` as a physical concern - `useBoxCollision` has been renamed to `useBoxCollider` - `useCircleCollision` has been renamed to `useCircleCollider` - `usePolygonCollision` has been renamed to `usePolygonCollider` - `useEdgeCollision` has been renamed to `useEdgeCollider` - Renamed `ex.CollisionArea` to `ex.CollisionShape` - `ex.CircleArea` has been renamed to `ex.Circle` - `ex.PolygonArea` has been renamed to `ex.ConvexPolygon` - `ex.EdgeArea` has been renamed to `ex.Edge` - Renamed `getWidth()` & `setWidth()` to property `width` - Actor and BoundingBox are affected - Renamed `getHeight()` & `setHeight()` to property `height` - Actor and BoundingBox are affected - Renamed `getCenter()` to the property `center` - Actor, BoundingBox, and Cell are affected - Renamed `getBounds()` to the property `bounds` - Actor, Collider, and Shapes are affected - Renamed `getRelativeBounds()` to the property `localBounds` - Actor, Collider, and Shapes are affected - Renamed `moi()` to the property `inertia` standing for moment of inertia - Renamed `restition` to the property `bounciness` - Moved `collisionType` to `Actor.body.collider.type` - Moved `Actor.integrate` to `Actor.body.integrate` - Removed `NaiveCollisionBroadphase` as it was no longer used - Renamed methods and properties will be available until `v0.24.0` - Deprecated collision attributes on actor, use `Actor.body.collider` - `Actor.x` & `Actor.y` will be removed in `v0.24.0` use `Actor.pos.x` & `Actor.pos.y` - `Actor.collisionArea` will be removed in `v0.24.0` use `Actor.body.collider.shape` - `Actor.getLeft()`, `Actor.getRight()`, `Actor.getTop()`, and `Actor.getBottom` are deprecated - Use `Actor.body.collider.bounds.(left|right|top|bottom)` - `Actor.getGeometry()` and `Actor.getRelativeGeometry()` are removed, use `Collider` - Collision related properties on Actor moved to `Collider`, use`Actor.body.collider` - `Actor.torque` - `Actor.mass` - `Actor.moi` - `Actor.friction` - `Actor.restition` - Collision related methods on Actor moved to `Collider`, use`Actor.body.collider` or `Actor.body.collider.bounds` - `Actor.getSideFromIntersect(intersect)` -> `BoundingBox.sideFromIntersection` - `Actor.collidesWithSide(actor)` -> `Actor.body.collider.bounds.intersectWithSide` - `Actor.collides(actor)` -> `Actor.body.collider.bounds.intersect` --- .github/CONTRIBUTING.md | 81 ++-- CHANGELOG.md | 64 ++- STYLEGUIDE.md | 36 ++ package-lock.json | 118 +++--- src/engine/Actor.ts | 380 +++++++++++------- src/engine/Algebra.ts | 13 +- src/engine/Camera.ts | 10 +- src/engine/Collision/Body.ts | 313 +++++++++------ src/engine/Collision/BoundingBox.ts | 348 +++++++++------- src/engine/Collision/Circle.ts | 304 ++++++++++++++ src/engine/Collision/CircleArea.ts | 216 ---------- src/engine/Collision/Collider.ts | 247 ++++++++++++ src/engine/Collision/CollisionArea.ts | 76 ---- src/engine/Collision/CollisionContact.ts | 107 +++-- src/engine/Collision/CollisionJumpTable.ts | 69 ++-- src/engine/Collision/CollisionResolver.ts | 5 +- src/engine/Collision/CollisionShape.ts | 106 +++++ src/engine/Collision/CollisionType.ts | 30 ++ .../{PolygonArea.ts => ConvexPolygon.ts} | 196 ++++++--- src/engine/Collision/DynamicTree.ts | 22 +- .../DynamicTreeCollisionBroadphase.ts | 74 ++-- src/engine/Collision/{EdgeArea.ts => Edge.ts} | 137 +++++-- src/engine/Collision/Index.ts | 11 +- .../Collision/NaiveCollisionBroadphase.ts | 118 ------ src/engine/Collision/Pair.ts | 30 +- src/engine/Collision/Shape.ts | 62 +++ src/engine/Collision/Side.ts | 10 +- src/engine/Deprecated.ts | 87 +--- src/engine/Docs/Actors.md | 6 +- src/engine/Docs/BoxAndPolygonShape.md | 37 ++ src/engine/Docs/CircleShape.md | 20 + src/engine/Docs/Constructors.md | 4 +- src/engine/Docs/EdgeShape.md | 21 + src/engine/Docs/Labels.md | 4 +- src/engine/Docs/Physics.md | 129 +++++- src/engine/Docs/Triggers.md | 2 +- src/engine/EventDispatcher.ts | 28 +- src/engine/Events.ts | 60 ++- src/engine/Interfaces/Clonable.ts | 3 + src/engine/Label.ts | 5 +- src/engine/Particles.ts | 11 +- src/engine/Scene.ts | 11 +- src/engine/TileMap.ts | 27 +- src/engine/Traits/EulerMovement.ts | 28 -- src/engine/Traits/Index.ts | 1 - src/engine/Traits/OffscreenCulling.ts | 2 +- .../Traits/TileMapCollisionDetection.ts | 10 +- src/engine/Trigger.ts | 15 +- src/engine/UIActor.ts | 7 +- src/engine/Util/CullingBox.ts | 2 +- src/engine/Util/Decorators.ts | 15 + src/engine/Util/Util.ts | 11 + src/engine/index.ts | 3 +- src/spec/ActorSpec.ts | 188 +++++---- src/spec/BoundingBoxSpec.ts | 64 +-- src/spec/CameraSpec.ts | 4 +- src/spec/CollisionContactSpec.ts | 66 +-- src/spec/CollisionGroupSpec.ts | 64 +-- ...isionAreaSpec.ts => CollisionShapeSpec.ts} | 303 ++++++++++++-- src/spec/CollisionSpec.ts | 76 ++-- src/spec/DynamicTreeBroadphaseSpec.ts | 26 +- src/spec/EngineSpec.ts | 3 +- src/spec/EventSpec.ts | 4 - src/spec/GroupSpec.ts | 13 +- src/spec/ParticleSpec.ts | 14 +- src/spec/PointerInputSpec.ts | 4 +- src/spec/ScaleSpec.ts | 2 +- src/spec/SceneSpec.ts | 40 +- src/spec/TimescalingSpec.ts | 4 +- src/spec/TriggerSpec.ts | 12 +- src/spec/UIActorSpec.ts | 7 +- src/spec/images/CollisionShapeSpec/circle.png | Bin 0 -> 6811 bytes src/spec/images/CollisionShapeSpec/edge.png | Bin 0 -> 7313 bytes .../images/CollisionShapeSpec/triangle.png | Bin 0 -> 7598 bytes 74 files changed, 2921 insertions(+), 1705 deletions(-) create mode 100644 src/engine/Collision/Circle.ts delete mode 100644 src/engine/Collision/CircleArea.ts create mode 100644 src/engine/Collision/Collider.ts delete mode 100644 src/engine/Collision/CollisionArea.ts create mode 100644 src/engine/Collision/CollisionShape.ts create mode 100644 src/engine/Collision/CollisionType.ts rename src/engine/Collision/{PolygonArea.ts => ConvexPolygon.ts} (59%) rename src/engine/Collision/{EdgeArea.ts => Edge.ts} (55%) delete mode 100644 src/engine/Collision/NaiveCollisionBroadphase.ts create mode 100644 src/engine/Collision/Shape.ts create mode 100644 src/engine/Docs/BoxAndPolygonShape.md create mode 100644 src/engine/Docs/CircleShape.md create mode 100644 src/engine/Docs/EdgeShape.md create mode 100644 src/engine/Interfaces/Clonable.ts delete mode 100644 src/engine/Traits/EulerMovement.ts rename src/spec/{CollisionAreaSpec.ts => CollisionShapeSpec.ts} (68%) create mode 100644 src/spec/images/CollisionShapeSpec/circle.png create mode 100644 src/spec/images/CollisionShapeSpec/edge.png create mode 100644 src/spec/images/CollisionShapeSpec/triangle.png diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 36ebd4fc..a0789a24 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,12 +1,15 @@ # How to Contribute #### Code of Conduct + This project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project, you agree to abide by its terms. #### Questions + Have questions? Ask them in our [forum]! #### Table of Contents + - [Reporting Bugs](#reporting-bugs) - [Suggesting Improvements](#suggesting-improvements) - [Submitting Changes](#submitting-changes) @@ -19,44 +22,49 @@ Have questions? Ask them in our [forum]! - [Tests](#tests) - [Documentation](#documentation) - [Issue Labels](#issue-labels) - ## Reporting Bugs + Before reporting a bug, please perform the following troubleshooting steps: -1. Check to see if the problem has already been reported - - Take a look through the list of [known bugs][search-label-bug] to see if someone has already created an issue that describes the problem you’re experiencing. If an issue already exists, consider adding any additional context you have about the problem in a comment on that issue. -2. Try the latest stable version of Excalibur - - If you’re not using the latest [release][releases], the problem may already be fixed. Please upgrade to the latest stable version and see if you still experience the problem. - - Alternatively, if you’re using a new unstable release, try rolling back to the latest stable release. -3. Try using older versions of Excalibur - - If you’re already using the latest release, try out the previous few versions. This will help us determine where the problem first appeared. -4. Try different browsers - - The problem you’re seeing may only appear in certain browsers or mobile devices. If you can, please try several different browsers/platforms to see if the issue persists. +1. Check to see if the problem has already been reported + - Take a look through the list of [known bugs][search-label-bug] to see if someone has already created an issue that describes the problem you’re experiencing. If an issue already exists, consider adding any additional context you have about the problem in a comment on that issue. +2. Try the latest stable version of Excalibur + - If you’re not using the latest [release][releases], the problem may already be fixed. Please upgrade to the latest stable version and see if you still experience the problem. + - Alternatively, if you’re using a new unstable release, try rolling back to the latest stable release. +3. Try using older versions of Excalibur + - If you’re already using the latest release, try out the previous few versions. This will help us determine where the problem first appeared. +4. Try different browsers + - The problem you’re seeing may only appear in certain browsers or mobile devices. If you can, please try several different browsers/platforms to see if the issue persists. ## Suggesting Improvements + Please do a quick search through our [backlog][issues] to see if your improvement has already been suggested. If so, feel free to provide additional comments or thoughts on the existing issue. ## Submitting Changes ### Getting Started + Below is the general workflow for submitting changes: -1. [Discuss an issue you want to contribute to](#discussing-a-contribution) -2. Create a fork of Excalibur -3. Commit to your fork with your initial changes -4. [Submit a work-in-progress pull request to discuss with the maintainers](#creating-a-pull-request) -5. Make changes to your pull request as needed -6. Once your changes are merged, celebrate! +1. [Discuss an issue you want to contribute to](#discussing-a-contribution) +2. Create a fork of Excalibur +3. Commit to your fork with your initial changes +4. [Submit a work-in-progress pull request to discuss with the maintainers](#creating-a-pull-request) +5. Make changes to your pull request as needed +6. Once your changes are merged, celebrate! If you’re not sure where to start, take a look at the "good first issue" or "help wanted" [issue labels](#issue-labels). + - Issues tagged with "good first issue" are designed as an introduction to contributing to open source and the Excalibur project as a whole. - Issues tagged with "help wanted" tend to be more involved than good first issues. #### Discussing a Contribution + It's helpful to let us know that you'd like to contribute for an issue, to prevent duplicate work. Ask us any questions you have about the issue, so that we can clarify the work you'll need to do. We're here to help! #### Creating a Pull Request + - Please ensure that there is an issue created for what you're working on. This helps us track the work being done! - Open a pull request as soon as you feel you have the beginning of something workable, or if you have design ideas to discuss. Getting feedback from us early will help you with your work! We will flag the pull request as Work-In-Progress while we work with you on your contribution. - Do all of your work in a new git branch. Only include code in the branch for the single issue you are working on. @@ -73,14 +81,16 @@ It's helpful to let us know that you'd like to contribute for an issue, to preve - Format your pull request title as: [#issue_number] Your commit message (where issue_number is the issue you're closing), and fill out the pull request template that automatically populates the editor window. Please format your pull request title according to our [commit message styleguide](#commit-messages). #### Deprecating Code + If you've replaced a piece of Excalibur's API, please mark it as `@obsolete` and provide the new preferred method of performing the same task. Don't forget to include which release it will be removed in! Deprecations are typically performed during the next release, so if your changes are made for the 0.1.0 release, they will be removed in 0.2.0. If the code you are deprecating is called anywhere else in Excalibur, or in any documentation, please update those places to use the new code you've written. -example: +example: + ```ts /** @obsolete use [[SomeClass]].someNewFunction instead **/ -@obsolete({message: 'ex.SomeClass.someFunction is deprecated, and will be removed in 0.2.0', +@obsolete({message: 'ex.SomeClass.someFunction is deprecated, and will be removed in 0.2.0', alternateMethod: 'SomeClass.someNewFunction'}) public someFunction() {...} ``` @@ -94,7 +104,7 @@ The Excalibur public API (i.e. `ex.*`) is defined in `src/engine/index.ts`. Any An example of exporting all public members from a new `MyClass.ts` that contains a `MyClass` ES6 class: ```ts -export * from './MyClass' +export * from './MyClass'; // ex.MyClass will be exposed ``` @@ -103,20 +113,24 @@ If the members should be aliased under a different name (namespaced) such as `ex ```ts // ex.Feature namespace import * as feature from './MyClass'; -export { feature as Feature } +export { feature as Feature }; // ex.Feature.MyClass will be exposed ``` ## Styleguides #### Code + A number of our code formatting rules are enforced via linting. When you build Excalibur on your computer, the linter will make sure that certain aspects of your code are formatted properly. Additionally: + - Use 3 spaces for indenting - All methods must explicitly specify their access modifier (public, private, etc.) - Use the CamelCase naming convention, with a lowercase first letter for variables. #### Commit Messages + Follow the guidelines below to help maintain a readable and informative git history: + - Use present tense verbs (“Fix bug where…” instead of “Fixed bug where…”) - Use imperative mood (“Add new feature” instead of “Adds new feature”) - Capitalize the first letter of the first line @@ -128,6 +142,7 @@ Follow the guidelines below to help maintain a readable and informative git hist - If your change is small, you may only need to write a single line commit message, e.x. “Fix typo in documentation” Here are the guidelines applied in a sample commit message, along with some additional helpful hints: + ``` Summarize what the commit does in <=50 characters @@ -152,43 +167,43 @@ bottom of your commit message. Resolves: #100 See also: #200, #300 ``` + #### Tests + All features, changes, and bug fixes must be tested by specifications (unit tests). Write tests to cover any potential scenarios your code introduces. Here’s an example: + ```javascript describe('a monkey', () => { - it('climbs trees', () => { - // put your spec here to show that monkeys climb trees - }); - describe('when the monkey is hungry', () => { - it('eats a banana', () => { - // put your spec here to show that this is true - }); + it('climbs trees', () => { + // put your spec here to show that monkeys climb trees + }); + describe('when the monkey is hungry', () => { + it('eats a banana', () => { + // put your spec here to show that this is true }); + }); }); ``` #### Documentation -- Add JSDoc comments to all public and protected methods + +- Add JSDoc comments to all public and protected methods - Link to other classes using the TypeDoc double bracket notation. ## Issue Labels + - [good first issue][search-label-good first issue]: issues that are good starting points for new contributors to open source - [help wanted][search-label-help wanted]: issues that are more in-depth and may require a certain platform or skillset to implement - [bug][search-label-bug]: a problem or an unexpected behavior If you'd like to contribute, these labels are good places to start. Our remaining labels are documented on the [Labels page](https://github.com/excaliburjs/Excalibur/labels). - - - [forum]: https://groups.google.com/forum/#!forum/excaliburjs [releases]: https://github.com/excaliburjs/Excalibur/releases [issues]: https://github.com/excaliburjs/Excalibur/issues - [keep-a-changelog]: http://keepachangelog.com/en/0.3.0/ - [search-label-good first issue]: https://github.com/excaliburjs/Excalibur/labels/good%20first%20issue [search-label-help wanted]: https://github.com/excaliburjs/Excalibur/labels/help%20wanted [search-label-bug]: https://github.com/excaliburjs/Excalibur/labels/bug diff --git a/CHANGELOG.md b/CHANGELOG.md index cf03d250..b09c56fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,15 +11,74 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added - +- New `ex.Collider` type which is the container for all collision related behavior and state. Actor is now extracted from collision. +- Added interface `Clonable` to indicate if an object contains a clone method +- Added interface `Eventable` to indicated if an object can emit and receive events +- `ex.Vector.scale` now also works with vector input +- `ex.BoundingBox.fromDimension(width: number, height: number)` can generate a bounding box from a width and height +- `ex.BoundingBox.translate(pos: Vector)` will create a new bounding box shifted by `pos` +- `ex.BoundingBox.scale(scale: Vector)` will create a new bounding box scaled by `scale` +- Added `isActor()` and `isCollider()` type guards +- Added `ex.CollisionShape.draw` collision shapes can now be drawn, actor's will use these shapes if no other drawing is specified ### Changed - Changed event handlers in excalibur to expect non-null event objects, before `hander: (event?: GameEvent) => void` implied that event could be null. This change addresses ([#1147](https://github.com/excaliburjs/Excalibur/issues/1147)) making strict null/function checks compatible with new typescript. +- Changed collision system to remove actor coupling, in addition `ex.Collider` is a new type that encapsulates all collision behavior. Use `ex.Actor.body.collider` to interact with collisions in Excalibur ([#1119](https://github.com/excaliburjs/Excalibur/issues/1119)) + + - Add new `ex.Collider` type that is the housing for all collision related code + - The source of truth for `ex.CollisionType` is now on collider, with a convenience getter on actor + - The collision system now operates on `ex.Collider`'s not `ex.Actor`'s + - `ex.CollisionType` has been moved to a separate file outside of `Actor` + - CollisionType is switched to a string enum, style guide also updated + - `ex.CollisionPair` now operates on a pair of `ex.Colliders`'s instead of `ex.Actors`'s + - `ex.CollisionContact` now operates on a pair of `ex.Collider`'s instead of `ex.Actors`'s + - `ex.Body` has been modified to house all the physical position/transform information + - Integration has been moved from actor to `Body` as a physical concern + - `useBoxCollision` has been renamed to `useBoxCollider` + - `useCircleCollision` has been renamed to `useCircleCollider` + - `usePolygonCollision` has been renamed to `usePolygonCollider` + - `useEdgeCollision` has been renamed to `useEdgeCollider` + - Renamed `ex.CollisionArea` to `ex.CollisionShape` + - `ex.CircleArea` has been renamed to `ex.Circle` + - `ex.PolygonArea` has been renamed to `ex.ConvexPolygon` + - `ex.EdgeArea` has been renamed to `ex.Edge` + - Renamed `getWidth()` & `setWidth()` to property `width` + - Actor and BoundingBox are affected + - Renamed `getHeight()` & `setHeight()` to property `height` + - Actor and BoundingBox are affected + - Renamed `getCenter()` to the property `center` + - Actor, BoundingBox, and Cell are affected + - Renamed `getBounds()` to the property `bounds` + - Actor, Collider, and Shapes are affected + - Renamed `getRelativeBounds()` to the property `localBounds` + - Actor, Collider, and Shapes are affected + - Renamed `moi()` to the property `inertia` standing for moment of inertia + - Renamed `restition` to the property `bounciness` + - Moved `collisionType` to `Actor.body.collider.type` + - Moved `Actor.integrate` to `Actor.body.integrate` ### Deprecated - +- Removed `NaiveCollisionBroadphase` as it was no longer used +- Renamed methods and properties will be available until `v0.24.0` +- Deprecated collision attributes on actor, use `Actor.body.collider` + + - `Actor.x` & `Actor.y` will be removed in `v0.24.0` use `Actor.pos.x` & `Actor.pos.y` + - `Actor.collisionArea` will be removed in `v0.24.0` use `Actor.body.collider.shape` + - `Actor.getLeft()`, `Actor.getRight()`, `Actor.getTop()`, and `Actor.getBottom` are deprecated + - Use `Actor.body.collider.bounds.(left|right|top|bottom)` + - `Actor.getGeometry()` and `Actor.getRelativeGeometry()` are removed, use `Collider` + - Collision related properties on Actor moved to `Collider`, use`Actor.body.collider` + - `Actor.torque` + - `Actor.mass` + - `Actor.moi` + - `Actor.friction` + - `Actor.restition` + - Collision related methods on Actor moved to `Collider`, use`Actor.body.collider` or `Actor.body.collider.bounds` + - `Actor.getSideFromIntersect(intersect)` -> `BoundingBox.sideFromIntersection` + - `Actor.collidesWithSide(actor)` -> `Actor.body.collider.bounds.intersectWithSide` + - `Actor.collides(actor)` -> `Actor.body.collider.bounds.intersect` ### Fixed @@ -27,6 +86,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Fixed polyfill application by exporting a `polyfill()` function that can be called. ([#1132](https://github.com/excaliburjs/Excalibur/issues/1132)) + - Fixed Color.lighten() ([#1084]) diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md index db2f087b..2c6eb5ac 100644 --- a/STYLEGUIDE.md +++ b/STYLEGUIDE.md @@ -98,3 +98,39 @@ class Engine { ### DON’T - Use an “I” prefix, do not use things like `IDrawable` should be `Drawable` + +## Enums + +#### DO + +- Use `number`ed or `string` enums, preferring `string` enums for more robust type support, robust refactorings, and debuggability at run-time + +```typescript +export enum CollisionType { + /** + * Actors with the `PreventCollision` setting do not participate in any + * collisions and do not raise collision events. + */ + PreventCollision = 'PreventCollision', + /** + * Actors with the `Passive` setting only raise collision events, but are not + * influenced or moved by other actors and do not influence or move other actors. + */ + Passive = 'Passive', + /** + * Actors with the `Active` setting raise collision events and participate + * in collisions with other actors and will be push or moved by actors sharing + * the `Active` or `Fixed` setting. + */ + Active = 'Active', + /** + * Actors with the `Fixed` setting raise collision events and participate in + * collisions with other actors. Actors with the `Fixed` setting will not be + * pushed or moved by other actors sharing the `Fixed`. Think of Fixed + * actors as "immovable/onstoppable" objects. If two `Fixed` actors meet they will + * not be pushed or moved by each other, they will not interact except to throw + * collision events. + */ + Fixed = 'Fixed' +} +``` diff --git a/package-lock.json b/package-lock.json index 10c0b36c..60351562 100644 --- a/package-lock.json +++ b/package-lock.json @@ -713,7 +713,7 @@ }, "async": { "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz", "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", "dev": true }, @@ -782,7 +782,7 @@ "dependencies": { "jsesc": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", "dev": true }, @@ -1105,7 +1105,7 @@ }, "browserify-aes": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "dev": true, "requires": { @@ -1150,7 +1150,7 @@ }, "browserify-rsa": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "dev": true, "requires": { @@ -1236,7 +1236,7 @@ }, "cacache": { "version": "10.0.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz", + "resolved": "http://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz", "integrity": "sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==", "dev": true, "requires": { @@ -1316,7 +1316,7 @@ }, "camelcase-keys": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "dev": true, "requires": { @@ -2047,7 +2047,7 @@ }, "colors": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "resolved": "http://registry.npmjs.org/colors/-/colors-1.1.2.tgz", "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", "dev": true }, @@ -2321,7 +2321,7 @@ }, "create-hash": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "dev": true, "requires": { @@ -2334,7 +2334,7 @@ }, "create-hmac": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "dev": true, "requires": { @@ -2702,7 +2702,7 @@ }, "diffie-hellman": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "dev": true, "requires": { @@ -2858,7 +2858,7 @@ }, "engine.io-client": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", + "resolved": "http://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", "dev": true, "requires": { @@ -2969,7 +2969,7 @@ }, "es6-promise": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-0.1.2.tgz", + "resolved": "http://registry.npmjs.org/es6-promise/-/es6-promise-0.1.2.tgz", "integrity": "sha1-8RLCn+paCZhTn8tqL9IUQ9KPBfc=", "dev": true }, @@ -3086,7 +3086,7 @@ }, "eventemitter2": { "version": "0.4.14", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "resolved": "http://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", "dev": true }, @@ -3881,7 +3881,7 @@ }, "fs-access": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz", "integrity": "sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=", "dev": true, "requires": { @@ -3942,7 +3942,7 @@ }, "get-stream": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", "dev": true }, @@ -4095,7 +4095,7 @@ "dependencies": { "semver": { "version": "4.3.6", - "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", + "resolved": "http://registry.npmjs.org/semver/-/semver-4.3.6.tgz", "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", "dev": true } @@ -4113,7 +4113,7 @@ "dependencies": { "semver": { "version": "4.3.6", - "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", + "resolved": "http://registry.npmjs.org/semver/-/semver-4.3.6.tgz", "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", "dev": true } @@ -4143,7 +4143,7 @@ }, "grunt-contrib-connect": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/grunt-contrib-connect/-/grunt-contrib-connect-1.0.2.tgz", + "resolved": "http://registry.npmjs.org/grunt-contrib-connect/-/grunt-contrib-connect-1.0.2.tgz", "integrity": "sha1-XPkzuRpnOGBEJzwLJERgPNmIebo=", "dev": true, "requires": { @@ -4318,7 +4318,7 @@ "dependencies": { "rimraf": { "version": "2.2.6", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.6.tgz", + "resolved": "http://registry.npmjs.org/rimraf/-/rimraf-2.2.6.tgz", "integrity": "sha1-xZWXVpsU2VatKcrMQr3d9fDqT0w=", "dev": true } @@ -4580,7 +4580,7 @@ }, "http-errors": { "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", "dev": true, "requires": { @@ -4866,7 +4866,7 @@ }, "is-accessor-descriptor": { "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "resolved": "http://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "dev": true, "requires": { @@ -4896,7 +4896,7 @@ }, "is-builtin-module": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "resolved": "http://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", "dev": true, "requires": { @@ -4914,7 +4914,7 @@ }, "is-data-descriptor": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "resolved": "http://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", "dev": true, "requires": { @@ -5444,7 +5444,7 @@ }, "jsesc": { "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", "dev": true }, @@ -5480,7 +5480,7 @@ }, "json5": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "resolved": "http://registry.npmjs.org/json5/-/json5-0.5.1.tgz", "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", "dev": true }, @@ -5691,7 +5691,7 @@ }, "load-json-file": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { @@ -5955,7 +5955,7 @@ }, "media-typer": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true }, @@ -5982,7 +5982,7 @@ }, "meow": { "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz", "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "dev": true, "requires": { @@ -6092,7 +6092,7 @@ }, "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, @@ -6137,7 +6137,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "requires": { @@ -6146,7 +6146,7 @@ "dependencies": { "minimist": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true } @@ -6274,7 +6274,7 @@ }, "ncp": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.5.1.tgz", + "resolved": "http://registry.npmjs.org/ncp/-/ncp-0.5.1.tgz", "integrity": "sha1-dDmFMW49tFkoG1hxaehFc1oFQ58=", "dev": true }, @@ -6335,7 +6335,7 @@ "dependencies": { "buffer": { "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "dev": true, "requires": { @@ -6513,7 +6513,7 @@ }, "opn": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/opn/-/opn-4.0.2.tgz", + "resolved": "http://registry.npmjs.org/opn/-/opn-4.0.2.tgz", "integrity": "sha1-erwi5kTf9jsKltWrfyeQwPAavJU=", "dev": true, "requires": { @@ -6533,7 +6533,7 @@ "dependencies": { "minimist": { "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", "dev": true }, @@ -6627,7 +6627,7 @@ }, "os-tmpdir": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, @@ -6751,7 +6751,7 @@ }, "path-browserify": { "version": "0.0.0", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "resolved": "http://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", "dev": true }, @@ -6772,7 +6772,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-key": { @@ -6825,7 +6825,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, @@ -7283,7 +7283,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -7359,13 +7359,13 @@ }, "regjsgen": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "resolved": "http://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", "dev": true }, "regjsparser": { "version": "0.1.5", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "resolved": "http://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", "dev": true, "requires": { @@ -7482,7 +7482,7 @@ }, "resolve": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "resolved": "http://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", "dev": true }, @@ -7563,7 +7563,7 @@ }, "safe-regex": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "dev": true, "requires": { @@ -7700,7 +7700,7 @@ }, "sha.js": { "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dev": true, "requires": { @@ -7725,7 +7725,7 @@ }, "shelljs": { "version": "0.2.6", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.2.6.tgz", + "resolved": "http://registry.npmjs.org/shelljs/-/shelljs-0.2.6.tgz", "integrity": "sha1-kEktcv/MgVmXa6umL7D2iE8MM3g=", "dev": true }, @@ -7920,7 +7920,7 @@ }, "socket.io-parser": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", + "resolved": "http://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==", "dev": true, "requires": { @@ -8223,7 +8223,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" @@ -8231,7 +8231,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" @@ -8248,7 +8248,7 @@ }, "strip-eof": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "dev": true }, @@ -8539,7 +8539,7 @@ }, "through2": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.1.tgz", + "resolved": "http://registry.npmjs.org/through2/-/through2-2.0.1.tgz", "integrity": "sha1-OE51MU1J8y3hLuu4E2uOtrXVnak=", "dev": true, "requires": { @@ -8555,7 +8555,7 @@ }, "readable-stream": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", "dev": true, "requires": { @@ -8569,7 +8569,7 @@ }, "string_decoder": { "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", "dev": true } @@ -8801,7 +8801,7 @@ }, "lodash": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.3.1.tgz", + "resolved": "http://registry.npmjs.org/lodash/-/lodash-1.3.1.tgz", "integrity": "sha1-pGY7U2hriV/wdOK6UE37dqjit3A=", "dev": true }, @@ -8887,7 +8887,7 @@ }, "underscore.string": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", + "resolved": "http://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", "integrity": "sha1-18D6KvXVoaZ/QlPa7pgTLnM/Dxk=", "dev": true }, @@ -9370,7 +9370,7 @@ }, "tty-browserify": { "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "resolved": "http://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", "dev": true }, @@ -9797,7 +9797,7 @@ }, "vm-browserify": { "version": "0.0.4", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "resolved": "http://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", "dev": true, "requires": { @@ -11769,7 +11769,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "dev": true, "requires": { @@ -11825,7 +11825,7 @@ }, "xmlbuilder": { "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", "dev": true }, diff --git a/src/engine/Actor.ts b/src/engine/Actor.ts index 6f7fa9fb..131fb39f 100644 --- a/src/engine/Actor.ts +++ b/src/engine/Actor.ts @@ -1,4 +1,3 @@ -import { Physics } from './Physics'; import { Class } from './Class'; import { BoundingBox } from './Collision/BoundingBox'; import { Texture } from './Resources/Texture'; @@ -35,7 +34,7 @@ import { Logger } from './Util/Log'; import { ActionContext } from './Actions/ActionContext'; import { ActionQueue } from './Actions/Action'; import { Vector } from './Algebra'; -import { CollisionArea } from './Collision/CollisionArea'; +import { CollisionShape } from './Collision/CollisionShape'; import { Body } from './Collision/Body'; import { Side } from './Collision/Side'; import { Eventable } from './Interfaces/Evented'; @@ -46,6 +45,14 @@ import * as Effects from './Drawing/SpriteEffects'; import * as Util from './Util/Util'; import * as Events from './Events'; import { PointerEvents } from './Interfaces/PointerEvents'; +import { CollisionType } from './Collision/CollisionType'; +import { obsolete } from './Util/Decorators'; +import { Collider } from './Collision/Collider'; +import { Shape } from './Collision/Shape'; + +export function isActor(x: any): x is Actor { + return x instanceof Actor; +} /** * [[include:Constructors.md]] @@ -62,7 +69,6 @@ export interface ActorArgs extends Partial { color?: Color; visible?: boolean; body?: Body; - collisionType?: CollisionType; } export interface ActorDefaults { @@ -95,33 +101,48 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv * 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 body: Body = new Body(this); + public get body(): Body { + return this._body; + } + + public set body(body: Body) { + this._body = body; + this._body.actor = this; + } + + private _body: Body; /** - * Gets the collision area shape to use for collision possible options are [CircleArea|circles], [PolygonArea|polygons], and - * [EdgeArea|edges]. + * Gets the collision geometry shape to use for collision possible options are [Circle|circles], [ConvexPolygon|polygons], and + * [Edge|edges]. + * @obsolete Use Actor.body.collider.shape, collisionArea will be removed in v0.24.0 */ - public get collisionArea(): CollisionArea { - return this.body.collisionArea; + @obsolete({ message: 'Actor.collisionArea will be removed in v0.24.0', alternateMethod: 'Actor.body.collider.shape' }) + public get collisionArea(): CollisionShape { + return this.body.collider.shape; } /** - * Gets the collision area shape to use for collision possible options are [CircleArea|circles], [PolygonArea|polygons], and - * [EdgeArea|edges]. + * Gets the collision geometry shape to use for collision possible options are [Circle|circles], [ConvexPolygon|polygons], and + * [Edge|edges]. + * @obsolete use Actor.body.collider.shape, collisionArea will be removed in v0.24.0 */ - public set collisionArea(area: CollisionArea) { - this.body.collisionArea = area; + public set collisionArea(area: CollisionShape) { + this.body.collider.shape = area; } /** * Gets the x position of the actor relative to it's parent (if any) + * @obsolete ex.Actor.x will be removed in v0.24.0, use ex.Actor.pos.x */ + @obsolete({ message: 'ex.Actor.x will be removed in v0.24.0', alternateMethod: 'ex.Actor.pos.x, or ex.Actor.body.pos.x' }) public get x(): number { return this.body.pos.x; } /** * Sets the x position of the actor relative to it's parent (if any) + * @obsolete ex.Actor.x will be removed in v0.24.0, use ex.Actor.pos.x */ public set x(theX: number) { this.body.pos.x = theX; @@ -129,13 +150,16 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv /** * Gets the y position of the actor relative to it's parent (if any) + * @obsolete ex.Actor.y will be removed in v0.24.0, use ex.Actor.pos.y */ + @obsolete({ message: 'ex.Actor.y will be removed in v0.24.0', alternateMethod: 'ex.Actor.pos.y, or ex.Actor.body.pos.y' }) public get y(): number { return this.body.pos.y; } /** * Sets the y position of the actor relative to it's parent (if any) + * @obsolete ex.Actor.y will be removed in v0.24.0, use ex.Actor.pos.y */ public set y(theY: number) { this.body.pos.y = theY; @@ -197,11 +221,6 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv this.body.oldVel.setTo(theVel.x, theVel.y); } - /** - * Gets/sets the acceleration of the actor from the last frame. This does not include the global acc [[Physics.acc]]. - */ - public oldAcc: Vector = Vector.Zero; - /** * Gets the acceleration vector of the actor in pixels/second/second. An acceleration pointing down such as (0, 100) may be * useful to simulate a gravitational effect. @@ -217,6 +236,20 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv this.body.acc.setTo(theAcc.x, theAcc.y); } + /** + * Sets the acceleration of the actor from the last frame. This does not include the global acc [[Physics.acc]]. + */ + public set oldAcc(theAcc: Vector) { + this.body.oldAcc.setTo(theAcc.x, theAcc.y); + } + + /** + * Gets the acceleration of the actor from the last frame. This does not include the global acc [[Physics.acc]]. + */ + public get oldAcc(): Vector { + return this.body.oldAcc; + } + /** * Gets the rotation of the actor in radians. 1 radian = 180/PI Degrees. */ @@ -247,13 +280,16 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv /** * Gets the current torque applied to the actor. Torque can be thought of as rotational force + * @obsolete ex.Actor.torque will be removed in v0.24.0, use ex.Actor.body.torque */ + @obsolete({ message: 'ex.Actor.torque will be removed in v0.24.0', alternateMethod: 'ex.Actor.body.torque' }) public get torque() { return this.body.torque; } /** * Sets the current torque applied to the actor. Torque can be thought of as rotational force + * @obsolete ex.Actor.torque will be removed in v0.24.0, use ex.Actor.body.torque */ public set torque(theTorque: number) { this.body.torque = theTorque; @@ -261,60 +297,71 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv /** * Get the current mass of the actor, mass can be thought of as the resistance to acceleration. + * @obsolete ex.Actor.mass will be removed in v0.24.0, use ex.Actor.body.collider.mass */ + @obsolete({ message: 'ex.Actor.mass will be removed in v0.24.0', alternateMethod: 'ex.Actor.body.collider.mass' }) public get mass() { - return this.body.mass; + return this.body.collider.mass; } /** * Sets the mass of the actor, mass can be thought of as the resistance to acceleration. + * @obsolete ex.Actor.mass will be removed in v0.24.0, use ex.Actor.body.collider.mass */ public set mass(theMass: number) { - this.body.mass = theMass; + this.body.collider.mass = theMass; } /** * Gets the current moment of inertia, moi can be thought of as the resistance to rotation. + * @obsolete ex.Actor.moi will be removed in v0.24.0, use ex.Actor.body.collider.inertia */ + @obsolete({ message: 'ex.Actor.moi will be removed in v0.24.0', alternateMethod: 'ex.Actor.body.collider.inertia' }) public get moi() { - return this.body.moi; + return this.body.collider.inertia; } /** * Sets the current moment of inertia, moi can be thought of as the resistance to rotation. + * @obsolete ex.Actor.moi will be removed in v0.24.0, use ex.Actor.body.collider.inertia */ public set moi(theMoi: number) { - this.body.moi = theMoi; + this.body.collider.inertia = theMoi; } /** * Gets the coefficient of friction on this actor, this can be thought of as how sticky or slippery an object is. + * @obsolete ex.Actor.friction will be removed in v0.24.0, use ex.Actor.body.collider.friction */ + @obsolete({ message: 'ex.Actor.friction will be removed in v0.24.0', alternateMethod: 'ex.Actor.body.collider.friction' }) public get friction() { - return this.body.friction; + return this.body.collider.friction; } /** * Sets the coefficient of friction of this actor, this can ve thought of as how stick or slippery an object is. */ public set friction(theFriction: number) { - this.body.friction = theFriction; + this.body.collider.friction = theFriction; } /** * Gets the coefficient of restitution of this actor, represents the amount of energy preserved after collision. Think of this * as bounciness. + * @obsolete ex.Actor.restitution will be removed in v0.24.0, use ex.Actor.body.collider.restitution */ + @obsolete({ message: 'ex.Actor.restitution will be removed in v0.24.0', alternateMethod: 'ex.Actor.body.collider.bounciness' }) public get restitution() { - return this.body.restitution; + return this.body.collider.bounciness; } /** * Sets the coefficient of restitution of this actor, represents the amount of energy preserved after collision. Think of this * as bounciness. + * @obsolete ex.Actor.restitution will be removed in v0.24.0, use ex.Actor.body.collider.restitution */ public set restitution(theRestitution: number) { - this.body.restitution = theRestitution; + this.body.collider.bounciness = theRestitution; } /** @@ -334,23 +381,61 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv private _width: number = 0; /** - * The scale vector of the actor + * Gets the scale vector of the actor + */ + public get scale(): Vector { + return this.body.scale; + } + + /** + * Sets the scale vector of the actor + */ + public set scale(scale: Vector) { + this.body.scale = scale; + this.width = this.width; + } + + /** + * Gets the old scale of the actor last frame + */ + public get oldScale(): Vector { + return this.body.oldScale; + } + + /** + * Sets the the old scale of the acotr last frame + */ + public set oldScale(scale: Vector) { + this.body.oldScale = scale; + } + + /** + * Gets the x scalar velocity of the actor in scale/second */ - public scale: Vector = Vector.One; + public get sx(): number { + return this.body.sx; + } /** - * The scale of the actor last frame + * Sets the x scalar velocity of the actor in scale/second */ - public oldScale: Vector = Vector.One; + public set sx(scalePerSecondX: number) { + this.body.sx = scalePerSecondX; + } /** - * The x scalar velocity of the actor in scale/second + * Gets the y scalar velocity of the actor in scale/second */ - public sx: number = 0; //scale/sec + public get sy(): number { + return this.body.sy; + } + /** - * The y scalar velocity of the actor in scale/second + * Sets the y scale velocity of the actor in scale/second */ - public sy: number = 0; //scale/sec + public set sy(scalePerSecondY: number) { + this.body.sy = scalePerSecondY; + } /** * Indicates whether the actor is physically in the viewport @@ -396,18 +481,27 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv * The children of this actor */ public children: Actor[] = []; + /** * Gets or sets the current collision type of this actor. By * default it is ([[CollisionType.PreventCollision]]). + * @obsolete ex.Actor.collisionType will be removed in v0.24.0, use ex.Actor.body.collider.type */ - public collisionType: CollisionType = CollisionType.PreventCollision; - public collisionGroups: string[] = []; + @obsolete({ message: 'ex.Actor.collisionType will be removed in v0.24.0', alternateMethod: 'ex.Actor.body.collider.type' }) + public get collisionType(): CollisionType { + return this.body.collider.type; + } /** - * Flag to be set when any property change would result in a geometry recalculation - * @internal + * Gets or sets the current collision type of this actor. By + * default it is ([[CollisionType.PreventCollision]]). + * @obsolete ex.Actor.collisionType will be removed in v0.24.0, use ex.Actor.body.collider.type */ - private _geometryDirty: boolean = false; + public set collisionType(type: CollisionType) { + this.body.collider.type = type; + } + + public collisionGroups: string[] = []; private _collisionHandlers: { [key: string]: { (actor: Actor): void }[] } = {}; private _isInitialized: boolean = false; @@ -471,18 +565,41 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv constructor(xOrConfig?: number | ActorArgs, y?: number, width?: number, height?: number, color?: Color) { super(); + let shouldInitializeBody = true; if (xOrConfig && typeof xOrConfig === 'object') { const config = xOrConfig; xOrConfig = config.pos ? config.pos.x : config.x; y = config.pos ? config.pos.y : config.y; width = config.width; height = config.height; + + if (config.body) { + shouldInitializeBody = false; + this.body = config.body; + } } - this.pos.x = xOrConfig || 0; - this.pos.y = y || 0; + // initialize default options + this._initDefaults(); + + // Body and collider bounds are still determined by actor width/height this._width = width || 0; this._height = height || 0; + + // Initialize default collider to be a box + if (shouldInitializeBody) { + this.body = new Body({ + collider: new Collider({ + type: CollisionType.Passive, + shape: Shape.Box(this._width, this._height, this.anchor) + }) + }); + } + + // Position uses body to store values must be initialized after body + this.pos.x = xOrConfig || 0; + this.pos.y = y || 0; + if (color) { this.color = color; // set default opacity of an actor to the color @@ -497,12 +614,6 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv // Build the action queue this.actionQueue = new ActionQueue(this); this.actions = new ActionContext(this); - - // initialize default options - this._initDefaults(); - - // Initialize default collision area to be box - this.body.useBoxCollision(); } /** @@ -870,7 +981,7 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv * @param actor The child actor to add */ public add(actor: Actor) { - actor.collisionType = CollisionType.PreventCollision; + actor.body.collider.type = CollisionType.PreventCollision; if (Util.addItemToArray(actor, this.children)) { actor.parent = this; } @@ -993,59 +1104,96 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv /** * Get the center point of an actor */ + @obsolete({ message: 'Will be removed in v0.24.0', alternateMethod: 'Actor.center' }) public getCenter(): Vector { - return new Vector( - this.pos.x + this.getWidth() / 2 - this.anchor.x * this.getWidth(), - this.pos.y + this.getHeight() / 2 - this.anchor.y * this.getHeight() - ); + return new Vector(this.pos.x + this.width / 2 - this.anchor.x * this.width, this.pos.y + this.height / 2 - this.anchor.y * this.height); + } + + /** + * Get the center point of an actor + */ + public get center(): Vector { + return new Vector(this.pos.x + this.width / 2 - this.anchor.x * this.width, this.pos.y + this.height / 2 - this.anchor.y * this.height); + } + + public get width() { + return this._width * this.getGlobalScale().x; + } + + public set width(width: number) { + this._width = width / this.scale.x; + this.body.collider.shape = Shape.Box(this._width, this._height, this.anchor); + this.body.markCollisionShapeDirty(); } + /** * Gets the calculated width of an actor, factoring in scale */ + @obsolete({ message: 'Will be removed in v0.24.0', alternateMethod: 'Actor.width' }) public getWidth() { - return this._width * this.getGlobalScale().x; + return this.width; } /** * Sets the width of an actor, factoring in the current scale */ + @obsolete({ message: 'Will be removed in v0.24.0', alternateMethod: 'Actor.width' }) public setWidth(width: number) { - this._width = width / this.scale.x; - this._geometryDirty = true; + this.width = width; } + + public get height() { + return this._height * this.getGlobalScale().y; + } + + public set height(height: number) { + this._height = height / this.scale.y; + this.body.collider.shape = Shape.Box(this._width, this._height, this.anchor); + this.body.markCollisionShapeDirty(); + } + /** * Gets the calculated height of an actor, factoring in scale */ + @obsolete({ message: 'Will be removed in v0.24.0', alternateMethod: 'Actor.height' }) public getHeight() { - return this._height * this.getGlobalScale().y; + return this.height; } /** * Sets the height of an actor, factoring in the current scale */ + @obsolete({ message: 'Will be removed in v0.24.0', alternateMethod: 'Actor.height' }) public setHeight(height: number) { - this._height = height / this.scale.y; - this._geometryDirty = true; + this.height = height; } + /** * Gets the left edge of the actor */ + @obsolete({ message: 'Will be removed in v0.24.0', alternateMethod: 'Actor.body.collider.bounds.left' }) public getLeft() { return this.getBounds().left; } + /** * Gets the right edge of the actor */ + @obsolete({ message: 'Will be removed in v0.24.0', alternateMethod: 'Actor.body.collider.bounds.right' }) public getRight() { return this.getBounds().right; } + /** * Gets the top edge of the actor */ + @obsolete({ message: 'Will be removed in v0.24.0', alternateMethod: 'Actor.body.collider.bounds.top' }) public getTop() { return this.getBounds().top; } + /** * Gets the bottom edge of the actor */ + @obsolete({ message: 'Will be removed in v0.24.0', alternateMethod: 'Actor.body.collider.bounds.bottom' }) public getBottom() { return this.getBounds().bottom; } @@ -1124,12 +1272,13 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv /** * Returns the actor's [[BoundingBox]] calculated for this instant in world space. */ + @obsolete({ message: 'Will be removed in v0.24.0', alternateMethod: 'Actor.body.collider.bounds' }) public getBounds(rotated: boolean = true): BoundingBox { // todo cache bounding box const anchor = this._getCalculatedAnchor(); const pos = this.getWorldPos(); - const bb = new BoundingBox(pos.x - anchor.x, pos.y - anchor.y, pos.x + this.getWidth() - anchor.x, pos.y + this.getHeight() - anchor.y); + const bb = new BoundingBox(pos.x - anchor.x, pos.y - anchor.y, pos.x + this.width - anchor.x, pos.y + this.height - anchor.y); return rotated ? bb.rotate(this.rotation, pos) : bb; } @@ -1137,10 +1286,11 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv /** * Returns the actor's [[BoundingBox]] relative to the actor's position. */ + @obsolete({ message: 'Will be removed in v0.24.0', alternateMethod: 'Actor.body.collider.localBounds' }) public getRelativeBounds(rotated: boolean = true): BoundingBox { // todo cache bounding box const anchor = this._getCalculatedAnchor(); - const bb = new BoundingBox(-anchor.x, -anchor.y, this.getWidth() - anchor.x, this.getHeight() - anchor.y); + const bb = new BoundingBox(-anchor.x, -anchor.y, this.width - anchor.x, this.height - anchor.y); return rotated ? bb.rotate(this.rotation) : bb; } @@ -1148,6 +1298,7 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv /** * Returns the actors unrotated geometry in world coordinates */ + @obsolete({ message: 'Will be removed in v0.24.0', alternateMethod: 'Actor.body.collider.bounds.getPoints()' }) public getGeometry(): Vector[] { return this.getBounds(false).getPoints(); } @@ -1155,17 +1306,11 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv /** * Return the actor's unrotated geometry relative to the actor's position */ + @obsolete({ message: 'Will be removed in v0.24.0', alternateMethod: 'Actor.body.collider.localBounds.getPoints()' }) public getRelativeGeometry(): Vector[] { return this.getRelativeBounds(false).getPoints(); } - /** - * Indicates that the actor's collision geometry needs to be recalculated for accurate collisions - */ - public get isGeometryDirty() { - return this._geometryDirty; - } - /** * Tests whether the x/y specified are contained in the actor * @param x X coordinate to test (in world coordinates) @@ -1173,7 +1318,10 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv * @param recurse checks whether the x/y are contained in any child actors (if they exist). */ public contains(x: number, y: number, recurse: boolean = false): boolean { - const containment = this.getBounds().contains(new Vector(x, y)); + // These shenanigans are to handle child actor containment, + // the only time getWorldPos and pos are different is a child actor + const childShift = this.getWorldPos().sub(this.pos); + const containment = this.body.collider.bounds.translate(childShift).contains(new Vector(x, y)); if (recurse) { return ( @@ -1190,7 +1338,9 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv /** * Returns the side of the collision based on the intersection * @param intersect The displacement vector returned by a collision + * @obsolete Actor.getSideFromIntersect will be removed in v0.24.0, use [[BoundingBox.sideFromIntersection]] */ + @obsolete({ message: 'Actor.getSideFromIntersect will be removed in v0.24.0', alternateMethod: 'BoundingBox.sideFromIntersection' }) public getSideFromIntersect(intersect: Vector) { if (intersect) { if (Math.abs(intersect.x) > Math.abs(intersect.y)) { @@ -1210,7 +1360,9 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv /** * Test whether the actor has collided with another actor, returns the side of the current actor that collided. * @param actor The other actor to test + * @obsolete Actor.collidesWithSide will be removed in v0.24.0, use [[Actor.bounds.intersectWithSide]] */ + @obsolete({ message: 'Actor.collidesWithSide will be removed in v0.24.0', alternateMethod: 'Actor.bounds.intersectWithSide' }) public collidesWithSide(actor: Actor): Side { const separationVector = this.collides(actor); if (!separationVector) { @@ -1234,13 +1386,17 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv * Test whether the actor has collided with another actor, returns the intersection vector on collision. Returns * `null` when there is no collision; * @param actor The other actor to test + * @obsolete Actor.collides will be removed in v0.24.0, use [[Actor.bounds.interesect]] to get boudings intersection, + * or [[Actor.body.collider.collide]] to collide with another collider */ + @obsolete({ message: 'Actor.collides will be removed in v0.24.0', alternateMethod: 'Actor.bounds.intersect or Actor.' }) public collides(actor: Actor): Vector { - const bounds = this.getBounds(); - const otherBounds = actor.getBounds(); - const intersect = bounds.collides(otherBounds); + const bounds = this.body.collider.bounds; + const otherBounds = actor.body.collider.bounds; + const intersect = bounds.intersect(otherBounds); return intersect; } + /** * Register a handler to fire when this actor collides with another in a specified group * @param group The group name to listen for @@ -1274,7 +1430,7 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv // #endregion private _getCalculatedAnchor(): Vector { - return new Vector(this.getWidth() * this.anchor.x, this.getHeight() * this.anchor.y); + return new Vector(this.width * this.anchor.x, this.height * this.anchor.y); } protected _reapplyEffects(drawing: Drawable) { @@ -1283,37 +1439,6 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv } // #region Update - /** - * Perform euler integration at the specified time step - */ - public integrate(delta: number) { - // Update placements based on linear algebra - const seconds = delta / 1000; - - const totalAcc = this.acc.clone(); - // Only active vanilla actors are affected by global acceleration - if (this.collisionType === CollisionType.Active) { - totalAcc.addEqual(Physics.acc); - } - - this.vel.addEqual(totalAcc.scale(seconds)); - this.pos.addEqual(this.vel.scale(seconds)).addEqual(totalAcc.scale(0.5 * seconds * seconds)); - - this.rx += this.torque * (1.0 / this.moi) * seconds; - this.rotation += this.rx * seconds; - - this.scale.x += (this.sx * delta) / 1000; - this.scale.y += (this.sy * delta) / 1000; - - if (!this.scale.equals(this.oldScale)) { - // change in scale effects the geometry - this._geometryDirty = true; - } - - // Update physics body - this.body.update(); - this._geometryDirty = false; - } /** * Called by the Engine, updates the state of the actor @@ -1339,14 +1464,11 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv this._effectsDirty = true; } - // Capture old values before integration step updates them - this.oldVel.setTo(this.vel.x, this.vel.y); - this.oldPos.setTo(this.pos.x, this.pos.y); - this.oldAcc.setTo(this.acc.x, this.acc.y); - this.oldScale.setTo(this.scale.x, this.scale.y); + // capture old transform + this.body.captureOldTransform(); // Run Euler integration - this.integrate(delta); + this.body.integrate(delta); // Update actor pipeline (movement, collision detection, event propagation, offscreen culling) for (const trait of this.traits) { @@ -1434,9 +1556,8 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv this.currentDrawing.draw(ctx, offsetX, offsetY); } else { - if (this.color) { - ctx.fillStyle = this.color.toString(); - ctx.fillRect(0, 0, this._width, this._height); + if (this.color && this.body && this.body.collider && this.body.collider.shape) { + this.body.collider.shape.draw(ctx, this.color, new Vector(this.width * this.anchor.x, this.height * this.anchor.y)); } } ctx.restore(); @@ -1500,10 +1621,10 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv public debugDraw(ctx: CanvasRenderingContext2D) { this.emit('predebugdraw', new PreDebugDrawEvent(ctx, this)); - this.body.debugDraw(ctx); + this.body.collider.debugDraw(ctx); // Draw actor bounding box - const bb = this.getBounds(); + const bb = this.body.collider.bounds; bb.debugDraw(ctx); // Draw actor Id @@ -1526,7 +1647,7 @@ export class ActorImpl extends Class implements Actionable, Eventable, PointerEv // Unit Circle debug draw ctx.strokeStyle = Color.Yellow.toString(); ctx.beginPath(); - const radius = Math.min(this.getWidth(), this.getHeight()); + const radius = Math.min(this.width, this.height); ctx.arc(this.getWorldPos().x, this.getWorldPos().y, radius, 0, Math.PI * 2); ctx.closePath(); ctx.stroke(); @@ -1597,34 +1718,3 @@ export class Actor extends Configurable(ActorImpl) { super(xOrConfig, y, width, height, color); } } - -/** - * An enum that describes the types of collisions actors can participate in - */ -export enum CollisionType { - /** - * Actors with the `PreventCollision` setting do not participate in any - * collisions and do not raise collision events. - */ - PreventCollision, - /** - * Actors with the `Passive` setting only raise collision events, but are not - * influenced or moved by other actors and do not influence or move other actors. - */ - Passive, - /** - * Actors with the `Active` setting raise collision events and participate - * in collisions with other actors and will be push or moved by actors sharing - * the `Active` or `Fixed` setting. - */ - Active, - /** - * Actors with the `Fixed` setting raise collision events and participate in - * collisions with other actors. Actors with the `Fixed` setting will not be - * pushed or moved by other actors sharing the `Fixed`. Think of Fixed - * actors as "immovable/onstoppable" objects. If two `Fixed` actors meet they will - * not be pushed or moved by each other, they will not interact except to throw - * collision events. - */ - Fixed -} diff --git a/src/engine/Algebra.ts b/src/engine/Algebra.ts index 6cd1c79d..3449c93d 100644 --- a/src/engine/Algebra.ts +++ b/src/engine/Algebra.ts @@ -1,10 +1,11 @@ import { Engine } from './Engine'; import * as Util from './Util/Util'; +import { Clonable } from './Interfaces/Clonable'; /** * A 2D vector on a plane. */ -export class Vector { +export class Vector implements Clonable { /** * A (0, 0) vector */ @@ -151,8 +152,14 @@ export class Vector { * Scales a vector's by a factor of size * @param size The factor to scale the magnitude by */ - public scale(size: number): Vector { - return new Vector(this.x * size, this.y * size); + public scale(scale: Vector): Vector; + public scale(size: number): Vector; + public scale(sizeOrScale: number | Vector): Vector { + if (sizeOrScale instanceof Vector) { + return new Vector(this.x * sizeOrScale.x, this.y * sizeOrScale.y); + } else { + return new Vector(this.x * sizeOrScale, this.y * sizeOrScale); + } } /** diff --git a/src/engine/Camera.ts b/src/engine/Camera.ts index bd7d6dac..59802a35 100644 --- a/src/engine/Camera.ts +++ b/src/engine/Camera.ts @@ -90,7 +90,7 @@ export enum Axis { export class LockCameraToActorStrategy implements CameraStrategy { constructor(public target: Actor) {} public action = (target: Actor, _cam: Camera, _eng: Engine, _delta: number) => { - const center = target.getCenter(); + const center = target.center; return center; }; } @@ -101,7 +101,7 @@ export class LockCameraToActorStrategy implements CameraStrategy { export class LockCameraToActorAxisStrategy implements CameraStrategy { constructor(public target: Actor, public axis: Axis) {} public action = (target: Actor, cam: Camera, _eng: Engine, _delta: number) => { - const center = target.getCenter(); + const center = target.center; const currentFocus = cam.getFocus(); if (this.axis === Axis.X) { return new Vector(center.x, currentFocus.y); @@ -126,7 +126,7 @@ export class ElasticToActorStrategy implements CameraStrategy { */ constructor(public target: Actor, public cameraElasticity: number, public cameraFriction: number) {} public action = (target: Actor, cam: Camera, _eng: Engine, _delta: number) => { - const position = target.getCenter(); + const position = target.center; let focus = cam.getFocus(); let cameraVel = new Vector(cam.dx, cam.dy); @@ -157,7 +157,7 @@ export class RadiusAroundActorStrategy implements CameraStrategy { */ constructor(public target: Actor, public radius: number) {} public action = (target: Actor, cam: Camera, _eng: Engine, _delta: number) => { - const position = target.getCenter(); + const position = target.center; const focus = cam.getFocus(); const direction = position.sub(focus); @@ -613,7 +613,7 @@ export class Camera extends Class implements CanUpdate, CanInitialize { ctx.setLineDash([5, 15]); ctx.lineWidth = 5; ctx.strokeStyle = 'white'; - ctx.strokeRect(this.viewport.left, this.viewport.top, this.viewport.getWidth(), this.viewport.getHeight()); + ctx.strokeRect(this.viewport.left, this.viewport.top, this.viewport.width, this.viewport.height); ctx.closePath(); } diff --git a/src/engine/Collision/Body.ts b/src/engine/Collision/Body.ts index f25d746a..f4568825 100644 --- a/src/engine/Collision/Body.ts +++ b/src/engine/Collision/Body.ts @@ -1,26 +1,80 @@ -import { Physics, CollisionResolutionStrategy } from './../Physics'; -import { EdgeArea } from './EdgeArea'; -import { CircleArea } from './CircleArea'; -import { CollisionArea } from './CollisionArea'; -import { PolygonArea } from './PolygonArea'; -import { BoundingBox } from './BoundingBox'; -import { Pair } from './Pair'; - import { Vector } from '../Algebra'; import { Actor } from '../Actor'; -import { Color } from '../Drawing/Color'; -import * as DrawUtil from '../Util/DrawUtil'; +import { Collider } from './Collider'; +import { CollisionType } from './CollisionType'; +import { Physics } from '../Physics'; +import { obsolete } from '../Util/Decorators'; +import { PreCollisionEvent, PostCollisionEvent, CollisionStartEvent, CollisionEndEvent } from '../Events'; +import { Clonable } from '../Interfaces/Clonable'; +import { Shape } from './Shape'; + +export interface BodyOptions { + /** + * Optionally the actory associated with this body + */ + actor?: Actor; + /** + * An optional collider to use in this body, if none is specified a default Box collider will be created. + */ + collider?: Collider; +} -export class Body { +/** + * Body describes all the physical properties pos, vel, acc, rotation, angular velocity + */ +export class Body implements Clonable { + private _collider: Collider; + public actor: Actor; /** * Constructs a new physics body associated with an actor */ - constructor(public actor: Actor) {} + constructor({ actor, collider }: BodyOptions) { + if (!actor && !collider) { + throw new Error('An actor or collider are required to create a body'); + } + + this.actor = actor; + if (!collider && actor) { + this.collider = this.useBoxCollider(actor.width, actor.height, actor.anchor); + } else { + this.collider = collider; + } + } + + public get id() { + return this.actor ? this.actor.id : -1; + } /** - * [[ICollisionArea|Collision area]] of this physics body, defines the shape for rigid body collision + * Returns a clone of this body, not associated with any actor */ - public collisionArea: CollisionArea = null; + public clone() { + return new Body({ + actor: null, + collider: this.collider.clone() + }); + } + + public get active() { + return this.actor ? !this.actor.isKilled() : false; + } + + public get center() { + return this.pos; + } + + // TODO allow multiple colliders for a single body + public set collider(collider: Collider) { + if (collider) { + this._collider = collider; + this._collider.body = this; + this._wireColliderEventsToActor(); + } + } + + public get collider(): Collider { + return this._collider; + } /** * The (x, y) position of the actor this will be in the middle of the actor if the @@ -50,46 +104,57 @@ export class Body { */ public acc: Vector = new Vector(0, 0); + /** + * Gets/sets the acceleration of the actor from the last frame. This does not include the global acc [[Physics.acc]]. + */ + public oldAcc: Vector = Vector.Zero; + /** * The current torque applied to the actor */ public torque: number = 0; /** - * The current mass of the actor, mass can be thought of as the resistance to acceleration. + * The current "motion" of the actor, used to calculated sleep in the physics simulation */ - public mass: number = 1.0; + public motion: number = 10; /** - * The current moment of inertia, moi can be thought of as the resistance to rotation. + * Gets/sets the rotation of the body from the last frame. */ - public moi: number = 1000; + public oldRotation: number = 0; // radians /** - * The current "motion" of the actor, used to calculated sleep in the physics simulation + * The rotation of the actor in radians */ - public motion: number = 10; + public rotation: number = 0; // radians /** - * The coefficient of friction on this actor + * The scale vector of the actor */ - public friction: number = 0.99; + public scale: Vector = Vector.One; /** - * The coefficient of restitution of this actor, represents the amount of energy preserved after collision + * The scale of the actor last frame */ - public restitution: number = 0.2; + public oldScale: Vector = Vector.One; /** - * The rotation of the actor in radians + * The x scalar velocity of the actor in scale/second */ - public rotation: number = 0; // radians + public sx: number = 0; //scale/sec + /** + * The y scalar velocity of the actor in scale/second + */ + public sy: number = 0; //scale/sec /** * The rotational velocity of the actor in radians/second */ public rx: number = 0; //radians/sec + private _geometryDirty = false; + private _totalMtv: Vector = Vector.Zero; /** @@ -108,139 +173,159 @@ export class Body { } /** - * Returns the body's [[BoundingBox]] calculated for this instant in world space. + * Flags the shape dirty and must be recalculated in world space */ - public getBounds(): BoundingBox { - if (Physics.collisionResolutionStrategy === CollisionResolutionStrategy.Box) { - return this.actor.getBounds(); - } else { - return this.collisionArea.getBounds(); - } + public markCollisionShapeDirty() { + this._geometryDirty = true; + } + + public get isColliderShapeDirty(): boolean { + return this._geometryDirty; } /** - * Returns the actor's [[BoundingBox]] relative to the actors position. + * Sets the old versions of pos, vel, acc, and scale. */ - public getRelativeBounds(): BoundingBox { - if (Physics.collisionResolutionStrategy === CollisionResolutionStrategy.Box) { - return this.actor.getRelativeBounds(); - } else { - return this.actor.getRelativeBounds(); - } + public captureOldTransform() { + // Capture old values before integration step updates them + this.oldVel.setTo(this.vel.x, this.vel.y); + this.oldPos.setTo(this.pos.x, this.pos.y); + this.oldAcc.setTo(this.acc.x, this.acc.y); + this.oldScale.setTo(this.scale.x, this.scale.y); + this.oldRotation = this.rotation; } /** - * Updates the collision area geometry and internal caches + * Perform euler integration at the specified time step */ - public update() { - if (this.collisionArea) { - // Update the geometry if needed - if (this.actor && this.actor.isGeometryDirty && this.collisionArea instanceof PolygonArea) { - this.collisionArea.points = this.actor.getRelativeGeometry(); - } + public integrate(delta: number) { + // Update placements based on linear algebra + const seconds = delta / 1000; + + const totalAcc = this.acc.clone(); + // Only active vanilla actors are affected by global acceleration + if (this.collider.type === CollisionType.Active) { + totalAcc.addEqual(Physics.acc); + } - this.collisionArea.recalc(); + this.vel.addEqual(totalAcc.scale(seconds)); + this.pos.addEqual(this.vel.scale(seconds)).addEqual(totalAcc.scale(0.5 * seconds * seconds)); + + this.rx += this.torque * (1.0 / this.collider.inertia) * seconds; + this.rotation += this.rx * seconds; + + this.scale.x += (this.sx * delta) / 1000; + this.scale.y += (this.sy * delta) / 1000; + + if (!this.scale.equals(this.oldScale)) { + // change in scale effects the geometry + this._geometryDirty = true; } + + // Update colliders + this.collider.update(); + this._geometryDirty = false; } /** - * Sets up a box collision area based on the current bounds of the associated actor of this physics body. + * Sets up a box geometry based on the current bounds of the associated actor of this physics body. * * By default, the box is center is at (0, 0) which means it is centered around the actors anchor. */ - public useBoxCollision(center: Vector = Vector.Zero) { - this.collisionArea = new PolygonArea({ - body: this, - points: this.actor.getRelativeGeometry(), - pos: center // position relative to actor - }); + public useBoxCollider(width: number, height: number, anchor: Vector = Vector.Half, center: Vector = Vector.Zero): Collider { + this.collider.shape = Shape.Box(width, height, anchor, center); + return this.collider; + } - // in case of a nan moi, coalesce to a safe default - this.moi = this.collisionArea.getMomentOfInertia() || this.moi; + /** + * @obsolete Body.useBoxCollision will be removed in v0.24.0 use [[Body.useBoxCollider]] + */ + @obsolete({ message: 'Will be removed in v0.24.0', alternateMethod: 'Body.useBoxCollider' }) + public useBoxCollision(center: Vector = Vector.Zero) { + this.useBoxCollider(this.actor.width, this.actor.height, this.actor.anchor, center); } /** - * Sets up a polygon collision area based on a list of of points relative to the anchor of the associated actor of this physics body. + * Sets up a [[ConvexPolygon|convex polygon]] collision geometry based on a list of of points relative + * to the anchor of the associated actor + * of this physics body. * * Only [convex polygon](https://en.wikipedia.org/wiki/Convex_polygon) definitions are supported. * * By default, the box is center is at (0, 0) which means it is centered around the actors anchor. */ - public usePolygonCollision(points: Vector[], center: Vector = Vector.Zero) { - this.collisionArea = new PolygonArea({ - body: this, - points: points, - pos: center // position relative to actor - }); + public usePolygonCollider(points: Vector[], center: Vector = Vector.Zero): Collider { + this.collider.shape = Shape.Polygon(points, false, center); + return this.collider; + } - // in case of a nan moi, collesce to a safe default - this.moi = this.collisionArea.getMomentOfInertia() || this.moi; + /** + * @obsolete Body.usePolygonCollision will be removed in v0.24.0 use [[Body.usePolygonCollider]] + */ + @obsolete({ message: 'Will be removed in v0.24.0', alternateMethod: 'Body.usePolygonCollider' }) + public usePolygonCollision(points: Vector[], center: Vector = Vector.Zero) { + this.usePolygonCollider(points, center); } /** - * Sets up a [[CircleArea|circle collision area]] with a specified radius in pixels. + * Sets up a [[Circle|circle collision geometry]] with a specified radius in pixels. * * By default, the box is center is at (0, 0) which means it is centered around the actors anchor. */ + public useCircleCollider(radius: number, center: Vector = Vector.Zero): Collider { + this.collider.shape = Shape.Circle(radius, center); + return this.collider; + } + + /** + * @obsolete Body.useCircleCollision will be removed in v0.24.0, use [[Body.useCircleCollider]] + */ + @obsolete({ message: 'Will be removed in v0.24.0', alternateMethod: 'Body.useCircleCollider' }) public useCircleCollision(radius?: number, center: Vector = Vector.Zero) { - if (!radius) { - radius = this.actor.getWidth() / 2; - } - this.collisionArea = new CircleArea({ - body: this, - radius: radius, - pos: center - }); - this.moi = this.collisionArea.getMomentOfInertia() || this.moi; + this.useCircleCollider(radius, center); } /** - * Sets up an [[EdgeArea|edge collision]] with a start point and an end point relative to the anchor of the associated actor + * Sets up an [[Edge|edge collision geometry]] with a start point and an end point relative to the anchor of the associated actor * of this physics body. * * By default, the box is center is at (0, 0) which means it is centered around the actors anchor. */ - public useEdgeCollision(begin: Vector, end: Vector) { - this.collisionArea = new EdgeArea({ - begin: begin, - end: end, - body: this - }); - - this.moi = this.collisionArea.getMomentOfInertia() || this.moi; - } - - /* istanbul ignore next */ - public debugDraw(ctx: CanvasRenderingContext2D) { - // Draw motion vectors - if (Physics.showMotionVectors) { - DrawUtil.vector(ctx, Color.Yellow, this.pos, this.acc.add(Physics.acc)); - DrawUtil.vector(ctx, Color.Red, this.pos, this.vel); - DrawUtil.point(ctx, Color.Red, this.pos); - } - - if (Physics.showBounds) { - this.getBounds().debugDraw(ctx, Color.Yellow); - } - - if (Physics.showArea) { - this.collisionArea.debugDraw(ctx, Color.Green); - } + public useEdgeCollider(begin: Vector, end: Vector): Collider { + this.collider.shape = Shape.Edge(begin, end); + return this.collider; } /** - * Returns a boolean indicating whether this body collided with - * or was in stationary contact with - * the body of the other [[Actor]] + * @obsolete Body.useEdgeCollision will be removed in v0.24.0, use [[Body.useEdgeCollider]] */ - public touching(other: Actor): boolean { - const pair = new Pair(this, other.body); - pair.collide(); - - if (pair.collision) { - return true; - } + @obsolete({ message: 'Will be removed in v0.24.0', alternateMethod: 'Body.useEdgeCollider' }) + public useEdgeCollision(begin: Vector, end: Vector) { + this.useEdgeCollider(begin, end); + } - return false; + // TODO remove this, eventually events will stay local to the thing they are around + private _wireColliderEventsToActor() { + this.collider.clear(); + this.collider.on('precollision', (evt: PreCollisionEvent) => { + if (this.actor) { + this.actor.emit('precollision', new PreCollisionEvent(evt.target.body.actor, evt.other.body.actor, evt.side, evt.intersection)); + } + }); + this.collider.on('postcollision', (evt: PostCollisionEvent) => { + if (this.actor) { + this.actor.emit('postcollision', new PostCollisionEvent(evt.target.body.actor, evt.other.body.actor, evt.side, evt.intersection)); + } + }); + this.collider.on('collisionstart', (evt: CollisionStartEvent) => { + if (this.actor) { + this.actor.emit('collisionstart', new CollisionStartEvent(evt.target.body.actor, evt.other.body.actor, evt.pair)); + } + }); + this.collider.on('collisionend', (evt: CollisionEndEvent) => { + if (this.actor) { + this.actor.emit('collisionend', new CollisionEndEvent(evt.target.body.actor, evt.other.body.actor)); + } + }); } } diff --git a/src/engine/Collision/BoundingBox.ts b/src/engine/Collision/BoundingBox.ts index f623e28d..3a0f172d 100644 --- a/src/engine/Collision/BoundingBox.ts +++ b/src/engine/Collision/BoundingBox.ts @@ -1,34 +1,15 @@ -import { PolygonArea } from './PolygonArea'; +import { ConvexPolygon } from './ConvexPolygon'; import { Actor } from '../Actor'; import { Vector, Ray } from '../Algebra'; import { Color } from '../Drawing/Color'; - -/** - * Interface all collidable objects must implement - */ -export interface Collidable { - /** - * Test whether this bounding box collides with another one. - * - * @param collidable Other collidable to test - * @returns Vector The intersection vector that can be used to resolve the collision. - * If there is no collision, `null` is returned. - */ - collides(collidable: Collidable): Vector; - /** - * Tests wether a point is contained within the collidable - * @param point The point to test - */ - contains(point: Vector): boolean; - - debugDraw(ctx: CanvasRenderingContext2D): void; -} +import { obsolete } from '../Util/Decorators'; +import { Side } from './Side'; /** * Axis Aligned collision primitive for Excalibur. */ -export class BoundingBox implements Collidable { +export class BoundingBox { /** * @param left x coordinate of the left edge * @param top y coordinate of the top edge @@ -37,6 +18,30 @@ export class BoundingBox implements Collidable { */ constructor(public left: number = 0, public top: number = 0, public right: number = 0, public bottom: number = 0) {} + /** + * Given bounding box A & B, returns the side relative to A when intersection is performed. + * @param intersection Intersection vector between 2 bounding boxes + */ + public static getSideFromIntersection(intersection: Vector): Side { + if (!intersection) { + return Side.None; + } + if (intersection) { + if (Math.abs(intersection.x) > Math.abs(intersection.y)) { + if (intersection.x < 0) { + return Side.Right; + } + return Side.Left; + } else { + if (intersection.y < 0) { + return Side.Bottom; + } + return Side.Top; + } + } + return Side.None; + } + public static fromPoints(points: Vector[]): BoundingBox { let minX = Infinity; let minY = Infinity; @@ -59,20 +64,64 @@ export class BoundingBox implements Collidable { return new BoundingBox(minX, minY, maxX, maxY); } + public static fromDimension(width: number, height: number, anchor: Vector = Vector.Half, pos: Vector = Vector.Zero) { + return new BoundingBox( + -width * anchor.x + pos.x, + -height * anchor.y + pos.y, + width - width * anchor.x + pos.x, + height - height * anchor.y + pos.y + ); + } + /** * Returns the calculated width of the bounding box */ + @obsolete({ message: 'Will be removed in v0.24.0', alternateMethod: 'BoundingBox.width' }) public getWidth() { + return this.width; + } + + /** + * Returns the calculated width of the bounding box + */ + public get width() { return this.right - this.left; } /** * Returns the calculated height of the bounding box */ + @obsolete({ message: 'Will be removed in v0.24.0', alternateMethod: 'BoundingBox.height' }) public getHeight() { + return this.height; + } + + /** + * Returns the calculated height of the bounding box + */ + public get height() { return this.bottom - this.top; } + /** + * Returns the center of the bounding box + */ + @obsolete({ message: 'Will be removed in v0.24.0', alternateMethod: 'BoundingBox.center' }) + public getCenter(): Vector { + return new Vector((this.left + this.right) / 2, (this.top + this.bottom) / 2); + } + + /** + * Returns the center of the bounding box + */ + public get center(): Vector { + return new Vector((this.left + this.right) / 2, (this.top + this.bottom) / 2); + } + + public translate(pos: Vector): BoundingBox { + return new BoundingBox(this.left + pos.x, this.top + pos.y, this.right + pos.x, this.bottom + pos.y); + } + /** * Rotates a bounding box by and angle and around a point, if no point is specified (0, 0) is used by default. The resulting bounding * box is also axis-align. This is useful when a new axis-aligned bounding box is needed for rotated geometry. @@ -82,12 +131,17 @@ export class BoundingBox implements Collidable { return BoundingBox.fromPoints(points); } + public scale(scale: Vector, point: Vector = Vector.Zero): BoundingBox { + const shifted = this.translate(point); + return new BoundingBox(shifted.left * scale.x, shifted.top * scale.y, shifted.right * scale.x, shifted.bottom * scale.y); + } + /** * Returns the perimeter of the bounding box */ public getPerimeter(): number { - const wx = this.getWidth(); - const wy = this.getHeight(); + const wx = this.width; + const wy = this.height; return 2 * (wx + wy); } @@ -103,8 +157,8 @@ export class BoundingBox implements Collidable { /** * Creates a Polygon collision area from the points of the bounding box */ - public toPolygon(actor?: Actor): PolygonArea { - return new PolygonArea({ + public toPolygon(actor?: Actor): ConvexPolygon { + return new ConvexPolygon({ body: actor ? actor.body : null, points: this.getPoints(), pos: Vector.Zero @@ -197,147 +251,165 @@ export class BoundingBox implements Collidable { } public get dimensions(): Vector { - return new Vector(this.getWidth(), this.getHeight()); + return new Vector(this.width, this.height); } /** - * Test wether this bounding box collides with another returning, + * Test wether this bounding box intersects with another returning * the intersection vector that can be used to resolve the collision. If there - * is no collision null is returned. + * is no intersection null is returned. * - * @returns A Vector in the direction of the current BoundingBox - * @param collidable Other collidable to test + * @param other Other [[BoundingBox]] to test intersection with + * @returns A Vector in the direction of the current BoundingBox, this <- other */ - public collides(collidable: Collidable): Vector { - if (collidable instanceof BoundingBox) { - const other: BoundingBox = collidable; - const totalBoundingBox = this.combine(other); - - // If the total bounding box is less than or equal the sum of the 2 bounds then there is collision - if ( - totalBoundingBox.getWidth() < other.getWidth() + this.getWidth() && - totalBoundingBox.getHeight() < other.getHeight() + this.getHeight() && - !totalBoundingBox.dimensions.equals(other.dimensions) && - !totalBoundingBox.dimensions.equals(this.dimensions) - ) { - // collision - let overlapX = 0; - // right edge is between the other's left and right edge + public intersect(other: BoundingBox): Vector { + const totalBoundingBox = this.combine(other); + + // If the total bounding box is less than or equal the sum of the 2 bounds then there is collision + if ( + totalBoundingBox.width < other.width + this.width && + totalBoundingBox.height < other.height + this.height && + !totalBoundingBox.dimensions.equals(other.dimensions) && + !totalBoundingBox.dimensions.equals(this.dimensions) + ) { + // collision + let overlapX = 0; + // right edge is between the other's left and right edge + /** + * +-this-+ + * | | + * | +-other-+ + * +----|-+ | + * | | + * +-------+ + * <--- + * ^ overlap + */ + if (this.right >= other.left && this.right <= other.right) { + overlapX = other.left - this.right; + // right edge is past the other's right edge /** - * +-this-+ - * | | - * | +-other-+ - * +----|-+ | - * | | - * +-------+ - * <--- + * +-other-+ + * | | + * | +-this-+ + * +----|--+ | + * | | + * +------+ + * ---> * ^ overlap */ - if (this.right >= other.left && this.right <= other.right) { - overlapX = other.left - this.right; - // right edge is past the other's right edge - /** - * +-other-+ - * | | - * | +-this-+ - * +----|--+ | - * | | - * +------+ - * ---> - * ^ overlap - */ - } else { - overlapX = other.right - this.left; - } + } else { + overlapX = other.right - this.left; + } - let overlapY = 0; - // top edge is between the other's top and bottom edge + let overlapY = 0; + // top edge is between the other's top and bottom edge + /** + * +-other-+ + * | | + * | +-this-+ | <- overlap + * +----|--+ | | + * | | \ / + * +------+ ' + */ + if (this.top <= other.bottom && this.top >= other.top) { + overlapY = other.bottom - this.top; + // top edge is above the other top edge /** - * +-other-+ - * | | - * | +-this-+ | <- overlap - * +----|--+ | | - * | | \ / - * +------+ ' + * +-this-+ . + * | | / \ + * | +-other-+ | <- overlap + * +----|-+ | | + * | | + * +-------+ */ - if (this.top <= other.bottom && this.top >= other.top) { - overlapY = other.bottom - this.top; - // top edge is above the other top edge - /** - * +-this-+ . - * | | / \ - * | +-other-+ | <- overlap - * +----|-+ | | - * | | - * +-------+ - */ - } else { - overlapY = other.top - this.bottom; - } + } else { + overlapY = other.top - this.bottom; + } - if (Math.abs(overlapX) < Math.abs(overlapY)) { - return new Vector(overlapX, 0); + if (Math.abs(overlapX) < Math.abs(overlapY)) { + return new Vector(overlapX, 0); + } else { + return new Vector(0, overlapY); + } + // Case of total containment of one bounding box by another + } else if (totalBoundingBox.dimensions.equals(other.dimensions) || totalBoundingBox.dimensions.equals(this.dimensions)) { + let overlapX = 0; + // this is wider than the other + if (this.width - other.width >= 0) { + // This right edge is closest to the others right edge + if (this.right - other.right <= other.left - this.left) { + overlapX = other.left - this.right; + // This left edge is closest to the others left edge } else { - return new Vector(0, overlapY); + overlapX = other.right - this.left; } - // Case of total containment of one bounding box by another - } else if (totalBoundingBox.dimensions.equals(other.dimensions) || totalBoundingBox.dimensions.equals(this.dimensions)) { - let overlapX = 0; - // this is wider than the other - if (this.getWidth() - other.getWidth() >= 0) { - // This right edge is closest to the others right edge - if (this.right - other.right <= other.left - this.left) { - overlapX = other.left - this.right; - // This left edge is closest to the others left edge - } else { - overlapX = other.right - this.left; - } - // other is wider than this + // other is wider than this + } else { + // This right edge is closest to the others right edge + if (other.right - this.right <= this.left - other.left) { + overlapX = this.left - other.right; + // This left edge is closest to the others left edge } else { - // This right edge is closest to the others right edge - if (other.right - this.right <= this.left - other.left) { - overlapX = this.left - other.right; - // This left edge is closest to the others left edge - } else { - overlapX = this.right - other.left; - } + overlapX = this.right - other.left; } + } - let overlapY = 0; - // this is taller than other - if (this.getHeight() - other.getHeight() >= 0) { - // The bottom edge is closest to the others bottom edge - if (this.bottom - other.bottom <= other.top - this.top) { - overlapY = other.top - this.bottom; - } else { - overlapY = other.bottom - this.top; - } - // other is taller than this + let overlapY = 0; + // this is taller than other + if (this.height - other.height >= 0) { + // The bottom edge is closest to the others bottom edge + if (this.bottom - other.bottom <= other.top - this.top) { + overlapY = other.top - this.bottom; } else { - // The bottom edge is closest to the others bottom edge - if (other.bottom - this.bottom <= this.top - other.top) { - overlapY = this.top - other.bottom; - } else { - overlapY = this.bottom - other.top; - } + overlapY = other.bottom - this.top; } - - if (Math.abs(overlapX) < Math.abs(overlapY)) { - return new Vector(overlapX, 0); + // other is taller than this + } else { + // The bottom edge is closest to the others bottom edge + if (other.bottom - this.bottom <= this.top - other.top) { + overlapY = this.top - other.bottom; } else { - return new Vector(0, overlapY); + overlapY = this.bottom - other.top; } + } + + if (Math.abs(overlapX) < Math.abs(overlapY)) { + return new Vector(overlapX, 0); } else { - return null; + return new Vector(0, overlapY); } + } else { + return null; } + } - return null; + /** + * Test whether the bounding box has intersected with another bounding box, returns the side of the current bb that intersected. + * @param bb The other actor to test + */ + public intersectWithSide(bb: BoundingBox): Side { + const intersect = this.intersect(bb); + return BoundingBox.getSideFromIntersection(intersect); + } + + /** + * Test wether this bounding box collides with another returning, + * the intersection vector that can be used to resolve the collision. If there + * is no collision null is returned. + * + * @returns A Vector in the direction of the current BoundingBox + * @param boundingBox Other collidable to test + * @obsolete BoundingBox.collides will be removed in v0.24.0, use BoundingBox.intersect + */ + @obsolete({ message: 'BoundingBox.collides will be removed in v0.24.0', alternateMethod: 'BoundingBox.intersect' }) + public collides(boundingBox: BoundingBox): Vector { + return this.intersect(boundingBox); } /* istanbul ignore next */ public debugDraw(ctx: CanvasRenderingContext2D, color: Color = Color.Yellow) { ctx.strokeStyle = color.toString(); - ctx.strokeRect(this.left, this.top, this.getWidth(), this.getHeight()); + ctx.strokeRect(this.left, this.top, this.width, this.height); } } diff --git a/src/engine/Collision/Circle.ts b/src/engine/Collision/Circle.ts new file mode 100644 index 00000000..ae0eb9ed --- /dev/null +++ b/src/engine/Collision/Circle.ts @@ -0,0 +1,304 @@ +import { BoundingBox } from './BoundingBox'; +import { CollisionJumpTable } from './CollisionJumpTable'; +import { CollisionContact } from './CollisionContact'; +import { CollisionShape } from './CollisionShape'; +import { ConvexPolygon } from './ConvexPolygon'; +import { Edge } from './Edge'; + +import { Vector, Ray, Projection } from '../Algebra'; +import { Physics } from '../Physics'; +import { Color } from '../Drawing/Color'; +import { Collider } from './Collider'; + +// @obsolete Remove in v0.24.0 +import { Body } from './Body'; +// =========================== + +export interface CircleOptions { + /** + * Optional position to shift the circle relative to the collider, by default (0, 0). + */ + pos?: Vector; + /** + * Required radius of the circle + */ + radius: number; + /** + * Optional collider to associate with this shape + */ + collider?: Collider; + + // @obsolete Will be removed in v0.24.0 please use [[collider]] to set and retrieve body information + body?: Body; +} + +/** + * This is a circle collision shape for the excalibur rigid body physics simulation + * + * Example: + * [[include:CircleShape.md]] + */ +export class Circle implements CollisionShape { + /** + * Position of the circle relative to the collider, by default (0, 0) meaning the shape is positioned on top of the collider. + */ + public pos: Vector = Vector.Zero; + + public get worldPos(): Vector { + if (this.collider && this.collider.body) { + return this.collider.body.pos.add(this.pos); + } + return this.pos; + } + + /** + * This is the radius of the circle + */ + public radius: number; + + /** + * Reference to the actor associated with this collision shape + * @obsolete Will be removed in v0.24.0 please use [[collider]] to retrieve body information + */ + public body: Body; + + /** + * The collider associated for this shape, if any. + */ + public collider?: Collider; + + constructor(options: CircleOptions) { + this.pos = options.pos || Vector.Zero; + this.radius = options.radius || 0; + this.collider = options.collider || null; + + // @obsolete Remove next release in v0.24.0, code exists for backwards compat + if (options.body) { + this.collider = options.body.collider; + this.body = this.collider.body; + } + // ================================== + } + + /** + * Returns a clone of this shape, not associated with any collider + */ + public clone(): Circle { + return new Circle({ + pos: this.pos.clone(), + radius: this.radius, + collider: null, + body: null + }); + } + + /** + * Get the center of the collision shape in world coordinates + */ + public get center(): Vector { + if (this.collider && this.collider.body) { + return this.pos.add(this.collider.body.pos); + } + return this.pos; + } + + /** + * Tests if a point is contained in this collision shape + */ + public contains(point: Vector): boolean { + let pos = this.pos; + if (this.collider && this.collider.body) { + pos = this.collider.body.pos; + } + const distance = pos.distance(point); + if (distance <= this.radius) { + return true; + } + return false; + } + + /** + * Casts a ray at the Circl shape and returns the nearest point of collision + * @param ray + */ + public rayCast(ray: Ray, max: number = Infinity): Vector { + //https://en.wikipedia.org/wiki/Line%E2%80%93sphere_intersection + const c = this.center; + const dir = ray.dir; + const orig = ray.pos; + + const discriminant = Math.sqrt(Math.pow(dir.dot(orig.sub(c)), 2) - Math.pow(orig.sub(c).distance(), 2) + Math.pow(this.radius, 2)); + + if (discriminant < 0) { + // no intersection + return null; + } else { + let toi = 0; + if (discriminant === 0) { + toi = -dir.dot(orig.sub(c)); + if (toi > 0 && toi < max) { + return ray.getPoint(toi); + } + return null; + } else { + const toi1 = -dir.dot(orig.sub(c)) + discriminant; + const toi2 = -dir.dot(orig.sub(c)) - discriminant; + + const mintoi = Math.min(toi1, toi2); + if (mintoi <= max) { + return ray.getPoint(mintoi); + } + return null; + } + } + } + + /** + * @inheritdoc + */ + public collide(shape: CollisionShape): CollisionContact { + if (shape instanceof Circle) { + return CollisionJumpTable.CollideCircleCircle(this, shape); + } else if (shape instanceof ConvexPolygon) { + return CollisionJumpTable.CollideCirclePolygon(this, shape); + } else if (shape instanceof Edge) { + return CollisionJumpTable.CollideCircleEdge(this, shape); + } else { + throw new Error(`Circle could not collide with unknown CollisionShape ${typeof shape}`); + } + } + + /** + * Find the point on the shape furthest in the direction specified + */ + public getFurthestPoint(direction: Vector): Vector { + return this.center.add(direction.normalize().scale(this.radius)); + } + + /** + * Get the axis aligned bounding box for the circle shape in world coordinates + */ + public get bounds(): BoundingBox { + let bodyPos = Vector.Zero; + if (this.collider && this.collider.body) { + bodyPos = this.collider.body.pos; + } + return new BoundingBox( + this.pos.x + bodyPos.x - this.radius, + this.pos.y + bodyPos.y - this.radius, + this.pos.x + bodyPos.x + this.radius, + this.pos.y + bodyPos.y + this.radius + ); + } + + /** + * Get the axis aligned bounding box for the circle shape in local coordinates + */ + public get localBounds(): BoundingBox { + return new BoundingBox(this.pos.x - this.radius, this.pos.y - this.radius, this.pos.x + this.radius, this.pos.y + this.radius); + } + + /** + * Get axis not implemented on circles, since there are infinite axis in a circle + */ + public get axes(): Vector[] { + return null; + } + + /** + * Returns the moment of inertia of a circle given it's mass + * https://en.wikipedia.org/wiki/List_of_moments_of_inertia + */ + public get inertia(): number { + const mass = this.collider ? this.collider.mass : Physics.defaultMass; + return (mass * this.radius * this.radius) / 2; + } + + /** + * Tests the separating axis theorem for circles against polygons + */ + public testSeparatingAxisTheorem(polygon: ConvexPolygon): Vector { + const axes = polygon.axes; + const pc = polygon.center; + // Special SAT with circles + const closestPointOnPoly = polygon.getFurthestPoint(this.pos.sub(pc)); + axes.push(this.pos.sub(closestPointOnPoly).normalize()); + + let minOverlap = Number.MAX_VALUE; + let minAxis = null; + let minIndex = -1; + for (let i = 0; i < axes.length; i++) { + const proj1 = polygon.project(axes[i]); + const proj2 = this.project(axes[i]); + const overlap = proj1.getOverlap(proj2); + if (overlap <= 0) { + return null; + } else { + if (overlap < minOverlap) { + minOverlap = overlap; + minAxis = axes[i]; + minIndex = i; + } + } + } + if (minIndex < 0) { + return null; + } + return minAxis.normalize().scale(minOverlap); + } + + /* istanbul ignore next */ + public recalc(): void { + // circles don't cache + } + + /** + * Project the circle along a specified axis + */ + public project(axis: Vector): Projection { + const scalars = []; + const point = this.center; + const dotProduct = point.dot(axis); + scalars.push(dotProduct); + scalars.push(dotProduct + this.radius); + scalars.push(dotProduct - this.radius); + return new Projection(Math.min.apply(Math, scalars), Math.max.apply(Math, scalars)); + } + + public draw(ctx: CanvasRenderingContext2D, color: Color = Color.Green, pos: Vector = Vector.Zero) { + const newPos = pos.add(this.pos); + ctx.beginPath(); + ctx.fillStyle = color.toString(); + ctx.arc(newPos.x, newPos.y, this.radius, 0, Math.PI * 2); + ctx.closePath(); + ctx.fill(); + } + + /* istanbul ignore next */ + public debugDraw(ctx: CanvasRenderingContext2D, color: Color = Color.Green) { + const body = this.collider.body; + const pos = body ? body.pos.add(this.pos) : this.pos; + const rotation = body ? body.rotation : 0; + + ctx.beginPath(); + ctx.strokeStyle = color.toString(); + ctx.arc(pos.x, pos.y, this.radius, 0, Math.PI * 2); + ctx.closePath(); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(pos.x, pos.y); + ctx.lineTo(Math.cos(rotation) * this.radius + pos.x, Math.sin(rotation) * this.radius + pos.y); + ctx.closePath(); + ctx.stroke(); + } +} + +/** + * @obsolete Use [[CircleOptions]], CircleAreaOptions will be removed in v0.24.0 + */ +export interface CircleAreaOptions extends CircleOptions {} + +/** + * @obsolete Use [[Circle]], CircleArea will be removed in v0.24.0 + */ +export class CircleArea extends Circle {} diff --git a/src/engine/Collision/CircleArea.ts b/src/engine/Collision/CircleArea.ts deleted file mode 100644 index 9b08e88e..00000000 --- a/src/engine/Collision/CircleArea.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { Body } from './Body'; -import { BoundingBox } from './BoundingBox'; -import { CollisionArea } from './CollisionArea'; -import { PolygonArea } from './PolygonArea'; -import { EdgeArea } from './EdgeArea'; -import { CollisionJumpTable } from './CollisionJumpTable'; -import { CollisionContact } from './CollisionContact'; - -import { Vector, Ray, Projection } from '../Algebra'; -import { Physics } from '../Physics'; -import { Color } from '../Drawing/Color'; - -export interface CircleAreaOptions { - pos?: Vector; - radius?: number; - body?: Body; -} - -/** - * This is a circle collision area for the excalibur rigid body physics simulation - */ -export class CircleArea implements CollisionArea { - /** - * This is the center position of the circle, relative to the body position - */ - public pos: Vector = Vector.Zero; - /** - * This is the radius of the circle - */ - public radius: number; - /** - * The actor associated with this collision area - */ - public body: Body; - - constructor(options: CircleAreaOptions) { - this.pos = options.pos || Vector.Zero; - this.radius = options.radius || 0; - this.body = options.body || null; - } - - /** - * Get the center of the collision area in world coordinates - */ - public getCenter(): Vector { - if (this.body) { - return this.pos.add(this.body.pos); - } - return this.pos; - } - - /** - * Tests if a point is contained in this collision area - */ - public contains(point: Vector): boolean { - const distance = this.body.pos.distance(point); - if (distance <= this.radius) { - return true; - } - return false; - } - - /** - * Casts a ray at the CircleArea and returns the nearest point of collision - * @param ray - */ - public rayCast(ray: Ray, max: number = Infinity): Vector { - //https://en.wikipedia.org/wiki/Line%E2%80%93sphere_intersection - const c = this.getCenter(); - const dir = ray.dir; - const orig = ray.pos; - - const discriminant = Math.sqrt(Math.pow(dir.dot(orig.sub(c)), 2) - Math.pow(orig.sub(c).distance(), 2) + Math.pow(this.radius, 2)); - - if (discriminant < 0) { - // no intersection - return null; - } else { - let toi = 0; - if (discriminant === 0) { - toi = -dir.dot(orig.sub(c)); - if (toi > 0 && toi < max) { - return ray.getPoint(toi); - } - return null; - } else { - const toi1 = -dir.dot(orig.sub(c)) + discriminant; - const toi2 = -dir.dot(orig.sub(c)) - discriminant; - - const mintoi = Math.min(toi1, toi2); - if (mintoi <= max) { - return ray.getPoint(mintoi); - } - return null; - } - } - } - - /** - * @inheritdoc - */ - public collide(area: CollisionArea): CollisionContact { - if (area instanceof CircleArea) { - return CollisionJumpTable.CollideCircleCircle(this, area); - } else if (area instanceof PolygonArea) { - return CollisionJumpTable.CollideCirclePolygon(this, area); - } else if (area instanceof EdgeArea) { - return CollisionJumpTable.CollideCircleEdge(this, area); - } else { - throw new Error(`Circle could not collide with unknown ICollisionArea ${typeof area}`); - } - } - - /** - * Find the point on the shape furthest in the direction specified - */ - public getFurthestPoint(direction: Vector): Vector { - return this.getCenter().add(direction.normalize().scale(this.radius)); - } - - /** - * Get the axis aligned bounding box for the circle area - */ - public getBounds(): BoundingBox { - return new BoundingBox( - this.pos.x + this.body.pos.x - this.radius, - this.pos.y + this.body.pos.y - this.radius, - this.pos.x + this.body.pos.x + this.radius, - this.pos.y + this.body.pos.y + this.radius - ); - } - - /** - * Get axis not implemented on circles, since there are infinite axis in a circle - */ - public getAxes(): Vector[] { - return null; - } - - /** - * Returns the moment of inertia of a circle given it's mass - * https://en.wikipedia.org/wiki/List_of_moments_of_inertia - */ - public getMomentOfInertia(): number { - const mass = this.body ? this.body.mass : Physics.defaultMass; - return (mass * this.radius * this.radius) / 2; - } - - /** - * Tests the separating axis theorem for circles against polygons - */ - public testSeparatingAxisTheorem(polygon: PolygonArea): Vector { - const axes = polygon.getAxes(); - const pc = polygon.getCenter(); - // Special SAT with circles - const closestPointOnPoly = polygon.getFurthestPoint(this.pos.sub(pc)); - axes.push(this.pos.sub(closestPointOnPoly).normalize()); - - let minOverlap = Number.MAX_VALUE; - let minAxis = null; - let minIndex = -1; - for (let i = 0; i < axes.length; i++) { - const proj1 = polygon.project(axes[i]); - const proj2 = this.project(axes[i]); - const overlap = proj1.getOverlap(proj2); - if (overlap <= 0) { - return null; - } else { - if (overlap < minOverlap) { - minOverlap = overlap; - minAxis = axes[i]; - minIndex = i; - } - } - } - if (minIndex < 0) { - return null; - } - return minAxis.normalize().scale(minOverlap); - } - - /* istanbul ignore next */ - public recalc(): void { - // circles don't cache - } - - /** - * Project the circle along a specified axis - */ - public project(axis: Vector): Projection { - const scalars = []; - const point = this.getCenter(); - const dotProduct = point.dot(axis); - scalars.push(dotProduct); - scalars.push(dotProduct + this.radius); - scalars.push(dotProduct - this.radius); - return new Projection(Math.min.apply(Math, scalars), Math.max.apply(Math, scalars)); - } - - /* istanbul ignore next */ - public debugDraw(ctx: CanvasRenderingContext2D, color: Color = Color.Green) { - const pos = this.body ? this.body.pos.add(this.pos) : this.pos; - const rotation = this.body ? this.body.rotation : 0; - - ctx.beginPath(); - ctx.strokeStyle = color.toString(); - ctx.arc(pos.x, pos.y, this.radius, 0, Math.PI * 2); - ctx.closePath(); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(pos.x, pos.y); - ctx.lineTo(Math.cos(rotation) * this.radius + pos.x, Math.sin(rotation) * this.radius + pos.y); - ctx.closePath(); - ctx.stroke(); - } -} diff --git a/src/engine/Collision/Collider.ts b/src/engine/Collision/Collider.ts new file mode 100644 index 00000000..55350ca5 --- /dev/null +++ b/src/engine/Collision/Collider.ts @@ -0,0 +1,247 @@ +import { Color } from '../Drawing/Color'; +import * as DrawUtil from '../Util/DrawUtil'; +import { Eventable } from '../Interfaces/Index'; +import { GameEvent } from '../Events'; +import { Actor } from '../Actor'; +import { Body } from './Body'; +import { CollisionShape } from './CollisionShape'; +import { Vector } from '../Algebra'; +import { Physics } from '../Physics'; +import { BoundingBox } from './BoundingBox'; +import { CollisionType } from './CollisionType'; +import { CollisionContact } from './CollisionContact'; +import { EventDispatcher } from '../EventDispatcher'; +import { Pair } from './Pair'; +import { Clonable } from '../Interfaces/Clonable'; + +/** + * Type guard function to determine whether something is a Collider + */ +export function isCollider(x: Actor | Collider): x is Collider { + return x instanceof Collider; +} + +export interface ColliderOptions { + /** + * Optional [[shape|Shape]] to use with this collider, the shape defines the collidable region along with the [[bounding box|BoundingBox]] + */ + shape?: CollisionShape; + /** + * Optional body to associate with this collider + */ + body?: Body; + /** + * Optional [[collision type|CollisionType]], if not specified the default is [[CollisionType.PreventCollision]] + */ + type?: CollisionType; + /** + * Optional local bounds if other bounds are required instead of the bounding box from the shape. This overrides shape bounds. + */ + localBounds?: BoundingBox; + /** + * Optional flag to indicate moment of inertia from the shape should be used, by default it is true. + */ + useShapeInertia?: boolean; +} + +/** + * Collider describes material properties like shape, + * bounds, friction of the physics object. Only **one** collider can be associated with a body at a time + */ + +export class Collider implements Eventable, Clonable { + private _shape: CollisionShape; + public useShapeInertia: boolean; + private _events: EventDispatcher = new EventDispatcher(this); + + constructor({ body, type, shape, useShapeInertia = true }: ColliderOptions) { + // If shape is not supplied see if the body has an existing collider with a shape + if (body && body.collider && !shape) { + this._shape = body.collider.shape; + } else { + this._shape = shape; + this.body = body; + } + this.useShapeInertia = useShapeInertia; + this._shape.collider = this; + this.type = type; + } + + /** + * Returns a clone of the current collider, not associated with any body + */ + public clone() { + return new Collider({ + body: null, + type: this.type, + shape: this._shape.clone() + }); + } + + /** + * Get the unique id of the collider + */ + public get id(): number { + return this.body ? this.body.id : -1; + } + + /** + * Gets or sets the current collision type of this collider. By + * default it is ([[CollisionType.PreventCollision]]). + */ + public type: CollisionType = CollisionType.PreventCollision; + + /** + * Get the shape of the collider as a [[CollisionShape]] + */ + public get shape(): CollisionShape { + return this._shape; + } + + /** + * Set the shape of the collider as a [[CollisionShape]], if useShapeInertia is set the collider will use inertia from the shape. + */ + public set shape(shape: CollisionShape) { + this._shape = shape; + this._shape.collider = this; + if (this.useShapeInertia) { + this.inertia = isNaN(this._shape.inertia) ? this.inertia : this._shape.inertia; + } + } + + /** + * Return a reference to the body associated with this collider + */ + public body: Body; + + /** + * The center of the collider in world space + */ + public get center(): Vector { + return this.bounds.center; + } + + /** + * Is this collider active, if false it wont collide + */ + public get active(): boolean { + return this.body.active; + } + + /** + * Collide 2 colliders and product a collision contact if there is a collision, null if none + * + * Collision vector is in the direction of the other collider. Away from this collider, this -> other. + * @param other + */ + public collide(other: Collider): CollisionContact | null { + return this.shape.collide(other.shape); + } + + /** + * The current mass of the actor, mass can be thought of as the resistance to acceleration. + */ + public mass: number = 1.0; + + /** + * The current moment of inertia, moment of inertia can be thought of as the resistance to rotation. + */ + public inertia: number = 1000; + + /** + * The coefficient of friction on this actor + */ + public friction: number = 0.99; + + /** + * The also known as coefficient of restitution of this actor, represents the amount of energy preserved after collision or the + * bounciness. If 1, it is 100% bouncy, 0 it completely absorbs. + */ + public bounciness: number = 0.2; + + /** + * Returns a boolean indicating whether this body collided with + * or was in stationary contact with + * the body of the other [[Collider]] + */ + public touching(other: Collider): boolean { + const pair = new Pair(this, other); + pair.collide(); + + if (pair.collision) { + return true; + } + + return false; + } + + /** + * Returns the collider's [[BoundingBox]] calculated for this instant in world space. + * If there is no shape, a point bounding box is returned + */ + public get bounds(): BoundingBox { + if (this.shape) { + return this.shape.bounds; + } + + if (this.body) { + return new BoundingBox().translate(this.body.pos); + } + return new BoundingBox(); + } + + /** + * Returns the collider's [[BoundingBox]] relative to the body's position. + * If there is no shape, a point boudning box is returned + */ + public get localBounds(): BoundingBox { + if (this.shape) { + return this.shape.localBounds; + } + return new BoundingBox(); + } + + /** + * Updates the collision shapes geometry and internal caches if needed + */ + public update() { + if (this.shape) { + this.shape.recalc(); + } + } + + emit(eventName: string, event: GameEvent): void { + this._events.emit(eventName, event); + } + on(eventName: string, handler: (event: GameEvent) => void): void { + this._events.on(eventName, handler); + } + off(eventName: string, handler?: (event: GameEvent) => void): void { + this._events.off(eventName, handler); + } + once(eventName: string, handler: (event: GameEvent) => void): void { + this._events.once(eventName, handler); + } + + clear() { + this._events.clear(); + } + + /* istanbul ignore next */ + public debugDraw(ctx: CanvasRenderingContext2D) { + // Draw motion vectors + if (Physics.showMotionVectors) { + DrawUtil.vector(ctx, Color.Yellow, this.body.pos, this.body.acc.add(Physics.acc)); + DrawUtil.vector(ctx, Color.Red, this.body.pos, this.body.vel); + DrawUtil.point(ctx, Color.Red, this.body.pos); + } + + if (Physics.showBounds) { + this.bounds.debugDraw(ctx, Color.Yellow); + } + + if (Physics.showArea) { + this.shape.debugDraw(ctx, Color.Green); + } + } +} diff --git a/src/engine/Collision/CollisionArea.ts b/src/engine/Collision/CollisionArea.ts deleted file mode 100644 index a20c00ed..00000000 --- a/src/engine/Collision/CollisionArea.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Color } from '../Drawing/Color'; -import { CollisionContact } from './CollisionContact'; -import { Body } from './Body'; -import { BoundingBox } from './BoundingBox'; -import { Vector, Projection, Ray } from '../Algebra'; - -/** - * A collision area is a region of space that can detect when other collision areas intersect - * for the purposes of colliding 2 objects in excalibur. - */ -export interface CollisionArea { - /** - * Position of the collision area relative to the actor if it exists - */ - pos: Vector; - - /** - * Reference to the actor associated with this collision area - */ - body: Body; - - /** - * The center point of the collision area, for example if the area is a circle it would be the center. - */ - getCenter(): Vector; - - /** - * Find the furthest point on the convex hull of this particular area in a certain direction. - */ - getFurthestPoint(direction: Vector): Vector; - - /** - * Return the axis-aligned bounding box of the collision area - */ - getBounds(): BoundingBox; - - /** - * Return the axes of this particular shape - */ - getAxes(): Vector[]; - - /** - * Return the calculated moment of intertia for this area - */ - getMomentOfInertia(): number; - - // All new ICollisionAreas need to do the following - // Create a new collision function in the CollisionJumpTable against all the primitives - // Currently there are 3 primitive collision areas 3! = 6 jump functions - collide(area: CollisionArea): CollisionContact; - - /** - * Return wether the area contains a point inclusive to it's border - */ - contains(point: Vector): boolean; - - /** - * Return the point on the border of the collision area that intersects with a ray (if any). - */ - rayCast(ray: Ray, max?: number): Vector; - - /** - * Create a projection of this area along an axis. Think of this as casting a "shadow" along an axis - */ - project(axis: Vector): Projection; - - /** - * Recalculates internal caches and values - */ - recalc(): void; - - /** - * Draw any debug information - */ - debugDraw(ctx: CanvasRenderingContext2D, color: Color): void; -} diff --git a/src/engine/Collision/CollisionContact.ts b/src/engine/Collision/CollisionContact.ts index f17aa999..719251b9 100644 --- a/src/engine/Collision/CollisionContact.ts +++ b/src/engine/Collision/CollisionContact.ts @@ -1,14 +1,13 @@ -import { CollisionArea } from './CollisionArea'; import { Body } from './Body'; - -import { Actor, CollisionType } from '../Actor'; import { Vector } from '../Algebra'; import { Physics, CollisionResolutionStrategy } from '../Physics'; import { PostCollisionEvent, PreCollisionEvent } from '../Events'; import * as Util from '../Util/Util'; +import { CollisionType } from './CollisionType'; +import { Collider } from './Collider'; /** - * Collision contacts are used internally by Excalibur to resolve collision between actors. This + * Collision contacts are used internally by Excalibur to resolve collision between colliders. This * Pair prevents collisions from being evaluated more than one time */ export class CollisionContact { @@ -17,29 +16,29 @@ export class CollisionContact { */ id: string; /** - * The first rigid body in the collision + * The first collider in the collision */ - bodyA: CollisionArea; + colliderA: Collider; /** - * The second rigid body in the collision + * The second collider in the collision */ - bodyB: CollisionArea; + colliderB: Collider; /** - * The minimum translation vector to resolve penetration, pointing away from bodyA + * The minimum translation vector to resolve penetration, pointing away from colliderA */ mtv: Vector; /** - * The point of collision shared between bodyA and bodyB + * The point of collision shared between colliderA and colliderB */ point: Vector; /** - * The collision normal, pointing away from bodyA + * The collision normal, pointing away from colliderA */ normal: Vector; - constructor(bodyA: CollisionArea, bodyB: CollisionArea, mtv: Vector, point: Vector, normal: Vector) { - this.bodyA = bodyA; - this.bodyB = bodyB; + constructor(colliderA: Collider, colliderB: Collider, mtv: Vector, point: Vector, normal: Vector) { + this.colliderA = colliderA; + this.colliderB = colliderB; this.mtv = mtv; this.point = point; this.normal = normal; @@ -55,84 +54,82 @@ export class CollisionContact { } } - private _applyBoxImpulse(bodyA: Actor, bodyB: Actor, mtv: Vector) { - if (bodyA.collisionType === CollisionType.Active && bodyB.collisionType !== CollisionType.Passive) { + private _applyBoxImpulse(colliderA: Collider, colliderB: Collider, mtv: Vector) { + if (colliderA.type === CollisionType.Active && colliderB.type !== CollisionType.Passive) { // Resolve overlaps - if (bodyA.collisionType === CollisionType.Active && bodyB.collisionType === CollisionType.Active) { + if (colliderA.type === CollisionType.Active && colliderB.type === CollisionType.Active) { // split overlaps if both are Active mtv = mtv.scale(0.5); } // Apply mtv - bodyA.pos.y += mtv.y; - bodyA.pos.x += mtv.x; + colliderA.body.pos.y += mtv.y; + colliderA.body.pos.x += mtv.x; const mtvDir = mtv.normalize(); // only adjust if velocity is opposite - if (mtvDir.dot(bodyA.vel) < 0) { + if (mtvDir.dot(colliderA.body.vel) < 0) { // Cancel out velocity in direction of mtv - const velAdj = mtvDir.scale(mtvDir.dot(bodyA.vel.negate())); + const velAdj = mtvDir.scale(mtvDir.dot(colliderA.body.vel.negate())); - bodyA.vel = bodyA.vel.add(velAdj); + colliderA.body.vel = colliderA.body.vel.add(velAdj); } - bodyA.emit('postcollision', new PostCollisionEvent(bodyA, bodyB, Util.getSideFromVector(mtv), mtv)); + colliderA.emit('postcollision', new PostCollisionEvent(colliderA, colliderB, Util.getSideFromDirection(mtv), mtv)); } } private _resolveBoxCollision() { - const bodyA = this.bodyA.body.actor; - const bodyB = this.bodyB.body.actor; - const side = Util.getSideFromVector(this.mtv); + const side = Util.getSideFromDirection(this.mtv); const mtv = this.mtv.negate(); // Publish collision events on both participants - bodyA.emit('precollision', new PreCollisionEvent(bodyA, bodyB, side, mtv)); - bodyB.emit('precollision', new PreCollisionEvent(bodyB, bodyA, Util.getOppositeSide(side), mtv.negate())); + this.colliderA.emit('precollision', new PreCollisionEvent(this.colliderA, this.colliderB, side, mtv)); + this.colliderB.emit('precollision', new PreCollisionEvent(this.colliderB, this.colliderA, Util.getOppositeSide(side), mtv.negate())); - this._applyBoxImpulse(bodyA, bodyB, mtv); - this._applyBoxImpulse(bodyB, bodyA, mtv.negate()); + this._applyBoxImpulse(this.colliderA, this.colliderB, mtv); + this._applyBoxImpulse(this.colliderB, this.colliderA, mtv.negate()); } private _resolveRigidBodyCollision() { // perform collison on bounding areas - const bodyA: Body = this.bodyA.body; - const bodyB: Body = this.bodyB.body; - const mtv = this.mtv; // normal pointing away from bodyA - let normal = this.normal; // normal pointing away from bodyA - if (bodyA.actor === bodyB.actor) { + const bodyA: Body = this.colliderA.body; + const bodyB: Body = this.colliderB.body; + const mtv = this.mtv; // normal pointing away from colliderA + let normal = this.normal; // normal pointing away from colliderA + if (bodyA === bodyB) { // sanity check for existing pairs return; } // Publish collision events on both participants - const side = Util.getSideFromVector(this.mtv); - bodyA.actor.emit('precollision', new PreCollisionEvent(this.bodyA.body.actor, this.bodyB.body.actor, side, this.mtv)); - bodyB.actor.emit( + const side = Util.getSideFromDirection(this.mtv); + this.colliderA.emit('precollision', new PreCollisionEvent(this.colliderA, this.colliderB, side, this.mtv)); + this.colliderB.emit( 'precollision', - new PreCollisionEvent(this.bodyB.body.actor, this.bodyA.body.actor, Util.getOppositeSide(side), this.mtv.negate()) + new PreCollisionEvent(this.colliderB, this.colliderA, Util.getOppositeSide(side), this.mtv.negate()) ); // If any of the participants are passive then short circuit - if (bodyA.actor.collisionType === CollisionType.Passive || bodyB.actor.collisionType === CollisionType.Passive) { + if (this.colliderA.type === CollisionType.Passive || this.colliderB.type === CollisionType.Passive) { return; } - const invMassA = bodyA.actor.collisionType === CollisionType.Fixed ? 0 : 1 / bodyA.mass; - const invMassB = bodyB.actor.collisionType === CollisionType.Fixed ? 0 : 1 / bodyB.mass; + const invMassA = this.colliderA.type === CollisionType.Fixed ? 0 : 1 / this.colliderA.mass; + const invMassB = this.colliderB.type === CollisionType.Fixed ? 0 : 1 / this.colliderB.mass; - const invMoiA = bodyA.actor.collisionType === CollisionType.Fixed ? 0 : 1 / bodyA.moi; - const invMoiB = bodyB.actor.collisionType === CollisionType.Fixed ? 0 : 1 / bodyB.moi; + const invMoiA = this.colliderA.type === CollisionType.Fixed ? 0 : 1 / this.colliderA.inertia; + const invMoiB = this.colliderB.type === CollisionType.Fixed ? 0 : 1 / this.colliderB.inertia; // average restitution more relistic - const coefRestitution = Math.min(bodyA.restitution, bodyB.restitution); + const coefRestitution = Math.min(this.colliderA.bounciness, this.colliderB.bounciness); - const coefFriction = Math.min(bodyA.friction, bodyB.friction); + const coefFriction = Math.min(this.colliderA.friction, this.colliderB.friction); normal = normal.normalize(); const tangent = normal.normal().normalize(); - const ra = this.point.sub(this.bodyA.getCenter()); // point relative to bodyA position - const rb = this.point.sub(this.bodyB.getCenter()); /// point relative to bodyB + const ra = this.point.sub(this.colliderA.center); // point relative to colliderA position + const rb = this.point.sub(this.colliderB.center); /// point relative to colliderB // Relative velocity in linear terms // Angular to linear velocity formula -> omega = v/r @@ -156,13 +153,13 @@ export class CollisionContact { const impulse = -((1 + coefRestitution) * rvNormal) / (invMassA + invMassB + invMoiA * raTangent * raTangent + invMoiB * rbTangent * rbTangent); - if (bodyA.actor.collisionType === CollisionType.Fixed) { + if (this.colliderA.type === CollisionType.Fixed) { bodyB.vel = bodyB.vel.add(normal.scale(impulse * invMassB)); if (Physics.allowRigidBodyRotation) { bodyB.rx -= impulse * invMoiB * -rb.cross(normal); } bodyB.addMtv(mtv); - } else if (bodyB.actor.collisionType === CollisionType.Fixed) { + } else if (this.colliderB.type === CollisionType.Fixed) { bodyA.vel = bodyA.vel.sub(normal.scale(impulse * invMassA)); if (Physics.allowRigidBodyRotation) { bodyA.rx += impulse * invMoiA * -ra.cross(normal); @@ -200,13 +197,13 @@ export class CollisionContact { frictionImpulse = t.scale(-impulse * coefFriction); } - if (bodyA.actor.collisionType === CollisionType.Fixed) { + if (this.colliderA.type === CollisionType.Fixed) { // apply frictional impulse bodyB.vel = bodyB.vel.add(frictionImpulse.scale(invMassB)); if (Physics.allowRigidBodyRotation) { bodyB.rx += frictionImpulse.dot(t) * invMoiB * rb.cross(t); } - } else if (bodyB.actor.collisionType === CollisionType.Fixed) { + } else if (this.colliderB.type === CollisionType.Fixed) { // apply frictional impulse bodyA.vel = bodyA.vel.sub(frictionImpulse.scale(invMassA)); if (Physics.allowRigidBodyRotation) { @@ -225,10 +222,10 @@ export class CollisionContact { } } - bodyA.actor.emit('postcollision', new PostCollisionEvent(this.bodyA.body.actor, this.bodyB.body.actor, side, this.mtv)); - bodyB.actor.emit( + this.colliderA.emit('postcollision', new PostCollisionEvent(this.colliderA, this.colliderB, side, this.mtv)); + this.colliderB.emit( 'postcollision', - new PostCollisionEvent(this.bodyB.body.actor, this.bodyA.body.actor, Util.getOppositeSide(side), this.mtv.negate()) + new PostCollisionEvent(this.colliderB, this.colliderA, Util.getOppositeSide(side), this.mtv.negate()) ); } } diff --git a/src/engine/Collision/CollisionJumpTable.ts b/src/engine/Collision/CollisionJumpTable.ts index 9d1c8d20..290c2fab 100644 --- a/src/engine/Collision/CollisionJumpTable.ts +++ b/src/engine/Collision/CollisionJumpTable.ts @@ -1,15 +1,15 @@ -import { CircleArea } from './CircleArea'; +import { Circle } from './Circle'; import { CollisionContact } from './CollisionContact'; -import { PolygonArea } from './PolygonArea'; -import { EdgeArea } from './EdgeArea'; +import { ConvexPolygon } from './ConvexPolygon'; +import { Edge } from './Edge'; import { Vector } from '../Algebra'; export let CollisionJumpTable = { - CollideCircleCircle(circleA: CircleArea, circleB: CircleArea): CollisionContact { + CollideCircleCircle(circleA: Circle, circleB: Circle): CollisionContact { const radius = circleA.radius + circleB.radius; - const circleAPos = circleA.body.pos.add(circleA.pos); - const circleBPos = circleB.body.pos.add(circleB.pos); + const circleAPos = circleA.worldPos; + const circleBPos = circleB.worldPos; if (circleAPos.distance(circleBPos) > radius) { return null; } @@ -19,17 +19,17 @@ export let CollisionJumpTable = { const pointOfCollision = circleA.getFurthestPoint(axisOfCollision); - return new CollisionContact(circleA, circleB, mvt, pointOfCollision, axisOfCollision); + return new CollisionContact(circleA.collider, circleB.collider, mvt, pointOfCollision, axisOfCollision); }, - CollideCirclePolygon(circle: CircleArea, polygon: PolygonArea): CollisionContact { + CollideCirclePolygon(circle: Circle, polygon: ConvexPolygon): CollisionContact { let minAxis = circle.testSeparatingAxisTheorem(polygon); if (!minAxis) { return null; } // make sure that the minAxis is pointing away from circle - const samedir = minAxis.dot(polygon.getCenter().sub(circle.getCenter())); + const samedir = minAxis.dot(polygon.center.sub(circle.center)); minAxis = samedir < 0 ? minAxis.negate() : minAxis; const verts: Vector[] = []; @@ -46,12 +46,18 @@ export let CollisionJumpTable = { return null; } - return new CollisionContact(circle, polygon, minAxis, verts.length === 2 ? verts[0].average(verts[1]) : verts[0], minAxis.normalize()); + return new CollisionContact( + circle.collider, + polygon.collider, + minAxis, + verts.length === 2 ? verts[0].average(verts[1]) : verts[0], + minAxis.normalize() + ); }, - CollideCircleEdge(circle: CircleArea, edge: EdgeArea): CollisionContact { + CollideCircleEdge(circle: Circle, edge: Edge): CollisionContact { // center of the circle - const cc = circle.getCenter(); + const cc = circle.center; // vector in the direction of the edge const e = edge.end.sub(edge.begin); @@ -67,7 +73,13 @@ export let CollisionJumpTable = { if (dda > circle.radius * circle.radius) { return null; // no collision } - return new CollisionContact(circle, edge, da.normalize().scale(circle.radius - Math.sqrt(dda)), edge.begin, da.normalize()); + return new CollisionContact( + circle.collider, + edge.collider, + da.normalize().scale(circle.radius - Math.sqrt(dda)), + edge.begin, + da.normalize() + ); } // Potential region B collision (circle is on the right side of the edge, after the end) @@ -77,7 +89,13 @@ export let CollisionJumpTable = { if (ddb > circle.radius * circle.radius) { return null; } - return new CollisionContact(circle, edge, db.normalize().scale(circle.radius - Math.sqrt(ddb)), edge.end, db.normalize()); + return new CollisionContact( + circle.collider, + edge.collider, + db.normalize().scale(circle.radius - Math.sqrt(ddb)), + edge.end, + db.normalize() + ); } // Otherwise potential region AB collision (circle is in the middle of the edge between the beginning and end) @@ -103,7 +121,7 @@ export let CollisionJumpTable = { n = n.normalize(); const mvt = n.scale(Math.abs(circle.radius - Math.sqrt(dd))); - return new CollisionContact(circle, edge, mvt.negate(), pointOnEdge, n.negate()); + return new CollisionContact(circle.collider, edge.collider, mvt.negate(), pointOnEdge, n.negate()); }, CollideEdgeEdge(): CollisionContact { @@ -111,7 +129,7 @@ export let CollisionJumpTable = { return null; }, - CollidePolygonEdge(polygon: PolygonArea, edge: EdgeArea): CollisionContact { + CollidePolygonEdge(polygon: ConvexPolygon, edge: Edge): CollisionContact { // 3 cases: // (1) Polygon lands on the full face // (2) Polygon lands on the right point @@ -123,23 +141,24 @@ export let CollisionJumpTable = { if (polygon.contains(edge.begin)) { const { distance: mtv, face } = polygon.getClosestFace(edge.begin); if (mtv) { - return new CollisionContact(polygon, edge, mtv.negate(), edge.begin.add(mtv.negate()), face.normal().negate()); + return new CollisionContact(polygon.collider, edge.collider, mtv.negate(), edge.begin.add(mtv.negate()), face.normal().negate()); } } if (polygon.contains(edge.end)) { const { distance: mtv, face } = polygon.getClosestFace(edge.end); if (mtv) { - return new CollisionContact(polygon, edge, mtv.negate(), edge.end.add(mtv.negate()), face.normal().negate()); + return new CollisionContact(polygon.collider, edge.collider, mtv.negate(), edge.end.add(mtv.negate()), face.normal().negate()); } } - const pc = polygon.getCenter(); - const ec = edge.getCenter(); + const pc = polygon.center; + const ec = edge.center; const dir = ec.sub(pc).normalize(); // build a temporary polygon from the edge to use SAT - const linePoly = new PolygonArea({ + const linePoly = new ConvexPolygon({ + collider: edge.collider, points: [edge.begin, edge.end, edge.end.add(dir.scale(30)), edge.begin.add(dir.scale(30))] }); @@ -154,10 +173,10 @@ export let CollisionJumpTable = { edgeNormal = edgeNormal.dot(dir) < 0 ? edgeNormal.negate() : edgeNormal; minAxis = minAxis.dot(dir) < 0 ? minAxis.negate() : minAxis; - return new CollisionContact(polygon, edge, minAxis, polygon.getFurthestPoint(edgeNormal), edgeNormal); + return new CollisionContact(polygon.collider, edge.collider, minAxis, polygon.getFurthestPoint(edgeNormal), edgeNormal); }, - CollidePolygonPolygon(polyA: PolygonArea, polyB: PolygonArea): CollisionContact { + CollidePolygonPolygon(polyA: ConvexPolygon, polyB: ConvexPolygon): CollisionContact { // do a SAT test to find a min axis if it exists let minAxis = polyA.testSeparatingAxisTheorem(polyB); @@ -167,7 +186,7 @@ export let CollisionJumpTable = { } // make sure that minAxis is pointing from A -> B - const sameDir = minAxis.dot(polyB.getCenter().sub(polyA.getCenter())); + const sameDir = minAxis.dot(polyB.center.sub(polyA.center)); minAxis = sameDir < 0 ? minAxis.negate() : minAxis; // find rough point of collision @@ -190,6 +209,6 @@ export let CollisionJumpTable = { const contact = verts.length === 2 ? verts[0].add(verts[1]).scale(0.5) : verts[0]; - return new CollisionContact(polyA, polyB, minAxis, contact, minAxis.normalize()); + return new CollisionContact(polyA.collider, polyB.collider, minAxis, contact, minAxis.normalize()); } }; diff --git a/src/engine/Collision/CollisionResolver.ts b/src/engine/Collision/CollisionResolver.ts index 21b8829f..a78a6394 100644 --- a/src/engine/Collision/CollisionResolver.ts +++ b/src/engine/Collision/CollisionResolver.ts @@ -1,7 +1,6 @@ import { Body } from './Body'; import { FrameStats } from '../Debug'; import { Pair } from './Pair'; -import { Actor } from '../Actor'; import { CollisionResolutionStrategy } from '../Physics'; /** @@ -21,7 +20,7 @@ export interface CollisionBroadphase { /** * Detect potential collision pairs */ - broadphase(targets: Actor[], delta: number, stats?: FrameStats): Pair[]; + broadphase(targets: Body[], delta: number, stats?: FrameStats): Pair[]; /** * Identify actual collisions from those pairs, and calculate collision impulse @@ -41,7 +40,7 @@ export interface CollisionBroadphase { /** * Update the internal structures to track bodies */ - update(targets: Actor[], delta: number): number; + update(targets: Body[], delta: number): number; /** * Draw any debug information diff --git a/src/engine/Collision/CollisionShape.ts b/src/engine/Collision/CollisionShape.ts new file mode 100644 index 00000000..ced47b31 --- /dev/null +++ b/src/engine/Collision/CollisionShape.ts @@ -0,0 +1,106 @@ +import { Color } from '../Drawing/Color'; +import { CollisionContact } from './CollisionContact'; +import { Body } from './Body'; +import { BoundingBox } from './BoundingBox'; +import { Vector, Projection, Ray } from '../Algebra'; +import { Collider } from './Collider'; +import { Clonable } from '../Interfaces/Clonable'; + +/** + * A collision shape specifies the geometry that can detect when other collision shapes intersect + * for the purposes of colliding 2 objects in excalibur. + */ +export interface CollisionShape extends Clonable { + /** + * Position of the collision shape relative to the collider, by default (0, 0) meaning the shape is positioned on top of the collider. + */ + pos: Vector; + + /** + * Postion of the collision shape in world coordinates + */ + worldPos: Vector; + + /** + * Reference to the actor associated with this collision shape + * @obsolete Will be removed in v0.24.0 please use [[collider]] + */ + body: Body; + + /** + * Reference to the collider associated with this collision shape geometry + */ + collider?: Collider; + + /** + * The center point of the collision shape, for example if the shape is a circle it would be the center. + */ + center: Vector; + + /** + * Find the furthest point on the convex hull of this particular shape in a certain direction. + */ + getFurthestPoint(direction: Vector): Vector; + + /** + * Return the axis-aligned bounding box of the collision shape in world coordinates + */ + bounds: BoundingBox; + + /** + * Return the axis-aligned boudning box of the collision shape in local coordinates + */ + localBounds: BoundingBox; + + /** + * Return the axes of this particular shape + */ + axes: Vector[]; + + /** + * Return the calculated moment of intertia for this shape + */ + inertia: number; + + // All new CollisionShape need to do the following + // Create a new collision function in the CollisionJumpTable against all the primitives + // Currently there are 3 primitive collision shape 3! = 6 jump functions + collide(shape: CollisionShape): CollisionContact; + + /** + * Return wether the shape contains a point inclusive to it's border + */ + contains(point: Vector): boolean; + + /** + * Return the point on the border of the collision shape that intersects with a ray (if any). + */ + rayCast(ray: Ray, max?: number): Vector; + + /** + * Create a projection of this shape along an axis. Think of this as casting a "shadow" along an axis + */ + project(axis: Vector): Projection; + + /** + * Recalculates internal caches and values + */ + recalc(): void; + + /** + * Draw the shape + * @param ctx + * @param color + */ + draw(ctx: CanvasRenderingContext2D, color?: Color, pos?: Vector): void; + + /** + * Draw any debug information + */ + debugDraw(ctx: CanvasRenderingContext2D, color: Color): void; +} + +/** + * @obsolete Use interface [[CollisionShape]], CollisionArea will be deprecated in v0.24.0 + */ +export interface CollisionArea extends CollisionShape {} diff --git a/src/engine/Collision/CollisionType.ts b/src/engine/Collision/CollisionType.ts new file mode 100644 index 00000000..9ac3a60d --- /dev/null +++ b/src/engine/Collision/CollisionType.ts @@ -0,0 +1,30 @@ +/** + * An enum that describes the types of collisions actors can participate in + */ +export enum CollisionType { + /** + * Actors with the `PreventCollision` setting do not participate in any + * collisions and do not raise collision events. + */ + PreventCollision = 'PreventCollision', + /** + * Actors with the `Passive` setting only raise collision events, but are not + * influenced or moved by other actors and do not influence or move other actors. + */ + Passive = 'Passive', + /** + * Actors with the `Active` setting raise collision events and participate + * in collisions with other actors and will be push or moved by actors sharing + * the `Active` or `Fixed` setting. + */ + Active = 'Active', + /** + * Actors with the `Fixed` setting raise collision events and participate in + * collisions with other actors. Actors with the `Fixed` setting will not be + * pushed or moved by other actors sharing the `Fixed`. Think of Fixed + * actors as "immovable/onstoppable" objects. If two `Fixed` actors meet they will + * not be pushed or moved by each other, they will not interact except to throw + * collision events. + */ + Fixed = 'Fixed' +} diff --git a/src/engine/Collision/PolygonArea.ts b/src/engine/Collision/ConvexPolygon.ts similarity index 59% rename from src/engine/Collision/PolygonArea.ts rename to src/engine/Collision/ConvexPolygon.ts index c652140b..d08f8ad3 100644 --- a/src/engine/Collision/PolygonArea.ts +++ b/src/engine/Collision/ConvexPolygon.ts @@ -1,49 +1,107 @@ -import { Color } from './../Drawing/Color'; -import { Physics } from './../Physics'; +import { Color } from '../Drawing/Color'; +import { Physics } from '../Physics'; import { BoundingBox } from './BoundingBox'; -import { EdgeArea } from './EdgeArea'; +import { Edge } from './Edge'; import { CollisionJumpTable } from './CollisionJumpTable'; -import { CircleArea } from './CircleArea'; +import { Circle } from './Circle'; import { CollisionContact } from './CollisionContact'; -import { CollisionArea } from './CollisionArea'; +import { CollisionShape } from './CollisionShape'; import { Body } from './Body'; -import { Vector, Line, Ray, Projection } from './../Algebra'; +import { Vector, Line, Ray, Projection } from '../Algebra'; +import { Collider } from './Collider'; + +export interface ConvexPolygonOptions { + /** + * Point relative to a collider's position + */ -export interface PolygonAreaOptions { pos?: Vector; - points?: Vector[]; + /** + * Points in the polygon in order around the perimeter in local coordinates + */ + points: Vector[]; + /** + * Whether points are specified in clockwise or counter clockwise order, default counter-clockwise + */ clockwiseWinding?: boolean; + /** + * Collider to associate optionally with this shape + */ + collider?: Collider; + /** + * @obsolete Will be removed in v0.24.0 please use [[collider]] to set and retrieve body information + */ + body?: Body; } /** - * Polygon collision area for detecting collisions for actors, or independently + * Polygon collision shape for detecting collisions + * + * Example: + * [[include:BoxAndPolygonShape.md]] */ -export class PolygonArea implements CollisionArea { +export class ConvexPolygon implements CollisionShape { public pos: Vector; public points: Vector[]; + + /** + * @obsolete Will be removed in v0.24.0 please use [[collider]] to set and retrieve body information + */ public body: Body; + /** + * Collider associated with this shape + */ + public collider?: Collider; + private _transformedPoints: Vector[] = []; private _axes: Vector[] = []; private _sides: Line[] = []; - constructor(options: PolygonAreaOptions) { + constructor(options: ConvexPolygonOptions) { this.pos = options.pos || Vector.Zero; const winding = !!options.clockwiseWinding; this.points = (winding ? options.points.reverse() : options.points) || []; - this.body = options.body || null; + this.collider = this.collider = options.collider || null; + + // @obsolete Remove next release in v0.24.0, code exists for backwards compat + if (options.body) { + this.collider = options.body.collider; + this.body = this.collider.body; + } + // ================================== // calculate initial transformation this._calculateTransformation(); } /** - * Get the center of the collision area in world coordinates + * Returns a clone of this ConvexPolygon, not associated with any collider */ - public getCenter(): Vector { - if (this.body) { - return this.body.pos.add(this.pos); + public clone(): ConvexPolygon { + return new ConvexPolygon({ + pos: this.pos.clone(), + points: this.points.map((p) => p.clone()), + collider: null, + body: null + }); + } + + public get worldPos(): Vector { + if (this.collider && this.collider.body) { + return this.collider.body.pos.add(this.pos); + } + return this.pos; + } + + /** + * Get the center of the collision shape in world coordinates + */ + public get center(): Vector { + const body = this.collider ? this.collider.body : null; + if (body) { + return body.pos.add(this.pos); } return this.pos; } @@ -52,13 +110,18 @@ export class PolygonArea implements CollisionArea { * Calculates the underlying transformation from the body relative space to world space */ private _calculateTransformation() { - const pos = this.body ? this.body.pos.add(this.pos) : this.pos; - const angle = this.body ? this.body.rotation : 0; + const body = this.collider ? this.collider.body : null; + const pos = body ? body.pos.add(this.pos) : this.pos; + const angle = body ? body.rotation : 0; + const scale = body ? body.scale : Vector.One; const len = this.points.length; this._transformedPoints.length = 0; // clear out old transform for (let i = 0; i < len; i++) { - this._transformedPoints[i] = this.points[i].rotate(angle).add(pos); + this._transformedPoints[i] = this.points[i] + .scale(scale) + .rotate(angle) + .add(pos); } } @@ -66,7 +129,16 @@ export class PolygonArea implements CollisionArea { * Gets the points that make up the polygon in world space, from actor relative space (if specified) */ public getTransformedPoints(): Vector[] { - if (!this._transformedPoints.length) { + // only recalculate geometry if, hasn't been calculated + if ( + !this._transformedPoints.length || + // or the position or rotation has changed in world space + (this.collider && + this.collider.body && + (!this.collider.body.oldPos.equals(this.collider.body.pos) || + this.collider.body.oldRotation !== this.collider.body.rotation || + this.collider.body.oldScale !== this.collider.body.scale)) + ) { this._calculateTransformation(); } return this._transformedPoints; @@ -94,12 +166,11 @@ export class PolygonArea implements CollisionArea { this._axes.length = 0; this._transformedPoints.length = 0; this.getTransformedPoints(); - this.getAxes(); this.getSides(); } /** - * Tests if a point is contained in this collision area in world space + * Tests if a point is contained in this collision shape in world space */ public contains(point: Vector): boolean { // Always cast to the right, as long as we cast in a consitent fixed direction we @@ -119,19 +190,19 @@ export class PolygonArea implements CollisionArea { } /** - * Returns a collision contact if the 2 collision areas collide, otherwise collide will + * Returns a collision contact if the 2 collision shapes collide, otherwise collide will * return null. - * @param area + * @param shape */ - public collide(area: CollisionArea): CollisionContact { - if (area instanceof CircleArea) { - return CollisionJumpTable.CollideCirclePolygon(area, this); - } else if (area instanceof PolygonArea) { - return CollisionJumpTable.CollidePolygonPolygon(this, area); - } else if (area instanceof EdgeArea) { - return CollisionJumpTable.CollidePolygonEdge(this, area); + public collide(shape: CollisionShape): CollisionContact { + if (shape instanceof Circle) { + return CollisionJumpTable.CollideCirclePolygon(shape, this); + } else if (shape instanceof ConvexPolygon) { + return CollisionJumpTable.CollidePolygonPolygon(this, shape); + } else if (shape instanceof Edge) { + return CollisionJumpTable.CollidePolygonEdge(this, shape); } else { - throw new Error(`Polygon could not collide with unknown ICollisionArea ${typeof area}`); + throw new Error(`Polygon could not collide with unknown CollisionShape ${typeof shape}`); } } @@ -181,35 +252,27 @@ export class PolygonArea implements CollisionArea { } /** - * Get the axis aligned bounding box for the polygon area + * Get the axis aligned bounding box for the polygon shape in world coordinates */ - public getBounds(): BoundingBox { - // todo there is a faster way to do this + public get bounds(): BoundingBox { const points = this.getTransformedPoints(); - const minX = points.reduce(function(prev, curr) { - return Math.min(prev, curr.x); - }, 999999999); - const maxX = points.reduce(function(prev, curr) { - return Math.max(prev, curr.x); - }, -99999999); - - const minY = points.reduce(function(prev, curr) { - return Math.min(prev, curr.y); - }, 9999999999); - const maxY = points.reduce(function(prev, curr) { - return Math.max(prev, curr.y); - }, -9999999999); - - return new BoundingBox(minX, minY, maxX, maxY); + return BoundingBox.fromPoints(points); + } + + /** + * Get the axis aligned bounding box for the polygon shape in local coordinates + */ + public get localBounds(): BoundingBox { + return BoundingBox.fromPoints(this.points); } /** * Get the moment of inertia for an arbitrary polygon * https://en.wikipedia.org/wiki/List_of_moments_of_inertia */ - public getMomentOfInertia(): number { - const mass = this.body ? this.body.mass : Physics.defaultMass; + public get inertia(): number { + const mass = this.collider ? this.collider.mass : Physics.defaultMass; let numerator = 0; let denominator = 0; for (let i = 0; i < this.points.length; i++) { @@ -251,9 +314,9 @@ export class PolygonArea implements CollisionArea { } /** - * Get the axis associated with the edge + * Get the axis associated with the convex polygon */ - public getAxes(): Vector[] { + public get axes(): Vector[] { if (this._axes.length) { return this._axes; } @@ -272,10 +335,10 @@ export class PolygonArea implements CollisionArea { * Perform Separating Axis test against another polygon, returns null if no overlap in polys * Reference http://www.dyn4j.org/2010/01/sat/ */ - public testSeparatingAxisTheorem(other: PolygonArea): Vector { + public testSeparatingAxisTheorem(other: ConvexPolygon): Vector { const poly1 = this; const poly2 = other; - const axes = poly1.getAxes().concat(poly2.getAxes()); + const axes = poly1.axes.concat(poly2.axes); let minOverlap = Number.MAX_VALUE; let minAxis = null; @@ -320,6 +383,21 @@ export class PolygonArea implements CollisionArea { return new Projection(min, max); } + public draw(ctx: CanvasRenderingContext2D, color: Color = Color.Green, pos: Vector = Vector.Zero) { + ctx.beginPath(); + ctx.fillStyle = color.toString(); + const newPos = pos.add(this.pos); + // Iterate through the supplied points and construct a 'polygon' + const firstPoint = this.points[0].add(newPos); + ctx.moveTo(firstPoint.x, firstPoint.y); + this.points.map((p) => p.add(newPos)).forEach(function(point) { + ctx.lineTo(point.x, point.y); + }); + ctx.lineTo(firstPoint.x, firstPoint.y); + ctx.closePath(); + ctx.fill(); + } + /* istanbul ignore next */ public debugDraw(ctx: CanvasRenderingContext2D, color: Color = Color.Red) { ctx.beginPath(); @@ -335,3 +413,9 @@ export class PolygonArea implements CollisionArea { ctx.stroke(); } } + +/** + * @obsolete Use [[ConvexPolygonOptions]], PolygonAreaOptions will be removed in v0.24.0 + */ +export interface PolygonAreaOptions extends ConvexPolygonOptions {} +export class PolygonArea extends ConvexPolygon {} diff --git a/src/engine/Collision/DynamicTree.ts b/src/engine/Collision/DynamicTree.ts index 18fc10fd..53d322f8 100644 --- a/src/engine/Collision/DynamicTree.ts +++ b/src/engine/Collision/DynamicTree.ts @@ -201,12 +201,12 @@ export class DynamicTree { public trackBody(body: Body) { const node = new TreeNode(); node.body = body; - node.bounds = body.getBounds(); + node.bounds = body.collider.bounds; node.bounds.left -= 2; node.bounds.top -= 2; node.bounds.right += 2; node.bounds.bottom += 2; - this.nodes[body.actor.id] = node; + this.nodes[body.id] = node; this._insert(node); } @@ -214,17 +214,15 @@ export class DynamicTree { * Updates the dynamic tree given the current bounds of each body being tracked */ public updateBody(body: Body) { - const node = this.nodes[body.actor.id]; + const node = this.nodes[body.id]; if (!node) { return false; } - const b = body.getBounds(); + const b = body.collider.bounds; // if the body is outside the world no longer update it if (!this.worldBounds.contains(b)) { - Logger.getInstance().warn( - 'Actor with id ' + body.actor.id + ' is outside the world bounds and will no longer be tracked for physics' - ); + Logger.getInstance().warn('Collider with id ' + body.id + ' is outside the world bounds and will no longer be tracked for physics'); this.untrackBody(body); return false; } @@ -263,13 +261,13 @@ export class DynamicTree { * Untracks a body from the dynamic tree */ public untrackBody(body: Body) { - const node = this.nodes[body.actor.id]; + const node = this.nodes[body.collider.id]; if (!node) { return; } this._remove(node); - this.nodes[body.actor.id] = null; - delete this.nodes[body.actor.id]; + this.nodes[body.collider.id] = null; + delete this.nodes[body.collider.id]; } /** @@ -407,9 +405,9 @@ export class DynamicTree { * the tree until all possible colliders have been returned. */ public query(body: Body, callback: (other: Body) => boolean): void { - const bounds = body.getBounds(); + const bounds = body.collider.bounds; const helper = (currentNode: TreeNode): boolean => { - if (currentNode && currentNode.bounds.collides(bounds)) { + if (currentNode && currentNode.bounds.intersect(bounds)) { if (currentNode.isLeaf() && currentNode.body !== body) { if (callback.call(body, currentNode.body)) { return true; diff --git a/src/engine/Collision/DynamicTreeCollisionBroadphase.ts b/src/engine/Collision/DynamicTreeCollisionBroadphase.ts index 0a117afe..71823fb6 100644 --- a/src/engine/Collision/DynamicTreeCollisionBroadphase.ts +++ b/src/engine/Collision/DynamicTreeCollisionBroadphase.ts @@ -5,11 +5,12 @@ import { Pair } from './Pair'; import { Body } from './Body'; import { Vector, Ray } from '../Algebra'; -import { Actor, CollisionType } from '../Actor'; import { FrameStats } from '../Debug'; import { CollisionResolutionStrategy } from '../Physics'; import { Logger } from '../Util/Log'; import { CollisionStartEvent, CollisionEndEvent } from '../Events'; +import { CollisionType } from './CollisionType'; +import { Collider } from './Collider'; export class DynamicTreeCollisionBroadphase implements CollisionBroadphase { private _dynamicCollisionTree = new DynamicTree(); @@ -40,24 +41,25 @@ export class DynamicTreeCollisionBroadphase implements CollisionBroadphase { this._dynamicCollisionTree.untrackBody(target); } - private _shouldGenerateCollisionPair(actorA: Actor, actorB: Actor) { + private _shouldGenerateCollisionPair(colliderA: Collider, colliderB: Collider) { // if the collision pair has been calculated already short circuit - const hash = Pair.calculatePairHash(actorA.body, actorB.body); + const hash = Pair.calculatePairHash(colliderA, colliderB); if (this._collisionHash[hash]) { return false; // pair exists easy exit return false } - return Pair.canCollide(actorA, actorB); + return Pair.canCollide(colliderA, colliderB); } /** * Detects potential collision pairs in a broadphase approach with the dynamic aabb tree strategy */ - public broadphase(targets: Actor[], delta: number, stats?: FrameStats): Pair[] { + public broadphase(targets: Body[], delta: number, stats?: FrameStats): Pair[] { const seconds = delta / 1000; + // Retrieve the list of potential colliders, exclude killed, prevented, and self - const potentialColliders = targets.filter((other) => { - return !other.isKilled() && other.collisionType !== CollisionType.PreventCollision; + const potentialColliders = targets.map((t) => t.collider).filter((other) => { + return other.active && other.type !== CollisionType.PreventCollision; }); // clear old list of collision pairs @@ -65,14 +67,14 @@ export class DynamicTreeCollisionBroadphase implements CollisionBroadphase { this._collisionHash = {}; // check for normal collision pairs - let actor: Actor; + let collider: Collider; for (let j = 0, l = potentialColliders.length; j < l; j++) { - actor = potentialColliders[j]; + collider = potentialColliders[j]; // Query the collision tree for potential colliders - this._dynamicCollisionTree.query(actor.body, (other: Body) => { - if (this._shouldGenerateCollisionPair(actor, other.actor)) { - const pair = new Pair(actor.body, other); + this._dynamicCollisionTree.query(collider.body, (other: Body) => { + if (this._shouldGenerateCollisionPair(collider, other.collider)) { + const pair = new Pair(collider, other.collider); this._collisionHash[pair.id] = true; this._collisionPairCache.push(pair); } @@ -87,19 +89,19 @@ export class DynamicTreeCollisionBroadphase implements CollisionBroadphase { // Check dynamic tree for fast moving objects // Fast moving objects are those moving at least there smallest bound per frame if (Physics.checkForFastBodies) { - for (const actor of potentialColliders) { + for (const collider of potentialColliders) { // Skip non-active objects. Does not make sense on other collison types - if (actor.collisionType !== CollisionType.Active) { + if (collider.type !== CollisionType.Active) { continue; } // Maximum travel distance next frame const updateDistance = - actor.vel.magnitude() * seconds + // velocity term - actor.acc.magnitude() * 0.5 * seconds * seconds; // acc term + collider.body.vel.magnitude() * seconds + // velocity term + collider.body.acc.magnitude() * 0.5 * seconds * seconds; // acc term // Find the minimum dimension - const minDimension = Math.min(actor.body.getBounds().getHeight(), actor.body.getBounds().getWidth()); + const minDimension = Math.min(collider.bounds.height, collider.bounds.width); if (Physics.disableMinimumSpeedForFastBody || updateDistance > minDimension / 2) { if (stats) { stats.physics.fastBodies++; @@ -107,20 +109,20 @@ export class DynamicTreeCollisionBroadphase implements CollisionBroadphase { // start with the oldPos because the integration for actors has already happened // objects resting on a surface may be slightly penatrating in the current position - const updateVec = actor.pos.sub(actor.oldPos); - const centerPoint = actor.body.collisionArea.getCenter(); - const furthestPoint = actor.body.collisionArea.getFurthestPoint(actor.vel); + const updateVec = collider.body.pos.sub(collider.body.oldPos); + const centerPoint = collider.shape.center; + const furthestPoint = collider.shape.getFurthestPoint(collider.body.vel); const origin: Vector = furthestPoint.sub(updateVec); - const ray: Ray = new Ray(origin, actor.vel); + const ray: Ray = new Ray(origin, collider.body.vel); // back the ray up by -2x surfaceEpsilon to account for fast moving objects starting on the surface ray.pos = ray.pos.add(ray.dir.scale(-2 * Physics.surfaceEpsilon)); let minBody: Body; let minTranslate: Vector = new Vector(Infinity, Infinity); this._dynamicCollisionTree.rayCastQuery(ray, updateDistance + Physics.surfaceEpsilon * 2, (other: Body) => { - if (actor.body !== other && other.collisionArea) { - const hitPoint = other.collisionArea.rayCast(ray, updateDistance + Physics.surfaceEpsilon * 10); + if (collider.body !== other && other.collider.shape) { + const hitPoint = other.collider.shape.rayCast(ray, updateDistance + Physics.surfaceEpsilon * 10); if (hitPoint) { const translate = hitPoint.sub(origin); if (translate.magnitude() < minTranslate.magnitude()) { @@ -133,7 +135,7 @@ export class DynamicTreeCollisionBroadphase implements CollisionBroadphase { }); if (minBody && Vector.isValid(minTranslate)) { - const pair = new Pair(actor.body, minBody); + const pair = new Pair(collider, minBody.collider); if (!this._collisionHash[pair.id]) { this._collisionHash[pair.id] = true; this._collisionPairCache.push(pair); @@ -141,11 +143,11 @@ export class DynamicTreeCollisionBroadphase implements CollisionBroadphase { // move the fast moving object to the other body // need to push into the surface by ex.Physics.surfaceEpsilon const shift = centerPoint.sub(furthestPoint); - actor.pos = origin + collider.body.pos = origin .add(shift) .add(minTranslate) .add(ray.dir.scale(2 * Physics.surfaceEpsilon)); - actor.body.collisionArea.recalc(); + collider.shape.recalc(); if (stats) { stats.physics.fastBodyCollisions++; @@ -181,11 +183,11 @@ export class DynamicTreeCollisionBroadphase implements CollisionBroadphase { pair.resolve(strategy); if (pair.collision) { - pair.bodyA.applyMtv(); - pair.bodyB.applyMtv(); + pair.colliderA.body.applyMtv(); + pair.colliderB.body.applyMtv(); // todo still don't like this, this is a small integration step to resolve narrowphase collisions - pair.bodyA.actor.integrate(delta * Physics.collisionShift); - pair.bodyB.actor.integrate(delta * Physics.collisionShift); + pair.colliderA.body.integrate(delta * Physics.collisionShift); + pair.colliderB.body.integrate(delta * Physics.collisionShift); } } @@ -201,8 +203,8 @@ export class DynamicTreeCollisionBroadphase implements CollisionBroadphase { // find all new collisions if (!this._lastFramePairsHash[p.id]) { - const actor1 = p.bodyA.actor; - const actor2 = p.bodyB.actor; + const actor1 = p.colliderA; + const actor2 = p.colliderB; actor1.emit('collisionstart', new CollisionStartEvent(actor1, actor2, p)); actor2.emit('collisionstart', new CollisionStartEvent(actor2, actor1, p)); } @@ -211,8 +213,8 @@ export class DynamicTreeCollisionBroadphase implements CollisionBroadphase { // find all old collisions for (const p of this._lastFramePairs) { if (!currentFrameHash[p.id]) { - const actor1 = p.bodyA.actor; - const actor2 = p.bodyB.actor; + const actor1 = p.colliderA; + const actor2 = p.colliderB; actor1.emit('collisionend', new CollisionEndEvent(actor1, actor2)); actor2.emit('collisionend', new CollisionEndEvent(actor2, actor1)); } @@ -226,12 +228,12 @@ export class DynamicTreeCollisionBroadphase implements CollisionBroadphase { /** * Update the dynamic tree positions */ - public update(targets: Actor[]): number { + public update(targets: Body[]): number { let updated = 0; const len = targets.length; for (let i = 0; i < len; i++) { - if (this._dynamicCollisionTree.updateBody(targets[i].body)) { + if (this._dynamicCollisionTree.updateBody(targets[i])) { updated++; } } diff --git a/src/engine/Collision/EdgeArea.ts b/src/engine/Collision/Edge.ts similarity index 55% rename from src/engine/Collision/EdgeArea.ts rename to src/engine/Collision/Edge.ts index 2deceef7..86d7037f 100644 --- a/src/engine/Collision/EdgeArea.ts +++ b/src/engine/Collision/Edge.ts @@ -2,57 +2,104 @@ import { Body } from './Body'; import { BoundingBox } from './BoundingBox'; import { CollisionContact } from './CollisionContact'; import { CollisionJumpTable } from './CollisionJumpTable'; -import { CollisionArea } from './CollisionArea'; -import { CircleArea } from './CircleArea'; -import { PolygonArea } from './PolygonArea'; +import { CollisionShape } from './CollisionShape'; +import { Circle } from './Circle'; +import { ConvexPolygon } from './ConvexPolygon'; import { Vector, Ray, Projection } from '../Algebra'; import { Physics } from '../Physics'; import { Color } from '../Drawing/Color'; +import { Collider } from './Collider'; -export interface EdgeAreaOptions { - begin?: Vector; - end?: Vector; +export interface EdgeOptions { + /** + * The beginning of the edge defined in local coordinates to the collider + */ + begin: Vector; + /** + * The ending of the edge defined in local coordinates to the collider + */ + end: Vector; + /** + * Optionally the collider associated with this edge + */ + collider?: Collider; + + // @obsolete Will be removed in v0.24.0 please use [[collider]] to set and retrieve body information body?: Body; } -export class EdgeArea implements CollisionArea { +/** + * Edge is a single line collision shape to create collisions with a single line. + * + * Example: + * [[include:EdgeShape.md]] + */ +export class Edge implements CollisionShape { body: Body; + collider?: Collider; pos: Vector; begin: Vector; end: Vector; - constructor(options: EdgeAreaOptions) { + constructor(options: EdgeOptions) { this.begin = options.begin || Vector.Zero; this.end = options.end || Vector.Zero; - this.body = options.body || null; + this.collider = options.collider || null; + this.pos = this.center; - this.pos = this.getCenter(); + // @obsolete Remove next release in v0.24.0, code exists for backwards compat + if (options.body) { + this.collider = options.body.collider; + this.body = this.collider.body; + } + // ================================== + } + + /** + * Returns a clone of this Edge, not associated with any collider + */ + public clone(): Edge { + return new Edge({ + begin: this.begin.clone(), + end: this.end.clone(), + collider: null, + body: null + }); + } + + public get worldPos(): Vector { + if (this.collider && this.collider.body) { + return this.collider.body.pos.add(this.pos); + } + return this.pos; } /** * Get the center of the collision area in world coordinates */ - public getCenter(): Vector { + public get center(): Vector { const pos = this.begin.average(this.end).add(this._getBodyPos()); return pos; } private _getBodyPos(): Vector { let bodyPos = Vector.Zero; - if (this.body.pos) { - bodyPos = this.body.pos; + if (this.collider && this.collider.body) { + bodyPos = this.collider.body.pos; } return bodyPos; } private _getTransformedBegin(): Vector { - const angle = this.body ? this.body.rotation : 0; + const body = this.collider ? this.collider.body : null; + const angle = body ? body.rotation : 0; return this.begin.rotate(angle).add(this._getBodyPos()); } private _getTransformedEnd(): Vector { - const angle = this.body ? this.body.rotation : 0; + const body = this.collider ? this.collider.body : null; + const angle = body ? body.rotation : 0; return this.end.rotate(angle).add(this._getBodyPos()); } @@ -115,15 +162,15 @@ export class EdgeArea implements CollisionArea { /** * @inheritdoc */ - public collide(area: CollisionArea): CollisionContact { - if (area instanceof CircleArea) { - return CollisionJumpTable.CollideCircleEdge(area, this); - } else if (area instanceof PolygonArea) { - return CollisionJumpTable.CollidePolygonEdge(area, this); - } else if (area instanceof EdgeArea) { + public collide(shape: CollisionShape): CollisionContact { + if (shape instanceof Circle) { + return CollisionJumpTable.CollideCircleEdge(shape, this); + } else if (shape instanceof ConvexPolygon) { + return CollisionJumpTable.CollidePolygonEdge(shape, this); + } else if (shape instanceof Edge) { return CollisionJumpTable.CollideEdgeEdge(); } else { - throw new Error(`Edge could not collide with unknown ICollisionArea ${typeof area}`); + throw new Error(`Edge could not collide with unknown CollisionShape ${typeof shape}`); } } @@ -140,24 +187,27 @@ export class EdgeArea implements CollisionArea { } } + private _boundsFromBeginEnd(begin: Vector, end: Vector) { + return new BoundingBox(Math.min(begin.x, end.x), Math.min(begin.y, end.y), Math.max(begin.x, end.x), Math.max(begin.y, end.y)); + } + /** - * Get the axis aligned bounding box for the circle area + * Get the axis aligned bounding box for the edge shape in world space */ - public getBounds(): BoundingBox { + public get bounds(): BoundingBox { const transformedBegin = this._getTransformedBegin(); const transformedEnd = this._getTransformedEnd(); - return new BoundingBox( - Math.min(transformedBegin.x, transformedEnd.x), - Math.min(transformedBegin.y, transformedEnd.y), - Math.max(transformedBegin.x, transformedEnd.x), - Math.max(transformedBegin.y, transformedEnd.y) - ); + return this._boundsFromBeginEnd(transformedBegin, transformedEnd); + } + + public get localBounds(): BoundingBox { + return this._boundsFromBeginEnd(this.begin, this.end); } /** * Get the axis associated with the edge */ - public getAxes(): Vector[] { + public get axes(): Vector[] { const e = this._getTransformedEnd().sub(this._getTransformedBegin()); const edgeNormal = e.normal(); @@ -173,8 +223,8 @@ export class EdgeArea implements CollisionArea { * Get the moment of inertia for an edge * https://en.wikipedia.org/wiki/List_of_moments_of_inertia */ - public getMomentOfInertia(): number { - const mass = this.body ? this.body.mass : Physics.defaultMass; + public get inertia(): number { + const mass = this.collider ? this.collider.mass : Physics.defaultMass; const length = this.end.sub(this.begin).distance() / 2; return mass * length * length; } @@ -201,6 +251,17 @@ export class EdgeArea implements CollisionArea { return new Projection(Math.min.apply(Math, scalars), Math.max.apply(Math, scalars)); } + public draw(ctx: CanvasRenderingContext2D, color: Color = Color.Green, pos: Vector = Vector.Zero) { + const begin = this.begin.add(pos); + const end = this.end.add(pos); + ctx.strokeStyle = color.toString(); + ctx.beginPath(); + ctx.moveTo(begin.x, begin.y); + ctx.lineTo(end.x, end.y); + ctx.closePath(); + ctx.stroke(); + } + /* istanbul ignore next */ public debugDraw(ctx: CanvasRenderingContext2D, color: Color = Color.Red) { ctx.strokeStyle = color.toString(); @@ -211,3 +272,13 @@ export class EdgeArea implements CollisionArea { ctx.stroke(); } } + +/** + * @obsolete Use [[EdgeOptions]], EdgeAreaOptions will be removed in v0.24.0 + */ +export interface EdgeAreaOptions extends EdgeOptions {} + +/** + * @obsolete Use [[Edge]], EdgeArea will be removed in v0.24.0 + */ +export class EdgeArea extends Edge {} diff --git a/src/engine/Collision/Index.ts b/src/engine/Collision/Index.ts index f51ff0fc..02104061 100644 --- a/src/engine/Collision/Index.ts +++ b/src/engine/Collision/Index.ts @@ -1,15 +1,16 @@ export * from './Body'; +export * from './Collider'; export * from './BoundingBox'; -export * from './CircleArea'; +export * from './Circle'; export * from './CollisionContact'; export * from './CollisionJumpTable'; export * from './DynamicTree'; export * from './DynamicTreeCollisionBroadphase'; -export * from './EdgeArea'; -export * from './CollisionArea'; +export * from './Edge'; +export * from './CollisionShape'; export * from './CollisionResolver'; export * from './Physics'; -export * from './NaiveCollisionBroadphase'; export * from './Pair'; -export * from './PolygonArea'; +export * from './ConvexPolygon'; export * from './Side'; +export * from './Shape'; diff --git a/src/engine/Collision/NaiveCollisionBroadphase.ts b/src/engine/Collision/NaiveCollisionBroadphase.ts deleted file mode 100644 index c4653291..00000000 --- a/src/engine/Collision/NaiveCollisionBroadphase.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Physics } from './../Physics'; -import { CollisionContact } from './CollisionContact'; -import { Pair } from './Pair'; -import { Actor, CollisionType } from './../Actor'; -import { CollisionBroadphase } from './CollisionResolver'; -import { CollisionStartEvent, CollisionEndEvent } from '../Events'; - -export class NaiveCollisionBroadphase implements CollisionBroadphase { - private _lastFramePairs: Pair[] = []; - private _lastFramePairsHash: { [pairId: string]: Pair } = {}; - - public track() { - // pass - } - - public untrack() { - // pass - } - - /** - * Detects potential collision pairs in a broadphase approach with the dynamic aabb tree strategy - */ - public broadphase(targets: Actor[]): Pair[] { - // Retrieve the list of potential colliders, exclude killed, prevented, and self - const potentialColliders = targets.filter((other) => { - return !other.isKilled() && other.collisionType !== CollisionType.PreventCollision; - }); - - let actor1: Actor; - let actor2: Actor; - const collisionPairs: Pair[] = []; - - for (let j = 0, l = potentialColliders.length; j < l; j++) { - actor1 = potentialColliders[j]; - - for (let i = j + 1; i < l; i++) { - actor2 = potentialColliders[i]; - - let minimumTranslationVector; - if ((minimumTranslationVector = actor1.collides(actor2))) { - const pair = new Pair(actor1.body, actor2.body); - pair.collision = new CollisionContact( - actor1.collisionArea, - actor2.collisionArea, - minimumTranslationVector, - actor1.pos, - minimumTranslationVector - ); - if ( - !collisionPairs.some((cp) => { - return cp.id === pair.id; - }) - ) { - collisionPairs.push(pair); - } - } - } - } - return collisionPairs; - } - - /** - * Identify actual collisions from those pairs, and calculate collision impulse - */ - public narrowphase(pairs: Pair[]): Pair[] { - return pairs; - } - - public runCollisionStartEnd(pairs: Pair[]) { - const currentFrameHash: { [pairId: string]: Pair } = {}; - - for (const p of pairs) { - // load currentFrameHash - currentFrameHash[p.id] = p; - - // find all new collisions - if (!this._lastFramePairsHash[p.id]) { - const actor1 = p.bodyA.actor; - const actor2 = p.bodyB.actor; - actor1.emit('collisionstart', new CollisionStartEvent(actor1, actor2, p)); - actor2.emit('collisionstart', new CollisionStartEvent(actor2, actor1, p)); - } - } - - // find all old collisions - for (const p of this._lastFramePairs) { - if (!currentFrameHash[p.id]) { - const actor1 = p.bodyA.actor; - const actor2 = p.bodyB.actor; - actor1.emit('collisionend', new CollisionEndEvent(actor1, actor2)); - actor2.emit('collisionend', new CollisionEndEvent(actor2, actor1)); - } - } - - // reset the last frame cache - this._lastFramePairs = pairs; - this._lastFramePairsHash = currentFrameHash; - } - - /** - * Resolve the position and velocity of the physics bodies - */ - public resolve(pairs: Pair[]): Pair[] { - for (const pair of pairs) { - pair.resolve(Physics.collisionResolutionStrategy); - } - - return pairs.filter((p) => p.canCollide); - } - - public update(): number { - return 0; - } - - public debugDraw() { - return; - } -} diff --git a/src/engine/Collision/Pair.ts b/src/engine/Collision/Pair.ts index be8691b5..a93a1bc9 100644 --- a/src/engine/Collision/Pair.ts +++ b/src/engine/Collision/Pair.ts @@ -1,10 +1,10 @@ import { Physics } from './../Physics'; import { Color } from './../Drawing/Color'; -import { Body } from './Body'; import { CollisionContact } from './CollisionContact'; -import { CollisionType, Actor } from '../Actor'; import { CollisionResolutionStrategy } from '../Physics'; import * as DrawUtil from '../Util/DrawUtil'; +import { CollisionType } from './CollisionType'; +import { Collider } from './Collider'; /** * Models a potential collision between 2 bodies @@ -13,23 +13,23 @@ export class Pair { public id: string = null; public collision: CollisionContact = null; - constructor(public bodyA: Body, public bodyB: Body) { - this.id = Pair.calculatePairHash(bodyA, bodyB); + constructor(public colliderA: Collider, public colliderB: Collider) { + this.id = Pair.calculatePairHash(colliderA, colliderB); } - public static canCollide(actorA: Actor, actorB: Actor) { + public static canCollide(colliderA: Collider, colliderB: Collider) { // if both are fixed short circuit - if (actorA.collisionType === CollisionType.Fixed && actorB.collisionType === CollisionType.Fixed) { + if (colliderA.type === CollisionType.Fixed && colliderB.type === CollisionType.Fixed) { return false; } // if the either is prevent collision short circuit - if (actorB.collisionType === CollisionType.PreventCollision || actorA.collisionType === CollisionType.PreventCollision) { + if (colliderB.type === CollisionType.PreventCollision || colliderA.type === CollisionType.PreventCollision) { return false; } // if either is dead short circuit - if (actorA.isKilled() || actorB.isKilled()) { + if (!colliderA.active || !colliderB.active) { return false; } @@ -40,8 +40,8 @@ export class Pair { * Returns whether or not it is possible for the pairs to collide */ public get canCollide(): boolean { - const actorA = this.bodyA.actor; - const actorB = this.bodyB.actor; + const actorA = this.colliderA; + const actorB = this.colliderB; return Pair.canCollide(actorA, actorB); } @@ -49,7 +49,7 @@ export class Pair { * Runs the collison intersection logic on the members of this pair */ public collide() { - this.collision = this.bodyA.collisionArea.collide(this.bodyB.collisionArea); + this.collision = this.colliderA.collide(this.colliderB); } /** @@ -64,11 +64,11 @@ export class Pair { /** * Calculates the unique pair hash id for this collision pair */ - public static calculatePairHash(bodyA: Body, bodyB: Body): string { - if (bodyA.actor.id < bodyB.actor.id) { - return `#${bodyA.actor.id}+${bodyB.actor.id}`; + public static calculatePairHash(colliderA: Collider, colliderB: Collider): string { + if (colliderA.id < colliderB.id) { + return `#${colliderA.id}+${colliderB.id}`; } else { - return `#${bodyB.actor.id}+${bodyA.actor.id}`; + return `#${colliderB.id}+${colliderA.id}`; } } diff --git a/src/engine/Collision/Shape.ts b/src/engine/Collision/Shape.ts new file mode 100644 index 00000000..9c116d99 --- /dev/null +++ b/src/engine/Collision/Shape.ts @@ -0,0 +1,62 @@ +import { ConvexPolygon } from './ConvexPolygon'; +import { Circle } from './Circle'; +import { Edge } from './Edge'; +import { BoundingBox } from './BoundingBox'; +import { Vector } from '../Algebra'; + +/** + * Excalibur shape helper for defining collision shapes quickly + */ +export class Shape { + /** + * Creates a box collision shape, under the hood defines a [[ConvexPolygon]] collision shape + * @param width Width of the box + * @param height Height of the box + * @param anchor Anchor of the box (default (.5, .5)) which positions the box relative to the center of the collider's position + * @param center Optional offset relative to the collider in local coordinates + */ + static Box(width: number, height: number, anchor: Vector = Vector.Half, center: Vector = Vector.Zero): ConvexPolygon { + return new ConvexPolygon({ + points: new BoundingBox(-width * anchor.x, -height * anchor.y, width - width * anchor.x, height - height * anchor.y).getPoints(), + pos: center + }); + } + + /** + * Creates a new [[arbitrary polygon|ConvexPolygon]] collision shape + * @param points Points specified in counter clockwise + * @param clockwiseWinding Optionally changed the winding of points, by default false meaning counter-clockwise winding. + * @param center Optional offset relative to the collider in local coordinates + */ + static Polygon(points: Vector[], clockwiseWinding: boolean = false, center: Vector = Vector.Zero): ConvexPolygon { + return new ConvexPolygon({ + points: points, + pos: center, + clockwiseWinding: clockwiseWinding + }); + } + + /** + * Creates a new [[circle|Circle]] collision shape + * @param radius Radius of the circle shape + * @param center Optional offset relative to the collider in local coordinates + */ + static Circle(radius: number, center: Vector = Vector.Zero): Circle { + return new Circle({ + radius: radius, + pos: center + }); + } + + /** + * Creates a new [[edge|Edge]] collision shape + * @param begin Beginning of the edge in local coordinates to the collider + * @param end Ending of the edge in local coordinates to the collider + */ + static Edge(begin: Vector, end: Vector): Edge { + return new Edge({ + begin: begin, + end: end + }); + } +} diff --git a/src/engine/Collision/Side.ts b/src/engine/Collision/Side.ts index 319ccf53..89e9f875 100644 --- a/src/engine/Collision/Side.ts +++ b/src/engine/Collision/Side.ts @@ -2,9 +2,9 @@ * An enum that describes the sides of an Actor for collision */ export enum Side { - None, - Top, - Bottom, - Left, - Right + None = 'None', + Top = 'Top', + Bottom = 'Bottom', + Left = 'Left', + Right = 'Right' } diff --git a/src/engine/Deprecated.ts b/src/engine/Deprecated.ts index 246098df..16dc4c72 100644 --- a/src/engine/Deprecated.ts +++ b/src/engine/Deprecated.ts @@ -2,7 +2,7 @@ import { Actionable } from './Actions/Actionable'; import { Trait } from './Interfaces/Trait'; import { Drawable } from './Interfaces/Drawable'; import { CanInitialize, CanActivate, CanDeactivate, CanUpdate, CanDraw, CanBeKilled } from './Interfaces/LifecycleEvents'; -import { CollisionArea } from './Collision/CollisionArea'; +import { CollisionShape } from './Collision/CollisionShape'; import { Eventable } from './Interfaces/Evented'; import { PointerEvents } from './Interfaces/PointerEvents'; import { CameraStrategy } from './Camera'; @@ -10,7 +10,7 @@ import { Loadable } from './Interfaces/Loadable'; import { Action } from './Actions/Action'; import { ActorArgs, ActorDefaults } from './Actor'; import { CapturePointerConfig } from './Traits/CapturePointer'; -import { Collidable, CircleAreaOptions, CollisionBroadphase, EdgeAreaOptions, EnginePhysics, PolygonAreaOptions } from './Collision/Index'; +import { CircleOptions, CollisionBroadphase, EdgeOptions, EnginePhysics, ConvexPolygonOptions } from './Collision/Index'; import { Physics } from './Physics'; import { DebugFlags } from './DebugFlags'; import { CollidersHash, FrameStatistics, FrameDurationStats, PhysicsStatistics, FrameActorStats } from './Debug'; @@ -33,373 +33,306 @@ import { BorderRadius } from './Util/DrawUtil'; import { Appender } from './Util/Log'; /** - * @deprecated Use ActorsUnderPointer, IActorsUnderPointer will be deprecated in v0.23.0 * @obsolete Use ActorsUnderPointer, IActorsUnderPointer will be deprecated in v0.23.0 */ export type IActorsUnderPointer = ActorsUnderPointer; /** - * @deprecated Use AbsolutePosition, IAbsolutePosition will be deprecated in v0.23.0 * @obsolete Use AbsolutePosition, IAbsolutePosition will be deprecated in v0.23.0 */ export type IAbsolutePosition = AbsolutePosition; /** - * @deprecated Use Action, IAction will be deprecated in v0.23.0 * @obsolete Use Action, IAction will be deprecated in v0.23.0 */ export type IAction = Action; /** - * @deprecated Use Actionable, IActionable will be deprecated in v0.23.0 * @obsolete Use Actionable, IActionable will be deprecated in v0.23.0 */ export type IActionable = Actionable; /** - * @deprecated Use ActorArgs, IActorArgs will be deprecated in v0.23.0 * @obsolete Use ActorArgs, IActorArgs will be deprecated in v0.23.0 */ export type IActorArgs = ActorArgs; /** - * @deprecated Use ActorDefaults, IActorDefaults will be deprecated in v0.23.0 * @obsolete Use ActorDefaults, IActorDefaults will be deprecated in v0.23.0 */ export type IActorDefaults = ActorDefaults; /** - * @deprecated Use Trait, IActorTrait will be removed v0.23.0 * @obsolete Use Trait, IActorTrait will be removed v0.23.0 */ export type IActorTrait = Trait; /** - * @deprecated Use AnimationArgs, IAnimationArgs will be removed v0.23.0 * @obsolete Use AnimationArgs, IAnimationArgs will be removed v0.23.0 */ export type IAnimationArgs = AnimationArgs; /** - * @deprecated Use Appender, IAppender will be removed v0.23.0 * @obsolete Use Appender, IAppender will be removed v0.23.0 */ export type IAppender = Appender; /** - * @deprecated Use Audio, IAudio will be removed v0.23.0 * @obsolete Use Audio, IAudio will be removed v0.23.0 */ export type IAudio = Audio; /** - * @deprecated Use AudioImplementation, IAudioImplementation will be removed v0.23.0 * @obsolete Use AudioImplementation, IAudioImplementation will be removed v0.23.0 */ export type IAudioImplementation = AudioImplementation; /** - * @deprecated Use BorderRadius, IBorderRadius will be removed v0.23.0 * @obsolete Use BorderRadius, IBorderRadius will be removed v0.23.0 */ export type IBorderRadius = BorderRadius; /** - * @deprecated Use CanInitialize, ICanInitialize will be removed v0.23.0 * @obsolete Use CanInitialize, ICanInitialize will be removed v0.23.0 */ export type ICanInitialize = CanInitialize; /** - * @deprecated Use CanActivate, ICanActivate will be removed v0.23.0 * @obsolete Use CanActivate, ICanActivate will be removed v0.23.0 */ export type ICanActivate = CanActivate; /** - * @deprecated Use CanDeactivate, ICanDeactivate will be removed v0.23.0 * @obsolete Use CanDeactivate, ICanDeactivate will be removed v0.23.0 */ export type ICanDeactivate = CanDeactivate; /** - * @deprecated Use CanUpdate, ICanUpdate will be removed in v0.23.0 * @obsolete Use CanUpdate, ICanUpdate will be removed in v0.23.0 */ export type ICanUpdate = CanUpdate; /** - * @deprecated Use CanDraw, ICanDraw will be removed in v0.23.0 * @obsolete Use CanDraw, ICanDraw will be removed in v0.23.0 */ export type ICanDraw = CanDraw; /** - * @deprecated Use CanBeKilled, ICanBeKilled will be removed in v0.23.0 * @obsolete Use CanBeKilled, ICanBeKilled will be removed in v0.23.0 */ export type ICanBeKilled = CanBeKilled; /** - * @deprecated Use CameraStrategy, ICameraStrategy will be removed in v0.23.0 * @obsolete Use CameraStrategy, ICameraStrategy will be removed in v0.23.0 */ export type ICameraStrategy = CameraStrategy; /** - * @deprecated Use CellArgs, ICellArgs will be removed in v0.23.0 * @obsolete Use CellArgs, ICellArgs will be removed in v0.23.0 */ export type ICellArgs = CellArgs; /** - * @deprecated Use Collidable, ICollidable will be removed in v0.23.0 - * @obsolete Use Collidable, ICollidable will be removed in v0.23.0 + * @obsolete Use CollisionShape, ICollisionArea will be removed in v0.23.0 */ -export type ICollidable = Collidable; +export type ICollisionArea = CollisionShape; /** - * @deprecated Use CollisionArea, ICollisionArea will be removed in v0.23.0 - * @obsolete Use CollisionArea, ICollisionArea will be removed in v0.23.0 - */ -export type ICollisionArea = CollisionArea; - -/** - * @deprecated Use DetectedFeatures, IDetectedFeatures will be removed in v0.23.0 * @obsolete Use DetectedFeatures, IDetectedFeatures will be removed in v0.23.0 */ export type IDetectedFeatures = DetectedFeatures; /** - * @deprecated Use ExResponseTypesLookup, IExResponseTypesLookup will be removed in v0.23.0 * @obsolete Use ExResponseTypesLookup, IExResponseTypesLookup will be removed in v0.23.0 */ export type IExResponseTypesLookup = ExResponseTypesLookup; /** - * @deprecated Use Physics, IPhysics will be removed in v0.23.0 * @obsolete Use Physics, IPhysics will be removed in v0.23.0 */ export type IPhysics = Physics; /** - * @deprecated Use DebugFlags, IDebugFlags will be removed in v0.23.0 * @obsolete Use DebugFlags, IDebugFlags will be removed in v0.23.0 */ export type IDebugFlags = DebugFlags; /** - * @deprecated Use CollisionBroadphase, ICollisionBroadphase will be removed in v0.23.0 * @obsolete Use CollisionBroadphase, ICollisionBroadphase will be removed in v0.23.0 */ export type ICollisionBroadphase = CollisionBroadphase; /** - * @deprecated Use CollidersHash, IColliderHash will be removed in v0.23.0 * @obsolete Use CollidersHash, IColliderHash will be removed in v0.23.0 */ export type IColliderHash = CollidersHash; /** - * @deprecated Use CircleAreaOptions, ICircleAreaOptions will be removed in v0.23.0 - * @obsolete Use CircleAreaOptions, ICircleAreaOptions will be removed in v0.23.0 + * @obsolete Use CircleOptions, ICircleAreaOptions will be removed in v0.23.0 */ -export type ICircleAreaOptions = CircleAreaOptions; +export type ICircleAreaOptions = CircleOptions; /** - * @deprecated Use EdgeAreaOptions, IEdgeAreaOptions will be removed in v0.23.0 - * @obsolete Use EdgeAreaOptions, IEdgeAreaOptions will be removed in v0.23.0 + * @obsolete Use EdgeOptions, IEdgeAreaOptions will be removed in v0.23.0 */ -export type IEdgeAreaOptions = EdgeAreaOptions; +export type IEdgeAreaOptions = EdgeOptions; /** - * @deprecated Use PolygonAreaOptions, IPolygonAreaOptions will be removed in v0.23.0 - * @obsolete Use PolygonAreaOptions, IPolygonAreaOptions will be removed in v0.23.0 + * @obsolete Use ConvexPolygonOptions, IPolygonAreaOptions will be removed in v0.23.0 */ -export type IPolygonAreaOptions = PolygonAreaOptions; +export type IPolygonAreaOptions = ConvexPolygonOptions; /** - * @deprecated Use EngineOptions, IEngineOptions will be removed in v0.23.0 * @obsolete Use EngineOptions, IEngineOptions will be removed in v0.23.0 */ export type IEngineOptions = EngineOptions; /** - * @deprecated Use EnginePhysics, IEnginePhysics will be removed in v0.23.0 * @obsolete Use EnginePhysics, IEnginePhysics will be removed in v0.23.0 */ export type IEnginePhysics = EnginePhysics; /** - * @deprecated Use EngineInput, IEngineInput will be removed in v0.23.0 * @obsolete Use EngineInput, IEngineInput will be removed in v0.23.0 */ export type IEngineInput = EngineInput; /** - * @deprecated Use FrameStatistics, IFrameStats will be removed in v0.23.0 * @obsolete Use FrameStatistics, IFrameStats will be removed in v0.23.0 */ export type IFrameStats = FrameStatistics; /** - * @deprecated Use FrameDurationStats, IFrameDurationStats will be removed in v0.23.0 * @obsolete Use FrameDurationStats, IFrameDurationStats will be removed in v0.23.0 */ export type IFrameDurationStats = FrameDurationStats; /** - * @deprecated Use FrameActorStats, IFrameActorStates will be removed in v0.23.0 * @obsolete Use FrameActorStats, IFrameActorStates will be removed in v0.23.0 */ export type IFrameActorStates = FrameActorStats; /** - * @deprecated Use PhysicsStatistics, IPhysicsStats will be removed in v0.23.0 * @obsolete Use PhysicsStatistics, IPhysicsStats will be removed in v0.23.0 */ export type IPhysicsStats = PhysicsStatistics; /** - * @deprecated Use Drawable, IDrawable will be removed v0.23.0 * @obsolete Use Drawable, IDrawable will be removed v0.23.0 */ export type IDrawable = Drawable; /** - * @deprecated Use Eventable, IEvented will be removed in v0.23.0 * @obsolete Use Eventable, IEvented will be removed in v0.23.0 */ export type IEvented = Eventable; /** - * @deprecated Use NavigatorGamepads, INavigatorGamepads will be removed in v0.23.0 * @obsolete Use NavigatorGamepads, INavigatorGamepads will be removed in v0.23.0 */ export type INavigatorGamepads = NavigatorGamepads; /** - * @deprecated Use NavigatorGamepad, INavigatorGamepad will be removed in v0.23.0 * @obsolete Use NavigatorGamepad, INavigatorGamepad will be removed in v0.23.0 */ export type INavigatorGamepad = NavigatorGamepad; /** - * @deprecated Use GamepadConfiguration, IGamepadConfiguration will be removed in v0.23.0 * @obsolete Use GamepadConfiguration, IGamepadConfiguration will be removed in v0.23.0 */ export type IGamepadConfiguration = GamepadConfiguration; /** - * @deprecated Use ObsoleteOptions, IObsoleteOptions will be removed in v0.23.0 * @obsolete Use ObsoleteOptions, IObsoleteOptions will be removed in v0.23.0 */ export type IObsoleteOptions = ObsoleteOptions; /** - * @deprecated Use PointerEvents, IPointerEvents will be removed in v0.23.0 * @obsolete Use PointerEvents, IPointerEvents will be removed in v0.23.0 */ export type IPointerEvents = PointerEvents; /** - * @deprecated Use Loadable, ILoadable will be removed in v0.23.0 * @obsolete Use Loadable, ILoadable will be removed in v0.23.0 */ export type ILoadable = Loadable; /** - * @deprecated Use CanLoad, ILoader will be removed in v0.23.0 * @obsolete Use CanLoad, ILoader will be removed in v0.23.0 */ export type ILoader = CanLoad; /** - * @deprecated Use CapturePointerConfig, ICapturePointerConfig will be removed in v0.23.0 * @obsolete Use CapturePointerConfig, ICapturePointerConfig will be removed in v0.23.0 */ export type ICapturePointerConfig = CapturePointerConfig; /** - * @deprecated Use PromiseLike, IPromise will be removed in v0.23.0 * @obsolete Use PromiseLike, IPromise will be removed in v0.23.0 */ export type IPromise = PromiseLike; /** - * @deprecated Use SpriteEffect, ISpriteEffect will be removed in v0.23.0 * @obsolete Use SpriteEffect, ISpriteEffect will be removed in v0.23.0 */ export type ISpriteEffect = SpriteEffect; /** - * @deprecated Use SpriteArgs, ISpriteArgs will be removed in v0.23.0 * @obsolete Use SpriteArgs, ISpriteArgs will be removed in v0.23.0 */ export type ISpriteArgs = SpriteArgs; /** - * @deprecated Use SpriteFontArgs, ISpriteFontInitArgs will be removed in v0.23.0 * @obsolete Use SpriteFontArgs, ISpriteFontInitArgs will be removed in v0.23.0 */ export type ISpriteFontInitArgs = SpriteFontArgs; /** - * @deprecated Use SpriteFontOptions, ISpriteFrontOptions will be removed in v0.23.0 * @obsolete Use SpriteFontOptions, ISpriteFrontOptions will be removed in v0.23.0 */ export type ISpriteFrontOptions = SpriteFontOptions; /** - * @deprecated Use TileMapArgs, ITileMapArgs will be removed in v0.23.0 * @obsolete Use TileMapArgs, ITileMapArgs will be removed in v0.23.0 */ export type ITileMapArgs = TileMapArgs; /** - * @deprecated Use TouchEvent, ITouchEvent will be removed in v0.23.0 * @obsolete Use TouchEvent, ITouchEvent will be removed in v0.23.0 */ export type ITouchEvent = TouchEvent; /** - * @deprecated Use Touch, ITouch will be removed in v0.23.0 * @obsolete Use Touch, ITouch will be removed in v0.23.0 */ export type ITouch = Touch; /** - * @deprecated Use TriggerOptions, ITriggerOptions will be removed in v0.23.0 * @obsolete Use TriggerOptions, ITriggerOptions will be removed in v0.23.0 */ export type ITriggerOptions = TriggerOptions; /** - * @deprecated Use ParticleArgs, IParticleArgs will be removed in v0.23.0 * @obsolete Use ParticleArgs, IParticleArgs will be removed in v0.23.0 */ export type IParticleArgs = ParticleArgs; /** - * @deprecated Use ParticleEmitterArgs, IParticleEmitterArgs will be removed in v0.23.0 * @obsolete Use ParticleEmitterArgs, IParticleEmitterArgs will be removed in v0.23.0 */ export type IParticleEmitterArgs = ParticleEmitterArgs; /** - * @deprecated Use PerlinOptions, IPerlinGeneratorOptions will be removed in v0.23.0 * @obsolete Use PerlinOptions, IPerlinGeneratorOptions will be removed in v0.23.0 */ export type IPerlinGeneratorOptions = PerlinOptions; /** - * @deprecated Use PostProcessor, IPostProcessor will be removed in v0.23.0 * @obsolete Use PostProcessor, IPostProcessor will be removed in v0.23.0 */ export type IPostProcessor = PostProcessor; /** - * @deprecated Use LabelArgs, ILabelArgs will be removed in v0.23.0 * @obsolete Use LabelArgs, ILabelArgs will be removed in v0.23.0 */ export type ILabelArgs = LabelArgs; diff --git a/src/engine/Docs/Actors.md b/src/engine/Docs/Actors.md index c3aebeb4..a6a65b04 100644 --- a/src/engine/Docs/Actors.md +++ b/src/engine/Docs/Actors.md @@ -215,19 +215,21 @@ By default Actors do not participate in collisions. If you wish to make an actor participate, you need to switch from the default [[CollisionType.PreventCollision|prevent collision]] to [[CollisionType.Active|active]], [[CollisionType.Fixed|fixed]], or [[CollisionType.Passive|passive]] collision type. +For more information on collisions, please read about [[Physics|rigid body physics]]. + ```ts public Player extends ex.Actor { constructor() { super(); // set preferred CollisionType - this.collisionType = ex.CollisionType.Active; + this.body.collider.type = ex.CollisionType.Active; } } // or set the collisionType const actor = new ex.Actor(); -actor.collisionType = ex.CollisionType.Active; +actor.body.collider.type = ex.CollisionType.Active; ``` ## Traits diff --git a/src/engine/Docs/BoxAndPolygonShape.md b/src/engine/Docs/BoxAndPolygonShape.md new file mode 100644 index 00000000..fe479370 --- /dev/null +++ b/src/engine/Docs/BoxAndPolygonShape.md @@ -0,0 +1,37 @@ +## Box and ConvexPolygon Collision Shapes + +Excalibur has a [[shape|Shape]] static helper to create boxes and [[polygons|ConvexPolygon]] for collisions in your game. + +The default shape for a collider is a box, a custom box shape and [[collider|Collider]] can be created for an [[actor|Actor]] [[body|Body]]. The `ex.Shape.Box` helper actually creates a [[ConvexPolygon]] shape in Excalibur. + +```typescript +const block = new ex.Actor({ + pos: new ex.Vector(400, 400), + color: ex.Color.Red, + body: new ex.Body({ + collider: new ex.Collider({ + shape: ex.Shape.Box(50, 50) + type: ex.CollisionType.Active; + }) + }) +}); +``` + +Creating a custom [[convex polygon|ConvexPolygon]] shape is just as simple. Excalibur only supports arbitrary convex shapes as a ConvexPolygon, this means no "cavities" in the shape, for example "pac-man" is not a convex shape. + +The `points` in a [[convex polygon|ConvexPolygon]] have counter-clockwise winding by default, this means the points must be listed in counter-clockwise order around the shape to work. This can be switched by supplying `true` or `false` to the winding argument `ex.Shape.Polygon([...], true)` for clockwise winding. + +**Keep in mind**, points are defined local to the [[body|Body]] or [[actor|Actor]]. Meaning that the triangle defined below is centered around `ex.Vector(400, 400)` in world space. + +```typescript +const triangle = new ex.Actor({ + pos: new ex.Vector(400, 400), + color: ex.Color.Red, + body: new ex.Body({ + collider: new ex.Collider({ + shape: ex.Shape.Polygon([new ex.Vector(0, -100), new ex.Vector(-100, 50), new ex.Vector(100, 50)]) + type: ex.CollisionType.Active; + }) + }) +}); +``` diff --git a/src/engine/Docs/CircleShape.md b/src/engine/Docs/CircleShape.md new file mode 100644 index 00000000..c9cc88b6 --- /dev/null +++ b/src/engine/Docs/CircleShape.md @@ -0,0 +1,20 @@ +## Circle Collision Shape + +Excalibur has a [[shape|Shape]] static helper to create [[circles|Circle]] for collisions in your game. + +The default shape for a collider is a box, however a custom [[circle|Circle]] shape and [[collider|Collider]] can be created for an [[actor|Actor]] [[body|Body]]. + +This example creates a circle of `radius = 50`. + +```typescript +const circle = new ex.Actor({ + pos: new ex.Vector(400, 400), + color: ex.Color.Red, + body: new ex.Body({ + collider: new ex.Collider({ + shape: ex.Shape.Circle(50) + type: ex.CollisionType.Active; + }) + }) +}); +``` diff --git a/src/engine/Docs/Constructors.md b/src/engine/Docs/Constructors.md index dcff660b..658b54c6 100644 --- a/src/engine/Docs/Constructors.md +++ b/src/engine/Docs/Constructors.md @@ -6,7 +6,7 @@ For example instead of doing this: ```typescript const actor = new ex.Actor(1, 2, 100, 100, ex.Color.Red); -actor.collisionType = ex.CollisionType.Active; +actor.body.collider.type = ex.CollisionType.Active; ``` This is possible: @@ -17,10 +17,10 @@ const options: IActorArgs = { width: 100, height: 100, color: ex.Color.Red, - collisionType: ex.CollisionType.Active } const actor = new ex.Actor(options); +actor.body.collider.type = ex.CollisionType.Active; ``` In fact you can create a duplicate this way diff --git a/src/engine/Docs/EdgeShape.md b/src/engine/Docs/EdgeShape.md new file mode 100644 index 00000000..9a837a4e --- /dev/null +++ b/src/engine/Docs/EdgeShape.md @@ -0,0 +1,21 @@ +## Edge Collision Shape + +Excalibur has a [[shape|Shape]] static helper to create [[edges|Edge]] for collisions in your game. + +The default shape for a collider is a box, however a custom [[edge|Edge]] shape and [[collider|Collider]] can be created for an [[actor|Actor]] [[body|Body]]. + +[[Edges|Edge]] are useful for creating walls, barriers, or platforms in your game. + +**Keep in mind**, edges are defined local to the [[body|Body]] or [[actor|Actor]]. Meaning that the edge defined below starts at `ex.Vector(100, 100)` and goes to `ex.Vector(130, 400)` in world space. It is recommended when defining edges to leave the first coordinate `ex.Vector.Zero` to avoid confusion. + +```typescript +const wall = new ex.Actor({ + pos: new ex.Vector(100, 300), + color: ex.Color.Blue, + body: new ex.Body({ + collider: new ex.Collider({ + shape: ex.Shape.Edge(new ex.Vector.Zero(), new ex.Vector(30, 100)) + }) + }) +}); +``` diff --git a/src/engine/Docs/Labels.md b/src/engine/Docs/Labels.md index 9c3950e1..0def6eb2 100644 --- a/src/engine/Docs/Labels.md +++ b/src/engine/Docs/Labels.md @@ -11,8 +11,8 @@ var game = new ex.Engine(); var label = new ex.Label('Hello World', 50, 50, '10px Arial'); // properties var label = new ex.Label(); -label.x = 50; -label.y = 50; +label.pos.x = 50; +label.pos.y = 50; label.fontFamily = 'Arial'; label.fontSize = 10; label.fontUnit = ex.FontUnit.Px; // pixels are the default diff --git a/src/engine/Docs/Physics.md b/src/engine/Docs/Physics.md index 25dc5843..721bc317 100644 --- a/src/engine/Docs/Physics.md +++ b/src/engine/Docs/Physics.md @@ -2,9 +2,71 @@ Excalibur comes built in with two physics systems. The first system is [[Collisi simple axis-aligned way of doing basic collision detection for non-rotated rectangular areas, defined by an actor's [[BoundingBox|bounding box]]. +## Physics hierarchy + +Excalibur physics are organized into a hierarchy, each has a specific single role in the collision system. + +``` +Actor (game entity) + -> Body (transform infomation) + -> Collider (collision related information) + -> Shape (geometry for collision) +``` + +For example: + +```typescript +const actor = new ex.Actor({ + pos: new ex.Vector(100, 100), + body: new ex.Body({ + vel: new ex.Vector(20, 0), // velocity + acc: new ex.Vector(0, 100), // acceleration + collider: new ex.Collider({ + mass: 100, // mass of 100 + type: ex.CollisionType.Active, // active collision type + shape: ex.Shape.Circle(50) // circle geometry of radius 50 + }) + }) +}); +``` + +### Actor/Body + +Actor's have position, velocity, and acceleration in physical space, all of these positional physical attributes are contained inside the [[Body]]. Only 1 actor can be associated with a [[Body]]. + +**[[Body]]** is the container for all transform related information for physics and any associated colliders. + +This looks like this: + +```typescript +const actor = new ex.Actor({ + // actor's position is stored on a default body + pos: new ex.Vector(40, 40); +}); + +// actor position is stored on the body, actor.pos is a convenience +actor.pos === actor.body.pos + +// actor velocity is stored on the body, actor.vel is a convenience +actor.vel === actor.body.vel + +// actor acceleration is stored on the body, actor.acc is a convenience +actor.acc === actor.body.acc +``` + +### Collider + +[[Body]]'s have a default box collider that is derived from the width and height of the [[Actor]] associated. Only 1 [[Collider]] can be associated with a [[Body]], and by extension an [[Actor]]. (Collision events are re-emitted onto [[Actor]]) + +**[[Collider]]** is the container of all collision related information, collision type, collision events, mass, inertia, friction, bounciness, shape, etc. + +### Shape + +[[Collider]]'s have [[CollisionShape]]'s that represent physical geometry. The possible shapes in Excalibur are [[Circle]], [[Edge]], and [[ConvexPolygon]]. A collider can only have 1 [[CollisionShape]] associated with them at a time. + ## Collision Types -Actors have the default collision type of [[CollisionType.PreventCollision]], this is so actors don't accidentally opt into something computationally expensive. **In order for actors to participate in collisions** and the global physics system, actors **must** have a collision type of [[CollisionType.Active]] or [[CollisionType.Fixed]]. +Colliders have the default collision type of [[CollisionType.PreventCollision]], this is so colliders don't accidentally opt into something computationally expensive. **In order for colliders to participate in collisions** and the global physics system, colliders **must** have a collision type of [[CollisionType.Active]] or [[CollisionType.Fixed]]. ### Prevent @@ -52,10 +114,10 @@ This matrix shows what will happen with 2 actors of any collision type. To enable physics in your game it is as simple as setting [[Physics.enabled]] to true and picking your [[CollisionResolutionStrategy]] -Excalibur supports 3 different types of collision area shapes in its physics simulation: [[PolygonArea|polygons]], -[[CircleArea|circles]], and [[EdgeArea|edges]]. To use any one of these areas on an actor there are convenience methods off of -the [[Actor|actor]] [[Body|physics body]]: [[Body.useBoxCollision|useBoxCollision]], -[[Body.usePolygonCollision|usePolygonCollision]], [[Body.useCircleCollision|useCircleCollision]], and [[Body.useEdgeCollision]] +Excalibur supports 3 different types of collision area shapes in its physics simulation: [[ConvexPolygon|polygons]], +[[Circle|circles]], and [[Edge|edges]]. To use any one of these areas on an actor there are convenience methods off of +the [[Actor|actor]] [[Body|physics body]]: [[Body.useBoxCollider|useBoxCollider]], +[[Body.usePolygonCollider|usePolygonCollider]], [[Body.useCircleCollider|useCircleCollider]], and [[Body.useEdgeCollider]] ## Collision Event Lifecycle @@ -69,6 +131,8 @@ Use cases for the **collisionstart** event may be detecting when an actor has to ```typescript actor.on('collisionstart', () => {...}) +// or +actor.body.collider.on('collisionstart', () => {...}) ``` ### Collision End "collisionend" @@ -79,16 +143,20 @@ Use cases for the **collisionend** event might be to detect when an actor has le ```typescript actor.on('collisionend', () => {...}) +// or +actor.body.collider.on('collisionend', () => {...}) ``` ### Pre Collision "precollision" The **precollision** event is fired **every frame** where a collision pair is found and two bodies are intersecting. -This event is useful for building in custom collision resolution logic in Passive-Passive or Active-Passive scenarios. For example in a breakout game you may want to tweak the angle of richochet of the ball depending on which side of the paddle you hit. +This event is useful for building in custom collision resolution logic in Passive-Passive or Active-Passive scenarios. For example in a breakout game you may want to tweak the angle of ricochet of the ball depending on which side of the paddle you hit. ```typescript actor.on('precollision', () => {...}) +// or +actor.body.collider.on('precollision', () => {...}) ``` ### Post Collision "postcollision" @@ -99,6 +167,8 @@ Post collision would be useful if you need to know that collision resolution is ```typescript actor.on('postcollision', () => {...}) +// or +actor.body.collider.on('postcollision', () => {...}) ``` ## Example Active-Active/Active-Fixed scenario @@ -115,37 +185,62 @@ ex.Physics.collisionResolutionStrategy = ex.CollisionResolutionStrategy.RigidBod ex.Physics.acc.setTo(0, 700); const block = new ex.Actor({ - x: 300, - y: 0, + pos: new ex.Vector(300, 0), width: 20, height: 20, - color: ex.Color.Blue.clone(), - collisionType: ex.CollisionType.Active + color: ex.Color.Blue }); -block.body.useBoxCollision(); // useBoxCollision is the default, technically optional +block.body.useBoxCollider(); // useBoxCollision is the default, technically optional +block.body.collider.type = ex.CollisionType.Active; game.add(block); +// or + +const block = new ex.Actor({ + pos: new ex.Vector(300, 0), + color: ex.Color.Blue, + body: new ex.Body({ + collider: new ex.Collider({ + type: ex.CollisionType.Active, + shape: ex.Shape.Box(20, 20) + }) + }) +}); + const circle = new ex.Actor({ x: 301, y: 100, width: 20, height: 20, - color: ex.Color.Red.clone(), - collisionType: ex.CollisionType.Active + color: ex.Color.Red }); -circle.body.useCircleCollision(10); +circle.body.useCircleCollider(10); +circle.body.collider.type = ex.CollisionType.Active; game.add(circle); +// or + +const circle = new ex.Actor({ + pos: new ex.Vector(301, 100), + color: ex.Color.Red, + body: new ex.Body({ + collider: new ex.Collider({ + shape: ex.Shape.Circle(10), + type: ex.CollisionType.Active + }) + }) +}); + const ground = new ex.Actor({ x: 300, y: 380, width: 600, height: 10, - color: ex.Color.Black.clone(), - collisionType: ex.CollisionType.Fixed + color: ex.Color.Black; }); -ground.body.useBoxCollision(); // optional +ground.body.useBoxCollider(); // optional +groundbody.collider.type = ex.CollisionType.Fixed; game.add(ground); // start the game diff --git a/src/engine/Docs/Triggers.md b/src/engine/Docs/Triggers.md index f903a734..af099a7a 100644 --- a/src/engine/Docs/Triggers.md +++ b/src/engine/Docs/Triggers.md @@ -27,7 +27,7 @@ var trigger = new ex.Trigger({ var actor = new ex.Actor(100, 0, 40, 40, ex.Color.Red); // Enable collision on actor (else trigger won't fire) -actor.collisionType = ex.CollisionType.Active; +actor.body.collider.type = ex.CollisionType.Active; // tell the actor to move across the trigger with a velocity of 100 actor.actions.moveTo(100, 200, 100); diff --git a/src/engine/EventDispatcher.ts b/src/engine/EventDispatcher.ts index 12415727..23fd9d2e 100644 --- a/src/engine/EventDispatcher.ts +++ b/src/engine/EventDispatcher.ts @@ -10,25 +10,33 @@ import { Eventable } from './Interfaces/Evented'; * * [[include:Events.md]] */ -export class EventDispatcher implements Eventable { - private _handlers: { [key: string]: { (event: GameEvent): void }[] } = {}; - private _wiredEventDispatchers: EventDispatcher[] = []; +export class EventDispatcher implements Eventable { + private _handlers: { [key: string]: { (event: GameEvent): void }[] } = {}; + private _wiredEventDispatchers: Eventable[] = []; - private _target: any; + private _target: T; /** * @param target The object that will be the recipient of events from this event dispatcher */ - constructor(target: any) { + constructor(target: T) { this._target = target; } + /** + * Clears any existing handlers or wired event dispatchers on this event dispatcher + */ + public clear() { + this._handlers = {}; + this._wiredEventDispatchers = []; + } + /** * Emits an event for target * @param eventName The name of the event to publish * @param event Optionally pass an event data object to the handler */ - public emit(eventName: string, event: GameEvent) { + public emit(eventName: string, event: GameEvent) { if (!eventName) { // key not mapped return; @@ -64,7 +72,7 @@ export class EventDispatcher implements Eventable { * @param eventName The name of the event to subscribe to * @param handler The handler callback to fire on this event */ - public on(eventName: string, handler: (event: GameEvent) => void) { + public on(eventName: string, handler: (event: GameEvent) => void) { eventName = eventName.toLowerCase(); if (!this._handlers[eventName]) { this._handlers[eventName] = []; @@ -86,7 +94,7 @@ export class EventDispatcher implements Eventable { * @param handler Optionally the specific handler to unsubscribe * */ - public off(eventName: string, handler?: (event: GameEvent) => void) { + public off(eventName: string, handler?: (event: GameEvent) => void) { eventName = eventName.toLowerCase(); const eventHandlers = this._handlers[eventName]; @@ -111,8 +119,8 @@ export class EventDispatcher implements Eventable { * @param eventName The name of the event to subscribe to once * @param handler The handler of the event that will be auto unsubscribed */ - public once(eventName: string, handler: (event: GameEvent) => void) { - const metaHandler = (event: GameEvent) => { + public once(eventName: string, handler: (event: GameEvent) => void) { + const metaHandler = (event: GameEvent) => { const ev = event || new GameEvent(); ev.target = ev.target || this._target; diff --git a/src/engine/Events.ts b/src/engine/Events.ts index 4c02e9c5..75f126ae 100644 --- a/src/engine/Events.ts +++ b/src/engine/Events.ts @@ -8,6 +8,7 @@ import { TileMap } from './TileMap'; import { Side } from './Collision/Side'; import * as Input from './Input/Index'; import { Pair, Camera } from './index'; +import { Collider } from './Collision/Collider'; export enum EventTypes { Kill = 'kill', @@ -158,11 +159,17 @@ export type pointerdragmove = 'pointerdragmove'; * some events are unique to a type, others are not. * */ -export class GameEvent { +export class GameEvent { /** * Target object for this event. */ public target: T; + + /** + * Other target object for this event + */ + public other: U | null; + /** * determines, if event bubbles to the target's ancestors */ @@ -443,59 +450,94 @@ export class HiddenEvent extends GameEvent { /** * Event thrown on an [[Actor|actor]] when a collision will occur this frame if it resolves */ -export class PreCollisionEvent extends GameEvent { +export class PreCollisionEvent extends GameEvent { /** * @param actor The actor the event was thrown on * @param other The actor that will collided with the current actor * @param side The side that will be collided with the current actor * @param intersection Intersection vector */ - constructor(public actor: Actor, public other: Actor, public side: Side, public intersection: Vector) { + constructor(actor: T, public other: T, public side: Side, public intersection: Vector) { super(); this.target = actor; } + + public get actor() { + return this.target; + } + + public set actor(actor: T) { + this.target = actor; + } } /** * Event thrown on an [[Actor|actor]] when a collision has been resolved (body reacted) this frame */ -export class PostCollisionEvent extends GameEvent { +export class PostCollisionEvent extends GameEvent { /** * @param actor The actor the event was thrown on * @param other The actor that did collide with the current actor * @param side The side that did collide with the current actor * @param intersection Intersection vector */ - constructor(public actor: Actor, public other: Actor, public side: Side, public intersection: Vector) { + constructor(actor: T, public other: T, public side: Side, public intersection: Vector) { super(); this.target = actor; } + + public get actor() { + return this.target; + } + + public set actor(actor: T) { + this.target = actor; + } } /** * Event thrown the first time an [[Actor|actor]] collides with another, after an actor is in contact normal collision events are fired. */ -export class CollisionStartEvent extends GameEvent { +export class CollisionStartEvent extends GameEvent { /** * + * @param actor + * @param other + * @param pair */ - constructor(public actor: Actor, public other: Actor, public pair: Pair) { + constructor(actor: T, public other: T, public pair: Pair) { super(); this.target = actor; } + + public get actor() { + return this.target; + } + + public set actor(actor: T) { + this.target = actor; + } } /** * Event thrown when the [[Actor|actor]] is no longer colliding with another */ -export class CollisionEndEvent extends GameEvent { +export class CollisionEndEvent extends GameEvent { /** * */ - constructor(public actor: Actor, public other: Actor) { + constructor(actor: T, public other: T) { super(); this.target = actor; } + + public get actor() { + return this.target; + } + + public set actor(actor: T) { + this.target = actor; + } } /** diff --git a/src/engine/Interfaces/Clonable.ts b/src/engine/Interfaces/Clonable.ts new file mode 100644 index 00000000..88592030 --- /dev/null +++ b/src/engine/Interfaces/Clonable.ts @@ -0,0 +1,3 @@ +export interface Clonable { + clone(): T; +} diff --git a/src/engine/Label.ts b/src/engine/Label.ts index d8adcd91..fe984f38 100644 --- a/src/engine/Label.ts +++ b/src/engine/Label.ts @@ -1,9 +1,10 @@ import { Engine } from './Engine'; import { Color } from './Drawing/Color'; import { SpriteFont } from './Drawing/SpriteSheet'; -import { Actor, CollisionType } from './Actor'; +import { Actor } from './Actor'; import { Configurable } from './Configurable'; import { Vector } from './Algebra'; +import { CollisionType } from './Collision/CollisionType'; /** * Enum representing the different font size units * https://developer.mozilla.org/en-US/docs/Web/CSS/font-size @@ -219,7 +220,7 @@ export class LabelImpl extends Actor { this.text = text || ''; this.color = Color.Black; this.spriteFont = spriteFont; - this.collisionType = CollisionType.PreventCollision; + this.body.collider.type = CollisionType.PreventCollision; this.fontFamily = fontFamily || 'sans-serif'; // coalesce to default canvas font this._textShadowOn = false; diff --git a/src/engine/Particles.ts b/src/engine/Particles.ts index 59d23f64..1858f6b2 100644 --- a/src/engine/Particles.ts +++ b/src/engine/Particles.ts @@ -1,5 +1,5 @@ import { Engine } from './Engine'; -import { Actor, CollisionType } from './Actor'; +import { Actor } from './Actor'; import { Sprite } from './Drawing/Sprite'; import { Color } from './Drawing/Color'; import { Vector } from './Algebra'; @@ -8,6 +8,7 @@ import * as DrawUtil from './Util/DrawUtil'; import * as Traits from './Traits/Index'; import { Configurable } from './Configurable'; import { Random } from './Math/Random'; +import { CollisionType } from './Collision/CollisionType'; /** * An enum that represents the types of emitter nozzles @@ -355,7 +356,7 @@ export class ParticleEmitterImpl extends Actor { constructor(xOrConfig?: number | ParticleEmitterArgs, y?: number, width?: number, height?: number) { super(typeof xOrConfig === 'number' ? { x: xOrConfig, y: y, width: width, height: height } : xOrConfig); this._particlesToEmit = 0; - this.collisionType = CollisionType.PreventCollision; + this.body.collider.type = CollisionType.PreventCollision; this.particles = new Util.Collection(); this.deadParticles = new Util.Collection(); this.random = new Random(); @@ -399,8 +400,8 @@ export class ParticleEmitterImpl extends Actor { const dy = vel * Math.sin(angle); if (this.emitterType === EmitterType.Rectangle) { - ranX = Util.randomInRange(this.pos.x, this.pos.x + this.getWidth(), this.random); - ranY = Util.randomInRange(this.pos.y, this.pos.y + this.getHeight(), this.random); + ranX = Util.randomInRange(this.pos.x, this.pos.x + this.width, this.random); + ranY = Util.randomInRange(this.pos.y, this.pos.y + this.height, this.random); } else if (this.emitterType === EmitterType.Circle) { const radius = Util.randomInRange(0, this.radius, this.random); ranX = radius * Math.cos(angle) + this.pos.x; @@ -465,7 +466,7 @@ export class ParticleEmitterImpl extends Actor { if (this.focus) { ctx.fillRect(this.focus.x + this.pos.x, this.focus.y + this.pos.y, 3, 3); - DrawUtil.line(ctx, Color.Yellow, this.focus.x + this.pos.x, this.focus.y + this.pos.y, super.getCenter().x, super.getCenter().y); + DrawUtil.line(ctx, Color.Yellow, this.focus.x + this.pos.x, this.focus.y + this.pos.y, this.center.x, this.center.y); ctx.fillText('Focus', this.focus.x + this.pos.x, this.focus.y + this.pos.y); } } diff --git a/src/engine/Scene.ts b/src/engine/Scene.ts index 9445835c..7b3d134d 100644 --- a/src/engine/Scene.ts +++ b/src/engine/Scene.ts @@ -28,6 +28,7 @@ import * as Util from './Util/Util'; import * as Events from './Events'; import * as ActorUtils from './Util/Actors'; import { Trigger } from './Trigger'; +import { Body } from './Collision/Body'; /** * [[Actor|Actors]] are composed together into groupings called Scenes in * Excalibur. The metaphor models the same idea behind real world @@ -48,6 +49,11 @@ export class Scene extends Class implements CanInitialize, CanActivate, CanDeact */ public actors: Actor[] = []; + /** + * Physics bodies in the current scene + */ + private _bodies: Body[] = []; + /** * The triggers in the current scene */ @@ -342,6 +348,7 @@ export class Scene extends Class implements CanInitialize, CanActivate, CanDeact // Cycle through actors updating actors for (i = 0, len = this.actors.length; i < len; i++) { this.actors[i].update(engine, delta); + this._bodies[i] = this.actors[i].body; } // Cycle through triggers updating @@ -357,8 +364,8 @@ export class Scene extends Class implements CanInitialize, CanActivate, CanDeact // Run the broadphase and narrowphase if (this._broadphase && Physics.enabled) { const beforeBroadphase = Date.now(); - this._broadphase.update(this.actors, delta); - let pairs = this._broadphase.broadphase(this.actors, delta, engine.stats.currFrame); + this._broadphase.update(this._bodies, delta); + let pairs = this._broadphase.broadphase(this._bodies, delta, engine.stats.currFrame); const afterBroadphase = Date.now(); const beforeNarrowphase = Date.now(); diff --git a/src/engine/TileMap.ts b/src/engine/TileMap.ts index 5202c4aa..53159d11 100644 --- a/src/engine/TileMap.ts +++ b/src/engine/TileMap.ts @@ -8,6 +8,7 @@ import { Logger } from './Util/Log'; import { SpriteSheet } from './Drawing/SpriteSheet'; import * as Events from './Events'; import { Configurable } from './Configurable'; +import { obsolete } from './Util/Decorators'; /** * @hidden @@ -82,17 +83,17 @@ export class TileMapImpl extends Class { * is no collision null is returned. */ public collides(actor: Actor): Vector { - const width = actor.pos.x + actor.getWidth(); - const height = actor.pos.y + actor.getHeight(); - const actorBounds = actor.getBounds(); + const width = actor.pos.x + actor.width; + const height = actor.pos.y + actor.height; + const actorBounds = actor.body.collider.bounds; const overlaps: Vector[] = []; // trace points for overlap - for (let x = actorBounds.left; x <= width; x += Math.min(actor.getWidth() / 2, this.cellWidth / 2)) { - for (let y = actorBounds.top; y <= height; y += Math.min(actor.getHeight() / 2, this.cellHeight / 2)) { + for (let x = actorBounds.left; x <= width; x += Math.min(actor.width / 2, this.cellWidth / 2)) { + for (let y = actorBounds.top; y <= height; y += Math.min(actor.height / 2, this.cellHeight / 2)) { const cell = this.getCellByPoint(x, y); if (cell && cell.solid) { - const overlap = actorBounds.collides(cell.getBounds()); - const dir = actor.getCenter().sub(cell.getCenter()); + const overlap = actorBounds.intersect(cell.bounds); + const dir = actor.center.sub(cell.center); if (overlap && overlap.dot(dir) > 0) { overlaps.push(overlap); } @@ -343,15 +344,27 @@ export class CellImpl { /** * Returns the bounding box for this cell */ + @obsolete({ message: 'Will be removed in v0.24.0', alternateMethod: 'BoundingBox.bounds' }) public getBounds() { return this._bounds; } + + public get bounds() { + return this._bounds; + } + /** * Gets the center coordinate of this cell */ + @obsolete({ message: 'Will be removed in v0.24.0', alternateMethod: 'BoundingBox.center' }) public getCenter(): Vector { return new Vector(this.x + this.width / 2, this.y + this.height / 2); } + + public get center(): Vector { + return new Vector(this.x + this.width / 2, this.y + this.height / 2); + } + /** * Add another [[TileSprite]] to this cell */ diff --git a/src/engine/Traits/EulerMovement.ts b/src/engine/Traits/EulerMovement.ts deleted file mode 100644 index eb33c9d6..00000000 --- a/src/engine/Traits/EulerMovement.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Physics } from './../Physics'; -import { Trait } from '../Interfaces/Trait'; -import { Actor, CollisionType } from '../Actor'; -import { Engine } from '../Engine'; - -export class EulerMovement implements Trait { - public update(actor: Actor, _engine: Engine, delta: number) { - // Update placements based on linear algebra - const seconds = delta / 1000; - - const totalAcc = actor.acc.clone(); - // Only active vanilla actors are affected by global acceleration - if (actor.collisionType === CollisionType.Active) { - totalAcc.addEqual(Physics.acc); - } - - actor.oldVel = actor.vel; - actor.vel.addEqual(totalAcc.scale(seconds)); - - actor.pos.addEqual(actor.vel.scale(seconds)).addEqual(totalAcc.scale(0.5 * seconds * seconds)); - - actor.rx += actor.torque * (1.0 / actor.moi) * seconds; - actor.rotation += actor.rx * seconds; - - actor.scale.x += (actor.sx * delta) / 1000; - actor.scale.y += (actor.sy * delta) / 1000; - } -} diff --git a/src/engine/Traits/Index.ts b/src/engine/Traits/Index.ts index 07a7f3ff..34d0d223 100644 --- a/src/engine/Traits/Index.ts +++ b/src/engine/Traits/Index.ts @@ -1,4 +1,3 @@ export * from './CapturePointer'; -export * from './EulerMovement'; export * from './OffscreenCulling'; export * from './TileMapCollisionDetection'; diff --git a/src/engine/Traits/OffscreenCulling.ts b/src/engine/Traits/OffscreenCulling.ts index 3948955b..0482a68a 100644 --- a/src/engine/Traits/OffscreenCulling.ts +++ b/src/engine/Traits/OffscreenCulling.ts @@ -17,7 +17,7 @@ export class OffscreenCulling implements Trait { let actorBoundsOffscreen = false; if (engine && engine.currentScene && engine.currentScene.camera && engine.currentScene.camera.viewport) { - actorBoundsOffscreen = !engine.currentScene.camera.viewport.collides(actor.getBounds(true)); + actorBoundsOffscreen = !engine.currentScene.camera.viewport.intersect(actor.body.collider.bounds); } if (!actor.isOffScreen) { diff --git a/src/engine/Traits/TileMapCollisionDetection.ts b/src/engine/Traits/TileMapCollisionDetection.ts index af099f28..6d836cab 100644 --- a/src/engine/Traits/TileMapCollisionDetection.ts +++ b/src/engine/Traits/TileMapCollisionDetection.ts @@ -1,14 +1,16 @@ import { Trait } from '../Interfaces/Trait'; -import { Actor, CollisionType } from '../Actor'; +import { Actor } from '../Actor'; import { Engine } from '../Engine'; import { Vector } from '../Algebra'; import { Side } from '../Collision/Side'; import { PreCollisionEvent, PostCollisionEvent } from '../Events'; +import { CollisionType } from '../Collision/CollisionType'; +import { BoundingBox } from '../Collision/Index'; export class TileMapCollisionDetection implements Trait { public update(actor: Actor, engine: Engine) { const eventDispatcher = actor.eventDispatcher; - if (actor.collisionType !== CollisionType.PreventCollision && engine.currentScene && engine.currentScene.tileMaps) { + if (actor.body.collider.type !== CollisionType.PreventCollision && engine.currentScene && engine.currentScene.tileMaps) { for (let j = 0; j < engine.currentScene.tileMaps.length; j++) { const map = engine.currentScene.tileMaps[j]; let intersectMap: Vector; @@ -18,9 +20,9 @@ export class TileMapCollisionDetection implements Trait { if (max-- < 0) { break; } - side = actor.getSideFromIntersect(intersectMap); + side = BoundingBox.getSideFromIntersection(intersectMap); eventDispatcher.emit('precollision', new PreCollisionEvent(actor, null, side, intersectMap)); - if (actor.collisionType === CollisionType.Active) { + if (actor.body.collider.type === CollisionType.Active) { actor.pos.y += intersectMap.y; actor.pos.x += intersectMap.x; eventDispatcher.emit('postcollision', new PostCollisionEvent(actor, null, side, intersectMap)); diff --git a/src/engine/Trigger.ts b/src/engine/Trigger.ts index a3435396..0edaf96f 100644 --- a/src/engine/Trigger.ts +++ b/src/engine/Trigger.ts @@ -2,10 +2,11 @@ import { Color } from './Drawing/Color'; import { Engine } from './Engine'; import { ActionQueue } from './Actions/Action'; import { EventDispatcher } from './EventDispatcher'; -import { Actor, CollisionType } from './Actor'; +import { Actor, isActor } from './Actor'; import { Vector } from './Algebra'; import { ExitTriggerEvent, EnterTriggerEvent, CollisionEndEvent, CollisionStartEvent } from './Events'; import * as Util from './Util/Util'; +import { CollisionType } from './Collision/CollisionType'; /** * ITriggerOptions @@ -82,12 +83,12 @@ export class Trigger extends Actor { } this.visible = opts.visible; - this.collisionType = CollisionType.Passive; + this.body.collider.type = CollisionType.Passive; this.eventDispatcher = new EventDispatcher(this); this.actionQueue = new ActionQueue(this); - this.on('collisionstart', (evt: CollisionStartEvent) => { - if (this.filter(evt.other)) { + this.on('collisionstart', (evt: CollisionStartEvent) => { + if (isActor(evt.other) && this.filter(evt.other)) { this.emit('enter', new EnterTriggerEvent(this, evt.other)); this._dispatchAction(); // remove trigger if its done, -1 repeat forever @@ -97,8 +98,8 @@ export class Trigger extends Actor { } }); - this.on('collisionend', (evt: CollisionEndEvent) => { - if (this.filter(evt.other)) { + this.on('collisionend', (evt: CollisionEndEvent) => { + if (isActor(evt.other) && this.filter(evt.other)) { this.emit('exit', new ExitTriggerEvent(this, evt.other)); } }); @@ -129,7 +130,7 @@ export class Trigger extends Actor { ctx.save(); ctx.translate(this.pos.x, this.pos.y); - const bb = this.getBounds(); + const bb = this.body.collider.bounds; const wp = this.getWorldPos(); bb.left = bb.left - wp.x; bb.right = bb.right - wp.x; diff --git a/src/engine/UIActor.ts b/src/engine/UIActor.ts index 73dc77ba..ca0efb50 100644 --- a/src/engine/UIActor.ts +++ b/src/engine/UIActor.ts @@ -1,7 +1,9 @@ import { Vector } from './Algebra'; import { Engine } from './Engine'; -import { Actor, ActorArgs, CollisionType } from './Actor'; +import { Actor, ActorArgs } from './Actor'; import * as Traits from './Traits/Index'; +import { CollisionType } from './Collision/CollisionType'; +import { Shape } from './Collision/Shape'; /** * Helper [[Actor]] primitive for drawing UI's, optimized for UI drawing. Does @@ -28,7 +30,8 @@ export class UIActor extends Actor { this.traits = []; this.traits.push(new Traits.CapturePointer()); this.anchor.setTo(0, 0); - this.collisionType = CollisionType.PreventCollision; + this.body.collider.type = CollisionType.PreventCollision; + this.body.collider.shape = Shape.Box(this.width, this.height, this.anchor); this.enableCapturePointer = true; } diff --git a/src/engine/Util/CullingBox.ts b/src/engine/Util/CullingBox.ts index 0604f17c..b73eb3e5 100644 --- a/src/engine/Util/CullingBox.ts +++ b/src/engine/Util/CullingBox.ts @@ -25,7 +25,7 @@ export class CullingBox { const drawingWidth = actor.currentDrawing.drawWidth; const drawingHeight = actor.currentDrawing.drawHeight; const rotation = actor.rotation; - const anchor = actor.getCenter(); + const anchor = actor.center; const worldPos = actor.getWorldPos(); this._topLeft.x = worldPos.x - drawingWidth / 2; diff --git a/src/engine/Util/Decorators.ts b/src/engine/Util/Decorators.ts index a1e98dd6..b4ba9bcf 100644 --- a/src/engine/Util/Decorators.ts +++ b/src/engine/Util/Decorators.ts @@ -37,6 +37,11 @@ export function obsolete(options?: ObsoleteOptions): any { const constructor = function() { const args = Array.prototype.slice.call(arguments); Logger.getInstance().warn(message); + // tslint:disable-next-line: no-console + if (console.trace) { + // tslint:disable-next-line: no-console + console.trace(); + } return new method(...args); }; constructor.prototype = method.prototype; @@ -46,6 +51,11 @@ export function obsolete(options?: ObsoleteOptions): any { if (descriptor && descriptor.value) { method.value = function(this: any) { Logger.getInstance().warn(message); + // tslint:disable-next-line: no-console + if (console.trace) { + // tslint:disable-next-line: no-console + console.trace(); + } return descriptor.value.apply(this, arguments); }; return method; @@ -54,6 +64,11 @@ export function obsolete(options?: ObsoleteOptions): any { if (descriptor && descriptor.get) { method.get = function(this: any) { Logger.getInstance().warn(message); + // tslint:disable-next-line: no-console + if (console.trace) { + // tslint:disable-next-line: no-console + console.trace(); + } return descriptor.get.apply(this, arguments); }; } diff --git a/src/engine/Util/Util.ts b/src/engine/Util/Util.ts index 57d48d27..ec38a209 100644 --- a/src/engine/Util/Util.ts +++ b/src/engine/Util/Util.ts @@ -236,7 +236,18 @@ export function getOppositeSide(side: Side) { return Side.None; } +/** + * @obsolete use Util.getSideFromDirection + */ export function getSideFromVector(direction: Vector) { + return getSideFromDirection(direction); +} + +/** + * Returns the side in the direction of the vector supplied + * @param direction Vector to check + */ +export function getSideFromDirection(direction: Vector) { const directions = [Vector.Left, Vector.Right, Vector.Up, Vector.Down]; const directionEnum = [Side.Left, Side.Right, Side.Top, Side.Bottom]; diff --git a/src/engine/index.ts b/src/engine/index.ts index 4abc7d3a..3e4ba627 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -10,7 +10,8 @@ polyfill(); // that will be exposed as the `ex` global variable. export * from './Engine'; -export { Actor, ActorArgs as IActorArgs, CollisionType } from './Actor'; +export { Actor, ActorArgs as IActorArgs } from './Actor'; +export { CollisionType } from './Collision/CollisionType'; export * from './Algebra'; export * from './Camera'; export * from './Class'; diff --git a/src/spec/ActorSpec.ts b/src/spec/ActorSpec.ts index 69f6f66d..6e04904b 100644 --- a/src/spec/ActorSpec.ts +++ b/src/spec/ActorSpec.ts @@ -14,7 +14,7 @@ describe('A game actor', () => { jasmine.addMatchers(ExcaliburMatchers); engine = TestUtils.engine({ width: 100, height: 100 }); actor = new ex.Actor(); - actor.collisionType = ex.CollisionType.Active; + actor.body.collider.type = ex.CollisionType.Active; scene = new ex.Scene(engine); engine.currentScene = scene; @@ -55,21 +55,18 @@ describe('A game actor', () => { rotation: 2, rx: 0.1, z: 10, - restitution: 2, color: ex.Color.Red, - visible: false, - collisionType: ex.CollisionType.Fixed + visible: false }); const actor2 = new ex.Actor({ - x: 4, - y: 5 + pos: new ex.Vector(4, 5) }); - expect(actor.x).toBe(2); - expect(actor.y).toBe(3); - expect(actor.getWidth()).toBe(100); - expect(actor.getHeight()).toBe(200); + expect(actor.pos.x).toBe(2); + expect(actor.pos.y).toBe(3); + expect(actor.width).toBe(100); + expect(actor.height).toBe(200); expect(actor.vel.x).toBe(30); expect(actor.vel.y).toBe(40); expect(actor.acc.x).toBe(50); @@ -79,10 +76,8 @@ describe('A game actor', () => { expect(actor.z).toBe(10); expect(actor.color.toString()).toBe(ex.Color.Red.toString()); expect(actor.visible).toBe(false); - expect(actor.restitution).toBe(2); - expect(actor.collisionType).toBe(ex.CollisionType.Fixed); - expect(actor2.x).toBe(4); - expect(actor2.y).toBe(5); + expect(actor2.pos.x).toBe(4); + expect(actor2.pos.y).toBe(5); }); it('should have default properties set', () => { @@ -122,8 +117,8 @@ describe('A game actor', () => { const actor2 = new ex.Actor(); actor2.id = 40; - const hash = ex.Pair.calculatePairHash(actor.body, actor2.body); - const hash2 = ex.Pair.calculatePairHash(actor2.body, actor.body); + const hash = ex.Pair.calculatePairHash(actor.body.collider, actor2.body.collider); + const hash2 = ex.Pair.calculatePairHash(actor2.body.collider, actor.body.collider); expect(hash).toBe('#20+40'); expect(hash2).toBe('#20+40'); }); @@ -160,26 +155,26 @@ describe('A game actor', () => { }); it('can have its height and width scaled', () => { - expect(actor.getWidth()).toBe(0); - expect(actor.getHeight()).toBe(0); + expect(actor.width).toBe(0); + expect(actor.height).toBe(0); - actor.setWidth(20); - actor.setHeight(20); + actor.width = 20; + actor.height = 20; - expect(actor.getWidth()).toBe(20); - expect(actor.getHeight()).toBe(20); + expect(actor.width).toBe(20); + expect(actor.height).toBe(20); actor.scale.x = 2; actor.scale.y = 3; - expect(actor.getWidth()).toBe(40); - expect(actor.getHeight()).toBe(60); + expect(actor.width).toBe(40); + expect(actor.height).toBe(60); actor.scale.x = 0.5; actor.scale.y = 0.1; - expect(actor.getWidth()).toBe(10); - expect(actor.getHeight()).toBe(2); + expect(actor.width).toBe(10); + expect(actor.height).toBe(2); }); it('can have its height and width scaled by parent', () => { @@ -189,27 +184,27 @@ describe('A game actor', () => { actor.add(child); - expect(child.getWidth()).toBe(100); - expect(child.getHeight()).toBe(100); + expect(child.width).toBe(100); + expect(child.height).toBe(100); actor.scale.setTo(0.5, 0.5); - expect(child.getWidth()).toBe(25); - expect(child.getHeight()).toBe(25); + expect(child.width).toBe(25); + expect(child.height).toBe(25); }); it('can have a center point', () => { - actor.setHeight(100); - actor.setWidth(50); + actor.height = 100; + actor.width = 50; - let center = actor.getCenter(); + let center = actor.center; expect(center.x).toBe(0); expect(center.y).toBe(0); actor.pos.x = 100; actor.pos.y = 100; - center = actor.getCenter(); + center = actor.center; expect(center.x).toBe(100); expect(center.y).toBe(100); @@ -218,14 +213,14 @@ describe('A game actor', () => { actor.pos.x = 0; actor.pos.y = 0; - center = actor.getCenter(); + center = actor.center; expect(center.x).toBe(25); expect(center.y).toBe(50); actor.pos.x = 100; actor.pos.y = 100; - center = actor.getCenter(); + center = actor.center; expect(center.x).toBe(125); expect(center.y).toBe(150); }); @@ -234,55 +229,58 @@ describe('A game actor', () => { actor.pos.x = 0; actor.pos.y = 0; actor.anchor = new ex.Vector(0.5, 0.5); - actor.setWidth(100); - actor.setHeight(100); + actor.width = 100; + actor.height = 100; - expect(actor.getLeft()).toBe(-50); - expect(actor.getRight()).toBe(50); - expect(actor.getTop()).toBe(-50); - expect(actor.getBottom()).toBe(50); + expect(actor.body.collider.bounds.left).toBe(-50); + expect(actor.body.collider.bounds.right).toBe(50); + expect(actor.body.collider.bounds.top).toBe(-50); + expect(actor.body.collider.bounds.bottom).toBe(50); }); it('should have correct bounds when scaled', () => { actor.pos.x = 0; actor.pos.y = 0; - actor.setWidth(100); - actor.setHeight(100); + actor.width = 100; + actor.height = 100; actor.scale.setTo(2, 2); actor.anchor = new ex.Vector(0.5, 0.5); - expect(actor.getLeft()).toBe(-100); - expect(actor.getRight()).toBe(100); - expect(actor.getTop()).toBe(-100); - expect(actor.getBottom()).toBe(100); + actor.body.collider.shape.recalc(); + + expect(actor.body.collider.bounds.left).toBe(-100); + expect(actor.body.collider.bounds.right).toBe(100); + expect(actor.body.collider.bounds.top).toBe(-100); + expect(actor.body.collider.bounds.bottom).toBe(100); }); - it('should have correct bounds when parent is scaled', () => { + // @obsolete? colliders don't know anything about child actors + xit('should have correct bounds when parent is scaled', () => { actor.pos.x = 0; actor.pos.y = 0; - actor.setWidth(100); - actor.setHeight(100); + actor.width = 100; + actor.height = 100; actor.scale.setTo(2, 2); actor.anchor = new ex.Vector(0.5, 0.5); const child = new ex.Actor(0, 0, 50, 50); actor.add(child); - expect(child.getLeft()).toBe(-50); - expect(child.getRight()).toBe(50); - expect(child.getTop()).toBe(-50); - expect(child.getBottom()).toBe(50); + expect(child.body.collider.bounds.left).toBe(-50); + expect(child.body.collider.bounds.right).toBe(50); + expect(child.body.collider.bounds.top).toBe(-50); + expect(child.body.collider.bounds.bottom).toBe(50); }); - it('should have the correct bounds when scaled and rotated', () => { + xit('should have the correct bounds when scaled and rotated', () => { const actor = new ex.Actor(50, 50, 10, 10); // actor is now 20 high actor.scale.setTo(1, 2); // rotating the actor 90 degrees should make the actor 20 wide actor.rotation = Math.PI / 2; - const bounds = actor.getBounds(); - expect(bounds.getWidth()).toBeCloseTo(20, 0.001); - expect(bounds.getHeight()).toBeCloseTo(10, 0.001); + const bounds = actor.body.collider.bounds; + expect(bounds.width).toBeCloseTo(20, 0.001); + expect(bounds.height).toBeCloseTo(10, 0.001); expect(bounds.left).toBeCloseTo(40, 0.001); expect(bounds.right).toBeCloseTo(60, 0.001); @@ -290,15 +288,15 @@ describe('A game actor', () => { expect(bounds.bottom).toBeCloseTo(55, 0.001); }); - it('should have the correct relative bounds when scaled and rotated', () => { + xit('should have the correct relative bounds when scaled and rotated', () => { const actor = new ex.Actor(50, 50, 10, 10); // actor is now 20 high actor.scale.setTo(1, 2); // rotating the actor 90 degrees should make the actor 20 wide actor.rotation = Math.PI / 2; - const bounds = actor.getRelativeBounds(); - expect(bounds.getWidth()).toBeCloseTo(20, 0.001); - expect(bounds.getHeight()).toBeCloseTo(10, 0.001); + const bounds = actor.body.collider.localBounds; + expect(bounds.width).toBeCloseTo(20, 0.001); + expect(bounds.height).toBeCloseTo(10, 0.001); expect(bounds.left).toBeCloseTo(-10, 0.001); expect(bounds.right).toBeCloseTo(10, 0.001); @@ -306,20 +304,20 @@ describe('A game actor', () => { expect(bounds.bottom).toBeCloseTo(5, 0.001); }); - it('has a left, right, top, and bottom when the anchor is (0, 0)', () => { + xit('has a left, right, top, and bottom when the anchor is (0, 0)', () => { actor.pos.x = 100; actor.pos.y = 100; actor.anchor = new ex.Vector(0.0, 0.0); - actor.setWidth(100); - actor.setHeight(100); + actor.width = 100; + actor.height = 100; - expect(actor.getLeft()).toBe(100); - expect(actor.getRight()).toBe(200); - expect(actor.getTop()).toBe(100); - expect(actor.getBottom()).toBe(200); + expect(actor.body.collider.bounds.left).toBe(100); + expect(actor.body.collider.bounds.right).toBe(200); + expect(actor.body.collider.bounds.top).toBe(100); + expect(actor.body.collider.bounds.bottom).toBe(200); }); - it('should have the correct world geometry if rotated and scaled', () => { + xit('should have the correct world geometry if rotated and scaled', () => { const actor = new ex.Actor({ pos: new ex.Vector(50, 50), width: 10, height: 10 }); actor.scale.setTo(2, 2); actor.rotation = Math.PI / 2; @@ -332,7 +330,7 @@ describe('A game actor', () => { expect(geom[3].equals(new ex.Vector(40, 60))).toBe(true); }); - it('should have the correct relative geometry if rotated and scaled', () => { + xit('should have the correct relative geometry if rotated and scaled', () => { const actor = new ex.Actor({ pos: new ex.Vector(50, 50), width: 10, height: 10 }); actor.scale.setTo(2, 2); actor.rotation = Math.PI / 2; @@ -345,11 +343,11 @@ describe('A game actor', () => { expect(geom[3].equals(new ex.Vector(-10, 10))).toBe(true); }); - it('can contain points', () => { + xit('can contain points', () => { expect(actor.pos.x).toBe(0); expect(actor.pos.y).toBe(0); - actor.setWidth(20); - actor.setHeight(20); + actor.width = 20; + actor.height = 20; expect(actor.anchor.x).toBe(0.5); expect(actor.anchor.y).toBe(0.5); @@ -370,39 +368,39 @@ describe('A game actor', () => { const other = new ex.Actor(10, 10, 10, 10); // Actors are adjacent and not overlapping should not collide - expect(actor.collidesWithSide(other)).toBeFalsy(); - expect(other.collidesWithSide(actor)).toBeFalsy(); + expect(actor.body.collider.bounds.intersectWithSide(other.body.collider.bounds)).toBe(ex.Side.None); + expect(other.body.collider.bounds.intersectWithSide(actor.body.collider.bounds)).toBe(ex.Side.None); // move other actor into collision range from the right side other.pos.x = 9; other.pos.y = 0; - expect(actor.collidesWithSide(other)).toBe(ex.Side.Right); - expect(other.collidesWithSide(actor)).toBe(ex.Side.Left); + expect(actor.body.collider.bounds.intersectWithSide(other.body.collider.bounds)).toBe(ex.Side.Right); + expect(other.body.collider.bounds.intersectWithSide(actor.body.collider.bounds)).toBe(ex.Side.Left); // move other actor into collision range from the left side other.pos.x = -9; other.pos.y = 0; - expect(actor.collidesWithSide(other)).toBe(ex.Side.Left); - expect(other.collidesWithSide(actor)).toBe(ex.Side.Right); + expect(actor.body.collider.bounds.intersectWithSide(other.body.collider.bounds)).toBe(ex.Side.Left); + expect(other.body.collider.bounds.intersectWithSide(actor.body.collider.bounds)).toBe(ex.Side.Right); // move other actor into collision range from the top other.pos.x = 0; other.pos.y = -9; - expect(actor.collidesWithSide(other)).toBe(ex.Side.Top); - expect(other.collidesWithSide(actor)).toBe(ex.Side.Bottom); + expect(actor.body.collider.bounds.intersectWithSide(other.body.collider.bounds)).toBe(ex.Side.Top); + expect(other.body.collider.bounds.intersectWithSide(actor.body.collider.bounds)).toBe(ex.Side.Bottom); // move other actor into collision range from the bottom other.pos.x = 0; other.pos.y = 9; - expect(actor.collidesWithSide(other)).toBe(ex.Side.Bottom); - expect(other.collidesWithSide(actor)).toBe(ex.Side.Top); + expect(actor.body.collider.bounds.intersectWithSide(other.body.collider.bounds)).toBe(ex.Side.Bottom); + expect(other.body.collider.bounds.intersectWithSide(actor.body.collider.bounds)).toBe(ex.Side.Top); }); it('participates with another in a collision', () => { const actor = new ex.Actor(0, 0, 10, 10); - actor.collisionType = ex.CollisionType.Active; + actor.body.collider.type = ex.CollisionType.Active; const other = new ex.Actor(8, 0, 10, 10); - other.collisionType = ex.CollisionType.Active; + other.body.collider.type = ex.CollisionType.Active; let actorCalled = 'false'; let otherCalled = 'false'; @@ -661,12 +659,12 @@ describe('A game actor', () => { const scene = new ex.Scene(engine); const active = new ex.Actor(0, -50, 100, 100); - active.collisionType = ex.CollisionType.Active; + active.body.collider.type = ex.CollisionType.Active; active.vel.y = 10; active.acc.y = 1000; const fixed = new ex.Actor(-100, 50, 1000, 100); - fixed.collisionType = ex.CollisionType.Fixed; + fixed.body.collider.type = ex.CollisionType.Fixed; scene.add(active); scene.add(fixed); @@ -692,12 +690,12 @@ describe('A game actor', () => { it('with an active collision type can jump on a fixed type', () => { const scene = new ex.Scene(engine); const active = new ex.Actor(0, -50, 100, 100); - active.collisionType = ex.CollisionType.Active; + active.body.collider.type = ex.CollisionType.Active; active.vel.y = -100; ex.Physics.acc.setTo(0, 0); const fixed = new ex.Actor(-100, 50, 1000, 100); - fixed.collisionType = ex.CollisionType.Fixed; + fixed.body.collider.type = ex.CollisionType.Fixed; scene.add(active); scene.add(fixed); @@ -1291,8 +1289,7 @@ describe('A game actor', () => { const texture = new ex.Texture('base/src/spec/images/SpriteSpec/icon.png', true); texture.load().then(() => { const actor = new ex.Actor({ - x: engine.halfCanvasWidth, - y: engine.halfCanvasHeight, + pos: new ex.Vector(engine.halfCanvasWidth, engine.halfCanvasHeight), width: 10, height: 10, rotation: Math.PI / 4 @@ -1354,8 +1351,7 @@ describe('A game actor', () => { it('can be offscreen', () => { const actor = new ex.Actor({ - x: 0, - y: 0, + pos: ex.Vector.Zero, width: 10, height: 10 }); @@ -1365,7 +1361,7 @@ describe('A game actor', () => { expect(actor.isOffScreen).toBe(false, 'Actor should be onscreen'); - actor.x = 106; + actor.pos.x = 106; scene.update(engine, 100); expect(actor.isOffScreen).toBe(true, 'Actor should be offscreen'); diff --git a/src/spec/BoundingBoxSpec.ts b/src/spec/BoundingBoxSpec.ts index 9c44d5e0..3336cb61 100644 --- a/src/spec/BoundingBoxSpec.ts +++ b/src/spec/BoundingBoxSpec.ts @@ -8,23 +8,23 @@ describe('A Bounding Box', () => { }); it('has a width', () => { - expect(bb.getWidth()).toBe(10); + expect(bb.width).toBe(10); bb.right = 20; - expect(bb.getWidth()).toBe(20); + expect(bb.width).toBe(20); bb.left = -20; - expect(bb.getWidth()).toBe(40); + expect(bb.width).toBe(40); bb.top = -20; - expect(bb.getWidth()).toBe(40); + expect(bb.width).toBe(40); }); it('has a height', () => { - expect(bb.getHeight()).toBe(10); + expect(bb.height).toBe(10); bb.right = 20; - expect(bb.getHeight()).toBe(10); + expect(bb.height).toBe(10); bb.bottom = 20; - expect(bb.getHeight()).toBe(20); + expect(bb.height).toBe(20); bb.top = -20; - expect(bb.getHeight()).toBe(40); + expect(bb.height).toBe(40); }); it('can contain points', () => { @@ -53,13 +53,13 @@ describe('A Bounding Box', () => { const b3 = new ex.BoundingBox(12, 0, 28, 10); // bb should resolve by being displaced -8 to the left against b2 - expect(bb.collides(b2).x).toBe(-8); + expect(bb.intersect(b2).x).toBe(-8); // b2 should resolve by being displaced -8 to the left against b3 - expect(b2.collides(b3).x).toBe(-8); + expect(b2.intersect(b3).x).toBe(-8); // bb should not collide with b3, they are only touching - expect(bb.collides(b3)).toBeFalsy(); + expect(bb.intersect(b3)).toBeFalsy(); b2.top = 5; b2.left = 6; @@ -67,49 +67,49 @@ describe('A Bounding Box', () => { b2.bottom = 15; // bb should be displaced up and out by -5 against b2 - expect(bb.collides(b2).x).toBe(-4); + expect(bb.intersect(b2).x).toBe(-4); }); it('can collide with other bounding boxes with width/height (0,0)', () => { const bb = new ex.BoundingBox(18, 15, 18, 15); // point bounding box const bb2 = new ex.BoundingBox(0, 0, 20, 20); // square bounding box; - expect(bb2.collides(bb)).not.toBe(null, 'Point bounding boxes should still collide'); - expect(bb2.collides(bb).x).toBe(-2); - expect(bb2.collides(bb).y).toBe(0); + expect(bb2.intersect(bb)).not.toBe(null, 'Point bounding boxes should still collide'); + expect(bb2.intersect(bb).x).toBe(-2); + expect(bb2.intersect(bb).y).toBe(0); }); describe('when in full containment', () => { it('closest right', () => { const bb = new ex.BoundingBox(0, 0, 50, 50); const bb1 = new ex.BoundingBox(40, 8, 49, 12); - expect(bb.collides(bb1)).not.toBe(null); - expect(bb.collides(bb1).x).toBe(-10, 'X should be -10'); - expect(bb.collides(bb1).y).toBe(0, 'Y should be 0'); + expect(bb.intersect(bb1)).not.toBe(null); + expect(bb.intersect(bb1).x).toBe(-10, 'X should be -10'); + expect(bb.intersect(bb1).y).toBe(0, 'Y should be 0'); }); it('closet left', () => { const bb = new ex.BoundingBox(0, 0, 50, 50); const bb1 = new ex.BoundingBox(1, 15, 10, 20); - expect(bb.collides(bb1)).not.toBe(null); - expect(bb.collides(bb1).x).toBe(10, 'X should be 10'); - expect(bb.collides(bb1).y).toBe(0, 'Y should be 0'); + expect(bb.intersect(bb1)).not.toBe(null); + expect(bb.intersect(bb1).x).toBe(10, 'X should be 10'); + expect(bb.intersect(bb1).y).toBe(0, 'Y should be 0'); }); it('closest top', () => { const bb = new ex.BoundingBox(0, 0, 50, 50); const bb1 = new ex.BoundingBox(10, 1, 12, 10); - expect(bb.collides(bb1)).not.toBe(null); - expect(bb.collides(bb1).x).toBe(0, 'X should be 0'); - expect(bb.collides(bb1).y).toBe(10, 'Y should be 0'); + expect(bb.intersect(bb1)).not.toBe(null); + expect(bb.intersect(bb1).x).toBe(0, 'X should be 0'); + expect(bb.intersect(bb1).y).toBe(10, 'Y should be 0'); }); it('closest bottom', () => { const bb = new ex.BoundingBox(0, 0, 50, 50); const bb1 = new ex.BoundingBox(10, 40, 12, 49); - expect(bb.collides(bb1)).not.toBe(null); - expect(bb.collides(bb1).x).toBe(0, 'X should be 0'); - expect(bb.collides(bb1).y).toBe(-10, 'Y should be -10'); + expect(bb.intersect(bb1)).not.toBe(null); + expect(bb.intersect(bb1).x).toBe(0, 'X should be 0'); + expect(bb.intersect(bb1).y).toBe(-10, 'Y should be -10'); }); }); @@ -117,9 +117,9 @@ describe('A Bounding Box', () => { const bb1 = new ex.BoundingBox(0, 0, 10, 10); const bb2 = new ex.BoundingBox(0, 0, 10, 10); - expect(bb2.collides(bb1)).not.toBe(null); - expect(bb2.collides(bb1).x).toBe(0); - expect(bb2.collides(bb1).y).toBe(-10); + expect(bb2.intersect(bb1)).not.toBe(null); + expect(bb2.intersect(bb1).x).toBe(0); + expect(bb2.intersect(bb1).y).toBe(-10); }); it('can combine with other bounding boxes', () => { @@ -127,8 +127,8 @@ describe('A Bounding Box', () => { const b3 = new ex.BoundingBox(12, 0, 28, 10); const newBB = b2.combine(b3); - expect(newBB.getWidth()).toBe(26); - expect(newBB.getHeight()).toBe(10); + expect(newBB.width).toBe(26); + expect(newBB.height).toBe(10); expect(newBB.left).toBe(2); expect(newBB.right).toBe(28); diff --git a/src/spec/CameraSpec.ts b/src/spec/CameraSpec.ts index a8ae61d8..0b2224c5 100644 --- a/src/spec/CameraSpec.ts +++ b/src/spec/CameraSpec.ts @@ -25,9 +25,9 @@ describe('A camera', () => { engine.backgroundColor = ex.Color.Blue; actor.pos.x = 250; - actor.setWidth(10); + actor.width = 10; actor.pos.y = 250; - actor.setHeight(10); + actor.height = 10; actor.color = ex.Color.Red; scene = new ex.Scene(engine); scene.add(actor); diff --git a/src/spec/CollisionContactSpec.ts b/src/spec/CollisionContactSpec.ts index 2c39ab81..49e84fe7 100644 --- a/src/spec/CollisionContactSpec.ts +++ b/src/spec/CollisionContactSpec.ts @@ -6,16 +6,18 @@ describe('A CollisionContact', () => { beforeEach(() => { actorA = new ex.Actor(0, 0, 20, 20); - actorA.collisionType = ex.CollisionType.Active; - actorA.collisionArea = new ex.CircleArea({ + const colliderA = actorA.body.collider; + colliderA.type = ex.CollisionType.Active; + colliderA.shape = new ex.Circle({ radius: 10, body: actorA.body }); actorB = new ex.Actor(20, 0, 20, 20); - actorB.collisionType = ex.CollisionType.Active; + const colliderB = actorB.body.collider; + colliderB.type = ex.CollisionType.Active; - actorB.collisionArea = new ex.CircleArea({ + colliderB.shape = new ex.Circle({ radius: 10, body: actorB.body }); @@ -27,8 +29,8 @@ describe('A CollisionContact', () => { it('can be created', () => { const cc = new ex.CollisionContact( - actorA.collisionArea, - actorB.collisionArea, + actorA.body.collider, + actorB.body.collider, ex.Vector.Zero.clone(), new ex.Vector(10, 0), ex.Vector.Right.clone() @@ -37,21 +39,21 @@ describe('A CollisionContact', () => { }); it('can reslove in the Box system', () => { - actorB.x = 19; + actorB.pos.x = 19; const cc = new ex.CollisionContact( - actorA.collisionArea, - actorB.collisionArea, + actorA.body.collider, + actorB.body.collider, ex.Vector.Right.clone(), new ex.Vector(10, 0), ex.Vector.Right.clone() ); cc.resolve(ex.CollisionResolutionStrategy.Box); - expect(actorA.x).toBe(-0.5); - expect(actorA.y).toBe(0); + expect(actorA.pos.x).toBe(-0.5); + expect(actorA.pos.y).toBe(0); - expect(actorB.x).toBe(19.5); - expect(actorB.y).toBe(0); + expect(actorB.pos.x).toBe(19.5); + expect(actorB.pos.y).toBe(0); }); it('emits a collision event on both in the Box system', () => { @@ -66,10 +68,10 @@ describe('A CollisionContact', () => { emittedB = true; }); - actorB.x = 19; + actorB.pos.x = 19; const cc = new ex.CollisionContact( - actorA.collisionArea, - actorB.collisionArea, + actorA.body.collider, + actorB.body.collider, ex.Vector.Right.clone(), new ex.Vector(10, 0), ex.Vector.Right.clone() @@ -81,20 +83,20 @@ describe('A CollisionContact', () => { }); it('can reslove in the Dynamic system', () => { - expect(actorA.x).toBe(0, 'Actor A should be y=10'); - expect(actorA.y).toBe(0, 'Actor A should be y=0'); - expect(actorB.x).toBe(20, 'Actor B should be x=20'); - expect(actorB.y).toBe(0, 'Actor B should be y=0'); + expect(actorA.pos.x).toBe(0, 'Actor A should be y=10'); + expect(actorA.pos.y).toBe(0, 'Actor A should be y=0'); + expect(actorB.pos.x).toBe(20, 'Actor B should be x=20'); + expect(actorB.pos.y).toBe(0, 'Actor B should be y=0'); expect(actorA.vel.x).toBe(0, 'Actor A should not be moving in x'); expect(actorB.vel.x).toBe(0, 'Actor B should not be moving in x'); actorA.vel.x = 10; actorB.vel.x = -10; - actorB.x = 19; - actorA.collisionArea.recalc(); - actorB.collisionArea.recalc(); + actorB.pos.x = 19; + actorA.body.collider.shape.recalc(); + actorB.body.collider.shape.recalc(); const cc = new ex.CollisionContact( - actorA.collisionArea, - actorB.collisionArea, + actorA.body.collider, + actorB.body.collider, ex.Vector.Right.clone(), new ex.Vector(10, 0), ex.Vector.Right.clone() @@ -105,13 +107,13 @@ describe('A CollisionContact', () => { actorA.body.applyMtv(); actorB.body.applyMtv(); - expect(actorA.x).toBe(-0.5); - expect(actorA.y).toBe(0); + expect(actorA.pos.x).toBe(-0.5); + expect(actorA.pos.y).toBe(0); expect(actorA.vel.x).toBeLessThan(0); expect(actorA.vel.y).toBe(0); - expect(actorB.x).toBe(19.5); - expect(actorB.y).toBe(0); + expect(actorB.pos.x).toBe(19.5); + expect(actorB.pos.y).toBe(0); expect(actorB.vel.x).toBeGreaterThan(0); expect(actorB.vel.y).toBe(0); }); @@ -128,10 +130,10 @@ describe('A CollisionContact', () => { emittedB = true; }); - actorB.x = 19; + actorB.pos.x = 19; const cc = new ex.CollisionContact( - actorA.collisionArea, - actorB.collisionArea, + actorA.body.collider, + actorB.body.collider, ex.Vector.Right.clone(), new ex.Vector(10, 0), ex.Vector.Right.clone() diff --git a/src/spec/CollisionGroupSpec.ts b/src/spec/CollisionGroupSpec.ts index f6a4f180..dcd8ef89 100644 --- a/src/spec/CollisionGroupSpec.ts +++ b/src/spec/CollisionGroupSpec.ts @@ -12,8 +12,8 @@ describe('A Collision Group', () => { actor1 = new ex.Actor(100, 100, 100, 100); actor2 = new ex.Actor(100, 100, 100, 100); // Setting actor collision types to passive otherwise they push each other around - actor1.collisionType = ex.CollisionType.Passive; - actor2.collisionType = ex.CollisionType.Passive; + actor1.body.collider.type = ex.CollisionType.Passive; + actor2.body.collider.type = ex.CollisionType.Passive; scene.add(actor1); scene.add(actor2); @@ -21,64 +21,4 @@ describe('A Collision Group', () => { scene = new ex.Scene(engine); engine.currentScene = scene; }); - /* - it("does not effect actors without collision groupings", ()=>{ - expect(actor1.collides(actor2)).not.toBe(ex.Side.None); - expect(actor2.collides(actor1)).not.toBe(ex.Side.None); - }); - - it("handler should fire only on collision with registered group", ()=>{ - var collided = false; - actor1.onCollidesWith('group', function(){ - collided = true; - }); - - // Ensure that the handler is not fired without collision groups - expect(collided).toBe(false); - scene.update(engine, 20); - expect(collided).toBe(false); - - // Collision handler should fire - actor2.addCollisionGroup('group'); - expect(collided).toBe(false); - scene.update(engine, 20); - expect(collided).toBe(true); - - }); - - it("can fire with multiple handlers", ()=>{ - var collided1 = false; - var collided2 = false; - actor1.onCollidesWith('group1', function(){ - collided1 = true; - }); - actor1.onCollidesWith('group2', function(){ - collided2 = true; - }); - - actor2.addCollisionGroup('group1'); - actor2.addCollisionGroup('group2'); - - expect(collided1).toBe(false); - expect(collided2).toBe(false); - - scene.update(engine, 30); - - expect(collided1).toBe(true); - expect(collided2).toBe(true); - }); - - it("should pass back the collided actor in the callback", ()=>{ - - var actor = null; - actor1.onCollidesWith('group', (a)=>{ - actor = a; - }); - actor2.addCollisionGroup('group'); - expect(actor).toBeFalsy(); - - scene.update(engine, 30); - - expect(actor).toBe(actor2); - });*/ }); diff --git a/src/spec/CollisionAreaSpec.ts b/src/spec/CollisionShapeSpec.ts similarity index 68% rename from src/spec/CollisionAreaSpec.ts rename to src/spec/CollisionShapeSpec.ts index afb5ee30..57018216 100644 --- a/src/spec/CollisionAreaSpec.ts +++ b/src/spec/CollisionShapeSpec.ts @@ -1,11 +1,24 @@ import * as ex from '../../build/dist/excalibur'; +import { ExcaliburMatchers, ensureImagesLoaded } from 'excalibur-jasmine'; +import { TestUtils } from './util/TestUtils'; + +describe('Collision Shape', () => { + beforeAll(() => { + jasmine.addMatchers(ExcaliburMatchers); + }); + + describe('a Circle', () => { + let engine: ex.Engine; + let scene: ex.Scene; -describe('Collision areas', () => { - describe('A Circle shape', () => { let circle: ex.CircleArea; let actor: ex.Actor; beforeEach(() => { + engine = TestUtils.engine(); + scene = new ex.Scene(engine); + engine.currentScene = scene; + actor = new ex.Actor(0, 0, 20, 20); circle = new ex.CircleArea({ pos: ex.Vector.Zero.clone(), @@ -14,15 +27,38 @@ describe('Collision areas', () => { }); }); + afterEach(() => { + engine.stop(); + engine = null; + }); + it('exists', () => { expect(ex.CircleArea).toBeDefined(); }); it('can be constructed with empty args', () => { - const circle = new ex.CircleArea({}); + const circle = new ex.CircleArea({ + radius: 1 + }); expect(circle).not.toBeNull(); }); + it('can be cloned', () => { + const actor1 = new ex.Actor(0, 0, 20, 20); + const circle = new ex.CircleArea({ + collider: actor1.body.collider, + radius: 10, + pos: new ex.Vector(20, 25) + }); + + const sut = circle.clone(); + + expect(sut).not.toBe(circle); + expect(sut.pos).toBeVector(circle.pos); + expect(sut.pos).not.toBe(circle.pos); + expect(sut.collider).toBe(null); + }); + it('can be constructed with points', () => { const actor = new ex.Actor(0, 0, 10, 10); const circle = new ex.CircleArea({ @@ -35,7 +71,7 @@ describe('Collision areas', () => { it('has a center', () => { actor.pos.setTo(170, 300); - const center = circle.getCenter(); + const center = circle.center; expect(center.x).toBe(170); expect(center.y).toBe(300); }); @@ -43,7 +79,7 @@ describe('Collision areas', () => { it('has bounds', () => { actor.pos.setTo(400, 400); - const bounds = circle.getBounds(); + const bounds = circle.bounds; expect(bounds.left).toBe(390); expect(bounds.right).toBe(410); expect(bounds.top).toBe(390); @@ -82,14 +118,29 @@ describe('Collision areas', () => { it('doesnt have axes', () => { // technically circles have infinite axes - expect(circle.getAxes()).toBe(null); + expect(circle.axes).toBe(null); }); it('has a moment of inertia', () => { // following this formula //https://en.wikipedia.org/wiki/List_of_moments_of_inertia // I = m*r^2/2 - expect(circle.getMomentOfInertia()).toBe((circle.body.mass * circle.radius * circle.radius) / 2); + expect(circle.inertia).toBe((circle.body.collider.mass * circle.radius * circle.radius) / 2); + }); + + it('should collide without a collider or body', () => { + const circle1 = new ex.CircleArea({ + pos: new ex.Vector(0, 0), + radius: 5 + }); + + const circle2 = new ex.CircleArea({ + pos: new ex.Vector(9, 0), + radius: 5 + }); + + const cc = circle1.collide(circle2); + expect(cc.mtv).toBeVector(new ex.Vector(1, 0)); }); it('should collide with other circles when touching', () => { @@ -99,7 +150,7 @@ describe('Collision areas', () => { body: actor2.body }); - const directionOfBodyB = circle2.getCenter().sub(circle.getCenter()); + const directionOfBodyB = circle2.center.sub(circle.center); const contact = circle.collide(circle2); // there should be a collision contact formed @@ -130,13 +181,13 @@ describe('Collision areas', () => { it('should collide with other polygons when touching', () => { const actor2 = new ex.Actor(14.99, 0, 10, 10); // meh close enough - const poly = new ex.PolygonArea({ + const poly = new ex.ConvexPolygon({ pos: ex.Vector.Zero.clone(), - points: actor2.getRelativeBounds().getPoints(), + points: actor2.body.collider.localBounds.getPoints(), body: actor2.body }); - const directionOfBodyB = poly.getCenter().sub(circle.getCenter()); + const directionOfBodyB = poly.center.sub(circle.center); const contact = circle.collide(poly); // there should be a collision contact formed @@ -154,9 +205,9 @@ describe('Collision areas', () => { it('should not collide with other polygons when not touching', () => { const actor2 = new ex.Actor(16, 0, 10, 10); - const poly = new ex.PolygonArea({ + const poly = new ex.ConvexPolygon({ pos: ex.Vector.Zero.clone(), - points: actor2.getRelativeBounds().getPoints(), + points: actor2.body.collider.localBounds.getPoints(), body: actor2.body }); @@ -174,10 +225,10 @@ describe('Collision areas', () => { const edge = new ex.EdgeArea({ begin: new ex.Vector(0, 0), end: new ex.Vector(10, 0), - body: actor2.body + collider: actor2.body.collider }); - const directionOfBodyB = edge.getCenter().sub(circle.getCenter()); + const directionOfBodyB = edge.center.sub(circle.center); const contact = circle.collide(edge); // there should be a collision contact formed @@ -201,10 +252,10 @@ describe('Collision areas', () => { const edge = new ex.EdgeArea({ begin: new ex.Vector(0, 0), end: new ex.Vector(10, 0), - body: actor2.body + collider: actor2.body.collider }); - const directionOfBodyB = edge.getCenter().sub(circle.getCenter()); + const directionOfBodyB = edge.center.sub(circle.center); const contact = circle.collide(edge); // there should be a collision contact formed @@ -228,10 +279,10 @@ describe('Collision areas', () => { const edge = new ex.EdgeArea({ begin: new ex.Vector(0, 0), end: new ex.Vector(10, 0), - body: actor2.body + collider: actor2.body.collider }); - const directionOfBodyB = edge.getCenter().sub(circle.getCenter()); + const directionOfBodyB = edge.center.sub(circle.center); const contact = circle.collide(edge); // there should be a collision contact formed @@ -246,20 +297,86 @@ describe('Collision areas', () => { expect(contact.point.x).toBe(0); expect(contact.point.y).toBe(0); }); + + it('can be drawn', (done) => { + const circle = new ex.Circle({ + pos: new ex.Vector(100, 100), + radius: 30 + }); + + circle.draw(engine.ctx, ex.Color.Blue, new ex.Vector(50, 0)); + + ensureImagesLoaded(engine.canvas, 'src/spec/images/CollisionShapeSpec/circle.png').then(([canvas, image]) => { + expect(canvas).toEqualImage(image); + done(); + }); + }); + + it('can be drawn with actor', (done) => { + const circleActor = new ex.Actor({ + pos: new ex.Vector(150, 100), + color: ex.Color.Blue, + body: new ex.Body({ + collider: new ex.Collider({ + shape: ex.Shape.Circle(30) + }) + }) + }); + + scene.add(circleActor); + scene.draw(engine.ctx, 100); + + ensureImagesLoaded(engine.canvas, 'src/spec/images/CollisionShapeSpec/circle.png').then(([canvas, image]) => { + expect(canvas).toEqualImage(image); + done(); + }); + }); }); - describe('A Polygon shape', () => { + describe('a ConvexPolygon', () => { + let engine: ex.Engine; + let scene: ex.Scene; + beforeEach(() => { + engine = TestUtils.engine(); + scene = new ex.Scene(engine); + engine.currentScene = scene; + }); + + afterEach(() => { + engine.stop(); + engine = null; + }); + it('exists', () => { - expect(ex.PolygonArea).toBeDefined(); + expect(ex.ConvexPolygon).toBeDefined(); }); it('can be constructed with empty args', () => { - const poly = new ex.PolygonArea({}); + const poly = new ex.ConvexPolygon({ + points: [ex.Vector.One] + }); expect(poly).not.toBe(null); }); + it('can be cloned', () => { + const actor1 = new ex.Actor(0, 0, 20, 20); + const poly = new ex.ConvexPolygon({ + collider: actor1.body.collider, + points: [ex.Vector.One, ex.Vector.Half], + pos: new ex.Vector(20, 25) + }); + + const sut = poly.clone(); + + expect(sut).not.toBe(poly); + expect(sut.pos).toBeVector(poly.pos); + expect(sut.pos).not.toBe(poly.pos); + expect(sut.points.length).toBe(2); + expect(sut.collider).toBe(null); + }); + it('can be constructed with points', () => { - const poly = new ex.PolygonArea({ + const poly = new ex.ConvexPolygon({ pos: ex.Vector.Zero.clone(), points: [new ex.Vector(-10, -10), new ex.Vector(10, -10), new ex.Vector(10, 10), new ex.Vector(-10, 10)] }); @@ -267,7 +384,7 @@ describe('Collision areas', () => { }); it('can have be constructed with position', () => { - const poly = new ex.PolygonArea({ + const poly = new ex.ConvexPolygon({ pos: new ex.Vector(10, 0), points: [new ex.Vector(-10, -10), new ex.Vector(10, -10), new ex.Vector(10, 10), new ex.Vector(-10, 10)] }); @@ -284,18 +401,18 @@ describe('Collision areas', () => { }); it('can collide with other polygons', () => { - const polyA = new ex.PolygonArea({ + const polyA = new ex.ConvexPolygon({ pos: ex.Vector.Zero.clone(), // specified relative to the position points: [new ex.Vector(-10, -10), new ex.Vector(10, -10), new ex.Vector(10, 10), new ex.Vector(-10, 10)] }); - const polyB = new ex.PolygonArea({ + const polyB = new ex.ConvexPolygon({ pos: new ex.Vector(10, 0), points: [new ex.Vector(-10, -10), new ex.Vector(10, -10), new ex.Vector(10, 10), new ex.Vector(-10, 10)] }); - const directionOfBodyB = polyB.getCenter().sub(polyA.getCenter()); + const directionOfBodyB = polyB.center.sub(polyA.center); // should overlap by 10 pixels in x const contact = polyA.collide(polyB); @@ -316,7 +433,7 @@ describe('Collision areas', () => { it('can collide with the middle of an edge', () => { const actor = new ex.Actor(5, -6, 20, 20); actor.rotation = Math.PI / 4; - const polyA = new ex.PolygonArea({ + const polyA = new ex.ConvexPolygon({ pos: ex.Vector.Zero.clone(), // specified relative to the position points: [new ex.Vector(-5, -5), new ex.Vector(5, -5), new ex.Vector(5, 5), new ex.Vector(-5, 5)], @@ -331,7 +448,7 @@ describe('Collision areas', () => { body: actor2.body }); - const directionOfBodyB = edge.getCenter().sub(polyA.getCenter()); + const directionOfBodyB = edge.center.sub(polyA.center); const contact = polyA.collide(edge); @@ -347,7 +464,7 @@ describe('Collision areas', () => { it('can collide with the end of an edge', () => { const actor = new ex.Actor(0, -4, 20, 20); - const polyA = new ex.PolygonArea({ + const polyA = new ex.ConvexPolygon({ pos: ex.Vector.Zero.clone(), // specified relative to the position points: [new ex.Vector(-5, -5), new ex.Vector(5, -5), new ex.Vector(5, 5), new ex.Vector(-5, 5)], @@ -363,7 +480,7 @@ describe('Collision areas', () => { }); edge.recalc(); - const directionOfBodyB = edge.getCenter().sub(polyA.getCenter()); + const directionOfBodyB = edge.center.sub(polyA.center); const contact = polyA.collide(edge); expect(contact).not.toBe(null); @@ -375,7 +492,7 @@ describe('Collision areas', () => { it('can collide with the end of an edge regardless of order', () => { const actor = new ex.Actor(0, -4, 20, 20); - const polyA = new ex.PolygonArea({ + const polyA = new ex.ConvexPolygon({ pos: ex.Vector.Zero.clone(), // specified relative to the position points: [new ex.Vector(-5, -5), new ex.Vector(5, -5), new ex.Vector(5, 5), new ex.Vector(-5, 5)], @@ -391,7 +508,7 @@ describe('Collision areas', () => { }); edge.recalc(); - const directionOfBodyB = edge.getCenter().sub(polyA.getCenter()); + const directionOfBodyB = edge.center.sub(polyA.center); const contact = polyA.collide(edge); expect(contact).not.toBe(null); @@ -404,7 +521,7 @@ describe('Collision areas', () => { it('should not collide with the middle of an edge when not touching', () => { const actor = new ex.Actor(5, 0, 20, 20); actor.rotation = Math.PI / 4; - const polyA = new ex.PolygonArea({ + const polyA = new ex.ConvexPolygon({ pos: ex.Vector.Zero.clone(), // specified relative to the position points: [new ex.Vector(-5, -5), new ex.Vector(5, -5), new ex.Vector(5, 5), new ex.Vector(-5, 5)], @@ -419,7 +536,7 @@ describe('Collision areas', () => { body: actor2.body }); - const directionOfBodyB = edge.getCenter().sub(polyA.getCenter()); + const directionOfBodyB = edge.center.sub(polyA.center); const contact = polyA.collide(edge); @@ -429,7 +546,7 @@ describe('Collision areas', () => { it('should detected contained points', () => { const actor = new ex.Actor(0, 0, 20, 20); - const polyA = new ex.PolygonArea({ + const polyA = new ex.ConvexPolygon({ pos: ex.Vector.Zero.clone(), // specified relative to the position points: [new ex.Vector(-5, -5), new ex.Vector(5, -5), new ex.Vector(5, 5), new ex.Vector(-5, 5)], @@ -447,7 +564,7 @@ describe('Collision areas', () => { }); it('can calculate the closest face to a point', () => { - const polyA = new ex.PolygonArea({ + const polyA = new ex.ConvexPolygon({ pos: ex.Vector.Zero.clone(), // specified relative to the position points: [new ex.Vector(-5, -5), new ex.Vector(5, -5), new ex.Vector(5, 5), new ex.Vector(-5, 5)] @@ -489,7 +606,7 @@ describe('Collision areas', () => { it('can have ray cast to detect if the ray hits the polygon', () => { const actor = new ex.Actor(0, 0, 20, 20); - const polyA = new ex.PolygonArea({ + const polyA = new ex.ConvexPolygon({ pos: ex.Vector.Zero.clone(), // specified relative to the position points: [new ex.Vector(-5, -5), new ex.Vector(5, -5), new ex.Vector(5, 5), new ex.Vector(-5, 5)], @@ -509,13 +626,59 @@ describe('Collision areas', () => { expect(noHit).toBe(null); expect(tooFar).toBe(null, 'The polygon should be too far away for a hit'); }); + + it('can be drawn', (done) => { + const polygon = new ex.ConvexPolygon({ + pos: new ex.Vector(100, 100), + points: [new ex.Vector(0, -100), new ex.Vector(-100, 50), new ex.Vector(100, 50)] + }); + + polygon.draw(engine.ctx, ex.Color.Blue, new ex.Vector(50, 0)); + + ensureImagesLoaded(engine.canvas, 'src/spec/images/CollisionShapeSpec/triangle.png').then(([canvas, image]) => { + expect(canvas).toEqualImage(image); + done(); + }); + }); + + it('can be drawn with actor', (done) => { + const polygonActor = new ex.Actor({ + pos: new ex.Vector(150, 100), + color: ex.Color.Blue, + body: new ex.Body({ + collider: new ex.Collider({ + shape: ex.Shape.Polygon([new ex.Vector(0, -100), new ex.Vector(-100, 50), new ex.Vector(100, 50)]) + }) + }) + }); + + scene.add(polygonActor); + scene.draw(engine.ctx, 100); + + ensureImagesLoaded(engine.canvas, 'src/spec/images/CollisionShapeSpec/triangle.png').then(([canvas, image]) => { + expect(canvas).toEqualImage(image); + done(); + }); + }); }); - describe('An Edge shape', () => { + describe('an Edge', () => { let actor: ex.Actor = null; let edge: ex.EdgeArea = null; + let engine: ex.Engine; + let scene: ex.Scene; + + afterEach(() => { + engine.stop(); + engine = null; + }); + beforeEach(() => { + engine = TestUtils.engine(); + scene = new ex.Scene(engine); + engine.currentScene = scene; + actor = new ex.Actor(5, 0, 10, 10); edge = new ex.EdgeArea({ begin: new ex.Vector(-5, 0), @@ -525,12 +688,30 @@ describe('Collision areas', () => { }); it('has a center', () => { - const center = edge.getCenter(); + const center = edge.center; expect(center.x).toBe(5); expect(center.y).toBe(0); }); + it('can be cloned', () => { + const actor1 = new ex.Actor(0, 0, 20, 20); + const edge = new ex.Edge({ + collider: actor1.body.collider, + begin: ex.Vector.One, + end: ex.Vector.Half + }); + + const sut = edge.clone(); + + expect(sut).not.toBe(edge); + expect(sut.pos).toBeVector(edge.pos); + expect(sut.begin).toBeVector(edge.begin); + expect(sut.end).toBeVector(edge.end); + expect(sut.pos).not.toBe(edge.pos); + expect(sut.collider).toBe(null); + }); + it('has a length', () => { const length = edge.getLength(); expect(length).toBe(10); @@ -567,13 +748,13 @@ describe('Collision areas', () => { }); it('has 4 axes', () => { - const axes = edge.getAxes(); + const axes = edge.axes; expect(axes.length).toBe(4); }); it('has bounds', () => { actor.pos.setTo(400, 400); - const boundingBox = edge.getBounds(); + const boundingBox = edge.bounds; const transformedBegin = new ex.Vector(395, 400); const transformedEnd = new ex.Vector(405, 400); @@ -585,9 +766,43 @@ describe('Collision areas', () => { it('has a moi', () => { // following this formula https://en.wikipedia.org/wiki/List_of_moments_of_inertia // rotates from the middle treating the ends as a point mass - const moi = edge.getMomentOfInertia(); + const moi = edge.inertia; const length = edge.end.sub(edge.begin).distance() / 2; - expect(moi).toBeCloseTo(edge.body.mass * length * length, 0.001); + expect(moi).toBeCloseTo(edge.body.collider.mass * length * length, 0.001); + }); + + it('can be drawn', (done) => { + const edge = new ex.Edge({ + begin: new ex.Vector(100, 100), + end: new ex.Vector(400, 400) + }); + + edge.draw(engine.ctx, ex.Color.Blue, new ex.Vector(50, 0)); + + ensureImagesLoaded(engine.canvas, 'src/spec/images/CollisionShapeSpec/edge.png').then(([canvas, image]) => { + expect(canvas).toEqualImage(image); + done(); + }); + }); + + it('can be drawn with actor', (done) => { + const edgeActor = new ex.Actor({ + pos: new ex.Vector(150, 100), + color: ex.Color.Blue, + body: new ex.Body({ + collider: new ex.Collider({ + shape: ex.Shape.Edge(ex.Vector.Zero, new ex.Vector(300, 300)) + }) + }) + }); + + scene.add(edgeActor); + scene.draw(engine.ctx, 100); + + ensureImagesLoaded(engine.canvas, 'src/spec/images/CollisionShapeSpec/edge.png').then(([canvas, image]) => { + expect(canvas).toEqualImage(image); + done(); + }); }); }); }); diff --git a/src/spec/CollisionSpec.ts b/src/spec/CollisionSpec.ts index c70d5553..16ff9017 100644 --- a/src/spec/CollisionSpec.ts +++ b/src/spec/CollisionSpec.ts @@ -16,8 +16,8 @@ describe('A Collision', () => { actor1 = new ex.Actor(0, 0, 10, 10); actor2 = new ex.Actor(5, 5, 10, 10); - actor1.collisionType = ex.CollisionType.Active; - actor2.collisionType = ex.CollisionType.Active; + actor1.body.collider.type = ex.CollisionType.Active; + actor2.body.collider.type = ex.CollisionType.Active; engine.start(); engine.add(actor1); @@ -55,16 +55,16 @@ describe('A Collision', () => { it('order of actors collision should not matter when an Active and Active Collision', () => { const collisionTree = new ex.DynamicTreeCollisionBroadphase(); - actor1.collisionType = ex.CollisionType.Active; - actor2.collisionType = ex.CollisionType.Active; + actor1.body.collider.type = ex.CollisionType.Active; + actor2.body.collider.type = ex.CollisionType.Active; collisionTree.track(actor1.body); collisionTree.track(actor2.body); - let pairs = collisionTree.broadphase([actor1, actor2], 200); + let pairs = collisionTree.broadphase([actor1.body, actor2.body], 200); expect(pairs.length).toBe(1); - pairs = collisionTree.broadphase([actor2, actor1], 200); + pairs = collisionTree.broadphase([actor2.body, actor1.body], 200); expect(pairs.length).toBe(1); }); @@ -72,16 +72,16 @@ describe('A Collision', () => { it('order of actors collision should not matter when an Active and Passive Collision', () => { const collisionTree = new ex.DynamicTreeCollisionBroadphase(); - actor1.collisionType = ex.CollisionType.Active; - actor2.collisionType = ex.CollisionType.Passive; + actor1.body.collider.type = ex.CollisionType.Active; + actor2.body.collider.type = ex.CollisionType.Passive; collisionTree.track(actor1.body); collisionTree.track(actor2.body); - let pairs = collisionTree.broadphase([actor1, actor2], 200); + let pairs = collisionTree.broadphase([actor1.body, actor2.body], 200); expect(pairs.length).toBe(1); - pairs = collisionTree.broadphase([actor2, actor1], 200); + pairs = collisionTree.broadphase([actor2.body, actor1.body], 200); expect(pairs.length).toBe(1); }); @@ -89,16 +89,16 @@ describe('A Collision', () => { it('order of actors collision should not matter when an Active and PreventCollision', () => { const collisionTree = new ex.DynamicTreeCollisionBroadphase(); - actor1.collisionType = ex.CollisionType.Active; - actor2.collisionType = ex.CollisionType.PreventCollision; + actor1.body.collider.type = ex.CollisionType.Active; + actor2.body.collider.type = ex.CollisionType.PreventCollision; collisionTree.track(actor1.body); collisionTree.track(actor2.body); - let pairs = collisionTree.broadphase([actor1, actor2], 200); + let pairs = collisionTree.broadphase([actor1.body, actor2.body], 200); expect(pairs.length).toBe(0); - pairs = collisionTree.broadphase([actor2, actor1], 200); + pairs = collisionTree.broadphase([actor2.body, actor1.body], 200); expect(pairs.length).toBe(0); }); @@ -106,16 +106,16 @@ describe('A Collision', () => { it('order of actors collision should not matter when an Active and Fixed', () => { const collisionTree = new ex.DynamicTreeCollisionBroadphase(); - actor1.collisionType = ex.CollisionType.Active; - actor2.collisionType = ex.CollisionType.Fixed; + actor1.body.collider.type = ex.CollisionType.Active; + actor2.body.collider.type = ex.CollisionType.Fixed; collisionTree.track(actor1.body); collisionTree.track(actor2.body); - let pairs = collisionTree.broadphase([actor1, actor2], 200); + let pairs = collisionTree.broadphase([actor1.body, actor2.body], 200); expect(pairs.length).toBe(1); - pairs = collisionTree.broadphase([actor2, actor1], 200); + pairs = collisionTree.broadphase([actor2.body, actor1.body], 200); expect(pairs.length).toBe(1); }); @@ -123,16 +123,16 @@ describe('A Collision', () => { it('order of actors collision should not matter when an Fixed and Fixed', () => { const collisionTree = new ex.DynamicTreeCollisionBroadphase(); - actor1.collisionType = ex.CollisionType.Fixed; - actor2.collisionType = ex.CollisionType.Fixed; + actor1.body.collider.type = ex.CollisionType.Fixed; + actor2.body.collider.type = ex.CollisionType.Fixed; collisionTree.track(actor1.body); collisionTree.track(actor2.body); - let pairs = collisionTree.broadphase([actor1, actor2], 200); + let pairs = collisionTree.broadphase([actor1.body, actor2.body], 200); expect(pairs.length).toBe(0); - pairs = collisionTree.broadphase([actor2, actor1], 200); + pairs = collisionTree.broadphase([actor2.body, actor1.body], 200); expect(pairs.length).toBe(0); }); @@ -149,7 +149,7 @@ describe('A Collision', () => { actor2Collision++; }); - actor2.collisionType = ex.CollisionType.Passive; + actor2.body.collider.type = ex.CollisionType.Passive; for (let i = 0; i < 50; i++) { loop.advance(100); @@ -183,7 +183,7 @@ describe('A Collision', () => { it('should recognize when actor bodies are touching', () => { let touching = false; actor1.on('postupdate', function() { - if (actor1.body.touching(actor2)) { + if (actor1.body.collider.touching(actor2.body.collider)) { touching = true; } }); @@ -199,12 +199,12 @@ describe('A Collision', () => { ex.Physics.collisionResolutionStrategy = ex.CollisionResolutionStrategy.RigidBody; const activeBlock = new ex.Actor(200, 200, 50, 50, ex.Color.Red.clone()); - activeBlock.collisionType = ex.CollisionType.Active; + activeBlock.body.collider.type = ex.CollisionType.Active; activeBlock.vel.x = 100; engine.add(activeBlock); const passiveBlock = new ex.Actor(400, 200, 50, 50, ex.Color.DarkGray.clone()); - passiveBlock.collisionType = ex.CollisionType.Passive; + passiveBlock.body.collider.type = ex.CollisionType.Passive; passiveBlock.vel.x = -100; engine.add(passiveBlock); @@ -233,12 +233,12 @@ describe('A Collision', () => { ex.Physics.collisionResolutionStrategy = ex.CollisionResolutionStrategy.RigidBody; const activeBlock = new ex.Actor(200, 200, 50, 50, ex.Color.Red.clone()); - activeBlock.collisionType = ex.CollisionType.Active; + activeBlock.body.collider.type = ex.CollisionType.Active; activeBlock.vel.x = 100; engine.add(activeBlock); const passiveBlock = new ex.Actor(400, 200, 50, 50, ex.Color.DarkGray.clone()); - passiveBlock.collisionType = ex.CollisionType.Passive; + passiveBlock.body.collider.type = ex.CollisionType.Passive; passiveBlock.vel.x = -100; engine.add(passiveBlock); @@ -261,12 +261,12 @@ describe('A Collision', () => { ex.Physics.collisionResolutionStrategy = ex.CollisionResolutionStrategy.RigidBody; const activeBlock = new ex.Actor(200, 200, 50, 50, ex.Color.Red.clone()); - activeBlock.collisionType = ex.CollisionType.Active; + activeBlock.body.collider.type = ex.CollisionType.Active; activeBlock.vel.x = 100; engine.add(activeBlock); const passiveBlock = new ex.Actor(400, 200, 50, 50, ex.Color.DarkGray.clone()); - passiveBlock.collisionType = ex.CollisionType.Passive; + passiveBlock.body.collider.type = ex.CollisionType.Passive; passiveBlock.vel.x = -100; engine.add(passiveBlock); @@ -289,12 +289,12 @@ describe('A Collision', () => { ex.Physics.collisionResolutionStrategy = ex.CollisionResolutionStrategy.Box; const activeBlock = new ex.Actor(200, 200, 50, 50, ex.Color.Red.clone()); - activeBlock.collisionType = ex.CollisionType.Active; + activeBlock.body.collider.type = ex.CollisionType.Active; activeBlock.vel.x = 100; engine.add(activeBlock); const fixedBlock = new ex.Actor(400, 200, 50, 50, ex.Color.DarkGray.clone()); - fixedBlock.collisionType = ex.CollisionType.Fixed; + fixedBlock.body.collider.type = ex.CollisionType.Fixed; engine.add(fixedBlock); for (let i = 0; i < 20; i++) { @@ -308,11 +308,11 @@ describe('A Collision', () => { ex.Physics.collisionResolutionStrategy = ex.CollisionResolutionStrategy.Box; const activeBlock = new ex.Actor(350, 200, 50, 50, ex.Color.Red.clone()); - activeBlock.collisionType = ex.CollisionType.Active; + activeBlock.body.collider.type = ex.CollisionType.Active; engine.add(activeBlock); const fixedBlock = new ex.Actor(400, 200, 50, 50, ex.Color.DarkGray.clone()); - fixedBlock.collisionType = ex.CollisionType.Fixed; + fixedBlock.body.collider.type = ex.CollisionType.Fixed; engine.add(fixedBlock); activeBlock.vel.x = -100; @@ -328,12 +328,12 @@ describe('A Collision', () => { ex.Physics.collisionResolutionStrategy = ex.CollisionResolutionStrategy.RigidBody; const activeBlock = new ex.Actor(200, 200, 50, 50, ex.Color.Red.clone()); - activeBlock.collisionType = ex.CollisionType.Active; + activeBlock.body.collider.type = ex.CollisionType.Active; activeBlock.vel.x = 100; engine.add(activeBlock); const passiveBlock = new ex.Actor(400, 200, 50, 50, ex.Color.DarkGray.clone()); - passiveBlock.collisionType = ex.CollisionType.Passive; + passiveBlock.body.collider.type = ex.CollisionType.Passive; passiveBlock.vel.x = -100; engine.add(passiveBlock); @@ -353,12 +353,12 @@ describe('A Collision', () => { ex.Physics.collisionResolutionStrategy = ex.CollisionResolutionStrategy.RigidBody; const activeBlock = new ex.Actor(200, 200, 50, 50, ex.Color.Red.clone()); - activeBlock.collisionType = ex.CollisionType.Active; + activeBlock.body.collider.type = ex.CollisionType.Active; activeBlock.vel.x = 100; engine.add(activeBlock); const passiveBlock = new ex.Actor(400, 200, 50, 50, ex.Color.DarkGray.clone()); - passiveBlock.collisionType = ex.CollisionType.Passive; + passiveBlock.body.collider.type = ex.CollisionType.Passive; passiveBlock.vel.x = -100; engine.add(passiveBlock); diff --git a/src/spec/DynamicTreeBroadphaseSpec.ts b/src/spec/DynamicTreeBroadphaseSpec.ts index 645d8b28..6fe5286e 100644 --- a/src/spec/DynamicTreeBroadphaseSpec.ts +++ b/src/spec/DynamicTreeBroadphaseSpec.ts @@ -7,26 +7,26 @@ describe('A DynamicTree Broadphase', () => { beforeEach(() => { actorA = new ex.Actor(0, 0, 20, 20); - actorA.collisionType = ex.CollisionType.Active; - actorA.collisionArea = new ex.CircleArea({ - radius: 10, - body: actorA.body + const colliderA = actorA.body.collider; + colliderA.type = ex.CollisionType.Active; + colliderA.shape = new ex.Circle({ + radius: 10 }); actorB = new ex.Actor(20, 0, 20, 20); - actorB.collisionType = ex.CollisionType.Active; + const colliderB = actorB.body.collider; + colliderB.type = ex.CollisionType.Active; - actorB.collisionArea = new ex.CircleArea({ - radius: 10, - body: actorB.body + colliderB.shape = new ex.Circle({ + radius: 10 }); actorC = new ex.Actor(1000, 0, 20, 20); - actorC.collisionType = ex.CollisionType.Active; + const colliderC = actorC.body.collider; + colliderC.type = ex.CollisionType.Active; - actorC.collisionArea = new ex.CircleArea({ - radius: 10, - body: actorC.body + colliderC.shape = new ex.Circle({ + radius: 10 }); }); @@ -47,7 +47,7 @@ describe('A DynamicTree Broadphase', () => { dt.track(actorC.body); // only should be 1 pair since C is very far away - const pairs = dt.broadphase([actorA, actorB, actorC], 100); + const pairs = dt.broadphase([actorA.body, actorB.body, actorC.body], 100); expect(pairs.length).toBe(1); }); diff --git a/src/spec/EngineSpec.ts b/src/spec/EngineSpec.ts index c563dafb..c4cd3413 100644 --- a/src/spec/EngineSpec.ts +++ b/src/spec/EngineSpec.ts @@ -67,8 +67,7 @@ describe('The engine', () => { engine.currentScene = scene; engine.currentScene.add( new ex.Actor({ - x: 250, - y: 250, + pos: new ex.Vector(250, 250), width: 20, height: 20, color: ex.Color.Red diff --git a/src/spec/EventSpec.ts b/src/spec/EventSpec.ts index 7b776695..3e976bd7 100644 --- a/src/spec/EventSpec.ts +++ b/src/spec/EventSpec.ts @@ -66,10 +66,6 @@ describe('An Event Dispatcher', () => { expect(eventHistory).toEqual(subscriptions); }); - //it('can be subscribed to', () => { }); //TODO - - //it('can be unsubscribed from', () => { }); //TODO - it('can wire to other event dispatchers', () => { const newPubSub = new ex.EventDispatcher(null); pubsub.wire(newPubSub); diff --git a/src/spec/GroupSpec.ts b/src/spec/GroupSpec.ts index 325153d4..309debb5 100644 --- a/src/spec/GroupSpec.ts +++ b/src/spec/GroupSpec.ts @@ -1,6 +1,7 @@ import * as ex from '../../build/dist/excalibur'; import { Mocks } from './util/Mocks'; +// @obsolete in v0.24.0 describe('An Actor Group', () => { let engine: ex.Engine; let scene: ex.Scene; @@ -38,6 +39,7 @@ describe('An Actor Group', () => { expect(group.getMembers().length).toBe(2); }); + // @obsolete in v0.24.0 it('members ares automatically add to the scene', () => { const actor = new ex.Actor(); @@ -47,6 +49,7 @@ describe('An Actor Group', () => { expect(scene.contains(actor)).toBeTruthy(); }); + // @obsolete in v0.24.0 it('can remove members', () => { const actor = new ex.Actor(); group.add(actor); @@ -57,6 +60,7 @@ describe('An Actor Group', () => { expect(scene.contains(actor)).toBeTruthy(); }); + // @obsolete in v0.24.0 it('can aggregate events across multiple actors', () => { let eventCount = 0; // arrange @@ -79,7 +83,8 @@ describe('An Actor Group', () => { expect(eventCount).toBe(3); }); - it('can return the containing bounding box of all members', () => { + // @obsolete in v0.24.0 + xit('can return the containing bounding box of all members', () => { const a1 = new ex.Actor(0, 0, 100, 100); a1.anchor.setTo(0, 0); const a2 = new ex.Actor(100, 100, 200, 190); @@ -91,7 +96,8 @@ describe('An Actor Group', () => { expect(group.getBounds().getHeight()).toBe(290); }); - it('can get a random member', () => { + // @obsolete in v0.24.0 + xit('can get a random member', () => { // arrange const a1 = new ex.Actor(); const a2 = new ex.Actor(); @@ -104,6 +110,7 @@ describe('An Actor Group', () => { expect(group.contains(ran)).toBeTruthy(); }); + // @obsolete in v0.24.0 it('can move many actors at once by a delta', () => { const a1 = new ex.Actor(0, 0, 100, 100); const a2 = new ex.Actor(100, 100, 200, 190); @@ -118,6 +125,7 @@ describe('An Actor Group', () => { expect(a2.pos.y).toBe(110); }); + // @obsolete in v0.24.0 it('can rotate many actors at once by an angle', () => { const a1 = new ex.Actor(0, 0, 100, 100); a1.rotation = Math.PI / 3; @@ -131,6 +139,7 @@ describe('An Actor Group', () => { expect(a2.rotation).toBeCloseTo((Math.PI * 5) / 6, 0.001); }); + // @obsolete in v0.24.0 it('can call actions off of actors', () => { const a1 = new ex.Actor(0, 0, 100, 100); diff --git a/src/spec/ParticleSpec.ts b/src/spec/ParticleSpec.ts index f1dcda33..7f51ea75 100644 --- a/src/spec/ParticleSpec.ts +++ b/src/spec/ParticleSpec.ts @@ -21,8 +21,7 @@ describe('A particle', () => { it('should have props set by the constructor', () => { const emitter = new ex.ParticleEmitter({ - x: 400, - y: 100, + pos: new ex.Vector(400, 100), width: 20, height: 30, isEmitting: true, @@ -51,10 +50,10 @@ describe('A particle', () => { random: new ex.Random(1337) }); - expect(emitter.x).toBe(400); - expect(emitter.y).toBe(100); - expect(emitter.getWidth()).toBe(20); - expect(emitter.getHeight()).toBe(30); + expect(emitter.pos.x).toBe(400); + expect(emitter.pos.y).toBe(100); + expect(emitter.width).toBe(20); + expect(emitter.height).toBe(30); expect(emitter.isEmitting).toBe(true); expect(emitter.minVel).toBe(40); expect(emitter.maxVel).toBe(50); @@ -83,8 +82,7 @@ describe('A particle', () => { it('should emit particles', (done) => { const emitter = new ex.ParticleEmitter({ - x: 400, - y: 100, + pos: new ex.Vector(400, 100), width: 20, height: 30, isEmitting: true, diff --git a/src/spec/PointerInputSpec.ts b/src/spec/PointerInputSpec.ts index d48051c2..a5778973 100644 --- a/src/spec/PointerInputSpec.ts +++ b/src/spec/PointerInputSpec.ts @@ -116,13 +116,13 @@ describe('A pointer', () => { }); it('should not throw when checking if actors are under pointer if no pointer events have happened yet', () => { - const actor = new ex.Actor({ x: 50, y: 50, width: 100, height: 100 }); + const actor = new ex.Actor({ pos: new ex.Vector(50, 50), width: 100, height: 100 }); expect(() => engine.input.pointers.primary.isActorUnderPointer(actor)).not.toThrowError(); expect(engine.input.pointers.primary.isActorUnderPointer(actor)).toBe(false); }); it('should return true when an actor is under the pointer', () => { - const actor = new ex.Actor({ x: 50, y: 50, width: 100, height: 100 }); + const actor = new ex.Actor({ pos: new ex.Vector(50, 50), width: 100, height: 100 }); executeMouseEvent('pointerdown', document, null, 50, 50); expect(engine.input.pointers.primary.isActorUnderPointer(actor)).toBe(true); diff --git a/src/spec/ScaleSpec.ts b/src/spec/ScaleSpec.ts index f6207d65..625848e5 100644 --- a/src/spec/ScaleSpec.ts +++ b/src/spec/ScaleSpec.ts @@ -14,7 +14,7 @@ describe('A scaled and rotated actor', () => { actor = new ex.UIActor(50, 50, 100, 50); actor.color = ex.Color.Blue; - actor.collisionType = ex.CollisionType.Active; + actor.body.collider.type = ex.CollisionType.Active; engine = TestUtils.engine({ width: 800, height: 600 }); engine.setAntialiasing(false); diff --git a/src/spec/SceneSpec.ts b/src/spec/SceneSpec.ts index 6bc2d3c3..b72f7ba4 100644 --- a/src/spec/SceneSpec.ts +++ b/src/spec/SceneSpec.ts @@ -50,8 +50,8 @@ describe('A scene', () => { actor.traits.push(new ex.Traits.OffscreenCulling()); actor.pos.x = 0; actor.pos.y = 0; - actor.setWidth(10); - actor.setHeight(10); + actor.width = 10; + actor.height = 10; scene.add(actor); scene.update(engine, 100); @@ -66,8 +66,8 @@ describe('A scene', () => { actor.traits.push(new ex.Traits.OffscreenCulling()); actor.pos.x = -4; actor.pos.y = 0; - actor.setWidth(10); - actor.setHeight(10); + actor.width = 10; + actor.height = 10; scene.add(actor); scene.update(engine, 100); @@ -81,8 +81,8 @@ describe('A scene', () => { actor.traits.push(new ex.Traits.OffscreenCulling()); actor.pos.x = -6; actor.pos.y = 0; - actor.setWidth(10); - actor.setHeight(10); + actor.width = 10; + actor.height = 10; scene.add(actor); scene.update(engine, 100); @@ -97,8 +97,8 @@ describe('A scene', () => { actor.traits.push(new ex.Traits.OffscreenCulling()); actor.pos.x = 0; actor.pos.y = -4; - actor.setWidth(10); - actor.setHeight(10); + actor.width = 10; + actor.height = 10; scene.add(actor); scene.update(engine, 100); @@ -113,8 +113,8 @@ describe('A scene', () => { actor.traits.push(new ex.Traits.OffscreenCulling()); actor.pos.x = 0; actor.pos.y = -6; - actor.setWidth(10); - actor.setHeight(10); + actor.width = 10; + actor.height = 10; scene.add(actor); scene.update(engine, 100); @@ -129,8 +129,8 @@ describe('A scene', () => { actor.traits.push(new ex.Traits.OffscreenCulling()); actor.pos.x = 104; actor.pos.y = 0; - actor.setWidth(10); - actor.setHeight(10); + actor.width = 10; + actor.height = 10; scene.add(actor); scene.update(engine, 100); @@ -145,8 +145,8 @@ describe('A scene', () => { actor.traits.push(new ex.Traits.OffscreenCulling()); actor.pos.x = 106; actor.pos.y = 0; - actor.setWidth(10); - actor.setHeight(10); + actor.width = 10; + actor.height = 10; scene.add(actor); scene.update(engine, 100); @@ -161,8 +161,8 @@ describe('A scene', () => { actor.traits.push(new ex.Traits.OffscreenCulling()); actor.pos.x = 0; actor.pos.y = 104; - actor.setWidth(10); - actor.setHeight(10); + actor.width = 10; + actor.height = 10; scene.add(actor); scene.update(engine, 100); @@ -177,8 +177,8 @@ describe('A scene', () => { actor.traits.push(new ex.Traits.OffscreenCulling()); actor.pos.x = 0; actor.pos.y = 106; - actor.setWidth(10); - actor.setHeight(10); + actor.width = 10; + actor.height = 10; scene.add(actor); scene.update(engine, 100); @@ -196,8 +196,8 @@ describe('A scene', () => { actor.pos.x = 1010; actor.pos.y = 1010; - actor.setWidth(5); - actor.setHeight(5); + actor.width = 5; + actor.height = 5; scene.add(actor); scene.update(engine, 100); diff --git a/src/spec/TimescalingSpec.ts b/src/spec/TimescalingSpec.ts index 2a732bec..30595232 100644 --- a/src/spec/TimescalingSpec.ts +++ b/src/spec/TimescalingSpec.ts @@ -33,7 +33,7 @@ describe('The engine', () => { // actor moves twice as fast loop.advance(1100); - expect(actor.x).toBe(10, 'actor did not move twice as fast'); + expect(actor.pos.x).toBe(10, 'actor did not move twice as fast'); }); it('should run at 1/2 speed when timescale is 0.5', () => { @@ -47,6 +47,6 @@ describe('The engine', () => { // actor moves twice as slow loop.advance(2000); - expect(actor.x).toBeCloseTo(5, 0.2, 'actor did not move twice as slow'); + expect(actor.pos.x).toBeCloseTo(5, 0.2, 'actor did not move twice as slow'); }); }); diff --git a/src/spec/TriggerSpec.ts b/src/spec/TriggerSpec.ts index 7d2bfaa1..dbdf1f04 100644 --- a/src/spec/TriggerSpec.ts +++ b/src/spec/TriggerSpec.ts @@ -35,7 +35,7 @@ describe('A Trigger', () => { repeat: 1 }); const actor = new ex.Actor(0, 0, 10, 10); - actor.collisionType = ex.CollisionType.Active; + actor.body.collider.type = ex.CollisionType.Active; actor.vel.y = 10; engine.currentScene.add(trigger); engine.currentScene.add(actor); @@ -70,7 +70,7 @@ describe('A Trigger', () => { repeat: 3 }); const actor = new ex.Actor(0, 0, 10, 10); - actor.collisionType = ex.CollisionType.Active; + actor.body.collider.type = ex.CollisionType.Active; actor.vel.y = 10; engine.currentScene.add(trigger); engine.currentScene.add(actor); @@ -106,10 +106,10 @@ describe('A Trigger', () => { height: 100 }); - trigger.collisionType = ex.CollisionType.Passive; + trigger.body.collider.type = ex.CollisionType.Passive; const actor = new ex.Actor(0, 0, 10, 10); - actor.collisionType = ex.CollisionType.Active; + actor.body.collider.type = ex.CollisionType.Active; actor.vel.y = 10; trigger.on('collisionstart', (evt: ex.EnterTriggerEvent) => { @@ -139,7 +139,7 @@ describe('A Trigger', () => { }); const actor = new ex.Actor(0, 0, 10, 10); - actor.collisionType = ex.CollisionType.Active; + actor.body.collider.type = ex.CollisionType.Active; actor.vel.y = 10; engine.add(trigger); @@ -236,7 +236,7 @@ describe('A Trigger', () => { }); const actor = new ex.Actor(0, 100, 10, 10); - actor.collisionType = ex.CollisionType.Active; + actor.body.collider.type = ex.CollisionType.Active; engine.add(trigger); engine.add(actor); diff --git a/src/spec/UIActorSpec.ts b/src/spec/UIActorSpec.ts index 373cffbd..3b645e23 100644 --- a/src/spec/UIActorSpec.ts +++ b/src/spec/UIActorSpec.ts @@ -13,13 +13,12 @@ describe('A UIActor', () => { jasmine.addMatchers(ExcaliburMatchers); uiActor = new ex.UIActor({ - x: 50, - y: 50, + pos: new ex.Vector(50, 50), width: 100, height: 50, - color: ex.Color.Blue, - collisionType: ex.CollisionType.Active + color: ex.Color.Blue }); + uiActor.body.collider.type = ex.CollisionType.Active; engine = TestUtils.engine(); scene = new ex.Scene(engine); diff --git a/src/spec/images/CollisionShapeSpec/circle.png b/src/spec/images/CollisionShapeSpec/circle.png new file mode 100644 index 0000000000000000000000000000000000000000..1d0ef8daa6d8fa894b875a51f940744533047361 GIT binary patch literal 6811 zcmeAS@N?(olHy`uVBq!ia0y~yVEh8Y9Bd2>45zQ%?_yw(O7e7Z45^5Fd)K!kk5!iK z!}BFmTO*x1zw8w-7BJ%2Vz%`%myv-c=Sq&1v0H;yu2~Tfes!J3uZS7aAVq{|B5HNU= z2R491ghS#>laMP!AKP7CkU-biOGX z%$xb-t>-eu{VFG4{{HbmegEsjp6@(9HLGk_Kg_IMCnxU)$^pBX*zRh+aZISGo%DKg z%%1l0xTmqp3je1V{VAHt|IHAT113n6gn!^+dThDdxh}HrU)V8~`@Ww#Q-17?0;TnY zdM38o`nc#%_a1APx0%X&6up||Y(MYGYfrn$Jm1&-+`3l=lu@-4E{I3p4Yd7X@mKoi z_Q`V!L)LFNe*SmBB<;x$x7qI7U#{%Xa3G9Z;>+4~|9^aVTo%+ z|JwSx_3{t?atJ7FSj)s#8$G+;>)ppKD))Upy_;EdXUdaRDHSQp&OhG>Qc%d)yze8I z*UT4xhO{@3McKB|A-Jt@Xu zy85ldEB0$j%Eg}eT;;>W!m-Bpz=HL?RekyJy>cQZK!6beF^*#53aN&j1Uyj<0C+LOr5 zb!UX_@6?z7x2^rPy*?ilv<8dw{ezC(VmT(R@-unT^fhl)`=7P`oA%^R%8Y&SccmGb zShmF-n15mWJC>P;f|yXJV_3kMXX1Uiho)!{T4H&t|vnG?vu6`Ng@S zmWhQUC+NU}{Gj^W^?#3qZ~L!&viRD!Nv~@c9xvOMbw7O1`ssI93o00>Gd9~TH|u*7 zdF-Y7>bo^5^Xug1|L98Dkt)~!ND~xOb&SpX>OS-Cd1S$RH>Q5_t;EV2ZOMD}5%bSH zS9EANaEXQI*CnawmzBraC(qeAKGgZ8aI0Sop#ZERbLKoX7rl#l?5| zA+sNDmi?u?*HE&q?#2A8>M9Nm2bfrReldAj-B9k6_x;q`T-$fF=F9O(54*c7qxiiV z7#f{L3|_Q%Sf2Mc^<(}%vC{7LLi?%mtko};*eNnHc?%l6@XpM7`ut9pe#h_qefmAo zKmBSH7@54q4PJQXe^_$f{-4Fk+5W%sWZ!?7oKoTKE&}$HJ!A7e`}M5*zW=zbvOe-b z^ZHLG_ItN|4*TgK@7%!9xRhJs%Tn`-OV8#1&wSFVUc0@oyuv^-?uwu*7ZVG|8~+0f z-k;BIe4l$kJYtr+nnS~ZeXKmc_MM#ejq%^9+RWa{I8e$|O1Kan7=7jVk3~CNuRcxW z6j0c(sKGH_FwatcTj>35Wjd0};$0qt6N!4lh1c5mSIO&qUYata&S;)FDE8(yIIdq` zvq$aZ*RO`^%^I;CqKM@`|A2FpadoOLH*0x9W85r9I}WP*e<$VM}?6|UC7`?bfNU) zFH`TEPj}y|B$=mqUPF$NiN#MN;llPu7RQXs{@?m*xwKIJ_TSgv>Zd*v?q*%#HxrKe@s2 zzHsnq>(IKwIr?qcmj0F;0u~b*9PI_}U$U#HDV=1mVwsxhU)S`!*5qX6$q&mX*(3=l zI3%PpHt#dK8)RSbr)Uzt?$?+7)p_=>wl487?h|;=DEy&_9Hlpi5t9#u4H=5KDj3M;YEG#w)grr&BxUp8V)Svl=$MjIOdkR z)bjYGqOX_cRei6__i1}M{T#UI(iqFwyl?Ar1HZfS-bFtTaqQdv`OM)R8=imL!NMV6 z5yaSBw`5Q8|0j>Xynj)yx-;RO|E}LP`@M=1jeO$oz5~T8%RH5Y3)}z69GkBGGjdY+ znK_Tnd+K?$?LYI!(iRlRW=aVc%n#XZ7nhws{mE+2d)C|4Z`aP7Jt<}?Pyd-OKW)Sq znOMy95-ym>9h@VV@TX$Z`ExI>zw-OzR%|8{+FNc z)!(VVyub6miDaHVYPAWf(HcfuZ!DwjVu#UWz%-f+K;6pGzT9YOFj^Xb#y~~~OGYb& z(Mka{)Hm8h9Bmtnwhb6YD}~VxBmE{)KG@$-c>HyJ(eft@3=9mOu6{1-oD!M45zQ%?_yw(iSTrB45^5Fd-wDtmFL8<@!Uw!6Am#J+t(a9%zP|)?<0Br{{jjQ4GfG-k6Hgo)H91iI4p5Z z?*zdTjSP&A{1S__!5l`%?`#|(mP13s0(PE@qF_$Lf@)?K5KFIPq$?EQ}jc~Xk&$eBbk*S%LrS`*za~8W+%r{Km zuHx_@n9=cn$c69x938)yPi#|gNZ8S^fM5QB<#F+^#c6Msv2g5BR(SD##m{A)4p(MB z$c6b*#r@Cfh85NPEF1zqW;QIS7H4L4seN!kW+p@9QKqJS5;s1rW^t*Vx~7y{V!(>{I0|yU=i+c;r!g%7iV2-7c?u{@&{TLa zv$%BieXfpO>`o01^BNXBKYOKDkF(>Ks+zr$Lqbo(f_}-V&DXd)s`joF(+2!F;mk&jqPW@i3@!FoBpJQh2xKp!-c)vO#cN0O8c7_8Y`Ka?DEt6 z{&X~~us@U|%*1kvg=KHgC8tWJMf1#q&6Or-E4OY+I2XHe2t=qUyf~eqJ4apNRi$yMGLt1!(>!Bu^GX+o zEAolkI2oC?v$Fg(o5|170jezz)G;;fyYDk6qorX*zrUi?VnW}ufJA(HII>zshWf1mt1i{*F~m9pg8tdaB;Zc%=S|1F4LlWwv4}p zEn*!m_}4SOi`P(i)jDIElEZ^#4GZqGd@g3%FD_83-@?!+$LRRJaC`Qah86Dp*Pn9a zXeqpy&3A0$GVYFFUpTGo8#9@j>~fRNS$jBKVdp&^$k`(<@a3a&N#kBtm)iF~Eo2*I z7#%^`Ek|47)$P7x2RT?cIjSxg#HOY*2Hvl?r>*d+IQ{t(mTzJLU-mwKTr!bqk(_1NbfpVA3NOwwZ}_{CX;Iy)CwFHb&|-9q zSIv%Py3f_|Yr9LGL&JeoM#ud=Pup3-I68iDg=Pyu%9>r)`DzNUX7e5?7#-IaZsBTVXL0$< zm=Y}@p{?*D+5P#0bgqtHGmFc!neKCQ{QB-z=a^v37F^! zx8OVX#baWNBx9`73{p1Lpkn<34FPoVyCcSbHj@F zHnQo;8-g4zgujZK?aL1mW_c-O?Fu^eBN+Z2D7r%zIrle_JLH!KyW;PvTAsii5t&2=K-e@Siu=YDTlS4;T;Oo*oV(J%E z6khm#*rs|xTjAB-e`V?c`U)?;E(=pJaB;ZO&U4C;Ge$_@i?>gmi$bWwm0I4x zb3S{{A}1*D#eGJulY+0qmCI_c>^`%y)c)hS+u$&{VTJj@+tUvCv$EVhtl;o~nKAG` z!};YbUaTy8r(8~A@e>jFD!lyNx!;@|zv_$&m6)KFj-`*og~M#GtXO`rxa>WD?;J~< zzTh@_sQOfLx7VSkVa4&DORm#6Ier;MaSJO1Xe+$hIxoyySm4W+&RzVC8H|Dba_djm zswljW|M+tTL*sK+m%o+8)_wv4UpRw{+?b%n)PvOx3#x@#ICw+_zAkJoe&fp2w2$N3 zCKe7GMTJ-X8n=_%8Wx6Bq;e<<_mP6%;7(Z(?XHU<~A! z-|+e;H>gMWK!!2UKK?2u5;7mgm*{1 zuVr+szkhBzs5!hhZqYl*H;M``s?QwM8BfsR5YypJ}x(cuE3R~3H zgKG?XMy6J#MR6UOD`zt`)d` zuH=xgt6_!z{0Cb;bN|}XX{pc1)Xd^idtt*d`Opi;`!BF^2;7Kvxbl6T|GAeew&~OI zoEi>nV_LNDZAKHH+6%|SC8A6$Wnuzf=PsXIVCLvQW0AFh0;H_ZdSb|0`{ahqEQZFN zOpENw%c|$cT=+h(|FWQhfTqH$iOIS9razOWykc6BX!4h;_58djvA-~O(wDK0%z zm5GHzM@ZnS>xRJ7Tvb;rV%r!RV;BR^&&@r1mSyj;i&~(#b8xsK?;y9k6;g&N6fA34 zG2in0`+VIO_oaWbGBUB42@8BZ%eLugAxmwyp#=-5H^bHOD|X|T``lH(TJH8kGvvFs zt2uw=8p#VNI3%bu2HwwcZ#CKiquF@dkA zLz?R~UNpBqf5E~butQJb)%CS$|AH>0pF6t}lnmP%R_t#ydnW8CZ>+_x>d@fO+OQ&A zZYc7MxNgl92ySXW?J;m?5>+U*Dtrj1tLr=9A7v)s^-_8J=YZX zI75^PH1^f;>xm-QuO}}ptr?kEPO-Y|o%3ZOE9+myjN72F?Q2+Ze&*Hx{#q}Dmp?nh z!XXf$r|{}@XScb)m!yExf(i}_?haSf&jwvk_dOj6^3#rn750Arp0zdoQ=4<1LqMUR zuVID%jI9g&XG}5{QgB!h>u}|^`-iiPEVbV`E#(=RShfiYe9d+HIJueQ*SX7uW=t#` zIqC|pZudSp-pEpW{iKCBNR6n#S6O#uNW)4%VZ+{r74OZI9Pj5UopWwrXf$M6wD0l? zmfGc+7TkMQFWzYFCvnv6kS+S9P2yg%T=ZuNwECKiq( z93559HCVxoPY!_&VS%sfCUR9x-&4yepdg^F@Je>E=oh}yY9R#&g*b;R^W`=!u-DDA zXJX-yQC4{6-xqaZ{*`SYC-w;od|mFQ_rl-vb~^(j({ZkjU&4#Uzo<`n>)621_?XqD z_N%bKm#c@ase?Mh@=S~NebVIktfT6@L1P^J+#SC{3mo@rJuL@GUrViw%eD*0I@Ukm`vdG-GcgO}S55mqPA)M4g(BaWvEGaM`vf5t3pBo2!ToDO`imK$ zfC^BYAYJOXzC!+*0!XbR)678eFOL2f0yzZ~92R&f=)Y=;`)alZ943cZHZ9V7ad_VR z$3hAY4Gv2k{I0O>{S+I~4T>KQ6HbmQ;XSz^eGLvv8y19TMU6^~1_>ykj3x#qmeKq% fG)sm9`x)!ELgTe~DWM4f8|-=d literal 0 HcmV?d00001 diff --git a/src/spec/images/CollisionShapeSpec/triangle.png b/src/spec/images/CollisionShapeSpec/triangle.png new file mode 100644 index 0000000000000000000000000000000000000000..ce234eff9af4a25ab55dd9c3021c2dc437d2e91d GIT binary patch literal 7598 zcmeAS@N?(olHy`uVBq!ia0y~yVEh8Y9Bd2>45zQ%?_yw(E%kJ945^5Fd*|)MC&#w3 z1gznl#QUjpQR||bNxYLdLkc_te;aO|Ya7e3Y1O&kr>$RfPfker(Dta->W_TmL#F-z z{(g6_`<41*vF!u~MkbbfB0r9_9*Ag+|6e3?eE+|SXZQOaU#>r9cmFbnfP%vVUB=dT zyCrl!yw3k0`tPzl|KI0@HNPJ3zsCN*=eVGPL&Je^Ca$~eh5-*guly^2-{wR5^6!mD z@!Wgt zn$^i4F zM2y2ieYp=muK)h9^6typ8i{!x4GfL;tRlPmA9^-U2PyQ=l2&(UaL`(CAy~8ccp;DF zR_ndLr*Q}<6f`wB*)x`&b?39*YQ1;;&&CRECYC(0fEVWM2ItT7+itbqdtP{tFQ>o^ z7l(!ZLRa=?&nwC*ui9@Ovfr=aKoS#I?M8>=`_E-AFI)coV`8NeBa<|@#+Tm1=i~lf z3Eq9VTC7QviG{;PB;W;qXPMlNCs*Em+4ouf*Ao@k^I|E;KuJ4JYaxXI-31p`Ki-+WGa96@{OH?qPJsv~hlTork55eB@%GBQ zFLp+5=an21tQlMP#Xjke`*95vUW+UK6f?1K>``8Dp;juMBV^94x%X-(vv3IfXlQWy z&v0v2eNZtZtUrgbaLmzJaKWE#-t&*4Ad4&QWM&H~1Sl=I;N7&Ny!RByyuI?R@#+o< zMvSd?kqM8luLT87KmToEPJxIBhlTnwdEc_Gg7oUR1ha4mScv?(CVx-_B(QdWpuRsO z>{mX0whR>X`rqGHDl)O069{;bzQXV0nUx@)1=j>mQ*n6U$Jko8{9o>V+ovnIY7h3yOXO&mU|B|hcU}Rd&sqy8iQ0))9;Q6=a-jjcp-N4Wo%PR6K z?4*8SR(aL^SF^uvV&PcA#AQ2mR$bmqkRQI>UnH*J@PLQ0wXQAesqu8Ht=4

yoD_ z!K_W)zxN5q+D-3|^9#bPon996?aI3^|1On3?B^7S@O4*SUot zDLU)aPF=7^>Sj+dWK);AOHCB{on&R_XdVWEymV3(c|vZ3$ys2 z%(=&`qRhf^Mti}9=V#ybpPhT_Fvr|`&Kk)q9A`8aTzG!wZTH%_x8`?Tc=?x8XE6)M z8Px?Bo}YR>J9q9adx80vf3sis!^z0>mx*hy{o-=7lC1v;C4TSaTRx|=aFlRse0f(> zw_$GZZ|;khd-pdy_vaLNArkOn?oOKYQ!--kUG@?a9OvCm!%(z4W{uz4k(%&T|T^5DIt^T3GW> z|MPyC;~&>Qw)hvd{Z<^e<=*#Gw{KT90J*&T!v9r!HH4ObUu3Yxso_9Bt4P&V@kdwU zM328eZLwFpElHJ$WuJJ!3)A@4;A&ryc>PCT;-q@EatQp0a#(o1M$qrAX<*I%j?2}B z7GcT`56T%^<36fdYz;s1;j!wRd+Hn~yI9J^0$yxi{{Bi`HOoyVzxOjIYcjFq2?e|` zKluND`rKRj{R;61UfOOq@@zP8l!@za(uei+=T`m=``7<>$K~J0@B9&EWK!qW_%gZi ze*K={-??Ab|J`u;cXr*sSqzPnSwwz$Zs?bPedXVc6=jEB?lqI@7Q7Meu<-t*y}`eA z-z3*xd0D$=`Eo&p4~rU{>IFaiv@g%9zqjU-{+xT-$s0u)Ygt8pJ-unYHQul89>3Mz z`RT7uvT)dFF1RqCr8@V{%D<-jPwg+OTHkXth~=Abz>Ddr@2=EIe%)&SuxT82on7Y65YEqUd7>oG-GSsN%eNr2|wSV-bZvFmv^$mL)ob3M!7i8Jb z-SL=x&OPg*4Xlm1tRlZ&UbNmi-|x<6X218gkvCcnlreGry>epiE&r!)Yj#|&PS@L} zv|&$!lf7=z@^6hxSrAN>Uv+TCAVv71wJj@@eS`I)oVa{6e4#GZr1?t#Q+&ppfOqrTum zyW6uXc2kOb+b#F5pSXCtLI6mw>ys;XJwx+VXmI*3url~}`;9*rr!)Dz-^b3x(#ECnWx3_tTl3XE zv*&YK?ES6BAt0f(;KKZ8C0X^e&dBdkfAHnqZ3Tyf84XVLU6)q=)zmxO*#OP@?Mz&M z6)*m0n{z8!tNA>K#a{cxsnZx5%UMOL{uEklJs!d~U$mgAKJ1JQBh!6OjV~`27i9I% z;MuG6;LE?RdHYoz9t6DLw`W@Zt=)yqjl*JZf4~gtzJulV zhqbNto}VyDmQ#dNPkV92?*H>WPoypO>PxPku4EABu<-x>3v+KBuaBQ+;dbC>p&l~#W6U|jxv;;ebD366}dbwB=J-S?n; z?<;M~z3XSK`L1N(knq{5JWKuqZ{?A#On&dLSN%3+y3COi^TO}#<(j2pe}fLZy!+ju z;lOMrYw=zGLwDC}rim6*%{R5I7PbgXxa?G(W&d&Zrx)2QbMCD#{HD#aOz_5r3v+KR z|7oTBOPO(b`F(aqrg+Xb5ik7S{!@)`J@B%2s>)vWMn5v-o-NK-tJEu zcpGoCm}!+P|8{$yk$ugwhReTa?waSDpwu{*>+Z_hMf{gn`Ey$AwLh9VUyI*5{lD*lmv_q@5{w&Jx$my5-JE~p^*-GPU*4To*f8@zSL?eg z`)<0wDZMXRQ1!h~;D$%SMyK+u`!8;PT{W}ea`m&6t^;3~xIy0j62Dm7!ui0