flow: net + ecs
This commit is contained in:
parent
3dc7cf8a5b
commit
007ea66de1
9
package-lock.json
generated
9
package-lock.json
generated
|
@ -8,6 +8,7 @@
|
|||
"name": "silphius",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"@pixi/react": "^7.1.2",
|
||||
"pixi.js": "^8.1.5",
|
||||
"react": "^18.3.1",
|
||||
|
@ -2634,6 +2635,14 @@
|
|||
"react": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@msgpack/msgpack": {
|
||||
"version": "3.0.0-beta2",
|
||||
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.0.0-beta2.tgz",
|
||||
"integrity": "sha512-y+l1PNV0XDyY8sM3YtuMLK5vE3/hkfId+Do8pLo/OPxfxuFAUwcGz3oiiUuV46/aBpwTzZ+mRWVMtlSKbradhw==",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@ndelangen/get-tarball": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@ndelangen/get-tarball/-/get-tarball-3.0.9.tgz",
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
"vitest": "^1.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"@pixi/react": "^7.1.2",
|
||||
"pixi.js": "^8.1.5",
|
||||
"react": "^18.3.1",
|
||||
|
|
|
@ -9,6 +9,11 @@ import ClientContext from '../context/client';
|
|||
import Entities from './entities';
|
||||
import styles from './pixi.module.css';
|
||||
|
||||
import {Ecs} from '../ecs/index.js';
|
||||
import * as Components from '../ecs/components.js';
|
||||
|
||||
const ecs = new Ecs(Components);
|
||||
|
||||
export default function Pixi() {
|
||||
const client = useContext(ClientContext);
|
||||
const [entities, setEntities] = useState([]);
|
||||
|
@ -21,7 +26,18 @@ export default function Pixi() {
|
|||
break;
|
||||
}
|
||||
case 'tick': {
|
||||
setEntities(payload.entities);
|
||||
const {buffer, byteLength, byteOffset} = payload.entities;
|
||||
const view = new DataView(buffer, byteOffset, byteLength);
|
||||
ecs.decode(view);
|
||||
const entities = [];
|
||||
for (const entity of ecs.entities) {
|
||||
const {Position, Visible} = ecs.get(entity);
|
||||
entities.push({
|
||||
image: Visible.image,
|
||||
position: [Position.x, Position.y],
|
||||
})
|
||||
}
|
||||
setEntities(entities);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
|
|
@ -15,10 +15,10 @@ export default function Silphius() {
|
|||
let Client;
|
||||
switch (connectionTuple[0]) {
|
||||
case 'local':
|
||||
({default: Client} = await import('../client/local.js'));
|
||||
({default: Client} = await import('../net/client/local.js'));
|
||||
break;
|
||||
case 'remote':
|
||||
({default: Client} = await import('../client/remote.js'));
|
||||
({default: Client} = await import('../net/client/remote.js'));
|
||||
break;
|
||||
}
|
||||
const client = new Client();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {useContext, useEffect} from 'react';
|
||||
|
||||
import addKeyListener from '../add-key-listener.js';
|
||||
import {RESOLUTION} from '../constants';
|
||||
import {ACTION_MAP, RESOLUTION} from '../constants';
|
||||
import ClientContext from '../context/client';
|
||||
import Dom from './dom';
|
||||
import Pixi from './pixi';
|
||||
|
@ -9,13 +9,6 @@ import styles from './ui.module.css';
|
|||
|
||||
const ratio = RESOLUTION[0] / RESOLUTION[1];
|
||||
|
||||
// Handle input.
|
||||
const ACTION_MAP = {
|
||||
w: 'moveUp',
|
||||
d: 'moveRight',
|
||||
s: 'moveDown',
|
||||
a: 'moveLeft',
|
||||
};
|
||||
const KEY_MAP = {
|
||||
keyDown: 1,
|
||||
keyUp: 0,
|
||||
|
|
|
@ -2,3 +2,17 @@ export const RESOLUTION = [
|
|||
800,
|
||||
450,
|
||||
];
|
||||
|
||||
export const ACTION_MAP = {
|
||||
w: 'moveUp',
|
||||
d: 'moveRight',
|
||||
s: 'moveDown',
|
||||
a: 'moveLeft',
|
||||
};
|
||||
|
||||
export const MOVE_MAP = {
|
||||
'moveUp': 'up',
|
||||
'moveRight': 'right',
|
||||
'moveDown': 'down',
|
||||
'moveLeft': 'left',
|
||||
};
|
||||
|
|
17
src/ecs/components.js
Normal file
17
src/ecs/components.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
export const Controlled = {
|
||||
up: 'float32',
|
||||
right: 'float32',
|
||||
down: 'float32',
|
||||
left: 'float32',
|
||||
};
|
||||
|
||||
export const Position = {
|
||||
x: 'float32',
|
||||
y: 'float32',
|
||||
};
|
||||
|
||||
export const Visible = {
|
||||
image: 'string',
|
||||
};
|
||||
|
||||
export const Wandering = {};
|
|
@ -3,6 +3,8 @@ import {createRoot} from 'react-dom/client';
|
|||
|
||||
import Silphius from './components/silphius.jsx';
|
||||
|
||||
await import('./isomorphinit.js');
|
||||
|
||||
// Setup DOM.
|
||||
createRoot(document.querySelector('.silphius'))
|
||||
.render(createElement(Silphius));
|
||||
|
|
7
src/isomorphinit.js
Normal file
7
src/isomorphinit.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
// Gathering.
|
||||
const {default: PacketClass} = await import('./net/packet/packet.js');
|
||||
Object.values(await import('./net/packet/packets.js'))
|
||||
.forEach((Packet) => {
|
||||
PacketClass.register(Packet);
|
||||
});
|
||||
PacketClass.mapRegistered();
|
|
@ -1,10 +1,13 @@
|
|||
import Packet from "../packet/packet";
|
||||
|
||||
export default class Client {
|
||||
constructor() {
|
||||
this.listeners = [];
|
||||
}
|
||||
accept(data) {
|
||||
accept(packed) {
|
||||
const decoded = Packet.accept(packed);
|
||||
for (const i in this.listeners) {
|
||||
this.listeners[i](data);
|
||||
this.listeners[i](decoded);
|
||||
}
|
||||
}
|
||||
addMessageListener(listener) {
|
||||
|
@ -16,4 +19,7 @@ export default class Client {
|
|||
this.listeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
send(packet) {
|
||||
this.transmit(Packet.transmit(packet));
|
||||
}
|
||||
}
|
|
@ -2,12 +2,12 @@ import Client from './client.js';
|
|||
|
||||
export default class LocalClient extends Client {
|
||||
async connect() {
|
||||
this.worker = new Worker('../server/worker.js', {type: 'module'});
|
||||
this.worker = new Worker('/net/server/worker.js', {type: 'module'});
|
||||
this.worker.onmessage = (event) => {
|
||||
this.accept(event.data);
|
||||
};
|
||||
}
|
||||
send(message) {
|
||||
this.worker.postMessage(message);
|
||||
transmit(packed) {
|
||||
this.worker.postMessage(packed);
|
||||
}
|
||||
}
|
|
@ -3,15 +3,15 @@ import Client from './client.js';
|
|||
export default class RemoteClient extends Client {
|
||||
async connect() {
|
||||
this.socket = new WebSocket(`ws://${window.location.host}/ws`);
|
||||
this.socket.binaryType = 'arraybuffer';
|
||||
this.socket.onmessage = (event) => {
|
||||
this.accept(JSON.parse(event.data));
|
||||
this.accept(event.data);
|
||||
};
|
||||
await new Promise((resolve) => {
|
||||
this.socket.onopen = resolve;
|
||||
});
|
||||
}
|
||||
send(message) {
|
||||
this.socket.send(JSON.stringify(message));
|
||||
transmit(packed) {
|
||||
this.socket.send(packed);
|
||||
}
|
||||
}
|
||||
|
33
src/net/packet/action.js
Normal file
33
src/net/packet/action.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
import Packet from "./packet.js";
|
||||
|
||||
const WIRE_MAP = {
|
||||
'moveUp': 0,
|
||||
'moveRight': 1,
|
||||
'moveDown': 2,
|
||||
'moveLeft': 3,
|
||||
};
|
||||
Object.entries(WIRE_MAP)
|
||||
.forEach(([k, v]) => {
|
||||
WIRE_MAP[v] = k;
|
||||
});
|
||||
|
||||
export default class Action extends Packet {
|
||||
|
||||
static type = 'action';
|
||||
|
||||
static pack(payload) {
|
||||
return super.pack({
|
||||
type: WIRE_MAP[payload.type],
|
||||
value: payload.value,
|
||||
});
|
||||
}
|
||||
|
||||
static unpack(packed) {
|
||||
const unpacked = super.unpack(packed);
|
||||
return {
|
||||
type: WIRE_MAP[unpacked.type],
|
||||
value: unpacked.value,
|
||||
};
|
||||
}
|
||||
|
||||
};
|
7
src/net/packet/connect.js
Normal file
7
src/net/packet/connect.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import Packet from "./packet.js";
|
||||
|
||||
export default class Connect extends Packet {
|
||||
|
||||
static type = 'connect';
|
||||
|
||||
};
|
7
src/net/packet/connected.js
Normal file
7
src/net/packet/connected.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import Packet from "./packet.js";
|
||||
|
||||
export default class Connected extends Packet {
|
||||
|
||||
static type = 'connected';
|
||||
|
||||
};
|
68
src/net/packet/packet.js
Normal file
68
src/net/packet/packet.js
Normal file
|
@ -0,0 +1,68 @@
|
|||
import {encode, decode} from '@msgpack/msgpack';
|
||||
|
||||
export default class Packet {
|
||||
|
||||
static registered = {};
|
||||
|
||||
constructor(payload = {}) {
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
static accept(packed) {
|
||||
const decoded = decode(packed);
|
||||
const Packet = this.registered[decoded.id];
|
||||
return {
|
||||
type: Packet.type,
|
||||
payload: Packet.unpack(decoded),
|
||||
};
|
||||
}
|
||||
|
||||
static decode(buffer) {
|
||||
try {
|
||||
return this.unpack(decode(buffer));
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error(`decoding ${this.type}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
static encode(payload) {
|
||||
try {
|
||||
return encode(this.pack(payload));
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error(`encoding ${this.type}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
static mapRegistered() {
|
||||
this.registered = Object.fromEntries([
|
||||
...Object.entries(this.registered),
|
||||
...Object.entries(this.registered)
|
||||
.map(([type, Packet], id) => {
|
||||
Packet.id = id;
|
||||
return [id, Packet];
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
static pack(payload) {
|
||||
return {
|
||||
id: this.id,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
static register(Packet) {
|
||||
this.registered[Packet.type] = Packet;
|
||||
}
|
||||
|
||||
static transmit({type, payload}) {
|
||||
return this.registered[type].encode(payload);
|
||||
}
|
||||
|
||||
static unpack({payload}) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
}
|
4
src/net/packet/packets.js
Normal file
4
src/net/packet/packets.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
export {default as Action} from './action.js';
|
||||
export {default as Connect} from './connect.js';
|
||||
export {default as Connected} from './connected.js';
|
||||
export {default as Tick} from './tick.js';
|
7
src/net/packet/tick.js
Normal file
7
src/net/packet/tick.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import Packet from "./packet.js";
|
||||
|
||||
export default class Tick extends Packet {
|
||||
|
||||
static type = 'tick';
|
||||
|
||||
};
|
11
src/net/server/cell.js
Normal file
11
src/net/server/cell.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import {Ecs} from '../../ecs/index.js';
|
||||
import * as Components from '../../ecs/components.js';
|
||||
|
||||
export default class Cell {
|
||||
constructor() {
|
||||
this.ecs = new Ecs(Components);
|
||||
}
|
||||
tick(elapsed) {
|
||||
this.ecs.tick(elapsed);
|
||||
}
|
||||
}
|
119
src/net/server/server.js
Normal file
119
src/net/server/server.js
Normal file
|
@ -0,0 +1,119 @@
|
|||
import {MOVE_MAP} from '../../constants.js';
|
||||
import Cell from './cell.js';
|
||||
import {Ecs, System} from '../../ecs/index.js';
|
||||
import * as Components from '../../ecs/components.js';
|
||||
import Packet from "../packet/packet.js";
|
||||
|
||||
const SPEED = 100;
|
||||
const TPS = 60;
|
||||
|
||||
await import ('../../isomorphinit.js');
|
||||
|
||||
class MovementSystem extends System {
|
||||
|
||||
static queries() {
|
||||
return {
|
||||
default: ['Position', 'Controlled'],
|
||||
};
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const [position, controlled] of this.select('default')) {
|
||||
position.x += SPEED * elapsed * (controlled.right - controlled.left);
|
||||
position.y += SPEED * elapsed * (controlled.down - controlled.up);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default class Server {
|
||||
|
||||
constructor() {
|
||||
this.cells = [new Cell()];
|
||||
this.cells[0].ecs.addSystem(MovementSystem);
|
||||
this.connections = [];
|
||||
this.connectedPlayers = new Map();
|
||||
this.frame = 0;
|
||||
this.last = Date.now();
|
||||
}
|
||||
|
||||
accept(connection, packed) {
|
||||
const decoded = Packet.accept(packed);
|
||||
const {payload, type} = decoded;
|
||||
switch (type) {
|
||||
case 'connect': {
|
||||
this.send(
|
||||
connection,
|
||||
{
|
||||
type: 'connected',
|
||||
payload: [],
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'action': {
|
||||
const {ecs} = this.cells[0];
|
||||
const connectedPlayer = this.connectedPlayers.get(connection);
|
||||
if (payload.type in MOVE_MAP) {
|
||||
ecs.get(connectedPlayer).Controlled[MOVE_MAP[payload.type]] = payload.value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
connectPlayer(connection) {
|
||||
this.connections.push(connection);
|
||||
const {ecs} = this.cells[0];
|
||||
const entity = ecs.create({
|
||||
Controlled: {up: 0, right: 0, down: 0, left: 0},
|
||||
Position: {x: 50, y: 50},
|
||||
Visible: {image: './assets/bunny.png'},
|
||||
})
|
||||
this.connectedPlayers.set(connection, entity);
|
||||
}
|
||||
|
||||
disconnectPlayer(connection) {
|
||||
const entity = this.connectedPlayers.get(connection);
|
||||
this.connectedPlayers.delete(connection);
|
||||
this.connections.splice(this.connections.indexOf(connection), 1);
|
||||
}
|
||||
|
||||
async load() {
|
||||
}
|
||||
|
||||
send(connection, packet) {
|
||||
this.transmit(connection, Packet.transmit(packet));
|
||||
}
|
||||
|
||||
start() {
|
||||
return setInterval(() => {
|
||||
const elapsed = (Date.now() - this.last) / 1000;
|
||||
this.last = Date.now();
|
||||
this.tick(elapsed);
|
||||
}, 1000 / TPS);
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
this.cells[0].ecs.tick(elapsed);
|
||||
const {ecs} = this.cells[0];
|
||||
const view = new DataView(new ArrayBuffer(ecs.sizeOf(ecs.entities)));
|
||||
ecs.encode(ecs.entities, view);
|
||||
for (const connection of this.connections) {
|
||||
this.send(
|
||||
connection,
|
||||
{
|
||||
type: 'tick',
|
||||
payload: {
|
||||
entities: view,
|
||||
elapsed,
|
||||
frame: this.frame,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
this.frame += 1;
|
||||
}
|
||||
|
||||
}
|
|
@ -9,7 +9,7 @@ const {
|
|||
const wss = new WebSocketServer({port: SILPHIUS_WEBSOCKET_PORT});
|
||||
|
||||
const server = new class SocketServer extends Server {
|
||||
send(ws, data) { ws.send(JSON.stringify(data)); }
|
||||
transmit(ws, packed) { ws.send(packed); }
|
||||
}
|
||||
|
||||
await server.load();
|
||||
|
@ -20,7 +20,7 @@ wss.on('connection', function connection(ws) {
|
|||
ws.on('close', () => {
|
||||
server.disconnectPlayer(ws);
|
||||
})
|
||||
ws.on('message', (data) => {
|
||||
server.accept(ws, JSON.parse(data));
|
||||
ws.on('message', (packed) => {
|
||||
server.accept(ws, packed);
|
||||
});
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
import Server from './server.js';
|
||||
|
||||
const server = new class WorkerServer extends Server {
|
||||
send(connection, data) { postMessage(data); }
|
||||
transmit(connection, packed) { postMessage(packed); }
|
||||
}
|
||||
|
||||
await server.load();
|
||||
|
@ -9,4 +9,4 @@ server.start();
|
|||
|
||||
server.connectPlayer(undefined);
|
||||
|
||||
onmessage = ({data}) => { server.accept(undefined, data); };
|
||||
onmessage = (event) => { server.accept(undefined, event.data); };
|
|
@ -1,89 +0,0 @@
|
|||
const SPEED = 100;
|
||||
const TPS = 20;
|
||||
|
||||
const MOVE_MAP = {
|
||||
'moveUp': 0,
|
||||
'moveRight': 1,
|
||||
'moveDown': 2,
|
||||
'moveLeft': 3,
|
||||
};
|
||||
|
||||
export default class Server {
|
||||
|
||||
constructor() {
|
||||
this.connections = [];
|
||||
this.connectedPlayers = new Map();
|
||||
this.entities = [];
|
||||
this.frame = 0;
|
||||
this.last = Date.now();
|
||||
}
|
||||
|
||||
accept(connection, {type, payload}) {
|
||||
switch (type) {
|
||||
case 'connect': {
|
||||
this.send(connection, {type: 'connected', payload: Array.from(this.connectedPlayers.values())});
|
||||
break;
|
||||
}
|
||||
case 'action': {
|
||||
const connectedPlayer = this.connectedPlayers.get(connection);
|
||||
if (payload.type in MOVE_MAP) {
|
||||
connectedPlayer.movement[MOVE_MAP[payload.type]] = payload.value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
connectPlayer(connection) {
|
||||
this.connections.push(connection);
|
||||
const entity = {
|
||||
image: './assets/bunny.png',
|
||||
movement: [0, 0, 0, 0],
|
||||
position: [50, 50],
|
||||
};
|
||||
this.entities.push(entity);
|
||||
this.connectedPlayers.set(connection, entity);
|
||||
}
|
||||
|
||||
disconnectPlayer(connection) {
|
||||
const entity = this.connectedPlayers.get(connection);
|
||||
this.connectedPlayers.delete(connection);
|
||||
this.connections.splice(this.connections.indexOf(connection), 1);
|
||||
this.entities.splice(this.entities.indexOf(entity), 1);
|
||||
}
|
||||
|
||||
async load() {
|
||||
}
|
||||
|
||||
start() {
|
||||
return setInterval(() => {
|
||||
const elapsed = (Date.now() - this.last) / 1000;
|
||||
this.last = Date.now();
|
||||
this.tick(elapsed);
|
||||
}, 1000 / TPS);
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (const connection of this.connections) {
|
||||
const {movement, position} = this.connectedPlayers.get(connection);
|
||||
position[0] += SPEED * elapsed * (movement[1] - movement[3]);
|
||||
position[1] += SPEED * elapsed * (movement[2] - movement[0]);
|
||||
}
|
||||
for (const connection of this.connections) {
|
||||
this.send(
|
||||
connection,
|
||||
{
|
||||
type: 'tick',
|
||||
payload: {
|
||||
entities: this.entities,
|
||||
elapsed,
|
||||
frame: this.frame,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
this.frame += 1;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user