á 2015 á°ááá
áµáá¥ááá
ášáá³áᜠášáá
á ááµ á°áá°á
ášáá³ááá ášááᣠááá«ááµ ááá ášáá áá» á£áá¥á á°á«áᜠášáµá ášáá³áᜠáážá (ááá ááá« á á«áµáááá)ᢠá¥ááá áá á á°áá³á³á ááµášá á¥á á°áá«á á°á«ááŸáœá áááá£áᢠááᜠá³áá áš.io ášáá³ááœá¡-
á áá
áœáá ááµá¥, á¥ááŽáµ á¥áá°áá á¥ááášáá«áá ášá£á¶ áš.io ášáá³ ááá á©. ááá
, ášáá«áµááªááµ á¥áááµ á¥á» á á áááá: á¥áá° á áá£á¥ á«á ááá®áœá áášá³áµ á«áµááááá³á this
О
.io ášáá³ áá³á
ááµáá
ááµ á¥áá³á³á£ á¥áá á
á³ááá¢
ášáá³á á á£á ááá ááá¡ ááᜠá°á«ááŸáœ á£áá áµ ááµášá áá ááášá¥á áµáá£á á«ááœáᢠášá¥ááµá ááášá¥ á á«áµ-á°á áá®ááá°á®áœá á«áá¥áá á¥á ááᜠá°á«ááŸáœá áá®ááá°á®áœá á ááµáááµ áá á¥á«á áááá³áµ áááá«áá¢
1. ášáá®ááá± á áá áááá« / ááá á
á¥á á áá°áááá
ááá á®áµ á áááµ á¥áá áášá°á á¥áá²áœá ášáá³á ášáá³á¢
áá³áá ášáášá°ááµá áá ááá:
áááá¹ ášášáá³áá ášáµá á áááá ášáá«áµá°á³áµá á á£á á³ááá áš Node.js ášáµá áááá ááá¢á¶á¬áµ.io - á á á³áœ á¥á á á áááá áá«ášá ááᥠáááááᥠášáá¥á¶á¬áµ á€á°-ááœáááµá¢Webpackᢠ- ááá á áµá°á³á³áª. ááá Webpack á¥áá°áá áá ááá ᥠááœáá.á¥áá .
ášáá®áááµ ááá« ááá á áá á¥áá°áááµá á¥ááá¡-
public/
assets/
...
src/
client/
css/
...
html/
index.html
index.js
...
server/
server.js
...
shared/
constants.js
ášá áá¥/
á á áá ááµá¥ ááá ááá public/
á á áááá© á áµá³á²áµá²ááµ áᢠáá°ášááᢠááµá¥ public/assets/
á á¥á áá®áááµ á¥á
á áá ášáá ááµááœá ááá.
src /
ááá ášááá á®áµ á á ááá ááµá¥ ááᢠsrc/
. ááá¶áœ client/
О server/
áá«á³ážá áááá© á¥á shared/
á á°áá áá á¥á á á áááá© ášááᣠáá ááá ááááá¢
2. áµá¥á°á£áᜠ/ ášáá®áááµ á áá á¶áœ
ášáá á¥áá°á°á áá°á áá®ááá±á ááááá£áµ áááá á¥á« á áµáªá«á
á¥áá áááá.
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
ášáá«áµááªááµ (JS) á°áá á ááá¢á« áá¥á¥ ááᢠáá¥áá ášáá áááá«á á¥á ááᜠáá° ááá ááµá¥ ášááá¡ ááááœá á ášááá áááááá¢- ášá¥á ášáá¥áá ááá£á³ áá€áµ JS á ááá«á ááµá¥ áááá£á
dist/
. áá áá ááá ášá á¥á°ááááᢠjs á¥á á. - á¥á á¥áá áááá
á£á€á , á¥á á á°ááá á áááá©@babel/preset-env áá á®á á á³áŸáœ ášáá JS á®áµ ááááᢠ- á JS áááᜠášá°á áá± áááá CSS áááá£áµ á¥á á á ááµ áŠá³ ááá£áá áááá á¥ášá°á ááá ááᢠášá á¥ááááᢠcss á¥á á.
á¥ááá³ ášá¥á
á ááá áµááœá á áµá°ááá
ááááᢠ'[name].[contenthash].ext'
. áááá [name]
á áá€áµ áá¥á¡ áµá áá°á«áá (á á¥á ááá³ áá
game
) ᣠá¥á [contenthash]
á ááá áááµ áᜠáá°á«áᢠá¥áá°ááááá contenthash
). ášáášášá»á áá€áµ ášá¥áá³ ááá áµá áááá game.dbeee76e91a97d0c7207.js
.
ááá webpack.common.js
áá° ááá£á³ á¥á ášá°á ááá ášáá®áááµ áá
á®áœ ášáááµáá£á ášáá ášáµ áá
á ááá ááᢠášáááµ áá
á áá³á áážááá¡
webpack.dev.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'development',
});
ááá€á³áááµ, á áááµ áá°áµ ááµá¥ á¥áá áááá webpack.dev.js
, á¥á áá° áááá«á webpack.prod.js
áá° áááµ á áá°á«áá áµ áá ášá¥á
á áá ááœá áááá»ážáµ.
ášá á«á£á¢ á áá¥á
á áá
áœáá ááµá¥ ášá°áášáá©áµá á°ášááᜠáášá°á á¥áá²áœá áá®ááá±á á á á«á£á¢á ááœá áá á¥áá²áá á¥ááá«áá. áááá© ááá áá: á ááááªá«, áµááá± áá«á á áá áµ
$ git clone https://github.com/vzhou842/example-.io-game.git
$ cd example-.io-game
$ npm install
á¥á á¥ááµá ááááµ ááá áááµ! ášáááµ á áááá©á áááááᣠá ááá á«áá±
$ npm run develop
á¥á áá° áµá á á³áœ ááá±
3. ášá°áá á ááá¢á« áá¥áŠáœ
áá° ášáá³á á®áµ á¥á«á± á¥ááášáµá¢ ááááªá« áᜠá¥áááááá index.html
, á£á¢á«áá á²áá á, á á³á¹ ááááªá« ááááá. áá»áœá á á£á ááá ááááá¡-
index.html
áá³á .io ášáá³ á°á«ááµ
áá
ášá®áµ áá³á ááᜠáááµášá á áµáá¹ ááá á«á ááᣠá¥á ášá¥á ááᜠášáá¥á áá³ááᜠáá á°áá³á³á ááá á á°ááááᢠáá á®áµ áá áá á áá áá³á ááœááá¢
á¥á á áá:
HTML5 ážá« á á£á (<canvas>
) ášáá³áá áááµá«áµ ášááá ááá áµá¢<link>
ášá¥áá CSS á¥á á ááášááá¢<script>
ášá¥áá áá«áµááªááµ á¥á á ááášááá¢- áá ááá ášá°á áá áµá áá
<input>
á¥á áš PLAY ááá (<button>
).
ášááá» áá¹á ášá«á á áá á á³á¹ ášááá¢á« áá¥á¥ 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);
};
});
áá ááµá¥áµá¥ áááµá ááœááᣠááá áá á¥áá á¥á á¥ášá°á«áá° á áá°ááá¡
- ááᜠá áá«á³ JS ááááœá á ááµáá£áµ ááá¢
- CSS ááµáá£áµ (áµááá áá¥áá á CSS á¥á á ááµá¥ á¥áá°áá«á«áµá³ážá á«ááá)á¢
- ÐапÑÑк
connect()
ášá áááá© áá áááááµ ááááµášáµ á¥á ááááµdownloadAssets()
ášáá³áá áááµá«áµ ášáá«áµááá ááµááœá áááášáµá¢ - á°ášá 3 ášá°á ááá á áá ááá ááá áá³á«á (
playMenu
). - áš"PLAY" áááá ááá«á á°áá£á£áªáá á áááááµ ááᢠááá á²á«á á®á± ášáá³áá áááá«á á¥á á¥á ááá«ááµ ááá ááááœáá áá áááá© áááášááá¢
ášá¥á ášá°áá á-á áááá á áááá® ááá "áµá" á ááá áá° ááá ááµá¥ á áá¡áµ áááᜠááµá¥ áá index.js
. á áá áááá á á
á°á á°ášá°á á¥áááášá³áá.
4. ášá°áá á ááᥠááááá¥
á áá
ášáá³ ášá áááá© áá ááááááµ ášá³áá á€á°-ááœáááµá á¥áá ááááá¢
á ááµ ááá ááášáá src/client/networking.js
áá ááášá£ášá£á ááá á°á ášá áááá© áá áááááµ;
á áá³áš áášá¥.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);
};
áá á®áµ ááááœááµá á áµáá¹ á¥áá²á³á á á°á°áááá¢
á áá ááá ááµá¥ á¶áµáµ áá á°áá£á«áµ á áá¡-
- ášá áááá© áá ááááááµ á¥ášáášáá ááá¢
connectedPromise
áááááµ áµááá¥á á¥á» áá ášáááá°áᢠ- ááááá± áµá¬á³á ášáá ášááᶠáá°áá á°áá£á«áµá á¥ááááá£áá (
processGameUpdate()
ОonGameOver()
) ášá áááá© áááá áážá áááœá ááá¥áá¶áœá¢ - áá° áá á¥ááá«áá
play()
ОupdateDirection()
ááᜠááááœá áá áá á¥áá²áœá.
5. ášá°áá á á áá«ášá¥
ááµáá á áµááªá áá ááá³ášáµ ááá á áá áá!
ááá áá á«áá ášááµášááœá á ááµá£ ááá ââášáá«áµááááµá áááá ááµáᜠ(áá¥á¶áœ) ááášáµ á áá¥áᢠášáá¥áµ á áµá°á³á³áªá á¥áááá¡-
áá¥ášá¶áœ.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
, áá
á ášááá áµá áááá ášá¥áá áá áá á«áµá«á Image
. áá¥á± á²á«á á á ááµ áá ááµá¥ á¥áášáá»áá assets
ááá°áá± áá£á áá³ášá». á¥á«áá³áá± ááá°á¥ ááááá« áጠááášáµ ááááµáá³á (áááµáᣠááá áá¥á¶áœ) á¥ááá
á³ááᢠdownloadPromise
.
áá¥ášá¶á¹á á«áášá± á áá áá³ášáµ áááá ááœááᢠáá°á á²á á¥áá°á°ááášá, á áµáš-áᜠáá ááá³á, á¥áá áááá <canvas>
). ášá ášáá³ á á£á ááá áá áµááá
ášáášá°ááá áá³á á¥á» á«áµáááááá¢
- ááá£
- ášá°á«áᜠááášá¥
- á ášáá³á ááµá¥ á«á ááᜠá°á«ááŸáœ
- ááááœ
á áµááááá¹ á
áá¥áŠáœ á¥áá
á áᢠsrc/client/render.js
ášáá ášá°áášáá©áµá á á«áµ ááá®áœ á áµááá ášáá«ááá¥á¡-
áá ášá¥.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()
ášáá
ááá áá á°áá£á ááᢠstartRendering()
О stopRendering()
áš render loop á á¥áá
áµáᎠá 60 FPS ááá£á á©á¢
ášááá°á£á ášá³áµ á°áá£á«áµ á°ášá£á áµáá á«áᜠ(ááá³á ᣠrenderBullet()
) á«á á«á
á á áµááá á áá°ááᣠáá á ááµ ááá áá³á á¥áá
á áá¡-
áá ášá¥.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,
);
}
ááŽáá á¥ášá°á ááá áááá áᥠáá á getAsset()
, áá°á á²á á ááµá¥ á³áá·á asset.js
!
áµá ááᜠá áᥠášá³á¶áœ áááá ááááµ á«ááµ ášáášáá á«áá¥á¡á¢
src/á°áá á/render.js .
6. ášá°áá á áá€áµ
ášáá³ áááµá«áµ ááá á áá ááᢠáá«ááµ ášááœá! ášáá¥á¥á ááááá¥á© á á£á ááá áááá-ášá¥áá
áµááŽáá á á
á£á« ááááá¥, á áá€áá (á á®ááá°á áá) áá áá ááá áá« áá¹á (á áá£áá áá³áªá« áá) ááá«áµ ááœáá. áá
áá á°áá£á«á áááµášá á¥ááááá£ááá¢
áá
áá áá ááášá£ášá£á src/client/input.js
:
áá¥ááµ.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()
á¥áª ášáá«á°áá ášááµá°áµ á áµáá®áœ áážáᢠupdateDirection()
(áš networking.js
) ášáá€áµ ááµá°áµ á²ášá°áµ (ááá³á, á áᥠá²ááá³ááµ). updateDirection()
ášáá¥á áµ ááµá°áµá ášáá«áµá°áááµ á¥á ášášáá³áá ááá³ ášáá«á»áœáá ášá áááá© áá ááá¥ááµá á«áµá°ááá³áá¢
7. ášá°áá á ááá³
áá ááá á ááµá³á ášááááªá« ááá ááµá¥ á á£á á áµážááªá áá. áááááªá« áá áµá³áá¡áµ á«áá°ášá³áœááµ á°áµá á áµáášá¡! á¥áá²á«áá áááá áµáœááá á¥á á áá áá° á¥á± ááááµ áµáœááá á¢
ášá°áá áá/ášá áááá á®áµ ááá ááá ášáá«áµáááá ášáášášá»á ášá¥ááá áᜠááá ááᢠáááµ. ášá°áá á á á°ášááá ááá ášá°áááá ášá®áµ á áá£á¢ á áµá³ááµ?
áá ášá¥.js
import { getCurrentState } from './state';
function render() {
const { me, others, bullets } = getCurrentState();
// Do the rendering
// ...
}
getCurrentState()
á á°áá áá ááµá¥ ášášáá³áá áá
á³á ááá³ áá°á á ááá£á á ááááá áá ášá áááá© á á°áá ááµ ááááᜠáá á áááµášáµá¢ á áááá© ááá ášááœáá ášášáá³ áá»á»á« áá³á áážááá¡
{
"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
}
]
}
á¥á«áá³áá± ášášáá³ ááá á ááµáµ á°áá³á³á ááµá®áœá ááááá¢
- táá ááá áጠá¥áá°á°áá áš ášáá áá ášá áááá ášáá áá á°áá¢
- meáá áá ááá áµáá°áá áá á°á«áᜠáášáá¢
- áááœ: ááᜠá°á«ááŸáœ á á°áá³á³á ášáá³ ááµá¥ áµááá³á°á áášá áµááµáá¢
- á¥áá¶áœá ášáá³á ááµá¥ áµá áá®ááá¶áœ áášá áµááµáá¢
- ášááªáᜠá°áá³á áá á«áá ášááªáᜠá°áá³ ááá¥á¢ á áá áœáá ááµá¥, á¥á á áááášá³ážáá.
7.1 Naive á°áá á ááá³
ášáá
á á°áá£á á getCurrentState()
á á
áá¡ ášá°áá ááá ášášáá³ ááá ááᥠá¥á» áá ááááµ ášááœááá¢
naive-state.js
let lastGameUpdate = null;
// Handle a newly received game update.
export function processGameUpdate(update) {
lastGameUpdate = update;
}
export function getCurrentState() {
return lastGameUpdate;
}
ááá á¥á áááœ! áá á«á á«á á ááá á¢áá áá®á¢ ášáá á á°áá£á á á áá± áááá«áµ áœáá á«áá áµ ááá¡- ášáá¬á áá¥áá±á á á áááá á°ááµ áá¥ááµ áááµá£á.
ášáá¬á áá áášáááᜠá¥ááµ (áááµá á¥áªááœ
render()
) á á°ášááµ ááá FPS. ášáá³áᜠá á¥áááá áá á¢á«ááµ 60 FPS áááááµ áá¥á«á.
ášááááµ áá á: á áááá© ášášáá³ áááááœá áá°áá áᜠášáááá áµ áµááááœá¢ á¥ááá áá ášááá áá¥ááµ á«áá° áá. á á¥á ášáá³ á áááá© á á°ášááµ á 30 áá°á¶áœ áµáááᜠáá°á«áá¢
ášášáá³áá ášá
áᥠáá ááá á¥á» á«áášá¥áᣠFPS á áá ášá± áš 30 á áá á áá áá¥á ᣠáááá«á±á ášá áááá© á á°ášááµ áš30 á áá áááááœá á ááááá¢. á¥áá á«á render()
á á°ášááµ 60 ááᣠášáá« ášá¥ááá
á¥áªáᜠááµá¥ áááŸá¹ á ááµ á áááµ ááá á¥áá°áá áááá«áᣠá áá ášá± ááá ááá á á«á°áááᢠááá ášáá
á á°áá£á á áœáá ááᢠááááášáµ ášá°ááá . á á¥á© ášá¢áá°áááµ áá¥ááµá£ á°áá áá á áš33áᎠ(á áŽá®ááµ 30 á á°ášááµ) ášášáá³ ááá ááá ááá¢
á áá«á³áá ááá³, ááá ááá áá¹á á áá°áá. ášá áá áµáááá ááµá ášáášá°áá áááá-
ášáá
ááµ á á°áá£á á áá° áááášáµ á²áᣠá á°áá£á á¥á
á ášášá ááᢠášášáá³ ááá á 50ms áááášáµ ášá°ášá° ᣠášáá« ášá°áá á áážá«áᜠá°ášá᪠50 áᎠáááá«á±á á ááá ášášáá³áá ááá³ ášáá°áá ááá á¥á«áášá ááᢠáá
áá°á«áá¹ áá á«á
á ášáááᜠá¥áá°áá ááááµ áµáœáááœáá¡ ášáááá° á¥á¬áªáá ášáá³áá á«ážá ášá á¥á á«áá°ášáá á«á°ááááá¢
7.2 ášá°á»á»á ášá°áá á ááá³
á á áááá áá á áá³ááµ áá»á»á«ááœá á¥áá°ááááᢠá ááááªá«, á¥áá áááá áááášáµ áá³ášáµ á 100 ms. áá áááµ ášá°áá áá "ášá áá" ááá³ áá áá á á áááá© áá á«áá ášášáá³ ááá³ á 100 á.áŽ. ááá³á, á á áááá© áá á«áá áá ášáá 150, ášáá«á á°áá áá á áááá© á áá á± ášáá ášáá ááá³ á«ááá£á 50:
áá
ášáááááµ ášášáá³ ááá ááááœá áááµášá áš100ms ááµ áá°á ááá¡
ááá
á«áá ááá« ááá áááá
áá ášáá£á áŽááá áá áá á¥ááœááá
ášá°áá á-áá áµáá á« , áá á ášá³á°á áá áááášáµá á ááááµ á¥á© áµá« áá°á«á, ááá áá á áá áœáá ááµá¥ á áá«á°áµá.
ááá á¥ášá°á áááá áµ á«áá áá»á»á« ááᢠááµáá«á á£áááá¥ááµ. á áá³ášáµ áááášáµ áááá«áµá£ á¥á á¥ááá áá á á°áá áá ááµá¥ á«áá ášá áá áá á ááµ á¢á«ááµ á ááµ ááá á¥áááááᢠá²á á« getCurrentState()
, ááážá á¥ááœááá
áá ášáá¬á á°áá áœááá ááá³áá¡ á áá áá© áá¬ááœá á áááááá ášáá¬á áá¥ááµ ááµá«áµ á¥ááœááá!
7.3 ášá°á»á»á ášá°áá á ááá³á á áá°áá á áá
ášáµáá á« áá³á á src/client/state.js
ááá±áá render lag á¥á linear interpolation áá áááᣠáá áášá
á áá á áá°ááᢠá®á±á á áááµ áááᜠá¥áášáááᢠášááááªá«á áážááá¡-
state.js ááá 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()
. áá°á á²á á¥áá³ášáá á¥á«áá³áá± ášášáá³ ááá ášá áááá áá áá
á°áá á«á«áµá³áᢠááµáá ášá áááá© ááᣠ100ms ááá
ášá¥ ášááµá áµá« áááášáµá áá áá á¥áááááá ááá áá á á áááá© áá á«ááá áá ááœá á ááá
áᣠáááá«á±á áá»á»á«áá¹ áá° á¥á áááµášáµ áá á«á
á áá á¥áá°áá°á° ááá
á ááœááᢠá áááášá¡ ášááá³áá
á¥á áá¥áá± á á£á ááá«á ááœáá!
á áá
áœáá ááªá« áááááµ, á¥á áááá«á³á approximation áá áá ááœáá: á¥á ášááááªá«á ááá áá²á«áá á¥áá°á°ášá° á áµááµá. áá
á¥áááµ ášááᣠá áá
áá ášá áááá©á áá á¥ááá
áá á! ášá áááá©á ášáá áá
á°á á¥áášáá»ááᢠfirstServerTimestamp
á¥á ášá¥áá á á¥á
á á«á£á¢á«á (á°áá á) ášáá áá
á°á á á°áá³á³á á
áœá áµ ááµá¥ gameStart
.
áá. ášá áááá áá = ášá°áá á áá ááá ášáá áµá? ááááµá áá á "á áááá áá áá
á°á" á¥á "á°áá á ášáá áá
á°á" áá«ášá ášáááášá? áá
áµáá
á¥á«á áá! á ááµ á áááµ ááá á¥áá³ááá á³ááᢠDate.now()
á á°áá áá á¥á á á áááá© ááµá¥ ášá°áá«á© ášáá áá
á°ááœá áááá³á ᣠá¥á á á¥ááá
ááœáᜠá á«á£á¢á«á ááá³áᜠáá ášá°áá ášá° ááᢠášáá áá
á°áᜠá ááá ááœáᜠáá á ááµ á áááµ áááá á¥ááœá á á³áµá¡á¢
á áá áá á¥áá°áá°á« á°ášáµá°áá currentServerTime()
: áááá³á ášá áá ášááµá áá ášá áááá áá áá
á°á. á áá á áááá áá
ášá áááá© ášá áá áá áá (firstServerTimestamp <+ (Date.now() - gameStart)
ášáááš áááášáµ ()RENDER_DELAY
).
á áá ášášáá³ áááááœá á¥ááŽáµ á¥áá°áááá á¥áááášáµá¢ ášááá á áááá á²á°áá°á áá£áá processGameUpdate()
á¥á á á²á±á ááá áá° áµááµá á¥ááµááá ááá gameUpdates
. ášáá« ášáá
á°áš áµááµá³áá á á ááá áááá°áœ ášáá
á ááµ áááá ášáá© áááááœá á¥ááµááá³ááᢠášáá ášáµ áá»á»á«áááá«á±á á¥á ášá¥ááá²á
á ááááážááá¢
"áá°ášá³á áá»á»á«" áááµá áá? áá ášá áááá© áá á³á áá áá° áá á áááá³ááµ ášááááá ášááááªá«á áá»á»á«. áá á á¥ááá áááá« á áµá³ááµ?
ášášáá³á áá»á»á« á áá¥á³ á "ášá°áá á ááµá« áá" á áµá°áá« ášáá ášáµ áá»á»á« ááá¢
ášáá ášáµ áááá ááá á¥á
á áá áááá? ááááµáá áá»á»á«ááœá áá° ááá» ááµáá áá£á ášáááœáá? áá
áá áááá
á¥áááá á áášášá» á á°áá£á á©á á áµá¡á áµ getCurrentState()
:
state.js ááá 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),
};
}
}
á¶áµáµ áá³á®áœá á¥ááááá-
base < 0
áá áááµ á áá á¥áµá«áá áµ áá áµášáµ ááá ááááᜠášáá (ášáá á«ááá áµáá á« áááášá±getBaseUpdate()
). áá á ášáá³á ááááªá« áá á áá°áµ áááášáµ áááá«áµ áášá°áµ ááœááᢠá áá á áá£á, ášá°áá ááá ášá áᥠáá ááá á¥áá áááá.base
á«áá ášá áᥠáá ááá ááᢠáá á ááµááá áááášáµ ááá á°á«á ášá áááášá¥ áááááµ áááá«áµ ááá ááœáá. á áá á áá£áᣠá«ááá ášá áᥠáá ááá á¥ášá°á ááá ááá¢- ášá áá ášááµáªá« áá á ááµá áá á áá áá»á»á« á ááᣠáµááá á¥ááœááá á£áá ááá£áµ!
ášáášá áá state.js
ááá (ááá áá á á°ááº) áá³á¥ ášáá ášááµáá«á á£áááá¥ááµ áµáá á« ááᢠá¥á«áµá áá°áµ ášááá ášáá« áááá±áµá¢ state.js
áá
ááá 2. ášááᣠá áááá
á áá
ááá ášá¥áá ášááá£á ášáá áš Node.js ááᣠá¥áááášá³áá
1. ášá áááá ááá¢á« áá¥á¥
ášáµá á áááá©á áááµá°á³á°á á Node.js ášáá³áá
á³áá ášáµá áááá á¥áá ááááᢠsrc/server/server.js
:
á áááá.js ááá 1
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackConfig = require('../../webpack.dev.js');
// Setup an Express server
const app = express();
app.use(express.static('public'));
if (process.env.NODE_ENV === 'development') {
// Setup Webpack for development
const compiler = webpack(webpackConfig);
app.use(webpackDevMiddleware(compiler));
} else {
// Static serve the dist/ folder in production
app.use(express.static('dist'));
}
// Listen on port
const port = process.env.PORT || 3000;
const server = app.listen(port);
console.log(`Server listening on port ${port}`);
á ááááªá«á ááá áµá Webpack á¥áá°á°ááááá á áµá³ááµ? ášáá ášáá¥áá á áááá®áœá ášááá ááá áµ áá ááᢠá áááµ áááá¶áœ á¥áá ááážáááá¡-
- á°á áá
ášáá¥áá-áŽá-ááµááá ášá¥áá ášá¥áµááµ áá¬ááœá á á«áµ á°á á¥áá°áá ááááá£áµ ááá - á áµá³á²áµá²ááµ á áá á«áµá°ááá
dist/
ášááá± ááá£á³ á áá áááá»áœáá á ášáµáá ášáá¥áá á¥ááœáááá¢
ááá á áµááá á°áá£á server.js
á áááá©á áááá ááá¢
á áááá.js ááá 2
const socketio = require('socket.io');
const Constants = require('../shared/constants');
// Setup Express
// ...
const server = app.listen(port);
console.log(`Server listening on port ${port}`);
// Setup socket.io
const io = socketio(server);
// Listen for socket.io connections
io.on('connection', socket => {
console.log('Player connected!', socket.id);
socket.on(Constants.MSG_TYPES.JOIN_GAME, joinGame);
socket.on(Constants.MSG_TYPES.INPUT, handleInput);
socket.on('disconnect', onDisconnect);
});
áš socket.io áááááµ ášá áááá© áá á á°á³á« ááá³ á«áááá á áá áá á²á± á¶á¬áµ ášááµá°áµ á°áá£á£áªááœá á ááá
á°ááᢠášááµá°áµ á°áá£á£áªáᜠášá°áá áᜠášá°áá ááµá ááá¥ááµ áá ááµ áá á áá á ááµá°ááá á«áµá°ááá³áᢠgame
:
á áááá.js ááá 3
const Game = require('./game');
// ...
// Setup the Game
const game = new Game();
function joinGame(username) {
game.addPlayer(this, username);
}
function handleInput(dir) {
game.handleInput(this, dir);
}
function onDisconnect() {
game.removePlayer(this);
}
áš.io ášáá³ á¥ášáá áá ááᣠáµááá
á ááµ á
á á¥á» á¥áááááá Game
("ášáá³") - ááá á°á«ááŸáœ á á ááµ ááµášá ááµá¥ áá«áá³á! á ááá¥áá ááá áá
ááá á¥ááŽáµ á¥áá°áá°á« á¥áááášá³áá. Game
.
2. ášášáá³ á áááá®áœ
ááá Game
á á áááá© á á©á á á£á á áµááá ášáááá á áááá® ááá. áááµ áá á°áá£á«áµ á ááµá¡- ášá°á«áᜠá áµá°á³á°á О ášášáá³ ááµáá°á.
ášááááªá«á á°áá£á áááµá ášá°á«áᜠá áµá°á³á°á á¥ááááá¢
game.js ááá 1
const Constants = require('../shared/constants');
const Player = require('./player');
class Game {
constructor() {
this.sockets = {};
this.players = {};
this.bullets = [];
this.lastUpdateTime = Date.now();
this.shouldSendUpdate = false;
setInterval(this.update.bind(this), 1000 / 60);
}
addPlayer(socket, username) {
this.sockets[socket.id] = socket;
// Generate a position to start this player at.
const x = Constants.MAP_SIZE * (0.25 + Math.random() * 0.5);
const y = Constants.MAP_SIZE * (0.25 + Math.random() * 0.5);
this.players[socket.id] = new Player(socket.id, username, x, y);
}
removePlayer(socket) {
delete this.sockets[socket.id];
delete this.players[socket.id];
}
handleInput(socket, dir) {
if (this.players[socket.id]) {
this.players[socket.id].setDirection(dir);
}
}
// ...
}
á áá
ášáá³ á°á«ááŸá¹á á áá³ á¥ááá«ážááá id
their socket.io socket (áá« ášá°áá¡á£ ášáá« áááá± server.js
). Socket.io á«á± áá¥á«áá³áá± á¶á¬áµ áá© áááµá£á id
áµááá
áµááá
áá³á áášáá
á á«áµááááá. á¥á°áááá³ááᢠášá°á«áᜠáá³ááá«.
á«áá á á á¥áá¯áœá áááᣠá á ááµ ááá ááµá¥ á«á ášá á¥ááµ á°áááá®áœá á¥ááááá Game
:
sockets
ášá°á«áᜠáá³ááá«áá ášá°á«áá¹ áá ášá°áááá á¶á¬áµ áá ášáá«ááá ááá ááᢠá áá áá ááµá¥ á¶á¬á¶áœá á á°á«áᜠáá³ááá«ážá á¥ááµáá°ááµ á«áµáœáááá¢players
ášá°á«áᜠáá³ááá«áá ášá®á±>á°á«áᜠááá áá ášáá«ááá áá ááá¢
bullets
ášááá®áœ áµá¥áµá¥ ááᢠBullet
, á¥á±á ášá°áá°á á
á°á á°ášá°á ášááá.
lastUpdateTime
ášáá³á ááášášá» áá ášá°áááá áµ ášáá áá
á°á ááᢠá¥ááŽáµ á¥á
á áá á¥áá°ááá á á
áá¡ á¥áááášá³ááá¢
shouldSendUpdate
ášá³áµ á°áááá áá. á á ááááá á á
áá¡ á¥áááášá³ááá¢
ááŽáᜠaddPlayer()
, removePlayer()
О handleInput()
áá¥á«á«áµ á á«áµáááá, á ááµá¥ á¥á
á áá áááá server.js
. ášááµá³ááµ áœáá³áá áá°áµ ášááá áµáᜠášá á¥áá áááá±á¢
ášáášášá»á ááµáá constructor()
áááá«á ášááá áá°áµ ášáá³áᜠ(áš60 ááááᜠáµáááᜠáá)
game.js ááá 2
const Constants = require('../shared/constants');
const applyCollisions = require('./collisions');
class Game {
// ...
update() {
// Calculate time elapsed
const now = Date.now();
const dt = (now - this.lastUpdateTime) / 1000;
this.lastUpdateTime = now;
// Update each bullet
const bulletsToRemove = [];
this.bullets.forEach(bullet => {
if (bullet.update(dt)) {
// Destroy this bullet
bulletsToRemove.push(bullet);
}
});
this.bullets = this.bullets.filter(
bullet => !bulletsToRemove.includes(bullet),
);
// Update each player
Object.keys(this.sockets).forEach(playerID => {
const player = this.players[playerID];
const newBullet = player.update(dt);
if (newBullet) {
this.bullets.push(newBullet);
}
});
// Apply collisions, give players score for hitting bullets
const destroyedBullets = applyCollisions(
Object.values(this.players),
this.bullets,
);
destroyedBullets.forEach(b => {
if (this.players[b.parentID]) {
this.players[b.parentID].onDealtDamage();
}
});
this.bullets = this.bullets.filter(
bullet => !destroyedBullets.includes(bullet),
);
// Check if any players are dead
Object.keys(this.sockets).forEach(playerID => {
const socket = this.sockets[playerID];
const player = this.players[playerID];
if (player.hp <= 0) {
socket.emit(Constants.MSG_TYPES.GAME_OVER);
this.removePlayer(socket);
}
});
// Send a game update to each player every other time
if (this.shouldSendUpdate) {
const leaderboard = this.getLeaderboard();
Object.keys(this.sockets).forEach(playerID => {
const socket = this.sockets[playerID];
const player = this.players[playerID];
socket.emit(
Constants.MSG_TYPES.GAME_UPDATE,
this.createUpdate(player, leaderboard),
);
});
this.shouldSendUpdate = false;
} else {
this.shouldSendUpdate = true;
}
}
// ...
}
áᎠupdate()
áááá£áµ á á£á á áµááá ášáááá ášá áááá-áá á áááá® áááᢠášáá á«á áážááᣠá á
á°á á°ášá°áá¡-
- ááá á«á
á áá á«á°áá
dt
á«ááá ááá® á áááupdate()
. - á¥á«áá³áá±á áá®áááµ á«áµá³á á¥á á áµááá ášáá á«á áážáá. ášáá
á á°áá£á á á°áá£á á á áá á¥áááášá³áá. áá áá áá
á ááá
á áá«áœá ááá¢
bullet.update()
áááá³átrue
áá®ááá± áá¥ááµ á«áá áµ (ášááµášá© áá£)ᢠ- á¥á«áá³áá±á á°á«áᜠá«áááá á¥á á áµááá ášáá áá®áááµá á«á á
ááᢠáá
áá áµáá á« á áá á¥áááášá³áá -
player.update()
áá ááááµ ááœááá¢Bullet
. - á áá®ááá¶áœ á¥á á á°á«ááŸáœ áá«ášá ááá¶áœá áááµá»á
applyCollisions()
, áá á á á°á«ááŸáœ áá ášáá°áá± ášáá®ááá¶áœ áµá¥áµá¥ áááá³á. áá¥á«áá³áá± ášá°ááá° áá®áááµá£ ášá°á®á°áá á°á«áᜠáá¥áŠáœá á¥áášáá«áá (á áá ááplayer.onDealtDamage()
) á¥á ášáá«á áá®ááá±á ášáµááµá© áá á«áµááá±áµbullets
. - áááá ášá°áá°á á°á«ááŸáœ á«á³ááá á¥á á«á ááá¢
- ášášáá³ ááá áááá á°á«ááŸáœ ááá«á á ášá°ášáá± á áá á©á áµ áá ááá«áµ
update()
. áá ášáá ášá°á áá°áá ášá³áµ á°áááá ááášá³á°á áášá³áá.shouldSendUpdate
. áááá«á±áupdate()
60 áá / á° áá£áá ᣠášášáá³ áááááœá 30 áá / á° á¥ááá«ááᢠáµááá á ášá°ááµ áµáááᜠášá áááá á°ááµ 30 á°ááµ / á° áá (á ááááªá«á ááá ááµá¥ áµá ášá°ááµ áááᜠá°ááááá).
ááá ášášáá³ áááááœá á¥á» ááá© á áá á á©á ? á»áá áááµááá¥á¢ á á°ášááµ 30 ášášáá³ ááááᜠá¥á áážá!
ááá áá á¥áá á áµá°ááá¢
update()
á á°ášááµ 30 áá? ášášáá³áá ááµáá°á ááá»á»áᢠá¥á áá ášáá á«áá¢update()
ᣠášášáá³á ááµáá°á ášá áá áµáááá ááááᢠááá áá á áá°ááᜠá¥ááµ á¥á á áµáá°á±á¢update()
, áááá«á±á áá áµááµ ááµ ášáá áµá« áá - 60 á á°ášááµ á á áá.
ášáášá ááá Game
ááµá¥ á¥á
á áá ášáá ášášá³áµ ááŽááœá á«á«áµá³á update()
:
game.js ááá 3
class Game {
// ...
getLeaderboard() {
return Object.values(this.players)
.sort((p1, p2) => p2.score - p1.score)
.slice(0, 5)
.map(p => ({ username: p.username, score: Math.round(p.score) }));
}
createUpdate(player, leaderboard) {
const nearbyPlayers = Object.values(this.players).filter(
p => p !== player && p.distanceTo(player) <= Constants.MAP_SIZE / 2,
);
const nearbyBullets = this.bullets.filter(
b => b.distanceTo(player) <= Constants.MAP_SIZE / 2,
);
return {
t: Date.now(),
me: player.serializeForUpdate(),
others: nearbyPlayers.map(p => p.serializeForUpdate()),
bullets: nearbyBullets.map(b => b.serializeForUpdate()),
leaderboard,
};
}
}
getLeaderboard()
á á£á ááá - á°á«ááŸá¹á á áá€áµ áááµá£áᣠá ááµá±á áááµá³á á¥á ášá°á áá áµá á¥á áá¥á«áá³áá± áá¥á¥ áááá³áá¢
createUpdate()
ááµá¥ á¥á
á áá ááá update()
áá°á«ááŸáœ ášáášááá ášášáá³ áááááœá áááá áᢠááá á¥á«á ááŽááœá áá¥á«áµ áá serializeForUpdate()
ááááᜠášá°á°áá áš Player
О Bullet
. áá¥á«áá³áá± á°á«áᜠáášáá á¥á» á¥áá°áá«áµá°ááá áᥠáá á á
áᥠá°á«ááŸáœ á¥á áá®ááá¶áœ - ášá°á«áá¹ ááá áµáááá ášášáá³ áááᜠáášá ááµá°ááá á á«áµáááá!
3. á á áááá© áá ášášáá³ á¥áááœ
á á¥á ášáá³ ááµá¥ áá®ááá°á®áœ á¥á á°á«ááŸáœ á á£á á°áá³á³á áážáá¡ á¥áá± ášáá
ᣠáá¥á£ á°ááá³áᜠášášáá³ á¥ááᜠáážáᢠá á°á«ááŸáœ á¥á á áá®ááá¶áœ áá«ášá á«ááá á°áá³á³áááµ ááá áá ášáá ášáµ áááá á áá°áá á á¥áááá Object
:
ááá.js
class Object {
constructor(id, x, y, dir, speed) {
this.id = id;
this.x = x;
this.y = y;
this.direction = dir;
this.speed = speed;
}
update(dt) {
this.x += dt * this.speed * Math.sin(this.direction);
this.y -= dt * this.speed * Math.cos(this.direction);
}
distanceTo(object) {
const dx = this.x - object.x;
const dy = this.y - object.y;
return Math.sqrt(dx * dx + dy * dy);
}
setDirection(dir) {
this.direction = dir;
}
serializeForUpdate() {
return {
id: this.id,
x: this.x,
y: this.y,
};
}
}
á¥áá
ááá ášá°áá³á°á ááá ášáá. áá
ááá áá
á¥á«á á¥á© ááá
á
áááá. ááá á¥ááŽáµ á¥áá°áá á¥áá Bullet
áá ááá Object
:
á¥ááµ.js
const shortid = require('shortid');
const ObjectClass = require('./object');
const Constants = require('../shared/constants');
class Bullet extends ObjectClass {
constructor(parentID, x, y, dir) {
super(shortid(), x, y, dir, Constants.BULLET_SPEED);
this.parentID = parentID;
}
// Returns true if the bullet should be destroyed
update(dt) {
super.update(dt);
return this.x < 0 || this.x > Constants.MAP_SIZE || this.y < 0 || this.y > Constants.MAP_SIZE;
}
}
áµáá á« Bullet
á á£á á áá! ášáášááᢠObject
ášáášá°ááµ á
á¥á«áᜠá¥á»:
- á¥á
áá á áá áá
shortid áá áá£á áµáááµid
áá®áááµ. - ááµá áášáá
parentID
áá áá áá®áááµ ášáá ášáá á°á«áᜠáášá³á°á á¥ááµáµáœáᢠ- ášáááá» á¥áŽáµ á áášá áá
update()
, áá á áá á¥á©á áátrue
áá®ááá± ášááµášá© áá ášáá (á áášášá»á ááá áá áµááá áá³á á¥áá°á°ááááá á«áµá³áá±?)
áá°áá
á¥áááµ Player
:
á°á«ááœ.js
const ObjectClass = require('./object');
const Bullet = require('./bullet');
const Constants = require('../shared/constants');
class Player extends ObjectClass {
constructor(id, username, x, y) {
super(id, x, y, Math.random() * 2 * Math.PI, Constants.PLAYER_SPEED);
this.username = username;
this.hp = Constants.PLAYER_MAX_HP;
this.fireCooldown = 0;
this.score = 0;
}
// Returns a newly created bullet, or null.
update(dt) {
super.update(dt);
// Update score
this.score += dt * Constants.SCORE_PER_SECOND;
// Make sure the player stays in bounds
this.x = Math.max(0, Math.min(Constants.MAP_SIZE, this.x));
this.y = Math.max(0, Math.min(Constants.MAP_SIZE, this.y));
// Fire a bullet, if needed
this.fireCooldown -= dt;
if (this.fireCooldown <= 0) {
this.fireCooldown += Constants.PLAYER_FIRE_COOLDOWN;
return new Bullet(this.id, this.x, this.y, this.direction);
}
return null;
}
takeBulletDamage() {
this.hp -= Constants.BULLET_DAMAGE;
}
onDealtDamage() {
this.score += Constants.SCORE_BULLET_HIT;
}
serializeForUpdate() {
return {
...(super.serializeForUpdate()),
direction: this.direction,
hp: this.hp,
};
}
}
á°á«ááŸáœ ášáá®ááá¶áœ ášá áá ááµá¥áµá¥ áážá, áµááá
á¥ááµ á°ášá᪠ááµá®áœ á áá
ááá ááµá¥ áááᥠá áá£ážá. ášá¥á± áᎠupdate()
á¥á áµá«ááœá áá°á«á, á á°ááá, ááá á«ááá© á á²áµ ášá°áá ášáá áá®áááµ áááá³á fireCooldown
(á áá°áá ááá áµááá
áá³á á¥áá°á°ááááá á áµá³ááµ?) á¥áá²áá ááŽáá á«á«áááá serializeForUpdate()
, áááá«á±á á ášáá³ ááá ááµá¥ áá°á«áá¹ á°ášá᪠ááµá®áœá áá«á°áµ á áá¥á.
ášáá ášáµ ááá ááá Object
- á®áµá ááµáá áááµáááµ á áµááá á¥ááá. ááá³á, ááá ášáá Object
á¥á«áá³áá± ášášáá³ ááá á ááµ á áááµ á á°áá£á á ááášá ááá£á distanceTo()
, á¥á á¥ááá
á áá áµáá á«áᜠá á áá«á³ áááᜠáá áá
á³áµ á
á áµ áááá. áá
á á°áá ááµááá
áá®ááá¶áœ á áµááá áááá.ášáá°áá áá¥á á²ášáá Object
áááᜠá¥á«á°á áážá.
4. áááµá ááášáµ
áá¥á ášáášá á¥ážáá ááá áá®ááá± á°á«ááŸá¹á á²áá± ááášáµ áá! áá
áá á®áµ ášáµáá± ááµá¥ á«áµá³áá± update()
á ááá ááµá¥ Game
:
ášáá³.js
const applyCollisions = require('./collisions');
class Game {
// ...
update() {
// ...
// Apply collisions, give players score for hitting bullets
const destroyedBullets = applyCollisions(
Object.values(this.players),
this.bullets,
);
destroyedBullets.forEach(b => {
if (this.players[b.parentID]) {
this.players[b.parentID].onDealtDamage();
}
});
this.bullets = this.bullets.filter(
bullet => !destroyedBullets.includes(bullet),
);
// ...
}
}
ááŽáá á°áá£á«á ááµášá á áá¥á applyCollisions()
, áá
á á°á«ááŸáœá ášááá±áµá áááá áá®ááá¶áœ áááá³á. á¥áá° á¥áµá áá, áá
á ááµášá ášá£áµ á áá°áá áááá«á±á
- ááá ášááá© ááá®áœ áá áŠáœ áážá, áá á ášáááµ áááá«á ááá°áá á ááá á áᜠáá.
- á áµááµáá áᎠá áá
distanceTo()
, á ááá ááµá¥ á£ááá ááá ááµá¥ á°áá£á«á á«á°ášáááObject
.
ášá¥á ášáááµ áááá« á á°áá£á á áá áááµááá¢
ááá¶áœ.js
const Constants = require('../shared/constants');
// Returns an array of bullets to be destroyed.
function applyCollisions(players, bullets) {
const destroyedBullets = [];
for (let i = 0; i < bullets.length; i++) {
// Look for a player (who didn't create the bullet) to collide each bullet with.
// As soon as we find one, break out of the loop to prevent double counting a bullet.
for (let j = 0; j < players.length; j++) {
const bullet = bullets[i];
const player = players[j];
if (
bullet.parentID !== player.id &&
player.distanceTo(bullet) <= Constants.PLAYER_RADIUS + Constants.BULLET_RADIUS
) {
destroyedBullets.push(bullet);
player.takeBulletDamage();
break;
}
}
}
return destroyedBullets;
}
áá ááá ášáááµ áááµ á á¥ááá³á áá ášá°áá°ášá° áá á ááášáá»ážá áá«ášá á«áá áááµ áš á«á²á®á»ážá áµáá á«áá° ášáá áááµ áá áŠáœ ááá«á. á áááµ áá áŠáœ ááášáᜠáá«ášá á«áá áááµ á áµááá ášá«á²á«ážá áµáá áá á¥á©á ášááá áµ ááá³ á¥áá á áá¢
á¥áá
áá³á°á¥á£ážá ášááá¡ áááµ á°ášá᪠ááœá³áᜠá á-
- áá®ááá± ášáá ášáá á°á«áᜠááá³áµ ášáá áµáᢠáá
á ááááá ááá ááœáá
bullet.parentID
Ñplayer.id
. - á¥á á°á«ááŸáœ á á°áá³á³á áá á²áá© áá®ááá± á ááµ áá á¥á» ááá³áµ á áá áµá¢ áŠáá¬á°á©á á áá áá áá
áá áœáá á¥ááá³ááá
break
: ášáá®ááá± áá ášááá á°á«áá¹ á¥áá°á°áá ááááá á ááá áá°ááá¥áá áá®áááµ á¥ááá¥áááá¢
áášášá»á
ááŒá áá! áš.io áµá ášáá³ áááá á ááá á«áá¥ááµá áááá ááá ážááááᢠáá¥á áá á á? ášá«áµáá áš.io ášáá³ áááá¡!
ááá ášááá á®áµ áááµ ááá á¥á ášá°áá á ááá¢
ááá: hab.com