Add files

This commit is contained in:
x0x7 2024-08-07 18:44:21 -05:15
commit 069d80e867
12 changed files with 480 additions and 0 deletions

4
Makefile Normal file
View File

@ -0,0 +1,4 @@
run:
./run.js

0
README.md Normal file
View File

5
app.js Normal file
View File

@ -0,0 +1,5 @@
const express = require('express');
const app = express();
app.use(express.static('public'));
module.exports = app;

41
emitstate.js Normal file
View File

@ -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);

12
gamestate.js Normal file
View File

@ -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;

7
http.js Normal file
View File

@ -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;

4
io.js Normal file
View File

@ -0,0 +1,4 @@
var gameState = require('./gamestate');
var io = require('socket.io')(require('./http'));
module.exports = io;

241
physics.js Normal file
View File

@ -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.propSpeed<HOVER_PROP_SPEED) model.propSpeed=HOVER_PROP_SPEED;
}
else if(model.propSpeed<HOVER_PROP_SPEED) {
model.propSpeed+=PROP_CHANGE_PER_MS*DRONE_STABILITY_FACTOR*deltaT;
if(model.propSpeed>HOVER_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 };

19
public/index.html Normal file
View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<title>Drone Warfare Prototype</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babylonjs/7.19.1/babylon.min.js"></script>
<script src="/socket.io/socket.io.js"></script>
<style>
#renderCanvas {
width: 100%;
height: 100%;
touch-action: none;
}
</style>
</head>
<body>
<canvas id="renderCanvas"></canvas>
<script src="/render.js"></script>
</body>
</html>

98
public/render.js Normal file
View File

@ -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');
}
});

3
run.js Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env node
require('./server.js');

46
server.js Normal file
View File

@ -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];
});
});