Skip to content

Commit

Permalink
fix: TileMap packing + TileMap debug draw configurable (#2851)
Browse files Browse the repository at this point in the history
This PR makes TileMap debug draw configurable, and by default reduces the draw burden by toggling off the grid display by default.

This PR also fixes a tilemap packing bug where some situation would result in incorrect colliders


https://github.com/excaliburjs/Excalibur/assets/612071/104bfb9d-c8e3-431e-a454-e8bed89ad960
  • Loading branch information
eonarheim authored Dec 22, 2023
1 parent 410647a commit 109f0e6
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 48 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Added

- Ability to configure TileMap debug drawing with the `ex.Engine.debug.tilemap` property.
- Materials have a new convenience method for updating uniforms
```typescript
game.input.pointers.primary.on('move', evt => {
Expand All @@ -28,6 +29,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Fixed

- Fixed issue where TileMap solid tiles tile packing algorithm would incorrectly merge tiles in certain situations.
- Sprite tint was not respected when supplied in the constructor, this has been fixed!
- Adjusted the `FontCache` font timeout to 400 ms and makes it configurable as a static `FontCache.FONT_TIMEOUT`. This is to help prevent a downward spiral on mobile devices that might take a long while to render a few starting frames causing the cache to repeatedly clear and never recover.

Expand All @@ -43,7 +45,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Changed

-
- TileMap debug draw is now less verbose by default to save draw cycles when toggling to debug

<!--------------------------------- DO NOT EDIT BELOW THIS LINE --------------------------------->
<!--------------------------------- DO NOT EDIT BELOW THIS LINE --------------------------------->
Expand Down
15 changes: 15 additions & 0 deletions sandbox/tests/tilemap-pack/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>TileMap Packing</title>
</head>
<body>
<h1>TileMap Packing</h1>
<p>Should collapse bounds into minimum geometry to represent colliders</p>
<script src="../../lib/excalibur.js"></script>
<script src="index.js"></script>
</body>
</html>
78 changes: 78 additions & 0 deletions sandbox/tests/tilemap-pack/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/// <reference path='../../lib/excalibur.d.ts' />

var game = new ex.Engine({
width: 600,
height: 600
});

game.toggleDebug();
game.debug.entity.showId = false;
game.debug.tilemap.showSolidBounds = true;
game.debug.tilemap.showGrid = true;

var tm = new ex.TileMap({
pos: ex.vec(200, 200),
tileWidth: 16,
tileHeight: 16,
columns: 6,
rows: 4
});

tm.getTile(0, 0).solid = true;
tm.getTile(0, 1).solid = true;
tm.getTile(0, 2).solid = true;
tm.getTile(0, 3).solid = true;

tm.getTile(1, 0).solid = false;
tm.getTile(1, 1).solid = false;
tm.getTile(1, 2).solid = false;
tm.getTile(1, 3).solid = false;

tm.getTile(2, 0).solid = false;
tm.getTile(2, 1).solid = false;
tm.getTile(2, 2).solid = false;
tm.getTile(2, 3).solid = false;

tm.getTile(3, 0).solid = true;
tm.getTile(3, 1).solid = true;
tm.getTile(3, 2).solid = true;
tm.getTile(3, 3).solid = true;

tm.getTile(4, 0).solid = true;
tm.getTile(4, 1).solid = true;
tm.getTile(4, 2).solid = true;
tm.getTile(4, 3).solid = true;

game.add(tm);
game.input.pointers.primary.on('down', (evt: ex.PointerEvent) => {
const tile = tm.getTileByPoint(evt.worldPos);
if (tile) {
tile.solid = !tile.solid;
}
});

let currentPointer!: ex.Vector;
game.input.pointers.primary.on('down', (moveEvent) => {
if (moveEvent.button === ex.PointerButton.Right) {
currentPointer = moveEvent.worldPos;
game.currentScene.camera.move(currentPointer, 300, ex.EasingFunctions.EaseInOutCubic);
}
});

document.oncontextmenu = () => false;

game.input.pointers.primary.on('wheel', (wheelEvent) => {
// wheel up
game.currentScene.camera.pos = currentPointer;
if (wheelEvent.deltaY < 0) {
game.currentScene.camera.zoom *= 1.2;
} else {
game.currentScene.camera.zoom /= 1.2;
}
});

game.start().then(() => {
game.currentScene.camera.pos = ex.vec(250, 225);
game.currentScene.camera.zoom = 3.5;
currentPointer = game.currentScene.camera.pos;
});
13 changes: 13 additions & 0 deletions src/engine/Debug/Debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,19 @@ export class Debug implements DebugFlags {

showZoom: false
};

public tilemap = {
showAll: false,

showGrid: false,
gridColor: Color.Red,
gridWidth: .5,
showSolidBounds: false,
solidBoundsColor: Color.fromHex('#8080807F'), // grayish
showColliderGeometry: true,
colliderGeometryColor: Color.Green,
showQuadTree: false
};
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/engine/Debug/DebugSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export class DebugSystem extends System<TransformComponent> {
if (!debugDraw.useTransform) {
this._graphicsContext.restore();
}
debugDraw.draw(this._graphicsContext);
debugDraw.draw(this._graphicsContext, this._engine.debug);
if (!debugDraw.useTransform) {
this._graphicsContext.save();
this._applyTransform(entity);
Expand Down
3 changes: 2 additions & 1 deletion src/engine/Graphics/DebugGraphicsComponent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ExcaliburGraphicsContext } from '.';
import { Debug } from '../Debug';
import { Component } from '../EntityComponentSystem/Component';


Expand All @@ -10,7 +11,7 @@ import { Component } from '../EntityComponentSystem/Component';
*/
export class DebugGraphicsComponent extends Component<'ex.debuggraphics'> {
readonly type = 'ex.debuggraphics';
constructor(public draw: (ctx: ExcaliburGraphicsContext) => void, public useTransform = true) {
constructor(public draw: (ctx: ExcaliburGraphicsContext, debugFlags: Debug) => void, public useTransform = true) {
super();
}
}
156 changes: 111 additions & 45 deletions src/engine/TileMap/TileMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import { removeItemFromArray } from '../Util/Util';
import { MotionComponent } from '../EntityComponentSystem/Components/MotionComponent';
import { ColliderComponent } from '../Collision/ColliderComponent';
import { CompositeCollider } from '../Collision/Colliders/CompositeCollider';
import { Color } from '../Color';
import { DebugGraphicsComponent } from '../Graphics/DebugGraphicsComponent';
import { Collider } from '../Collision/Colliders/Collider';
import { PostDrawEvent, PostUpdateEvent, PreDrawEvent, PreUpdateEvent } from '../Events';
import { EventEmitter, EventKey, Handler, Subscription } from '../EventEmitter';
import { CoordPlane } from '../Math/coord-plane';
import { QuadTree } from '../Collision/Detection/QuadTree';
import { Debug } from '../Debug';

export interface TileMapOptions {
/**
Expand Down Expand Up @@ -220,7 +220,7 @@ export class TileMap extends Entity {
onPostDraw: (ctx, delta) => this.draw(ctx, delta)
})
);
this.addComponent(new DebugGraphicsComponent((ctx) => this.debug(ctx), false));
this.addComponent(new DebugGraphicsComponent((ctx, debugFlags) => this.debug(ctx, debugFlags), false));
this.addComponent(new ColliderComponent());
this._graphics = this.get(GraphicsComponent);
this._transform = this.get(TransformComponent);
Expand Down Expand Up @@ -314,14 +314,55 @@ export class TileMap extends Entity {
this._composite = this._collider.useCompositeCollider([]);
let current: BoundingBox;

// Bad square tesselation algo
/**
* Returns wether or not the 2 boxes share an edge and are the same height
* @param prev
* @param next
* @returns true if they share and edge, false if not
*/
const shareEdges = (prev: BoundingBox, next: BoundingBox) => {
if (prev && next) {
// same top/bottom
return prev.top === next.top &&
prev.bottom === next.bottom &&
// Shared right/left edge
prev.right === next.left;
}
return false;
};

/**
* Potentially merges the current collider into a list of previous ones, mutating the list
* If checkAndCombine returns true, the collider was successfully merged and should be thrown away
* @param current current collider to test
* @param colliders List of colliders to consider merging with
* @param maxLookBack The amount of colliders to look back for combindation
* @returns false when no combination found, true when successfully combined
*/
const checkAndCombine = (current: BoundingBox, colliders: BoundingBox[], maxLookBack = 10) => {
if (!current) {
return false;
}
// walk backwards through the list of colliders and combine with the first that shares an edge
for (let i = colliders.length - 1; i >= 0; i--) {
if (maxLookBack-- < 0) {
// blunt the O(n^2) algorithm a bit
return false;
}
const prev = colliders[i];
if (shareEdges(prev, current)) {
colliders[i] = prev.combine(current);
return true;
}
}
return false;
};

// ? configurable bias perhaps, horizontal strips vs. vertical ones
// Bad tile collider packing algorithm
for (let i = 0; i < this.columns; i++) {
// Scan column for colliders
for (let j = 0; j < this.rows; j++) {
// Columns start with a new collider
if (j === 0) {
current = null;
}
const tile = this.tiles[i + j * this.columns];
// Current tile in column is solid build up current collider
if (tile.solid) {
Expand All @@ -335,11 +376,11 @@ export class TileMap extends Entity {
this._composite.addCollider(collider);
}
//we push any current collider before nulling the current run
if (current) {
if (current && !checkAndCombine(current, colliders)) {
colliders.push(current);
}
current = null;
// Use the bounding box
// Use the bounding box
} else {
if (!current) {
// no current run, start one
Expand All @@ -351,23 +392,20 @@ export class TileMap extends Entity {
}
} else {
// Not solid skip and cut off the current collider
if (current) {
// End of run check and combine
if (current && !checkAndCombine(current, colliders)) {
colliders.push(current);
}
current = null;
}
}
// After a column is complete check to see if it can be merged into the last one
if (current) {
// if previous is the same combine it
const prev = colliders[colliders.length - 1];
if (prev && prev.top === current.top && prev.bottom === current.bottom) {
colliders[colliders.length - 1] = prev.combine(current);
} else {
// else new collider
colliders.push(current);
}
// Eno of run check and combine
if (current && !checkAndCombine(current, colliders)) {
// else new collider if no combination
colliders.push(current);
}
current = null;
}

for (const c of colliders) {
Expand Down Expand Up @@ -421,7 +459,7 @@ export class TileMap extends Entity {
this.onPreUpdate(engine, delta);
this.emit('preupdate', new PreUpdateEvent(engine, delta, this));
if (!this._oldPos.equals(this.pos) ||
this._oldRotation !== this.rotation ||
this._oldRotation !== this.rotation ||
!this._oldScale.equals(this.scale)) {
this.flagCollidersDirty();
this.flagTilesDirty();
Expand Down Expand Up @@ -487,39 +525,67 @@ export class TileMap extends Entity {
this.emit('postdraw', new PostDrawEvent(ctx as any, delta, this));
}

public debug(gfx: ExcaliburGraphicsContext) {
public debug(gfx: ExcaliburGraphicsContext, debugFlags: Debug) {
const {
showAll,
showGrid,
gridColor,
gridWidth,
showSolidBounds: showColliderBounds,
solidBoundsColor: colliderBoundsColor,
showColliderGeometry,
colliderGeometryColor,
showQuadTree
} = debugFlags.tilemap;
const width = this.tileWidth * this.columns * this.scale.x;
const height = this.tileHeight * this.rows * this.scale.y;
const pos = this.pos;
for (let r = 0; r < this.rows + 1; r++) {
const yOffset = vec(0, r * this.tileHeight * this.scale.y);
gfx.drawLine(pos.add(yOffset), pos.add(vec(width, yOffset.y)), Color.Red, 2);
}
if (showGrid || showAll) {
for (let r = 0; r < this.rows + 1; r++) {
const yOffset = vec(0, r * this.tileHeight * this.scale.y);
gfx.drawLine(pos.add(yOffset), pos.add(vec(width, yOffset.y)), gridColor, gridWidth);
}

for (let c = 0; c < this.columns + 1; c++) {
const xOffset = vec(c * this.tileWidth * this.scale.x, 0);
gfx.drawLine(pos.add(xOffset), pos.add(vec(xOffset.x, height)), Color.Red, 2);
for (let c = 0; c < this.columns + 1; c++) {
const xOffset = vec(c * this.tileWidth * this.scale.x, 0);
gfx.drawLine(pos.add(xOffset), pos.add(vec(xOffset.x, height)), gridColor, gridWidth);
}
}

const colliders = this._composite.getColliders();
gfx.save();
gfx.translate(this.pos.x, this.pos.y);
gfx.scale(this.scale.x, this.scale.y);
for (const collider of colliders) {
const grayish = Color.Gray;
grayish.a = 0.5;
const bounds = collider.localBounds;
const pos = collider.worldPos.sub(this.pos);
gfx.drawRectangle(pos, bounds.width, bounds.height, grayish);
if (showAll || showColliderBounds || showColliderGeometry) {
const colliders = this._composite.getColliders();
gfx.save();
gfx.translate(this.pos.x, this.pos.y);
gfx.scale(this.scale.x, this.scale.y);
for (const collider of colliders) {
const bounds = collider.localBounds;
const pos = collider.worldPos.sub(this.pos);
if (showColliderBounds) {
gfx.drawRectangle(pos, bounds.width, bounds.height, colliderBoundsColor);
}
}
gfx.restore();
if (showColliderGeometry) {
for (const collider of colliders) {
collider.debug(gfx, colliderGeometryColor);
}
}
}
gfx.restore();
gfx.save();
gfx.z = 999;
this._quadTree.debug(gfx);
for (let i = 0; i < this.tiles.length; i++) {
this.tiles[i].bounds.draw(gfx);

if (showAll || showQuadTree || showColliderBounds) {
gfx.save();
gfx.z = 999;
if (showQuadTree) {
this._quadTree.debug(gfx);
}

if (showColliderBounds) {
for (let i = 0; i < this.tiles.length; i++) {
this.tiles[i].bounds.draw(gfx);
}
}
gfx.restore();
}
gfx.restore();
}
}

Expand Down
Loading

0 comments on commit 109f0e6

Please sign in to comment.