ืืฆื ื-2015
ืืืงืจื ืฉืืขืืื ืื ืฉืืขืชื ืขื ืืืฉืืงืื ืืืื ืืขืืจ, ืืื ืื ืืฉืืงื ืืื ืืจื ื ืืจืืื ืืฉืชืชืคืื ืืืื ื ืฉืงื ืืฉืืง ืืื (ืืื ืฆืืจื ืืืฉืืื). ืืืจื ืืื ืื ืืชืืืืืื ืขื ืฉืืงื ืื ืืจืืืื ืจืืื ืืืืชื ืืืจื. ืืฉืืงื io ืืคืืจืกืืื ืืืจืื:
ืืคืืกื ืื ื ืืืืง ืืืฆื ืฆืืจ ืืฉืืง .io ืืืคืก. ืืฉืืื ืื, ืจืง ืืืข ื-Javascript ืืกืคืืง: ืืชื ืฆืจืื ืืืืื ืืืจืื ืืื ืชืืืืจ this
ะธ
ืืืืื ืืืฉืืง .io
ืืกืืืข ืืืืืื, ื ืคื ื ื
ืืืฉืืง ืื ืคืฉืื: ืืชื ืฉืืื ืืกืคืื ื ืืืืจื ืฉืื ืืฉ ืฉืืงื ืื ืืืจืื. ืืกืคืื ื ืฉืื ืืืจื ืงืืืขืื ืืืืืืืืช ืืืชื ืื ืกื ืืคืืืข ืืฉืืงื ืื ืืืจืื ืชืื ืืื ืืืื ืขืืช ืืืงืืืขืื ืฉืืื.
1. ืกืงืืจื ืงืฆืจื / ืืื ื ืืคืจืืืงื
ืื ื ืืืืืฅ
ืืืืจืื ืงืื ืืงืืจ ืืฉืืง ืืืืืื ืืื ืฉืชืืื ืืขืงืื ืืืจื.
ืืืืืื ืืฉืชืืฉืช ืืืืจืื ืืืืื:
ืืงืกืคืจืก ืืื ืืกืืจืช ืืืื ืืจื ื ืืคืืคืืืจืืช ืืืืชืจ ืฉื Node.js ืืื ืืืช ืืช ืฉืจืช ืืืื ืืจื ื ืฉื ืืืฉืืง.socket.io - ืกืคืจืืืช websocket ืืืืืคืช ื ืชืื ืื ืืื ืืคืืคื ืืฉืจืช.Webpack - ืื ืื ืืืืื. ืืชื ืืืื ืืงืจืื ืขื ืืื ืืืฉืชืืฉ ื-Webpack.ืืื .
ืื ื ืจืื ืืื ื ืกืคืจืืืช ืืคืจืืืงื:
public/
assets/
...
src/
client/
css/
...
html/
index.html
index.js
...
server/
server.js
...
shared/
constants.js
ืฆืืืืจื /
ืืื ืืชืืงืืื public/
ืืืืฉ ืืืืคื ืกืืื ืขื ืืื ืืฉืจืช. IN 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
ืืื ื ืงืืืช ืืื ืืกื ืฉื ืืงืื Javascript (JS). Webpack ืืชืืื ืืืื ืืืืคืฉ ืืืืคื ืจืงืืจืกืืื ืืืจ ืงืืฆืื ืืืืืืื ืืืจืื.- ืืคืื JS ืฉื ื-Webpack build ืฉืื ื ืืืื ืืืืงื ืืกืคืจืืื
dist/
. ืื ื ืืงืจื ืืงืืืฅ ืืื ืฉืื ื ืืืืืช js. - ืื ื ืืฉืชืืฉืื
ืืื , ืืืคืจื ืืชืฆืืจื@babel/preset-env ืืืืจืช ืงืื JS ืฉืื ื ืืืคืืคื ืื ืืฉื ืื ืืืชืจ. - ืื ื ืืฉืชืืฉืื ืืชืืกืฃ ืืื ืืืืฅ ืืช ืื ื-CSS ืืืื ืืชืืืืกืื ืงืืฆื JS ืืืฉืื ืืืชื ืืืงืื ืืื. ืื ื ืืงืจื ืื ืฉืื ื ืืืืืช css.
ืืืื ืฉืืช ืื ืืฉืืืช ืงืืฆืื ืืืืจืื ืฉื ืืืืืืช '[name].[contenthash].ext'
. ืื ืืืืืื [name]
ืืืืืฃ ืืฉื ืฉื ื ืงืืืช ืืงืื (ืืืงืจื ืฉืื ื, ืื game
), ื [contenthash]
ืืืืืฃ ื-hash ืฉื ืชืืื ืืงืืืฅ. ืื ืื ื ืขืืฉืื ืืช ืื ืืื 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>
ืืื ืืืืกืืฃ ืืช ืืืืืช ื-Javascript ืฉืื ื.- ืชืคืจืื ืจืืฉื ืขื ืฉื ืืฉืชืืฉ
<input>
ืืืคืชืืจ ื-PLAY (<button>
).
ืืืืจ ืืขืื ืช ืืฃ ืืืืช, ืืืคืืคื ืืชืืื ืืืคืขืื ืงืื Javascript, ืืื ืืงืืืฅ 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 (ืื ืฉ-Webpack ืืืืขืช ืืืืื ืืืชื ืืืืืืช ื-CSS ืฉืื ื).
- ืืคืขืื
connect()
ืืืฆืืจ ืืืืืจ ืขื ืืฉืจืช ืืืืคืขืืdownloadAssets()
ืืื ืืืืจืื ืชืืื ืืช ืืืจืืฉืืช ืืขืืืื ืืืฉืืง. - ืืืืจ ืกืืื ืฉืื 3 ืืชืคืจืื ืืจืืฉื ืืืฆื (
playMenu
). - ืืืืจืช ืืืืคื ืืืืืฆื ืขื ืืคืชืืจ "PLAY". ืืืฉืจ ืืืคืชืืจ ื ืืืฅ, ืืงืื ืืืชืื ืืช ืืืฉืืง ืืืืืจ ืืฉืจืช ืฉืื ื ืืืื ืื ืืฉืืง.
ื"ืืฉืจ" ืืขืืงืจื ืฉื ืืืืืืื ืฉื ืฉืจืช-ืืงืื ืฉืื ื ืืื ืืืืชื ืงืืฆืื ืฉืืืืื ืขื ืืื ืืงืืืฅ index.js
. ืืขืช ื ืฉืงืื ืืช ืืืื ืืคื ืืกืืจ.
4. ืืืืคืช ื ืชืื ื ืืงืืืืช
ืืืฉืืง ืื, ืื ื ืืฉืชืืฉืื ืืกืคืจืืื ืืืืขื ืืื ืืชืงืฉืจ ืขื ืืฉืจืช
ืืืื ืื ื ืงืืืฅ ืืื src/client/networking.js
ืื ืืืื ืื ืชืงืฉืืจืช ืขื ืืฉืจืช:
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);
};
ืงืื ืื ืื ืืชืงืฆืจ ืืขื ืืฆืืจื ืืืืืจืืช.
ืืฉ ืฉืืืฉ ืคืขืืืืช ืขืืงืจืืืช ืืงืืืฅ ืืื:
- ืื ืื ื ืื ืกืื ืืืชืืืจ ืืฉืจืช.
connectedPromise
ืืืชืจ ืจืง ืืืฉืจ ืืฆืจื ื ืืืืืจ. - ืื ืืืืืืจ ืืฆืืื, ืื ื ืจืืฉืืื ืคืื ืงืฆืืืช ืืชืงืฉืจืืช ืืืืจืช (
processGameUpdate()
ะธonGameOver()
) ืืืืืขืืช ืฉืื ื ืืืืืื ืืงืื ืืืฉืจืช. - ืื ืื ื ืืืืฆืืื
play()
ะธupdateDirection()
ืืื ืฉืงืืฆืื ืืืจืื ืืืืื ืืืฉืชืืฉ ืืื.
5. ืขืืืื ืืงืื
ืื ืืืื ืืืฆืื ืืช ืืชืืื ื ืขื ืืืกื!
...ืืื ืืคื ื ืฉื ืืื ืืขืฉืืช ืืืช, ืขืืื ื ืืืืจืื ืืช ืื ืืชืืื ืืช (ืืืฉืืืื) ืืืจืืฉืื ืืฉื ืื. ืืื ื ืืชืื ืื ืื ืืฉืืืื:
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
, ืฉืืงืฉืจ ืืช ืืืคืชื ืฉื ืฉื ืืงืืืฅ ืืขืจื ืืืืืืืงื Image
. ืืืฉืจ ืืืฉืื ื ืืขื, ืื ื ืืืืกื ืื ืืืชื ืืืืืืืงื assets
ืืืืฉื ืืืืจื ืืขืชืื. ืืชื ืื ืืฉืื ืืืื ืืืจืฉื ืืืืจืื (ืืืืืจ, ืื ืืฉืืืื), ืื ื ืืืคืฉืจืื downloadPromise
.
ืืืืจ ืืืจืืช ืืืฉืืืื, ืชืืื ืืืชืืื ืืขืืืื. ืืคื ืฉื ืืืจ ืงืืื ืืื, ืืื ืืฆืืืจ ืขื ืืฃ ืืื ืืจื ื, ืื ื ืืฉืชืืฉืื <canvas>
). ืืืฉืืง ืฉืื ื ืื ืคืฉืื, ืื ืื ืื ื ืฆืจืืืื ืจืง ืืฆืืืจ ืืช ืืืืจืื ืืืืื:
- ะคะพะฝ
- ืกืคืื ืช ืฉืืงื ืื
- ืฉืืงื ืื ืืืจืื ืืืฉืืง
- ืคืืืื
ืื ื ืืงืืขืื ืืืฉืืืื src/client/render.js
, ืืืฆืืืื ืืืืืง ืืช ืืจืืขืช ืืคืจืืืื ืืืคืืจืืื ืืืขืื:
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()
ืืื ืืคืื ืงืฆืื ืืขืืงืจืืช ืฉื ืงืืืฅ ืื. startRendering()
ะธ stopRendering()
ืืฉืืื ืืืคืขืืช ืืืืืช ืืขืืืื ื-60 FPS.
ืืืฉืืืื ืงืื ืงืจืืืื ืฉื ืคืื ืงืฆืืืช ืขืืืจืืช ืขืืืื ืืืืืืช (ืืืฉื. renderBullet()
) ืื ืื ืื ืืฉืืืื, ืืื ืื ื ืืืืื ืืืช ืคืฉืืื:
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,
);
}
ืฉืืื ืื ืฉืื ื ืืฉืชืืฉืื ืืฉืืื getAsset()
, ืฉื ืจืื ืืขืืจ ื asset.js
!
ืื ืืชื ืืขืื ืืื ืืืืื ืขื ืขืืืจื ืขืืืื ืืืจืื, ืงืจื ืืช ืืฉืืจ.
src/client/render.js .
6. ืงืื ืืงืื
ืืืืข ืืืื ืืขืฉืืช ืืฉืืง ื ืืชื ืืฉืืง! ืขืจืืช ืืืงืจื ืชืืื ืคืฉืืื ืืืื: ืืื ืืฉื ืืช ืืช ืืืืื ืืชื ืืขื, ืืชื ืืืื ืืืฉืชืืฉ ืืขืืืจ (ืืืืฉื) ืื ืืืขืช ืืืกื (ืืืืฉืืจ ื ืืื). ืืื ืืืืฉื ืืืช, ื ืืจืฉื
ืืืื ืืื ืื src/client/input.js
:
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()
ืื ืืืืื ื ืืืจืืขืื ืฉืืชืงืฉืจืื updateDirection()
(ืืชืื networking.js
) ืืืฉืจ ืืชืจืืฉ ืืืจืืข ืงืื (ืืืืืื, ืืืฉืจ ืืขืืืจ ืืืื). updateDirection()
ืืืคื ืืืขืืจืช ืืืืขืืช ืขื ืืฉืจืช, ืฉืืืคื ืืืืจืืข ืืงืื ืืืขืืื ืืช ืืฆื ืืืฉืืง ืืืชืื.
7. ืกืืืืก ืืงืื
ืืงืืข ืืื ืืื ืืงืฉื ืืืืชืจ ืืืืง ืืจืืฉืื ืฉื ืืคืืกื. ืื ืชืชืืืืฉ ืื ืืชื ืื ืืืื ืืช ืื ืืคืขื ืืจืืฉืื ื ืฉืืชื ืงืืจื ืืช ืื! ืืชื ืืืื ืืคืืื ืืืื ืขืืื ืืืืืืจ ืืืื ืืืืืจ ืืืชืจ.
ืืืืง ืืืืจืื ืืคืืื ืืืจืืฉ ืืืฉืืืช ืงืื ืืืงืื/ืฉืจืช ืืื ืืื. ืืืืจืื ืืช ืงืืข ืืงืื ืืงืืข ืขืืืื ืืืงืื?
render.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: ืืืืข ืขื ืื ืื ืฉืืงืื ืขืืืื ืื.
- ืืืจืื: ืืขืจื ืืืืข ืขื ืฉืืงื ืื ืืืจืื ืืืฉืชืชืคืื ืืืืชื ืืฉืืง.
- ืืืืจืื: ืืขืจื ืืืืข ืขื ืงืืืขืื ืืืฉืืง.
- leaderboard: ื ืชืื ื Leaderboard ื ืืืืืื. ืืคืืกื ืื, ืื ื ืฉืงืื ืืืชื.
7.1 ืืฆื ืืงืื ื ืืืื
ืืืฉืื ื ืืืื 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 ืคืขืืื ืืฉื ืืื, ืื ืืืฆืืช ืืืฉืืืืช ืืืื ืคืฉืื ืืฆืืืจื ืืืืฉ ืืช ืืืชื ืืืืจ, ืืืขืฆื ืื ืืขืฉื ืืืื. ืืขืื ื ืืกืคืช ืืืืฉืื ืื ืืืื ืืื ืฉืื ืืืขืืื ืืขืืืืืื. ืขื ืืืืจืืช ืืื ืืจื ื ืืืืืืืืช, ืืืงืื ืืงืื ืขืืืื ืืฉืืง ืืืืืง ืื 33ms (30 ืืฉื ืืื):
ืืืจืื ืืฆืขืจ, ืฉืื ืืืจ ืืื ื ืืืฉืื. ืชืืื ื ืืฆืืืืชืืช ืืืชืจ ืชืืื:
ืืืืฉืื ืื ืืืื ืืื ืืืขืฉื ืืืงืจื ืืืจืืข ืืืืชืจ ืืื ืื ืืืข ื-latency. ืื ืขืืืื ืืฉืืง ืืชืงืื ืืืืืืจ ืฉื 50ms, ืื ืืืื ื ืืงืืืืช ืชืืกืคืช ืฉื 50 ืืืคืืืช ืืฉื ืืื ืืืืืื ืฉืืื ืขืืืื ืืฆืืื ืืช ืืฆื ืืืฉืืง ืืืขืืืื ืืงืืื. ืืชื ืืืืืื ืืชืืจ ืืขืฆืืื ืืื ืื ืื ื ืื ืืฉืืงื: ืืืืื ืฉืจืืจืืชืืช ืชืืจืื ืืืฉืืง ืืืจืืืฉ ืงืืคืฆื ื ืืื ืืฆืื.
7.2 ืืฆื ืืงืื ืืฉืืคืจ
ื ืืฆืข ืืื ืฉืืคืืจืื ืืืืฉืื ืื ืืืื. ืจืืฉืืช, ืื ื ืืฉืชืืฉืื ืขืืืื ืืขืืืื ืืืฉื 100 ืืืคืืืช ืืฉื ืืื. ืื ืืืืจ ืฉืืืฆื ื"ื ืืืื" ืฉื ืืืงืื ืชืืื ืืคืืจ ืืืจื ืืฆื ืืืฉืืง ืืฉืจืช ื-100ms. ืืืืืื, ืื ืืฉืขื ืืฉืจืช ืืื 150, ืื ืืืงืื ืืฆืื ืืช ืืืฆื ืฉืื ืืื ืืฉืจืช ืืืืชื ืืื 50:
ืื ื ืืชื ืื ื ืืืืจ ืฉื 100ms ืืื ืืฉืจืื ืืื ื ืขืืืื ืืืชื ืฆืคืืืื ืฉื ืืืฉืืง:
ืืชืืืจื ืขืืืจ ืื ืชืืื ืงืืืขื
ืื ืื ื ืืืืืื ืื ืืืฉืชืืฉ ืืืื ืืงื ืืืจืช ืฉื ืงืจืืช
ืืืืื ืืฆื ืืืงืื , ืฉืขืืฉื ืขืืืื ืืืื ืืืคืืชืช ืืฉืืืื ืื ืชืคืกืช, ืื ืื ืืกืืงืจ ืืคืืกื ืืื.
ืฉืืคืืจ ื ืืกืฃ ืฉืื ื ืืฉืชืืฉืื ืื ืืื ืืื ืืจืคืืืฆืื ืืื ืืจืืช. ืขืงื ืคืืืืจ ืืขืืืื, ืืืจื ืืื ืื ืื ื ืืงืืืืื ืืคืืืช ืขืืืื ืืื ืืืืื ืื ืืืื ืืืงืื. ืืฉืืชืงืฉืจืื getCurrentState()
, ืื ืื ื ืืืืืื ืืืฆืข
ืื ืคืืชืจ ืืช ืืขืืืช ืงืฆื ืืคืจืืืืื: ืืขืช ื ืืื ืืืฆืื ืคืจืืืืื ืืืืืืืื ืืื ืงืฆื ืคืจืืืืื ืฉื ืจืฆื!
7.3 ืืืฉืื ืืฆื ืืงืื ืืฉืืคืจ
ืืืืื ืืืืฉืื ื src/client/state.js
ืืฉืชืืฉ ืื ื-Rend lag ืืื ืืืื ืืจืคืืืฆืื ืืื ืืืจืืช, ืืื ืื ืืืืจื ืืื. ืืืื ื ืฉืืืจ ืืช ืืงืื ืืฉื ื ืืืงืื. ืื ื ืืจืืฉืื:
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 ืืืืืจื ืืฉืจืช, ืืื ืืขืืื ืื ื ืืข ืืช ืืฉืขื ืื ืืืืืช ืืฉืจืช, ืื ืื ืื ื ืื ืืืืืื ืืืขืช ืืื ืืื ืืงื ืขื ืฉืืฃ ืืื ืืืขืืืื ืื ืืืืข ืืืื ื. ืืืื ืืจื ื ืืื ื ืฆืคืื ืืืืืจืืชื ืขืฉืืื ืืืฉืชื ืืช ืืืื!
ืืื ืืขืงืืฃ ืืช ืืืขืื ืืื, ืื ืื ื ืืืืืื ืืืฉืชืืฉ ืืงืืจืื ืกืืืจ: ืื ืื ื ืืืขืืื ืคื ืื ืฉืืขืืืื ืืจืืฉืื ืืืืข ืืื. ืื ืื ืืื ื ืืื, ืื ืืืื ื ืืืืขืื ืืช ืืื ืืฉืจืช ืืจืืข ืืืกืืื ืืื! ืื ื ืืืืกื ืื ืืช ืืืชืืช ืืืื ืฉื ืืฉืจืช firstServerTimestamp
ืืืฉืืืจ ืขื ืฉืื ื ืืงืืื ืืืชืืช ืืื (ืืงืื) ืืืืชื ืจืืข ื gameStart
.
ืื ืืื. ืื ืื ืืืืจ ืืืืืช ืืื ืฉืจืช = ืืื ืืงืื? ืืืืข ืื ื ืืืืื ืื ืืื "ืืืชืืช ืืื ืฉื ืฉืจืช" ื"ืืืชืืช ืืื ืฉื ืืงืื"? ืื ืฉืืื ืืฆืืื ืช! ืืกืชืืจ ืฉืื ืื ืืืชื ืืืจ. Date.now()
ืืืืืจ ืืืชืืืช ืืื ืฉืื ืืช ืืืงืื ืืืฉืจืช, ืืื ืชืืื ืืืืจืืื ืืงืืืืื ืืืืื ืืช ืืื. ืืขืืื ืื ืชื ืื ืฉืืืชืืืช ืืืื ืืืื ืืืืช ืืื ืืืืื ืืช.
ืขืืฉืื ืื ืื ื ืืืื ืื ืื ืขืืฉื currentServerTime()
: ืื ืืืืจ ืืืชืืช ืืืื ืฉื ืืฉืจืช ืฉื ืืื ืืขืืืื ืื ืืืื. ืืืืืื ืืืจืืช, ืื ืืืื ืื ืืืื ืฉื ืืฉืจืช (firstServerTimestamp <+ (Date.now() - gameStart)
) ืืื ืืก ืขืืืื ืืขืืืื (RENDER_DELAY
).
ืขืืฉืื ืืืื ื ืกืชืื ืขื ืืื ืื ืื ื ืืืคืืื ืืขืืืื ื ืืฉืืงืื. ืืืฉืจ ืืชืงืื ืืฉืจืช ืืขืืืื ืื, ืืื ื ืงืจื processGameUpdate()
ืืื ืื ื ืฉืืืจืื ืืช ืืขืืืื ืืืืฉ ืืืขืจื gameUpdates
. ืืืืจ ืืื, ืืื ืืืืืง ืืช ืืฉืืืืฉ ืืืืืจืื, ืื ื ืืกืืจืื ืืช ืื ืืขืืืื ืื ืืืฉื ืื ืืคื ื ืื ืขืืืื ืืกืืกืื ืื ืื ื ืื ืฆืจืืืื ืืืชื ืืืชืจ.
ืืื "ืขืืืื ืืกืืกื"? ืึถื ืืช ืืขืืืื ืืจืืฉืื ืื ื ืืืฆืืื ืขื ืืื ืืขืืจ ืืืืจื ืืืฉืขื ืื ืืืืืช ืฉื ืืฉืจืช. ืืืืจืื ืืช ืืชืจืฉืื ืืื?
ืขืืืื ืืืฉืืง ืืฉืืจืืช ืืฉืืื ื-"Client Render Time" ืืื ืขืืืื ืืืกืืก.
ืืื ืืฉืืฉ ืขืืืื ืืืกืืก? ืืื ืื ืื ื ืืืืืื ืืืืจืื ืขืืืื ืื ืืงื ืืืกืืก? ืืื ืืืืื ืืช ืื, ืืืื ะะฐะบะพะฝะตั-ัะพ ืืฉืงืื ืืช ืืืืฉืื 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
:
server.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? ืื ืืืงืื ืฉืื ื ืฉืชืืฉ ืืชืฆืืจืืช ื-Webpack ืฉืื ื. ื ืฉืชืืฉ ืืื ืืฉืชื ืืจืืื:
- ืืืฉืชืืฉ
webpack-dev-middleware ืืื ืืช ืืืืฉ ืืืืคื ืืืืืืื ืืช ืืืืืืช ืืคืืชืื ืฉืื ื, ืื - ืืขืืจ ืชืืงืืื ืกืืืืช
dist/
, ืฉืืชืืื Webpack ืืืชืื ืืช ืืงืืฆืื ืฉืื ื ืืืืจ ืื ืืืช ืืืืฆืืจ.
ืขืื ืืฉืืื ืืฉืืื server.js
ืื ืืืืืืจ ืืช ืืฉืจืช
server.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
:
server.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
ืฉืงืข socket.io ืฉืืื (ืื ืืชื ืืชืืืื, ืืืืจ ืื 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
:
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
:
bullet.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
ืจืง ืืช ืืืจืืืืช ืืืืืช:
- ืฉืืืืฉ ืืืืืื
ืงืฆืจืื ืืืืจ ืืงืจืืid
ืงึถืึทืข. - ืืืกืคืช ืฉืื
parentID
ืืื ืฉืชืืื ืืขืงืื ืืืจ ืืฉืืงื ืฉืืฆืจ ืืช ืืงืืืข ืืื. - ืืืกืคืช ืขืจื ืืืืจื ื
update()
, ืฉืฉืืื ืtrue
ืื ืืงืืืข ื ืืฆื ืืืืฅ ืืืืจื (ืืืืจืื ืฉืืืืจื ื ืขื ืื ืืกืขืืฃ ืืืืจืื?).
ืืืื ื ืขืืืจ ื Player
:
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
:
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
.
ืื ื ืจืื ืืืืฉืื ืฉืื ื ืฉื ืืืืื ืืชื ืืฉืืช:
collisions.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 ืืฉืื!
ืื ืืงืื ืืืืืื ืืื ืงืื ืคืชืื ืืืชืคืจืกื ื-
ืืงืืจ: www.habr.com