Added a new LayersIterator class

This class iterates recursively over layers, flattening groups.
This enables us to simplify the code when we iterate layers. We can remove all recursive function calls in the GameScene code (it is delegated to the LayersIterator)
This commit is contained in:
David Négrier 2021-04-15 22:39:35 +02:00
parent 66b90be0da
commit 6815fe7a0a
6 changed files with 384 additions and 65 deletions

View File

@ -1,4 +1,5 @@
import {ITiledMap, ITiledMapLayer} from "../Map/ITiledMap"; import {ITiledMap, ITiledMapLayer} from "../Map/ITiledMap";
import {LayersIterator} from "../Map/LayersIterator";
export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map<string, string | boolean | number>) => void; export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map<string, string | boolean | number>) => void;
@ -10,8 +11,10 @@ export class GameMap {
private key: number|undefined; private key: number|undefined;
private lastProperties = new Map<string, string|boolean|number>(); private lastProperties = new Map<string, string|boolean|number>();
private callbacks = new Map<string, Array<PropertyChangeCallback>>(); private callbacks = new Map<string, Array<PropertyChangeCallback>>();
public readonly layersIterator: LayersIterator;
public constructor(private map: ITiledMap) { public constructor(private map: ITiledMap) {
this.layersIterator = new LayersIterator(map);
} }
/** /**
@ -52,15 +55,10 @@ export class GameMap {
return this.lastProperties; return this.lastProperties;
} }
// helper for recursive group layer support private getProperties(key: number): Map<string, string|boolean|number> {
private getPropertiesHelper(key: number, layers: ITiledMapLayer[], properties: Map<string, string|boolean|number>): Map<string, string|boolean|number> { const properties = new Map<string, string|boolean|number>();
for (const layer of layers) {
if (layer.type === 'group') {
this.getPropertiesHelper(key, layer.layers, properties);
continue;
}
for (const layer of this.layersIterator) {
if (layer.type !== 'tilelayer') { if (layer.type !== 'tilelayer') {
continue; continue;
} }
@ -82,11 +80,6 @@ export class GameMap {
return properties; return properties;
} }
private getProperties(key: number): Map<string, string|boolean|number> {
const properties = new Map<string, string|boolean|number>();
return this.getPropertiesHelper(key, this.map.layers, properties);
}
private trigger(propName: string, oldValue: string | number | boolean | undefined, newValue: string | number | boolean | undefined, allProps: Map<string, string | boolean | number>) { private trigger(propName: string, oldValue: string | number | boolean | undefined, newValue: string | number | boolean | undefined, allProps: Map<string, string | boolean | number>) {
const callbacksArray = this.callbacks.get(propName); const callbacksArray = this.callbacks.get(propName);
if (callbacksArray !== undefined) { if (callbacksArray !== undefined) {

View File

@ -18,7 +18,14 @@ import {
RESOLUTION, RESOLUTION,
ZOOM_LEVEL ZOOM_LEVEL
} from "../../Enum/EnvironmentVariable"; } from "../../Enum/EnvironmentVariable";
import {ITiledMap, ITiledMapLayer, ITiledMapLayerProperty, ITiledMapObject, ITiledTileSet} from "../Map/ITiledMap"; import {
ITiledMap,
ITiledMapLayer,
ITiledMapLayerProperty,
ITiledMapObject,
ITiledMapTileLayer,
ITiledTileSet
} from "../Map/ITiledMap";
import {AddPlayerInterface} from "./AddPlayerInterface"; import {AddPlayerInterface} from "./AddPlayerInterface";
import {PlayerAnimationDirections} from "../Player/Animation"; import {PlayerAnimationDirections} from "../Player/Animation";
import {PlayerMovement} from "./PlayerMovement"; import {PlayerMovement} from "./PlayerMovement";
@ -77,6 +84,7 @@ import DOMElement = Phaser.GameObjects.DOMElement;
import {Subscription} from "rxjs"; import {Subscription} from "rxjs";
import {worldFullMessageStream} from "../../Connexion/WorldFullMessageStream"; import {worldFullMessageStream} from "../../Connexion/WorldFullMessageStream";
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
import {LayersIterator} from "../Map/LayersIterator";
import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager"; import {PinchManager} from "../UserInput/PinchManager";
import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick"; import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick";
@ -353,31 +361,6 @@ export class GameScene extends ResizableScene implements CenterListener {
} }
} }
// helper for recursive group layer support
private createHelper(layers: ITiledMapLayer[], depth: integer, prefix: string): integer {
for(const layer of layers) {
if(layer.type === 'tilelayer') {
this.addLayer(this.Map.createStaticLayer(prefix + layer.name, this.Terrains, 0, 0).setDepth(depth));
const exitSceneUrl = this.getExitSceneUrl(layer);
if(exitSceneUrl !== undefined) {
this.loadNextGame(exitSceneUrl);
}
const exitUrl = this.getExitUrl(layer);
if(exitUrl !== undefined) {
this.loadNextGame(exitUrl);
}
}
if(layer.type === 'group') {
this.createHelper(layer.layers, depth, prefix + layer.name + '/');
}
if(layer.type === 'objectgroup' && layer.name === 'floorLayer') {
depth = 10000;
}
}
return depth;
}
//hook create scene //hook create scene
create(): void { create(): void {
gameManager.gameSceneIsCreated(this); gameManager.gameSceneIsCreated(this);
@ -411,9 +394,27 @@ export class GameScene extends ResizableScene implements CenterListener {
//add layer on map //add layer on map
this.Layers = new Array<Phaser.Tilemaps.StaticTilemapLayer>(); this.Layers = new Array<Phaser.Tilemaps.StaticTilemapLayer>();
if (this.createHelper(this.mapFile.layers, -2, '') === -2) { let depth = -2;
throw new Error('Your map MUST contain a layer of type "objectgroup" whose name is "floorLayer" that represents the layer characters are drawn at.'); for (const layer of this.gameMap.layersIterator) {
if (layer.type === 'tilelayer') {
this.addLayer(this.Map.createStaticLayer(layer.name, this.Terrains, 0, 0).setDepth(depth));
const exitSceneUrl = this.getExitSceneUrl(layer);
if (exitSceneUrl !== undefined) {
this.loadNextGame(exitSceneUrl);
}
const exitUrl = this.getExitUrl(layer);
if (exitUrl !== undefined) {
this.loadNextGame(exitUrl);
}
}
if (layer.type === 'objectgroup' && layer.name === 'floorLayer') {
depth = 10000;
}
}
if (depth === -2) {
throw new Error('Your map MUST contain a layer of type "objectgroup" whose name is "floorLayer" that represents the layer characters are drawn at. This layer cannot be contained in a group.');
} }
this.initStartXAndStartY(); this.initStartXAndStartY();
@ -963,20 +964,15 @@ ${escapedMessage}
} }
} }
private initPositionFromLayerNameHelper(layerName: string, layers : ITiledMapLayer[]) { private initPositionFromLayerName(layerName: string) {
for (const layer of layers) { for (const layer of this.gameMap.layersIterator) {
if (layerName === layer.name && layer.type === 'tilelayer' && (layerName === defaultStartLayerName || this.isStartLayer(layer))) { if ((layerName === layer.name || layer.name.endsWith('/'+layerName)) && layer.type === 'tilelayer' && (layerName === defaultStartLayerName || this.isStartLayer(layer))) {
const startPosition = this.startUser(layer); const startPosition = this.startUser(layer);
this.startX = startPosition.x + this.mapFile.tilewidth/2; this.startX = startPosition.x + this.mapFile.tilewidth/2;
this.startY = startPosition.y + this.mapFile.tileheight/2; this.startY = startPosition.y + this.mapFile.tileheight/2;
} else if (layer.type === 'group') {
this.initPositionFromLayerNameHelper(layerName, layer.layers);
} }
} }
}
private initPositionFromLayerName(layerName: string) {
this.initPositionFromLayerNameHelper(layerName, this.mapFile.layers);
} }
private getExitUrl(layer: ITiledMapLayer): string|undefined { private getExitUrl(layer: ITiledMapLayer): string|undefined {
@ -999,7 +995,7 @@ ${escapedMessage}
} }
private getProperty(layer: ITiledMapLayer|ITiledMap, name: string): string|boolean|number|undefined { private getProperty(layer: ITiledMapLayer|ITiledMap, name: string): string|boolean|number|undefined {
const properties: ITiledMapLayerProperty[] = layer.properties; const properties: ITiledMapLayerProperty[]|undefined = layer.properties;
if (!properties) { if (!properties) {
return undefined; return undefined;
} }
@ -1011,7 +1007,7 @@ ${escapedMessage}
} }
private getProperties(layer: ITiledMapLayer|ITiledMap, name: string): (string|number|boolean|undefined)[] { private getProperties(layer: ITiledMapLayer|ITiledMap, name: string): (string|number|boolean|undefined)[] {
const properties: ITiledMapLayerProperty[] = layer.properties; const properties: ITiledMapLayerProperty[]|undefined = layer.properties;
if (!properties) { if (!properties) {
return []; return [];
} }
@ -1025,7 +1021,7 @@ ${escapedMessage}
await gameManager.loadMap(room, this.scene); await gameManager.loadMap(room, this.scene);
} }
private startUser(layer: ITiledMapLayer): PositionInterface { private startUser(layer: ITiledMapTileLayer): PositionInterface {
const tiles = layer.data; const tiles = layer.data;
if (typeof(tiles) === 'string') { if (typeof(tiles) === 'string') {
throw new Error('The content of a JSON map must be filled as a JSON array, not as a string'); throw new Error('The content of a JSON map must be filled as a JSON array, not as a string');

View File

@ -14,7 +14,7 @@ export interface ITiledMap {
* Map orientation (orthogonal) * Map orientation (orthogonal)
*/ */
orientation: string; orientation: string;
properties: ITiledMapLayerProperty[]; properties?: ITiledMapLayerProperty[];
/** /**
* Render order (right-down) * Render order (right-down)
@ -24,6 +24,11 @@ export interface ITiledMap {
tilewidth: number; tilewidth: number;
tilesets: ITiledTileSet[]; tilesets: ITiledTileSet[];
version: number; version: number;
compressionlevel?: number;
infinite?: boolean;
nextlayerid?: number;
tiledversion?: string;
type?: string;
} }
export interface ITiledMapLayerProperty { export interface ITiledMapLayerProperty {
@ -38,19 +43,35 @@ export interface ITiledMapLayerProperty {
value: boolean value: boolean
}*/ }*/
export interface ITiledMapLayer { export type ITiledMapLayer = ITiledMapGroupLayer | ITiledMapObjectLayer | ITiledMapTileLayer;
export interface ITiledMapGroupLayer {
id?: number,
name: string;
opacity: number;
properties?: ITiledMapLayerProperty[];
type: "group";
visible: boolean;
x: number;
y: number;
/**
* Layers for group layer
*/
layers: ITiledMapLayer[];
}
export interface ITiledMapTileLayer {
id?: number,
data: number[]|string; data: number[]|string;
height: number; height: number;
name: string; name: string;
opacity: number; opacity: number;
properties: ITiledMapLayerProperty[]; properties?: ITiledMapLayerProperty[];
encoding: string; encoding?: string;
compression?: string; compression?: string;
/** type: "tilelayer";
* Type of layer (tilelayer, objectgroup)
*/
type: string;
visible: boolean; visible: boolean;
width: number; width: number;
x: number; x: number;
@ -59,13 +80,29 @@ export interface ITiledMapLayer {
/** /**
* Draw order (topdown (default), index) * Draw order (topdown (default), index)
*/ */
draworder: string; draworder?: string;
objects: ITiledMapObject[]; }
export interface ITiledMapObjectLayer {
id?: number,
height: number;
name: string;
opacity: number;
properties?: ITiledMapLayerProperty[];
encoding?: string;
compression?: string;
type: "objectgroup";
visible: boolean;
width: number;
x: number;
y: number;
/** /**
* Layers for group layer * Draw order (topdown (default), index)
*/ */
layers: this[]; draworder?: string;
objects: ITiledMapObject[];
} }
export interface ITiledMapObject { export interface ITiledMapObject {

View File

@ -0,0 +1,44 @@
import {ITiledMap, ITiledMapLayer} from "./ITiledMap";
/**
* Iterates over the layers of a map, flattening the grouped layers
*/
export class LayersIterator implements IterableIterator<ITiledMapLayer> {
private layers: ITiledMapLayer[] = [];
private pointer: number = 0;
constructor(private map: ITiledMap) {
this.initLayersList(map.layers, '');
}
private initLayersList(layers : ITiledMapLayer[], prefix : string) {
for (const layer of layers) {
if (layer.type === 'group') {
this.initLayersList(layer.layers, prefix + layer.name + '/');
} else {
const layerWithNewName = { ...layer };
layerWithNewName.name = prefix+layerWithNewName.name;
this.layers.push(layerWithNewName);
}
}
}
public next(): IteratorResult<ITiledMapLayer> {
if (this.pointer < this.layers.length) {
return {
done: false,
value: this.layers[this.pointer++]
}
} else {
return {
done: true,
value: null
}
}
}
[Symbol.iterator](): IterableIterator<ITiledMapLayer> {
return new LayersIterator(this.map);
}
}

View File

@ -0,0 +1,147 @@
import "jasmine";
import {Room} from "../../../src/Connexion/Room";
import {LayersIterator} from "../../../src/Phaser/Map/LayersIterator";
describe("Layers iterator", () => {
it("should iterate maps with no group", () => {
const layersIterator = new LayersIterator({
"compressionlevel":-1,
"height":2,
"infinite":false,
"layers":[
{
"data":[0, 0, 0, 0],
"height":2,
"id":1,
"name":"Tile Layer 1",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":2,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0],
"height":2,
"id":1,
"name":"Tile Layer 2",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":2,
"x":0,
"y":0
}],
"nextlayerid":2,
"nextobjectid":1,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"2021.03.23",
"tileheight":32,
"tilesets":[],
"tilewidth":32,
"type":"map",
"version":1.5,
"width":2
})
const layers = [];
for (const layer of layersIterator) {
layers.push(layer.name);
}
expect(layers).toEqual(['Tile Layer 1', 'Tile Layer 2']);
});
it("should iterate maps with recursive groups", () => {
const layersIterator = new LayersIterator({
"compressionlevel":-1,
"height":2,
"infinite":false,
"layers":[
{
"id":6,
"layers":[
{
"id":5,
"layers":[
{
"data":[0, 0, 0, 0],
"height":2,
"id":10,
"name":"Tile3",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":2,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0],
"height":2,
"id":9,
"name":"Tile2",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":2,
"x":0,
"y":0
}],
"name":"Group 3",
"opacity":1,
"type":"group",
"visible":true,
"x":0,
"y":0
},
{
"id":7,
"layers":[
{
"data":[0, 0, 0, 0],
"height":2,
"id":8,
"name":"Tile1",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":2,
"x":0,
"y":0
}],
"name":"Group 2",
"opacity":1,
"type":"group",
"visible":true,
"x":0,
"y":0
}],
"name":"Group 1",
"opacity":1,
"type":"group",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":11,
"nextobjectid":1,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"2021.03.23",
"tileheight":32,
"tilesets":[],
"tilewidth":32,
"type":"map",
"version":1.5,
"width":2
})
const layers = [];
for (const layer of layersIterator) {
layers.push(layer.name);
}
expect(layers).toEqual(['Group 1/Group 3/Tile3', 'Group 1/Group 3/Tile2', 'Group 1/Group 2/Tile1']);
});
});

102
maps/tests/grouped_map.json Normal file
View File

@ -0,0 +1,102 @@
{ "compressionlevel":-1,
"height":10,
"infinite":false,
"layers":[
{
"id":7,
"layers":[
{
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"height":10,
"id":1,
"name":"floor",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":2,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
}],
"name":"Group 2",
"opacity":1,
"type":"group",
"visible":true,
"x":0,
"y":0
},
{
"id":6,
"layers":[
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":5,
"name":"jitsiConf",
"opacity":1,
"properties":[
{
"name":"jitsiRoom",
"type":"string",
"value":"myRoom"
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
}],
"name":"Group 1",
"opacity":1,
"type":"group",
"visible":true,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"floorLayer",
"objects":[],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":8,
"nextobjectid":1,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"2021.03.23",
"tileheight":32,
"tilesets":[
{
"columns":11,
"firstgid":1,
"image":"tileset1.png",
"imageheight":352,
"imagewidth":352,
"margin":0,
"name":"tileset1",
"spacing":0,
"tilecount":121,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.5,
"width":10
}