game.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. import * as CANNON from './cannon-es.js'
  2. import Entity from './Entity.js'
  3. Object.defineProperty(Array.prototype, 'shuffle', {
  4. value: function () {
  5. for (let i = this.length - 1; i > 0; i--) {
  6. const j = Math.floor(Math.random() * (i + 1));
  7. [this[i], this[j]] = [this[j], this[i]];
  8. }
  9. return this;
  10. }
  11. });
  12. //TODO
  13. //connect with server
  14. //download last 20(?) frames
  15. //simulate next 10(?) frames
  16. // frames 1-30 exist
  17. //start drawing frame 23 (last + input delay)
  18. //each update add new "future" frame
  19. //when player pressed input, apply change to proper future frames
  20. //current frame 23
  21. //player presses go forward - add acceleration to frame 26
  22. //when server info comes in, look at where entity should be by frame and adjust values
  23. //current frame 23, simulated out to 30
  24. //server frame 20 comes in, entity should be moving
  25. //cannot edit past frames, but can make changes to future frames
  26. //set entity position as computed for frame 24
  27. var socket
  28. var scheme = document.location.protocol === "https:" ? "wss" : "ws"
  29. var port = document.location.port ? (":" + document.location.port) : "80"
  30. const userId = `player` + `${Math.floor(255 * Math.random())}`.padStart(3, "0");
  31. const playerColor = ["red", "orange", "black", "green", "blue", "violet"].shuffle()[0]
  32. // const gameSocketUrl = scheme + "://game.bubblesocket:" + port
  33. const gameSocketUrl = "ws://game.bubblesocket"
  34. var canvas = null;
  35. var context = null;
  36. var movements = []
  37. var hasInitialFrame = false
  38. var sessionId = null;
  39. var entityId = null;
  40. // var frictionCoefficient = 0.05;
  41. const keys = {}
  42. var simulationData = {
  43. entityFrames: [],
  44. inputFrames: [],
  45. currentFrame: 0,
  46. displayFrame: 0,
  47. baseFrame: 0,
  48. serverFrame: 0,
  49. }
  50. const FRAME_INPUT_DELAY = 3;
  51. const simulationComputeBuffer = 30;
  52. // var inLockStep = false
  53. // var lockedFrame = 0
  54. var cannonWorld
  55. var lastTime = new Date().getTime()
  56. var fixedTimeStep = 1.0 / 60.0 //60 fps
  57. var maxSubSteps = 3
  58. var playerEntity
  59. const InputList = {
  60. THRUST: 1,
  61. TURNLEFT: 2,
  62. TURNRIGHT: 3,
  63. }
  64. const MessageType = {
  65. Connect: "01",
  66. Disconnect: "02",
  67. PlayerConnect: "03",
  68. PlayerDisconnect: "04",
  69. AddInputs: "05",
  70. Resync: "06",
  71. }
  72. const possibleInputs = [
  73. 'thrust',
  74. 'thrust',
  75. 'turnLeft',
  76. 'turnRight',
  77. ]
  78. let fakePlayerInputs = []
  79. document.addEventListener("DOMContentLoaded", () => {
  80. canvas = document.createElement("canvas")
  81. canvas.style.width = "300px"
  82. canvas.style.height = "150px"
  83. canvas.width = 300
  84. canvas.height = 150
  85. context = canvas.getContext('2d')
  86. context.width = 300
  87. context.height = 150
  88. const connectButton = document.getElementById("connect")
  89. document.getElementById("container").appendChild(canvas)
  90. connectButton.addEventListener("click", () => {
  91. if (socket) {
  92. socketSend(MessageType.Disconnect, {})
  93. socket.close(1000, "Closing from client");
  94. connectButton.innerHTML = "Connect"
  95. return
  96. }
  97. socket = new WebSocket(gameSocketUrl);
  98. socket.onopen = (event) => {
  99. socketSend(MessageType.Connect, [userId, playerColor])
  100. connectButton.innerHTML = "Disconnect"
  101. }
  102. socket.onclose = (event) => {
  103. if(!event.wasClean) {
  104. console.log(`socket did not close cleanly:`, event)
  105. connectButton.innerHTML = "Connect"
  106. }
  107. socket = null
  108. }
  109. socket.onerror = (event) => {
  110. console.error(`socket error:`, event)
  111. socket = null
  112. }
  113. socket.onmessage = (event) => {
  114. socketReceive(event)
  115. }
  116. connectButton.innerHTML = "Connecting..."
  117. })
  118. window.addEventListener("keydown", (e) => {
  119. if (keys[e.code]) {
  120. return
  121. }
  122. keys[e.code] = true
  123. let keyCode = keybindInputTranslate(e.code);
  124. if (keyCode != null) {
  125. movements.push([keyCode, 1])
  126. }
  127. })
  128. window.addEventListener("keyup", (e) => {
  129. if (!keys[e.code]) {
  130. return
  131. }
  132. keys[e.code] = false
  133. let keyCode = keybindInputTranslate(e.code)
  134. if (keyCode != null) {
  135. movements.push([keyCode, 0])
  136. }
  137. })
  138. window.requestAnimationFrame(animate)
  139. setInterval(() => {
  140. let time = new Date().getTime()
  141. update((time - lastTime )/ 1000)
  142. lastTime = time
  143. }, parseInt(fixedTimeStep * 1000))
  144. setInterval(broadcast, 32) //30fps
  145. // runFakePlayer();
  146. })
  147. function keybindInputTranslate(code) {
  148. switch (code) {
  149. case "KeyW": return InputList.UP
  150. case "KeyS": return InputList.DOWN
  151. case "KeyA": return InputList.LEFT
  152. case "KeyD": return InputList.RIGHT
  153. default: return null;
  154. }
  155. }
  156. function runFakePlayer() {
  157. let delay = Math.floor(1700 * Math.random()) + 300;
  158. setTimeout(() => {
  159. if (socket && hasInitialFrame) {
  160. let selectedInput = possibleInputs.shuffle()[0]
  161. fakePlayerInputs.push(playerEntity[selectedInput].bind(playerEntity))
  162. }
  163. }, delay);
  164. setTimeout(() => {
  165. if (socket && hasInitialFrame) {
  166. fakePlayerInputs = []
  167. }
  168. runFakePlayer()
  169. }, delay + 32);
  170. }
  171. function init(latestFrame) {
  172. // cannonWorld = new CANNON.World();
  173. // playerEntity = new Entity(userId, playerColor)
  174. // cannonWorld.gravity.set(0,0,0)
  175. // playerEntity.attachToWorld(cannonWorld)
  176. // runFakePlayer()
  177. // simulationData.entityFrames = []
  178. // simulationData.serverFrame = latestFrame.id
  179. // simulationData.currentFrame = latestFrame.id + FRAME_INPUT_DELAY
  180. // let startingFrame = {}
  181. // latestFrame.entities.forEach(entity => {
  182. // startingFrame[entity.id] = deserializeFrameEntity(entity)
  183. // })
  184. // for (let frameId = latestFrame.id; frameId <= simulationData.currentFrame + simulationComputeBuffer; frameId++) {
  185. // simulationData.entityFrames[frameId] = JSON.parse(JSON.stringify(startingFrame))
  186. // }
  187. // simulationData.baseFrame = 0
  188. // simulationData.displayFrame = simulationData.baseFrame + simulationComputeBuffer;
  189. }
  190. // function deserializeFrameEntity(entity) {
  191. // return {
  192. // id: parseInt(entity.id),
  193. // acceleration: {
  194. // x: parseFloat(entity.acceleration[0]),
  195. // y: parseFloat(entity.acceleration[1]),
  196. // },
  197. // velocity: {
  198. // x: parseFloat(entity.velocity[0]),
  199. // y: parseFloat(entity.velocity[1]),
  200. // },
  201. // position: {
  202. // x: parseFloat(entity.position[0]),
  203. // y: parseFloat(entity.position[1]),
  204. // },
  205. // color: entity.color,
  206. // speed: parseFloat(entity.speed),
  207. // }
  208. // }
  209. function deserializeFrameEntity(entity) {
  210. return {
  211. id: parseInt(entity.id),
  212. // acceleration: {
  213. // x: parseFloat(entity.acceleration[0]),
  214. // y: parseFloat(entity.acceleration[1]),
  215. // },
  216. // velocity: {
  217. // x: parseFloat(entity.velocity[0]),
  218. // y: parseFloat(entity.velocity[1]),
  219. // },
  220. position: {
  221. x: parseFloat(entity.position[0]),
  222. y: parseFloat(entity.position[1]),
  223. },
  224. // color: entity.color,
  225. // speed: parseFloat(entity.speed),
  226. }
  227. }
  228. function animate() {
  229. draw(context)
  230. window.requestAnimationFrame(animate)
  231. }
  232. function update(dt) {
  233. if (!socket || !hasInitialFrame) {
  234. return
  235. }
  236. // fakePlayerInputs.forEach(input => {
  237. // input()
  238. // })
  239. // playerEntity.update()
  240. // playerEntity.worldEdgeWrap(canvas)
  241. // cannonWorld.step(fixedTimeStep, dt, maxSubSteps)
  242. // if (inLockStep && lockedFrame == simulationData.baseFrame) {
  243. // return
  244. // }
  245. // for(let frameId = simulationData.baseFrame; frameId <= simulationData.displayFrame; frameId++) {
  246. // let frameInputs = simulationData.inputFrames[frameId] ?? {}
  247. // let frame = simulationData.entityFrames[frameId]
  248. // let frameEntities = Object.values(frame) ?? []
  249. // for(let i in frameEntities) {
  250. // let entityClone = JSON.parse(JSON.stringify(frameEntities[i]))
  251. // let entityInputs = frameInputs[entityClone.id] ?? []
  252. // applyInputsToEntity(entityClone, entityInputs)
  253. // updateEntity(entityClone)
  254. // let nextFrame = simulationData.entityFrames[frameId + 1] ?? {}
  255. // nextFrame[entityClone.id] = entityClone
  256. // }
  257. // }
  258. // lockedFrame = simulationData.baseFrame
  259. }
  260. function broadcast() {
  261. if(!socket || !hasInitialFrame) {
  262. return;
  263. }
  264. // if (inLockStep && lockedFrame == simulationData.baseFrame) {
  265. // return
  266. // }
  267. // var inputs = JSON.parse(JSON.stringify(movements))
  268. // var targetInputFrame = simulationData.displayFrame - FRAME_INPUT_DELAY
  269. // var inputFrame = simulationData.inputFrames[targetInputFrame] ?? {}
  270. // inputFrame[entityId] = inputs
  271. // simulationData.inputFrames[targetInputFrame] = inputFrame
  272. // movements = []
  273. // let lastEntityFrame = simulationData.entityFrames[simulationData.displayFrame];
  274. // simulationData.baseFrame++
  275. // simulationData.displayFrame = simulationData.baseFrame + simulationComputeBuffer
  276. // console.log(`drawing ${this.id}`)
  277. // simulationData.entityFrames[simulationData.displayFrame] = JSON.parse(JSON.stringify(lastEntityFrame))
  278. // // console.log(simulationData.baseFrame, simulationData.displayFrame, simulationData.entityFrames.length)
  279. // if (inputs.length > 0) {
  280. // socketSend(MessageType.AddInputs, [targetInputFrame, inputs])
  281. // }
  282. }
  283. // function applyInputsToEntity(entity, inputs) {
  284. // inputs.forEach(input => {
  285. // switch(input[0]) {
  286. // case InputList.UP:
  287. // entity.acceleration.y = input[1] ? -1 * entity.speed : 0;
  288. // break;
  289. // case InputList.DOWN:
  290. // entity.acceleration.y = input[1] ? 1 * entity.speed : 0;
  291. // break;
  292. // case InputList.LEFT:
  293. // entity.acceleration.x = input[1] ? -1 * entity.speed : 0;
  294. // break;
  295. // case InputList.RIGHT:
  296. // entity.acceleration.x = input[1] ? 1 * entity.speed : 0;
  297. // break;
  298. // }
  299. // })
  300. // }
  301. // function updateEntity(entity) {
  302. // entity.velocity.x += entity.acceleration.x
  303. // entity.velocity.y += entity.acceleration.y
  304. // entity.velocity.x *= (1 - frictionCoefficient)
  305. // entity.velocity.y *= (1 - frictionCoefficient)
  306. // entity.position.x += entity.velocity.x
  307. // entity.position.y += entity.velocity.y
  308. // if (entity.position.x > canvas.width) {
  309. // entity.position.x -= canvas.width
  310. // }
  311. // if (entity.position.x < 0) {
  312. // entity.position.x += canvas.width
  313. // }
  314. // if (entity.position.y > canvas.height) {
  315. // entity.position.y -= canvas.height
  316. // }
  317. // if (entity.position.y < 0) {
  318. // entity.position.y += canvas.height
  319. // }
  320. // }
  321. function socketSend(messageType, payload) {
  322. if (!socket || socket.readyState !== WebSocket.OPEN) {
  323. return
  324. }
  325. let blobData = [messageType, JSON.stringify(payload)]
  326. let blob = new Blob(blobData, { type: 'application/json' });
  327. console.log("socket send:", blobData)
  328. socket.send(blob)
  329. }
  330. async function socketReceive(packet) {
  331. let response = await packet.data.text()
  332. let messageType = response.substr(0, 2)
  333. let messageData = response.substr(2, response.length - 2)
  334. let payload = JSON.parse(messageData);
  335. switch (messageType) {
  336. case MessageType.Connect:
  337. console.log("connect", payload)
  338. sessionId = payload.sessionId
  339. entityId = parseInt(payload.entityId)
  340. init(payload.latestFrame)
  341. hasInitialFrame = true
  342. break;
  343. case MessageType.Disconnect:
  344. console.log("disconnect", payload)
  345. break;
  346. // case MessageType.PlayerConnect:
  347. // console.log("player connect", payload)
  348. // let newEntityId = parseInt(payload[0])
  349. // let frameAdded = parseInt(payload[1])
  350. // let newEntity = deserializeFrameEntity(payload[2])
  351. // for (let i = simulationData.baseFrame; i <= simulationData.displayFrame; i++) {
  352. // simulationData.entityFrames[i][newEntityId] = newEntity
  353. // }
  354. // console.log(simulationData.entityFrames[simulationData.displayFrame])
  355. // break;
  356. // case MessageType.PlayerDisconnect:
  357. // console.log("player disconnect", payload, simulationData.entityFrames)
  358. // let entityIdToRemove = parseInt(payload[0])
  359. // let disconnectFrame = parseInt(payload[1])
  360. // for (let i = simulationData.baseFrame; i <= simulationData.displayFrame; i++) {
  361. // delete simulationData.entityFrames[i][entityIdToRemove]
  362. // }
  363. // break;
  364. // case MessageType.AddInputs:
  365. // console.log("received movements", payload)
  366. // break;
  367. case MessageType.Resync:
  368. hasInitialFrame = true
  369. // console.log("resync", payload)
  370. simulationData.serverFrame = payload[0]
  371. let entityFrame = payload[1]
  372. let startingFrame = {}
  373. entityFrame.entities.forEach(entity => {
  374. startingFrame[entity.id] = deserializeFrameEntity(entity)
  375. })
  376. // for (let frameId = entityFrame.id; frameId <= simulationData.currentFrame + simulationComputeBuffer; frameId++) {
  377. simulationData.entityFrames[simulationData.serverFrame] = JSON.parse(JSON.stringify(startingFrame))
  378. // }
  379. simulationData.baseFrame = 0
  380. simulationData.displayFrame = simulationData.baseFrame + simulationComputeBuffer;
  381. break
  382. default:
  383. console.warn("unknown message type", messageType, payload)
  384. break
  385. }
  386. }
  387. // var acceleration = {x: 0, y: 0}
  388. // var velocity = {x: 0, y: 0}
  389. // var position = {x: 20, y: 20}
  390. function drawPlayer(ctx, playerData) {
  391. ctx.beginPath()
  392. ctx.arc(playerData.position.x, playerData.position.y, 4, 0, 2 * Math.PI)
  393. ctx.fill()
  394. }
  395. function draw(ctx) {
  396. context.clearRect(0, 0, canvas.width, canvas.height)
  397. if (!socket) {
  398. ctx.fillStyle = "black"
  399. ctx.fillText("Disconnected", 50, 50)
  400. } else if (socket && !hasInitialFrame) {
  401. ctx.fillStyle = "black"
  402. ctx.fillText("Syncing with server", 50, 50)
  403. } else if(socket && hasInitialFrame) {
  404. // playerEntity.draw(ctx)
  405. // let player = simulationData.entityFrames[simulationData.displayFrame][entityId]
  406. let currentEntities = Object.values(simulationData.entityFrames[simulationData.serverFrame])
  407. currentEntities.forEach(entity => {
  408. drawPlayer(ctx, entity);
  409. })
  410. }
  411. }