feat: interpolation
This commit is contained in:
parent
5c25fee186
commit
48a45533f5
110
app/client/interpolator.js
Normal file
110
app/client/interpolator.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
export default class Interpolator {
|
||||
duration = 0;
|
||||
latest;
|
||||
location = 0;
|
||||
penultimate;
|
||||
tracking = [];
|
||||
accept(state) {
|
||||
const packet = state;
|
||||
if ('Tick' !== packet.type) {
|
||||
return;
|
||||
}
|
||||
this.penultimate = this.latest;
|
||||
this.latest = packet;
|
||||
this.tracking = [];
|
||||
if (this.penultimate) {
|
||||
this.duration = this.penultimate.payload.elapsed;
|
||||
const [from, to] = [this.penultimate.payload.ecs, this.latest.payload.ecs];
|
||||
for (const entityId in from) {
|
||||
for (const componentName in from[entityId]) {
|
||||
if (
|
||||
['Camera', 'Position'].includes(componentName)
|
||||
&& to[entityId]?.[componentName]
|
||||
) {
|
||||
this.tracking.push({
|
||||
entityId,
|
||||
componentName,
|
||||
properties: ['x', 'y'],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.location = 0;
|
||||
}
|
||||
interpolate(elapsed) {
|
||||
if (0 === this.tracking.length) {
|
||||
return undefined;
|
||||
}
|
||||
this.location += elapsed;
|
||||
const fraction = this.location / this.duration;
|
||||
const [from, to] = [this.penultimate.payload.ecs, this.latest.payload.ecs];
|
||||
const interpolated = {};
|
||||
for (const {entityId, componentName, properties} of this.tracking) {
|
||||
if (!interpolated[entityId]) {
|
||||
interpolated[entityId] = {};
|
||||
}
|
||||
if (!interpolated[entityId][componentName]) {
|
||||
interpolated[entityId][componentName] = {};
|
||||
}
|
||||
for (const property of properties) {
|
||||
if (
|
||||
!(property in from[entityId][componentName])
|
||||
|| !(property in to[entityId][componentName])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
interpolated[entityId][componentName][property] = (
|
||||
from[entityId][componentName][property]
|
||||
+ (
|
||||
fraction
|
||||
* (to[entityId][componentName][property] - from[entityId][componentName][property])
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'Tick',
|
||||
payload: {
|
||||
ecs: interpolated,
|
||||
elapsed,
|
||||
frame: this.penultimate.payload.frame + fraction,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let handle;
|
||||
const interpolator = new Interpolator();
|
||||
let last;
|
||||
|
||||
const interpolate = (now) => {
|
||||
const elapsed = (now - last) / 1000;
|
||||
last = now;
|
||||
const interpolated = interpolator.interpolate(elapsed);
|
||||
if (interpolated) {
|
||||
handle = requestAnimationFrame(interpolate);
|
||||
postMessage(interpolated);
|
||||
}
|
||||
else {
|
||||
handle = null;
|
||||
}
|
||||
}
|
||||
|
||||
onmessage = async (event) => {
|
||||
interpolator.accept(event.data);
|
||||
if (interpolator.penultimate) {
|
||||
postMessage({
|
||||
type: 'Tick',
|
||||
payload: {
|
||||
ecs: interpolator.penultimate.payload.ecs,
|
||||
elapsed: last ? (performance.now() - last) / 1000 : 0,
|
||||
frame: interpolator.penultimate.payload.frame,
|
||||
},
|
||||
});
|
||||
if (!handle) {
|
||||
last = performance.now();
|
||||
handle = requestAnimationFrame(interpolate);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,26 +1,38 @@
|
|||
import Client from '@/net/client.js';
|
||||
import {decode, encode} from '@/net/packets/index.js';
|
||||
|
||||
export default class LocalClient extends Client {
|
||||
server = null;
|
||||
interpolator = null;
|
||||
async connect() {
|
||||
this.worker = new Worker(
|
||||
this.server = new Worker(
|
||||
new URL('../server/worker.js', import.meta.url),
|
||||
{type: 'module'},
|
||||
);
|
||||
this.worker.addEventListener('message', (event) => {
|
||||
this.interpolator = new Worker(
|
||||
new URL('./interpolator.js', import.meta.url),
|
||||
{type: 'module'},
|
||||
);
|
||||
this.interpolator.addEventListener('message', (event) => {
|
||||
this.accept(event.data);
|
||||
});
|
||||
this.server.addEventListener('message', (event) => {
|
||||
if (0 === event.data) {
|
||||
this.worker.terminate();
|
||||
this.worker = undefined;
|
||||
this.server.terminate();
|
||||
this.server = null;
|
||||
return;
|
||||
}
|
||||
this.throughput.$$down += event.data.byteLength;
|
||||
this.accept(event.data);
|
||||
this.interpolator.postMessage(decode(event.data));
|
||||
});
|
||||
}
|
||||
disconnect() {
|
||||
this.worker.postMessage(0);
|
||||
this.server.postMessage(0);
|
||||
this.interpolator.terminate();
|
||||
}
|
||||
transmit(packed) {
|
||||
transmit(packet) {
|
||||
const packed = encode(packet);
|
||||
this.throughput.$$up += packed.byteLength;
|
||||
this.worker.postMessage(packed);
|
||||
this.server.postMessage(packed);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
import {encode} from '@/net/packets/index.js';
|
||||
import {withResolvers} from '@/util/promise.js';
|
||||
|
||||
let connected = false;
|
||||
let socket;
|
||||
|
||||
const {promise, resolve} = withResolvers();
|
||||
|
||||
onmessage = async (event) => {
|
||||
if (!connected) {
|
||||
const url = new URL(`wss://${event.data.host}/ws`)
|
||||
socket = new WebSocket(url.href);
|
||||
socket.binaryType = 'arraybuffer';
|
||||
socket.addEventListener('open', resolve);
|
||||
socket.addEventListener('error', () => {
|
||||
postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'}));
|
||||
close();
|
||||
});
|
||||
await promise;
|
||||
socket.removeEventListener('open', resolve);
|
||||
socket.addEventListener('message', (event) => {
|
||||
postMessage(event.data);
|
||||
});
|
||||
socket.addEventListener('close', () => {
|
||||
postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'}));
|
||||
close();
|
||||
});
|
||||
postMessage(encode({type: 'ConnectionStatus', payload: 'connected'}));
|
||||
connected = true;
|
||||
return;
|
||||
}
|
||||
await promise;
|
||||
socket.send(event.data);
|
||||
};
|
|
@ -1,67 +1,37 @@
|
|||
import Client from '@/net/client.js';
|
||||
import {encode} from '@/net/packets/index.js';
|
||||
import {CLIENT_PREDICTION} from '@/util/constants.js';
|
||||
import {withResolvers} from '@/util/promise.js';
|
||||
import {decode, encode} from '@/net/packets/index.js';
|
||||
|
||||
export default class RemoteClient extends Client {
|
||||
constructor() {
|
||||
super();
|
||||
if (CLIENT_PREDICTION) {
|
||||
this.worker = undefined;
|
||||
}
|
||||
else {
|
||||
this.socket = undefined;
|
||||
}
|
||||
}
|
||||
socket = null;
|
||||
interpolator = null;
|
||||
async connect(host) {
|
||||
if (CLIENT_PREDICTION) {
|
||||
this.worker = new Worker(
|
||||
new URL('./prediction.js', import.meta.url),
|
||||
{type: 'module'},
|
||||
);
|
||||
this.worker.postMessage({host});
|
||||
this.worker.onmessage = (event) => {
|
||||
this.throughput.$$down += event.data.byteLength;
|
||||
this.accept(event.data);
|
||||
};
|
||||
}
|
||||
else {
|
||||
const url = new URL(`wss://${host}/ws`)
|
||||
this.socket = new WebSocket(url.href);
|
||||
this.socket.binaryType = 'arraybuffer';
|
||||
const onMessage = (event) => {
|
||||
this.throughput.$$down += event.data.byteLength;
|
||||
this.accept(event.data);
|
||||
}
|
||||
const {promise, resolve} = withResolvers();
|
||||
this.socket.addEventListener('open', resolve);
|
||||
this.socket.addEventListener('error', () => {
|
||||
this.accept(encode({type: 'ConnectionStatus', payload: 'aborted'}));
|
||||
});
|
||||
await promise;
|
||||
this.socket.removeEventListener('open', resolve);
|
||||
this.socket.addEventListener('message', onMessage);
|
||||
this.socket.addEventListener('close', () => {
|
||||
this.accept(encode({type: 'ConnectionStatus', payload: 'aborted'}));
|
||||
});
|
||||
this.accept(encode({type: 'ConnectionStatus', payload: 'connected'}));
|
||||
}
|
||||
this.interpolator = new Worker(
|
||||
new URL('./interpolator.js', import.meta.url),
|
||||
{type: 'module'},
|
||||
);
|
||||
this.interpolator.addEventListener('message', (event) => {
|
||||
this.accept(event.data);
|
||||
});
|
||||
const url = new URL(`wss://${host}/ws`)
|
||||
this.socket = new WebSocket(url.href);
|
||||
this.socket.binaryType = 'arraybuffer';
|
||||
this.socket.addEventListener('message', (event) => {
|
||||
this.interpolator.postMessage(decode(event.data));
|
||||
});
|
||||
this.socket.addEventListener('close', () => {
|
||||
this.accept({type: 'ConnectionStatus', payload: 'aborted'});
|
||||
});
|
||||
this.socket.addEventListener('error', () => {
|
||||
this.accept({type: 'ConnectionStatus', payload: 'aborted'});
|
||||
});
|
||||
this.accept({type: 'ConnectionStatus', payload: 'connected'});
|
||||
}
|
||||
disconnect() {
|
||||
if (CLIENT_PREDICTION) {
|
||||
this.worker.terminate();
|
||||
}
|
||||
else {
|
||||
this.socket.close();
|
||||
}
|
||||
this.interpolator.terminate();
|
||||
}
|
||||
transmit(packed) {
|
||||
transmit(packet) {
|
||||
const packed = encode(packet);
|
||||
this.throughput.$$up += packed.byteLength;
|
||||
if (CLIENT_PREDICTION) {
|
||||
this.worker.postMessage(packed);
|
||||
}
|
||||
else {
|
||||
this.socket.send(packed);
|
||||
}
|
||||
this.socket.send(packed);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import {useEffect, useState} from 'react';
|
||||
import {Outlet, useParams} from 'react-router-dom';
|
||||
|
||||
import {decode, encode} from '@/net/packets/index.js';
|
||||
|
||||
import styles from './play.module.css';
|
||||
|
||||
export default function Play() {
|
||||
|
@ -20,15 +18,7 @@ export default function Play() {
|
|||
({default: Client} = await import('@/client/remote.js'));
|
||||
break;
|
||||
}
|
||||
class SilphiusClient extends Client {
|
||||
accept(packed) {
|
||||
super.accept(decode(packed));
|
||||
}
|
||||
transmit(packet) {
|
||||
super.transmit(encode(packet));
|
||||
}
|
||||
}
|
||||
setClient(() => SilphiusClient);
|
||||
setClient(() => Client);
|
||||
}
|
||||
loadClient();
|
||||
}, [type]);
|
||||
|
|
|
@ -37,7 +37,7 @@ export default async function createPlayer(id) {
|
|||
Magnet: {strength: 24},
|
||||
Player: {},
|
||||
Position: {x: 128, y: 448},
|
||||
Speed: {speed: 300},
|
||||
Speed: {speed: 100},
|
||||
Sound: {},
|
||||
Sprite: {
|
||||
anchorX: 0.5,
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
CHUNK_SIZE,
|
||||
RESOLUTION,
|
||||
TPS,
|
||||
UPS,
|
||||
} from '@/util/constants.js';
|
||||
import {withResolvers} from '@/util/promise.js';
|
||||
|
||||
|
@ -16,6 +17,8 @@ import createHouse from './create/house.js';
|
|||
import createPlayer from './create/player.js';
|
||||
import createTown from './create/town.js';
|
||||
|
||||
const UPS_PER_S = 1 / UPS;
|
||||
|
||||
const cache = new LRUCache({
|
||||
max: 128,
|
||||
});
|
||||
|
@ -31,6 +34,7 @@ export default class Engine {
|
|||
incomingActions = new Map();
|
||||
last;
|
||||
server;
|
||||
updateElapsed = 0;
|
||||
|
||||
constructor(Server) {
|
||||
this.ecses = {};
|
||||
|
@ -402,12 +406,16 @@ export default class Engine {
|
|||
const loop = async () => {
|
||||
const now = performance.now() / 1000;
|
||||
const elapsed = now - this.last;
|
||||
this.updateElapsed += elapsed;
|
||||
this.last = now;
|
||||
this.acceptActions();
|
||||
this.tick(elapsed);
|
||||
this.update(elapsed);
|
||||
this.setClean();
|
||||
this.frame += 1;
|
||||
if (this.updateElapsed >= UPS_PER_S) {
|
||||
this.update(this.updateElapsed);
|
||||
this.setClean();
|
||||
this.frame += 1;
|
||||
this.updateElapsed -= UPS_PER_S;
|
||||
}
|
||||
this.handle = setTimeout(loop, 1000 / TPS);
|
||||
};
|
||||
loop();
|
||||
|
|
|
@ -13,4 +13,6 @@ export const RESOLUTION = {
|
|||
|
||||
export const SERVER_LATENCY = 0;
|
||||
|
||||
export const TPS = 60;
|
||||
export const TPS = 30;
|
||||
|
||||
export const UPS = 5;
|
||||
|
|
Loading…
Reference in New Issue
Block a user