flow: massive sync update

This commit is contained in:
cha0s 2019-09-29 13:20:08 -05:00
parent 4534d0b5ea
commit 631e07e4c6
8 changed files with 119 additions and 348 deletions

View File

@ -7,7 +7,13 @@ import {Stage} from '@avocado/graphics';
import {ActionRegistry, InputPacket} from '@avocado/input';
import {Vector} from '@avocado/math';
import {SocketClient} from '@avocado/net/client/socket';
import {BundlePacket, ClientSynchronizer, SocketIoParser} from '@avocado/net';
import {
BundlePacket,
ClientSynchronizer,
idFromSynchronized,
SocketIoParser,
SynchronizedCreatePacket,
} from '@avocado/net';
import {clearAnimation, setAnimation} from '@avocado/timing';
import {World} from '@avocado/physics/matter/world';
import {Room, RoomView} from '@avocado/topdown';
@ -59,7 +65,7 @@ export class App extends decorate(class {}) {
super();
const config = this.readConfig();
// Room.
this.room = new Room();
this.room = null;
// World time.
this.worldTime = new WorldTime();
// Graphics.
@ -70,13 +76,10 @@ export class App extends decorate(class {}) {
this.renderHandle = undefined;
this.rps = new CycleTracker(1 / 60); // Refresh rate, actually.
this.stage = new Stage(config.visibleSize, config.visibleScale);
this.roomView = new RoomView(this.room, this.stage.renderer);
this.stage.addChild(this.roomView);
this.roomView = null;
this.on('darknessChanged', this.applyLighting, this);
this.on('isFocusedChanged', this.applyMuting, this)
this.on('isMenuOpenedChanged', this.applyMuting, this)
// Listen for new entities.
this.room.on('entityAdded', this.onRoomEntityAdded, this);
// Input.
this.actionRegistry = new ActionRegistry();
this.actionRegistry.mapKeysToActions(config.actionKeyMap);
@ -97,14 +100,49 @@ export class App extends decorate(class {}) {
this.tps = new CycleTracker(config.simulationFrequency);
this.simulationHandle = undefined;
this.world = config.doPhysicsSimulation ? new World() : undefined;
this.room.world = this.world;
this.room.world.stepTime = config.simulationFrequency;
this.world.stepTime = config.simulationFrequency;
// State synchronization.
this.state = undefined;
this.synchronizer = new ClientSynchronizer();
this.synchronizer.addSynchronized(this.worldTime);
}
acceptPacket(packet) {
this.synchronizer.acceptPacket(packet);
// Keep refs to new synchronizeds.
if (packet instanceof SynchronizedCreatePacket) {
const roomId = idFromSynchronized(Room);
const {type, id} = packet.data.synchronized;
switch (type) {
// Track room.
case roomId:
const room = this.synchronizer.synchronized(type, id);
this.onRoomCreated(room);
break;
}
}
if (packet instanceof SelfEntityPacket) {
// Set self entity.
const selfEntity = this.room.findEntity(packet.data);
if (selfEntity) {
this.selfEntity = selfEntity;
this.selfEntityUuid = undefined;
// Add back our self entity traits.
const selfEntityOnlyTraits = [
'controllable',
'followed',
];
for (const type of selfEntityOnlyTraits) {
selfEntity.addTrait(type);
}
const {camera} = selfEntity;
this.stage.camera = camera;
// Avoid the initial 'lerp.
camera.realPosition = camera.position;
}
}
}
applyLighting() {
const roomView = this.roomView;
if (!roomView) {
@ -165,7 +203,7 @@ export class App extends decorate(class {}) {
});
this.socket.on('connect', () => {
this.removeFromDom(document.querySelector('.app'));
this.room.layers.destroy();
this.room = null;
this.selfEntity = undefined;
this.selfEntityUuid = undefined;
this.isConnected = true;
@ -272,12 +310,11 @@ export class App extends decorate(class {}) {
this.hasReceivedState = true;
}
if (packet instanceof BundlePacket) {
return this.synchronizer.acceptPackets(packet.data);
for (let i = 0; i < packet.data.length; i++) {
this.onPacket(packet.data[i]);
}
}
if (packet instanceof SelfEntityPacket) {
this.selfEntityUuid = packet.data;
}
this.synchronizer.acceptPackets([packet]);
this.acceptPacket(packet);
}
onPointerDown(event) {
@ -301,6 +338,26 @@ export class App extends decorate(class {}) {
this.pointingAt = [-1, -1];
}
onRoomCreated(room) {
// Keep tabs on the room.
if (this.room) {
this.room.off('entityAdded', this.onRoomEntityAdded);
}
this.room = room;
// View.
this.stage.removeChild(this.roomView);
this.roomView = new RoomView(this.room, this.stage.renderer);
this.stage.addChild(this.roomView);
// Listen for new entities.
this.room.on('entityAdded', this.onRoomEntityAdded, this);
const allEntities = this.room.allEntities();
for (let i = 0; i < allEntities.length; i++) {
this.onRoomEntityAdded(allEntities[i]);
}
// Physics.
this.room.world = this.world;
}
onRoomEntityAdded(entity) {
// Traits that shouldn't be on client.
const noClientTraits = [
@ -339,21 +396,6 @@ export class App extends decorate(class {}) {
entity.container.night(this.darkness);
}
entity.stage = this.stage;
// Set self entity.
if (this.selfEntityUuid) {
if (entity === this.room.findEntity(this.selfEntityUuid)) {
this.selfEntity = entity;
this.selfEntityUuid = undefined;
// Add back our self entity traits.
for (const type of selfEntityOnlyTraits) {
entity.addTrait(type);
}
const {camera} = entity;
this.stage.camera = camera;
// Avoid the initial 'lerp.
camera.realPosition = camera.position;
}
}
}
onWheel(event) {
@ -570,7 +612,9 @@ export class App extends decorate(class {}) {
}
// Tick.
this.worldTime.tick(elapsed);
this.room.tick(elapsed);
if (this.room) {
this.room.tick(elapsed);
}
// Sample.
this.tps.sample(elapsed);
}, 1000 * config.simulationFrequency);

View File

@ -60,9 +60,11 @@ export class Vulnerable extends Trait {
}
hydrate() {
this._isHydrating = true;
this.addEmitter();
this.addEmitterRenderer();
if (AVOCADO_CLIENT) {
this._isHydrating = true;
this.addEmitter();
this.addEmitterRenderer();
}
}
acceptDamage(damage) {

View File

@ -79,13 +79,15 @@ export class Item extends decorate(Trait) {
}
hydrate() {
const promises = [];
for (const index in this.params.slotImages) {
promises.push(Image.load(this.params.slotImages[index]).then((image) => {
this._slotImages[index] = image;
}));
if (AVOCADO_CLIENT) {
const promises = [];
for (const index in this.params.slotImages) {
promises.push(Image.load(this.params.slotImages[index]).then((image) => {
this._slotImages[index] = image;
}));
}
return Promise.all(promises);
}
return Promise.all(promises);
}
get itemActions() {

View File

@ -45,16 +45,10 @@ export class Receptacle extends decorate(Trait) {
item.qty = slotSpec.qty;
// Set wielder.
item.wielder = this.entity;
// On the client, hydrate the item before adding it to the inventory.
if (AVOCADO_CLIENT) {
item.hydrate().then(() => {
this.entity.addItemToSlot(item, index);
});
}
// Server just adds it.
else {
// Hydrate the item before adding it to the inventory.
item.hydrate().then(() => {
this.entity.addItemToSlot(item, index);
}
});
});
}
}

View File

@ -28,7 +28,7 @@ export class WorldTime extends decorate(class {}) {
acceptPacket(packet) {
if (packet instanceof WorldTimePacket) {
this.fromNetwork(packet.data.hour);
this.fromNetwork(packet.data);
}
}
@ -51,14 +51,10 @@ export class WorldTime extends decorate(class {}) {
fromJSON(json) {
if (json.hour) {
this._hour = this.fromNetwork(json.hour);
this._hour = json.hour / MAGIC_TO_FIT_HOUR_INTO_USHORT;
}
}
fromNetwork(hour) {
this._hour = hour / MAGIC_TO_FIT_HOUR_INTO_USHORT;
}
get hour() {
return this._hour;
}
@ -71,7 +67,7 @@ export class WorldTime extends decorate(class {}) {
if (!this._isUpdateReady) {
return;
}
return new WorldTimePacket(this.toNetwork());
return new WorldTimePacket(this.toNetwork(informed));
}
secondsPerHour() {
@ -84,14 +80,10 @@ export class WorldTime extends decorate(class {}) {
this.ticker.tick(elapsed);
}
toNetwork() {
toJSON() {
return {
hour: (this._hour * MAGIC_TO_FIT_HOUR_INTO_USHORT) >> 0,
};
}
toJSON() {
return this.toNetwork();
}
}

View File

@ -67,12 +67,12 @@ export function kittyFireJSON() {
// for (let i = 0; i < 20; ++i) {
// addEntityWithRandomPosition('/flower-barrel.entity.json');
// }
for (let i = 0; i < 3; ++i) {
addEntityWithRandomPosition('/mama-kitty-spawner.entity.json');
}
for (let i = 0; i < 5; ++i) {
addEntityWithRandomPosition('/fire.entity.json');
}
// for (let i = 0; i < 3; ++i) {
// addEntityWithRandomPosition('/mama-kitty-spawner.entity.json');
// }
// for (let i = 0; i < 5; ++i) {
// addEntityWithRandomPosition('/fire.entity.json');
// }
// for (let i = 0; i < 1; ++i) {
// addEntityWithRandomPosition('/blue-fire.entity.json');
// }

View File

@ -57,14 +57,15 @@ export default class Game {
this.informables.splice(index, 1);
}
});
// Sync world time.
entity.addSynchronized(this.worldTime);
// Add entity to room.
if (this.room) {
this.room.addEntityToLayer(entity, 0);
}
// Initial information.
entity.inform();
Promise.resolve(entity.hydrate()).then(() => {
// Sync world time.
entity.addSynchronized(this.worldTime);
// Add entity to room.
if (this.room) {
this.room.addEntityToLayer(entity, 0);
entity.addSynchronized(this.room);
}
});
// Listen for events.
socket.on('packet', this.createPacketListener(socket));
socket.on('disconnect', this.createDisconnectionListener(socket));
@ -110,6 +111,9 @@ export default class Game {
this.informables[i].inform();
}
// Clean packets.
if (this.room) {
this.room.cleanPackets();
}
this.worldTime.cleanPackets();
}

View File

@ -8,6 +8,8 @@ import {EntityCreatePacket, EntityPacket, EntityRemovePacket, Trait} from '@avoc
import {Rectangle, Vector} from '@avocado/math';
import {BundlePacket, ServerSynchronizer} from '@avocado/net';
import {SelfEntityPacket} from '../../common/packets/self-entity.packet';
const decorate = compose(
);
@ -19,7 +21,7 @@ export class Informed extends decorate(Trait) {
constructor(entity, params, state) {
super(entity, params, state);
this.seenEntities = [];
this._sentSelfEntityPacket = false;
this._socket = undefined;
this._synchronizer = new ServerSynchronizer();
}
@ -44,251 +46,6 @@ export class Informed extends decorate(Trait) {
);
}
deduplicateEntityCreatePackets(packets) {
const created = new Map();
return packets.filter((packet) => {
const entity = packet.entity;
if (!entity) {
return true;
}
// Only care about creates.
if (!(packet instanceof EntityCreatePacket)) {
return true;
}
if (created.has(entity)) {
return false;
}
created.set(entity, true);
return true;
});
}
filter(packets) {
packets = packets.filter((packet) => {
return !(packet instanceof EntityPacket);
});
// // Filter invisible entities.
// packets = this.filterInvisibleEntityPackets(packets);
// // Reduce entities by range.
// const [
// inRangeEntities,
// outOfRangeEntities,
// visibleEntities,
// ] = this.reducePacketEntitiesByRange(packets);
// // TODO? Upgrade seen entity packets that are out of range to entity
// // remembers.
// // Filter the out of range entity updates.
// packets = this.filterOutOfRangeEntityPackets(
// packets,
// outOfRangeEntities
// );
// // Deduplicate entity creates.
// packets = this.deduplicateEntityCreatePackets(packets);
// // Filter known creates.
// packets = this.filterKnownEntityCreatePackets(packets);
// // Inject create packets.
// packets = this.injectEntityCreatePackets(packets, visibleEntities);
// // Inject removes for any previously seen entity that isn't visible
// // anymore.
// packets = this.injectEntityRemovePackets(packets, visibleEntities);
// // "See" entities.
// this.markEntitiesSeen(visibleEntities);
// // Unsee any removed entities.
// this.markEntitiesUnseen(packets);
return packets;
}
filterInvisibleEntityPackets(packets) {
return packets.filter((packet) => {
const entity = packet.entity;
if (!entity) {
return true;
}
// Removes could be on destroyed entities, so pass them.
if (packet instanceof EntityRemovePacket) {
return true;
}
return entity.isVisible;
});
}
filterKnownEntityCreatePackets(packets) {
return packets.filter((packet) => {
if (!packet.entity) {
return true;
}
if (!(packet instanceof EntityCreatePacket)) {
return true;
}
return !this.hasSeenEntity(packet.entity);
});
}
filterOutOfRangeEntityPackets(packets, outOfRangeEntities) {
return packets.filter((packet) => {
const entity = packet.entity;
if (!entity) {
return true;
}
// Send removes even if they're out of range, if client knows about
// them.
if (
packet instanceof EntityRemovePacket
&& this.hasSeenEntity(entity)
) {
return true;
}
return -1 === outOfRangeEntities.indexOf(packet.entity);
});
}
hasSeenEntity(entity) {
return -1 !== this.seenEntities.indexOf(entity);
}
injectEntityCreatePackets(packets, visibleEntities) {
// Get a list of all visible but not yet seen entities.
const visibleButNotYetSeen = [];
for (let i = 0; i < visibleEntities.length; i++) {
const entity = visibleEntities[i];
if (!this.hasSeenEntity(entity)) {
visibleButNotYetSeen.push(entity);
}
}
// Get a list of all existing created entities.
const allExistingCreatedEntities = packets.filter((packet) => {
return packet instanceof EntityCreatePacket;
}).map((packet) => {
return packet.entity;
});
// JIT inject creates before any unknown updates.
for (let i = 0; i < packets.length; i++) {
const packet = packets[i];
// Only care about entity packets.
if (!(packet instanceof EntityPacket)) {
continue;
}
// Only unknown.
const entity = packet.entity;
if (this.hasSeenEntity(entity)) {
continue;
}
if (-1 !== allExistingCreatedEntities.indexOf(entity)) {
continue;
}
// Not creates nor removes.
if (
packet instanceof EntityCreatePacket
|| packet instanceof EntityRemovePacket
) {
// This does count as seen.
const index = visibleButNotYetSeen.indexOf(entity);
if (-1 !== index) {
visibleButNotYetSeen.splice(index, 1);
}
continue;
}
// Inject.
packets.splice(
i,
0,
new EntityCreatePacket(entity.mergeDiff(), entity)
);
i += 1;
// We've seen it.
const index = visibleButNotYetSeen.indexOf(entity);
if (-1 !== index) {
visibleButNotYetSeen.splice(index, 1);
}
allExistingCreatedEntities.push(entity);
}
// Append creates for any visible-but-not-yet-seen entities.
for (let i = 0; i < visibleButNotYetSeen.length; i++) {
const entity = visibleButNotYetSeen[i];
// Skip any existing creates.
if (-1 === allExistingCreatedEntities.indexOf(entity)) {
packets.push(new EntityCreatePacket(entity.mergeDiff(), entity));
}
}
return packets;
}
injectEntityRemovePackets(packets, visibleEntities) {
const alreadyRemovedEntities = packets.filter((packet) => {
return packet instanceof EntityRemovePacket;
}).map((packet) => {
return packet.entity;
});
for (let i = 0; i < this.seenEntities.length; i++) {
const entity = this.seenEntities[i];
if (-1 === visibleEntities.indexOf(entity)) {
if (-1 === alreadyRemovedEntities.indexOf(entity)) {
packets.push(new EntityRemovePacket({}, entity));
}
}
}
return packets;
}
markEntitiesSeen(visibleEntities) {
for (let i = 0; i < visibleEntities.length; i++) {
const entity = visibleEntities[i];
if (-1 === this.seenEntities.indexOf(entity)) {
this.seenEntities.push(entity);
}
}
}
markEntitiesUnseen(packets) {
const removedEntities = packets.filter((packet) => {
return packet instanceof EntityRemovePacket;
}).map((packet) => {
return packet.entity;
});
for (let i = 0; i < removedEntities.length; i++) {
const entity = removedEntities[i];
const index = this.seenEntities.indexOf(entity)
if (-1 !== index) {
this.seenEntities.splice(index, 1);
}
}
}
reducePacketEntitiesByRange(packets) {
// Unique packet entities.
const packetEntitiesMap = new Map();
for (let i = 0; i < packets.length; i++) {
const entity = packets[i].entity;
if (entity && !packetEntitiesMap.has(entity)) {
packetEntitiesMap.set(entity, true);
}
}
// Locate visible entities.
const areaToInform = this.areaToInform;
const visibleEntities = this.entity.room.visibleEntities(areaToInform);
// Document out of range entities.
const inRangeEntities = [];
const outOfRangeEntities = [];
let it = packetEntitiesMap.keys();
for (let value = it.next(); !value.done; value = it.next()) {
const entity = value.value;
if (-1 === visibleEntities.indexOf(entity)) {
if (-1 === outOfRangeEntities.indexOf(entity)) {
outOfRangeEntities.push(entity);
}
}
else {
if (-1 === inRangeEntities.indexOf(entity)) {
inRangeEntities.push(entity);
}
}
}
return [inRangeEntities, outOfRangeEntities, visibleEntities];
}
get socket() {
return this._socket;
}
@ -303,42 +60,18 @@ export class Informed extends decorate(Trait) {
inform: () => {
const payload = this._synchronizer.packetsFor(this.entity);
// const payload = [];
// for (let i = 0; i < this._willWatch.length; i++) {
// const packets = this._willWatch[i].packets(this.entity);
// for (let j = 0; j < packets.length; j++) {
// payload.push(packets[j]);
// }
// }
// for (let i = 0; i < this._watching.length; i++) {
// const packets = this._watching[i].packets(this.entity);
// for (let j = 0; j < packets.length; j++) {
// payload.push(packets[j]);
// }
// }
// for (let i = 0; i < this._willWatch.length; i++) {
// this._watching.push(this._willWatch[i]);
// }
// this._willWatch = [];
// // Clean all packets.
// for (let i = 0; i < this._watching.length; i++) {
// this._watching[i].cleanPackets();
// }
if (0 === payload.length) {
return;
}
if (!this._sentSelfEntityPacket) {
this._sentSelfEntityPacket = true;
payload.push(new SelfEntityPacket(this.entity.numericUid));
}
if (this._socket) {
this._socket.send(new BundlePacket(payload));
}
},
seesEntity: (entity) => {
return Rectangle.isTouching(
this.entity.areaToInform,
entity.visibleAabb
);
},
addSynchronized: (synchronized) => {
this._synchronizer.addSynchronized(synchronized);
},