Skip to content

Commit fd72b1e

Browse files
authored
Merge pull request #1340 from melonjs/feature/tiled-object-factory
Add extensible Tiled object factory registry
2 parents 6528f74 + 8b28a9d commit fd72b1e

19 files changed

Lines changed: 2117 additions & 221 deletions

File tree

packages/melonjs/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22

33
## [18.3.0] (melonJS 2)
44

5+
### Added
6+
- Tiled: extensible object factory registry for `TMXTileMap.getObjects()` — object creation is now dispatched through a `Map`-based registry (like `loader.setParser`), with built-in factories for text, tile, and shape objects, plus class-based factories for Entity, Collectable, Trigger, Light2d, Sprite, NineSliceSprite, ImageLayer, and ColorLayer
7+
- Tiled: new public `registerTiledObjectFactory(type, factory)` and `registerTiledObjectClass(name, Constructor)` APIs allowing plugins to register custom Tiled object handlers by class name without modifying engine code
8+
- Tiled: `detectObjectType()` now checks `settings.class` and `settings.name` against the factory registry before falling through to structural detection, enabling class-based dispatch for custom types
9+
510
### Fixed
611
- WebGLRenderer: `setBlendMode()` now tracks the `premultipliedAlpha` flag — previously only the mode name was checked, causing incorrect GL blend function when mixing PMA and non-PMA textures with the same blend mode
12+
- TMX: fix crash in `getObjects(false)` when a map contains an empty object group (Container.children lazily initialized)
713

814
### Chore
915
- Minimum Node.js version is now 24.0.0 (Node 18/20 EOL, Node 22 in maintenance)

packages/melonjs/src/index.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ export * as input from "./input/input.ts";
8484
// Backward compatibility for deprecated method or properties
8585
export * from "./lang/deprecated.js";
8686
export { level } from "./level/level.js";
87+
export {
88+
registerTiledObjectClass,
89+
registerTiledObjectFactory,
90+
} from "./level/tiled/TMXObjectFactory.js";
91+
92+
import { registerBuiltinTiledClass } from "./level/tiled/TMXObjectFactory.js";
93+
8794
export * as loader from "./loader/loader.js";
8895
export { Color } from "./math/color.ts";
8996
export * as math from "./math/math.ts";
@@ -216,6 +223,21 @@ export function boot() {
216223
pool.register("Tween", Tween, true);
217224
pool.register("ColorLayer", ColorLayer, true);
218225

226+
// ensure built-in classes are registered as Tiled object factories
227+
// (redundant with pool.register auto-registration, but ensures
228+
// built-ins remain available if pool behavior changes in the future)
229+
registerBuiltinTiledClass("Renderable", Renderable);
230+
registerBuiltinTiledClass("Text", Text);
231+
registerBuiltinTiledClass("BitmapText", BitmapText);
232+
registerBuiltinTiledClass("Entity", Entity);
233+
registerBuiltinTiledClass("Collectable", Collectable);
234+
registerBuiltinTiledClass("Trigger", Trigger);
235+
registerBuiltinTiledClass("Light2d", Light2d);
236+
registerBuiltinTiledClass("Sprite", Sprite);
237+
registerBuiltinTiledClass("NineSliceSprite", NineSliceSprite);
238+
registerBuiltinTiledClass("ImageLayer", ImageLayer);
239+
registerBuiltinTiledClass("ColorLayer", ColorLayer);
240+
219241
// publish Boot notification
220242
eventEmitter.emit(BOOT);
221243

packages/melonjs/src/level/level.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,9 @@ export const level = {
9595
case "tmx":
9696
// just load the level with the XML stuff
9797
if (levels[levelId] == null) {
98-
//console.log("loading "+ levelId);
9998
levels[levelId] = new TMXTileMap(levelId, getTMX(levelId));
100-
// level index
10199
levelIdx.push(levelId);
102100
} else {
103-
//console.log("level %s already loaded", levelId);
104101
return false;
105102
}
106103

packages/melonjs/src/level/tiled/TMXGroup.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { clamp } from "./../../math/math.ts";
1+
import { clamp } from "../../math/math.ts";
22
import TMXLayer from "./TMXLayer.js";
33
import TMXObject from "./TMXObject.js";
44
import { applyTMXProperties, tiledBlendMode } from "./TMXUtils.js";

packages/melonjs/src/level/tiled/TMXLayer.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { vector2dPool } from "../../math/vector2d.ts";
2-
import Renderable from "./../../renderable/renderable.js";
3-
import CanvasRenderer from "./../../video/canvas/canvas_renderer";
4-
import { createCanvas } from "./../../video/video.js";
2+
import Renderable from "../../renderable/renderable.js";
3+
import CanvasRenderer from "../../video/canvas/canvas_renderer";
4+
import { createCanvas } from "../../video/video.js";
55
import Tile from "./TMXTile.js";
66
import * as TMXUtils from "./TMXUtils.js";
77

@@ -210,10 +210,6 @@ export default class TMXLayer extends Renderable {
210210

211211
// called when the layer is added to the game world or a container
212212
onActivateEvent() {
213-
if (this.animatedTilesets === undefined) {
214-
this.animatedTilesets = [];
215-
}
216-
217213
if (this.tilesets) {
218214
const tileset = this.tilesets.tilesets;
219215
for (let i = 0; i < tileset.length; i++) {

packages/melonjs/src/level/tiled/TMXObject.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { linePool } from "../../geometries/line.ts";
33
import { pointPool } from "../../geometries/point.ts";
44
import { polygonPool } from "../../geometries/polygon.ts";
55
import { roundedRectanglePool } from "../../geometries/roundrect.ts";
6-
import { degToRad } from "./../../math/math.ts";
6+
import { degToRad } from "../../math/math.ts";
77
import { vector2dPool } from "../../math/vector2d.ts";
88
import Tile from "./TMXTile.js";
99
import { applyTMXProperties } from "./TMXUtils.js";
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
import { polygonPool } from "../../geometries/polygon.ts";
2+
import { warning } from "../../lang/console.js";
3+
import { vector2dPool } from "../../math/vector2d.ts";
4+
import { setPoolRegisterCallback } from "../../system/legacy_pool.js";
5+
import { createShapeObject } from "./factories/shape.js";
6+
import { createTextObject } from "./factories/text.js";
7+
import { createTileObject } from "./factories/tile.js";
8+
import TMXLayer from "./TMXLayer.js";
9+
10+
/**
11+
* registry of Tiled object factory functions
12+
* @ignore
13+
*/
14+
const factories = new Map();
15+
16+
/**
17+
* tracks class constructors registered via registerTiledObjectClass
18+
* (used to detect duplicate registrations with different constructors)
19+
* @ignore
20+
*/
21+
const registeredClasses = new Map();
22+
23+
/**
24+
* whether built-in factories have been registered
25+
* @ignore
26+
*/
27+
let factoriesInitialized = false;
28+
29+
/**
30+
* Return a default shape (polygon) for the given dimensions,
31+
* or the existing shapes if already defined in settings.
32+
* @param {object} settings - TMX object settings
33+
* @returns {Polygon|object[]} shape(s) for the object body
34+
* @ignore
35+
*/
36+
export function getDefaultShape(settings) {
37+
if (typeof settings.shapes !== "undefined") {
38+
return settings.shapes;
39+
}
40+
return polygonPool.get(0, 0, [
41+
vector2dPool.get(0, 0),
42+
vector2dPool.get(settings.width, 0),
43+
vector2dPool.get(settings.width, settings.height),
44+
]);
45+
}
46+
47+
/**
48+
* Detect the object type from its TMX settings.
49+
* The detection order is:
50+
* 1. TMXLayer instances → "layer" (passthrough)
51+
* 2. Object class matching a registered factory → class name
52+
* 3. Object name matching a registered factory → name
53+
* 4. Object with text data → "text"
54+
* 5. Object with tile/gid data → "tile"
55+
* 6. Everything else → "shape"
56+
* @param {object} settings - TMX object settings
57+
* @returns {string} the factory type key
58+
* @ignore
59+
*/
60+
export function detectObjectType(settings) {
61+
if (settings instanceof TMXLayer) {
62+
return "layer";
63+
}
64+
// check if a factory is registered for this object's class
65+
if (typeof settings.class === "string" && factories.has(settings.class)) {
66+
return settings.class;
67+
}
68+
// check if a factory is registered for this object's name
69+
if (typeof settings.name === "string" && factories.has(settings.name)) {
70+
return settings.name;
71+
}
72+
if (typeof settings.text === "object") {
73+
return "text";
74+
}
75+
if (typeof settings.tile === "object") {
76+
return "tile";
77+
}
78+
return "shape";
79+
}
80+
81+
/**
82+
* Register a factory function for a given Tiled object type.
83+
* When a Tiled map is loaded, objects are matched to factories using the following
84+
* priority: object class → object name → structural type ("text", "tile", "shape").
85+
* <br><br>
86+
* Built-in structural types are: `"text"`, `"tile"`, `"shape"`.
87+
* Custom types are matched against the object's Tiled `class` or `name` property.
88+
* <br><br>
89+
* For simple cases where you just want to map a Tiled class to a constructor,
90+
* use {@link registerTiledObjectClass} instead.
91+
* @category Tilemap
92+
* @param {string} type - the object type key (built-in type or Tiled class/name)
93+
* @param {Function} factory - factory function with signature `(settings, map) => Renderable`
94+
* @example
95+
* // register a custom factory for "Spine" objects in Tiled
96+
* // (set the object class to "Spine" in Tiled, and add atlasFile/jsonFile custom properties)
97+
* registerTiledObjectFactory("Spine", (settings, map) => {
98+
* const obj = new Spine(settings.x, settings.y, settings);
99+
* obj.pos.z = settings.z;
100+
* return obj;
101+
* });
102+
*
103+
* @example
104+
* // override the built-in "shape" factory to add custom behavior
105+
* registerTiledObjectFactory("shape", (settings, map) => {
106+
* const obj = new Renderable(settings.x, settings.y, settings.width, settings.height);
107+
* obj.pos.z = settings.z;
108+
* // add custom initialization...
109+
* return obj;
110+
* });
111+
*/
112+
export function registerTiledObjectFactory(type, factory) {
113+
if (typeof factory !== "function") {
114+
throw new Error("invalid factory function for " + type);
115+
}
116+
117+
if (typeof factories.get(type) !== "undefined") {
118+
warning("overriding Tiled object factory for " + type + " type");
119+
}
120+
121+
factories.set(type, factory);
122+
}
123+
124+
/**
125+
* Register a class constructor as a Tiled object factory.
126+
* When an object with a matching `class` or `name` is found in a Tiled map,
127+
* an instance of the given constructor will be created with `new Constructor(x, y, settings)`.
128+
* <br><br>
129+
* This is a convenience wrapper around {@link registerTiledObjectFactory}.
130+
* If the same class is registered twice with the same constructor, the call is silently ignored.
131+
* If a different constructor is registered for the same name, an error is thrown.
132+
* <br><br>
133+
* Note: classes registered via {@link pool.register} are also automatically registered
134+
* as Tiled object factories (unless {@link pool.autoRegisterTiled} is set to `false`).
135+
* @category Tilemap
136+
* @param {string} name - the Tiled class or object name to match
137+
* @param {Function} Constructor - class constructor with signature `(x, y, settings)`
138+
* @example
139+
* // register a custom enemy class for use in Tiled maps
140+
* // In Tiled: set the object class to "Enemy" and add custom properties
141+
* registerTiledObjectClass("Enemy", Enemy);
142+
*
143+
* @example
144+
* // equivalent to pool.register (which auto-registers for Tiled too)
145+
* pool.register("CoinEntity", CoinEntity, true);
146+
* // CoinEntity is now available both in the pool AND as a Tiled object factory
147+
*/
148+
export function registerTiledObjectClass(name, Constructor) {
149+
const existing = registeredClasses.get(name);
150+
if (typeof existing !== "undefined") {
151+
if (existing === Constructor) {
152+
// same class already registered — no-op
153+
return;
154+
}
155+
throw new Error(
156+
"a different class is already registered for Tiled type: " + name,
157+
);
158+
}
159+
registeredClasses.set(name, Constructor);
160+
registerTiledObjectFactory(name, (settings) => {
161+
const obj = new Constructor(settings.x, settings.y, settings);
162+
obj.pos.z = settings.z;
163+
return obj;
164+
});
165+
}
166+
167+
/**
168+
* pending class registrations queued before initFactories runs
169+
* @ignore
170+
*/
171+
const pendingClasses = [];
172+
173+
/**
174+
* Queue a class for registration as a Tiled object factory.
175+
* Registrations are applied when initFactories() runs (on first
176+
* createTMXObject call), avoiding circular import issues at module load time.
177+
* @param {string} name - the Tiled class or name to match
178+
* @param {Function} Constructor - class constructor with signature (x, y, settings)
179+
* @ignore
180+
*/
181+
export function registerBuiltinTiledClass(name, Constructor) {
182+
if (factoriesInitialized) {
183+
// already initialized, register immediately
184+
registerTiledObjectClass(name, Constructor);
185+
} else {
186+
pendingClasses.push([name, Constructor]);
187+
}
188+
}
189+
190+
/**
191+
* Register built-in factories and apply pending class registrations.
192+
* Called lazily on first createTMXObject call, after all modules are fully loaded.
193+
* @ignore
194+
*/
195+
function initFactories() {
196+
// only register built-in structural factories if not already overridden
197+
if (!factories.has("text")) {
198+
registerTiledObjectFactory("text", createTextObject);
199+
}
200+
if (!factories.has("tile")) {
201+
registerTiledObjectFactory("tile", createTileObject);
202+
}
203+
if (!factories.has("shape")) {
204+
registerTiledObjectFactory("shape", createShapeObject);
205+
}
206+
207+
// apply pending class-based factories
208+
for (const entry of pendingClasses) {
209+
const [name, Constructor, factoryFn] = entry;
210+
if (typeof factoryFn === "function") {
211+
// pool-registered: use provided factory (preserves pool.pull recycling)
212+
registeredClasses.set(name, Constructor);
213+
registerTiledObjectFactory(name, factoryFn);
214+
} else {
215+
// direct registration: use new Constructor()
216+
registerTiledObjectClass(name, Constructor);
217+
}
218+
}
219+
pendingClasses.length = 0;
220+
221+
factoriesInitialized = true;
222+
}
223+
224+
/**
225+
* Instantiate a TMX object based on its settings, using the registered factory
226+
* for its detected type.
227+
* @param {object} settings - TMX object settings
228+
* @param {TMXTileMap} map - the parent tile map
229+
* @returns {Renderable} the instantiated object
230+
* @ignore
231+
*/
232+
export function createTMXObject(settings, map) {
233+
if (!factoriesInitialized) {
234+
initFactories();
235+
}
236+
237+
const type = detectObjectType(settings);
238+
239+
// TMXLayer instances are already instantiated — pass through
240+
if (type === "layer") {
241+
return settings;
242+
}
243+
244+
const factory = factories.get(type);
245+
if (typeof factory === "undefined") {
246+
throw new Error("no Tiled object factory registered for type: " + type);
247+
}
248+
249+
return factory(settings, map);
250+
}
251+
252+
// wire pool.register() to automatically register Tiled object factories
253+
// uses pool.pull instead of new Constructor to preserve object recycling
254+
setPoolRegisterCallback((className, classObj, poolInstance) => {
255+
const factoryFn = (settings) => {
256+
const obj = poolInstance.pull(className, settings.x, settings.y, settings);
257+
obj.pos.z = settings.z;
258+
return obj;
259+
};
260+
261+
if (factoriesInitialized) {
262+
registeredClasses.set(className, classObj);
263+
registerTiledObjectFactory(className, factoryFn);
264+
} else {
265+
// queue as a raw factory (not via registerTiledObjectClass which uses new Constructor)
266+
pendingClasses.push([className, classObj, factoryFn]);
267+
}
268+
});

0 commit comments

Comments
 (0)