Skip to content

Commit 0f05f86

Browse files
Websocket Client-Server Framework Spike (#1198)
Co-authored-by: Pvty <[email protected]>
2 parents 7307511 + d2fc3c2 commit 0f05f86

File tree

6 files changed

+748
-1
lines changed

6 files changed

+748
-1
lines changed

fission/.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,4 @@ dist-ssr
3030
yarn.lock
3131

3232
test-results
33-
src/test/**/__screenshots__
33+
src/test/**/__screenshots__

multiplayer-spike/.gitignore

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
public/Downloadables
2+
package-lock.json
3+
bun.lockb
4+
5+
# Logs
6+
logs
7+
*.log
8+
npm-debug.log*
9+
yarn-debug.log*
10+
yarn-error.log*
11+
pnpm-debug.log*
12+
lerna-debug.log*
13+
14+
node_modules
15+
dist
16+
dist-ssr
17+
*.local
18+
19+
# Editor directories and files
20+
.vscode/*
21+
!.vscode/extensions.json
22+
.idea
23+
.DS_Store
24+
*.suo
25+
*.ntvs*
26+
*.njsproj
27+
*.sln
28+
*.sw?
29+
30+
yarn.lock

multiplayer-spike/client/client.js

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
class MultiplayerRobotClient {
2+
constructor() {
3+
this.ws = null;
4+
this.clientId = null;
5+
this.robotId = null;
6+
this.connected = false;
7+
8+
this.robots = new Map();
9+
this.worldSize = { width: 1000, height: 1000 };
10+
this.inputs = {
11+
forward: false,
12+
backward: false,
13+
left: false,
14+
right: false,
15+
};
16+
17+
this.canvas = document.getElementById("gameCanvas");
18+
this.ctx = this.canvas.getContext("2d");
19+
20+
this.statusEl = document.getElementById("status");
21+
this.playerCountEl = document.getElementById("playerCount");
22+
this.latencyEl = document.getElementById("latency");
23+
24+
this.setupCanvas();
25+
this.setupInputHandlers();
26+
this.connect();
27+
this.startRenderLoop();
28+
}
29+
30+
connect() {
31+
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
32+
const wsUrl = `${protocol}//${location.host}/game`;
33+
34+
console.log(`Connecting to ${wsUrl}...`);
35+
36+
try {
37+
this.ws = new WebSocket(wsUrl);
38+
39+
this.ws.onopen = () => {
40+
this.connected = true;
41+
this.statusEl.textContent = "Connected";
42+
console.log("Connected to game server");
43+
};
44+
45+
this.ws.onmessage = (event) => {
46+
this.handleServerMessage(event.data);
47+
};
48+
49+
this.ws.onclose = () => {
50+
this.connected = false;
51+
this.statusEl.textContent = "Disconnected";
52+
console.log("Disconnected from server");
53+
54+
setTimeout(() => {
55+
if (!this.connected) {
56+
console.log("Attempting to reconnect...");
57+
this.connect();
58+
}
59+
}, 3000);
60+
};
61+
62+
this.ws.onerror = (error) => {
63+
console.error("WebSocket error:", error);
64+
this.statusEl.textContent = "Error";
65+
};
66+
} catch (error) {
67+
console.error("Failed to connect:", error);
68+
this.statusEl.textContent = "Failed";
69+
}
70+
}
71+
72+
handleServerMessage(data) {
73+
try {
74+
const message = JSON.parse(data);
75+
76+
switch (message.type) {
77+
case "init":
78+
this.handleInit(message.data);
79+
break;
80+
case "worldState":
81+
this.handleWorldState(message.data);
82+
break;
83+
case "robotJoined":
84+
this.handleRobotJoined(message.data);
85+
break;
86+
case "robotLeft":
87+
this.handleRobotLeft(message.data);
88+
break;
89+
case "ping":
90+
this.handlePing(message.data);
91+
break;
92+
}
93+
} catch (error) {
94+
console.error("Failed to parse server message:", error);
95+
}
96+
}
97+
98+
handleInit(data) {
99+
this.clientId = data.clientId;
100+
this.robotId = data.robotId;
101+
this.worldSize = data.worldSize;
102+
103+
data.robots.forEach((robot) => {
104+
this.robots.set(robot.id, robot);
105+
});
106+
107+
this.playerCountEl.textContent = data.robots.length;
108+
console.log(`Initialized as robot ${this.robotId.substring(0, 8)}`);
109+
}
110+
111+
handleWorldState(data) {
112+
data.robots.forEach((robotData) => {
113+
if (this.robots.has(robotData.id)) {
114+
const robot = this.robots.get(robotData.id);
115+
robot.position = robotData.position;
116+
robot.velocity = robotData.velocity;
117+
robot.rotation = robotData.rotation;
118+
}
119+
});
120+
}
121+
122+
handleRobotJoined(data) {
123+
this.robots.set(data.id, data);
124+
this.playerCountEl.textContent = this.robots.size;
125+
console.log(`Robot ${data.id.substring(0, 8)} joined`);
126+
}
127+
128+
handleRobotLeft(data) {
129+
this.robots.delete(data.robotId);
130+
this.playerCountEl.textContent = this.robots.size;
131+
console.log(`Robot ${data.robotId.substring(0, 8)} left`);
132+
}
133+
134+
handlePing(data) {
135+
this.send({
136+
type: "pong",
137+
data: { timestamp: data.timestamp },
138+
});
139+
140+
const latency = Date.now() - data.timestamp;
141+
this.latencyEl.textContent = latency;
142+
}
143+
144+
setupInputHandlers() {
145+
const keys = {
146+
KeyW: "forward",
147+
KeyS: "backward",
148+
KeyA: "left",
149+
KeyD: "right",
150+
};
151+
152+
document.addEventListener("keydown", (e) => {
153+
const action = keys[e.code];
154+
if (action && !this.inputs[action]) {
155+
this.inputs[action] = true;
156+
this.sendInputs();
157+
}
158+
});
159+
160+
document.addEventListener("keyup", (e) => {
161+
const action = keys[e.code];
162+
if (action && this.inputs[action]) {
163+
this.inputs[action] = false;
164+
this.sendInputs();
165+
}
166+
});
167+
168+
window.addEventListener("resize", () => {
169+
this.setupCanvas();
170+
});
171+
}
172+
173+
sendInputs() {
174+
this.send({
175+
type: "input",
176+
data: { ...this.inputs },
177+
});
178+
}
179+
180+
setupCanvas() {
181+
this.canvas.width = window.innerWidth;
182+
this.canvas.height = window.innerHeight;
183+
}
184+
185+
startRenderLoop() {
186+
const render = () => {
187+
this.render();
188+
requestAnimationFrame(render);
189+
};
190+
requestAnimationFrame(render);
191+
}
192+
193+
render() {
194+
this.ctx.fillStyle = "#000";
195+
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
196+
197+
const myRobot = this.robots.get(this.robotId);
198+
let offsetX = 0;
199+
let offsetY = 0;
200+
201+
if (myRobot) {
202+
offsetX = this.canvas.width / 2 - myRobot.position.x;
203+
offsetY = this.canvas.height / 2 - myRobot.position.y;
204+
}
205+
206+
this.ctx.strokeStyle = "#333";
207+
this.ctx.lineWidth = 2;
208+
this.ctx.strokeRect(
209+
offsetX,
210+
offsetY,
211+
this.worldSize.width,
212+
this.worldSize.height
213+
);
214+
215+
for (const robot of this.robots.values()) {
216+
this.drawRobot(robot, offsetX, offsetY);
217+
}
218+
}
219+
220+
drawRobot(robot, offsetX, offsetY) {
221+
const isOwnRobot = robot.id === this.robotId;
222+
const x = robot.position.x + offsetX;
223+
const y = robot.position.y + offsetY;
224+
225+
this.ctx.save();
226+
this.ctx.translate(x + 25, y + 25);
227+
this.ctx.rotate((robot.rotation * Math.PI) / 180);
228+
229+
this.ctx.fillStyle = isOwnRobot ? "#ff0000" : "#00ff00";
230+
this.ctx.fillRect(-25, -25, 50, 50);
231+
232+
this.ctx.fillStyle = "#fff";
233+
this.ctx.beginPath();
234+
this.ctx.moveTo(15, 0);
235+
this.ctx.lineTo(-5, -8);
236+
this.ctx.lineTo(-5, 8);
237+
this.ctx.closePath();
238+
this.ctx.fill();
239+
240+
this.ctx.restore();
241+
}
242+
243+
send(message) {
244+
if (this.connected && this.ws.readyState === WebSocket.OPEN) {
245+
this.ws.send(JSON.stringify(message));
246+
}
247+
}
248+
}
249+
250+
document.addEventListener("DOMContentLoaded", () => {
251+
window.gameClient = new MultiplayerRobotClient();
252+
});

multiplayer-spike/client/index.html

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Multiplayer Robot Simulator</title>
7+
<style>
8+
* {
9+
margin: 0;
10+
padding: 0;
11+
box-sizing: border-box;
12+
}
13+
14+
body {
15+
font-family: monospace;
16+
background: #222;
17+
color: white;
18+
overflow: hidden;
19+
height: 100vh;
20+
}
21+
22+
#gameCanvas {
23+
background: #000;
24+
display: block;
25+
}
26+
27+
#info {
28+
position: absolute;
29+
top: 10px;
30+
left: 10px;
31+
background: rgba(0, 0, 0, 0.7);
32+
padding: 10px;
33+
font-size: 12px;
34+
border: 1px solid #333;
35+
}
36+
37+
#controls {
38+
position: absolute;
39+
bottom: 10px;
40+
left: 10px;
41+
background: rgba(0, 0, 0, 0.7);
42+
padding: 10px;
43+
font-size: 12px;
44+
border: 1px solid #333;
45+
}
46+
</style>
47+
</head>
48+
<body>
49+
<canvas id="gameCanvas"></canvas>
50+
51+
<div id="info">
52+
<div>s: <span id="status">Connecting...</span></div>
53+
<div>count: <span id="playerCount">0</span></div>
54+
<div>ping: <span id="latency">-</span>ms</div>
55+
</div>
56+
57+
<script src="client.js"></script>
58+
</body>
59+
</html>

multiplayer-spike/package.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "synthesis-multiplayer-spike",
3+
"version": "1.0.0",
4+
"description": "Real-time multiplayer robot simulator spike using WebSockets",
5+
"type": "module",
6+
"main": "server.js",
7+
"scripts": {
8+
"start": "node server.js",
9+
"dev": "node --watch server.js",
10+
"client": "npx http-server client -p 8080 -o"
11+
},
12+
"dependencies": {
13+
"express": "^4.18.2",
14+
"helmet": "^8.1.0",
15+
"uuid": "^9.0.1",
16+
"ws": "^8.18.0"
17+
},
18+
"devDependencies": {
19+
"@types/node": "^20.10.0",
20+
"@types/uuid": "^9.0.7",
21+
"@types/ws": "^8.5.10"
22+
},
23+
"engines": {
24+
"node": ">=18.0.0"
25+
}
26+
}

0 commit comments

Comments
 (0)