feat: entity packets!

This commit is contained in:
cha0s 2019-04-28 22:35:20 -05:00
parent d10f619bde
commit 1a5015f4b6
11 changed files with 159 additions and 86 deletions

View File

@ -3,6 +3,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
// 2nd party.
import {compose} from '@avocado/core';
import {EntityPacketSynchronizer} from '@avocado/entity';
import {Stage} from '@avocado/graphics';
import {ActionRegistry, InputPacket} from '@avocado/input';
import {Vector} from '@avocado/math';
@ -93,10 +94,10 @@ export class App extends decorate(class {}) {
this.pointingAt = [-1, -1];
this.pointerMovementHandle = undefined;
// Net.
// this.AugmentedParser = augmentParserWithThroughput(SocketIoParser);
this.AugmentedParser = augmentParserWithThroughput(SocketIoParser);
this.entityPacketSynchronizer = new EntityPacketSynchronizer();
this.hasReceivedState = false;
this.isConnected = false;
this.packetsToSend = [];
this.socket = undefined;
// Simulation.
this.tps = new CycleTracker(config.simulationFrequency);
@ -168,7 +169,7 @@ export class App extends decorate(class {}) {
connect() {
const config = this.readConfig();
this.socket = new SocketClient(config.connectionUrl, {
// parser: this.AugmentedParser,
parser: this.AugmentedParser,
});
this.socket.on('connect', () => {
this.removeFromDom(document.querySelector('.app'));
@ -211,26 +212,6 @@ export class App extends decorate(class {}) {
return reactContainer;
}
sendPackets() {
// Merge.
const packetMergeMap = new Map();
for (let i = 0; i < this.packetsToSend.length; i++) {
const packet = this.packetsToSend[i];
const Packet = packet.constructor;
if (!packetMergeMap.has(Packet)) {
packetMergeMap.set(Packet, packet);
continue;
}
packetMergeMap.get(Packet).mergeWith(packet);
}
const packetList = Array.from(packetMergeMap.values());
for (let i = 0; i < packetList.length; i++) {
const packet = packetList[i];
this.socket.send(packet);
}
this.packetsToSend = [];
}
eventIsInUi(event) {
let walk = event.target;
while (walk) {
@ -266,6 +247,7 @@ export class App extends decorate(class {}) {
}
onPacket(packet) {
this.entityPacketSynchronizer.acceptPacket(packet);
if (packet instanceof StateKeysPacket) {
this.unpacker.registerKeys(packet.data);
}
@ -313,6 +295,8 @@ export class App extends decorate(class {}) {
}
onRoomEntityAdded(entity) {
// Packets.
this.entityPacketSynchronizer.trackEntity(entity);
// Traits that shouldn't be on client.
const noClientTraits = [
'behaved',
@ -454,7 +438,7 @@ export class App extends decorate(class {}) {
const DebugUiComponent = <DebugUi
actionRegistry={this.actionRegistry}
app={this}
// Parser={this.AugmentedParser}
Parser={this.AugmentedParser}
socket={this.socket}
stage={this.stage}
/>;
@ -479,7 +463,7 @@ export class App extends decorate(class {}) {
this.inputHandle = setInterval(() => {
if (this.actionState !== this.actionRegistry.state) {
this.actionState = this.actionRegistry.state;
this.packetsToSend.push(InputPacket.fromState(this.actionState));
this.socket.send(InputPacket.fromState(this.actionState));
}
}, 1000 * config.inputFrequency);
// Mouse/touch movement.
@ -544,7 +528,12 @@ export class App extends decorate(class {}) {
this.synchronizer.tick(elapsed);
this.state = this.synchronizer.state;
// Emit packets.
this.sendPackets();
this.entityPacketSynchronizer.flushPackets((packetEntity, packets) => {
for (let i = 0; i < packets.length; i++) {
const packet = packets[i];
this.socket.send(packet);
}
});
// Sample.
this.tps.sample(elapsed);
}, 1000 * config.simulationFrequency);

View File

@ -4,6 +4,8 @@ import React from 'react';
import {compose} from '@avocado/core';
import contempo from 'contempo';
import {AFFINITY_FIRE} from '../../common/combat/constants';
const decorate = compose(
contempo(`
.damage {
@ -21,7 +23,7 @@ const decorate = compose(
font-weight: bold;
text-shadow: 0.5px 0.5px 0px black;
}
.particle .text.fire.is-damage {
.particle .text.affinity-${AFFINITY_FIRE}.is-damage {
color: #FFA500;
text-shadow:
0.5px 0.5px black,

View File

@ -7,6 +7,7 @@ import {Vector} from '@avocado/math';
import contempo from 'contempo';
// 1st party.
import {usePropertyChange} from '../hooks/use-property-change';
import Connection from './connection';
import SelfEntity from './self-entity';
import Timers from './timers';
@ -61,6 +62,7 @@ const DebugUi = ({
}}
>
<Timers app={app} />
<Connection Parser={app.AugmentedParser} socket={app.socket} />
<SelfEntity app={app} />
</div>
</div>;

View File

@ -0,0 +1,2 @@
export const AFFINITY_PHYSICAL = 0;
export const AFFINITY_FIRE = 1;

View File

@ -0,0 +1,24 @@
import {EntityPacket} from '@avocado/entity';
export class DamagePacket extends EntityPacket {
static get schema() {
const superSchema = super.schema;
superSchema.data[0].damages = [
{
amount: 'varuint',
damageSpec: {
affinity: 'uint8',
},
from: 'string',
isDamage: 'bool',
},
];
return superSchema;
}
mergeWith(other) {
this.data[0].damages.push(...other.data[0].damages);
}
}

View File

@ -2,6 +2,8 @@ import * as I from 'immutable';
import {Trait} from '@avocado/entity';
import {AFFINITY_PHYSICAL} from './constants';
export class Damaging extends Trait {
static defaultParams() {
@ -16,7 +18,7 @@ export class Damaging extends Trait {
const damageSpecsJSON = this.params.get('damageSpecs').toJS();
this._damageSpecs = damageSpecsJSON.map((damageSpec) => {
return {
affinity: 'physical',
affinity: AFFINITY_PHYSICAL,
lock: 0.1,
power: 0,
variance: 0.2,

View File

@ -28,7 +28,7 @@ class DamageTextNode extends TextNode {
spanClassName() {
const {damageSpec, isDamage} = this.damage;
let className = super.spanClassName();
className += ' ' + damageSpec.affinity;
className += ' affinity-' + damageSpec.affinity;
if (isDamage) {
className += ' is-damage';
}
@ -70,7 +70,7 @@ export class DamageEmitter {
new Proton.Mass(1),
new Proton.Life(2),
new Proton.Velocity(
new Proton.Span(80, 120),
new Proton.Span(50, 90),
new Proton.Vector3D(0, 5, 0),
27.5
),
@ -81,7 +81,7 @@ export class DamageEmitter {
const behaviors = [
new Proton.Alpha(1, .25),
new Proton.Scale(.8, 1.2),
new Proton.Force(0, -1, 0),
new Proton.Force(0, -0.5, 0),
rot,
];
this.emitter.createParticle(initializers, behaviors);

View File

@ -9,6 +9,7 @@ import {
} from '@avocado/behavior';
import {hasGraphics, TextNodeRenderer} from '@avocado/graphics';
import {DamagePacket} from './damage.packet';
import {DamageEmitter} from './emitter';
export class Vulnerable extends Trait {
@ -33,15 +34,8 @@ export class Vulnerable extends Trait {
}
}
static defaultState() {
return {
damageList: I.Map(),
};
}
initialize() {
this.damageId = 0;
this.damageList = {};
this._hasAddedEmitter = false;
this._hasAddedEmitterRenderer = false;
this._isHydrating = false;
@ -58,35 +52,6 @@ export class Vulnerable extends Trait {
this.addEmitterRenderer();
}
patchStateStep(key, step) {
if ('state' !== key) {
return;
}
const stateKey = step.path.substr(1);
const value = this.transformPatchValue(stateKey, step.value);
const stateKeyParts = stateKey.split('/');
switch (stateKeyParts[0]) {
case 'damageList':
if (!value) {
break;
}
for (let i = 0; i < value.length; ++i) {
const damage = value[i];
if (this.entity.is('listed')) {
damage.from = this.entity.list.findEntity(damage.from);
}
else {
damage.from = undefined;
}
this.acceptDamage(damage);
}
break;
default:
super.patchStateStep(key, step);
break;
}
}
acceptDamage(damage) {
const context = createContext();
context.add('entity', this.entity);
@ -102,6 +67,23 @@ export class Vulnerable extends Trait {
});
}
acceptPacket(packet) {
if (packet instanceof DamagePacket) {
packet.forEachData(({damages}) => {
for (let i = 0; i < damages.length; ++i) {
const damage = damages[i];
if (this.entity.is('listed')) {
damage.from = this.entity.list.findEntity(damage.from);
}
else {
damage.from = undefined;
}
this.acceptDamage(damage);
}
});
}
}
addEmitter() {
if (!this._isHydrating) {
return;
@ -153,6 +135,18 @@ export class Vulnerable extends Trait {
this.addEmitterRenderer();
},
tookDamage: (damage) => {
if (AVOCADO_SERVER) {
this.entity.emit(
'sendPacket',
new DamagePacket([{
uuid: this.entity.instanceUuid,
damages: [damage],
}])
);
}
},
traitsChanged: () => {
this.addEmitter();
this.addEmitterRenderer();
@ -190,9 +184,6 @@ export class Vulnerable extends Trait {
}
}
amount = Math.abs(amount);
if (!this.damageList[entity.instanceUuid]) {
this.damageList[entity.instanceUuid] = [];
}
const damage = {
id: this.damageId++,
isDamage,
@ -200,7 +191,6 @@ export class Vulnerable extends Trait {
damageSpec,
from: entity.instanceUuid,
};
this.damageList[entity.instanceUuid].push(damage);
this.entity.emit('tookDamage', damage);
}
},
@ -216,11 +206,6 @@ export class Vulnerable extends Trait {
}
}
if (AVOCADO_SERVER) {
if (Object.keys(this.damageList).length > 0) {
this.state = this.state.set('damageList', I.Map(this.damageList));
this.isDirty = true;
this.damageList = {};
}
const keys = Array.from(this.locks.keys());
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];

View File

@ -3,6 +3,8 @@ import {Vector} from '@avocado/math';
import {World} from '@avocado/physics/matter/world';
import {Room} from '@avocado/topdown';
import {AFFINITY_FIRE} from '../common/combat/constants';
// Behaviors!
const move = buildInvoke(['entity', 'moveFor'], [
buildInvoke(['global', 'randomNumber'], [0.25, 2.5, false])
@ -65,7 +67,7 @@ function fireJSON(position) {
damagingSound: 'fire',
damageSpecs: [
{
affinity: 'fire',
affinity: AFFINITY_FIRE,
lock: 0.15,
power: 5,
variance: 0.25,

View File

@ -3,6 +3,7 @@ import msgpack from 'msgpack-lite';
import {performance} from 'perf_hooks';
// 3rd party.
// 2nd party.
import {EntityPacketSynchronizer} from '@avocado/entity';
import {InputPacket} from '@avocado/input';
import {Synchronizer} from '@avocado/state';
import {Ticker} from '@avocado/timing';
@ -15,8 +16,14 @@ export default class Game {
constructor() {
const config = this.readConfig();
// Packets.
this.entityPacketSynchronizer = new EntityPacketSynchronizer();
// Room.
this.room = createRoom();
for (const entity of this.room.allEntities()) {
this.onEntityAddedToRoom(entity);
}
this.room.on('entityAdded', this.onEntityAddedToRoom, this);
// World time. Start at 10 am for testing.
this.worldTime = new WorldTime();
this.worldTime.hour = 10;
@ -42,6 +49,32 @@ export default class Game {
fn();
}
bundleEntityPackets() {
const bundledEntityPackets = new Map();
this.entityPacketSynchronizer.flushPackets((packetEntity, packets) => {
for (let i = 0; i < this.informables.length; ++i) {
const entity = this.informables[i];
if (!entity.seesEntity(packetEntity)) {
return;
}
if (!bundledEntityPackets.has(entity)) {
bundledEntityPackets.set(entity, new Map());
}
for (let j = 0; j < packets.length; j++) {
const packet = packets[j];
const Packet = packet.constructor;
if (!bundledEntityPackets.get(entity).has(Packet)) {
bundledEntityPackets.get(entity).set(Packet, packet);
}
else {
bundledEntityPackets.get(entity).get(Packet).bundleWith(packet);
}
}
}
});
return bundledEntityPackets;
}
acceptConnection(socket) {
// Create and track a new entity for the connection.
const entity = createEntityForConnection(socket);
@ -87,18 +120,38 @@ export default class Game {
createPacketListener(socket) {
const {entity} = socket;
return (packet) => {
this.entityPacketSynchronizer.acceptPacket(packet);
if (packet instanceof InputPacket) {
entity.inputState = packet.toState();
}
};
}
flushEntityPackets() {
const bundledEntityPackets = this.bundleEntityPackets();
const entities = Array.from(bundledEntityPackets.keys());
for (let i = 0; i < entities.length; i++) {
const entity = entities[i];
const packets = Array.from(bundledEntityPackets.get(entity).values());
for (let j = 0; j < packets.length; j++) {
const packet = packets[j];
entity.socket.send(packet);
}
}
}
inform() {
// Inform entities of the new state.
for (let i = 0; i < this.informables.length; ++i) {
const entity = this.informables[i];
entity.inform(this.synchronizer.state);
}
// Flush entity packets.
this.flushEntityPackets();
}
onEntityAddedToRoom(entity) {
this.entityPacketSynchronizer.trackEntity(entity);
}
readConfig() {

View File

@ -30,6 +30,18 @@ export class Informed extends decorate(Trait) {
}
}
get areaToInform() {
// Reduce entity list to visible.
const room = this.entity.room;
const camera = this.entity.camera;
// Blow up camera rectangle to compensate for camera desync.
const size = Rectangle.size(camera.rectangle);
return Rectangle.expand(
camera.rectangle,
Vector.scale(size, 0.5),
);
}
entityOverrides(path, fn) {
const pathOverrides = [
['physical', 'state', 'addedToPhysics'],
@ -174,18 +186,11 @@ export class Informed extends decorate(Trait) {
reduceState(state) {
// Set client's self entity.
state = state.set('selfEntity', this.entity.instanceUuid);
// Reduce entity list to visible.
const areaToInform = this.entity.areaToInform;
const room = this.entity.room;
const camera = this.entity.camera;
// Blow up camera rectangle to compensate for camera desync.
const size = Rectangle.size(camera.rectangle);
const visibleArea = Rectangle.expand(
camera.rectangle,
Vector.scale(size, 0.5),
);
// Write over entity list for every layer.
for (const {index, layer} of room.layers) {
const visibleEntities = layer.visibleEntities(visibleArea);
const visibleEntities = layer.visibleEntities(areaToInform);
const reduceEntityListRaw = {};
for (let i = 0; i < visibleEntities.length; ++i) {
const entity = visibleEntities[i];
@ -236,6 +241,13 @@ export class Informed extends decorate(Trait) {
this._socket.send(new StatePacket(packed));
},
seesEntity: (entity) => {
const areaToInform = this.entity.areaToInform;
const room = this.entity.room;
const visibleEntities = room.visibleEntities(areaToInform);
return -1 !== visibleEntities.indexOf(entity);
}
};
}