From 6815fe7a0a965895af888b7465606a398594d70c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 15 Apr 2021 22:39:35 +0200 Subject: [PATCH] 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) --- front/src/Phaser/Game/GameMap.ts | 19 +-- front/src/Phaser/Game/GameScene.ts | 76 +++++----- front/src/Phaser/Map/ITiledMap.ts | 61 ++++++-- front/src/Phaser/Map/LayersIterator.ts | 44 ++++++ front/tests/Phaser/Map/LayersIteratorTest.ts | 147 +++++++++++++++++++ maps/tests/grouped_map.json | 102 +++++++++++++ 6 files changed, 384 insertions(+), 65 deletions(-) create mode 100644 front/src/Phaser/Map/LayersIterator.ts create mode 100644 front/tests/Phaser/Map/LayersIteratorTest.ts create mode 100644 maps/tests/grouped_map.json diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index c6d58eb2..5fe91b62 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -1,4 +1,5 @@ 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) => void; @@ -10,8 +11,10 @@ export class GameMap { private key: number|undefined; private lastProperties = new Map(); private callbacks = new Map>(); + public readonly layersIterator: LayersIterator; public constructor(private map: ITiledMap) { + this.layersIterator = new LayersIterator(map); } /** @@ -52,15 +55,10 @@ export class GameMap { return this.lastProperties; } - // helper for recursive group layer support - private getPropertiesHelper(key: number, layers: ITiledMapLayer[], properties: Map): Map { - - for (const layer of layers) { - if (layer.type === 'group') { - this.getPropertiesHelper(key, layer.layers, properties); - continue; - } + private getProperties(key: number): Map { + const properties = new Map(); + for (const layer of this.layersIterator) { if (layer.type !== 'tilelayer') { continue; } @@ -82,11 +80,6 @@ export class GameMap { return properties; } - private getProperties(key: number): Map { - const properties = new Map(); - return this.getPropertiesHelper(key, this.map.layers, properties); - } - private trigger(propName: string, oldValue: string | number | boolean | undefined, newValue: string | number | boolean | undefined, allProps: Map) { const callbacksArray = this.callbacks.get(propName); if (callbacksArray !== undefined) { diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index e33f1023..806fc32e 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -18,7 +18,14 @@ import { RESOLUTION, ZOOM_LEVEL } 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 {PlayerAnimationDirections} from "../Player/Animation"; import {PlayerMovement} from "./PlayerMovement"; @@ -77,6 +84,7 @@ import DOMElement = Phaser.GameObjects.DOMElement; import {Subscription} from "rxjs"; import {worldFullMessageStream} from "../../Connexion/WorldFullMessageStream"; import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; +import {LayersIterator} from "../Map/LayersIterator"; import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {PinchManager} from "../UserInput/PinchManager"; 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 create(): void { gameManager.gameSceneIsCreated(this); @@ -411,9 +394,27 @@ export class GameScene extends ResizableScene implements CenterListener { //add layer on map this.Layers = new Array(); - - if (this.createHelper(this.mapFile.layers, -2, '') === -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.'); + + let depth = -2; + 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(); @@ -963,20 +964,15 @@ ${escapedMessage} } } - private initPositionFromLayerNameHelper(layerName: string, layers : ITiledMapLayer[]) { - for (const layer of layers) { - if (layerName === layer.name && layer.type === 'tilelayer' && (layerName === defaultStartLayerName || this.isStartLayer(layer))) { + private initPositionFromLayerName(layerName: string) { + for (const layer of this.gameMap.layersIterator) { + if ((layerName === layer.name || layer.name.endsWith('/'+layerName)) && layer.type === 'tilelayer' && (layerName === defaultStartLayerName || this.isStartLayer(layer))) { const startPosition = this.startUser(layer); this.startX = startPosition.x + this.mapFile.tilewidth/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 { @@ -999,7 +995,7 @@ ${escapedMessage} } private getProperty(layer: ITiledMapLayer|ITiledMap, name: string): string|boolean|number|undefined { - const properties: ITiledMapLayerProperty[] = layer.properties; + const properties: ITiledMapLayerProperty[]|undefined = layer.properties; if (!properties) { return undefined; } @@ -1011,7 +1007,7 @@ ${escapedMessage} } private getProperties(layer: ITiledMapLayer|ITiledMap, name: string): (string|number|boolean|undefined)[] { - const properties: ITiledMapLayerProperty[] = layer.properties; + const properties: ITiledMapLayerProperty[]|undefined = layer.properties; if (!properties) { return []; } @@ -1025,7 +1021,7 @@ ${escapedMessage} await gameManager.loadMap(room, this.scene); } - private startUser(layer: ITiledMapLayer): PositionInterface { + private startUser(layer: ITiledMapTileLayer): PositionInterface { const tiles = layer.data; if (typeof(tiles) === 'string') { throw new Error('The content of a JSON map must be filled as a JSON array, not as a string'); diff --git a/front/src/Phaser/Map/ITiledMap.ts b/front/src/Phaser/Map/ITiledMap.ts index 6dfb4a2c..359b8e52 100644 --- a/front/src/Phaser/Map/ITiledMap.ts +++ b/front/src/Phaser/Map/ITiledMap.ts @@ -14,7 +14,7 @@ export interface ITiledMap { * Map orientation (orthogonal) */ orientation: string; - properties: ITiledMapLayerProperty[]; + properties?: ITiledMapLayerProperty[]; /** * Render order (right-down) @@ -24,6 +24,11 @@ export interface ITiledMap { tilewidth: number; tilesets: ITiledTileSet[]; version: number; + compressionlevel?: number; + infinite?: boolean; + nextlayerid?: number; + tiledversion?: string; + type?: string; } export interface ITiledMapLayerProperty { @@ -38,19 +43,35 @@ export interface ITiledMapLayerProperty { 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; height: number; name: string; opacity: number; - properties: ITiledMapLayerProperty[]; - encoding: string; + properties?: ITiledMapLayerProperty[]; + encoding?: string; compression?: string; - /** - * Type of layer (tilelayer, objectgroup) - */ - type: string; + type: "tilelayer"; visible: boolean; width: number; x: number; @@ -59,13 +80,29 @@ export interface ITiledMapLayer { /** * Draw order (topdown (default), index) */ - draworder: string; - objects: ITiledMapObject[]; + draworder?: string; +} + +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 { diff --git a/front/src/Phaser/Map/LayersIterator.ts b/front/src/Phaser/Map/LayersIterator.ts new file mode 100644 index 00000000..501a5f7b --- /dev/null +++ b/front/src/Phaser/Map/LayersIterator.ts @@ -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 { + + 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 { + if (this.pointer < this.layers.length) { + return { + done: false, + value: this.layers[this.pointer++] + } + } else { + return { + done: true, + value: null + } + } + } + + [Symbol.iterator](): IterableIterator { + return new LayersIterator(this.map); + } +} diff --git a/front/tests/Phaser/Map/LayersIteratorTest.ts b/front/tests/Phaser/Map/LayersIteratorTest.ts new file mode 100644 index 00000000..3b9d0d9b --- /dev/null +++ b/front/tests/Phaser/Map/LayersIteratorTest.ts @@ -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']); + }); +}); diff --git a/maps/tests/grouped_map.json b/maps/tests/grouped_map.json new file mode 100644 index 00000000..1e6c3e35 --- /dev/null +++ b/maps/tests/grouped_map.json @@ -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 +} \ No newline at end of file