From 3631a7f66970587ef0cbea3c869223fe44e58d59 Mon Sep 17 00:00:00 2001 From: cha0s Date: Mon, 13 May 2019 21:07:51 -0500 Subject: [PATCH] refactor: immutablen't --- TODO.md | 11 +- packages/behavior/traits/behaved.trait.js | 2 +- packages/entity/entity.packet.js | 25 ---- packages/entity/index.js | 71 +++-------- packages/entity/list/index.js | 94 ++++++--------- packages/entity/packet-synchronizer.js | 79 ------------- .../entity/packets/entity-create.packet.js | 35 ++++++ .../entity/packets/entity-remember.packet.js | 0 .../entity/packets/entity-remove.packet.js | 3 + packages/entity/packets/entity.packet.js | 22 ++++ .../packets/trait-directional.packet.js | 11 ++ .../entity/packets/trait-positioned.packet.js | 11 ++ packages/entity/trait/index.js | 25 +--- packages/entity/traits/directional.trait.js | 18 +++ packages/entity/traits/positioned.trait.js | 56 ++++----- packages/graphics/traits/visible.trait.js | 16 +-- packages/state/synchronized.js | 98 +--------------- packages/state/synchronizer.js | 60 +++------- .../timing/packets/trait-animated.packet.js | 12 ++ packages/timing/traits/animated.trait.js | 26 ++++- packages/topdown/layer.js | 51 ++++++-- packages/topdown/layers.js | 110 +++++++++--------- .../topdown/packets/layer-create.packet.js | 18 +++ .../packets/room-size-update.packet.js | 12 ++ .../topdown/packets/tile-update.packet.js | 12 ++ packages/topdown/room.js | 60 ++++++++-- packages/topdown/tiles.js | 57 ++++----- 27 files changed, 467 insertions(+), 528 deletions(-) delete mode 100644 packages/entity/entity.packet.js delete mode 100644 packages/entity/packet-synchronizer.js create mode 100644 packages/entity/packets/entity-create.packet.js create mode 100644 packages/entity/packets/entity-remember.packet.js create mode 100644 packages/entity/packets/entity-remove.packet.js create mode 100644 packages/entity/packets/entity.packet.js create mode 100644 packages/entity/packets/trait-directional.packet.js create mode 100644 packages/entity/packets/trait-positioned.packet.js create mode 100644 packages/timing/packets/trait-animated.packet.js create mode 100644 packages/topdown/packets/layer-create.packet.js create mode 100644 packages/topdown/packets/room-size-update.packet.js create mode 100644 packages/topdown/packets/tile-update.packet.js diff --git a/TODO.md b/TODO.md index 7f7abfc..7d1c9db 100644 --- a/TODO.md +++ b/TODO.md @@ -20,13 +20,20 @@ - ❌ entityList.fromJSON() - ❌ Socket WebWorker can't connect in Firefox - ✔ Don't run emitter destruction tickers on server -- ❌ Investigate unrolling equalsClose +- ✔ Investigate unrolling equalsClose - ✔ Bitshifts for on_positionChanged x/y boxing - ✔ Memoize Object.getOwnPropertyNames results per trait constructor - ✔ EE optimizations (lookupEmitListeners) -- ✔ Core.fastApply, search for /\(.../ +- ✔ Core.fastApply, search for /\(\.\.\./ - ✔ Rename visibleBoundingBox(es)? to visibleAabb(s)? - ❌ Property.fastAccess to skip getter, this.entity.currentAnimation - ✔ Trait::isDirty should be flat - ✔ Trait params fromJS is super slow - ✔ Entity::is is slow? +- ❌ Manual state sync + - ✔ Synchronized provides packet updates + - ✔ Phase out EntityPacketSynchronizer + - ❌ Implement ALL trait state update packets + - ❌ Implement Entity remember packets + - ✔ Implement Entity remove packets +- ❌ Save state/param extensions separately on create instead of only merging diff --git a/packages/behavior/traits/behaved.trait.js b/packages/behavior/traits/behaved.trait.js index bf6f597..dccc871 100644 --- a/packages/behavior/traits/behaved.trait.js +++ b/packages/behavior/traits/behaved.trait.js @@ -35,7 +35,7 @@ export class Behaved extends decorate(Trait) { const routinesJSON = this.params.routines; this._routines = (new Routines()).fromJSON(routinesJSON); this._routines.context = this._context; - this.updateCurrentRoutine(this.state.get('currentRoutine')); + this.updateCurrentRoutine(this.state.currentRoutine); } destroy() { diff --git a/packages/entity/entity.packet.js b/packages/entity/entity.packet.js deleted file mode 100644 index 49efdb7..0000000 --- a/packages/entity/entity.packet.js +++ /dev/null @@ -1,25 +0,0 @@ -import {Packet} from '@avocado/net'; - -export class EntityPacket extends Packet { - - static get schema() { - return { - ...super.schema, - data: [{ - uuid: 'string', - }], - }; - } - - bundleWith(other) { - this.data.push(other.data[0]); - } - - forEachData(fn) { - for (let i = 0; i < this.data.length; i++) { - const data = this.data[i]; - fn(data); - } - } - -} diff --git a/packages/entity/index.js b/packages/entity/index.js index f63089e..7babcba 100644 --- a/packages/entity/index.js +++ b/packages/entity/index.js @@ -6,6 +6,7 @@ import {compose, EventEmitter, fastApply} from '@avocado/core'; import {Resource} from '@avocado/resource'; import {Synchronized} from '@avocado/state'; +import {EntityCreatePacket} from './packets/entity-create.packet'; import {hasTrait, lookupTrait} from './trait/registry'; const debug = D('@avocado:entity:traits'); @@ -86,7 +87,6 @@ export class Entity extends decorate(Resource) { super(); this._hooks = {}; this._traits = {}; - this._traitsTypesDirty = []; this._traitsFlat = []; this._traitTickers = []; this._traitRenderTickers = []; @@ -158,11 +158,6 @@ export class Entity extends decorate(Resource) { type: Trait.type(), }); } - // Add state. - this.state = this.state.set(type, I.Map({ - params: instance.params, - state: instance.state, - })) // Track trait. this._traits[type] = instance; this._traitsFlat.push(instance); @@ -201,8 +196,7 @@ export class Entity extends decorate(Resource) { hydrate() { const promises = []; for (let i = 0; i < this._traitsFlat.length; i++) { - const instance = this._traitsFlat[i]; - promises.push(instance.hydrate()); + promises.push(this._traitsFlat[i].hydrate()); } return Promise.all(promises); } @@ -231,26 +225,21 @@ export class Entity extends decorate(Resource) { return type in this._traits; } - patchStateStep(type, step) { - let instance = this._traits[type]; - // New trait requested? - if (!this._traits[type]) { - // Doesn't exist? - if (!hasTrait(type)) { - return; - } - if ('params' in step.value) { - step.value.params = step.value.params.toJS(); - } - this.addTrait(type, step.value); - instance = this._traits[type]; - this.state = this.state.setIn([type, 'params'], instance.params); + packetsForUpdate(force = false) { + const packets = []; + if (force) { + const packet = new EntityCreatePacket(this.toJSON(), this); + packets.push(packet); } else { - // Accept state. - instance.patchState([step]); + for (let i = 0; i < this._traitsFlat.length; i++) { + const traitPackets = this._traitsFlat[i].packetsForUpdate(); + for (let j = 0; j < traitPackets.length; j++) { + packets.push(traitPackets[j]); + } + } } - this.state = this.state.setIn([type, 'state'], instance.state); + return packets; } renderTick(elapsed) { @@ -296,8 +285,6 @@ export class Entity extends decorate(Resource) { this.off(eventName, listeners[eventName]); } instance._memoizedListeners = {}; - // Remove state. - this.state = this.state.delete(type); // Remove instance. delete this._traits[type]; this._traitsFlat.splice(this._traitsFlat.indexOf(instance), 1); @@ -325,34 +312,10 @@ export class Entity extends decorate(Resource) { types.forEach((type) => this.removeTrait(type)); } - setTraitDirty(type) { - this._traitsTypesDirty.push(type); - if (this.is('listed')) { - this.list && this.list.markEntityDirty(this); - } - } - tick(elapsed) { for (let i = 0; i < this._traitTickers.length; i++) { this._traitTickers[i](elapsed); } - if (AVOCADO_SERVER) { - this.tickMutateState(); - } - } - - tickMutateState() { - if (0 === this._traitsTypesDirty.length) { - return; - } - this.state = this.state.withMutations((state) => { - for (let i = 0; i < this._traitsTypesDirty.length; i++) { - const type = this._traitsTypesDirty[i]; - const instance = this._traits[type]; - state.setIn([type, 'state'], instance.state); - } - }); - this._traitsTypesDirty = []; } toJSON() { @@ -368,12 +331,12 @@ export class Entity extends decorate(Resource) { } -export {EntityPacket} from './entity.packet'; +export {EntityCreatePacket} from './packets/entity-create.packet'; +export {EntityRemovePacket} from './packets/entity-remove.packet'; +export {EntityPacket} from './packets/entity.packet'; export {EntityList, EntityListView} from './list'; -export {EntityPacketSynchronizer} from './packet-synchronizer'; - export { hasTrait, lookupTrait, diff --git a/packages/entity/list/index.js b/packages/entity/list/index.js index fef2a7f..2d939b4 100644 --- a/packages/entity/list/index.js +++ b/packages/entity/list/index.js @@ -1,10 +1,9 @@ -import * as I from 'immutable'; -import mapValues from 'lodash.mapvalues'; - import {compose, EventEmitter} from '@avocado/core'; import {QuadTree, Rectangle, Vector} from '@avocado/math'; import {Synchronized} from '@avocado/state'; +import {EntityCreatePacket} from '../packets/entity-create.packet'; +import {EntityRemovePacket} from '../packets/entity-remove.packet'; import {Entity} from '../index'; const decorate = compose( @@ -17,8 +16,9 @@ export class EntityList extends decorate(class {}) { constructor() { super(); this._afterDestructionTickers = []; - this._dirtyEntities = []; this._entities = {}; + this._entitiesJustAdded = []; + this._entitiesJustRemoved = []; this._entityTickers = [] this._flatEntities = []; this._quadTree = new QuadTree(); @@ -31,12 +31,22 @@ export class EntityList extends decorate(class {}) { } } + acceptPacket(packet) { + if (packet instanceof EntityCreatePacket) { + const entity = new Entity(packet.data); + entity.instanceUuid = packet.data.uuid; + this.addEntity(entity); + } + } + addEntity(entity) { const uuid = entity.instanceUuid; this._entities[uuid] = entity; this._flatEntities.push(entity); this._entityTickers.push(entity.tick); - this.state = this.state.set(uuid, entity.state); + if (AVOCADO_SERVER) { + this._entitiesJustAdded.push(entity); + } entity.setIntoList(this); entity.once('destroy', () => { this.removeEntity(entity); @@ -52,8 +62,8 @@ export class EntityList extends decorate(class {}) { } destroy() { - for (const entity of this) { - entity.destroy(); + for (let i = 0; i < this._flatEntities.length; i++) { + this._flatEntities[i].destroy(); } } @@ -63,40 +73,27 @@ export class EntityList extends decorate(class {}) { } } - markEntityDirty(entity) { - if (AVOCADO_CLIENT) { - return; - } - if (-1 === this._dirtyEntities.indexOf(entity)) { - this._dirtyEntities.push(entity); - } - } - - patchStateStep(uuid, step) { - const entity = this._entities[uuid]; - if ('/' === step.path) { - switch (step.op) { - case 'add': - // New entity. Create with patch as traits. - const newEntity = new Entity({ - traits: step.value, - }); - newEntity.instanceUuid = uuid; - this.addEntity(newEntity); - break; - case 'remove': - // Maybe already destroyed on the client? - if (entity && entity.is('existent')) { - entity.destroy(); - } - break; + packetsForUpdate(force = false) { + const packets = []; + if (!force) { + for (let i = 0; i < this._entitiesJustAdded.length; i++) { + const entity = this._entitiesJustAdded[i]; + packets.push(new EntityCreatePacket(entity.toJSON(), entity)); } - return; + this._entitiesJustAdded = []; + for (let i = 0; i < this._entitiesJustRemoved.length; i++) { + const entity = this._entitiesJustRemoved[i]; + packets.push(new EntityRemovePacket({}, entity)); + } + this._entitiesJustRemoved = []; } - if ('replace' === step.op && entity) { - // Exists; patch. - entity.patchState([step]); + for (let i = 0; i < this._flatEntities.length; i++) { + const entityPackets = this._flatEntities[i].packetsForUpdate(force); + for (let j = 0; j < entityPackets.length; j++) { + packets.push(entityPackets[j]); + } } + return packets; } get quadTree() { @@ -108,10 +105,12 @@ export class EntityList extends decorate(class {}) { if (!(uuid in this._entities)) { return; } + if (AVOCADO_SERVER) { + this._entitiesJustRemoved.push(entity); + } delete this._entities[uuid]; this._flatEntities.splice(this._flatEntities.indexOf(entity), 1); this._entityTickers.splice(this._entityTickers.indexOf(entity.tick), 1); - this.state = this.state.delete(uuid); this.emit('entityRemoved', entity); } @@ -122,10 +121,6 @@ export class EntityList extends decorate(class {}) { } // Run normal tickers. this.tickEntities(elapsed) - // Update state. - if (AVOCADO_SERVER) { - this.tickMutateState(); - } } tickAfterDestructionTickers(elapsed) { @@ -149,19 +144,6 @@ export class EntityList extends decorate(class {}) { } } - tickMutateState(state) { - if (0 === this._dirtyEntities.length) { - return; - } - this.state = this.state.withMutations((state) => { - for (let i = 0; i < this._dirtyEntities.length; i++) { - const entity = this._dirtyEntities[i]; - state.set(entity.$$avocado_property_instanceUuid, entity.state); - } - }); - this._dirtyEntities = []; - } - visibleEntities(query) { const entities = []; const entitiesChecked = []; diff --git a/packages/entity/packet-synchronizer.js b/packages/entity/packet-synchronizer.js deleted file mode 100644 index 25e6f67..0000000 --- a/packages/entity/packet-synchronizer.js +++ /dev/null @@ -1,79 +0,0 @@ -import {EntityPacket} from '@avocado/entity'; - -export class EntityPacketSynchronizer { - - constructor() { - this.packetsToSynchronize = new Map(); - this.trackedEntities = new Map(); - } - - acceptPacket(packet) { - if (!(packet instanceof EntityPacket)) { - return; - } - for (let i = 0; i < packet.data.length; i++) { - const data = packet.data[i]; - const packetEntity = this.trackedEntities.get(data.uuid); - if (!packetEntity) { - continue; - } - const Packet = packet.constructor; - packetEntity.acceptPacket(new Packet([data])); - } - } - - flushPackets(flusher) { - const flushed = new Map(); - const it = this.packetsToSynchronize.entries(); - for (let value = it.next(); !value.done; value = it.next()) { - const entity = value.value[0]; - const packets = value.value[1]; - const mergedPackets = this._mergePackets(packets); - flushed.set(entity, mergedPackets); - } - this.packetsToSynchronize.clear(); - return flushed; - } - - _mergePackets(packets) { - const mergedPackets = new Map(); - for (let i = 0; i < packets.length; i++) { - const packet = packets[i]; - const Packet = packet.constructor; - if (!mergedPackets.has(Packet)) { - mergedPackets.set(Packet, packet); - } - else { - mergedPackets.get(Packet).mergeWith(packet); - } - } - return mergedPackets; - } - - _queuePacketFor(entity, packet) { - if (!this.packetsToSynchronize.has(entity)) { - this.packetsToSynchronize.set(entity, []); - } - this.packetsToSynchronize.get(entity).push(packet); - } - - trackEntity(entity) { - this.trackedEntities.set(entity.instanceUuid, entity); - entity.once('destroy', () => { - this.trackedEntities.delete(entity.instanceUuid); - }); - this._watchEntityPackets(entity); - } - - _watchEntityPackets(entity) { - const onSendPacket = (packet) => { - this._queuePacketFor(entity, packet); - }; - entity.on('sendPacket', onSendPacket); - entity.once('destroy', () => { - this.packetsToSynchronize.delete(entity); - entity.off('sendPacket', onSendPacket); - }); - } - -} diff --git a/packages/entity/packets/entity-create.packet.js b/packages/entity/packets/entity-create.packet.js new file mode 100644 index 0000000..c69b1e1 --- /dev/null +++ b/packages/entity/packets/entity-create.packet.js @@ -0,0 +1,35 @@ +import msgpack from 'msgpack-lite'; + +import {EntityPacket} from './entity.packet'; + +export class EntityCreatePacket extends EntityPacket { + + constructor(data, entity) { + if ('undefined' !== typeof entity) { + data.uuid = entity.instanceUuid; + data.layer = entity.layer.index; + } + super(data); + this.entity = entity; + } + + static pack(packet) { + return this.builder.encode({ + _id: packet.data[0], + data: msgpack.encode(packet.data[1]), + }) + } + + static get schema() { + return { + ...super.schema, + data: 'buffer', + }; + } + + static unpack(packet) { + const {data} = this.builder.decode(packet); + return msgpack.decode(data); + } + +} diff --git a/packages/entity/packets/entity-remember.packet.js b/packages/entity/packets/entity-remember.packet.js new file mode 100644 index 0000000..e69de29 diff --git a/packages/entity/packets/entity-remove.packet.js b/packages/entity/packets/entity-remove.packet.js new file mode 100644 index 0000000..82ae55a --- /dev/null +++ b/packages/entity/packets/entity-remove.packet.js @@ -0,0 +1,3 @@ +import {EntityPacket} from './entity.packet'; + +export class EntityRemovePacket extends EntityPacket {} diff --git a/packages/entity/packets/entity.packet.js b/packages/entity/packets/entity.packet.js new file mode 100644 index 0000000..de4d0c5 --- /dev/null +++ b/packages/entity/packets/entity.packet.js @@ -0,0 +1,22 @@ +import {Packet} from '@avocado/net'; + +export class EntityPacket extends Packet { + + constructor(data, entity) { + if ('undefined' !== typeof entity) { + data.uuid = entity.instanceUuid; + } + super(data); + this.entity = entity; + } + + static get schema() { + return { + ...super.schema, + data: { + uuid: 'string', + }, + }; + } + +} diff --git a/packages/entity/packets/trait-directional.packet.js b/packages/entity/packets/trait-directional.packet.js new file mode 100644 index 0000000..6022a46 --- /dev/null +++ b/packages/entity/packets/trait-directional.packet.js @@ -0,0 +1,11 @@ +import {EntityPacket} from './entity.packet'; + +export class TraitDirectionalPacket extends EntityPacket { + + static get schema() { + const schema = super.schema; + schema.data.direction = 'uint8'; + return schema; + } + +} diff --git a/packages/entity/packets/trait-positioned.packet.js b/packages/entity/packets/trait-positioned.packet.js new file mode 100644 index 0000000..cc06c7d --- /dev/null +++ b/packages/entity/packets/trait-positioned.packet.js @@ -0,0 +1,11 @@ +import {EntityPacket} from './entity.packet'; + +export class TraitPositionedPacket extends EntityPacket { + + static get schema() { + const schema = super.schema; + schema.data.position = 'uint32'; + return schema; + } + +} diff --git a/packages/entity/trait/index.js b/packages/entity/trait/index.js index ab439b7..94af0e3 100644 --- a/packages/entity/trait/index.js +++ b/packages/entity/trait/index.js @@ -1,4 +1,3 @@ -import * as I from 'immutable'; import merge from 'lodash.merge'; import {compose, Property} from '@avocado/core'; @@ -18,7 +17,8 @@ export class Trait extends decorate(class {}) { const ctor = this.constructor; this._memoizedListeners = undefined; this.params = Object.assign({}, ctor.defaultParams(), params); - this.state = I.fromJS(ctor.defaultState()).merge(I.fromJS(state)); + this.state = Object.assign({}, ctor.defaultState(), state); + this.previousState = JSON.parse(JSON.stringify(this.state)); if (this.tick) { this.tick = this.tick.bind(this); } @@ -56,24 +56,10 @@ export class Trait extends decorate(class {}) { return {}; } - patchStateStep(key, step) { - if ('state' !== key) { - return; - } - const stateKey = step.path.substr(1); - const value = this.transformPatchValue(stateKey, step.value); - if (stateKey in this.entity) { - this.entity[stateKey] = value; - } - else { - this.state = this.state.set(stateKey, value); - } - } - toJSON() { return { params: this.params, - state: this.state.toJS(), + state: this.state, }; } @@ -112,15 +98,14 @@ export function StateProperty(key, meta = {}) { this.entity.emit(...args); }; meta.initialize = meta.initialize || function() { - this[transformedProperty] = this.state.get(key); + this[transformedProperty] = this.state[key]; } meta.get = meta.get || new Function(` return this.${transformedProperty}; `); meta.set = meta.set || new Function('value', ` - this.entity.setTraitDirty(this.constructor.type()); this.${transformedProperty} = value; - this.state = this.state.set('${key}', value); + this.state['${key}'] = value; `); return Property(key, meta)(Superclass); } diff --git a/packages/entity/traits/directional.trait.js b/packages/entity/traits/directional.trait.js index 9df3a89..9ea111f 100644 --- a/packages/entity/traits/directional.trait.js +++ b/packages/entity/traits/directional.trait.js @@ -2,6 +2,7 @@ import {compose} from '@avocado/core'; import {Vector} from '@avocado/math'; import {StateProperty, Trait} from '../trait'; +import {TraitDirectionalPacket} from '../packets/trait-directional.packet'; const decorate = compose( StateProperty('direction', { @@ -33,6 +34,23 @@ export class Directional extends decorate(Trait) { this.directionCount = this.params.directionCount; } + acceptPacket(packet) { + if (packet instanceof TraitDirectionalPacket) { + this.entity.direction = packet.data.direction; + } + } + + packetsForUpdate() { + const packets = []; + if (this.state.direction !== this.previousState.direction) { + packets.push(new TraitDirectionalPacket({ + direction: this.state.direction, + }, this.entity)); + this.previousState.direction = this.state.direction; + } + return packets; + } + listeners() { const listeners = {}; if (this.params.trackMovement) { diff --git a/packages/entity/traits/positioned.trait.js b/packages/entity/traits/positioned.trait.js index e433deb..4f6374e 100644 --- a/packages/entity/traits/positioned.trait.js +++ b/packages/entity/traits/positioned.trait.js @@ -2,6 +2,7 @@ import {compose, EventEmitter} from '@avocado/core'; import {Vector} from '@avocado/math'; import {Trait} from '../trait'; +import {TraitPositionedPacket} from '../packets/trait-positioned.packet'; const decorate = compose( EventEmitter, @@ -30,8 +31,8 @@ export class Positioned extends decorate(Trait) { constructor(entity, params, state) { super(entity, params, state); this.on('_positionChanged', this.on_positionChanged, this); - const x = this.state.get('x') >> 2; - const y = this.state.get('y') >> 2; + const x = this.state.x >> 2; + const y = this.state.y >> 2; this._position = [x, y]; this.entity.position[0] = x; this.entity.position[1] = y; @@ -50,16 +51,19 @@ export class Positioned extends decorate(Trait) { } } + acceptPacket(packet) { + if (packet instanceof TraitPositionedPacket) { + this.serverX = (packet.data.position & 0xFFFF) >> 2; + this.serverY = (packet.data.position >> 16) >> 2; + } + } + on_positionChanged(oldPosition, newPosition) { this.entity.position[0] = newPosition[0]; this.entity.position[1] = newPosition[1]; if (AVOCADO_SERVER) { - const x = newPosition[0] << 2; - const y = newPosition[1] << 2; - this.state = this.state.withMutations((state) => { - state.set('x', x).set('y', y); - }); - this.entity.setTraitDirty('positioned'); + this.state.x = newPosition[0] << 2; + this.state.y = newPosition[1] << 2; } this.entity.emit('positionChanged', oldPosition, newPosition); } @@ -68,36 +72,26 @@ export class Positioned extends decorate(Trait) { this.serverPositionDirty = true; } - patchStateStep(key, step) { - if ('state' !== key) { - return; - } - const stateKey = step.path.substr(1); - const value = this.transformPatchValue(stateKey, step.value); - switch (stateKey) { - case 'x': - this.serverX = value; - break; - case 'y': - this.serverY = value; - break; - default: - super.patchStateStep(key, step); - break; + packetsForUpdate() { + const packets = []; + if ( + this.state.x !== this.previousState.x + || this.state.y !== this.previousState.y + ) { + const packed = ((this.state.y) << 16) | (this.state.x << 0); + packets.push(new TraitPositionedPacket({ + position: packed, + }, this.entity)); + this.previousState.x = this.state.x; + this.previousState.y = this.state.y; } + return packets; } set relaxServerPositionConstraintIfNearerThan(nearerThan) { this._relaxServerPositionConstraintIfNearerThan = nearerThan; } - transformPatchValue(key, value) { - if (-1 !== ['x', 'y'].indexOf(key)) { - return value / 4; - } - super.transformPatchValue(key, value); - } - listeners() { return { diff --git a/packages/graphics/traits/visible.trait.js b/packages/graphics/traits/visible.trait.js index e1e8bc8..8f55fa0 100644 --- a/packages/graphics/traits/visible.trait.js +++ b/packages/graphics/traits/visible.trait.js @@ -54,14 +54,14 @@ export class Visible extends decorate(Trait) { if (filter) { this._container.setFilter(filter); } - this._container.isVisible = this.state.get('isVisible'); + this._container.isVisible = this.state.isVisible; } this._rawVisibleAabb = [0, 0, 0, 0]; this.scheduledBoundingBoxUpdate = true; this.trackPosition = this.params.trackPosition; - const scale = this.state.get('visibleScale'); - this._visibleScale = [scale.get(0), scale.get(1)]; - this.onZIndexChanged(this.state.get('zIndex')); + const scale = this.state.visibleScale; + this._visibleScale = [scale[0], scale[1]]; + this.onZIndexChanged(this.state.zIndex); } destroy() { @@ -88,7 +88,7 @@ export class Visible extends decorate(Trait) { } set rawVisibleScale(scale) { - this.entity.visibleScale = I.List(scale); + this.entity.visibleScale = scale; } synchronizePosition() { @@ -107,7 +107,7 @@ export class Visible extends decorate(Trait) { } get visibleScaleX() { - return this.entity.visibleScale.get(0); + return this.state.visibleScale[0]; } set visibleScaleX(x) { @@ -115,7 +115,7 @@ export class Visible extends decorate(Trait) { } get visibleScaleY() { - return this.entity.visibleScale.get(1); + return this.state.visibleScale[1]; } set visibleScaleY(y) { @@ -155,7 +155,7 @@ export class Visible extends decorate(Trait) { visibleScaleChanged: () => { const scale = this.entity.visibleScale; - this._visibleScale = [scale.get(0), scale.get(1)]; + this._visibleScale = [scale[0], scale[1]]; if (this._container) { this._container.scale = this._visibleScale; } diff --git a/packages/state/synchronized.js b/packages/state/synchronized.js index 577a19b..8e7b9c2 100644 --- a/packages/state/synchronized.js +++ b/packages/state/synchronized.js @@ -11,105 +11,11 @@ const decorate = compose( export function Synchronized(Superclass) { return class Synchronized extends decorate(Superclass) { - constructor(...args) { - super(...args); - this.state = I.Map(); - this._childrenNeedInitialization = true; - this._childrenWithState = [] - this._childrenWithoutState = []; - this._childrenTickers = []; - } + acceptPacket(packet) {} - ensureSynchronizedChildren() { - if (!this._childrenNeedInitialization) { - return; - } - this._childrenNeedInitialization = false; - const synchronizedChildren = this.synchronizedChildren(); - for (let i = 0; i < synchronizedChildren.length; ++i) { - const key = synchronizedChildren[i]; - if ( - 'undefined' !== typeof this[key] - && 'undefined' !== typeof this[key].tick - ) { - this._childrenTickers.push(this[key].tick.bind(this[key])); - } - if ( - 'undefined' !== typeof this[key] - && 'undefined' !== typeof this[key].state - ) { - this._childrenWithState.push(key); - } - else { - this._childrenWithoutState.push(key); - } - } - } - - patchState(patch) { - for (const step of patch) { - const {op, path, value} = step; - if ('/' === path) { - for (const key in value) { - this.patchStateStep(key, { - op, - path, - value: value[key], - }); - } - } - else { - const [key, substep] = nextStep(step); - this.patchStateStep(key, { - op: substep.op, - path: substep.path, - value: substep.value, - }); - } - } - } - - patchStateStep(key, step) { - if (!(key in this)) { - return; - } - if ( - 'undefined' !== typeof this[key] - && 'undefined' !== typeof this[key].patchState - ) { - this[key].patchState([step]); - } - else { - this[key] = step.value; - } - } - - synchronizedChildren() { + packetsForUpdate(force = false) { return []; } - tickSynchronized(elapsed) { - this.ensureSynchronizedChildren(); - for (let i = 0; i < this._childrenTickers.length; ++i) { - this._childrenTickers[i](elapsed); - } - if (AVOCADO_SERVER) { - this.tickMutateState(); - } - } - - tickMutateState() { - this.state = this.state.withMutations((state) => { - for (let i = 0; i < this._childrenWithState.length; ++i) { - const key = this._childrenWithState[i]; - state.set(key, this[key].state); - } - for (let i = 0; i < this._childrenWithoutState.length; ++i) { - const key = this._childrenWithoutState[i]; - state.set(key, this[key]); - } - }); - } - } } diff --git a/packages/state/synchronizer.js b/packages/state/synchronizer.js index d07f066..ce86db7 100644 --- a/packages/state/synchronizer.js +++ b/packages/state/synchronizer.js @@ -4,55 +4,25 @@ import {nextStep} from './next-step'; export class Synchronizer { - constructor(statefuls) { - this._state = I.Map(); - this._statefuls = statefuls; - this.updateState(); + constructor(children) { + this.children = children; } - patchState(patch) { - const stepMap = {}; - for (const step of patch) { - const [key, substep] = nextStep(step) - if (!stepMap[key]) { - stepMap[key] = []; + acceptPacket(packet) { + for (let i = 0; i < this.children.length; i++) { + this.children[i].acceptPacket(packet); + } + } + + packetsForUpdate(force = false) { + const packetsForUpdate = []; + for (let i = 0; i < this.children.length; i++) { + const childPacketsForUpdate = this.children[i].packetsForUpdate(force); + for (let j = 0; j < childPacketsForUpdate.length; j++) { + packetsForUpdate.push(childPacketsForUpdate[j]); } - stepMap[key].push(substep); - } - for (const key in stepMap) { - const stateful = this._statefuls[key]; - if (!stateful) { - continue; - } - stateful.patchState(stepMap[key]); - } - } - - setStateful(key, stateful) { - this._statefuls[key] = stateful; - } - - get state() { - return this._state; - } - - tick(elapsed) { - for (const key in this._statefuls) { - const stateful = this._statefuls[key]; - stateful.tick(elapsed); - } - this.updateState(); - } - - updateState() { - if (AVOCADO_SERVER) { - this._state = this._state.withMutations((state) => { - for (const key in this._statefuls) { - const stateful = this._statefuls[key]; - state.set(key, stateful.state); - } - }); } + return packetsForUpdate; } } diff --git a/packages/timing/packets/trait-animated.packet.js b/packages/timing/packets/trait-animated.packet.js new file mode 100644 index 0000000..5ead43b --- /dev/null +++ b/packages/timing/packets/trait-animated.packet.js @@ -0,0 +1,12 @@ +import {EntityPacket} from '@avocado/entity'; + +export class TraitAnimatedPacket extends EntityPacket { + + static get schema() { + const schema = super.schema; + schema.data.currentAnimation = 'string'; + schema.data.isAnimating = 'bool'; + return schema; + } + +} diff --git a/packages/timing/traits/animated.trait.js b/packages/timing/traits/animated.trait.js index 331fb6d..c719218 100644 --- a/packages/timing/traits/animated.trait.js +++ b/packages/timing/traits/animated.trait.js @@ -4,6 +4,7 @@ import {Rectangle, Vector} from '@avocado/math'; import {Animation} from '../animation'; import {AnimationView} from '../animation-view'; +import {TraitAnimatedPacket} from '../packets/trait-animated.packet'; const decorate = compose( StateProperty('currentAnimation', { @@ -40,7 +41,7 @@ export class Animated extends decorate(Trait) { this.animationViews = undefined; this.animationsPromise = undefined; this._cachedAabbs = {}; - this._currentAnimation = this.state.get('currentAnimation'); + this._currentAnimation = this.state.currentAnimation; } destroy() { @@ -61,6 +62,13 @@ export class Animated extends decorate(Trait) { this._cachedAabbs = {}; } + acceptPacket(packet) { + if (packet instanceof TraitAnimatedPacket) { + this.entity.currentAnimation = packet.data.currentAnimation; + this.entity.isAnimating = packet.data.isAnimating; + } + } + hideAnimation(key) { if (!this.animationViews) { return; @@ -142,6 +150,22 @@ export class Animated extends decorate(Trait) { return this._animations[key].offset; } + packetsForUpdate() { + const packets = []; + if ( + this.state.currentAnimation !== this.previousState.currentAnimation + || this.state.isAnimating !== this.previousState.isAnimating + ) { + packets.push(new TraitAnimatedPacket({ + currentAnimation: this.state.currentAnimation, + isAnimating: this.state.isAnimating, + }, this.entity)); + this.previousState.currentAnimation = this.state.currentAnimation; + this.previousState.isAnimating = this.state.isAnimating; + } + return packets; + } + setSpriteScale() { if (!this.animationViews) { return; diff --git a/packages/topdown/layer.js b/packages/topdown/layer.js index 9e96801..1eb3d97 100644 --- a/packages/topdown/layer.js +++ b/packages/topdown/layer.js @@ -1,13 +1,13 @@ -import * as I from 'immutable'; - import {compose, EventEmitter, Property} from '@avocado/core'; -import {Entity, EntityList} from '@avocado/entity'; +import {Entity, EntityCreatePacket, EntityList} from '@avocado/entity'; import {Vector} from '@avocado/math'; import {ShapeList} from '@avocado/physics'; import {Synchronized} from '@avocado/state'; import {Tiles} from './tiles'; import {Tileset} from './tileset'; +import {LayerCreatePacket} from './packets/layer-create.packet'; +import {TileUpdatePacket} from './packets/tile-update.packet'; const decorate = compose( EventEmitter, @@ -25,9 +25,10 @@ const decorate = compose( export class Layer extends decorate(class {}) { - constructor() { + constructor(json) { super(); this.entityList = new EntityList(); + this.index = -1; this.tileGeometry = []; this.tiles = new Tiles(); // Listeners. @@ -36,8 +37,19 @@ export class Layer extends decorate(class {}) { this.tiles.on('dataChanged', this.onTileDataChanged, this); this.on('tilesetChanged', this.onTilesetChanged, this); this.on('tilesetUriChanged', this.onTilesetUriChanged, this); - this.onTilesetUriChanged(); this.on('worldChanged', this.onWorldChanged, this); + if ('undefined' !== typeof json) { + this.fromJSON(json); + } + } + + acceptPacket(packet) { + if (packet instanceof TileUpdatePacket) { + this.tiles.acceptPacket(packet); + } + if (packet instanceof EntityCreatePacket) { + this.entityList.acceptPacket(packet); + } } addEntity(entity) { @@ -92,10 +104,9 @@ export class Layer extends decorate(class {}) { fromJSON(json) { if (json.entities) { - json.entities.forEach((entityJSON) => { - const entity = new Entity(entityJSON); - this.entityList.addEntity(entity); - }); + for (let i = 0; i < json.entities.length; i++) { + this.entityList.addEntity(new Entity(json.entities[i])); + } } if (json.tiles) { this.tiles.fromJSON(json.tiles) @@ -156,6 +167,19 @@ export class Layer extends decorate(class {}) { } } + packetsForUpdate(force = false) { + const packets = []; + // Create layer during a force. + if (force) { + packets.push(new LayerCreatePacket(this.toJSON())); + } + const entityListPackets = this.entityList.packetsForUpdate(force); + for (let i = 0; i < entityListPackets.length; i++) { + packets.push(entityListPackets[i]); + } + return packets; + } + removeEntity(entity) { this.entityList.removeEntity(entity); } @@ -177,7 +201,14 @@ export class Layer extends decorate(class {}) { } tick(elapsed) { - this.tickSynchronized(elapsed); + this.entityList.tick(elapsed); + } + + toJSON() { + return { + tilesetUri: this.tilesetUri, + tiles: this.tiles.toJSON(), + }; } visibleEntities(query) { diff --git a/packages/topdown/layers.js b/packages/topdown/layers.js index c95ddd6..7a17d88 100644 --- a/packages/topdown/layers.js +++ b/packages/topdown/layers.js @@ -1,9 +1,12 @@ import * as I from 'immutable'; import {arrayUnique, compose, EventEmitter, flatten} from '@avocado/core'; +import {EntityCreatePacket} from '@avocado/entity'; import {Synchronized} from '@avocado/state'; import {Layer} from './layer'; +import {LayerCreatePacket} from './packets/layer-create.packet'; +import {TileUpdatePacket} from './packets/tile-update.packet'; const decorate = compose( EventEmitter, @@ -12,15 +15,29 @@ const decorate = compose( export class Layers extends decorate(class {}) { - constructor() { + constructor(json) { super(); - this.layers = {}; + this.layers = []; + if ('undefined' !== typeof json) { + this.fromJSON(json); + } } *[Symbol.iterator]() { - for (const index in this.layers) { - const layer = this.layers[index]; - yield {index, layer}; + for (let i = 0; i < this.layers.length; i++) { + yield this.layers[i]; + } + } + + acceptPacket(packet) { + if (packet instanceof LayerCreatePacket) { + this.addLayer(new Layer(packet.data)); + } + if (packet instanceof TileUpdatePacket) { + this.layers[packet.data.layer].acceptPacket(packet); + } + if (packet instanceof EntityCreatePacket) { + this.layers[packet.data.layer].acceptPacket(packet); } } @@ -32,34 +49,36 @@ export class Layers extends decorate(class {}) { layer.addEntity(entity); } - addLayer(index, layer) { + addLayer(layer) { layer.on('entityAdded', this.onEntityAddedToLayers, this); layer.on('entityRemoved', this.onEntityRemovedFromLayers, this); - this.layers[index] = layer; - this.emit('layerAdded', layer, index); + this.layers.push(layer); + layer.index = this.layers.length - 1; + this.emit('layerAdded', layer); } allEntities() { let allEntities = []; - for (const index in this.layers) { - const layer = this.layers[index]; - allEntities = allEntities.concat(layer.allEntities()); + for (let i = 0; i < this.layers.length; i++) { + const layerEntities = this.layers[i].allEntities(); + for (let j = 0; j < layerEntities.length; j++) { + allEntities.push(layerEntities[j]); + } } return allEntities; } destroy() { - for (const index in this.layers) { - const layer = this.layers[index]; - this.removeLayer(index); + for (let i = 0; i < this.layers.length; i++) { + const layer = this.layers[i]; + this.removeLayer(layer); layer.destroy(); } } findEntity(uuid) { - for (const index in this.layers) { - const layer = this.layers[index]; - const foundEntity = layer.findEntity(uuid); + for (let i = 0; i < this.layers.length; i++) { + const foundEntity = this.layers[i].findEntity(uuid); if (foundEntity) { return foundEntity; } @@ -67,13 +86,8 @@ export class Layers extends decorate(class {}) { } fromJSON(json) { - if (json) { - for (const index in json) { - const layerJSON = json[index]; - const layer = new Layer() - this.addLayer(index, layer); - layer.fromJSON(layerJSON); - } + for (let i = 0; i < json.length; i++) { + this.addLayer(new Layer(json[i])); } return this; } @@ -90,12 +104,15 @@ export class Layers extends decorate(class {}) { this.emit('entityRemoved', entity); } - patchStateStep(index, step) { - const layer = this.layers[index] ? this.layers[index] : new Layer(); - if (!this.layers[index]) { - this.addLayer(index, layer); + packetsForUpdate(force = false) { + const packets = []; + for (let i = 0; i < this.layers.length; i++) { + const layerPacketsForUpdate = this.layers[i].packetsForUpdate(force); + for (let j = 0; j < layerPacketsForUpdate.length; j++) { + packets.push(layerPacketsForUpdate[j]); + } } - layer.patchState([step]); + return packets; } removeEntityFromLayer(entity, layerIndex) { @@ -106,41 +123,30 @@ export class Layers extends decorate(class {}) { layer.removeEntity(entity); } - removeLayer(index) { - const layer = this.layers[index]; - if (!layer) { + // TODO: fix index for remaining layers + removeLayer(layer) { + const index = this.layers.indexOf(layer); + if (-1 === index) { return; } layer.off('entityAdded', this.onEntityAddedToLayers); layer.off('entityRemoved', this.onEntityRemovedFromLayers); - delete this.layers[index]; - this.emit('layerRemoved', layer, index); + this.layers.splice(index, 1); + this.emit('layerRemoved', layer); } tick(elapsed) { - if (this.layers) { - for (const index in this.layers) { - const layer = this.layers[index]; - layer.tick(elapsed); - } - if (AVOCADO_SERVER) { - this.state = this.state.withMutations((state) => { - for (const index in this.layers) { - const layer = this.layers[index]; - state.set(index, layer.state); - } - }); - } + for (let i = 0; i < this.layers.length; i++) { + this.layers[i].tick(elapsed); } } visibleEntities(query) { const entities = []; - for (const index in this.layers) { - const layer = this.layers[index]; - const layerVisibleEntities = layer.visibleEntities(query); - for (let i = 0; i < layerVisibleEntities.length; i++) { - const layerVisibleEntity = layerVisibleEntities[i]; + for (let i = 0; i < this.layers.length; i++) { + const layerVisibleEntities = this.layers[i].visibleEntities(query); + for (let j = 0; j < layerVisibleEntities.length; j++) { + const layerVisibleEntity = layerVisibleEntities[j]; entities.push(layerVisibleEntity); } } diff --git a/packages/topdown/packets/layer-create.packet.js b/packages/topdown/packets/layer-create.packet.js new file mode 100644 index 0000000..c1cc8ea --- /dev/null +++ b/packages/topdown/packets/layer-create.packet.js @@ -0,0 +1,18 @@ +import {Packet} from '@avocado/net'; + +export class LayerCreatePacket extends Packet { + + static get schema() { + return { + ...super.schema, + data: { + tilesetUri: 'string', + tiles: { + size: ['uint16'], + data: ['uint16'], + }, + }, + }; + } + +} diff --git a/packages/topdown/packets/room-size-update.packet.js b/packages/topdown/packets/room-size-update.packet.js new file mode 100644 index 0000000..979bf88 --- /dev/null +++ b/packages/topdown/packets/room-size-update.packet.js @@ -0,0 +1,12 @@ +import {Packet} from '@avocado/net'; + +export class RoomSizeUpdatePacket extends Packet { + + static get schema() { + return { + ...super.schema, + data: 'uint32', + }; + } + +} diff --git a/packages/topdown/packets/tile-update.packet.js b/packages/topdown/packets/tile-update.packet.js new file mode 100644 index 0000000..e9d010a --- /dev/null +++ b/packages/topdown/packets/tile-update.packet.js @@ -0,0 +1,12 @@ +import {Packet} from '@avocado/net'; + +export class TileUpdatePacket extends Packet { + + static get schema() { + return { + ...super.schema, + data: ['uint16'], + }; + } + +} diff --git a/packages/topdown/room.js b/packages/topdown/room.js index dc8c53d..b76c02e 100644 --- a/packages/topdown/room.js +++ b/packages/topdown/room.js @@ -1,11 +1,15 @@ import * as I from 'immutable'; import {compose, EventEmitter, Property} from '@avocado/core'; +import {EntityCreatePacket, EntityPacket, EntityRemovePacket} from '@avocado/entity'; import {Vector} from '@avocado/math'; import {RectangleShape} from '@avocado/physics'; import {Synchronized} from '@avocado/state'; import {Layers} from './layers'; +import {LayerCreatePacket} from './packets/layer-create.packet'; +import {RoomSizeUpdatePacket} from './packets/room-size-update.packet'; +import {TileUpdatePacket} from './packets/tile-update.packet'; const ROOM_BOUND_SIZE = 64; const HALF_ROOM_BOUND_SIZE = ROOM_BOUND_SIZE / 2; @@ -36,6 +40,34 @@ export class Room extends decorate(class {}) { this.on('worldChanged', this.onWorldChanged, this); } + acceptPacket(packet) { + if ( + packet instanceof LayerCreatePacket + || packet instanceof TileUpdatePacket + || packet instanceof EntityCreatePacket + ) { + this.layers.acceptPacket(packet); + } + if (packet instanceof EntityRemovePacket) { + const entity = this.findEntity(packet.data.uuid); + if (entity) { + entity.destroy(); + } + return; + } + if (packet instanceof EntityPacket) { + const entity = this.findEntity(packet.data.uuid); + if (entity) { + entity.acceptPacket(packet); + } + } + if (packet instanceof RoomSizeUpdatePacket) { + const x = packet.data & 0xFFFF; + const y = packet.data >> 16; + this.size = [x, y]; + } + } + addEntityToLayer(entity, layerIndex = 0) { this.layers.addEntityToLayer(entity, layerIndex); } @@ -78,7 +110,7 @@ export class Room extends decorate(class {}) { this.emit('entityRemoved', entity); } - onLayerAdded(layer, index) { + onLayerAdded(layer) { layer.world = this.world; } @@ -88,7 +120,7 @@ export class Room extends decorate(class {}) { onWorldChanged() { const world = this.world; - for (const {layer} of this.layers) { + for (const layer of this.layers) { layer.world = world; for (const entity of layer.entityList) { if (entity.is('physical')) { @@ -100,16 +132,22 @@ export class Room extends decorate(class {}) { this.updateBounds(); } - removeEntityFromLayer(entity, layerIndex = 0) { - this.layers.removeEntityFromLayer(entity, layerIndex); + packetsForUpdate(force = false) { + const packets = []; + if (force) { + const packed = (this.height << 16) | (this.width); + packets.push(new RoomSizeUpdatePacket(packed)); + } + // Layers packets. + const layersPacketsForUpdate = this.layers.packetsForUpdate(force); + for (let i = 0; i < layersPacketsForUpdate.length; i++) { + packets.push(layersPacketsForUpdate[i]); + } + return packets; } - synchronizedChildren() { - return [ - 'width', - 'height', - 'layers', - ]; + removeEntityFromLayer(entity, layerIndex) { + this.layers.removeEntityFromLayer(entity, layerIndex); } updateBounds() { @@ -152,7 +190,7 @@ export class Room extends decorate(class {}) { } tick(elapsed) { - this.tickSynchronized(elapsed); + this.layers.tick(elapsed); if (this.world) { this.world.tick(elapsed); } diff --git a/packages/topdown/tiles.js b/packages/topdown/tiles.js index c64f362..e7e2409 100644 --- a/packages/topdown/tiles.js +++ b/packages/topdown/tiles.js @@ -4,6 +4,8 @@ import {compose, EventEmitter} from '@avocado/core'; import {Rectangle, Vector} from '@avocado/math'; import {Synchronized} from '@avocado/state'; +import {TileUpdatePacket} from './packets/tile-update.packet'; + const decorate = compose( EventEmitter, Vector.Mixin('size', 'width', 'height', { @@ -14,9 +16,17 @@ const decorate = compose( export class Tiles extends decorate(class {}) { - constructor() { + constructor(json) { super(); - this.data = I.List(); + this.data = []; + if ('undefined' !== typeof json) { + this.fromJSON(json); + } + } + + acceptPacket(packet) { + if (packet instanceof TileUpdatePacket) { + } } forEachTile(fn) { @@ -25,7 +35,7 @@ export class Tiles extends decorate(class {}) { let i = 0; for (let k = 0; k < height; ++k) { for (let j = 0; j < width; ++j) { - fn(this.data.get(i), x, y, i); + fn(this.data[i], x, y, i); ++i; ++x; } @@ -39,26 +49,11 @@ export class Tiles extends decorate(class {}) { this.size = json.size; } if (json.data) { - this.data = I.fromJS(json.data); + this.data = json.data.slice(0); } return this; } - patchStateStep(key, step) { - if ('data' === key) { - const oldData = this.data; - for (const i in step.value) { - const index = parseInt(i); - this.data = this.data.set(index, step.value[i]); - } - if (oldData !== this.data) { - this.emit('dataChanged'); - } - return; - } - super.patchStateStep(key, step); - } - get rectangle() { return Rectangle.compose([0, 0], this.size); } @@ -69,10 +64,10 @@ export class Tiles extends decorate(class {}) { return; } const index = y * this.width + x; - if (index < 0 || index >= this.data.size) { + if (index < 0 || index >= this.data.length) { return; } - this.data = this.data.set(index, tile); + this.data[index] = tile; this.emit('dataChanged'); } @@ -94,7 +89,7 @@ export class Tiles extends decorate(class {}) { const slice = new Array(sliceWidth * sliceHeight); for (let j = 0; j < sliceHeight; ++j) { for (let i = 0; i < sliceWidth; ++i) { - slice[sliceRow + x] = this.data.get(dataRow + x); + slice[sliceRow + x] = this.data[dataRow + x]; x++; } sliceRow += sliceWidth; @@ -104,26 +99,14 @@ export class Tiles extends decorate(class {}) { return slice; } - synchronizedChildren() { - return [ - 'width', - 'height', - 'data', - ]; - } - - tick(elapsed) { - this.tickSynchronized(elapsed); - } - tileAt(x, y) { - return this.data.get(y * this.width + x); + return this.data[y * this.width + x]; } toJSON() { return { - size: [...this.size], - data: [...this.data.toJS()], + size: Vector.copy(this.size), + data: this.data.slice(0), }; }