Skip to content

Instantly share code, notes, and snippets.

@EncodeTheCode
Created June 29, 2026 16:14
Show Gist options
  • Select an option

  • Save EncodeTheCode/a5816adca54377c4bff404ad0783cc34 to your computer and use it in GitHub Desktop.

Select an option

Save EncodeTheCode/a5816adca54377c4bff404ad0783cc34 to your computer and use it in GitHub Desktop.
class NetClient {
constructor(serverUrl) {
this.ws = null;
this.serverUrl = serverUrl;
this.connected = false;
this.authenticated = false;
this.seq = 0;
}
connect() {
this.ws = new WebSocket(this.serverUrl);
this.ws.onopen = () => {
this.connected = true;
console.log("Connected to server");
};
this.ws.onmessage = (e) => {
this.onMessage(JSON.parse(e.data));
};
this.ws.onclose = () => {
this.connected = false;
console.log("Disconnected");
};
}
send(data) {
if (!this.connected) return;
this.ws.send(JSON.stringify(data));
}
onMessage(msg) {
if (msg.type === "auth_ok") {
this.authenticated = true;
console.log("Authenticated:", msg.user_id);
}
}
}
// =======================================================
// 🧠 INPUT SYSTEM (KEYBOARD STATE)
// =======================================================
class InputController {
constructor() {
this.keys = {};
window.addEventListener("keydown", (e) => this.keys[e.key] = true);
window.addEventListener("keyup", (e) => this.keys[e.key] = false);
}
getMovementVector() {
let dx = 0, dy = 0, dz = 0;
if (this.keys["w"]) dz -= 1;
if (this.keys["s"]) dz += 1;
if (this.keys["a"]) dx -= 1;
if (this.keys["d"]) dx += 1;
return { dx, dy, dz };
}
isRunning() {
return !!this.keys["Shift"];
}
isWalking() {
return !!this.keys[" "];
}
}
// =======================================================
// 🧍 PLAYER (LOCAL GAME ENTITY)
// =======================================================
class Player {
constructor() {
this.user_id = null;
this.x = 0;
this.y = 0;
this.z = 0;
this.px = 0;
this.py = 0;
this.pz = 0;
this.health = 100;
this.armor = 0;
this.weapon_id = 0;
this.primary_ammo = 30;
this.secondary_ammo = 90;
this.walking = false;
this.running = false;
this.ping = 0;
}
// store previous position for interpolation
savePrevious() {
this.px = this.x;
this.py = this.y;
this.pz = this.z;
}
applyServerState(s) {
this.px = this.x;
this.py = this.y;
this.pz = this.z;
this.x = s.x;
this.y = s.y;
this.z = s.z;
this.health = s.health;
this.armor = s.armor;
this.weapon_id = s.weapon_id;
this.primary_ammo = s.primary_ammo;
this.secondary_ammo = s.secondary_ammo;
this.walking = s.walking;
this.running = s.running;
this.ping = s.ping;
this.user_id = s.user_id;
}
}
// =======================================================
// 🌍 WORLD STATE (ALL PLAYERS)
// =======================================================
class World {
constructor() {
this.players = {};
}
update(serverPlayers) {
this.players = serverPlayers;
}
getPlayer(id) {
return this.players[id];
}
}
// =======================================================
// 🎮 MAIN GAME CLIENT (FULL SYSTEM)
// =======================================================
class GameClient {
constructor(serverUrl) {
this.net = new NetClient(serverUrl);
this.input = new InputController();
this.player = new Player();
this.world = new World();
this.tickRate = 20;
this.pingRate = 1000;
}
// =========================
// START GAME
// =========================
start() {
this.net.connect();
this.setupNetworking();
this.startLoops();
this.gameLoop();
}
// =========================
// AUTH
// =========================
sendAuth() {
this.net.send({
type: "auth",
user_id: "player_123",
token: "secure_token"
});
}
// =========================
// NETWORK HANDLING
// =========================
setupNetworking() {
this.net.onMessage = (msg) => {
if (msg.type === "auth_ok") {
this.net.authenticated = true;
this.player.user_id = msg.user_id;
console.log("Logged in as:", msg.user_id);
}
if (msg.type === "world") {
this.world.update(msg.players);
// find self inside world
for (const id in msg.players) {
const p = msg.players[id];
if (p.user_id === this.player.user_id) {
this.player.applyServerState(p);
}
}
}
};
}
// =========================
// SEND MOVEMENT PACKET
// =========================
sendMovement() {
const m = this.input.getMovementVector();
this.net.send({
type: "update",
seq: this.net.seq++,
dx: m.dx,
dy: m.dy,
dz: m.dz,
walking: this.input.isWalking(),
running: this.input.isRunning()
});
}
// =========================
// SEND COMBAT STATE
// =========================
sendCombat() {
this.net.send({
type: "combat",
weapon_id: this.player.weapon_id,
primary_ammo: this.player.primary_ammo,
secondary_ammo: this.player.secondary_ammo
});
}
// =========================
// SEND PING
// =========================
sendPing() {
this.net.send({
type: "ping",
t: Date.now()
});
}
// =========================
// INTERPOLATION (SMOOTH MOVEMENT)
// =========================
lerp(a, b, t) {
return a + (b - a) * t;
}
getSmoothPosition(p) {
return {
x: this.lerp(p.px, p.x, 0.15),
y: this.lerp(p.py, p.y, 0.15),
z: this.lerp(p.pz, p.z, 0.15)
};
}
// =========================
// MAIN GAME LOOP
// =========================
gameLoop() {
const loop = () => {
// update movement every frame
if (this.net.connected && this.net.authenticated) {
this.sendMovement();
}
requestAnimationFrame(loop);
};
loop();
}
// =========================
// TIMED NETWORK LOOPS
// =========================
startLoops() {
// ping
setInterval(() => {
this.sendPing();
}, this.pingRate);
// combat sync
setInterval(() => {
this.sendCombat();
}, 200);
}
}
// =======================================================
// 🚀 BOOT GAME CLIENT
// =======================================================
const game = new GameClient("ws://localhost:4445");
game.start();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment