From 069d80e8675cf016742f59403947a98de7883807 Mon Sep 17 00:00:00 2001 From: x0x7 Date: Wed, 7 Aug 2024 18:44:21 -0515 Subject: [PATCH] Add files --- Makefile | 4 + README.md | 0 app.js | 5 + emitstate.js | 41 ++++++++ gamestate.js | 12 +++ http.js | 7 ++ io.js | 4 + physics.js | 241 ++++++++++++++++++++++++++++++++++++++++++++++ public/index.html | 19 ++++ public/render.js | 98 +++++++++++++++++++ run.js | 3 + server.js | 46 +++++++++ 12 files changed, 480 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100644 app.js create mode 100644 emitstate.js create mode 100644 gamestate.js create mode 100644 http.js create mode 100644 io.js create mode 100644 physics.js create mode 100644 public/index.html create mode 100644 public/render.js create mode 100755 run.js create mode 100644 server.js diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6516b7d --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ + + +run: + ./run.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/app.js b/app.js new file mode 100644 index 0000000..8ca8158 --- /dev/null +++ b/app.js @@ -0,0 +1,5 @@ + +const express = require('express'); +const app = express(); +app.use(express.static('public')); +module.exports = app; diff --git a/emitstate.js b/emitstate.js new file mode 100644 index 0000000..e1fd47b --- /dev/null +++ b/emitstate.js @@ -0,0 +1,41 @@ +const io = require('./io'); +var gameState = require('./gamestate'); +const STATE_BUFFER_SIZE = 300; // 10 seconds at 30 FPS +const EMIT_FPS=10; +const EMIT_MS=Math.round(1000/EMIT_FPS); +let stateBuffer = new Array(STATE_BUFFER_SIZE); +let stateBufferIndex = 0; +let lastSerializedState = null; +let lastSerializedTimestamp = 0; + +module.exports = emitState; +function emitState() { + gameState.timestamp = Date.now(); + + // Serialize the state only if it has changed since last time + if (gameState.timestamp !== lastSerializedTimestamp) { + lastSerializedState = JSON.stringify(gameState); + lastSerializedTimestamp = gameState.timestamp; + } + //console.log(gameState.clients); + //console.log(gameState.models.human1); + + // Store the current state in the ring buffer + stateBuffer[stateBufferIndex] = lastSerializedState; + stateBufferIndex = (stateBufferIndex + 1) % STATE_BUFFER_SIZE; + + // Emit state to clients + Object.entries(gameState.clients).forEach(([socketId, client]) => { + const socket = io.sockets.sockets.get(socketId); + if (socket) { + if (client.viewDelay > 0) { + const delayedStateIndex = (stateBufferIndex - Math.round(client.viewDelay / EMIT_MS) + STATE_BUFFER_SIZE) % STATE_BUFFER_SIZE; + const delayedState = stateBuffer[delayedStateIndex]; + if (delayedState) socket.emit('stateUpdate', JSON.parse(delayedState)); + } else { + socket.emit('stateUpdate', JSON.parse(lastSerializedState)); + } + } + }); +} +setInterval(emitState,EMIT_MS); diff --git a/gamestate.js b/gamestate.js new file mode 100644 index 0000000..b128ca1 --- /dev/null +++ b/gamestate.js @@ -0,0 +1,12 @@ + +let gameState = { + timestamp: Date.now(), + models: { + human1:{ id: 'human1', type: 'human', x: 0, y: 0, z: 1.8, pitch: 0, yaw: 0, roll: 0,dx:0,dy:0,dz:0}, + human2:{ id: 'human2', type: 'human', x: 100, y: 100, z: 1.8, pitch: 0, yaw: 0, roll: 0 ,dx:0,dy:0,dz:0}, + drone1:{ id: 'drone1', type: 'drone', x: 50, y: 50, z: 20, pitch: 0, yaw: 0, roll: 0, propSpeed: 3 ,dx:0,dy:0,dz:0,dpitch:0,dyaw:0,droll:0}, + antenna1:{ id: 'antenna1', type: 'antenna', x: 75, y: 75, z: 5, pitch: 0, yaw: 0, roll: 0} + }, + clients: {} +}; +module.exports = gameState; diff --git a/http.js b/http.js new file mode 100644 index 0000000..2954ed5 --- /dev/null +++ b/http.js @@ -0,0 +1,7 @@ + +var http = require('http').createServer(require('./app.js')); +const PORT = 3000; +http.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); +}); +module.exports = http; diff --git a/io.js b/io.js new file mode 100644 index 0000000..53ffd3c --- /dev/null +++ b/io.js @@ -0,0 +1,4 @@ + +var gameState = require('./gamestate'); +var io = require('socket.io')(require('./http')); +module.exports = io; diff --git a/physics.js b/physics.js new file mode 100644 index 0000000..5f5af2e --- /dev/null +++ b/physics.js @@ -0,0 +1,241 @@ +var gameState = require('./gamestate'); + +const PHYSICS_FPS = 30; +var GRAVITY = 9.81; // m/s^2 +GRAVITY/=3; +const GRAVITY_MS = GRAVITY / 1000; // m/ms^2 +const HOVER_PROP_SPEED = 3; +const MIN_PROP_SPEED = -3; +const MAX_PROP_SPEED = 10; +const DRONE_THRUST_PER_PROP_SPEED = GRAVITY_MS / HOVER_PROP_SPEED; +const PROP_CTRL_MS = 9000; // Time to go from min to max by holding down the control +const PROP_CHANGE_PER_MS = (MAX_PROP_SPEED - MIN_PROP_SPEED) / PROP_CTRL_MS; +const PHYSICS_MS = Math.round(1000 / PHYSICS_FPS); +const JUMP_VELOCITY = 1; // m/s +const DRONE_SENSITIVITY = 0.01; // rad/s/s +const DRONE_SENSITIVITY_MS = DRONE_SENSITIVITY/1000; // rad/s/ms +const DRONE_STABILITY_FACTOR = 2/3; +var BABYLON = null; +import('@babylonjs/core').then(i=>BABYLON=i); + +// Update game state at approximately 30 FPS +setInterval(updateGameState, PHYSICS_MS); + +function updateGameState() { + var deltaT = PHYSICS_MS; + gameState.timestamp = Date.now(); + + // Apply control physics + Object.values(gameState.clients).forEach(client => applyControlPhysics(client, deltaT)); + + // Apply physics to all models + Object.values(gameState.models).forEach(model => { + if (model.type === 'human') applyHumanPhysics(model, deltaT); + if (model.type === 'drone') applyDronePhysics(model, deltaT); + }); + +} + +function applyControlPhysics(client, deltaT) { + const model = gameState.models[client.controllingId]; + //console.log('control model',model); + if (!model) return; + if (model.type === 'human') applyHumanControlPhysics(client, model, deltaT); + if (model.type === 'drone') applyDroneControlPhysics(client, model, deltaT); +} + +function applyHumanPhysics(model, deltaT) { + // Apply gravity + model.dz -= GRAVITY_MS * deltaT; + + // Apply movement + model.x += model.dx * deltaT; + model.y += model.dy * deltaT; + model.z += model.dz * deltaT; + + + // Apply walk slowing + var backthrust = 0.1*GRAVITY_MS; + var velocity = Math.sqrt(model.dx*model.dx+model.dy*model.dy+model.dz*model.dz); + var nx = model.dx/velocity; + var ny = model.dy/velocity; + var nz = model.dz/velocity; + var backx = -nx*deltaT*backthrust; + var backy = -ny*deltaT*backthrust; + var backz = -nz*deltaT*backthrust; + if(backx>model.dx) { + model.dx=0; + model.dy=0; + model.dz=0; + } + else { + model.dx=backx; + model.dy=backy; + model.dz=backz; + } + + // Prevent going below ground + if (model.z < 1.8) { + model.z = 1.8; + model.dz = 0; + } +} + +function applyDronePhysics(model, deltaT) { + // Apply gravity + //console.log('pre physics',model); + + // Apply thrust + if(BABYLON) { + model.dz -= GRAVITY_MS * deltaT; + const thrust = model.propSpeed * DRONE_THRUST_PER_PROP_SPEED; + const thrustVector = new BABYLON.Vector3(0, thrust, 0); + const rotationMatrix = BABYLON.Matrix.RotationYawPitchRoll(model.yaw, model.pitch, model.roll); + const rotatedThrustVector = BABYLON.Vector3.TransformNormal(thrustVector, rotationMatrix); + //console.log('prop vector',rotatedThrustVector); + + model.dx += rotatedThrustVector.x * deltaT; + model.dy += rotatedThrustVector.z * deltaT; + model.dz += rotatedThrustVector.y * deltaT; + //console.log({gravity:GRAVITY_MS*deltaT,thrust:rotatedThrustVector.y*deltaT}); + } + + // Apply movement + model.x += model.dx * deltaT; + model.y += model.dy * deltaT; + model.z += model.dz * deltaT; + model.pitch += model.dpitch * deltaT; + model.yaw += model.dyaw * deltaT; + model.roll += model.droll * deltaT; + //console.log({dronedz:model.dz}); + + // Apply drag (air resistance) + //const dragFactor = 0.1 * deltaT; + //model.dx *= (1 - dragFactor); + //model.dy *= (1 - dragFactor); + //model.dz *= (1 - dragFactor); + //model.dpitch *= (1 - dragFactor); + //model.dyaw *= (1 - dragFactor); + //model.droll *= (1 - dragFactor); + //Apply linear drag + var velocity = Math.sqrt(model.dx*model.dx+model.dy*model.dy+model.dz*model.dz); + if(velocity) { + var drag_coef=500; + var dragForce = drag_coef*velocity*velocity; + var dragForce_ms = dragForce/1000; + var nx = model.dx/velocity; + var ny = model.dy/velocity; + var nz = model.dz/velocity; + model.dx-=nx*dragForce_ms*deltaT; + model.dy-=ny*dragForce_ms*deltaT; + model.dz-=nz*dragForce_ms*deltaT; + //console.log({velocity,drag_coef,dragForce,nx,ny,nz,deltaT}); + } + //Apply rotational drag + const rotationSpeed = DRONE_STABILITY_FACTOR * DRONE_SENSITIVITY_MS * deltaT; + if(model.droll>0) { + model.droll-=rotationSpeed; + if(model.droll<0) model.droll=0; + } + else if(model.droll<0) { + model.droll+=rotationSpeed; + if(model.droll>0) model.droll=0; + } + if(model.dpitch>0) { + model.dpitch-=rotationSpeed; + if(model.dpitch<0) model.dpitch=0; + } + else if(model.dpitch<0) { + model.dpitch+=rotationSpeed; + if(model.dpitch>0) model.dpitch=0; + } + if(model.dyaw>0) { + model.dyaw-=rotationSpeed; + if(model.dyaw<0) model.dyaw=0; + } + else if(model.dyaw<0) { + model.dyaw+=rotationSpeed; + if(model.dyaw>0) model.dyaw=0; + } + // Apply prop speed centering + if(model.propSpeed>HOVER_PROP_SPEED) { + model.propSpeed-=PROP_CHANGE_PER_MS*DRONE_STABILITY_FACTOR*deltaT; + if(model.propSpeedHOVER_PROP_SPEED) model.propSpeed=HOVER_PROP_SPEED; + } + + // Prevent going below ground + if (model.z < 0) { + //console.log('Hit ground',{z:model.z,dz:model.dz}); + model.z = 0; + model.dz = 0; + } + //console.log('post physics',model); +} + +function applyHumanControlPhysics(client, model, deltaT) { + //console.log('applyHumanControlPhysics'); + var walkthrust_ms = 0.3*GRAVITY_MS + var radpersec = Math.PI; + var radperms = radpersec/1000; + + + if (client.controlState['w']) { + model.dx += Math.sin(model.yaw)*walkthrust_ms*deltaT; + model.dy += Math.cos(model.yaw)*walkthrust_ms*deltaT; + } + if (client.controlState['s']) { + model.dx -= Math.sin(model.yaw)*walkthrust_ms*deltaT; + model.dy -= Math.cos(model.yaw)*walkthrust_ms*deltaT; + } + if (client.controlState['a']) { + model.dx -= Math.cos(model.yaw)*walkthrust_ms*deltaT; + model.dy += Math.sin(model.yaw)*walkthrust_ms*deltaT; + } + if (client.controlState['d']) { + model.dx += Math.cos(model.yaw)*walkthrust_ms*deltaT; + model.dy -= Math.sin(model.yaw)*walkthrust_ms*deltaT; + } + if (client.controlState['ArrowLeft']) model.yaw -= radperms*deltaT; + if (client.controlState['ArrowRight']) model.yaw += radperms*deltaT; + if (client.controlState['ArrowUp']) model.pitch -= radperms*deltaT; + if (client.controlState['ArrowDown']) model.pitch += radperms*deltaT; + if (client.controlState['j'] && Math.abs(model.z-1.8)<0.1) { + model.dz = JUMP_VELOCITY; + } +} + +function applyDroneControlPhysics(client, model, deltaT) { + const rotationSpeed = DRONE_SENSITIVITY_MS * deltaT; // 1 rad/s + + if (client.controlState['d']) model.droll -= rotationSpeed; + if (client.controlState['a']) model.droll += rotationSpeed; + if (client.controlState['s']) model.dpitch -= rotationSpeed; + if (client.controlState['w']) model.dpitch += rotationSpeed; + if (client.controlState['q']) model.dyaw -= rotationSpeed; + if (client.controlState['e']) model.dyaw += rotationSpeed; + if (client.controlState['ArrowLeft']) model.dyaw -= rotationSpeed; + if (client.controlState['ArrowRight']) model.dyaw += rotationSpeed; + if (client.controlState['f']) { + model.roll = model.pitch = 0; + model.droll = model.dpitch = model.dyaw = 0; + } + if (client.controlState['g']) { + model.dx = model.dy = model.dz = 0; + model.roll = model.pitch = 0; + model.droll = model.dpitch = model.dyaw = 0; + } + if (client.controlState['ArrowUp']) { + model.propSpeed += PROP_CHANGE_PER_MS * deltaT; + if (model.propSpeed > MAX_PROP_SPEED) model.propSpeed = MAX_PROP_SPEED; + } + if (client.controlState['ArrowDown']) { + model.propSpeed -= PROP_CHANGE_PER_MS * deltaT; + if (model.propSpeed < MIN_PROP_SPEED) model.propSpeed = MIN_PROP_SPEED; + } +} + +module.exports = {updateGameState }; diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..4984858 --- /dev/null +++ b/public/index.html @@ -0,0 +1,19 @@ + + + + Drone Warfare Prototype + + + + + + + + + diff --git a/public/render.js b/public/render.js new file mode 100644 index 0000000..9804993 --- /dev/null +++ b/public/render.js @@ -0,0 +1,98 @@ +const socket = io(); + +let gameState = null; +let clientId = null; +const modelMeshes = {}; + +const canvas = document.getElementById("renderCanvas"); +const engine = new BABYLON.Engine(canvas, true); +var {scene,camera} = createScene(); + +function createScene() { + var scene = new BABYLON.Scene(engine); + var camera = new BABYLON.FreeCamera("camera", new BABYLON.Vector3(0, 5, -10), scene); + camera.setTarget(BABYLON.Vector3.Zero()); + camera.attachControl(canvas, true); + + const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene); + + // Create ground + const ground = BABYLON.MeshBuilder.CreateGround("ground", {width: 200, height: 200}, scene); + const groundMaterial = new BABYLON.StandardMaterial("groundMaterial", scene); + groundMaterial.diffuseColor = new BABYLON.Color3(0.5, 0.5, 0.5); + ground.material = groundMaterial; + + return {scene,camera}; +} + +function createOrUpdateMesh(model) { + if (!modelMeshes[model.id]) { + let mesh; + if (model.type === 'human') { + mesh = BABYLON.MeshBuilder.CreateBox(model.id, {height: 1.8, width: 0.5, depth: 0.5}, scene); + mesh.material = new BABYLON.StandardMaterial(model.id + "Material", scene); + mesh.material.diffuseColor = new BABYLON.Color3(0, 0, 1); + } else if (model.type === 'drone') { + mesh = BABYLON.MeshBuilder.CreateBox(model.id, {size: 1}, scene); + mesh.material = new BABYLON.StandardMaterial(model.id + "Material", scene); + mesh.material.diffuseColor = new BABYLON.Color3(1, 0, 0); + } else if (model.type === 'antenna') { + mesh = BABYLON.MeshBuilder.CreateCylinder(model.id, {height: 5, diameter: 0.5}, scene); + mesh.material = new BABYLON.StandardMaterial(model.id + "Material", scene); + mesh.material.diffuseColor = new BABYLON.Color3(0, 1, 0); + } + modelMeshes[model.id] = mesh; + } + + const mesh = modelMeshes[model.id]; + mesh.position.set(model.x, model.z, model.y); + mesh.rotation.set(model.pitch, model.yaw, model.roll); +} + +socket.on('connect', () => { + clientId = socket.id; +}); + +socket.on('initialState', (state) => { + gameState = state; + Object.values(state.models).forEach(createOrUpdateMesh); +}); + +socket.on('stateUpdate', (state) => { + gameState = state; + console.log(gameState); + console.log(gameState.models.human1); + Object.values(state.models).forEach(createOrUpdateMesh); + + const client = gameState.clients[clientId]; + if (client) { + const viewerModel = gameState.models[client.viewingId] + if (viewerModel) { + camera.position.set(viewerModel.x, viewerModel.z + 1.8, viewerModel.y); + camera.rotation.set(viewerModel.pitch, viewerModel.yaw, viewerModel.roll); + } + } +}); + + +engine.runRenderLoop(() => { + scene.render(); +}); + +window.addEventListener("resize", () => { + engine.resize(); +}); + +document.addEventListener('keydown', (event) => { + socket.emit('keyEvent', { key: event.key, isDown: true }); +}); + +document.addEventListener('keyup', (event) => { + socket.emit('keyEvent', { key: event.key, isDown: false }); +}); + +document.addEventListener('keypress', (event) => { + if (event.key === 'v') { + socket.emit('toggleView'); + } +}); diff --git a/run.js b/run.js new file mode 100755 index 0000000..e3b6e7c --- /dev/null +++ b/run.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +require('./server.js'); diff --git a/server.js b/server.js new file mode 100644 index 0000000..a11620f --- /dev/null +++ b/server.js @@ -0,0 +1,46 @@ +var app = require('./app'); +const http = require('./http'); +const io = require('./io'); +const emitState = require('./emitstate.js'); +const physics = require('./physics'); + +var gameState = require('./gamestate'); + +io.on('connection', (socket) => { + console.log('A user connected'); + + gameState.clients[socket.id] = { + controlState: {}, + controllingId: 'human1', + viewingId: 'human1', + controlDelay: 0, + viewDelay: 0 + }; + + socket.emit('initialState', gameState); + + socket.on('keyEvent', ({ key, isDown }) => { + const client = gameState.clients[socket.id]; + if (client.controlDelay > 0) { + setTimeout(() => { + client.controlState[key] = isDown; + }, client.controlDelay); + } else { + client.controlState[key] = isDown; + } + }); + + socket.on('toggleView', () => { + const client = gameState.clients[socket.id]; + client.viewingId = client.viewingId === 'human1' ? 'drone1' : 'human1'; + client.controllingId = client.viewingId; + client.controlDelay = client.controllingId === 'human1' ? 0 : 250; + client.viewDelay = client.viewingId === 'human1' ? 0 : 250; + }); + + socket.on('disconnect', () => { + delete gameState.clients[socket.id]; + }); +}); + +