├── README.md
├── index.html
├── main.js
├── package.json
└── src
├── client
├── Client.js
└── InterpolatedPlayer.js
├── server
├── Client.js
└── Server.js
└── shared
├── Inputs.js
└── Player.js
/README.md:
--------------------------------------------------------------------------------
1 | # Client Side Prediction and Server reconciliation
2 |
3 | Client side prediction and server reconciliation implementation along with a little anti speed hack.
4 |
5 | [Live Demo](https://prediction-side-client.herokuapp.com/)
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Client Side Prediction
5 |
93 |
94 |
95 |
96 |
97 |
119 |
120 |
ASWD to move
121 |
QE to rotate
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
155 |
156 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | const WebSocket = require( 'ws' );
2 | const url = require( 'url' );
3 |
4 | const express = require( 'express' );
5 | const Server = require( './src/server/Server.js' );
6 |
7 | const app = express();
8 |
9 | app.use( express.static( __dirname ) );
10 |
11 | const port = process.env.PORT || 80;
12 |
13 | const httpServer = app.listen( port, function () {
14 |
15 | console.log( 'Server listening on port ' + port + '...' );
16 |
17 | } );
18 |
19 | const server = new Server();
20 |
21 | server.start( httpServer );
22 |
23 | setInterval( function () {
24 |
25 | server.update();
26 |
27 | }, 1000 / 10 );
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client-side-prediction",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node main.js"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "ws": "^7.3.1",
13 | "express": "latest"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/client/Client.js:
--------------------------------------------------------------------------------
1 | function Client() {
2 |
3 | const scope = this;
4 |
5 | this.gameCanvasEl = document.querySelector( '.game-canvas' );
6 |
7 | this.serverStateEl = document.querySelector( '.server-state' );
8 | this.predictedStateEl = document.querySelector( '.predicted-state' );
9 | this.interpolatedStateEl = document.querySelector( '.interpolated-state' );
10 | this.speedHackEl = document.querySelector( '.speed-hack' );
11 |
12 | this.gameCanvasEl.width = window.innerWidth;
13 | this.gameCanvasEl.height = window.innerHeight;
14 |
15 | this.context = this.gameCanvasEl.getContext( '2d' );
16 |
17 | this.webSocket = null;
18 |
19 | this.messages = [];
20 |
21 | this.player = new Player();
22 | this.inputs = new Inputs();
23 | this.inputsArray = [];
24 |
25 | this.historySize = 1024;
26 | this.history = [];
27 |
28 | this.tickNumber = 0;
29 |
30 | this.currentTime = null;
31 | this.lastTime = null;
32 | this.deltaTime = null;
33 |
34 | this.interpolatedPlayer = new InterpolatedPlayer();
35 | this.serverPlayer = new Player();
36 | this.speedHackPlayer = new Player();
37 |
38 | this.otherPlayers = [];
39 |
40 | this.trails = [];
41 |
42 | this.connect = function ( url ) {
43 |
44 | if ( scope.webSocket !== null ) {
45 |
46 | // disconnect
47 |
48 | }
49 |
50 | url = url.replace( 'http', 'ws' );
51 |
52 | scope.webSocket = new WebSocket( url );
53 |
54 | scope.webSocket.addEventListener( 'open', onWebSocketOpen );
55 | scope.webSocket.addEventListener( 'message', onWebSocketMessage );
56 | scope.webSocket.addEventListener( 'close', onWebSocketClose );
57 | scope.webSocket.addEventListener( 'error', onWebSocketError );
58 |
59 | }
60 |
61 | function onWebSocketOpen() {
62 |
63 | scope.onConnected();
64 |
65 | }
66 |
67 | function onWebSocketMessage( event ) {
68 |
69 | scope.onMessage( event.data );
70 |
71 | }
72 |
73 | function onWebSocketClose() {
74 |
75 | scope.onDisconnected();
76 |
77 | }
78 |
79 | function onWebSocketError( error ) {
80 |
81 | console.error( 'WebSockt error! ' + error );
82 |
83 | }
84 |
85 | window.addEventListener( 'keydown', onKeyDown, false );
86 | window.addEventListener( 'keyup', onKeyUp, false );
87 | window.addEventListener( 'resize', onWindowResize, false );
88 |
89 | function onKeyDown( event ) {
90 |
91 | scope.setInputsFromKeyCode( event.keyCode, true );
92 |
93 | }
94 |
95 | function onKeyUp( event ) {
96 |
97 | scope.setInputsFromKeyCode( event.keyCode, false );
98 |
99 | }
100 |
101 | function onWindowResize() {
102 |
103 | scope.gameCanvasEl.width = window.innerWidth;
104 | scope.gameCanvasEl.height = window.innerHeight;
105 |
106 | scope.draw();
107 |
108 | }
109 |
110 | }
111 |
112 | Object.assign( Client.prototype, {
113 |
114 | setInputsFromKeyCode: function ( keyCode, value ) {
115 |
116 | switch ( keyCode ) {
117 |
118 | case 65:
119 |
120 | this.inputs.moveLeft = value;
121 |
122 | break;
123 |
124 | case 87:
125 |
126 | this.inputs.moveForward = value;
127 |
128 | break;
129 |
130 | case 68:
131 |
132 | this.inputs.moveRight = value;
133 |
134 | break;
135 |
136 | case 83:
137 |
138 | this.inputs.moveBackward = value;
139 |
140 | break;
141 |
142 | case 81:
143 |
144 | this.inputs.rotateLeft = value;
145 |
146 | break;
147 |
148 | case 69:
149 |
150 | this.inputs.rotateRight = value;
151 |
152 | break;
153 |
154 | }
155 |
156 | },
157 |
158 | sendMessage: function ( message ) {
159 |
160 | if ( this.webSocket && this.webSocket.readyState === WebSocket.OPEN ) {
161 |
162 | this.webSocket.send( message );
163 |
164 | }
165 |
166 | },
167 |
168 | onConnected: function () {
169 |
170 | console.log( 'connected!' );
171 |
172 | },
173 |
174 | onMessage: function ( data ) {
175 |
176 | this.messages.push( data );
177 |
178 | },
179 |
180 | onDisconnected: function () {
181 |
182 | console.log( 'disconnected.' );
183 |
184 | },
185 |
186 | update: function () {
187 |
188 | this.currentTime = Date.now();
189 |
190 | if ( this.lastTime === null ) {
191 |
192 | this.lastTime = this.currentTime;
193 |
194 | return;
195 |
196 | }
197 |
198 | this.deltaTime = this.currentTime - this.lastTime;
199 | this.lastTime = this.currentTime;
200 |
201 | if ( ! this.webSocket || this.webSocket.readyState !== WebSocket.OPEN ) {
202 |
203 | console.log( 'not connected, skipping' );
204 |
205 | return;
206 |
207 | }
208 |
209 | this.inputs.deltaTime = this.deltaTime;
210 |
211 | if ( this.speedHackEl.checked ) {
212 |
213 | this.inputs.deltaTime *= 4;
214 |
215 | }
216 |
217 | const inputsClone = this.inputs.clone();
218 | this.inputsArray.push( inputsClone );
219 |
220 | this.sendMessage( JSON.stringify( {
221 | id: 'inputs',
222 | tickNumber: this.tickNumber,
223 | inputsArray: this.inputsArray
224 | } ) );
225 |
226 | this.inputsArray.length = 0;
227 |
228 | this.history[ this.tickNumber % this.historySize ] = {
229 | x: this.player.x,
230 | y: this.player.y,
231 | velX: this.player.velX,
232 | velY: this.player.velY,
233 | rotation: this.player.rotation,
234 | inputs: inputsClone
235 | };
236 |
237 | this.player.move( this.inputs );
238 |
239 | this.speedHackPlayer.move( this.inputs );
240 |
241 | while ( this.messages.length > 0 ) {
242 |
243 | const message = JSON.parse( this.messages.shift() );
244 |
245 | switch ( message.id ) {
246 |
247 | case 'update':
248 |
249 | const serverState = message.data;
250 |
251 | this.interpolatedPlayer.setNewState( serverState.x, serverState.y, serverState.rotation, this.currentTime );
252 |
253 | this.serverPlayer.x = serverState.x;
254 | this.serverPlayer.y = serverState.y;
255 | this.serverPlayer.rotation = serverState.rotation;
256 |
257 | let history = this.history[ message.data.tickNumber % this.historySize ];
258 |
259 | const error = Math.hypot( serverState.x - history.x, serverState.y - history.y ) + Math.abs( serverState.rotation - history.rotation );
260 |
261 | if ( error > 0.00001 ) {
262 |
263 | console.log( 'correcting' );
264 |
265 | this.player.x = serverState.x;
266 | this.player.y = serverState.y;
267 | this.player.velX = serverState.velX;
268 | this.player.velY = serverState.velY;
269 | this.player.rotation = serverState.rotation;
270 |
271 | let rewindTickNumber = serverState.tickNumber;
272 |
273 | while ( rewindTickNumber <= this.tickNumber ) {
274 |
275 | history = this.history[ rewindTickNumber % this.historySize ];
276 |
277 | history.x = this.player.x;
278 | history.y = this.player.y;
279 | history.velX = this.player.velX;
280 | history.velY = this.player.velY;
281 | history.rotation = this.player.rotation;
282 |
283 | this.player.move( history.inputs );
284 |
285 | rewindTickNumber ++;
286 |
287 | }
288 |
289 | }
290 |
291 | break;
292 |
293 | case 'worldUpdate':
294 |
295 | const players = [];
296 |
297 | while ( message.data.length > 0 ) {
298 |
299 | const data = message.data.shift();
300 |
301 | let player = this.otherPlayers.find( function ( player ) {
302 | return player.id === data.id;
303 | } );
304 |
305 | if ( player ) {
306 |
307 | player.setNewState( data.x, data.y, data.rotation, this.currentTime );
308 |
309 | } else {
310 |
311 | player = new InterpolatedPlayer();
312 | player.id = data.id;
313 | player.setNewState( data.x, data.y, data.rotation, 0 );
314 |
315 | }
316 |
317 | players.push( player );
318 |
319 | }
320 |
321 | this.otherPlayers = players;
322 |
323 | break;
324 |
325 | }
326 |
327 | }
328 |
329 | this.tickNumber ++;
330 |
331 | this.interpolatedPlayer.update( this.currentTime, 200 );
332 |
333 | for ( let i = 0; i < this.otherPlayers.length; i ++ ) {
334 |
335 | this.otherPlayers[ i ].update( this.currentTime, 200 );
336 |
337 | }
338 |
339 | const lastTrail = this.trails[ this.trails.length - 1 ];
340 |
341 | this.trails.push( {
342 | x: this.player.x,
343 | y: this.player.y
344 | } );
345 |
346 | if ( this.trails.length > 10 ) {
347 |
348 | this.trails.shift();
349 |
350 | }
351 |
352 | },
353 |
354 | drawPlayer: function ( player, hue, arrowColor ) {
355 |
356 | this.context.fillStyle = 'hsl(' + hue + ', 100%, 60%)';
357 | this.context.strokeStyle = 'hsl(' + hue + ', 100%, 40%)';
358 |
359 | this.context.lineWidth = 6;
360 | this.context.lineCap = 'round';
361 | this.context.lineJoin = 'round';
362 |
363 | this.context.beginPath();
364 | this.context.arc( player.x, player.y, player.radius, 0, Math.PI * 2 );
365 | this.context.closePath();
366 |
367 | this.context.fill();
368 |
369 | this.context.beginPath();
370 | this.context.arc( player.x, player.y, player.radius + this.context.lineWidth / 2, 0, Math.PI * 2 );
371 | this.context.closePath();
372 |
373 | this.context.stroke();
374 |
375 | this.context.strokeStyle = arrowColor;
376 | this.context.lineWidth = 8;
377 |
378 | const arrowSize = ( Math.sin( Date.now() / 100 ) * 0.5 + 0.5 ) * 10 + 50;
379 |
380 | this.context.save();
381 |
382 | this.context.translate( player.x, player.y );
383 | this.context.rotate( player.rotation );
384 |
385 | this.context.translate( player.radius + 15, 0 );
386 |
387 | this.context.beginPath();
388 |
389 | this.context.moveTo( 0, 0 );
390 | this.context.lineTo( arrowSize, 0 );
391 | this.context.moveTo( arrowSize * 0.80, - 10 );
392 | this.context.lineTo( arrowSize, 0 );
393 | this.context.lineTo( arrowSize * 0.80, 10 );
394 |
395 | this.context.stroke();
396 |
397 | this.context.restore();
398 |
399 | },
400 |
401 | draw: function () {
402 |
403 | this.context.globalAlpha = 1;
404 |
405 | this.context.fillStyle = '#d4d4d4';
406 |
407 | this.context.fillRect( 0, 0, this.gameCanvasEl.width, this.gameCanvasEl.height );
408 |
409 | this.context.save();
410 |
411 | this.context.translate( this.gameCanvasEl.width / 2, this.gameCanvasEl.height / 2 );
412 |
413 | if ( ! this.webSocket || this.webSocket.readyState !== WebSocket.OPEN ) {
414 |
415 | this.context.font = 'bolder 50px arial';
416 | this.context.textBaseline = 'middle';
417 | this.context.textAlign = 'center';
418 |
419 | this.context.fillStyle = '#fff';
420 | this.context.strokeStyle = '#222';
421 |
422 | this.context.lineJoin = 'round';
423 | this.context.lineCap = 'round';
424 |
425 | this.context.lineWidth = 6;
426 |
427 | if ( this.webSocket.readyState < WebSocket.OPEN ) {
428 |
429 | text = 'Connecting...';
430 |
431 | } else {
432 |
433 | text = 'Disconnected!';
434 |
435 | }
436 |
437 | this.context.strokeText( text, 0, 0 );
438 | this.context.fillText( text, 0, 0 );
439 |
440 | this.context.restore();
441 |
442 | return;
443 |
444 | }
445 |
446 | this.context.lineWidth = 6;
447 | this.context.strokeStyle = '#aaa';
448 |
449 | this.context.strokeRect(
450 | - this.player.areaSizeX / 2 - this.context.lineWidth / 2,
451 | - this.player.areaSizeY / 2 - this.context.lineWidth / 2,
452 | this.player.areaSizeX + this.context.lineWidth,
453 | this.player.areaSizeY + this.context.lineWidth
454 | );
455 |
456 | for ( let i = 0; i < this.otherPlayers.length; i ++ ) {
457 |
458 | this.drawPlayer( this.otherPlayers[ i ], 150, '#999' );
459 |
460 | }
461 |
462 | this.context.globalAlpha = 0.5;
463 |
464 | if ( this.interpolatedStateEl.checked ) {
465 |
466 | this.drawPlayer( this.interpolatedPlayer, 0, '#333' );
467 |
468 | }
469 |
470 | if ( this.serverStateEl.checked ) {
471 |
472 | this.drawPlayer( this.serverPlayer, 200, '#333' );
473 |
474 | }
475 |
476 | if ( this.speedHackEl.checked ) {
477 |
478 | this.drawPlayer( this.speedHackPlayer, 30, '#333' );
479 |
480 | }
481 |
482 | if ( this.predictedStateEl.checked ) {
483 |
484 | this.context.globalAlpha = 0.5;
485 |
486 | this.context.strokeStyle = 'white';
487 | this.context.lineWidth = this.player.radius * 2;
488 |
489 | this.context.beginPath();
490 |
491 | for ( let i = 0; i < this.trails.length; i ++ ) {
492 |
493 | this.context.lineTo( this.trails[ i ].x, this.trails[ i ].y );
494 |
495 | }
496 |
497 | this.context.stroke();
498 |
499 | this.context.closePath();
500 |
501 | this.context.globalAlpha = 1;
502 |
503 | this.drawPlayer( this.player, 80, '#333' );
504 |
505 | }
506 |
507 | this.context.restore();
508 |
509 | }
510 |
511 | } );
--------------------------------------------------------------------------------
/src/client/InterpolatedPlayer.js:
--------------------------------------------------------------------------------
1 | function InterpolatedPlayer() {
2 |
3 | Player.call( this );
4 |
5 | this.oldX = this.newX = this.x;
6 | this.oldY = this.newY = this.y;
7 | this.oldRotation = this.newRotation = this.rotation;
8 | this.updateTime = 0;
9 |
10 | }
11 |
12 | Object.assign( InterpolatedPlayer.prototype, {
13 |
14 | setNewState: function ( x, y, rotation, time ) {
15 |
16 | this.newX = x;
17 | this.newY = y;
18 | this.newRotation = rotation;
19 |
20 | this.oldX = this.x;
21 | this.oldY = this.y;
22 | this.oldRotation = this.rotation;
23 |
24 | this.updateTime = time;
25 |
26 | },
27 |
28 | update: function ( currentTime, period ) {
29 |
30 | const t = Math.min( ( currentTime - this.updateTime ) / period, 1 );
31 |
32 | this.x = this.oldX + ( this.newX - this.oldX ) * t;
33 | this.y = this.oldY + ( this.newY - this.oldY ) * t;
34 | this.rotation = this.oldRotation + ( this.newRotation - this.oldRotation ) * t;
35 |
36 | }
37 |
38 | } );
--------------------------------------------------------------------------------
/src/server/Client.js:
--------------------------------------------------------------------------------
1 | function Client( socket ) {
2 |
3 | this.socket = socket;
4 |
5 | this.id = - 1;
6 |
7 | this.isAlive = true;
8 |
9 | this.messages = [];
10 |
11 | }
12 |
13 | Object.assign( Client.prototype, {
14 |
15 | sendMessage: function ( message ) {
16 |
17 | if ( this.socket && this.socket.readyState === this.socket.OPEN ) {
18 |
19 | this.socket.send( message );
20 |
21 | }
22 |
23 | }
24 |
25 | } );
26 |
27 | module.exports = Client;
--------------------------------------------------------------------------------
/src/server/Server.js:
--------------------------------------------------------------------------------
1 | const Player = require( '../shared/Player.js' );
2 | const Inputs = require( '../shared/Inputs.js' );
3 |
4 | const Client = require( './Client.js' );
5 |
6 | const WebSocket = require( 'ws' );
7 | const url = require( 'url' );
8 |
9 | function Server() {
10 |
11 | const scope = this;
12 |
13 | this.webSocketServer = null;
14 |
15 | this.allowedHostnames = [ 'localhost', 'prediction-side-client.herokuapp.com' ];
16 |
17 | this.clients = [];
18 |
19 | this.nextClientId = 0;
20 |
21 | this.currentTime = null;
22 | this.lastTime = null;
23 |
24 | this.start = function ( server ) {
25 |
26 | if ( scope.webSocketServer !== null ) {
27 |
28 | // idk maybe dispose that shit?
29 |
30 | }
31 |
32 | scope.webSocketServer = new WebSocket.Server( {
33 | server: server,
34 | perMessageDeflate: false
35 | } );
36 |
37 | const shouldHandle = scope.webSocketServer.shouldHandle;
38 |
39 | scope.webSocketServer.shouldHandle = function ( request ) {
40 |
41 | const hostname = url.parse( request.headers.origin ).hostname;
42 |
43 | if ( scope.allowedHostnames.indexOf( hostname ) === - 1 ) {
44 |
45 | return false;
46 |
47 | }
48 |
49 | return shouldHandle.call( scope.webSocketServer, request );
50 |
51 | }
52 |
53 | scope.webSocketServer.on( 'connection', function ( socket ) {
54 |
55 | const client = new Client( socket );
56 |
57 | scope.onClientConnected( client );
58 |
59 | socket.on( 'message', function ( message ) {
60 |
61 | scope.onClientMessage( client, message );
62 |
63 | } );
64 |
65 | socket.on( 'close', function () {
66 |
67 | scope.onClientDisconnected( client );
68 |
69 | } );
70 |
71 | } );
72 |
73 | }
74 |
75 | }
76 |
77 | Object.assign( Server.prototype, {
78 |
79 | onClientConnected: function ( client ) {
80 |
81 | this.clients.push( client );
82 |
83 | },
84 |
85 | onClientMessage: function ( client, message ) {
86 |
87 | client.messages.push( message );
88 |
89 | },
90 |
91 | onClientDisconnected: function ( client ) {
92 |
93 | client.isAlive = false;
94 |
95 | },
96 |
97 | update: function () {
98 |
99 | this.currentTime = Date.now();
100 |
101 | for ( let i = this.clients.length - 1; i >= 0; i -- ) {
102 |
103 | const client = this.clients[ i ];
104 |
105 | if ( client.isAlive === false ) {
106 |
107 | this.clients.splice( i, 1 );
108 |
109 | continue;
110 |
111 | }
112 |
113 | if ( client.id === - 1 ) {
114 |
115 | client.id = this.nextClientId ++;
116 |
117 | client.player = new Player();
118 | client.inputs = new Inputs();
119 |
120 | client.time = this.currentTime;
121 |
122 | }
123 |
124 | while ( client.messages.length > 0 ) {
125 |
126 | const message = JSON.parse( client.messages.shift() );
127 |
128 | switch ( message.id ) {
129 |
130 | case 'inputs':
131 |
132 | while ( message.inputsArray.length > 0 ) {
133 |
134 | const inputs = message.inputsArray.shift();
135 |
136 | if ( client.time + inputs.deltaTime > this.currentTime ) {
137 |
138 | inputs.deltaTime = this.currentTime - client.time;
139 |
140 | }
141 |
142 | client.time += inputs.deltaTime;
143 |
144 | client.player.move( inputs );
145 |
146 | }
147 |
148 | client.sendMessage( JSON.stringify( {
149 | id: 'update',
150 | data: {
151 | x: client.player.x,
152 | y: client.player.y,
153 | velX: client.player.velX,
154 | velY: client.player.velY,
155 | rotation: client.player.rotation,
156 | tickNumber: message.tickNumber + 1
157 | }
158 | } ) );
159 |
160 | break;
161 |
162 | }
163 |
164 | }
165 |
166 | }
167 |
168 | for ( let i = 0; i < this.clients.length; i ++ ) {
169 |
170 | const client = this.clients[ i ];
171 |
172 | const worldUpdateMessage = {
173 | id: 'worldUpdate',
174 | data: []
175 | };
176 |
177 | for ( let j = 0; j < this.clients.length; j ++ ) {
178 |
179 | if ( i === j ) continue;
180 |
181 | const other = this.clients[ j ];
182 |
183 | worldUpdateMessage.data.push( {
184 | id: other.id,
185 | x: other.player.x,
186 | y: other.player.y,
187 | rotation: other.player.rotation
188 | } );
189 |
190 | }
191 |
192 | client.sendMessage( JSON.stringify( worldUpdateMessage ) );
193 |
194 | }
195 |
196 | }
197 |
198 | } );
199 |
200 | module.exports = Server;
--------------------------------------------------------------------------------
/src/shared/Inputs.js:
--------------------------------------------------------------------------------
1 | function Inputs() {
2 |
3 | this.deltaTime = 0;
4 | this.moveLeft = false;
5 | this.moveRight = false;
6 | this.moveForward = false;
7 | this.moveBackward = false;
8 | this.rotateLeft = false;
9 | this.rotateRight = false;
10 |
11 | }
12 |
13 | Object.assign( Inputs.prototype, {
14 |
15 | clone: function () {
16 |
17 | let object = new this.constructor();
18 |
19 | object.deltaTime = this.deltaTime;
20 | object.moveLeft = this.moveLeft;
21 | object.moveRight = this.moveRight;
22 | object.moveForward = this.moveForward;
23 | object.moveBackward = this.moveBackward;
24 | object.rotateLeft = this.rotateLeft;
25 | object.rotateRight = this.rotateRight;
26 |
27 | return object;
28 |
29 | }
30 |
31 | } );
32 |
33 | if ( typeof module === 'object' ) {
34 |
35 | module.exports = Inputs;
36 |
37 | }
--------------------------------------------------------------------------------
/src/shared/Player.js:
--------------------------------------------------------------------------------
1 | function Player() {
2 |
3 | this.x = 0;
4 | this.y = 0;
5 | this.velX = 0;
6 | this.velY = 0;
7 | this.rotation = 0;
8 | this.radius = 34;
9 | this.angularVelocity = 0;
10 | this.areaSizeX = 700;
11 | this.areaSizeY = 500;
12 |
13 | }
14 |
15 | Object.assign( Player.prototype, {
16 |
17 | move: function ( inputs ) {
18 |
19 | const deltaTime = inputs.deltaTime / 1000;
20 |
21 | const maxAngularVelocity = 6;
22 | const theta = Math.PI * 2 * deltaTime;
23 |
24 |
25 | if ( inputs.rotateLeft ) {
26 |
27 | this.angularVelocity -= theta;
28 |
29 | } else if ( inputs.rotateRight ) {
30 |
31 | this.angularVelocity += theta;
32 |
33 | }
34 |
35 | const angularFriction = Math.PI / 180 * 180;
36 |
37 | const currentAngularSpeed = Math.abs( this.angularVelocity );
38 |
39 | this.angularVelocity = Math.sign( this.angularVelocity ) * Math.max( 0, currentAngularSpeed - angularFriction * deltaTime );
40 |
41 | this.rotation += this.angularVelocity * deltaTime;
42 |
43 | let dirX = 0;
44 | let dirY = 0;
45 |
46 | const sin = Math.sin( this.rotation );
47 | const cos = Math.cos( this.rotation );
48 |
49 | if ( inputs.moveForward ) {
50 |
51 | dirX += cos;
52 | dirY += sin;
53 |
54 | } else if ( inputs.moveBackward ) {
55 |
56 | dirX -= cos;
57 | dirY -= sin;
58 |
59 | }
60 |
61 | if ( inputs.moveRight ) {
62 |
63 | dirX += - sin;
64 | dirY += cos;
65 |
66 | } else if ( inputs.moveLeft ) {
67 |
68 | dirX -= - sin;
69 | dirY -= cos;
70 |
71 | }
72 |
73 | const acceleration = 400;
74 | const maxSpeed = 500;
75 | const friction = 50;
76 |
77 | const currentSpeed = Math.hypot( this.velX, this.velY );
78 |
79 | if ( currentSpeed > 0 ) {
80 |
81 | let amount = currentSpeed - friction * deltaTime;
82 |
83 | if ( amount < 0 ) {
84 |
85 | amount = 0;
86 |
87 | }
88 |
89 | const factor = amount / currentSpeed;
90 |
91 | this.velX *= factor;
92 | this.velY *= factor;
93 |
94 | }
95 |
96 | let amount = acceleration * deltaTime;
97 |
98 | if ( currentSpeed + amount > maxSpeed ) {
99 |
100 | amount = maxSpeed - currentSpeed;
101 |
102 | }
103 |
104 | this.velX += amount * dirX;
105 | this.velY += amount * dirY;
106 |
107 | this.x += this.velX * deltaTime;
108 | this.y += this.velY * deltaTime;
109 |
110 | const sx = this.areaSizeX / 2 - this.radius;
111 | const sy = this.areaSizeY / 2 - this.radius;
112 |
113 | if ( this.x < - sx ) {
114 |
115 | this.velX *= - 0.75;
116 | this.x = - sx;
117 |
118 | } else if ( this.x > sx ) {
119 |
120 | this.velX *= - 0.75;
121 | this.x = sx;
122 |
123 | }
124 |
125 | if ( this.y < - sy ) {
126 |
127 | this.velY *= - 0.75;
128 | this.y = - sy;
129 |
130 | } else if ( this.y > sy ) {
131 |
132 | this.velY *= - 0.75;
133 | this.y = sy;
134 |
135 | }
136 |
137 | }
138 |
139 | } );
140 |
141 | if ( typeof module === 'object' ) {
142 |
143 | module.exports = Player;
144 |
145 | }
--------------------------------------------------------------------------------