feat: basic client prediction
This commit is contained in:
parent
b0b13c8961
commit
2733968fa4
|
@ -38,7 +38,7 @@ export default class Interpolator {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
this.location += elapsed;
|
this.location += elapsed;
|
||||||
const fraction = this.location / this.duration;
|
const fraction = Math.min(1, this.location / this.duration);
|
||||||
const [from, to] = [this.penultimate.payload.ecs, this.latest.payload.ecs];
|
const [from, to] = [this.penultimate.payload.ecs, this.latest.payload.ecs];
|
||||||
const interpolated = {};
|
const interpolated = {};
|
||||||
for (const {entityId, componentName, properties} of this.tracking) {
|
for (const {entityId, componentName, properties} of this.tracking) {
|
||||||
|
|
|
@ -1,21 +1,51 @@
|
||||||
import Client from '@/net/client.js';
|
import Client from '@/net/client.js';
|
||||||
import {decode, encode} from '@/net/packets/index.js';
|
import {decode, encode} from '@/net/packets/index.js';
|
||||||
|
import {CLIENT_INTERPOLATION, CLIENT_PREDICTION} from '@/util/constants.js';
|
||||||
|
|
||||||
export default class LocalClient extends Client {
|
export default class LocalClient extends Client {
|
||||||
server = null;
|
server = null;
|
||||||
interpolator = null;
|
interpolator = null;
|
||||||
|
predictor = null;
|
||||||
async connect() {
|
async connect() {
|
||||||
this.server = new Worker(
|
this.server = new Worker(
|
||||||
new URL('../server/worker.js', import.meta.url),
|
new URL('../server/worker.js', import.meta.url),
|
||||||
{type: 'module'},
|
{type: 'module'},
|
||||||
);
|
);
|
||||||
this.interpolator = new Worker(
|
if (CLIENT_INTERPOLATION) {
|
||||||
new URL('./interpolator.js', import.meta.url),
|
this.interpolator = new Worker(
|
||||||
{type: 'module'},
|
new URL('./interpolator.js', import.meta.url),
|
||||||
);
|
{type: 'module'},
|
||||||
this.interpolator.addEventListener('message', (event) => {
|
);
|
||||||
this.accept(event.data);
|
this.interpolator.addEventListener('message', (event) => {
|
||||||
});
|
this.accept(event.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (CLIENT_PREDICTION) {
|
||||||
|
this.predictor = new Worker(
|
||||||
|
new URL('./predictor.js', import.meta.url),
|
||||||
|
{type: 'module'},
|
||||||
|
);
|
||||||
|
this.predictor.addEventListener('message', (event) => {
|
||||||
|
const [flow, packet] = event.data;
|
||||||
|
switch (flow) {
|
||||||
|
case 0: {
|
||||||
|
const packed = encode(packet);
|
||||||
|
this.throughput.$$up += packed.byteLength;
|
||||||
|
this.server.postMessage(packed);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1: {
|
||||||
|
if (CLIENT_INTERPOLATION) {
|
||||||
|
this.interpolator.postMessage(packet);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.accept(packet);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
this.server.addEventListener('message', (event) => {
|
this.server.addEventListener('message', (event) => {
|
||||||
if (0 === event.data) {
|
if (0 === event.data) {
|
||||||
this.server.terminate();
|
this.server.terminate();
|
||||||
|
@ -23,16 +53,32 @@ export default class LocalClient extends Client {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.throughput.$$down += event.data.byteLength;
|
this.throughput.$$down += event.data.byteLength;
|
||||||
this.interpolator.postMessage(decode(event.data));
|
const packet = decode(event.data);
|
||||||
|
if (CLIENT_PREDICTION) {
|
||||||
|
this.predictor.postMessage([1, packet]);
|
||||||
|
}
|
||||||
|
else if (CLIENT_INTERPOLATION) {
|
||||||
|
this.interpolator.postMessage(packet);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.accept(packet);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
disconnect() {
|
disconnect() {
|
||||||
this.server.postMessage(0);
|
this.server.postMessage(0);
|
||||||
this.interpolator.terminate();
|
if (CLIENT_INTERPOLATION) {
|
||||||
|
this.interpolator.terminate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
transmit(packet) {
|
transmit(packet) {
|
||||||
const packed = encode(packet);
|
if (CLIENT_PREDICTION) {
|
||||||
this.throughput.$$up += packed.byteLength;
|
this.predictor.postMessage([0, packet]);
|
||||||
this.server.postMessage(packed);
|
}
|
||||||
|
else {
|
||||||
|
const packed = encode(packet);
|
||||||
|
this.throughput.$$up += packed.byteLength;
|
||||||
|
this.server.postMessage(packed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
172
app/client/predictor.js
Normal file
172
app/client/predictor.js
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
import {LRUCache} from 'lru-cache';
|
||||||
|
|
||||||
|
import Components from '@/ecs/components/index.js';
|
||||||
|
import Ecs from '@/ecs/ecs.js';
|
||||||
|
import Systems from '@/ecs/systems/index.js';
|
||||||
|
import {withResolvers} from '@/util/promise.js';
|
||||||
|
|
||||||
|
const cache = new LRUCache({
|
||||||
|
max: 128,
|
||||||
|
});
|
||||||
|
|
||||||
|
class PredictionEcs extends Ecs {
|
||||||
|
async readAsset(uri) {
|
||||||
|
if (!cache.has(uri)) {
|
||||||
|
const {promise, resolve, reject} = withResolvers();
|
||||||
|
cache.set(uri, promise);
|
||||||
|
fetch(uri)
|
||||||
|
.then((response) => resolve(response.ok ? response.arrayBuffer() : new ArrayBuffer(0)))
|
||||||
|
.catch(reject);
|
||||||
|
}
|
||||||
|
return cache.get(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Flow = {
|
||||||
|
UP: 0,
|
||||||
|
DOWN: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Stage = {
|
||||||
|
UNACK: 0,
|
||||||
|
ACK: 1,
|
||||||
|
FINISHING: 2,
|
||||||
|
FINISHED: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = new Map();
|
||||||
|
|
||||||
|
let ecs = new PredictionEcs({Components, Systems});
|
||||||
|
|
||||||
|
let mainEntityId = 0;
|
||||||
|
|
||||||
|
function applyClientActions(elapsed) {
|
||||||
|
if (actions.size > 0) {
|
||||||
|
const main = ecs.get(mainEntityId);
|
||||||
|
const {Controlled} = main;
|
||||||
|
const finished = [];
|
||||||
|
for (const [id, action] of actions) {
|
||||||
|
if (Stage.UNACK === action.stage) {
|
||||||
|
if (!Controlled.locked) {
|
||||||
|
switch (action.action.type) {
|
||||||
|
case 'moveUp':
|
||||||
|
case 'moveRight':
|
||||||
|
case 'moveDown':
|
||||||
|
case 'moveLeft': {
|
||||||
|
Controlled[action.action.type] = action.action.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
action.steps.push(elapsed);
|
||||||
|
}
|
||||||
|
if (Stage.FINISHING === action.stage) {
|
||||||
|
if (!Controlled.locked) {
|
||||||
|
switch (action.action.type) {
|
||||||
|
case 'moveUp':
|
||||||
|
case 'moveRight':
|
||||||
|
case 'moveDown':
|
||||||
|
case 'moveLeft': {
|
||||||
|
Controlled[action.action.type] = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
action.stage = Stage.FINISHED;
|
||||||
|
}
|
||||||
|
if (Stage.FINISHED === action.stage) {
|
||||||
|
action.steps.shift();
|
||||||
|
if (0 === action.steps.length) {
|
||||||
|
finished.push(id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let leap = 0;
|
||||||
|
for (const step of action.steps) {
|
||||||
|
leap += step;
|
||||||
|
}
|
||||||
|
if (leap > 0) {
|
||||||
|
ecs.predict(main, leap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const id of finished) {
|
||||||
|
actions.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let downPromise;
|
||||||
|
|
||||||
|
const pending = new Map();
|
||||||
|
|
||||||
|
onmessage = async (event) => {
|
||||||
|
const [flow, packet] = event.data;
|
||||||
|
switch (flow) {
|
||||||
|
case Flow.UP: {
|
||||||
|
switch (packet.type) {
|
||||||
|
case 'Action': {
|
||||||
|
switch (packet.payload.type) {
|
||||||
|
case 'moveUp':
|
||||||
|
case 'moveRight':
|
||||||
|
case 'moveDown':
|
||||||
|
case 'moveLeft': {
|
||||||
|
if (0 === packet.payload.value) {
|
||||||
|
const ack = pending.get(packet.payload.type);
|
||||||
|
const action = actions.get(ack);
|
||||||
|
action.stage = Stage.FINISHING;
|
||||||
|
pending.delete(packet.payload.type);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const tx = {
|
||||||
|
action: packet.payload,
|
||||||
|
stage: Stage.UNACK,
|
||||||
|
steps: [],
|
||||||
|
};
|
||||||
|
packet.payload.ack = Math.random();
|
||||||
|
pending.set(packet.payload.type, packet.payload.ack);
|
||||||
|
actions.set(packet.payload.ack, tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
postMessage([0, packet]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Flow.DOWN: {
|
||||||
|
downPromise = Promise.resolve(downPromise).then(async () => {
|
||||||
|
switch (packet.type) {
|
||||||
|
case 'ActionAck': {
|
||||||
|
const action = actions.get(packet.payload.ack);
|
||||||
|
action.stage = Stage.ACK;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'Tick': {
|
||||||
|
for (const entityId in packet.payload.ecs) {
|
||||||
|
if (packet.payload.ecs[entityId]) {
|
||||||
|
if (packet.payload.ecs[entityId].MainEntity) {
|
||||||
|
mainEntityId = parseInt(entityId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await ecs.apply(packet.payload.ecs);
|
||||||
|
if (actions.size > 0) {
|
||||||
|
const main = ecs.get(mainEntityId);
|
||||||
|
const authoritative = structuredClone(main.toNet(main));
|
||||||
|
applyClientActions(packet.payload.elapsed);
|
||||||
|
if (ecs.diff[mainEntityId]) {
|
||||||
|
packet.payload.ecs[mainEntityId] = ecs.diff[mainEntityId];
|
||||||
|
}
|
||||||
|
await ecs.apply({[mainEntityId]: authoritative});
|
||||||
|
}
|
||||||
|
ecs.setClean();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
postMessage([1, packet]);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,22 +1,66 @@
|
||||||
import Client from '@/net/client.js';
|
import Client from '@/net/client.js';
|
||||||
import {decode, encode} from '@/net/packets/index.js';
|
import {decode, encode} from '@/net/packets/index.js';
|
||||||
|
import {CLIENT_INTERPOLATION, CLIENT_PREDICTION} from '@/util/constants.js';
|
||||||
|
|
||||||
export default class RemoteClient extends Client {
|
export default class RemoteClient extends Client {
|
||||||
socket = null;
|
socket = null;
|
||||||
interpolator = null;
|
interpolator = null;
|
||||||
|
predictor = null;
|
||||||
async connect(host) {
|
async connect(host) {
|
||||||
this.interpolator = new Worker(
|
this.interpolator = new Worker(
|
||||||
new URL('./interpolator.js', import.meta.url),
|
new URL('./interpolator.js', import.meta.url),
|
||||||
{type: 'module'},
|
{type: 'module'},
|
||||||
);
|
);
|
||||||
this.interpolator.addEventListener('message', (event) => {
|
if (CLIENT_INTERPOLATION) {
|
||||||
this.accept(event.data);
|
this.interpolator = new Worker(
|
||||||
});
|
new URL('./interpolator.js', import.meta.url),
|
||||||
|
{type: 'module'},
|
||||||
|
);
|
||||||
|
this.interpolator.addEventListener('message', (event) => {
|
||||||
|
this.accept(event.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (CLIENT_PREDICTION) {
|
||||||
|
this.predictor = new Worker(
|
||||||
|
new URL('./predictor.js', import.meta.url),
|
||||||
|
{type: 'module'},
|
||||||
|
);
|
||||||
|
this.predictor.addEventListener('message', (event) => {
|
||||||
|
const [flow, packet] = event.data;
|
||||||
|
switch (flow) {
|
||||||
|
case 0: {
|
||||||
|
const packed = encode(packet);
|
||||||
|
this.throughput.$$up += packed.byteLength;
|
||||||
|
this.socket.send(packed);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1: {
|
||||||
|
if (CLIENT_INTERPOLATION) {
|
||||||
|
this.interpolator.postMessage(packet);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.accept(packet);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
const url = new URL(`wss://${host}/ws`)
|
const url = new URL(`wss://${host}/ws`)
|
||||||
this.socket = new WebSocket(url.href);
|
this.socket = new WebSocket(url.href);
|
||||||
this.socket.binaryType = 'arraybuffer';
|
this.socket.binaryType = 'arraybuffer';
|
||||||
this.socket.addEventListener('message', (event) => {
|
this.socket.addEventListener('message', (event) => {
|
||||||
this.interpolator.postMessage(decode(event.data));
|
this.throughput.$$down += event.data.byteLength;
|
||||||
|
const packet = decode(event.data);
|
||||||
|
if (CLIENT_PREDICTION) {
|
||||||
|
this.predictor.postMessage([1, packet]);
|
||||||
|
}
|
||||||
|
else if (CLIENT_INTERPOLATION) {
|
||||||
|
this.interpolator.postMessage(packet);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.accept(packet);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
this.socket.addEventListener('close', () => {
|
this.socket.addEventListener('close', () => {
|
||||||
this.accept({type: 'ConnectionStatus', payload: 'aborted'});
|
this.accept({type: 'ConnectionStatus', payload: 'aborted'});
|
||||||
|
@ -27,11 +71,18 @@ export default class RemoteClient extends Client {
|
||||||
this.accept({type: 'ConnectionStatus', payload: 'connected'});
|
this.accept({type: 'ConnectionStatus', payload: 'connected'});
|
||||||
}
|
}
|
||||||
disconnect() {
|
disconnect() {
|
||||||
this.interpolator.terminate();
|
if (CLIENT_INTERPOLATION) {
|
||||||
|
this.interpolator.terminate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
transmit(packet) {
|
transmit(packet) {
|
||||||
const packed = encode(packet);
|
if (CLIENT_PREDICTION) {
|
||||||
this.throughput.$$up += packed.byteLength;
|
this.predictor.postMessage([0, packet]);
|
||||||
this.socket.send(packed);
|
}
|
||||||
|
else {
|
||||||
|
const packed = encode(packet);
|
||||||
|
this.throughput.$$up += packed.byteLength;
|
||||||
|
this.socket.send(packed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -144,11 +144,22 @@ export default class Component {
|
||||||
}
|
}
|
||||||
Component.ecs.markChange(this.entity, {[Component.constructor.componentName]: values})
|
Component.ecs.markChange(this.entity, {[Component.constructor.componentName]: values})
|
||||||
}
|
}
|
||||||
|
toFullJSON() {
|
||||||
|
const {properties} = concrete;
|
||||||
|
const json = {};
|
||||||
|
for (const key in properties) {
|
||||||
|
json[key] = this[key];
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
toNet(recipient, data) {
|
toNet(recipient, data) {
|
||||||
return data || Component.constructor.filterDefaults(this);
|
if (data) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
return this.toFullJSON();
|
||||||
}
|
}
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return Component.constructor.filterDefaults(this);
|
return this.toFullJSON();
|
||||||
}
|
}
|
||||||
async update(values) {
|
async update(values) {
|
||||||
for (const key in values) {
|
for (const key in values) {
|
||||||
|
|
|
@ -408,6 +408,16 @@ export default class Ecs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
predict(entity, elapsed) {
|
||||||
|
for (const systemName in this.Systems) {
|
||||||
|
const System = this.Systems[systemName];
|
||||||
|
if (!System.predict) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
System.predict(entity, elapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async readJson(uri) {
|
async readJson(uri) {
|
||||||
const key = ['$$json', uri].join(':');
|
const key = ['$$json', uri].join(':');
|
||||||
if (!cache.has(key)) {
|
if (!cache.has(key)) {
|
||||||
|
|
|
@ -3,6 +3,10 @@ import {normalizeVector} from '@/util/math.js';
|
||||||
|
|
||||||
export default class ApplyControlMovement extends System {
|
export default class ApplyControlMovement extends System {
|
||||||
|
|
||||||
|
predict(entity) {
|
||||||
|
this.tickSingle(entity);
|
||||||
|
}
|
||||||
|
|
||||||
static get priority() {
|
static get priority() {
|
||||||
return {
|
return {
|
||||||
before: 'IntegratePhysics',
|
before: 'IntegratePhysics',
|
||||||
|
@ -16,17 +20,25 @@ export default class ApplyControlMovement extends System {
|
||||||
}
|
}
|
||||||
|
|
||||||
tick() {
|
tick() {
|
||||||
for (const {Controlled, Forces, Speed} of this.select('default')) {
|
for (const entity of this.select('default')) {
|
||||||
if (!Controlled.locked) {
|
this.tickSingle(entity);
|
||||||
const movement = normalizeVector({
|
}
|
||||||
x: (Controlled.moveRight - Controlled.moveLeft),
|
}
|
||||||
y: (Controlled.moveDown - Controlled.moveUp),
|
|
||||||
});
|
tickSingle(entity) {
|
||||||
Forces.applyImpulse({
|
const {Controlled, Forces, Speed} = entity;
|
||||||
x: Speed.speed * movement.x,
|
if (!Controlled || !Forces | !Speed) {
|
||||||
y: Speed.speed * movement.y,
|
return;
|
||||||
});
|
}
|
||||||
}
|
if (!Controlled.locked) {
|
||||||
|
const movement = normalizeVector({
|
||||||
|
x: (Controlled.moveRight - Controlled.moveLeft),
|
||||||
|
y: (Controlled.moveDown - Controlled.moveUp),
|
||||||
|
});
|
||||||
|
Forces.applyImpulse({
|
||||||
|
x: Speed.speed * movement.x,
|
||||||
|
y: Speed.speed * movement.y,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,27 +3,39 @@ import {TAU} from '@/util/math.js';
|
||||||
|
|
||||||
export default class ControlDirection extends System {
|
export default class ControlDirection extends System {
|
||||||
|
|
||||||
|
predict(entity) {
|
||||||
|
this.tickSingle(entity);
|
||||||
|
}
|
||||||
|
|
||||||
tick() {
|
tick() {
|
||||||
for (const {Controlled, Direction} of this.ecs.changed(['Controlled'])) {
|
for (const entity of this.ecs.changed(['Controlled'])) {
|
||||||
const {locked, moveUp, moveRight, moveDown, moveLeft} = Controlled;
|
this.tickSingle(entity);
|
||||||
if (locked) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
0 === moveRight
|
|
||||||
&& 0 === moveDown
|
|
||||||
&& 0 === moveLeft
|
|
||||||
&& 0 === moveUp
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Direction.direction = (
|
|
||||||
TAU + Math.atan2(
|
|
||||||
moveDown - moveUp,
|
|
||||||
moveRight - moveLeft,
|
|
||||||
)
|
|
||||||
) % TAU;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tickSingle(entity) {
|
||||||
|
const {Controlled, Direction} = entity;
|
||||||
|
if (!Controlled || !Direction) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {locked, moveUp, moveRight, moveDown, moveLeft} = Controlled;
|
||||||
|
if (locked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
0 === moveRight
|
||||||
|
&& 0 === moveDown
|
||||||
|
&& 0 === moveLeft
|
||||||
|
&& 0 === moveUp
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Direction.direction = (
|
||||||
|
TAU + Math.atan2(
|
||||||
|
moveDown - moveUp,
|
||||||
|
moveRight - moveLeft,
|
||||||
|
)
|
||||||
|
) % TAU;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,10 @@ import {System} from '@/ecs/index.js';
|
||||||
|
|
||||||
export default class FollowCamera extends System {
|
export default class FollowCamera extends System {
|
||||||
|
|
||||||
|
predict(entity, elapsed) {
|
||||||
|
this.tickSingle(entity, elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
static get priority() {
|
static get priority() {
|
||||||
return {
|
return {
|
||||||
after: 'IntegratePhysics',
|
after: 'IntegratePhysics',
|
||||||
|
@ -25,11 +29,15 @@ export default class FollowCamera extends System {
|
||||||
}
|
}
|
||||||
|
|
||||||
tick(elapsed) {
|
tick(elapsed) {
|
||||||
for (const {id} of this.select('default')) {
|
for (const entity of this.select('default')) {
|
||||||
this.updateCamera(elapsed * 3, this.ecs.get(id));
|
this.tickSingle(entity, elapsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tickSingle(entity, elapsed) {
|
||||||
|
this.updateCamera(elapsed * 3, entity);
|
||||||
|
}
|
||||||
|
|
||||||
updateCamera(portion, entity) {
|
updateCamera(portion, entity) {
|
||||||
const {Camera, Position} = entity;
|
const {Camera, Position} = entity;
|
||||||
if (Camera && Position) {
|
if (Camera && Position) {
|
||||||
|
|
|
@ -2,6 +2,10 @@ import {System} from '@/ecs/index.js';
|
||||||
|
|
||||||
export default class IntegratePhysics extends System {
|
export default class IntegratePhysics extends System {
|
||||||
|
|
||||||
|
predict(entity, elapsed) {
|
||||||
|
this.tickSingle(entity, elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
static queries() {
|
static queries() {
|
||||||
return {
|
return {
|
||||||
default: ['Position', 'Forces'],
|
default: ['Position', 'Forces'],
|
||||||
|
@ -9,10 +13,18 @@ export default class IntegratePhysics extends System {
|
||||||
}
|
}
|
||||||
|
|
||||||
tick(elapsed) {
|
tick(elapsed) {
|
||||||
for (const {Position, Forces} of this.select('default')) {
|
for (const entity of this.select('default')) {
|
||||||
Position.x = Position.$$x + elapsed * (Forces.$$impulseX + Forces.$$forceX);
|
this.tickSingle(entity, elapsed);
|
||||||
Position.y = Position.$$y + elapsed * (Forces.$$impulseY + Forces.$$forceY);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tickSingle(entity, elapsed) {
|
||||||
|
const {Forces, Position} = entity;
|
||||||
|
if (!Forces || !Position) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Position.x = Position.$$x + elapsed * (Forces.$$impulseX + Forces.$$forceX);
|
||||||
|
Position.y = Position.$$y + elapsed * (Forces.$$impulseY + Forces.$$forceY);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,10 @@ import {System} from '@/ecs/index.js';
|
||||||
|
|
||||||
export default class ResetForces extends System {
|
export default class ResetForces extends System {
|
||||||
|
|
||||||
|
predict(entity, elapsed) {
|
||||||
|
this.tickSingle(entity, elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
static get priority() {
|
static get priority() {
|
||||||
return {phase: 'post'};
|
return {phase: 'post'};
|
||||||
}
|
}
|
||||||
|
@ -13,24 +17,32 @@ export default class ResetForces extends System {
|
||||||
}
|
}
|
||||||
|
|
||||||
tick(elapsed) {
|
tick(elapsed) {
|
||||||
for (const {Forces} of this.select('default')) {
|
for (const entity of this.select('default')) {
|
||||||
if (0 !== Forces.forceX) {
|
this.tickSingle(entity, elapsed);
|
||||||
const factorX = Math.pow(1 - Forces.dampingX, elapsed);
|
|
||||||
Forces.forceX *= factorX;
|
|
||||||
if (Math.abs(Forces.forceX) <= 1) {
|
|
||||||
Forces.forceX = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (0 !== Forces.forceY) {
|
|
||||||
const factorY = Math.pow(1 - Forces.dampingY, elapsed);
|
|
||||||
Forces.forceY *= factorY;
|
|
||||||
if (Math.abs(Forces.forceY) <= 1) {
|
|
||||||
Forces.forceY = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Forces.impulseX = 0;
|
|
||||||
Forces.impulseY = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tickSingle(entity, elapsed) {
|
||||||
|
const {Forces} = entity;
|
||||||
|
if (!Forces) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (0 !== Forces.forceX) {
|
||||||
|
const factorX = Math.pow(1 - Forces.dampingX, elapsed);
|
||||||
|
Forces.forceX *= factorX;
|
||||||
|
if (Math.abs(Forces.forceX) <= 1) {
|
||||||
|
Forces.forceX = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (0 !== Forces.forceY) {
|
||||||
|
const factorY = Math.pow(1 - Forces.dampingY, elapsed);
|
||||||
|
Forces.forceY *= factorY;
|
||||||
|
if (Math.abs(Forces.forceY) <= 1) {
|
||||||
|
Forces.forceY = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Forces.impulseX = 0;
|
||||||
|
Forces.impulseY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,10 @@ import {System} from '@/ecs/index.js';
|
||||||
|
|
||||||
export default class RunAnimations extends System {
|
export default class RunAnimations extends System {
|
||||||
|
|
||||||
|
predict(entity, elapsed) {
|
||||||
|
this.tickSingle(entity, elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
static queries() {
|
static queries() {
|
||||||
return {
|
return {
|
||||||
default: ['Sprite'],
|
default: ['Sprite'],
|
||||||
|
@ -9,15 +13,23 @@ export default class RunAnimations extends System {
|
||||||
}
|
}
|
||||||
|
|
||||||
tick(elapsed) {
|
tick(elapsed) {
|
||||||
for (const {Sprite} of this.select('default')) {
|
for (const entity of this.select('default')) {
|
||||||
if (0 === Sprite.speed || !Sprite.isAnimating) {
|
this.tickSingle(entity, elapsed);
|
||||||
continue;
|
}
|
||||||
}
|
}
|
||||||
Sprite.elapsed += elapsed / Sprite.speed;
|
|
||||||
while (Sprite.elapsed > 1) {
|
tickSingle(entity, elapsed) {
|
||||||
Sprite.elapsed -= 1;
|
const {Sprite} = entity;
|
||||||
Sprite.frame += 1;
|
if (!Sprite) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
if (0 === Sprite.speed || !Sprite.isAnimating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Sprite.elapsed += elapsed / Sprite.speed;
|
||||||
|
while (Sprite.elapsed >= 1) {
|
||||||
|
Sprite.elapsed -= 1;
|
||||||
|
Sprite.frame += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,10 @@ import {System} from '@/ecs/index.js';
|
||||||
|
|
||||||
export default class SpriteDirection extends System {
|
export default class SpriteDirection extends System {
|
||||||
|
|
||||||
|
predict(entity) {
|
||||||
|
this.tickSingle(entity);
|
||||||
|
}
|
||||||
|
|
||||||
static get priority() {
|
static get priority() {
|
||||||
return {
|
return {
|
||||||
after: 'ControlDirection',
|
after: 'ControlDirection',
|
||||||
|
@ -15,35 +19,43 @@ export default class SpriteDirection extends System {
|
||||||
}
|
}
|
||||||
|
|
||||||
tick() {
|
tick() {
|
||||||
for (const {Controlled, Direction, Sprite} of this.select('default')) {
|
for (const entity of this.select('default')) {
|
||||||
const parts = [];
|
this.tickSingle(entity);
|
||||||
if (Controlled) {
|
}
|
||||||
const {locked, moveUp, moveRight, moveDown, moveLeft} = Controlled;
|
}
|
||||||
if (locked) {
|
|
||||||
continue;
|
tickSingle(entity) {
|
||||||
}
|
const parts = [];
|
||||||
if ((moveUp > 0 || moveRight > 0 || moveDown > 0 || moveLeft > 0)) {
|
const {Controlled, Direction, Sprite} = entity;
|
||||||
parts.push('moving');
|
if (!Sprite) {
|
||||||
}
|
return;
|
||||||
else {
|
}
|
||||||
parts.push('idle');
|
if (Controlled) {
|
||||||
}
|
const {locked, moveUp, moveRight, moveDown, moveLeft} = Controlled;
|
||||||
|
if (locked) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (Direction) {
|
if ((moveUp > 0 || moveRight > 0 || moveDown > 0 || moveLeft > 0)) {
|
||||||
if (!Sprite.rotates) {
|
parts.push('moving');
|
||||||
const name = {
|
|
||||||
0: 'right',
|
|
||||||
1: 'down',
|
|
||||||
2: 'left',
|
|
||||||
3: 'up',
|
|
||||||
};
|
|
||||||
parts.push(name[Direction.quantize(4)]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (parts.length > 0) {
|
else {
|
||||||
if (Sprite.hasAnimation(parts.join(':'))) {
|
parts.push('idle');
|
||||||
Sprite.animation = parts.join(':');
|
}
|
||||||
}
|
}
|
||||||
|
if (Direction) {
|
||||||
|
if (!Sprite.rotates) {
|
||||||
|
const name = {
|
||||||
|
0: 'right',
|
||||||
|
1: 'down',
|
||||||
|
2: 'left',
|
||||||
|
3: 'up',
|
||||||
|
};
|
||||||
|
parts.push(name[Direction.quantize(4)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parts.length > 0) {
|
||||||
|
if (Sprite.hasAnimation(parts.join(':'))) {
|
||||||
|
Sprite.animation = parts.join(':');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -425,7 +425,7 @@ export default class Engine {
|
||||||
this.update(this.updateElapsed);
|
this.update(this.updateElapsed);
|
||||||
this.setClean();
|
this.setClean();
|
||||||
this.frame += 1;
|
this.frame += 1;
|
||||||
this.updateElapsed -= UPS_PER_S;
|
this.updateElapsed = this.updateElapsed % UPS_PER_S;
|
||||||
}
|
}
|
||||||
this.handle = setTimeout(loop, 1000 / TPS);
|
this.handle = setTimeout(loop, 1000 / TPS);
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,6 +2,8 @@ export const CHUNK_SIZE = 32;
|
||||||
|
|
||||||
export const CLIENT_LATENCY = 0;
|
export const CLIENT_LATENCY = 0;
|
||||||
|
|
||||||
|
export const CLIENT_INTERPOLATION = true;
|
||||||
|
|
||||||
export const CLIENT_PREDICTION = true;
|
export const CLIENT_PREDICTION = true;
|
||||||
|
|
||||||
export const IRL_MINUTES_PER_GAME_DAY = 20;
|
export const IRL_MINUTES_PER_GAME_DAY = 20;
|
||||||
|
@ -15,4 +17,4 @@ export const SERVER_LATENCY = 0;
|
||||||
|
|
||||||
export const TPS = 60;
|
export const TPS = 60;
|
||||||
|
|
||||||
export const UPS = 30;
|
export const UPS = 15;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user