Skip to content

Commit

Permalink
feat: [excaliburjs#2401] Implement arbitrary raycasting (excaliburjs#…
Browse files Browse the repository at this point in the history
…2410)

Closes excaliburjs#2401

This PR implements an arbitrary raycasting api in Excalibur

```typescript
const hits = engine.currentScene.physics.rayCast(new ex.Ray(player.pos, ex.Vector.Down), {
  maxDistance: 100,
  collisionGroup: blockGroup,
  searchAllColliders: false
});
```

<img width="797" alt="image" src="https://user-images.githubusercontent.com/612071/178625499-d9f97052-5b18-49fe-9440-89bd93e46ff6.png">

## Changes:

- Fix issue where TileMaps weren't correctly added to the collider data structure
- Add physics world with raycasting endpoint
  • Loading branch information
eonarheim authored Sep 11, 2022
1 parent 3719bd6 commit 0aed1ae
Show file tree
Hide file tree
Showing 11 changed files with 278 additions and 16 deletions.
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,28 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Added

- Added ability to perform arbitrary ray casts into `ex.Scene`, the `ex.PhysicsWorld` can be passed a variety of options to influence the types of ray cast hits that
are returned
```typescript
const engine = new ex.Engine({...});
const enemyGroup = ex.CollisionGroupManager.create('enemy');
const ray = new ex.Ray(ex.vec(0, 0), ex.Vector.Right);
const hits = engine.currentScene.physics.rayCast(ray, {
/**
* Optionally specify to search for all colliders that intersect the ray cast, not just the first which is the default
*/
searchAllColliders: true,
/**
* Optionally specify the maximum distance in pixels to ray cast, default is Infinity
*/
maxDistance: 100,
/**
* Optionally specify a collision group to consider in the ray cast, default is All
*/
collisionGroup: enemyGroup
});

```
- Added the emitted particle transform style as part of `ex.ParticleEmitter({particleTransform: ex.ParticleTransform.Global})`, [[ParticleTransform.Global]] is the default and emits particles as if they were world space objects, useful for most effects. If set to [[ParticleTransform.Local]] particles are children of the emitter and move relative to the emitter as they would in a parent/child actor relationship.
- Added `wasButtonReleased` and `wasButtonPressed` methods to [[ex.Input.Gamepad]]
- Added `clone()` method to `ex.SpriteSheet`
Expand Down
28 changes: 22 additions & 6 deletions sandbox/src/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,9 +401,11 @@ var tileBlockWidth = 64,
tileBlockHeight = 48,
spriteTiles = new ex.SpriteSheet({sprites: [ex.Sprite.from(imageBlocks)] });

var blockGroup = ex.CollisionGroupManager.create('ground');
// create a collision map
// var tileMap = new ex.TileMap(100, 300, tileBlockWidth, tileBlockHeight, 4, 500);
var tileMap = new ex.TileMap({ pos: ex.vec(100, 300), tileWidth: tileBlockWidth, tileHeight: tileBlockHeight, rows: 4, columns: 500 });
var tileMap = new ex.TileMap({name: 'tilemap', pos: ex.vec(100, 300), tileWidth: tileBlockWidth, tileHeight: tileBlockHeight, rows: 4, columns: 500 });
tileMap.get(ex.BodyComponent).group = blockGroup;
var blocks = ex.Sprite.from(imageBlocks);
// var flipped = spriteTiles.sprites[0].clone();
// flipped.flipVertical = true;
Expand Down Expand Up @@ -453,7 +455,6 @@ enum Animations {
}

var currentX = 0;
var blockGroup = ex.CollisionGroupManager.create('ground');
var color = new ex.Color(Math.random() * 255, Math.random() * 255, Math.random() * 255);
// Create the level
for (var i = 0; i < 36; i++) {
Expand All @@ -477,24 +478,28 @@ for (var i = 0; i < 36; i++) {
var platform = new ex.Actor({x: 400, y: 300, width: 200, height: 50, color: new ex.Color(0, 200, 0)});
platform.graphics.add(new ex.Rectangle({ color: new ex.Color(0, 200, 0), width: 200, height: 50 }));
platform.body.collisionType = ex.CollisionType.Fixed;
platform.body.group = blockGroup;
platform.actions.repeatForever(ctx => ctx.moveTo(200, 300, 100).moveTo(600, 300, 100).moveTo(400, 300, 100));
game.add(platform);

var platform2 = new ex.Actor({x: 800, y: 300, width: 200, height: 20, color: new ex.Color(0, 0, 140)});
platform2.graphics.add(new ex.Rectangle({ color: new ex.Color(0, 0, 140), width: 200, height: 20 }));
platform2.body.collisionType = ex.CollisionType.Fixed;
platform2.body.group = blockGroup;
platform2.actions.repeatForever(ctx => ctx.moveTo(2000, 300, 100).moveTo(2000, 100, 100).moveTo(800, 100, 100).moveTo(800, 300, 100));
game.add(platform2);

var platform3 = new ex.Actor({x: -200, y: 400, width: 200, height: 20, color: new ex.Color(50, 0, 100)});
platform3.graphics.add(new ex.Rectangle({ color: new ex.Color(50, 0, 100), width: 200, height: 20 }));
platform3.body.collisionType = ex.CollisionType.Fixed;
platform3.body.group = blockGroup;
platform3.actions.repeatForever(ctx => ctx.moveTo(-200, 800, 300).moveTo(-200, 400, 50).delay(3000).moveTo(-200, 300, 800).moveTo(-200, 400, 800));
game.add(platform3);

var platform4 = new ex.Actor({x: 75, y: 300, width: 100, height: 50, color: ex.Color.Azure});
platform4.graphics.add(new ex.Rectangle({ color: ex.Color.Azure, width: 100, height: 50 }));
platform4.body.collisionType = ex.CollisionType.Fixed;
platform4.body.group = blockGroup;
game.add(platform4);

// Test follow api
Expand All @@ -510,6 +515,17 @@ var player = new ex.Actor({
collider: ex.Shape.Capsule(32, 96),
collisionType: ex.CollisionType.Active
});
player.onPostUpdate = (engine) => {
var hits = engine.currentScene.physics.rayCast(new ex.Ray(player.pos, ex.Vector.Down), {
maxDistance: 100,
collisionGroup: blockGroup,
searchAllColliders: false
});
console.log(hits);
}
player.graphics.onPostDraw = (ctx) => {
ctx.drawLine(ex.Vector.Zero, ex.Vector.Down.scale(100), ex.Color.Red, 2);
}
player.body.canSleep = false;
player.graphics.copyGraphics = false;
follower.actions
Expand Down Expand Up @@ -543,10 +559,10 @@ player.addChild(healthbar);
// ctx.fillStyle = 'red';
// ctx.fillRect(0, 0, 100, 100);
// };
player.graphics.onPostDraw = (ctx: ex.ExcaliburGraphicsContext) => {
// ctx.debug.drawLine(ex.vec(0, 0), ex.vec(200, 0));
// ctx.debug.drawPoint(ex.vec(0, 0), { size: 20, color: ex.Color.Black });
};
// player.graphics.onPostDraw = (ctx: ex.ExcaliburGraphicsContext) => {
// // ctx.debug.drawLine(ex.vec(0, 0), ex.vec(200, 0));
// // ctx.debug.drawPoint(ex.vec(0, 0), { size: 20, color: ex.Color.Black });
// };

var healthbar2 = new ex.Rectangle({
width: 140,
Expand Down
18 changes: 13 additions & 5 deletions src/engine/Collision/CollisionSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import { CollisionResolutionStrategy, Physics } from './Physics';
import { ArcadeSolver } from './Solver/ArcadeSolver';
import { Collider } from './Colliders/Collider';
import { CollisionContact } from './Detection/CollisionContact';
import { DynamicTreeCollisionProcessor } from './Detection/DynamicTreeCollisionProcessor';
import { RealisticSolver } from './Solver/RealisticSolver';
import { CollisionSolver } from './Solver/Solver';
import { ColliderComponent } from './ColliderComponent';
import { CompositeCollider } from './Colliders/CompositeCollider';
import { Engine, ExcaliburGraphicsContext, Scene } from '..';

import { DynamicTreeCollisionProcessor } from './Detection/DynamicTreeCollisionProcessor';
import { PhysicsWorld } from './PhysicsWorld';
export class CollisionSystem extends System<TransformComponent | MotionComponent | ColliderComponent> {
public readonly types = ['ex.transform', 'ex.motion', 'ex.collider'] as const;
public systemType = SystemType.Update;
Expand All @@ -22,12 +22,19 @@ export class CollisionSystem extends System<TransformComponent | MotionComponent
private _engine: Engine;
private _realisticSolver = new RealisticSolver();
private _arcadeSolver = new ArcadeSolver();
private _processor = new DynamicTreeCollisionProcessor();
private _lastFrameContacts = new Map<string, CollisionContact>();
private _currentFrameContacts = new Map<string, CollisionContact>();
private _processor: DynamicTreeCollisionProcessor;

private _trackCollider = (c: Collider) => this._processor.track(c);
private _untrackCollider = (c: Collider) => this._processor.untrack(c);
private _trackCollider: (c: Collider) => void;
private _untrackCollider: (c: Collider) => void;

constructor(physics: PhysicsWorld) {
super();
this._processor = physics.collisionProcessor;
this._trackCollider = (c: Collider) => this._processor.track(c);
this._untrackCollider = (c: Collider) => this._processor.untrack(c);
}

notify(message: AddedEntity | RemovedEntity) {
if (isAddedSystemEntity(message)) {
Expand All @@ -49,6 +56,7 @@ export class CollisionSystem extends System<TransformComponent | MotionComponent

initialize(scene: Scene) {
this._engine = scene.engine;

}

update(entities: Entity[], elapsedMs: number): void {
Expand Down
68 changes: 67 additions & 1 deletion src/engine/Collision/Detection/DynamicTreeCollisionProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,42 @@ import { Collider } from '../Colliders/Collider';
import { CollisionContact } from '../Detection/CollisionContact';
import { BodyComponent } from '../BodyComponent';
import { CompositeCollider } from '../Colliders/CompositeCollider';
import { ExcaliburGraphicsContext } from '../..';
import { CollisionGroup } from '../Group/CollisionGroup';
import { ExcaliburGraphicsContext } from '../../Graphics/Context/ExcaliburGraphicsContext';

export interface RayCastHit {
/**
* The distance along the ray cast in pixels that a hit was detected
*/
distance: number;
/**
* Reference to the collider that was hit
*/
collider: Collider;
/**
* Reference to the body that was hit
*/
body: BodyComponent;
/**
* World space point of the hit
*/
point: Vector;
}

export interface RayCastOptions {
/**
* Optionally specify the maximum distance in pixels to ray cast, default is Infinity
*/
maxDistance?: number;
/**
* Optionally specify a collision group to consider in the ray cast, default is All
*/
collisionGroup?: CollisionGroup;
/**
* Optionally specify to search for all colliders that intersect the ray cast, not just the first which is the default
*/
searchAllColliders?: boolean;
}

/**
* Responsible for performing the collision broadphase (locating potential collisions) and
Expand All @@ -29,6 +64,37 @@ export class DynamicTreeCollisionProcessor implements CollisionProcessor {
return this._colliders;
}

public rayCast(ray: Ray, options?: RayCastOptions): RayCastHit[] {
const results: RayCastHit[] = [];
const maxDistance = options?.maxDistance ?? Infinity;
const collisionGroup = options?.collisionGroup ?? CollisionGroup.All;
const searchAllColliders = options?.searchAllColliders ?? false;
this._dynamicCollisionTree.rayCastQuery(ray, maxDistance, (collider) => {
const owner = collider.owner;
const maybeBody = owner.get(BodyComponent);
// Early exit if not the right group
if (collisionGroup.mask !== CollisionGroup.All.mask && maybeBody?.group?.mask !== collisionGroup.mask) {
return false;
}

const hit = collider.rayCast(ray, maxDistance);
if (hit) {
results.push({
distance: hit.sub(ray.pos).distance(),
point: hit,
collider: collider,
body: maybeBody
});
if (!searchAllColliders) {
// returning true exits the search
return true;
}
}
return false;
});
return results;
}

/**
* Tracks a physics body for collisions
*/
Expand Down
1 change: 1 addition & 0 deletions src/engine/Collision/Index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ export * from './Solver/Solver';
export * from './CollisionSystem';
export * from './MotionSystem';

export * from './PhysicsWorld';
export * from './Physics';
export * from './Side';
14 changes: 14 additions & 0 deletions src/engine/Collision/PhysicsWorld.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Ray } from '../Math/ray';
import { DynamicTreeCollisionProcessor, RayCastHit, RayCastOptions } from './Index';


export class PhysicsWorld {
public collisionProcessor: DynamicTreeCollisionProcessor;
constructor() {
this.collisionProcessor = new DynamicTreeCollisionProcessor();
}

public rayCast(ray: Ray, options?: RayCastOptions): RayCastHit[] {
return this.collisionProcessor.rayCast(ray, options);
}
}
4 changes: 3 additions & 1 deletion src/engine/EntityComponentSystem/SystemManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Scene, Util } from '..';
import { World } from './World';

export interface SystemCtor<T extends System> {
new (): T;
new (...args: any[]): T;
}

/**
Expand Down Expand Up @@ -41,6 +41,8 @@ export class SystemManager<ContextType> {
this.systems.push(system);
this.systems.sort((a, b) => a.priority - b.priority);
query.register(system);
// If systems are added and the manager has already been init'd
// then immediately init the system
if (this.initialized && system.initialize) {
system.initialize(this._world.context);
}
Expand Down
11 changes: 10 additions & 1 deletion src/engine/Scene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { ActionsSystem } from './Actions/ActionsSystem';
import { IsometricEntitySystem } from './TileMap/IsometricEntitySystem';
import { OffscreenSystem } from './Graphics/OffscreenSystem';
import { ExcaliburGraphicsContext } from './Graphics';
import { PhysicsWorld } from './Collision/PhysicsWorld';
/**
* [[Actor|Actors]] are composed together into groupings called Scenes in
* Excalibur. The metaphor models the same idea behind real world
Expand All @@ -55,6 +56,14 @@ export class Scene<TActivationData = unknown>
*/
public world = new World(this);

/**
* The Excalibur physics world for the scene. Used to interact
* with colliders included in the scene.
*
* Can be used to perform scene ray casts, track colliders, broadphase, and narrowphase.
*/
public physics = new PhysicsWorld();

/**
* The actors in the current scene
*/
Expand Down Expand Up @@ -108,7 +117,7 @@ export class Scene<TActivationData = unknown>
// Update
this.world.add(new ActionsSystem());
this.world.add(new MotionSystem());
this.world.add(new CollisionSystem());
this.world.add(new CollisionSystem(this.physics));
this.world.add(new PointerSystem());
this.world.add(new IsometricEntitySystem());
// Draw
Expand Down
3 changes: 3 additions & 0 deletions src/engine/TileMap/TileMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ export class TileMap extends Entity {
* Tiles colliders based on the solid tiles in the tilemap.
*/
private _updateColliders(): void {
this._collider.$colliderRemoved.notifyAll(this._composite);
this._composite.clearColliders();
const colliders: BoundingBox[] = [];
this._composite = this._collider.useCompositeCollider([]);
Expand Down Expand Up @@ -301,6 +302,8 @@ export class TileMap extends Entity {
this._composite.addCollider(collider);
}
this._collider.update();
// Notify that colliders have been updated
this._collider.$colliderAdded.notifyAll(this._composite);
}

/**
Expand Down
5 changes: 3 additions & 2 deletions src/spec/ActorSpec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as ex from '@excalibur';
import { ExcaliburAsyncMatchers, ExcaliburMatchers } from 'excalibur-jasmine';
import { PhysicsWorld } from '../engine/Collision/PhysicsWorld';
import { TestUtils } from './util/TestUtils';

describe('A game actor', () => {
Expand All @@ -21,7 +22,7 @@ describe('A game actor', () => {
actor = new ex.Actor({name: 'Default'});
actor.body.collisionType = ex.CollisionType.Active;
motionSystem = new ex.MotionSystem();
collisionSystem = new ex.CollisionSystem();
collisionSystem = new ex.CollisionSystem(new PhysicsWorld());
actionSystem = new ex.ActionsSystem();
scene = new ex.Scene();
scene.add(actor);
Expand Down Expand Up @@ -584,7 +585,7 @@ describe('A game actor', () => {
actor = new ex.Actor();
actor.body.collisionType = ex.CollisionType.Active;
motionSystem = new ex.MotionSystem();
collisionSystem = new ex.CollisionSystem();
collisionSystem = new ex.CollisionSystem(new PhysicsWorld());
scene = new ex.Scene();
scene.add(actor);
engine.addScene('test', scene);
Expand Down
Loading

0 comments on commit 0aed1ae

Please sign in to comment.