commit 069d80e8675cf016742f59403947a98de7883807 Author: x0x7 Date: Wed Aug 7 18:44:21 2024 -0515 Add files 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]; + }); +}); + +