Skip to content
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
4 changes: 4 additions & 0 deletions packages/melonjs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
- Effects: colors take an alpha. Every color option and `setColor` takes `[r, g, b]` or `[r, g, b, a]`, the alpha being the color's opacity (`[r, g, b]` stays opaque), `RadialGradientEffect` honours the alpha of its `Color`, and `DropShadowEffect` gains the `setColor` the others have
- Effects: `SwayEffect` leans a sprite or a layer from side to side, as if in a breeze, its top swaying most and everything from its anchor row down staying put. It is a `WaveEffect`, which now takes an `anchor` for the row its wave fades out at, and it wraps its own time so it never stutters on a phone's medium-precision floats
- Skills: the effects skill covers colors with an alpha and `SwayEffect`, with an example of each
- Tiled: a tile object with no class of its own is built by the class registered under its tile's class, as Tiled shows it, so a class set once on a tile in the tileset applies to every object placed from it. The object's own class or registered name still comes first, and a tile classed like a built-in (`Sprite`, `Trigger`, `shape`...) stays a plain tile
- Tiled: `TMXObject#resolvedProperty()` and `resolvedProperties()` return an object's custom properties together with those it inherits from its tile, the object's own winning, as the methods of the same names in Tiled's scripting API do. Only the object's own properties are set on it by name, as before
- Tiled: `TMXTileset#getTileClass()` returns the class set on a tile in Tiled's tileset editor
- Typings: `TMXObject`, the settings a class registered with `registerTiledObjectClass` (or a factory registered with `registerTiledObjectFactory`) receives, is exported as a type, and both functions are typed with it. A class or factory typed with its own settings still compiles

### Fixed
- Effects: a color given four values no longer breaks the effect. WebGL rejected it, and on WebGPU the fourth value overwrote the next setting, such as an outline's width; a color of any other length now throws a `TypeError` saying so
Expand Down
57 changes: 54 additions & 3 deletions packages/melonjs/skills/melonjs-tilemaps/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: melonjs-tilemaps
description: "Use this skill for Tiled maps in melonJS — loading TMX/TSX levels, spawning entities from Tiled objects, collision shapes authored in Tiled, isometric and hexagonal maps, and image layers. Covers registerTiledObjectClass and the pool.register name contract, camera bounds, compressed maps needing the inflate plugin, and the level director API. Triggers on: Tiled, TMX, TSX, tilemap, level.load, level.load async, await level.load, tileset, ImageLayer, isometric, hexagonal, staggered, pool.register, Collectable, Trigger, object layer, collision layer, parallax, registerTiledObjectClass, registerTiledObjectFactory."
description: "Use this skill for Tiled maps in melonJS — loading TMX/TSX levels, spawning entities from Tiled objects, collision shapes authored in Tiled, isometric and hexagonal maps, and image layers. Covers registerTiledObjectClass and the pool.register name contract, camera bounds, compressed maps needing the inflate plugin, and the level director API. Triggers on: Tiled, TMX, TSX, tilemap, level.load, level.load async, await level.load, tileset, ImageLayer, isometric, hexagonal, staggered, pool.register, Collectable, Trigger, object layer, collision layer, parallax, registerTiledObjectClass, registerTiledObjectFactory, TMXObject, resolvedProperty, resolvedProperties, tile class, tile properties."
license: MIT
---

Expand Down Expand Up @@ -72,7 +72,8 @@ object group in its own `Container` named after the group.
## Spawning entities from Tiled objects

Objects are matched against a factory registry, in this order: the object's
Tiled **class**, then its **name**, then the structural fallbacks `"text"`,
Tiled **class**, then its **name**, then, for a tile object with no class of
its own, its **tile's class**, then the structural fallbacks `"text"`,
`"tile"` and `"shape"`.

```js
Expand Down Expand Up @@ -122,6 +123,53 @@ registerTiledObjectClass("Trigger", MyTrigger); // whenever you like
The throw is for a clash between two of *your* classes under one name. For
that case `registerTiledObjectFactory` overwrites and only warns.

### Classes and properties set on tiles

In Tiled, a tile can carry a class and custom properties (tileset editor), and
every object placed from that tile shows them as its own. melonJS follows:

- **The tile's class.** A tile object with no class is built by the class
registered under its tile's class, so a designer sets "Chest" once on the
chest tile and every chest dropped in the map is a `Chest`. The object's
own class or registered name still comes first, and a tile classed like a
built-in (`Sprite`, `Trigger`, `shape`...) stays a plain tile.
- **The tile's properties.** Read them with `settings.resolvedProperty(name)`
or `settings.resolvedProperties()`, which merge the tile's properties under
the object's own, as Tiled's scripting API of the same names does. They are
**not** set on `settings` by name: only the object's own properties are,
as before, so a tile property named like a sprite setting never reaches a
plain tile sprite.

```ts
import { registerTiledObjectClass, Sprite, type TMXObject } from "melonjs";

// the custom properties set on the object itself, by name
interface DoorSettings extends TMXObject {
locked?: boolean;
}

class Door extends Sprite {
locked: boolean;
facing: string;
constructor(x: number, y: number, settings: DoorSettings) {
super(x, y, { image: "door" });
this.locked = settings.locked ?? false;
// on the object, or else on the door tile in the tileset
this.facing = settings.resolvedProperty("facing") ?? "N";
}
}
registerTiledObjectClass("Door", Door); // and give the door tile the class "Door"
```

`TMXObject` is the type of the `settings` a registered class (or factory)
receives: the object as read from the map, with `tile` (a `Tile`) on a tile
object. Extend it with the properties your class reads.

For a class that is not a `Sprite` itself (a `Container` holding the tile and
something else, say), `settings.tile.getRenderable()` returns the tile's sprite
as a plain tile object would get it: from a tileset image, an image collection
or an animated tile alike, with no region names to build by hand.

melonJS pre-registers `Renderable`, `Sprite`, `NineSliceSprite`, `Text`,
`BitmapText`, `ImageLayer`, `ColorLayer`, `Light2d`, `Collectable`, `Trigger`
and `Entity` as Tiled classes, so `Collectable` and `Trigger` work out of the
Expand Down Expand Up @@ -171,7 +219,8 @@ An object that matches no factory and carries no text or tile data falls to the
geometry, which the world auto-registers with the active adapter on `addChild`.

For per-object physics, set the object's custom properties in Tiled; they arrive
on the settings object passed to your entity's constructor.
on the settings object passed to your entity's constructor. Properties set on
the object's tile are read with `settings.resolvedProperty(name)`.

## Map orientations

Expand Down Expand Up @@ -281,6 +330,8 @@ unanimated layer into the offscreen-bake path instead.
| a character is always in front of (or behind) every wall | the walls are on a tile layer, or on another object layer than the character: each layer has its own `z`, compared before `y` |
| a small prop draws over a character standing in front of it | images of different heights: the y-sort uses `pos.y`, the top of a Tiled object's image |
| a character is stopped short of walls and props placed as tile objects | it collides with their default bodies (static, `ENEMY_OBJECT`, the whole image box); mask it to `collision.types.WORLD_SHAPE` when the collision layer holds the real shapes |
| a property set on a tile is undefined on the object's `settings` | tile properties are not copied onto `settings`: read them with `settings.resolvedProperty(name)` |
| objects placed from a classed tile come out as plain sprites | that class is not registered, it is a built-in name, or the object has a class or a registered name of its own, which come first |
| missing tiles or a blank map | the `.tsx` or tileset image was not preloaded |
| tilemap slow on some machines | layer fell back off the shader path — check the console warning for the reason |

Expand Down
1 change: 1 addition & 0 deletions packages/melonjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export * as input from "./input/input.ts";
export * from "./lang/deprecated.js";
export type { LevelLoadOptions } from "./level/level.js";
export { level } from "./level/level.js";
export type { default as TMXObject } from "./level/tiled/TMXObject.js";
export {
registerTiledObjectClass,
registerTiledObjectFactory,
Expand Down
124 changes: 117 additions & 7 deletions packages/melonjs/src/level/tiled/TMXObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { vector2dPool } from "../../math/vector2d.ts";
import Tile from "./TMXTile.js";
import { applyTMXProperties } from "./TMXUtils.js";

/**
* @import TMXTileMap from "./TMXTileMap.js";
*/

/**
* Detect the shape type from the TMX object settings.
* Shape markers are mutually exclusive; the first match wins.
Expand Down Expand Up @@ -36,12 +40,55 @@ function detectShape(settings) {
}

/**
* a TMX Object defintion, as defined in Tiled
* (Object definition is translated into the virtual `app.world` using `me.Renderable`)
* @ignore
* @internal
* An object of a Tiled map, as read from the map. It is the `settings` that
* a class registered with {@link registerTiledObjectClass} receives (and
* that a factory registered with {@link registerTiledObjectFactory} is
* given), before the object is turned into a renderable.
* <br><br>
* The object's own custom properties are set on it by name.
* {@link TMXObject#resolvedProperties} and {@link TMXObject#resolvedProperty}
* also include those a tile object inherits from its tile, as Tiled shows
* them. A tile object with no class of its own is matched by its tile's class.
* <br><br>
* In TypeScript, declare the custom properties a class reads by extending
* this type.
* @category Tilemap
* @example
* import { registerTiledObjectClass, Sprite, type TMXObject } from "melonjs";
*
* // the custom properties the class reads, as set on the object in Tiled
* interface ChestSettings extends TMXObject {
* locked?: boolean;
* }
*
* class Chest extends Sprite {
* locked: boolean;
* gold: number;
* constructor(x: number, y: number, settings: ChestSettings) {
* super(x, y, { image: "chest" });
* this.locked = settings.locked ?? false;
* // set on the object, or else on the chest tile in the tileset
* this.gold = settings.resolvedProperty("gold") ?? 10;
* }
* }
* // in Tiled, give the chest tile the class "Chest": every chest placed
* // in the map is then built by this class
* registerTiledObjectClass("Chest", Chest);
*/
export default class TMXObject {
/**
* the custom properties set on the object itself
* @type {Object<string, *>}
*/
#properties = Object.create(null);

/**
* @param {TMXTileMap} map - the map the object belongs to
* @param {object} settings - the object as parsed from the map
* @param {number} z - the depth of the object's layer
* @ignore
* @internal
*/
constructor(map, settings, z) {
/**
* point list in JSON format
Expand Down Expand Up @@ -88,7 +135,7 @@ export default class TMXObject {
/**
* object gid value
* when defined the object is a tiled object
* @type {number}
* @type {number|null}
*/
this.gid = +settings.gid || null;

Expand Down Expand Up @@ -193,9 +240,11 @@ export default class TMXObject {
this.text.textBaseline = settings.text.valign || "top";
this.text.width = this.width;
this.text.height = this.height;
applyTMXProperties(this.text, settings);
applyTMXProperties(this.#properties, settings);
Object.assign(this.text, this.#properties);
} else {
applyTMXProperties(this, settings);
applyTMXProperties(this.#properties, settings);
Object.assign(this, this.#properties);
if (!this.shapes) {
this.shapes = this.parseTMXShapes();
}
Expand All @@ -216,17 +265,78 @@ export default class TMXObject {
const tileset = tilesets.getTilesetByGid(this.gid);

if (tileset.isCollection === false) {
/**
* width of the object's tile in its tileset image, for a tile
* object from a tileset image (undefined otherwise)
* @type {number|undefined}
*/
this.framewidth = tileset.tilewidth;
/**
* height of the object's tile in its tileset image, for a tile
* object from a tileset image (undefined otherwise)
* @type {number|undefined}
*/
this.frameheight = tileset.tileheight;
// an object resized in Tiled keeps its size, and its tile is
// stretched to fill it; one saved without a size is the tile's
this.width ||= tileset.tilewidth;
this.height ||= tileset.tileheight;
}

/**
* the tile of a tile object (one with a `gid`), undefined otherwise
* @type {Tile|undefined}
*/
this.tile = new Tile(this.x, this.y, this.gid, tileset);
}

/**
* Return the object's custom properties, including those it inherits
* from its tile (for a tile object), as Tiled resolves them: a property
* the object sets itself wins over its tile's. Each call returns a new
* object; the values themselves are not copied.
* @returns {Object<string, *>} the resolved custom properties
* @example
* // a door tile has a "facing" property in the tileset: every door
* // object placed from it has it, unless the object sets its own
* const { facing = "N" } = settings.resolvedProperties();
*/
resolvedProperties() {
return { ...this.inheritedProperties(), ...this.#properties };
}

/**
* Return the value of one custom property of the object, or else of its
* tile (for a tile object), as Tiled resolves it.
* @param {string} name - name of the custom property
* @returns {*} the property's value, or undefined if neither the object
* nor its tile sets it
* @example
* const opens = settings.resolvedProperty("opens");
*/
resolvedProperty(name) {
if (Object.hasOwn(this.#properties, name)) {
return this.#properties[name];
}
const inherited = this.inheritedProperties();
return inherited !== undefined && Object.hasOwn(inherited, name)
? inherited[name]
: undefined;
}

/**
* the custom properties of the object's tile, if it is a tile object
* @returns {object|undefined}
* @ignore
* @internal
*/
inheritedProperties() {
const tile = this.tile;
return tile !== undefined
? tile.tileset.getTileProperties(tile.tileId)
: undefined;
}

/**
* parses the TMX shape definition and returns a corresponding array of shape objects
* @private
Expand Down
51 changes: 45 additions & 6 deletions packages/melonjs/src/level/tiled/TMXObjectFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import { createTextObject } from "./factories/text.js";
import { createTileObject } from "./factories/tile.js";
import TMXLayer from "./TMXLayer.js";

/**
* @import Renderable from "../../renderable/renderable.js";
* @import TMXObject from "./TMXObject.js";
* @import TMXTileMap from "./TMXTileMap.js";
*/

/**
* registry of Tiled object factory functions
* @ignore
Expand Down Expand Up @@ -41,6 +47,13 @@ let factoriesInitialized = false;
*/
const builtinNames = new Set();

/**
* the factories for an object's shape rather than its class
* @ignore
* @internal
*/
const STRUCTURAL_TYPES = new Set(["text", "tile", "shape"]);

/**
* Return a default shape (polygon) for the given dimensions,
* or the existing shapes if already defined in settings.
Expand Down Expand Up @@ -70,9 +83,11 @@ export function getDefaultShape(settings) {
* 1. TMXLayer instances → "layer" (passthrough)
* 2. Object class matching a registered factory → class name
* 3. Object name matching a registered factory → name
* 4. Object with text data → "text"
* 5. Object with tile/gid data → "tile"
* 6. Everything else → "shape"
* 4. A tile object with no class, whose tile's class matches a factory or
* class the game registered (never a built-in) → the tile's class
* 5. Object with text data → "text"
* 6. Object with tile/gid data → "tile"
* 7. Everything else → "shape"
* @param {object} settings - TMX object settings
* @returns {string} the factory type key
* @ignore
Expand All @@ -90,6 +105,20 @@ export function detectObjectType(settings) {
if (typeof settings.name === "string" && factories.has(settings.name)) {
return settings.name;
}
// a tile object with no class of its own takes its tile's class, as in
// Tiled. Only for a name the game registered itself, so a tile classed
// like a built-in ("shape", "Trigger"...) stays a tile
if (!settings.class && typeof settings.tile === "object") {
const tileClass = settings.tile.tileset.getTileClass(settings.tile.tileId);
if (
tileClass !== undefined &&
factories.has(tileClass) &&
!STRUCTURAL_TYPES.has(tileClass) &&
!builtinNames.has(tileClass)
) {
return tileClass;
}
}
if (typeof settings.text === "object") {
return "text";
}
Expand All @@ -106,12 +135,14 @@ export function detectObjectType(settings) {
* <br><br>
* Built-in structural types are: `"text"`, `"tile"`, `"shape"`.
* Custom types are matched against the object's Tiled `class` or `name` property.
* A tile object with no class of its own is matched by its tile's class.
* <br><br>
* For simple cases where you just want to map a Tiled class to a constructor,
* use {@link registerTiledObjectClass} instead.
* @category Tilemap
* @template [S=TMXObject]
* @param {string} type - the object type key (built-in type or Tiled class/name)
* @param {Function} factory - factory function with signature `(settings, map) => Renderable`
* @param {(settings: S, map: TMXTileMap) => Renderable} factory - factory function, given the Tiled object (a {@link TMXObject}) and its map
* @example
* // register a custom factory for "Spine" objects in Tiled
* // (set the object class to "Spine" in Tiled, and add atlasFile/jsonFile custom properties)
Expand Down Expand Up @@ -147,7 +178,14 @@ export function registerTiledObjectFactory(type, factory) {
/**
* Register a class constructor as a Tiled object factory.
* When an object with a matching `class` or `name` is found in a Tiled map,
* an instance of the given constructor will be created with `new Constructor(x, y, settings)`.
* an instance of the given constructor will be created with `new Constructor(x, y, settings)`,
* `settings` being the {@link TMXObject} as read from the map.
* <br><br>
* A tile object with no class of its own is matched by its tile's class, as in
* Tiled: give a tile a class in Tiled's tileset editor, and every object placed
* from that tile is built by the class registered under that name (an object's
* own class or name, when registered, still comes first). The tile's custom
* properties are read with {@link TMXObject#resolvedProperty}.
* <br><br>
* This is a convenience wrapper around {@link registerTiledObjectFactory}.
* If the same class is registered twice with the same constructor, the call is silently ignored.
Expand All @@ -156,8 +194,9 @@ export function registerTiledObjectFactory(type, factory) {
* Note: classes registered via {@link pool.register} are also automatically registered
* as Tiled object factories (unless {@link pool.autoRegisterTiled} is set to `false`).
* @category Tilemap
* @template [S=TMXObject]
* @param {string} name - the Tiled class or object name to match
* @param {Function} Constructor - class constructor with signature `(x, y, settings)`
* @param {new (x: number, y: number, settings: S) => Renderable} Constructor - class constructor, given the object's position and the object (a {@link TMXObject})
* @example
* // register a custom enemy class for use in Tiled maps
* // In Tiled: set the object class to "Enemy" and add custom properties
Expand Down
Loading
Loading