Skip to content

Commit

Permalink
feat: add layer collider configuration (#586)
Browse files Browse the repository at this point in the history
This PR allows users to deeply configure the collision behavior on tiled layers

Given a Tiled tile layer name or id you can configure
1. Whether to use tile colliders regardless of solid=true|false
2. Whether to use the tile colliders when the layer is not visible
3. Force a layer to be solid or not overriding anything specified in tiled
4. Add a collision group to use for the tile layer

```typescript
const tiledMap = new TiledResource('./orthogonal.tmx', {
   useMapBackgroundColor: true,
   layerConfig: {
      "Above": {
         isSolid: true
         useTileColliders: true,
         //useTileCollidersWhenInvisible: true, // use this instead of above usually
         collisionGroup: new ex.CollisionGroup("above", 0x01, 0x00111)
      }
   }
});
``` 

![image](https://github.com/user-attachments/assets/e1579ba2-9be6-4754-a8b2-e5046ca10a31)
  • Loading branch information
eonarheim authored Jan 17, 2025
1 parent a2d5a6c commit 747fc8e
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 20 deletions.
4 changes: 2 additions & 2 deletions example/orthogonal/orthogonal.tmx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="10" height="10" tilewidth="16" tileheight="16" infinite="0" backgroundcolor="#2df6f9" nextlayerid="5" nextobjectid="5">
<map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="10" height="10" tilewidth="16" tileheight="16" infinite="0" backgroundcolor="#2df6f9" nextlayerid="6" nextobjectid="5">
<tileset firstgid="1" name="fantasy" tilewidth="16" tileheight="16" tilecount="132" columns="12">
<image source="tilemap_packed.png" width="192" height="176"/>
<tile id="105" type="Collectable"/>
Expand Down Expand Up @@ -46,7 +46,7 @@
</layer>
<layer id="4" name="Above" width="10" height="10">
<properties>
<property name="solid" type="bool" value="true"/>
<property name="solid" type="bool" value="false"/>
</properties>
<data encoding="csv">
0,0,0,0,0,0,0,0,0,0,
Expand Down
26 changes: 19 additions & 7 deletions example/orthogonal/orthogonal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import * as ex from 'excalibur';
import { TiledResource } from '@excalibur-tiled';

class Player extends ex.Actor {
constructor(args: ex.ActorArgs) {
super(args);
}
override onPostUpdate(engine: ex.Engine) {
this.vel = ex.vec(0, 0)
const speed = 64;
Expand All @@ -21,8 +24,8 @@ class Player extends ex.Actor {
}

const game = new ex.Engine({
width: 800,
height: 600,
width: 800,
height: 600,
canvasElementId: 'game',
pointerScope: ex.PointerScope.Canvas,
antialiasing: false
Expand All @@ -31,14 +34,23 @@ game.toggleDebug();

const tiledMap = new TiledResource('./orthogonal.tmx', {
useMapBackgroundColor: true,
layerConfig: {
"Above": {
//isSolid: true
//useTileColliders: true,
useTileCollidersWhenInvisible: true,
collisionGroup: new ex.CollisionGroup("above", 0x01, 0x00111)
}
},
entityClassNameFactories: {
'player-start': (props) => {
return new Player({
pos: props.worldPos,
width: 16,
height: 16,
color: ex.Color.Blue,
collisionType: ex.CollisionType.Active
collisionType: ex.CollisionType.Active,
collisionGroup: new ex.CollisionGroup("player", 0x10, 0x00010)
});
}
}
Expand All @@ -63,8 +75,8 @@ const loader = new ex.Loader([tiledMap]);

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

game.input.pointers.primary.on('move', (moveEvent) => {
Expand All @@ -78,9 +90,9 @@ game.input.pointers.primary.on('wheel', (wheelEvent) => {
// wheel up
game.currentScene.camera.pos = currentPointer;
if (wheelEvent.deltaY < 0) {
game.currentScene.camera.zoom *= 1.2;
game.currentScene.camera.zoom *= 1.2;
} else {
game.currentScene.camera.zoom /= 1.2;
game.currentScene.camera.zoom /= 1.2;
}
});

Expand Down
39 changes: 36 additions & 3 deletions src/resource/iso-tile-layer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Color, ParallaxComponent, Vector, vec, GraphicsComponent, Logger, AnimationStrategy, IsometricMap, PolygonCollider, CircleCollider, IsometricTile, IsometricEntityComponent } from "excalibur";
import { Color, ParallaxComponent, Vector, vec, GraphicsComponent, Logger, AnimationStrategy, IsometricMap, PolygonCollider, CircleCollider, IsometricTile, IsometricEntityComponent, BodyComponent } from "excalibur";
import { mapProps } from "./properties";
import { TiledTileLayer, isCSV, isInfiniteLayer, needsDecoding } from "../parser/tiled-parser";
import { Decoder } from "./decoder";
Expand All @@ -23,6 +23,10 @@ export interface IsometricTileInfo {

export class IsoTileLayer implements Layer {
private logger = Logger.getInstance();
/**
* Numeric id given by Tiled
*/
public readonly id: string | number;
public readonly name: string;
class?: string | undefined;
/**
Expand All @@ -34,6 +38,10 @@ export class IsoTileLayer implements Layer {
*/
public readonly height: number = 0;

/**
* Whether the tile layer is visible in the original map
*/
public readonly visible: boolean;
properties = new Map<string, string | number | boolean>();

/**
Expand All @@ -50,9 +58,11 @@ export class IsoTileLayer implements Layer {

constructor(public tiledTileLayer: TiledTileLayer, public resource: TiledResource, public readonly order: number) {
this.name = tiledTileLayer.name;
this.id = tiledTileLayer.id;
this.class = tiledTileLayer.class;
this.width = tiledTileLayer.width;
this.height = tiledTileLayer.height;
this.visible = !!tiledTileLayer.visible;
mapProps(this, tiledTileLayer.properties);
}

Expand Down Expand Up @@ -130,16 +140,21 @@ export class IsoTileLayer implements Layer {
let tileset = this.resource.getTilesetForTileGid(gid);
let maybeTile = tileset.getTileByGid(gid);
if (!tiles) {
tiles = [{exTile: tile, tiledTile: maybeTile}];
tiles = [{ exTile: tile, tiledTile: maybeTile }];
} else {
tiles.push({exTile: tile, tiledTile: maybeTile});
tiles.push({ exTile: tile, tiledTile: maybeTile });
}
this._gidToTileInfo.set(gid, tiles);
tile.data.set(ExcaliburTiledProperties.TileData.Tiled, maybeTile);
}

private updateTile(tile: IsometricTile, gid: number, hasTint: boolean, tint: Color, isSolidLayer: boolean) {
this._recordTileData(gid, tile);
const maybeLayerConfig = this.resource.getLayerConfig(this.name) ||
this.resource.getLayerConfig(this.id);
if (maybeLayerConfig?.isSolid !== undefined) {
isSolidLayer = maybeLayerConfig.isSolid;
}
if (this.resource.useExcaliburWiring && isSolidLayer) {
tile.solid = true;
}
Expand Down Expand Up @@ -173,6 +188,14 @@ export class IsoTileLayer implements Layer {
for (let collider of colliders) {
tile.addCollider(collider);
}
if (maybeLayerConfig?.useTileColliders && colliders.length > 0) {
if (this.visible) {
tile.solid = true;
}
}
if (maybeLayerConfig?.useTileCollidersWhenInvisible && colliders.length > 0) {
tile.solid = true;
}

let animation = tileset.getAnimationForGid(gid);
if (animation) {
Expand Down Expand Up @@ -215,6 +238,8 @@ export class IsoTileLayer implements Layer {
}

async load(): Promise<void> {
const maybeLayerConfig = this.resource.getLayerConfig(this.name) ||
this.resource.getLayerConfig(this.id);
const layer = this.tiledTileLayer;
const isSolidLayer = !!this.properties.get(ExcaliburTiledProperties.Layer.Solid);
const opacity = this.tiledTileLayer.opacity;
Expand Down Expand Up @@ -249,6 +274,10 @@ export class IsoTileLayer implements Layer {
rows: layer.height,
elevation: order
});
if (maybeLayerConfig?.collisionGroup) {
const body = this.isometricMap.get(BodyComponent);
body.group = maybeLayerConfig.collisionGroup;
}
} else {
this.isometricMap = new IsometricMap({
name: this.name,
Expand All @@ -259,6 +288,10 @@ export class IsoTileLayer implements Layer {
rows: layer.height,
elevation: order
});
if (maybeLayerConfig?.collisionGroup) {
const body = this.isometricMap.get(BodyComponent);
body.group = maybeLayerConfig.collisionGroup;
}
}

// TODO make these optional params in the ctor
Expand Down
47 changes: 42 additions & 5 deletions src/resource/tile-layer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Color, ParallaxComponent, TileMap, Vector, vec, GraphicsComponent, Logger, AnimationStrategy, TransformComponent, Tile as ExTile } from "excalibur";
import { Color, ParallaxComponent, TileMap, Vector, vec, GraphicsComponent, Logger, AnimationStrategy, TransformComponent, Tile as ExTile, BodyComponent } from "excalibur";
import { mapProps } from "./properties";
import { TiledTileLayer, isCSV, isInfiniteLayer, needsDecoding } from "../parser/tiled-parser";
import { Decoder } from "./decoder";
Expand Down Expand Up @@ -26,6 +26,13 @@ export interface TileInfo {

export class TileLayer implements Layer {
private logger = Logger.getInstance();
/**
* Numeric id given by Tiled
*/
public readonly id: number;
/**
* Optional name given by a user in Tiled
*/
public readonly name: string;
public readonly class?: string;
/**
Expand All @@ -51,6 +58,11 @@ export class TileLayer implements Layer {

private _gidToTileInfo = new Map<number, TileInfo[]>();

/**
* Whether the tile layer is visible in the original map
*/
public readonly visible: boolean;

/**
* Returns the excalibur tiles that match a tiled gid
*/
Expand Down Expand Up @@ -144,9 +156,11 @@ export class TileLayer implements Layer {

constructor(public tiledTileLayer: TiledTileLayer, public resource: TiledResource, public readonly order: number) {
this.name = tiledTileLayer.name;
this.id = tiledTileLayer.id;
this.class = tiledTileLayer.class;
this.width = tiledTileLayer.width;
this.height = tiledTileLayer.height;
this.visible = !!tiledTileLayer.visible;
mapProps(this, tiledTileLayer.properties);
}

Expand All @@ -155,17 +169,22 @@ export class TileLayer implements Layer {
let tileset = this.resource.getTilesetForTileGid(gid);
let maybeTile = tileset.getTileByGid(gid);
if (!tiles) {
tiles = [{exTile: tile, tiledTile: maybeTile}];
tiles = [{ exTile: tile, tiledTile: maybeTile }];
} else {
tiles.push({exTile: tile, tiledTile: maybeTile});
tiles.push({ exTile: tile, tiledTile: maybeTile });
}
this._gidToTileInfo.set(gid, tiles);
tile.data.set(ExcaliburTiledProperties.TileData.Tiled, maybeTile);
}

private updateTile(tile: ExTile, gid: number, hasTint: boolean, tint: Color, isSolidLayer: boolean) {
this._recordTileData(gid, tile);
if (this.resource.useExcaliburWiring && isSolidLayer) {
const maybeLayerConfig = this.resource.getLayerConfig(this.name) ||
this.resource.getLayerConfig(this.id);
if (maybeLayerConfig?.isSolid !== undefined) {
isSolidLayer = maybeLayerConfig.isSolid;
}
if (this.resource.useExcaliburWiring && isSolidLayer && this.visible) {
tile.solid = true;
}

Expand All @@ -187,6 +206,14 @@ export class TileLayer implements Layer {
for (let collider of colliders) {
tile.addCollider(collider);
}
if (maybeLayerConfig?.useTileColliders && colliders.length > 0) {
if (this.visible) {
tile.solid = true;
}
}
if (maybeLayerConfig?.useTileCollidersWhenInvisible && colliders.length > 0) {
tile.solid = true;
}

let animation = headless ? null : tileset.getAnimationForGid(gid);
if (animation) {
Expand Down Expand Up @@ -229,6 +256,8 @@ export class TileLayer implements Layer {
}

async load() {
const maybeLayerConfig = this.resource.getLayerConfig(this.name) ||
this.resource.getLayerConfig(this.id);
const opacity = this.tiledTileLayer.opacity;
const hasTint = !!this.tiledTileLayer.tintcolor;
const tint = this.tiledTileLayer.tintcolor ? Color.fromHex(this.tiledTileLayer.tintcolor) : Color.Transparent;
Expand All @@ -254,6 +283,10 @@ export class TileLayer implements Layer {
columns: layer.width,
rows: layer.height
});
if (maybeLayerConfig?.collisionGroup) {
const body = this.tilemap.get(BodyComponent);
body.group = maybeLayerConfig.collisionGroup;
}
} else {
this.tilemap = new TileMap({
name: this.name,
Expand All @@ -263,6 +296,10 @@ export class TileLayer implements Layer {
columns: layer.width,
rows: layer.height,
});
if (maybeLayerConfig?.collisionGroup) {
const body = this.tilemap.get(BodyComponent);
body.group = maybeLayerConfig.collisionGroup;
}
}

// Common tilemap props
Expand All @@ -277,7 +314,7 @@ export class TileLayer implements Layer {
}
const graphics = this.tilemap.get(GraphicsComponent);
if (graphics) {
graphics.visible = this.tiledTileLayer.visible;
graphics.isVisible = this.tiledTileLayer.visible;
graphics.opacity = opacity;
}
if (layer.parallaxx || layer.parallaxy) {
Expand Down
Loading

0 comments on commit 747fc8e

Please sign in to comment.