-
-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 40f0041
Showing
17 changed files
with
12,208 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
coverage | ||
lib | ||
node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
coverage | ||
example | ||
node_modules | ||
src | ||
test | ||
.gitignore | ||
package-lock.json | ||
tsconfig.json | ||
webpack.config.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2020 Yannick Deubel (https://github.com/yandeu); Project Url: https://github.com/geckosio/snapshot-interpolation | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
# Snapshot Interpolation | ||
|
||
## A Snapshot Interpolation library for Real-Time Multiplayer Games | ||
|
||
Also called Entity Interpolation. | ||
|
||
The Interpolation Buffer is by default "latency + 3 serverFrames" long (Interpolation between 4 Snapshots). | ||
So if the **latency is 30ms** and the **ServerFrame is 16ms**, the Interpolation Buffer would be 78ms long. | ||
See [this video](https://youtu.be/Z9X4lysFr64?t=800). | ||
|
||
Easily includes Client-Side Prediction and **Server Reconciliation** using the Snapshot vault. | ||
|
||
Easily includes Lag Compensation | ||
|
||
Does not compress/encode the data. But you can easily do it yourself. | ||
|
||
## Example | ||
|
||
The [github repository](https://github.com/geckosio/snapshot-interpolation) contains a nice example: | ||
|
||
```bash | ||
$ git clone https://github.com/geckosio/snapshot-interpolation.git | ||
|
||
$ cd snapshot-interpolation | ||
|
||
$ npm install | ||
|
||
$ npm start | ||
``` | ||
|
||
## Usage | ||
|
||
#### server.js | ||
|
||
```js | ||
// initialize the library (add your server's fps in milliseconds) | ||
const SI = new SnapshotInterpolation(serverFPS) | ||
|
||
// your server update loop | ||
update() { | ||
// create a snapshot of the current world | ||
const snapshot = SI.snapshot.create(worldState) | ||
|
||
// add the snapshot to the vault in case you want to access it later (optional) | ||
SI.vault.add(snapshot) | ||
|
||
// send the snapshot to the client (using geckos.io or any other library) | ||
this.emit('update', snapshot) | ||
} | ||
``` | ||
|
||
#### client.js | ||
|
||
```js | ||
// initialize the library | ||
const SI = new SnapshotInterpolation() | ||
|
||
// when receiving the snapshot on the client | ||
this.on('update', (snapshot) => { | ||
// read the snapshot | ||
SI.snapshot.add(snapshot) | ||
} | ||
|
||
// your client update loop | ||
update() { | ||
// calculate the interpolation for the parameters x and y and return the snapshot | ||
const snapshot = SI.calcInterpolation('x y') | ||
|
||
// access your state in snapshot.state. | ||
const { state } = snapshot | ||
|
||
// apply the interpolated values to you game objects | ||
const { id, x, y } = state[0] | ||
if (hero.id === id) { | ||
hero.x = x | ||
hero.y = y | ||
} | ||
} | ||
``` | ||
## World State | ||
The World State has to be an Array with non nested Objects. | ||
```js | ||
const worldState = [ | ||
{ id: 'heroBlue', x: 23, y: 14, z: 47 }, | ||
{ id: 'heroRed', x: 23, y: 14, z: 47 }, | ||
{ id: 'heroGreen', x: 23, y: 14, z: 47 }, | ||
] | ||
``` | ||
## Compression | ||
You can compress the snapshots manually before sending them to the client, and decompress them when when the client receives them. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
import { SnapshotInterpolation, Vault } from '../../lib/index' | ||
import geckos from '@geckos.io/client' | ||
import { addLatencyAndPackagesLoss } from '../common' | ||
|
||
const channel = geckos() | ||
const SI = new SnapshotInterpolation() | ||
const playerVault = new Vault() | ||
const players = new Map() | ||
|
||
// set a interpolation buffer of 250ms | ||
SI.interpolationBuffer.set(250) | ||
|
||
const body = document.body | ||
body.style.padding = 0 | ||
body.style.margin = 0 | ||
body.style.overflow = 'hidden' | ||
|
||
const canvas = document.createElement('canvas') | ||
canvas.style.width = `${window.innerWidth}px` | ||
canvas.style.height = `${window.innerHeight}px` | ||
canvas.width = window.innerWidth | ||
canvas.height = window.innerHeight | ||
body.appendChild(canvas) | ||
const ctx = canvas.getContext('2d') | ||
|
||
let connected = false | ||
|
||
window.isBot = false | ||
let tick = 0 | ||
|
||
let keys = { | ||
left: false, | ||
up: false, | ||
right: false, | ||
down: false, | ||
} | ||
|
||
channel.onConnect(error => { | ||
if (error) { | ||
console.error(error.message) | ||
return | ||
} else { | ||
console.log('You are connected!') | ||
} | ||
|
||
connected = true | ||
|
||
channel.on('update', snapshot => { | ||
addLatencyAndPackagesLoss(() => { | ||
SI.snapshot.add(snapshot) | ||
}) | ||
}) | ||
|
||
channel.on('hit', entity => { | ||
addLatencyAndPackagesLoss(() => { | ||
console.log('You just hit ', entity) | ||
}) | ||
}) | ||
}) | ||
|
||
const render = () => { | ||
ctx.clearRect(0, 0, canvas.width, canvas.height) | ||
|
||
players.forEach(p => { | ||
ctx.beginPath() | ||
ctx.fillStyle = 'red' | ||
ctx.rect(p.x, p.y, 25, 40) | ||
ctx.fill() | ||
}) | ||
} | ||
|
||
const serverReconciliation = () => { | ||
const { left, up, right, down } = keys | ||
const player = players.get(channel.id) | ||
|
||
if (player) { | ||
// get the latest snapshot from the server | ||
const serverSnapshot = SI.vault.get() | ||
// get the closest player snapshot that matches the server snapshot time | ||
const playerSnapshot = playerVault.get(serverSnapshot.time, true) | ||
|
||
if (serverSnapshot && playerSnapshot) { | ||
// get the current player position on the server | ||
const serverPos = serverSnapshot.state.filter( | ||
s => s.playerId === channel.id | ||
)[0] | ||
|
||
// calculate the offset between server and client | ||
const offsetX = playerSnapshot.state.x - serverPos.x | ||
const offsetY = playerSnapshot.state.y - serverPos.y | ||
|
||
// check if the player is currently on the move | ||
const isMoving = left || up || right || down | ||
|
||
// we correct the position faster if the player moves | ||
const correction = isMoving ? 60 : 180 | ||
|
||
// apply a step by step correction of the player's position | ||
player.x -= offsetX / correction | ||
player.y -= offsetY / correction | ||
} | ||
} | ||
} | ||
|
||
const clientPrediction = () => { | ||
const { left, up, right, down } = keys | ||
const speed = 3 | ||
const player = players.get(channel.id) | ||
|
||
if (player) { | ||
if (left) player.x -= speed | ||
if (up) player.y -= speed | ||
if (right) player.x += speed | ||
if (down) player.y += speed | ||
playerVault.add(SI.createSnapshot({ x: player.x, y: player.y })) | ||
} | ||
} | ||
|
||
const loop = () => { | ||
tick++ | ||
if (connected) { | ||
if (window.isBot) { | ||
if (Math.sin(tick / 40) > 0) | ||
keys = { left: false, up: false, right: true, down: false } | ||
else keys = { left: true, up: false, right: false, down: false } | ||
} | ||
|
||
const update = [keys.left, keys.up, keys.right, keys.down] | ||
channel.emit('move', update) | ||
} | ||
|
||
clientPrediction() | ||
serverReconciliation() | ||
|
||
const snapshot = SI.calcInterpolation('x y') // interpolated | ||
// const snapshot = SI.vault.get() // latest | ||
if (snapshot) { | ||
const { state } = snapshot | ||
state.forEach(s => { | ||
const { playerId, x, y } = s | ||
// update player | ||
if (players.has(playerId)) { | ||
// do not update our own player (if we use clientPrediction and serverReconciliation) | ||
if (playerId === channel.id) return | ||
|
||
const player = players.get(playerId) | ||
player.x = x | ||
player.y = y | ||
} else { | ||
// add new player | ||
players.set(playerId, { x, y }) | ||
} | ||
}) | ||
} | ||
|
||
render() | ||
|
||
requestAnimationFrame(loop) | ||
} | ||
|
||
loop() | ||
|
||
canvas.addEventListener('pointerdown', e => { | ||
const { clientX, clientY } = e | ||
if (connected) | ||
channel.emit('shoot', { x: clientX, y: clientY, time: SI.serverTime }) | ||
}) | ||
|
||
document.addEventListener('keydown', e => { | ||
const { keyCode } = e | ||
switch (keyCode) { | ||
case 37: | ||
keys.left = true | ||
break | ||
case 38: | ||
keys.up = true | ||
break | ||
case 39: | ||
keys.right = true | ||
break | ||
case 40: | ||
keys.down = true | ||
break | ||
} | ||
}) | ||
|
||
document.addEventListener('keyup', e => { | ||
const { keyCode } = e | ||
switch (keyCode) { | ||
case 37: | ||
keys.left = false | ||
break | ||
case 38: | ||
keys.up = false | ||
break | ||
case 39: | ||
keys.right = false | ||
break | ||
case 40: | ||
keys.down = false | ||
break | ||
} | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
const addLatencyAndPackagesLoss = fnc => { | ||
if (Math.random() > 0.9) return // 10% package loss | ||
setTimeout(() => fnc(), 100 + Math.random() * 50) // random latency between 100 and 150 | ||
} | ||
|
||
exports.addLatencyAndPackagesLoss = addLatencyAndPackagesLoss |
Oops, something went wrong.