123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482 |
- 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);
- })
- }
- }
|