Add files
This commit is contained in:
commit
069d80e867
|
@ -0,0 +1,5 @@
|
|||
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
app.use(express.static('public'));
|
||||
module.exports = app;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
var gameState = require('./gamestate');
|
||||
var io = require('socket.io')(require('./http'));
|
||||
module.exports = io;
|
|
@ -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 };
|
|
@ -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>
|
|
@ -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');
|
||||
}
|
||||
});
|
|
@ -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];
|
||||
});
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue