Skip to content

Commit

Permalink
.
Browse files Browse the repository at this point in the history
  • Loading branch information
yandeu committed Jun 2, 2020
0 parents commit 40f0041
Show file tree
Hide file tree
Showing 17 changed files with 12,208 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
coverage
lib
node_modules
9 changes: 9 additions & 0 deletions .npmignore
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
21 changes: 21 additions & 0 deletions LICENSE
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.
95 changes: 95 additions & 0 deletions README.md
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.
203 changes: 203 additions & 0 deletions example/client/index.js
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
}
})
6 changes: 6 additions & 0 deletions example/common.js
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
Loading

0 comments on commit 40f0041

Please sign in to comment.