Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Tiled Animations #348

Merged
merged 4 commits into from
May 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ tiledMapResource.addTiledMapToScene(game.currentScene);

![](./readme/excalibur-object.png)

* **Camera Position & Zoom** - You may set the starting camera position and zoom
* **Camera Object Position & Zoom** - You may set the starting camera position and zoom

![](./readme/camera.png)
- In an object layer with a custom property "excalibur"=true
Expand All @@ -83,9 +83,9 @@ tiledMapResource.addTiledMapToScene(game.currentScene);

![](./readme/solid.png)
- In the Tiled layer properties, add a custom property named "Solid" with a boolean value `true`
- The presence of a tile in this layer indicates that space is solid, the abscence of a tile means it is not solid
- The presence of a tile in this layer indicates that space is solid, the absence of a tile means it is not solid

* **Colliders** - You may position Excalibur colliders within Tiled
* **Colliders Objects** - You may position Excalibur colliders within Tiled
![](./readme/collider.png)
- In an object layer with a custom property "excalibur"=true
- Create a "Circle" (ellipses are not supported) or "Rectangle"
Expand All @@ -96,15 +96,20 @@ tiledMapResource.addTiledMapToScene(game.currentScene);
- "Active" - participates in collision and can be pushed around
- "PreventCollision" - all collisions are ignored

* **Tile Custom Colliders** - You can leverage custom colliders in Tiled and they will be pulled into excalibur
![Add a tile collider](./readme/tile-collider.png)
- Must be in a layer marked with a custom property named "solid" with a value `true`
- Colliders are "Fixed"

* **Text** - You may insert excalibur labels within Tiled
![](./readme/text.png)
![Example of Tiled text](./readme/text.png)
- In an object layer with a custom property "excalibur"=true
- Create a Tiled Text object
- Optionally, you can set the "ZIndex" as a float custom tiled property
- **⚠ A word of caution around fonts ⚠** - fonts are different on every operating system (some may not be available to your user unless you explicitly load them into the page with a font loader). See [here for some detail](https://erikonarheim.com/posts/dont-test-fonts/)

* **Inserted Tile Objects** - You may insert tiles on or off grid in Tiled with inserted tiles
![](./readme/insertedtile.png)
![Example of an inserted Tile](./readme/insertedtile.png)
- In an object layer with a custom property "excalibur"=true
- Create a Tiled inserted Tile
- Optionally, you can set the "ZIndex" as a float custom tiled property
Expand All @@ -114,9 +119,15 @@ tiledMapResource.addTiledMapToScene(game.currentScene);
- "Active" - participates in collision and can be pushed around
- "PreventCollision" - all collisions are ignored


* **Tile Animations** - You can leverage tile animations in Tiled and they will be pulled into excalibur
![Tiled Tile Animation Editor](/./readme/animations.png)

* **Isometric Tile Maps** - Tiled isometric maps now work without any additional configuration!

## Not Yet Supported Out of the Box

* Currently Isometric and Hexagonal maps are not directly supported by Excalibur TileMaps, however the data is still parsed by this plugin and can be used manually by accessing the `RawTiledMap` in `TiledMapResource.data.rawMap` after loading.
* Currently Hexagonal maps are not directly supported by Excalibur TileMaps, however the data is still parsed by this plugin and can be used manually by accessing the `RawTiledMap` in `TiledMapResource.data.rawMap` after loading.

* Excalibur Text is limited at the moment and doesn't support Tiled word wrapping or Tiled text alignment other than the default "Left" horizontal, "Top" vertical alignments.

Expand Down
36 changes: 32 additions & 4 deletions example/example-city.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.8" tiledversion="1.8.0" orientation="orthogonal" renderorder="right-down" width="100" height="100" tilewidth="16" tileheight="16" infinite="0" nextlayerid="9" nextobjectid="9">
<map version="1.8" tiledversion="1.8.4" orientation="orthogonal" renderorder="right-down" width="100" height="100" tilewidth="16" tileheight="16" infinite="0" nextlayerid="10" nextobjectid="9">
<tileset firstgid="1" name="Kenny RPG Urban Pack" tilewidth="16" tileheight="16" tilecount="486" columns="27">
<image source="assets/kenny-rpg-urban-pack/tilemap_packed.png" width="432" height="288"/>
<tile id="190">
Expand Down Expand Up @@ -221,6 +221,34 @@
</object>
</objectgroup>
</tile>
<tile id="455">
<properties>
<property name="AnimationStrategy" value="loop"/>
<property name="OtherProp" value="someval"/>
</properties>
<animation>
<frame tileid="455" duration="300"/>
<frame tileid="482" duration="300"/>
</animation>
</tile>
<tile id="456">
<animation>
<frame tileid="456" duration="300"/>
<frame tileid="483" duration="300"/>
</animation>
</tile>
<tile id="457">
<animation>
<frame tileid="457" duration="300"/>
<frame tileid="484" duration="300"/>
</animation>
</tile>
<tile id="458">
<animation>
<frame tileid="458" duration="300"/>
<frame tileid="485" duration="300"/>
</animation>
</tile>
<tile id="477">
<objectgroup draworder="index" id="2">
<object id="1" x="16.0625" y="15.9375">
Expand Down Expand Up @@ -563,12 +591,12 @@
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,329,330,331,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,256,286,256,0,0,0,285,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,231,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,456,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,459,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,457,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
236,236,236,236,236,236,236,236,236,236,236,236,236,237,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
263,263,263,263,263,263,263,263,263,263,263,263,263,264,0,0,0,0,0,0,0,446,446,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
263,263,263,263,263,263,263,263,263,263,263,263,263,237,0,0,0,0,0,0,0,329,331,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
Expand Down
2 changes: 2 additions & 0 deletions files.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
declare module '*.tmx';
declare module '*.tmj';
declare module '*.tsx';
declare module '*.tsj';
declare module '*.json';
2 changes: 2 additions & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ module.exports = function(config) {
{ pattern: './example/**/*.png', included: false, served: true },
{ pattern: './test/**/*.tmx', included: false, served: true },
{ pattern: './test/**/*.tsx', included: false, served: true },
{ pattern: './test/**/*.tmj', included: false, served: true },
{ pattern: './test/**/*.tsj', included: false, served: true },
{ pattern: './test/**/*.png', included: false, served: true }
],

Expand Down
Binary file added readme/animations.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added readme/tile-collider.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/raw-tileset-tile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface RawTilesetTile {
imageheight: number;
imagewidth: number;
animation: TiledFrame[];
properites: TiledProperty[];
properties?: TiledProperty[];
terrain: number[];
objectgroup: RawTiledLayer;
probability: number;
Expand Down
21 changes: 19 additions & 2 deletions src/tiled-map-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import {
Collider,
CompositeCollider,
IsometricMap,
IsometricEntityComponent
IsometricEntityComponent,
Animation
} from 'excalibur';
import { ExcaliburData } from './tiled-types';
import { RawTiledTileset } from "./raw-tiled-tileset";
Expand Down Expand Up @@ -514,6 +515,17 @@ export class TiledMapResource implements Loadable<TiledMap> {
return [];
}

public getAnimationForGid(gid: number): Animation | null {
const normalizedGid = getCanonicalGid(gid);
const tileset = this.getTilesetForTile(normalizedGid);
const tileIndex = normalizedGid - tileset.firstGid;
const tileWithAnimation = tileset.tiles.find(t => t.id === tileIndex);
if (tileWithAnimation && tileWithAnimation.hasAnimation()) {
return tileWithAnimation.getAnimation(this);
}
return null;
}

private _calculateZIndex(entity: TiledEntity, tileLayerOrObjectGroup: TiledLayer | TiledObjectGroup): number {
let finalZ = entity.getProperty<number>('z')?.value ?? entity.getProperty<number>('zindex')?.value;

Expand Down Expand Up @@ -560,7 +572,7 @@ export class TiledMapResource implements Loadable<TiledMap> {
rows: this.data.height
});
tileMapLayer.addComponent(new TiledLayerComponent(layer));

// I know this looks goofy, but the entity and the layer "it belongs" to are the same here
tileMapLayer.z = this._calculateZIndex(layer, layer);
for (let i = 0; i < rawLayer.data.length; i++) {
Expand All @@ -572,6 +584,11 @@ export class TiledMapResource implements Loadable<TiledMap> {
for (let collider of colliders) {
tileMapLayer.tiles[i].addCollider(collider);
}
const animation = this.getAnimationForGid(gid);
if (animation) {
tileMapLayer.tiles[i].clearGraphics();
tileMapLayer.tiles[i].addGraphic(animation);
}
}
}
this._mapToRawLayer.set(tileMapLayer, rawLayer);
Expand Down
76 changes: 64 additions & 12 deletions src/tiled-tileset.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// tmx xml parsing
import { Matrix, vec } from 'excalibur';
import { Matrix, vec, Animation, Sprite, Frame, AnimationStrategy } from 'excalibur';
import * as parser from 'fast-xml-parser'
import { TiledLayer, TiledObjectGroup } from '.';
import { TiledObjectGroup } from '.';

import { TiledGrid, TiledMapTerrain, TiledProperty, TiledTileOffset, TiledWangSet } from "./tiled-types";
import { TiledFrame, TiledGrid, TiledMapTerrain, TiledProperty, TiledTileOffset, TiledWangSet } from "./tiled-types";
import { RawTiledTileset } from "./raw-tiled-tileset";
import { RawTilesetTile } from "./raw-tileset-tile";
import { TiledMapResource } from './tiled-map-resource';
import { getProperty } from './tiled-entity';

export class TiledTileset {
/**
Expand Down Expand Up @@ -112,10 +114,10 @@ export class TiledTileset {
let tiles: TiledTilesetTile[] = []
if (!Array.isArray(rawTileSet.tiles)) {
for (let id in (rawTileSet.tiles as any)) {
tiles.push(TiledTilesetTile.parse({...(rawTileSet.tiles as any)[id], id: +id}));
tiles.push(TiledTilesetTile.parse({...(rawTileSet.tiles as any)[id], id: +id}, tileSet));
}
} else {
tiles = (rawTileSet.tiles ?? []).map(t => TiledTilesetTile.parse(t));
tiles = (rawTileSet.tiles ?? []).map(t => TiledTilesetTile.parse(t, tileSet));
}

tileSet.tiles = tiles;
Expand Down Expand Up @@ -143,19 +145,67 @@ export class TiledTileset {

export class TiledTilesetTile {
id!: number;
tileset!: TiledTileset;
objectgroup?: TiledObjectGroup;
terrain?: number[];
animation?: TiledFrame[];
animationStrategy?: AnimationStrategy;
properties?: TiledProperty[];

hasAnimation() {
return !!this.animation;
}

getAnimation(map: TiledMapResource): Animation | null {
if (this.animation) {
let exFrames: Frame[] = [];
for (let frame of this.animation) {
exFrames.push({
graphic: map.getSpriteForGid(frame.tileid + this.tileset.firstGid),
duration: frame.duration
});
}
return new Animation({
frames: exFrames,
strategy: this.animationStrategy ?? AnimationStrategy.Loop
});
}
return null;
}

public static parse(rawTilesetTile: RawTilesetTile) {
public static parse(rawTilesetTile: RawTilesetTile, tileset: TiledTileset) {
const tile = new TiledTilesetTile();
tile.id = +rawTilesetTile.id;
tile.tileset = tileset;
if (rawTilesetTile.objectgroup) {
tile.objectgroup = TiledObjectGroup.parse(rawTilesetTile.objectgroup);
}
if (rawTilesetTile.terrain) {
tile.terrain = rawTilesetTile.terrain;
}
if (rawTilesetTile.animation) {
tile.animation = Array.isArray(rawTilesetTile.animation) ? rawTilesetTile.animation : [...(rawTilesetTile.animation as any).frame];
tile.properties = Array.isArray(rawTilesetTile.properties) ? rawTilesetTile.properties : (rawTilesetTile.properties as any)?.property ?? [];
if (tile.properties) {
const maybeStrategy = getProperty<string>(tile.properties, "animationstrategy")?.value;
switch(maybeStrategy?.toLowerCase()) {
case AnimationStrategy.End.toLowerCase():
tile.animationStrategy = AnimationStrategy.End;
break;
case AnimationStrategy.Freeze.toLowerCase():
tile.animationStrategy = AnimationStrategy.Freeze;
break;
case AnimationStrategy.Loop.toLowerCase():
tile.animationStrategy = AnimationStrategy.Loop;
break;
case AnimationStrategy.PingPong.toLowerCase():
tile.animationStrategy = AnimationStrategy.PingPong;
break;
default:
tile.animationStrategy = AnimationStrategy.Loop;
}
}
}
return tile;
}
}
Expand Down Expand Up @@ -207,7 +257,7 @@ export const parseExternalTsx = (tsxData: string, firstGid: number, source: stri

const result: TiledTileset = {
...rawTileset,
tiles: rawTileset.tiles.map(t => TiledTilesetTile.parse(t)),
tiles: [],
firstGid: rawTileset.firstgid,
tileWidth: rawTileset.tilewidth,
tileHeight: rawTileset.tileheight,
Expand All @@ -226,15 +276,13 @@ export const parseExternalTsx = (tsxData: string, firstGid: number, source: stri
diagonalFlipTransform: Matrix.identity().translate(rawTileset.tilewidth, rawTileset.tileheight).rotate(-Math.PI/2).scale(-1, 1)
};

result.tiles = rawTileset.tiles.map(t => TiledTilesetTile.parse(t, result));

return result;
}

export const parseExternalJson = (rawTileset: RawTiledTileset, firstGid: number, source: string): TiledTileset => {

let tiles: TiledTilesetTile[] = []
for (let id in rawTileset.tiles) {
tiles.push(TiledTilesetTile.parse({...rawTileset.tiles[id], id: +id}));
}
let tiles: TiledTilesetTile[] = [];

rawTileset.tiles = rawTileset.tiles ?? [];

Expand All @@ -261,5 +309,9 @@ export const parseExternalJson = (rawTileset: RawTiledTileset, firstGid: number,
diagonalFlipTransform: Matrix.identity().translate(rawTileset.tilewidth, rawTileset.tileheight).rotate(-Math.PI/2).scale(-1, 1)
};

for (let id in rawTileset.tiles) {
tiles.push(TiledTilesetTile.parse({...rawTileset.tiles[id], id: +id}, result));
}

return result;
}
37 changes: 37 additions & 0 deletions test/unit/animation.tmx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.8" tiledversion="1.8.4" orientation="orthogonal" renderorder="right-down" width="5" height="5" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="8">
<editorsettings>
<export format="json"/>
</editorsettings>
<tileset firstgid="1" name="Kenny RPG Urban Pack" tilewidth="16" tileheight="16" tilecount="486" columns="27">
<image source="assets/kenny-rpg-urban-pack/tilemap_packed.png" width="432" height="288"/>
<tile id="278">
<animation>
<frame tileid="456" duration="300"/>
<frame tileid="483" duration="300"/>
<frame tileid="429" duration="300"/>
</animation>
</tile>
</tileset>
<layer id="1" name="Ground" width="5" height="5">
<data encoding="csv">
1,2,2,2,3,
28,29,29,29,30,
28,29,29,29,30,
28,29,29,29,30,
55,56,56,56,57
</data>
</layer>
<layer id="3" name="Solid" width="5" height="5">
<properties>
<property name="solid" type="bool" value="true"/>
</properties>
<data encoding="csv">
0,0,0,0,0,
0,0,0,0,0,
0,0,279,0,0,
0,0,0,0,0,
0,0,0,0,0
</data>
</layer>
</map>
Loading