import * as CANNON from './cannon-es.js' import Entity from './Entity.js' Object.defineProperty(Array.prototype, 'shuffle', { value: function () { for (let i = this.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [this[i], this[j]] = [this[j], this[i]]; } return this; } }); //TODO //connect with server //download last 20(?) frames //simulate next 10(?) frames // frames 1-30 exist //start drawing frame 23 (last + input delay) //each update add new "future" frame //when player pressed input, apply change to proper future frames //current frame 23 //player presses go forward - add acceleration to frame 26 //when server info comes in, look at where entity should be by frame and adjust values //current frame 23, simulated out to 30 //server frame 20 comes in, entity should be moving //cannot edit past frames, but can make changes to future frames //set entity position as computed for frame 24 var socket var scheme = document.location.protocol === "https:" ? "wss" : "ws" var port = document.location.port ? (":" + document.location.port) : "80" const userId = `player` + `${Math.floor(255 * Math.random())}`.padStart(3, "0"); const playerColor = ["red", "orange", "black", "green", "blue", "violet"].shuffle()[0] // const gameSocketUrl = scheme + "://game.bubblesocket:" + port const gameSocketUrl = "ws://game.bubblesocket" var canvas = null; var context = null; var movements = [] var hasInitialFrame = false var sessionId = null; var entityId = null; // var frictionCoefficient = 0.05; const keys = {} var simulationData = { entityFrames: [], inputFrames: [], currentFrame: 0, displayFrame: 0, baseFrame: 0, serverFrame: 0, } const FRAME_INPUT_DELAY = 3; const simulationComputeBuffer = 30; // var inLockStep = false // var lockedFrame = 0 var cannonWorld var lastTime = new Date().getTime() var fixedTimeStep = 1.0 / 60.0 //60 fps var maxSubSteps = 3 var playerEntity const InputList = { THRUST: 1, TURNLEFT: 2, TURNRIGHT: 3, } const MessageType = { Connect: "01", Disconnect: "02", PlayerConnect: "03", PlayerDisconnect: "04", AddInputs: "05", Resync: "06", } const possibleInputs = [ 'thrust', 'thrust', 'turnLeft', 'turnRight', ] let fakePlayerInputs = [] document.addEventListener("DOMContentLoaded", () => { canvas = document.createElement("canvas") canvas.style.width = "300px" canvas.style.height = "150px" canvas.width = 300 canvas.height = 150 context = canvas.getContext('2d') context.width = 300 context.height = 150 const connectButton = document.getElementById("connect") document.getElementById("container").appendChild(canvas) connectButton.addEventListener("click", () => { if (socket) { socketSend(MessageType.Disconnect, {}) socket.close(1000, "Closing from client"); connectButton.innerHTML = "Connect" return } socket = new WebSocket(gameSocketUrl); socket.onopen = (event) => { socketSend(MessageType.Connect, [userId, playerColor]) connectButton.innerHTML = "Disconnect" } socket.onclose = (event) => { if(!event.wasClean) { console.log(`socket did not close cleanly:`, event) connectButton.innerHTML = "Connect" } socket = null } socket.onerror = (event) => { console.error(`socket error:`, event) socket = null } socket.onmessage = (event) => { socketReceive(event) } connectButton.innerHTML = "Connecting..." }) window.addEventListener("keydown", (e) => { if (keys[e.code]) { return } keys[e.code] = true let keyCode = keybindInputTranslate(e.code); if (keyCode != null) { movements.push([keyCode, 1]) } }) window.addEventListener("keyup", (e) => { if (!keys[e.code]) { return } keys[e.code] = false let keyCode = keybindInputTranslate(e.code) if (keyCode != null) { movements.push([keyCode, 0]) } }) window.requestAnimationFrame(animate) setInterval(() => { let time = new Date().getTime() update((time - lastTime )/ 1000) lastTime = time }, parseInt(fixedTimeStep * 1000)) setInterval(broadcast, 32) //30fps // runFakePlayer(); }) function keybindInputTranslate(code) { switch (code) { case "KeyW": return InputList.UP case "KeyS": return InputList.DOWN case "KeyA": return InputList.LEFT case "KeyD": return InputList.RIGHT default: return null; } } function runFakePlayer() { let delay = Math.floor(1700 * Math.random()) + 300; setTimeout(() => { if (socket && hasInitialFrame) { let selectedInput = possibleInputs.shuffle()[0] fakePlayerInputs.push(playerEntity[selectedInput].bind(playerEntity)) } }, delay); setTimeout(() => { if (socket && hasInitialFrame) { fakePlayerInputs = [] } runFakePlayer() }, delay + 32); } function init(latestFrame) { // cannonWorld = new CANNON.World(); // playerEntity = new Entity(userId, playerColor) // cannonWorld.gravity.set(0,0,0) // playerEntity.attachToWorld(cannonWorld) // runFakePlayer() // simulationData.entityFrames = [] // simulationData.serverFrame = latestFrame.id // simulationData.currentFrame = latestFrame.id + FRAME_INPUT_DELAY // let startingFrame = {} // latestFrame.entities.forEach(entity => { // startingFrame[entity.id] = deserializeFrameEntity(entity) // }) // for (let frameId = latestFrame.id; frameId <= simulationData.currentFrame + simulationComputeBuffer; frameId++) { // simulationData.entityFrames[frameId] = JSON.parse(JSON.stringify(startingFrame)) // } // simulationData.baseFrame = 0 // simulationData.displayFrame = simulationData.baseFrame + simulationComputeBuffer; } // function deserializeFrameEntity(entity) { // return { // id: parseInt(entity.id), // acceleration: { // x: parseFloat(entity.acceleration[0]), // y: parseFloat(entity.acceleration[1]), // }, // velocity: { // x: parseFloat(entity.velocity[0]), // y: parseFloat(entity.velocity[1]), // }, // position: { // x: parseFloat(entity.position[0]), // y: parseFloat(entity.position[1]), // }, // color: entity.color, // speed: parseFloat(entity.speed), // } // } function deserializeFrameEntity(entity) { return { id: parseInt(entity.id), // acceleration: { // x: parseFloat(entity.acceleration[0]), // y: parseFloat(entity.acceleration[1]), // }, // velocity: { // x: parseFloat(entity.velocity[0]), // y: parseFloat(entity.velocity[1]), // }, position: { x: parseFloat(entity.position[0]), y: parseFloat(entity.position[1]), }, // color: entity.color, // speed: parseFloat(entity.speed), } } function animate() { draw(context) window.requestAnimationFrame(animate) } function update(dt) { if (!socket || !hasInitialFrame) { return } // fakePlayerInputs.forEach(input => { // input() // }) // playerEntity.update() // playerEntity.worldEdgeWrap(canvas) // cannonWorld.step(fixedTimeStep, dt, maxSubSteps) // if (inLockStep && lockedFrame == simulationData.baseFrame) { // return // } // for(let frameId = simulationData.baseFrame; frameId <= simulationData.displayFrame; frameId++) { // let frameInputs = simulationData.inputFrames[frameId] ?? {} // let frame = simulationData.entityFrames[frameId] // let frameEntities = Object.values(frame) ?? [] // for(let i in frameEntities) { // let entityClone = JSON.parse(JSON.stringify(frameEntities[i])) // let entityInputs = frameInputs[entityClone.id] ?? [] // applyInputsToEntity(entityClone, entityInputs) // updateEntity(entityClone) // let nextFrame = simulationData.entityFrames[frameId + 1] ?? {} // nextFrame[entityClone.id] = entityClone // } // } // lockedFrame = simulationData.baseFrame } function broadcast() { if(!socket || !hasInitialFrame) { return; } // if (inLockStep && lockedFrame == simulationData.baseFrame) { // return // } // var inputs = JSON.parse(JSON.stringify(movements)) // var targetInputFrame = simulationData.displayFrame - FRAME_INPUT_DELAY // var inputFrame = simulationData.inputFrames[targetInputFrame] ?? {} // inputFrame[entityId] = inputs // simulationData.inputFrames[targetInputFrame] = inputFrame // movements = [] // let lastEntityFrame = simulationData.entityFrames[simulationData.displayFrame]; // simulationData.baseFrame++ // simulationData.displayFrame = simulationData.baseFrame + simulationComputeBuffer // console.log(`drawing ${this.id}`) // simulationData.entityFrames[simulationData.displayFrame] = JSON.parse(JSON.stringify(lastEntityFrame)) // // console.log(simulationData.baseFrame, simulationData.displayFrame, simulationData.entityFrames.length) // if (inputs.length > 0) { // socketSend(MessageType.AddInputs, [targetInputFrame, inputs]) // } } // function applyInputsToEntity(entity, inputs) { // inputs.forEach(input => { // switch(input[0]) { // case InputList.UP: // entity.acceleration.y = input[1] ? -1 * entity.speed : 0; // break; // case InputList.DOWN: // entity.acceleration.y = input[1] ? 1 * entity.speed : 0; // break; // case InputList.LEFT: // entity.acceleration.x = input[1] ? -1 * entity.speed : 0; // break; // case InputList.RIGHT: // entity.acceleration.x = input[1] ? 1 * entity.speed : 0; // break; // } // }) // } // function updateEntity(entity) { // entity.velocity.x += entity.acceleration.x // entity.velocity.y += entity.acceleration.y // entity.velocity.x *= (1 - frictionCoefficient) // entity.velocity.y *= (1 - frictionCoefficient) // entity.position.x += entity.velocity.x // entity.position.y += entity.velocity.y // if (entity.position.x > canvas.width) { // entity.position.x -= canvas.width // } // if (entity.position.x < 0) { // entity.position.x += canvas.width // } // if (entity.position.y > canvas.height) { // entity.position.y -= canvas.height // } // if (entity.position.y < 0) { // entity.position.y += canvas.height // } // } function socketSend(messageType, payload) { if (!socket || socket.readyState !== WebSocket.OPEN) { return } let blobData = [messageType, JSON.stringify(payload)] let blob = new Blob(blobData, { type: 'application/json' }); console.log("socket send:", blobData) socket.send(blob) } async function socketReceive(packet) { let response = await packet.data.text() let messageType = response.substr(0, 2) let messageData = response.substr(2, response.length - 2) let payload = JSON.parse(messageData); switch (messageType) { case MessageType.Connect: console.log("connect", payload) sessionId = payload.sessionId entityId = parseInt(payload.entityId) init(payload.latestFrame) hasInitialFrame = true break; case MessageType.Disconnect: console.log("disconnect", payload) break; // case MessageType.PlayerConnect: // console.log("player connect", payload) // let newEntityId = parseInt(payload[0]) // let frameAdded = parseInt(payload[1]) // let newEntity = deserializeFrameEntity(payload[2]) // for (let i = simulationData.baseFrame; i <= simulationData.displayFrame; i++) { // simulationData.entityFrames[i][newEntityId] = newEntity // } // console.log(simulationData.entityFrames[simulationData.displayFrame]) // break; // case MessageType.PlayerDisconnect: // console.log("player disconnect", payload, simulationData.entityFrames) // let entityIdToRemove = parseInt(payload[0]) // let disconnectFrame = parseInt(payload[1]) // for (let i = simulationData.baseFrame; i <= simulationData.displayFrame; i++) { // delete simulationData.entityFrames[i][entityIdToRemove] // } // break; // case MessageType.AddInputs: // console.log("received movements", payload) // break; case MessageType.Resync: hasInitialFrame = true // console.log("resync", payload) simulationData.serverFrame = payload[0] let entityFrame = payload[1] let startingFrame = {} entityFrame.entities.forEach(entity => { startingFrame[entity.id] = deserializeFrameEntity(entity) }) // for (let frameId = entityFrame.id; frameId <= simulationData.currentFrame + simulationComputeBuffer; frameId++) { simulationData.entityFrames[simulationData.serverFrame] = JSON.parse(JSON.stringify(startingFrame)) // } simulationData.baseFrame = 0 simulationData.displayFrame = simulationData.baseFrame + simulationComputeBuffer; break default: console.warn("unknown message type", messageType, payload) break } } // var acceleration = {x: 0, y: 0} // var velocity = {x: 0, y: 0} // var position = {x: 20, y: 20} function drawPlayer(ctx, playerData) { ctx.beginPath() ctx.arc(playerData.position.x, playerData.position.y, 4, 0, 2 * Math.PI) ctx.fill() } function draw(ctx) { context.clearRect(0, 0, canvas.width, canvas.height) if (!socket) { ctx.fillStyle = "black" ctx.fillText("Disconnected", 50, 50) } else if (socket && !hasInitialFrame) { ctx.fillStyle = "black" ctx.fillText("Syncing with server", 50, 50) } else if(socket && hasInitialFrame) { // playerEntity.draw(ctx) // let player = simulationData.entityFrames[simulationData.displayFrame][entityId] let currentEntities = Object.values(simulationData.entityFrames[simulationData.serverFrame]) currentEntities.forEach(entity => { drawPlayer(ctx, entity); }) } }