diff --git a/CHANGELOG.md b/CHANGELOG.md index 35692a1f1..f44055a67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,11 +15,18 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added +- Added ability to apply draw offset to `ex.IsometricMap` and `ex.Tilemap` +- Added `visibility` and `opacity` to `ex.IsometricMap` +- Added base elevation for `ex.IsometricMap` so multiple maps can sort correctly +- Added method to suppress convex polygon warning for library code usage +- Added more configuration options to debug draw flags, including isometric map controls - Added `actionstart` and `actioncomplete` events to the Actor that are fired when an action starts and completes ### Fixed +- Fixed infinite loop :bomb: when certain degenerate polygons were attempted to be triangulated! +- Fixed incorrect type on `ex.Tilemap.getTileByPoint()` - Fixed TS type on `GraphicsComponent` and allow `.material` to be null to unset, current workaround is using `.material = null as any` ### Updates @@ -28,7 +35,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Changed -- +- Tweaked debug draw to be less noisy by default +- Removed dependency on `ex.IsometricMap` in the `ex.IsometricEntityComponent`, this allows for greater flexibility when using the component when a map may not be known or constructed. diff --git a/sandbox/tests/polygon/index.html b/sandbox/tests/polygon/index.html new file mode 100644 index 000000000..f96a40eba --- /dev/null +++ b/sandbox/tests/polygon/index.html @@ -0,0 +1,12 @@ + + + + + + Polygon Triangulate + + + + + + \ No newline at end of file diff --git a/sandbox/tests/polygon/index.ts b/sandbox/tests/polygon/index.ts new file mode 100644 index 000000000..854e3ccdf --- /dev/null +++ b/sandbox/tests/polygon/index.ts @@ -0,0 +1,95 @@ +/// + + +var game = new ex.Engine({ + width: 800, + height: 600, + displayMode: ex.DisplayMode.FitScreenAndFill +}); +game.toggleDebug(); +game.debug.collider.showBounds = false; + +var actor = new ex.Actor({pos: ex.vec(200, 100)}); +// var shape = ex.Shape.Polygon([ +// ex.vec(0,0), +// ex.vec(-1.6875,-25.0625), +// ex.vec(6.3125,-25.0625), +// ex.vec(5.625,-33.125), +// ex.vec(21.5625,-32.8125), +// ex.vec(21.4375,-40.6875), +// ex.vec(29.1875,-41.9375), +// ex.vec(32.8125,-38.3125), +// ex.vec(33.0625,-21.8125), +// ex.vec(25.8125,-21.5625), +// ex.vec(24.125,10.375), +// ex.vec(-8.75,8.5), +// ex.vec(-11.8125,5.5625), +// ex.vec(-10.9375,-0.5625), +// ex.vec(-3.3125,-2.4375) +// ]); + +// var points = [ +// ex.vec(0,0), +// ex.vec(-1.6875,-25.0625), +// ex.vec(6.3125,-25.0625), +// ex.vec(5.625,-33.125), +// ex.vec(21.5625,-32.8125), +// ex.vec(21.4375,-40.6875), +// ex.vec(29.1875,-41.9375), +// ex.vec(32.8125,-38.3125), +// ex.vec(33.0625,-21.8125), +// ex.vec(25.8125,-21.5625), +// ex.vec(24.125,10.375), +// ex.vec(-8.75,8.5), +// ex.vec(-11.8125,5.5625), +// ex.vec(-10.9375,-0.5625), +// ex.vec(-3.3125,-2.4375) +// ] + +// var points = [ +// ex.vec(200,100), +// ex.vec(300,320), +// ex.vec(400,100), +// ex.vec(500,300), +// ex.vec(350,300), +// ex.vec(300,500), +// ex.vec(250,300), +// ex.vec(100,300)]; + +var points = [ + ex.vec(343,392), + ex.vec(475,103), + ex.vec(245,151), + ex.vec(193,323), + ex.vec(91, 279), + ex.vec(51, 301), + ex.vec(25, 381), + ex.vec(80, 334), + ex.vec(142,418), + ex.vec(325,480), + ex.vec(340,564), + ex.vec(468,597) +] + +var colinear = [ + ex.vec(160, 80), + ex.vec(80, 40), + ex.vec(0, 0), +]; + +function triangleArea(a: ex.Vector, b: ex.Vector, c: ex.Vector) { + return Math.abs((a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - c.y)))/2 +} + +console.log('triangle area', triangleArea(ex.vec(160, 80), +ex.vec(80, 40), +ex.vec(0, 0))); + +var shape = ex.Shape.Polygon(points, ex.Vector.Zero, true); + +var triangulated = shape.triangulate(); +actor.collider.set(triangulated); +game.add(actor); + + +game.start(); \ No newline at end of file diff --git a/src/engine/Collision/Colliders/CircleCollider.ts b/src/engine/Collision/Colliders/CircleCollider.ts index 7ec9b6458..bd1f5bcd0 100644 --- a/src/engine/Collision/Colliders/CircleCollider.ts +++ b/src/engine/Collision/Colliders/CircleCollider.ts @@ -253,7 +253,8 @@ export class CircleCollider extends Collider { return new Projection(Math.min.apply(Math, scalars), Math.max.apply(Math, scalars)); } - public debug(ex: ExcaliburGraphicsContext, color: Color) { + public debug(ex: ExcaliburGraphicsContext, color: Color, options?: { lineWidth: number }) { + const { lineWidth } = { ...{ lineWidth: 1 }, ...options }; const tx = this._transform; const scale = tx?.globalScale ?? Vector.One; const rotation = tx?.globalRotation ?? 0; @@ -262,7 +263,7 @@ export class CircleCollider extends Collider { ex.translate(pos.x, pos.y); ex.rotate(rotation); ex.scale(scale.x, scale.y); - ex.drawCircle((this.offset ?? Vector.Zero), this._naturalRadius, Color.Transparent, color, 2); + ex.drawCircle((this.offset ?? Vector.Zero), this._naturalRadius, Color.Transparent, color, lineWidth); ex.restore(); } } diff --git a/src/engine/Collision/Colliders/Collider.ts b/src/engine/Collision/Colliders/Collider.ts index 7b5c1e826..726b4518b 100644 --- a/src/engine/Collision/Colliders/Collider.ts +++ b/src/engine/Collision/Colliders/Collider.ts @@ -113,7 +113,7 @@ export abstract class Collider implements Clonable { abstract update(transform: Transform): void; - abstract debug(ex: ExcaliburGraphicsContext, color: Color): void; + abstract debug(ex: ExcaliburGraphicsContext, color: Color, options?: { lineWidth: number, pointSize: number }): void; abstract clone(): Collider; } diff --git a/src/engine/Collision/Colliders/CompositeCollider.ts b/src/engine/Collision/Colliders/CompositeCollider.ts index 6ed7b7d6e..c8701dd65 100644 --- a/src/engine/Collision/Colliders/CompositeCollider.ts +++ b/src/engine/Collision/Colliders/CompositeCollider.ts @@ -236,10 +236,10 @@ export class CompositeCollider extends Collider { } } - public debug(ex: ExcaliburGraphicsContext, color: Color) { + public debug(ex: ExcaliburGraphicsContext, color: Color, options?: { lineWidth: number, pointSize: number }) { const colliders = this.getColliders(); for (const collider of colliders) { - collider.debug(ex, color); + collider.debug(ex, color, options); } } diff --git a/src/engine/Collision/Colliders/PolygonCollider.ts b/src/engine/Collision/Colliders/PolygonCollider.ts index 26959e044..a9a12f22c 100644 --- a/src/engine/Collision/Colliders/PolygonCollider.ts +++ b/src/engine/Collision/Colliders/PolygonCollider.ts @@ -11,7 +11,7 @@ import { AffineMatrix } from '../../Math/affine-matrix'; import { Ray } from '../../Math/ray'; import { ClosestLineJumpTable } from './ClosestLineJumpTable'; import { Collider } from './Collider'; -import { ExcaliburGraphicsContext, Logger, range } from '../..'; +import { ExcaliburGraphicsContext, Logger } from '../..'; import { CompositeCollider } from './CompositeCollider'; import { Shape } from './Shape'; import { Transform } from '../../Math/transform'; @@ -25,6 +25,11 @@ export interface PolygonColliderOptions { * Points in the polygon in order around the perimeter in local coordinates. These are relative from the body transform position. */ points: Vector[]; + + /** + * Suppresses convexity warning + */ + suppressConvexWarning?: boolean; } /** @@ -72,10 +77,13 @@ export class PolygonCollider extends Collider { if (!counterClockwise) { this.points.reverse(); } + if (!this.isConvex()) { - this._logger.warn( - 'Excalibur only supports convex polygon colliders and will not behave properly.'+ - 'Call PolygonCollider.triangulate() to build a new collider composed of smaller convex triangles'); + if (!options.suppressConvexWarning) { + this._logger.warn( + 'Excalibur only supports convex polygon colliders and will not behave properly.' + + 'Call PolygonCollider.triangulate() to build a new collider composed of smaller convex triangles'); + } } // calculate initial transformation @@ -109,13 +117,13 @@ export class PolygonCollider extends Collider { for (const [i, point] of this.points.entries()) { oldPoint = newPoint; oldDirection = direction; - newPoint = point; + newPoint = point; direction = Math.atan2(newPoint.y - oldPoint.y, newPoint.x - oldPoint.x); if (oldPoint.equals(newPoint)) { return false; // repeat point } let angle = direction - oldDirection; - if (angle <= -Math.PI){ + if (angle <= -Math.PI) { angle += Math.PI * 2; } else if (angle > Math.PI) { angle -= Math.PI * 2; @@ -124,7 +132,7 @@ export class PolygonCollider extends Collider { if (angle === 0.0) { return false; } - orientation = angle > 0 ? 1 : -1; + orientation = angle > 0 ? 1 : -1; } else { if (orientation * angle <= 0) { return false; @@ -158,19 +166,48 @@ export class PolygonCollider extends Collider { throw Error('Invalid polygon'); } + const triangles: [Vector, Vector, Vector][] = []; + // algorithm likes clockwise + const vertices = [...this.points].reverse(); + let vertexCount = vertices.length; + /** - * Helper to get a vertex in the list + * Returns the previous index based on the current vertex */ - function getItem(index: number, list: T[]) { - if (index >= list.length) { - return list[index % list.length]; - } else if (index < 0) { - return list[index % list.length + list.length]; - } else { - return list[index]; + function getPrevIndex(index: number) { + return index === 0 ? vertexCount - 1 : index - 1; + } + + /** + * Retrieves the next index based on the current vertex + */ + function getNextIndex(index: number) { + return index === vertexCount - 1 ? 0 : index + 1; + } + + /** + * Whether or not the angle at this vertex index is convex + */ + function isConvex(index: number) { + const prev = getPrevIndex(index); + const next = getNextIndex(index); + + const va = vertices[prev]; + const vb = vertices[index]; + const vc = vertices[next]; + + // Check convexity + const leftArm = va.sub(vb); + const rightArm = vc.sub(vb); + // Positive cross product is convex + if (leftArm.cross(rightArm) < 0) { + return false; } + return true; } + const convexVertices = vertices.map((_,i) => isConvex(i)); + /** * Quick test for point in triangle */ @@ -193,61 +230,96 @@ export class PolygonCollider extends Collider { return true; } - const triangles: Vector[][] = []; - const vertices = [...this.points]; - const indices = range(0, this.points.length - 1); - - // 1. Loop through vertices clockwise - // if the vertex is convex (interior angle is < 180) (cross product positive) - // if the polygon formed by it's edges doesn't contain the points - // it's an ear add it to our list of triangles, and restart - - while (indices.length > 3) { - for (let i = 0; i < indices.length; i++) { - const a = indices[i]; - const b = getItem(i - 1, indices); - const c = getItem(i + 1, indices); - - const va = vertices[a]; - const vb = vertices[b]; - const vc = vertices[c]; - - // Check convexity - const leftArm = vb.sub(va); - const rightArm = vc.sub(va); - const isConvex = rightArm.cross(leftArm) > 0; // positive cross means convex - if (!isConvex) { - continue; - } + /** + * Calculate the area of the triangle + */ + // function triangleArea(a: Vector, b: Vector, c: Vector) { + // return Math.abs(a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - c.y))/2; + // } - let isEar = true; - // Check that if any vertices are in the triangle a, b, c - for (let j = 0; j < indices.length; j++) { - const vertIndex = indices[j]; - // We can skip these - if (vertIndex === a || vertIndex === b || vertIndex === c) { - continue; + /** + * Find the next suitable ear tip + */ + function findEarTip() { + for (let i = 0; i < vertexCount; i++) { + if (convexVertices[i]) { + + const prev = getPrevIndex(i); + const next = getNextIndex(i); + + const va = vertices[prev]; + const vb = vertices[i]; + const vc = vertices[next]; + + let isEar = true; + // Check that if any vertices are in the triangle a, b, c + for (let j = 0; j < vertexCount; j++) { + // We can skip these verts because they are the triangle we are testing + if (j === i || j === prev || j === next) { + continue; + } + const point = vertices[j]; + if (isPointInTriangle(point, va, vb, vc)) { + isEar = false; + break; + } } - const point = vertices[vertIndex]; - if (isPointInTriangle(point, vb, va, vc)) { - isEar = false; - break; + // Add ear to polygon list and remove from list + if (isEar) { + return i; } } + } - // Add ear to polygon list and remove from list - if (isEar) { - triangles.push([vb, va, vc]); - indices.splice(i, 1); - break; + // Fall back to any convex vertex + for (let i = 0; i < vertexCount; i++) { + if (convexVertices[i]) { + return i; } } + + // bail and return the first one? + return 0; + } + + /** + * Cut the ear and produce a triangle, update internal state + */ + function cutEarTip(index: number) { + const prev = getPrevIndex(index); + const next = getNextIndex(index); + + const va = vertices[prev]; + const vb = vertices[index]; + const vc = vertices[next]; + + // Clockwise winding + // if (triangleArea(va, vb, vc) > 0) { + triangles.push([va, vb, vc]); + // } + vertices.splice(index, 1); + convexVertices.splice(index, 1); + vertexCount--; + } + + // Loop over all the vertices finding ears + while (vertexCount > 3) { + const earIndex = findEarTip(); + cutEarTip(earIndex); + + // reclassify vertices + for (let i = 0; i < vertexCount; i++) { + convexVertices[i] = isConvex(i); + } } - triangles.push([vertices[indices[0]], vertices[indices[1]], vertices[indices[2]]]); + // Last triangle after the loop + triangles.push([vertices[0], vertices[1], vertices[2]]); - return new CompositeCollider(triangles.map(points => Shape.Polygon(points))); + // FIXME: there is a colinear triangle that sneaks in here sometimes + return new CompositeCollider( + triangles.map(points => Shape.Polygon(points, Vector.Zero, true))); } /** @@ -614,13 +686,14 @@ export class PolygonCollider extends Collider { return new Projection(min, max); } - public debug(ex: ExcaliburGraphicsContext, color: Color) { + public debug(ex: ExcaliburGraphicsContext, color: Color, options?: { lineWidth: number, pointSize: number }) { const firstPoint = this.getTransformedPoints()[0]; const points = [firstPoint, ...this.getTransformedPoints(), firstPoint]; + const { lineWidth, pointSize } = { ...{ lineWidth: 1, pointSize: 1 }, ...options }; for (let i = 0; i < points.length - 1; i++) { - ex.drawLine(points[i], points[i + 1], color, 2); - ex.drawCircle(points[i], 2, color); - ex.drawCircle(points[i + 1], 2, color); + ex.drawLine(points[i], points[i + 1], color, lineWidth); + ex.drawCircle(points[i], pointSize, color); + ex.drawCircle(points[i + 1], pointSize, color); } } } diff --git a/src/engine/Collision/Colliders/Shape.ts b/src/engine/Collision/Colliders/Shape.ts index 65b98d234..3ddcb34e3 100644 --- a/src/engine/Collision/Colliders/Shape.ts +++ b/src/engine/Collision/Colliders/Shape.ts @@ -31,10 +31,11 @@ export class Shape { * @param points Points specified in counter clockwise * @param offset Optional offset relative to the collider in local coordinates */ - static Polygon(points: Vector[], offset: Vector = Vector.Zero): PolygonCollider { + static Polygon(points: Vector[], offset: Vector = Vector.Zero, suppressConvexWarning = false): PolygonCollider { return new PolygonCollider({ points: points, - offset: offset + offset: offset, + suppressConvexWarning }); } diff --git a/src/engine/Debug/Debug.ts b/src/engine/Debug/Debug.ts index c2c930e9d..09460943c 100644 --- a/src/engine/Debug/Debug.ts +++ b/src/engine/Debug/Debug.ts @@ -246,7 +246,7 @@ export class Debug implements DebugFlags { */ public entity = { showAll: false, - showId: true, + showId: false, showName: false }; @@ -256,6 +256,7 @@ export class Debug implements DebugFlags { public transform = { showAll: false, + debugZIndex: 10_000_000, showPosition: false, showPositionLabel: false, positionColor: Color.Yellow, @@ -275,7 +276,7 @@ export class Debug implements DebugFlags { public graphics = { showAll: false, - showBounds: true, + showBounds: false, boundsColor: Color.Yellow }; @@ -285,13 +286,15 @@ export class Debug implements DebugFlags { public collider = { showAll: false, - showBounds: true, + showBounds: false, boundsColor: Color.Blue, showOwner: false, showGeometry: true, - geometryColor: Color.Green + geometryColor: Color.Green, + geometryLineWidth: 1, + geometryPointSize: .5 }; /** @@ -359,6 +362,20 @@ export class Debug implements DebugFlags { colliderGeometryColor: Color.Green, showQuadTree: false }; + + public isometric = { + showAll: false, + showPosition: false, + positionColor: Color.Yellow, + positionSize: 1, + showGrid: false, + gridColor: Color.Red, + gridWidth: 1, + showColliders: true, + colliderColor: Color.Green, + colliderLineWidth: 1, + colliderPointSize: .5 + }; } /** diff --git a/src/engine/Debug/DebugSystem.ts b/src/engine/Debug/DebugSystem.ts index 3a4062d43..fcc139321 100644 --- a/src/engine/Debug/DebugSystem.ts +++ b/src/engine/Debug/DebugSystem.ts @@ -91,6 +91,7 @@ export class DebugSystem extends System { this._pushCameraTransform(tx); this._graphicsContext.save(); + this._graphicsContext.z = txSettings.debugZIndex; this._applyTransform(entity); if (tx) { @@ -182,6 +183,9 @@ export class DebugSystem extends System { this._graphicsContext.restore(); + // World space + this._graphicsContext.save(); + this._graphicsContext.z = txSettings.debugZIndex; motion = entity.get(MotionComponent); if (motion) { if (motionSettings.showAll || motionSettings.showVelocity) { @@ -200,7 +204,10 @@ export class DebugSystem extends System { if (colliderComp) { const collider = colliderComp.get(); if ((colliderSettings.showAll || colliderSettings.showGeometry) && collider) { - collider.debug(this._graphicsContext, colliderSettings.geometryColor); + collider.debug(this._graphicsContext, colliderSettings.geometryColor, { + lineWidth: colliderSettings.geometryLineWidth, + pointSize: colliderSettings.geometryPointSize + }); } if (colliderSettings.showAll || colliderSettings.showBounds) { if (collider instanceof CompositeCollider) { @@ -225,6 +232,7 @@ export class DebugSystem extends System { } } + this._graphicsContext.restore(); this._popCameraTransform(tx); } diff --git a/src/engine/TileMap/IsometricEntityComponent.ts b/src/engine/TileMap/IsometricEntityComponent.ts index 45fa336a3..5a6d69c64 100644 --- a/src/engine/TileMap/IsometricEntityComponent.ts +++ b/src/engine/TileMap/IsometricEntityComponent.ts @@ -1,6 +1,13 @@ import { Component } from '../EntityComponentSystem/Component'; import { IsometricMap } from './IsometricMap'; +export interface IsometricEntityComponentOptions { + columns: number; + rows: number; + tileWidth: number; + tileHeight: number; +} + export class IsometricEntityComponent extends Component<'ex.isometricentity'> { public readonly type = 'ex.isometricentity'; /** @@ -8,14 +15,20 @@ export class IsometricEntityComponent extends Component<'ex.isometricentity'> { */ public elevation: number = 0; - public map: IsometricMap; + public readonly columns: number; + public readonly rows: number; + public readonly tileWidth: number; + public readonly tileHeight: number; /** * Specify the isometric map to use to position this entity's z-index - * @param map + * @param mapOrOptions */ - constructor(map: IsometricMap) { + constructor(mapOrOptions: IsometricMap | IsometricEntityComponentOptions) { super(); - this.map = map; + this.columns = mapOrOptions.columns; + this.rows = mapOrOptions.rows; + this.tileWidth = mapOrOptions.tileWidth; + this.tileHeight = mapOrOptions.tileHeight; } } \ No newline at end of file diff --git a/src/engine/TileMap/IsometricEntitySystem.ts b/src/engine/TileMap/IsometricEntitySystem.ts index bb4e4c6d6..9395765c3 100644 --- a/src/engine/TileMap/IsometricEntitySystem.ts +++ b/src/engine/TileMap/IsometricEntitySystem.ts @@ -15,7 +15,7 @@ export class IsometricEntitySystem extends System this.debug(ctx), false) + new DebugGraphicsComponent((ctx, debugFlags) => this.debug(ctx, debugFlags), false) ], options.name); - const { pos, tileWidth, tileHeight, columns: width, rows: height, renderFromTopOfGraphic, graphicsOffset } = options; + const { pos, tileWidth, tileHeight, columns: width, rows: height, renderFromTopOfGraphic, graphicsOffset, elevation } = options; this.transform = this.get(TransformComponent); if (pos) { @@ -292,6 +311,7 @@ export class IsometricMap extends Entity { this.renderFromTopOfGraphic = renderFromTopOfGraphic ?? this.renderFromTopOfGraphic; this.graphicsOffset = graphicsOffset ?? this.graphicsOffset; + this.elevation = elevation ?? this.elevation; this.tileWidth = tileWidth; this.tileHeight = tileHeight; this.columns = width; @@ -305,7 +325,6 @@ export class IsometricMap extends Entity { const tile = new IsometricTile(x, y, this.graphicsOffset, this); this.tiles[x + y * width] = tile; this.addChild(tile); - // TODO row/columns helpers } } } @@ -415,23 +434,47 @@ export class IsometricMap extends Entity { * Debug draw for IsometricMap, called internally by excalibur when debug mode is toggled on * @param gfx */ - public debug(gfx: ExcaliburGraphicsContext) { + public debug(gfx: ExcaliburGraphicsContext, debugFlags: Debug) { + const { + showAll, + showPosition, + positionColor, + positionSize, + showGrid, + gridColor, + gridWidth, + showColliders, + colliderColor, + colliderLineWidth, + colliderPointSize + } = debugFlags.isometric; gfx.save(); gfx.z = this._getMaxZIndex() + 0.5; - for (let y = 0; y < this.rows + 1; y++) { - const left = this.tileToWorld(vec(0, y)); - const right = this.tileToWorld(vec(this.columns, y)); - gfx.drawLine(left, right, Color.Red, 2); - } + if (showAll || showGrid) { + for (let y = 0; y < this.rows + 1; y++) { + const left = this.tileToWorld(vec(0, y)); + const right = this.tileToWorld(vec(this.columns, y)); + gfx.drawLine(left, right, gridColor, gridWidth); + } - for (let x = 0; x < this.columns + 1; x++) { - const top = this.tileToWorld(vec(x, 0)); - const bottom = this.tileToWorld(vec(x, this.rows)); - gfx.drawLine(top, bottom, Color.Red, 2); + for (let x = 0; x < this.columns + 1; x++) { + const top = this.tileToWorld(vec(x, 0)); + const bottom = this.tileToWorld(vec(x, this.rows)); + gfx.drawLine(top, bottom, gridColor, gridWidth); + } } - for (const tile of this.tiles) { - gfx.drawCircle(this.tileToWorld(vec(tile.x, tile.y)), 3, Color.Yellow); + if (showAll || showPosition) { + for (const tile of this.tiles) { + gfx.drawCircle(this.tileToWorld(vec(tile.x, tile.y)), positionSize, positionColor); + } + } + if (showAll || showColliders) { + for (const tile of this.tiles) { + for (const collider of tile.getColliders()) { + collider.debug(gfx, colliderColor, { lineWidth: colliderLineWidth, pointSize: colliderPointSize }); + } + } } gfx.restore(); } diff --git a/src/engine/TileMap/TileMap.ts b/src/engine/TileMap/TileMap.ts index a92ae978c..9acf18982 100644 --- a/src/engine/TileMap/TileMap.ts +++ b/src/engine/TileMap/TileMap.ts @@ -8,7 +8,6 @@ import { BodyComponent } from '../Collision/BodyComponent'; import { CollisionType } from '../Collision/CollisionType'; import { Shape } from '../Collision/Colliders/Shape'; import { ExcaliburGraphicsContext, Graphic, GraphicsComponent, hasGraphicsTick, ParallaxComponent } from '../Graphics'; -import { removeItemFromArray } from '../Util/Util'; import { MotionComponent } from '../EntityComponentSystem/Components/MotionComponent'; import { ColliderComponent } from '../Collision/ColliderComponent'; import { CompositeCollider } from '../Collision/Colliders/CompositeCollider'; @@ -437,7 +436,7 @@ export class TileMap extends Entity { * Returns the [[Tile]] by testing a point in world coordinates, * returns `null` if no Tile was found. */ - public getTileByPoint(point: Vector): Tile { + public getTileByPoint(point: Vector): Tile | null { const x = Math.floor((point.x - this.pos.x) / (this.tileWidth * this.scale.x)); const y = Math.floor((point.y - this.pos.y) / (this.tileHeight * this.scale.y)); const tile = this.getTile(x, y); @@ -507,17 +506,19 @@ export class TileMap extends Entity { for (let i = 0; i < tiles.length; i++) { const tile = tiles[i]; // get non-negative tile sprites + const offsets = tile.getGraphicsOffsets(); graphics = tile.getGraphics(); for (graphicsIndex = 0, graphicsLen = graphics.length; graphicsIndex < graphicsLen; graphicsIndex++) { // draw sprite, warning if sprite doesn't exist const graphic = graphics[graphicsIndex]; + const offset = offsets[graphicsIndex]; if (graphic) { if (hasGraphicsTick(graphic)) { graphic?.tick(delta, this._token); } const offsetY = this.renderFromTopOfGraphic ? 0 : (graphic.height - this.tileHeight); - graphic.draw(ctx, tile.x * this.tileWidth, tile.y * this.tileHeight - offsetY); + graphic.draw(ctx, tile.x * this.tileWidth + offset.x, tile.y * this.tileHeight - offsetY + offset.y); } } } @@ -618,7 +619,6 @@ export class Tile extends Entity { private _geometry: BoundingBox; private _pos: Vector; private _posDirty = false; - // private _transform: TransformComponent; /** * Return the world position of the top left corner of the tile @@ -678,6 +678,7 @@ export class Tile extends Entity { } private _graphics: Graphic[] = []; + private _offsets: Vector[] = []; /** * Current list of graphics for this tile @@ -686,19 +687,35 @@ export class Tile extends Entity { return this._graphics; } + /** + * Current list of offsets for this tile's graphics + */ + public getGraphicsOffsets(): readonly Vector[] { + return this._offsets; + } + /** * Add another [[Graphic]] to this TileMap tile * @param graphic */ - public addGraphic(graphic: Graphic) { + public addGraphic(graphic: Graphic, options?: { offset?: Vector }) { this._graphics.push(graphic); + if (options?.offset) { + this._offsets.push(options.offset); + } else { + this._offsets.push(Vector.Zero); + } } /** * Remove an instance of a [[Graphic]] from this tile */ public removeGraphic(graphic: Graphic) { - removeItemFromArray(graphic, this._graphics); + const index = this._graphics.indexOf(graphic); + if (index > -1) { + this._graphics.splice(index, 1); + this._offsets.splice(index, 1); + } } /** @@ -706,6 +723,7 @@ export class Tile extends Entity { */ public clearGraphics() { this._graphics.length = 0; + this._offsets.length = 0; } /** diff --git a/src/spec/CollisionShapeSpec.ts b/src/spec/CollisionShapeSpec.ts index 20c458fc0..afe785726 100644 --- a/src/spec/CollisionShapeSpec.ts +++ b/src/spec/CollisionShapeSpec.ts @@ -499,9 +499,9 @@ describe('Collision Shape', () => { const colliders = composite.getColliders() as ex.PolygonCollider[]; expect(colliders.length).toBe(3); - expect(colliders[0].points).toEqual([ex.vec(0, 0), ex.vec(10, 0), ex.vec(10, 10)]); - expect(colliders[1].points).toEqual([ex.vec(0, 0), ex.vec(10, 10), ex.vec(0, 10)]); - expect(colliders[2].points).toEqual([ex.vec(0, 0), ex.vec(5, 5), ex.vec(0, 10)]); + expect(colliders[0].points).toEqual([ex.vec(5, 5), ex.vec(0, 0), ex.vec(10, 0)]); + expect(colliders[1].points).toEqual([ex.vec(0, 10), ex.vec(5, 5), ex.vec(10, 0)]); + expect(colliders[2].points).toEqual([ex.vec(10, 0), ex.vec(10, 10), ex.vec(0, 10)]); expect(concave.isConvex()).withContext('Should be concave').toBe(false); }); diff --git a/src/spec/DebugSystemSpec.ts b/src/spec/DebugSystemSpec.ts index 238d88baa..6bb2df744 100644 --- a/src/spec/DebugSystemSpec.ts +++ b/src/spec/DebugSystemSpec.ts @@ -87,6 +87,8 @@ describe('DebugSystem', () => { actor.vel = ex.vec(100, 0); actor.acc = ex.vec(100, -100); engine.debug.motion.showAll = true; + engine.debug.collider.showGeometry = true; + engine.debug.collider.geometryLineWidth = 2; debugSystem.update([actor], 100); engine.graphicsContext.flush(); @@ -124,6 +126,7 @@ describe('DebugSystem', () => { const actor = new ex.Actor({ name: 'thingy', x: -100 + center.x, y: center.y, width: 50, height: 50, color: ex.Color.Yellow }); actor.id = 0; + engine.debug.entity.showId = true; engine.debug.collider.showAll = true; debugSystem.update([actor], 100); @@ -144,6 +147,7 @@ describe('DebugSystem', () => { actor.collider.useCompositeCollider([ex.Shape.Circle(50), ex.Shape.Box(150, 20), ex.Shape.Box(10, 150)]); actor.id = 0; engine.debug.collider.showAll = true; + engine.debug.collider.geometryLineWidth = 3; debugSystem.update([actor], 100); engine.graphicsContext.flush(); diff --git a/src/spec/IsometricMapSpec.ts b/src/spec/IsometricMapSpec.ts index ecee8d6f6..cab7db45c 100644 --- a/src/spec/IsometricMapSpec.ts +++ b/src/spec/IsometricMapSpec.ts @@ -111,6 +111,9 @@ describe('A IsometricMap', () => { engine.debug.entity.showName = false; engine.debug.entity.showId = false; engine.debug.graphics.showBounds = false; + engine.debug.transform.showPosition = true; + engine.debug.isometric.showGrid = true; + engine.debug.isometric.showPosition = true; const clock = engine.clock as ex.TestClock; const image = new ex.ImageSource('src/spec/images/IsometricMapSpec/cube.png'); await image.load(); diff --git a/src/spec/images/CollisionShapeSpec/circle-debug.png b/src/spec/images/CollisionShapeSpec/circle-debug.png index 3f75fd172..58fe24424 100644 Binary files a/src/spec/images/CollisionShapeSpec/circle-debug.png and b/src/spec/images/CollisionShapeSpec/circle-debug.png differ diff --git a/src/spec/images/CompositeColliderSpec/composite.png b/src/spec/images/CompositeColliderSpec/composite.png index 4387a0118..c2d8204ba 100644 Binary files a/src/spec/images/CompositeColliderSpec/composite.png and b/src/spec/images/CompositeColliderSpec/composite.png differ diff --git a/src/spec/images/DebugSystemSpec/body.png b/src/spec/images/DebugSystemSpec/body.png index f03d61788..245812ec0 100644 Binary files a/src/spec/images/DebugSystemSpec/body.png and b/src/spec/images/DebugSystemSpec/body.png differ diff --git a/src/spec/images/DebugSystemSpec/composite-collider.png b/src/spec/images/DebugSystemSpec/composite-collider.png index de6a6a034..9a3ed62d0 100644 Binary files a/src/spec/images/DebugSystemSpec/composite-collider.png and b/src/spec/images/DebugSystemSpec/composite-collider.png differ diff --git a/src/spec/images/DebugSystemSpec/graphics.png b/src/spec/images/DebugSystemSpec/graphics.png index b60974499..aeda6bc2a 100644 Binary files a/src/spec/images/DebugSystemSpec/graphics.png and b/src/spec/images/DebugSystemSpec/graphics.png differ diff --git a/src/spec/images/DebugSystemSpec/motion.png b/src/spec/images/DebugSystemSpec/motion.png index af4ab07d7..c214dcfc9 100644 Binary files a/src/spec/images/DebugSystemSpec/motion.png and b/src/spec/images/DebugSystemSpec/motion.png differ diff --git a/src/spec/images/IsometricMapSpec/cube-map-debug.png b/src/spec/images/IsometricMapSpec/cube-map-debug.png index 86a3aee0b..f71adfbd8 100644 Binary files a/src/spec/images/IsometricMapSpec/cube-map-debug.png and b/src/spec/images/IsometricMapSpec/cube-map-debug.png differ