原文:How to Build a Multiplayer (.io) Web Game, Part 1
GitHub: https://github.com/vzhou842/example-.io-game
深入探索一個(gè) .io
游戲的 Javascript client-side(客戶端)谭胚。
如果您以前從未聽說過 .io
游戲:它們是免費(fèi)的多人 web 游戲,易于加入(無需帳戶),
并且通常在一個(gè)區(qū)域內(nèi)讓許多玩家相互競爭。其他著名的 .io
游戲包括 Slither.io
和 Diep.io
腹缩。
- Slither.io:http://slither.io
- Diep.io:https://diep.io
在本文中,我們將了解如何從頭開始構(gòu)建.io游戲空扎。
您所需要的只是 Javascript 的實(shí)用知識:
您應(yīng)該熟悉 ES6
語法庆聘,this
關(guān)鍵字和 Promises
之類的內(nèi)容。
即使您對 Javascript 并不是最熟悉的勺卢,您仍然應(yīng)該可以閱讀本文的大部分內(nèi)容。
一個(gè) .io
游戲示例
為了幫助我們學(xué)習(xí)象对,我們將參考 https://example-io-game.victorzhou.com黑忱。
這是一款非常簡單的游戲:你和其他玩家一起控制競技場中的一艘船。
你的飛船會(huì)自動(dòng)發(fā)射子彈勒魔,你會(huì)試圖用自己的子彈擊中其他玩家甫煞,同時(shí)避開他們。
目錄
這是由兩部分組成的系列文章的第 1 部分冠绢。我們將在這篇文章中介紹以下內(nèi)容:
- 項(xiàng)目概況/結(jié)構(gòu):項(xiàng)目的高級視圖抚吠。
- 構(gòu)建/項(xiàng)目設(shè)置:開發(fā)工具、配置和設(shè)置弟胀。
- Client 入口:index.html 和 index.js楷力。
- Client 網(wǎng)絡(luò)通信:與服務(wù)器通信。
- Client 渲染:下載 image 資源 + 渲染游戲孵户。
- Client 輸入:讓用戶真正玩游戲萧朝。
- Client 狀態(tài):處理來自服務(wù)器的游戲更新。
1. 項(xiàng)目概況/結(jié)構(gòu)
我建議下載示例游戲的源代碼夏哭,以便您可以更好的繼續(xù)閱讀检柬。
我們的示例游戲使用了:
- Express,Node.js 最受歡迎的 Web 框架竖配,以為其 Web 服務(wù)器提供動(dòng)力何址。
- socket.io,一個(gè) websocket 庫进胯,用于在瀏覽器和服務(wù)器之間進(jìn)行通信用爪。
- Webpack,一個(gè)模塊打包器胁镐。
項(xiàng)目目錄的結(jié)構(gòu)如下所示:
public/
assets/
...
src/
client/
css/
...
html/
index.html
index.js
...
server/
server.js
...
shared/
constants.js
public/
我們的服務(wù)器將靜態(tài)服務(wù) public/
文件夾中的所有內(nèi)容项钮。 public/assets/
包含我們項(xiàng)目使用的圖片資源。
src/
所有源代碼都在 src/
文件夾中。
client/
和 server/
很容易說明烁巫,shared/
包含一個(gè)由 client 和 server 導(dǎo)入的常量文件署隘。
2. 構(gòu)建/項(xiàng)目設(shè)置
如前所述,我們正在使用 Webpack 模塊打包器來構(gòu)建我們的項(xiàng)目亚隙。讓我們看一下我們的 Webpack 配置:
webpack.common.js
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: {
game: './src/client/index.js',
},
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ['@babel/preset-env'],
},
},
},
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
'css-loader',
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'src/client/html/index.html',
}),
],
};
-
src/client/index.js
是 Javascript (JS) 客戶端入口點(diǎn)磁餐。Webpack 將從那里開始,遞歸地查找其他導(dǎo)入的文件阿弃。 - 我們的 Webpack 構(gòu)建的 JS 輸出將放置在
dist/
目錄中诊霹。我將此文件稱為 JS bundle。 - 我們正在使用 Babel渣淳,特別是
@babel/preset-env
配置脾还,來為舊瀏覽器編譯 JS 代碼。 - 我們正在使用一個(gè)插件來提取 JS 文件引用的所有 CSS 并將其捆綁在一起入愧。我將其稱為 CSS bundle鄙漏。
您可能已經(jīng)注意到奇怪的 '[name].[contenthash].ext'
捆綁文件名。
它們包括 Webpack 文件名替換:[name]
將替換為入口點(diǎn)名稱(這是game
)棺蛛,[contenthash]將替換為文件內(nèi)容的哈希怔蚌。
我們這樣做是為了優(yōu)化緩存 - 我們可以告訴瀏覽器永遠(yuǎn)緩存我們的 JS bundle,因?yàn)槿绻?JS bundle 更改旁赊,其文件名也將更改(contenthash
也會(huì)更改)桦踊。最終結(jié)果是一個(gè)文件名,例如:game.dbeee76e91a97d0c7207.js
终畅。
webpack.common.js
文件是我們在開發(fā)和生產(chǎn)配置中導(dǎo)入的基本配置文件籍胯。例如,下面是開發(fā)配置:
webpack.dev.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'development',
});
我們在開發(fā)過程中使用 webpack.dev.js
來提高效率离福,并在部署到生產(chǎn)環(huán)境時(shí)切換到 webpack.prod.js
來優(yōu)化包的大小芒炼。
本地設(shè)置
我建議在您的本地計(jì)算機(jī)上安裝該項(xiàng)目,以便您可以按照本文的其余內(nèi)容進(jìn)行操作术徊。
設(shè)置很簡單:首先本刽,確保已安裝 Node
和 NPM
。 然后赠涮,
$ git clone https://github.com/vzhou842/example-.io-game.git
$ cd example-.io-game
$ npm install
您就可以出發(fā)了子寓! 要運(yùn)行開發(fā)服務(wù)器,只需
$ npm run develop
并在網(wǎng)絡(luò)瀏覽器中訪問 localhost:3000
笋除。
當(dāng)您編輯代碼時(shí)斜友,開發(fā)服務(wù)器將自動(dòng)重建 JS 和 CSS bundles - 只需刷新即可查看更改!
3. Client 入口
讓我們來看看實(shí)際的游戲代碼垃它。首先鲜屏,我們需要一個(gè) index.html
頁面烹看,
這是您的瀏覽器訪問網(wǎng)站時(shí)首先加載的內(nèi)容。我們的將非常簡單:
index.html
<!DOCTYPE html>
<html>
<head>
<title>An example .io game</title>
<link type="text/css" rel="stylesheet" href="/game.bundle.css">
</head>
<body>
<canvas id="game-canvas"></canvas>
<script async src="/game.bundle.js"></script>
<div id="play-menu" class="hidden">
<input type="text" id="username-input" placeholder="Username" />
<button id="play-button">PLAY</button>
</div>
</body>
</html>
我們有:
- 我們將使用 HTML5 Canvas(
<canvas>
)元素來渲染游戲洛史。 -
<link>
包含我們的 CSS bundle惯殊。 -
<script>
包含我們的 Javascript bundle。 - 主菜單也殖,帶有用戶名
<input>
和“PLAY”
<button>
土思。
一旦主頁加載到瀏覽器中,我們的 Javascript 代碼就會(huì)開始執(zhí)行忆嗜,
從我們的 JS 入口文件 src/client/index.js
開始己儒。
index.js
import { connect, play } from './networking';
import { startRendering, stopRendering } from './render';
import { startCapturingInput, stopCapturingInput } from './input';
import { downloadAssets } from './assets';
import { initState } from './state';
import { setLeaderboardHidden } from './leaderboard';
import './css/main.css';
const playMenu = document.getElementById('play-menu');
const playButton = document.getElementById('play-button');
const usernameInput = document.getElementById('username-input');
Promise.all([
connect(),
downloadAssets(),
]).then(() => {
playMenu.classList.remove('hidden');
usernameInput.focus();
playButton.onclick = () => {
// Play!
play(usernameInput.value);
playMenu.classList.add('hidden');
initState();
startCapturingInput();
startRendering();
setLeaderboardHidden(false);
};
});
這似乎很復(fù)雜,但實(shí)際上并沒有那么多事情發(fā)生:
- 導(dǎo)入一堆其他 JS 文件捆毫。
- 導(dǎo)入一些 CSS(因此 Webpack 知道將其包含在我們的 CSS bundle 中)闪湾。
- 運(yùn)行
connect()
來建立到服務(wù)器的連接,運(yùn)行downloadAssets()
來下載渲染游戲所需的圖像绩卤。 - 步驟 3 完成后途样,顯示主菜單(
playMenu
)。 - 為 “PLAY” 按鈕設(shè)置一個(gè)點(diǎn)擊處理程序省艳。如果點(diǎn)擊,初始化游戲并告訴服務(wù)器我們準(zhǔn)備好玩了嫁审。
客戶端邏輯的核心駐留在由 index.js
導(dǎo)入的其他文件中跋炕。接下來我們將逐一討論這些問題。
4. Client 網(wǎng)絡(luò)通信
對于此游戲律适,我們將使用眾所周知的 socket.io
庫與服務(wù)器進(jìn)行通信辐烂。
Socket.io 包含對 WebSocket
的內(nèi)置支持,
這非常適合雙向通訊:我們可以將消息發(fā)送到服務(wù)器捂贿,而服務(wù)器可以通過同一連接向我們發(fā)送消息纠修。
我們將有一個(gè)文件 src/client/networking.js
,它負(fù)責(zé)所有與服務(wù)器的通信:
networking.js
import io from 'socket.io-client';
import { processGameUpdate } from './state';
const Constants = require('../shared/constants');
const socket = io(`ws://${window.location.host}`);
const connectedPromise = new Promise(resolve => {
socket.on('connect', () => {
console.log('Connected to server!');
resolve();
});
});
export const connect = onGameOver => (
connectedPromise.then(() => {
// Register callbacks
socket.on(Constants.MSG_TYPES.GAME_UPDATE, processGameUpdate);
socket.on(Constants.MSG_TYPES.GAME_OVER, onGameOver);
})
);
export const play = username => {
socket.emit(Constants.MSG_TYPES.JOIN_GAME, username);
};
export const updateDirection = dir => {
socket.emit(Constants.MSG_TYPES.INPUT, dir);
};
此文件中發(fā)生3件主要事情:
- 我們嘗試連接到服務(wù)器厂僧。只有建立連接后扣草,
connectedPromise
才能解析。 - 如果連接成功颜屠,我們注冊回調(diào)(
processGameUpdate()
和onGameOver()
)我們可能從服務(wù)器接收到的消息辰妙。 - 我們導(dǎo)出
play()
和updateDirection()
以供其他文件使用。
5. Client 渲染
是時(shí)候讓東西出現(xiàn)在屏幕上了!
但在此之前甫窟,我們必須下載所需的所有圖像(資源)密浑。讓我們寫一個(gè)資源管理器:
assets.js
const ASSET_NAMES = ['ship.svg', 'bullet.svg'];
const assets = {};
const downloadPromise = Promise.all(ASSET_NAMES.map(downloadAsset));
function downloadAsset(assetName) {
return new Promise(resolve => {
const asset = new Image();
asset.onload = () => {
console.log(`Downloaded ${assetName}`);
assets[assetName] = asset;
resolve();
};
asset.src = `/assets/${assetName}`;
});
}
export const downloadAssets = () => downloadPromise;
export const getAsset = assetName => assets[assetName];
管理 assets 并不難實(shí)現(xiàn)!主要思想是保留一個(gè) assets
對象粗井,它將文件名 key 映射到一個(gè) Image
對象值尔破。
當(dāng)一個(gè) asset
下載完成后街图,我們將其保存到 assets
對象中,以便以后檢索懒构。
最后餐济,一旦每個(gè) asset
下載都已 resolve(意味著所有 assets 都已下載),我們就 resolve downloadPromise
痴脾。
隨著資源的下載颤介,我們可以繼續(xù)進(jìn)行渲染。如前所述赞赖,我們正在使用 HTML5 畫布(<canvas>
)繪制到我們的網(wǎng)頁上滚朵。我們的游戲非常簡單,所以我們需要畫的是:
- 背景
- 我們玩家的飛船
- 游戲中的其他玩家
- 子彈
這是 src/client/render.js
的重要部分前域,它準(zhǔn)確地繪制了我上面列出的那四件事:
render.js
import { getAsset } from './assets';
import { getCurrentState } from './state';
const Constants = require('../shared/constants');
const { PLAYER_RADIUS, PLAYER_MAX_HP, BULLET_RADIUS, MAP_SIZE } = Constants;
// Get the canvas graphics context
const canvas = document.getElementById('game-canvas');
const context = canvas.getContext('2d');
// Make the canvas fullscreen
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
function render() {
const { me, others, bullets } = getCurrentState();
if (!me) {
return;
}
// Draw background
renderBackground(me.x, me.y);
// Draw all bullets
bullets.forEach(renderBullet.bind(null, me));
// Draw all players
renderPlayer(me, me);
others.forEach(renderPlayer.bind(null, me));
}
// ... Helper functions here excluded
let renderInterval = null;
export function startRendering() {
renderInterval = setInterval(render, 1000 / 60);
}
export function stopRendering() {
clearInterval(renderInterval);
}
render()
是該文件的主要函數(shù)辕近。startRendering()
和 stopRendering()
控制 60 FPS 渲染循環(huán)的激活。
各個(gè)渲染幫助函數(shù)(例如 renderBullet()
)的具體實(shí)現(xiàn)并不那么重要匿垄,但這是一個(gè)簡單的示例:
render.js
function renderBullet(me, bullet) {
const { x, y } = bullet;
context.drawImage(
getAsset('bullet.svg'),
canvas.width / 2 + x - me.x - BULLET_RADIUS,
canvas.height / 2 + y - me.y - BULLET_RADIUS,
BULLET_RADIUS * 2,
BULLET_RADIUS * 2,
);
}
請注意移宅,我們?nèi)绾问褂们懊嬖?asset.js
中看到的 getAsset()
方法!
如果你對其他渲染幫助函數(shù)感興趣椿疗,請閱讀 src/client/render.js
的其余部分漏峰。
6. Client 輸入???
現(xiàn)在該使游戲變得可玩了!我們的 control scheme 非常簡單:使用鼠標(biāo)(在桌面上)或觸摸屏幕(在移動(dòng)設(shè)備上)來控制移動(dòng)方向届榄。為此浅乔,我們將為 Mouse 和 Touch 事件注冊事件監(jiān)聽器。
src/client/input.js
會(huì)處理這些問題:
input.js
import { updateDirection } from './networking';
function onMouseInput(e) {
handleInput(e.clientX, e.clientY);
}
function onTouchInput(e) {
const touch = e.touches[0];
handleInput(touch.clientX, touch.clientY);
}
function handleInput(x, y) {
const dir = Math.atan2(x - window.innerWidth / 2, window.innerHeight / 2 - y);
updateDirection(dir);
}
export function startCapturingInput() {
window.addEventListener('mousemove', onMouseInput);
window.addEventListener('touchmove', onTouchInput);
}
export function stopCapturingInput() {
window.removeEventListener('mousemove', onMouseInput);
window.removeEventListener('touchmove', onTouchInput);
}
onMouseInput()
和 onTouchInput()
是事件監(jiān)聽器铝条,當(dāng)輸入事件發(fā)生(例如:鼠標(biāo)移動(dòng))時(shí)靖苇,
它們調(diào)用 updateDirection()
(來自 networking.js
)。
updateDirection()
負(fù)責(zé)向服務(wù)器發(fā)送消息班缰,服務(wù)器將處理輸入事件并相應(yīng)地更新游戲狀態(tài)贤壁。
7. Client 狀態(tài)
這部分是這篇文章中最先進(jìn)的部分。如果你一遍讀不懂所有內(nèi)容埠忘,不要灰心脾拆!請隨意跳過這一節(jié),稍后再來討論它莹妒。
完成客戶端代碼所需的最后一個(gè)難題是狀態(tài)假丧。還記得“客戶端渲染”部分的這段代碼嗎?
render.js
import { getCurrentState } from './state';
function render() {
const { me, others, bullets } = getCurrentState();
// Do the rendering
// ...
}
getCurrentState()
必須能夠根據(jù)從服務(wù)器接收到的游戲更新隨時(shí)向我們提供客戶端的當(dāng)前游戲狀態(tài)动羽。這是服務(wù)器可能發(fā)送的游戲更新示例:
{
"t": 1555960373725,
"me": {
"x": 2213.8050880413657,
"y": 1469.370893425012,
"direction": 1.3082443894581433,
"id": "AhzgAtklgo2FJvwWAADO",
"hp": 100
},
"others": [],
"bullets": [
{
"id": "RUJfJ8Y18n",
"x": 2354.029197099604,
"y": 1431.6848318262666
},
{
"id": "ctg5rht5s",
"x": 2260.546457727445,
"y": 1456.8088728920968
}
],
"leaderboard": [
{
"username": "Player",
"score": 3
}
]
}
每個(gè)游戲更新都具有以下 5 個(gè)字段:
- t:創(chuàng)建此更新的服務(wù)器時(shí)間戳包帚。
- me:接收更新的玩家的 player 信息。
- others:同一游戲中其他玩家的玩家信息數(shù)組运吓。
- bullets:在游戲中的 bullets 子彈信息的數(shù)組渴邦。
- leaderboard:當(dāng)前排行榜數(shù)據(jù)疯趟。
7.1 Native 客戶端狀態(tài)
getCurrentState()
的 native 實(shí)現(xiàn)可以直接返回最近收到的游戲更新的數(shù)據(jù)。
naive-state.js
let lastGameUpdate = null;
// Handle a newly received game update.
export function processGameUpdate(update) {
lastGameUpdate = update;
}
export function getCurrentState() {
return lastGameUpdate;
}
干凈整潔谋梭!如果那么簡單就好了信峻。此實(shí)現(xiàn)存在問題的原因之一是因?yàn)樗鼘秩編俾氏拗茷榉?wù)器 tick 速率。
- Frame Rate:每秒的幀數(shù)(即瓮床,
render()
調(diào)用)或 FPS盹舞。游戲通常以至少 60 FPS 為目標(biāo)。 - Tick Rate:服務(wù)器向客戶端發(fā)送游戲更新的速度隘庄。這通常低于幀速率踢步。對于我們的游戲,服務(wù)器以每秒30 ticks 的速度運(yùn)行丑掺。
如果我們僅提供最新的游戲更新获印,則我們的有效 FPS 不能超過 30,因?yàn)槲覀冇肋h(yuǎn)不會(huì)從服務(wù)器每秒收到超過 30 的更新街州。即使我們每秒調(diào)用 render()
60次兼丰,這些調(diào)用中的一半也只會(huì)重繪完全相同的內(nèi)容,實(shí)際上什么也沒做唆缴。
Native 實(shí)現(xiàn)的另一個(gè)問題是它很容易滯后鳍征。在完美的互聯(lián)網(wǎng)條件下,客戶端將完全每33毫秒(每秒30個(gè))收到一次游戲更新:
可悲的是面徽,沒有什么比這更完美艳丛。 一個(gè)更現(xiàn)實(shí)的表示可能看起來像這樣:
當(dāng)涉及到延遲時(shí),native 實(shí)現(xiàn)幾乎是最糟糕的情況斗忌。
如果游戲更新晚到50毫秒质礼,客戶端會(huì)多凍結(jié)50毫秒旺聚,因?yàn)樗栽阡秩厩耙粋€(gè)更新的游戲狀態(tài)织阳。
你可以想象這對玩家來說是多么糟糕的體驗(yàn):游戲會(huì)因?yàn)殡S機(jī)凍結(jié)而感到不安和不穩(wěn)定。
7.2 更好的客戶端狀態(tài)
我們將對這個(gè)簡單的實(shí)現(xiàn)進(jìn)行一些簡單的改進(jìn)砰粹。第一種是使用100毫秒的渲染延遲唧躲,這意味著“當(dāng)前”客戶端狀態(tài)總是比服務(wù)器的游戲狀態(tài)滯后100毫秒。例如碱璃,如果服務(wù)器的時(shí)間是150弄痹,客戶端呈現(xiàn)的狀態(tài)將是服務(wù)器在時(shí)間50時(shí)的狀態(tài):
這給了我們100毫秒的緩沖區(qū)來容忍不可預(yù)測的游戲更新到來:
這樣做的代價(jià)是恒定的100毫秒輸入延遲。對于擁有穩(wěn)定流暢的游戲玩法來說嵌器,這是一個(gè)小小的代價(jià)——大多數(shù)玩家(尤其是休閑玩家)甚至不會(huì)注意到游戲的延遲肛真。對人類來說,適應(yīng)恒定的100毫秒的延遲要比嘗試應(yīng)付不可預(yù)測的延遲容易得多爽航。
我們可以使用另一種稱為“客戶端預(yù)測”的技術(shù)蚓让,該技術(shù)可以有效地減少感知到的滯后乾忱,但這超出了本文的范圍。
我們將進(jìn)行的另一項(xiàng)改進(jìn)是使用線性插值历极。由于渲染延遲窄瘟,通常我們會(huì)比當(dāng)前客戶端時(shí)間早至少更新1次。每當(dāng)調(diào)用 getCurrentState()
時(shí)趟卸,我們都可以在當(dāng)前客戶端時(shí)間前后立即在游戲更新之間進(jìn)行線性插值:
這解決了我們的幀率問題:我們現(xiàn)在可以隨心所欲地渲染獨(dú)特的幀了蹄葱!
7.3 實(shí)現(xiàn)更好的客戶端狀態(tài)
src/client/state.js
中的示例實(shí)現(xiàn)使用了渲染延遲和線性插值,但有點(diǎn)長锄列。讓我們把它分解成幾個(gè)部分图云。這是第一個(gè):
state.js, Part 1
const RENDER_DELAY = 100;
const gameUpdates = [];
let gameStart = 0;
let firstServerTimestamp = 0;
export function initState() {
gameStart = 0;
firstServerTimestamp = 0;
}
export function processGameUpdate(update) {
if (!firstServerTimestamp) {
firstServerTimestamp = update.t;
gameStart = Date.now();
}
gameUpdates.push(update);
// Keep only one game update before the current server time
const base = getBaseUpdate();
if (base > 0) {
gameUpdates.splice(0, base);
}
}
function currentServerTime() {
return firstServerTimestamp + (Date.now() - gameStart) - RENDER_DELAY;
}
// Returns the index of the base update, the first game update before
// current server time, or -1 if N/A.
function getBaseUpdate() {
const serverTime = currentServerTime();
for (let i = gameUpdates.length - 1; i >= 0; i--) {
if (gameUpdates[i].t <= serverTime) {
return i;
}
}
return -1;
}
首先要了解的是 currentServerTime()
的功能。如前所述右蕊,每個(gè)游戲更新都包含服務(wù)器時(shí)間戳琼稻。我們希望使用渲染延遲來在服務(wù)器后渲染100毫秒,但我們永遠(yuǎn)不會(huì)知道服務(wù)器上的當(dāng)前時(shí)間饶囚,因?yàn)槲覀儾恢廊魏谓o定更新要花費(fèi)多長時(shí)間帕翻。互聯(lián)網(wǎng)是無法預(yù)測的萝风,并且變化很大嘀掸!
為了解決這個(gè)問題,我們將使用一個(gè)合理的近似方法:我們假設(shè)第一個(gè)更新立即到達(dá)规惰。如果這是真的睬塌,那么我們就會(huì)知道服務(wù)器在那一刻的時(shí)間!我們在 firstServerTimestamp
中存儲(chǔ)服務(wù)器時(shí)間戳歇万,在 gameStart
中存儲(chǔ)本地(客戶端)時(shí)間戳揩晴。
哇,等一下贪磺。服務(wù)器上的時(shí)間不應(yīng)該等于客戶端上的時(shí)間嗎硫兰?為什么在“服務(wù)器時(shí)間戳”和“客戶端時(shí)間戳”之間有區(qū)別?這是個(gè)好問題寒锚,讀者們劫映!事實(shí)證明,它們不一樣刹前。Date.now()
將根據(jù)客戶端和服務(wù)器的本地因素返回不同的時(shí)間戳泳赋。永遠(yuǎn)不要假設(shè)您的時(shí)間戳在不同機(jī)器之間是一致的。
現(xiàn)在很清楚 currentServerTime()
的作用了:它返回當(dāng)前渲染時(shí)間的服務(wù)器時(shí)間戳喇喉。換句話說祖今,它是當(dāng)前服務(wù)器時(shí)間(firstServerTimestamp + (Date.now() - gameStart)
) 減去渲染延遲(RENDER_DELAY
)。
接下來,讓我們了解如何處理游戲更新千诬。processGameUpdate()
在從服務(wù)器接收到更新時(shí)被調(diào)用撒踪,我們將新更新存儲(chǔ)在 gameUpdates
數(shù)組中。然后大渤,為了檢查內(nèi)存使用情況制妄,我們刪除了在基本更新之前的所有舊更新,因?yàn)槲覀儾辉傩枰鼈兞恕?/p>
基本更新到底是什么泵三? 這是我們從當(dāng)前服務(wù)器時(shí)間倒退時(shí)發(fā)現(xiàn)的第一個(gè)更新耕捞。 還記得這張圖嗎?
“客戶端渲染時(shí)間”左邊的游戲更新是基礎(chǔ)更新烫幕。
基礎(chǔ)更新的用途是什么俺抽?為什么我們可以丟棄基礎(chǔ)更新之前的更新?最后讓我們看看 getCurrentState()
的實(shí)現(xiàn)较曼,以找出:
state.js, Part 2
export function getCurrentState() {
if (!firstServerTimestamp) {
return {};
}
const base = getBaseUpdate();
const serverTime = currentServerTime();
// If base is the most recent update we have, use its state.
// Else, interpolate between its state and the state of (base + 1).
if (base < 0) {
return gameUpdates[gameUpdates.length - 1];
} else if (base === gameUpdates.length - 1) {
return gameUpdates[base];
} else {
const baseUpdate = gameUpdates[base];
const next = gameUpdates[base + 1];
const r = (serverTime - baseUpdate.t) / (next.t - baseUpdate.t);
return {
me: interpolateObject(baseUpdate.me, next.me, r),
others: interpolateObjectArray(baseUpdate.others, next.others, r),
bullets: interpolateObjectArray(baseUpdate.bullets, next.bullets, r),
};
}
}
我們處理3種情況:
-
base < 0
磷斧,意味著在當(dāng)前渲染時(shí)間之前沒有更新(請參見上面的getBaseUpdate()
的實(shí)現(xiàn))。由于渲染延遲捷犹,這可能會(huì)在游戲開始時(shí)發(fā)生弛饭。在這種情況下,我們將使用最新的更新萍歉。 -
base
是我們最新的更新(??)侣颂。這種情況可能是由于網(wǎng)絡(luò)連接的延遲或較差造成的。在本例中枪孩,我們還使用了最新的更新憔晒。 - 我們在當(dāng)前渲染時(shí)間之前和之后都有更新,所以我們可以插值蔑舞!
state.js 剩下的就是線性插值的實(shí)現(xiàn)拒担,這只是一些簡單(但很無聊)的數(shù)學(xué)運(yùn)算。如果您想查看攻询,請?jiān)?Github 上查看 state.js朵栖。
我是為少胞枕。
微信:uuhells123戒良。
公眾號:黑客下午茶殷绍。
謝謝點(diǎn)贊支持??????呆馁!