2015幎çºå£²
ãããã®ã²ãŒã ã«ã€ããŠèããããšããªãå Žåã¯ããããã¯ç°¡åã«ãã¬ã€ã§ããç¡æã®ãã«ããã¬ã€ã€ãŒ Web ã²ãŒã ã§ã (ã¢ã«ãŠã³ãã¯å¿
èŠãããŸãã)ã 圌ãã¯éåžžãåãã¢ãªãŒãã§å€ãã®æµå¯Ÿãã¬ã€ã€ãŒãšå¯ŸæŠããŸãã ãã®ä»ã®æå㪠.io ã²ãŒã :
ãã®æçš¿ã§ã¯ããã®æ¹æ³ã«ã€ããŠèª¬æããŸã .io ã²ãŒã ãæåããäœæããã ãã®ããã«ã¯ãJavaScript ã®ç¥èã ãã§ååã§ããæ§æãªã©ãç解ããå¿
èŠããããŸãã this
О
.io ã²ãŒã ã®äŸ
åŠç¿æ¯æŽã«ã€ããŠã¯ã以äžãåç
§ããŠãã ããã
ã²ãŒã ã¯éåžžã«ã·ã³ãã«ã§ããä»ã®ãã¬ã€ã€ãŒãããã¢ãªãŒãã§è¹ãå¶åŸ¡ããŸãã ããªãã®è¹ã¯èªåçã«çºå°ç©ãçºå°ããä»ã®ãã¬ã€ã€ãŒã®çºå°ç©ãé¿ããªããæ»æãè©Šã¿ãŸãã
1. ãããžã§ã¯ãã®æŠèŠã»æ§é
æšå¥šããŸã
ãœãŒã¹ã³ãŒããããŠã³ããŒããã ãµã³ãã«ã²ãŒã ãªã®ã§ãã©ããŒããŠãã ããã
ãã®äŸã§ã¯æ¬¡ã®ãã®ã䜿çšããŸãã
ãšã¯ã¹ãã¬ã¹ ã¯ãã²ãŒã ã® Web ãµãŒããŒã管çããæã人æ°ã®ãã Node.js Web ãã¬ãŒã ã¯ãŒã¯ã§ãããœã±ãã.io - ãã©ãŠã¶ãšãµãŒããŒéã§ããŒã¿ã亀æããããã® WebSocket ã©ã€ãã©ãªã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
JavaScript (JS) ã¯ã©ã€ã¢ã³ãã®ãšã³ã㪠ãã€ã³ãã§ãã Webpack ã¯ããããéå§ãããä»ã®ã€ã³ããŒãããããã¡ã€ã«ãååž°çã«æ€çŽ¢ããŸãã- Webpack ãã«ãã®åºå JS ã¯æ¬¡ã®ãã£ã¬ã¯ããªã«ãããŸãã
dist/
ã ãã®ãã¡ã€ã«ãç§ãã¡ã®ãã¡ã€ã«ãšåŒã³ãŸã jsããã±ãŒãž. - ã䜿çšããŠãããŸã
ããã« ãç¹ã«æ§æ@babel/ããªã»ããç°å¢ å€ããã©ãŠã¶çšã® JS ã³ãŒãããã©ã³ã¹ãã€ã«ããŸãã - ãã©ã°ã€ã³ã䜿çšããŠãJS ãã¡ã€ã«ã«ãã£ãŠåç §ããããã¹ãŠã® CSS ãæœåºãããããã XNUMX ãæã«çµåããŠããŸãã ãããç§ãã¡ã®ãã®ãšåŒã³ãŸã 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
ãããŠWebãã©ãŠã¶ã«ç§»åããŸã
3. ã¯ã©ã€ã¢ã³ãã®ãšã³ããªãã€ã³ã
ã²ãŒã ã®ã³ãŒãèªäœã«åãæãããŸãããã ãŸãããŒãžãå¿
èŠã§ã index.html
, ãµã€ãã«ã¢ã¯ã»ã¹ãããšããã©ãŠã¶ãæåã«ãµã€ããèªã¿èŸŒã¿ãŸãã ç§ãã¡ã®ããŒãžã¯éåžžã«ã·ã³ãã«ã«ãªããŸã:
index.htmlã
.io ã²ãŒã ã®äŸ éã¶
ãã®ã³ãŒãäŸã¯ããããããããããã«å°ãç°¡ç¥åãããŠãããä»ã®å€ãã®æçš¿äŸã§ãåæ§ã«ããŸãã å®å
šãªã³ãŒãã¯ãã€ã§ã次ã®å Žæã§ç¢ºèªã§ããŸãã
æã ã¯æã£ãŠããŸãïŒ
HTML5 ãã£ã³ãã¹èŠçŽ (<canvas>
) ãããã²ãŒã ã®ã¬ã³ããªã³ã°ã«äœ¿çšããŸãã<link>
CSS ããã±ãŒãžãè¿œå ããŸãã<script>
JavaScript ããã±ãŒãžãè¿œå ããŸãã- ãŠãŒã¶ãŒåä»ãã®ã¡ã€ã³ã¡ãã¥ãŒ
<input>
ããã³PLAYãã¿ã³ïŒ<button>
).
ããŒã ããŒãžãããŒãããåŸããã©ãŠã¶ã¯ãšã³ã㪠ãã€ã³ãã® JS ãã¡ã€ã«ããå§ãŸã JavaScript ã³ãŒãã®å®è¡ãéå§ããŸãã 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 ããã±ãŒãžã« CSS ãå«ããããšãèªèããŸã)ã
- èµ·åãã
connect()
ãµãŒããŒãšã®æ¥ç¶ã確ç«ããŠå®è¡ããã«ã¯downloadAssets()
ã²ãŒã ã®ã¬ã³ããªã³ã°ã«å¿ èŠãªã€ã¡ãŒãžãããŠã³ããŒãããŸãã - ã¹ããŒãž3çµäºåŸ ã¡ã€ã³ã¡ãã¥ãŒã衚瀺ãããŸãïŒ
playMenu
). - ãPLAYããã¿ã³ãæŒãããšãã®ãã³ãã©ãèšå®ããŸãã ãã¿ã³ãæŒããããšãã³ãŒãã¯ã²ãŒã ãåæåãããã¬ã€ããæºåãã§ããããšããµãŒããŒã«äŒããŸãã
ã¯ã©ã€ã¢ã³ããµãŒããŒããžãã¯ã®äž»ãªãæ žå¿ãã¯ããã¡ã€ã«ã«ãã£ãŠã€ã³ããŒãããããã¡ã€ã«ã«ãããŸãã index.js
ã ããã§ã¯ãããããã¹ãŠãé çªã«æ€èšããŠãããŸãã
4. 顧客ããŒã¿ã®äº€æ
ãã®ã²ãŒã ã§ã¯ãããç¥ãããã©ã€ãã©ãªã䜿çšããŠãµãŒããŒãšéä¿¡ããŸãã
ãã¡ã€ã«ã¯ XNUMX ã€ãããŸã 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);
};
ãã®ã³ãŒãããããããããããããã«ãããã«ççž®ãããŠããŸãã
ãã®ãã¡ã€ã«ã«ã¯ XNUMX ã€ã®äž»èŠãªã¢ã¯ã·ã§ã³ããããŸãã
- ãµãŒããŒã«æ¥ç¶ããããšããŠããŸãã
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
.
ãªãœãŒã¹ãããŠã³ããŒãããããã¬ã³ããªã³ã°ãéå§ã§ããŸãã åã«è¿°ã¹ãããã«ãç§ãã¡ã䜿çšããWebããŒãžã«æç»ããã«ã¯ <canvas>
ïŒã ç§ãã¡ã®ã²ãŒã ã¯éåžžã«åçŽãªã®ã§ã次ã®ãã®ãæç»ããã ãã§æžã¿ãŸãã
- èæ¯
- ãã¬ã€ã€ãŒã®è¹
- ã²ãŒã å ã®ä»ã®ãã¬ã€ã€ãŒ
- ã·ã§ã«
ããã«éèŠãªã¹ããããããããŸã src/client/render.js
ãäžèšã® XNUMX ã€ã®é
ç®ãæ£ç¢ºã«ã¬ã³ããªã³ã°ããŸãã
ã¬ã³ããªã³ã°.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()
) ã¯ããã»ã©éèŠã§ã¯ãããŸããããç°¡åãªäŸã XNUMX ã€ç€ºããŸãã
ã¬ã³ããªã³ã°.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. ã¯ã©ã€ã¢ã³ãã®ã¹ããŒã¿ã¹
ãã®ã»ã¯ã·ã§ã³ã¯ãæçš¿ã®æåã®éšåã®äžã§æãé£ããã§ãã åããŠèªãã ãšãã«ç解ã§ããªããŠããã£ããããªãã§ãã ããã ã¹ãããããŠåŸã§æ»ã£ãŠããããšãã§ããŸãã
ã¯ã©ã€ã¢ã³ã/ãµãŒã㌠ã³ãŒããå®æãããããã«å¿ èŠãªããºã«ã®æåŸã®ããŒã¹ã¯æ¬¡ã®ãšããã§ãã ç¶æ ã ãã¯ã©ã€ã¢ã³ã ã¬ã³ããªã³ã°ãã»ã¯ã·ã§ã³ã®ã³ãŒã ã¹ãããããèŠããŠããŸãã?
ã¬ã³ããªã³ã°.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
}
]
}
åã²ãŒã æŽæ°ã«ã¯ XNUMX ã€ã®åäžã®ãã£ãŒã«ããå«ãŸããŠããŸãã
- t: ãã®æŽæ°ããã€äœââæããããã瀺ããµãŒããŒã®ã¿ã€ã ã¹ã¿ã³ãã
- me: ãã®ã¢ããããŒããåä¿¡ãããã¬ã€ã€ãŒã«é¢ããæ å ±ã
- ä»äºº: åãã²ãŒã ã«åå ããŠããä»ã®ãã¬ã€ã€ãŒã«é¢ããäžé£ã®æ å ±ã
- 匟䞞: ã²ãŒã å ã®çºå°ç©ã«é¢ããäžé£ã®æ å ±ã
- ãªãŒããŒ: çŸåšã®ãªãŒããŒããŒãã®ããŒã¿ã ãã®èšäºã§ã¯ãããã«ã€ããŠã¯èæ ®ããŸããã
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;
}
çŽ æµã§æ確ã§ãïŒ ã§ããããããããªã«ç°¡åã ã£ããããã®ã«ã ãã®å®è£ ã«åé¡ãããçç±ã® XNUMX ã€ã¯æ¬¡ã®ãšããã§ãã ã¬ã³ããªã³ã° ãã¬ãŒã ã¬ãŒãããµãŒã㌠ã¯ãã㯠ã¬ãŒãã«å¶éããŸãã.
ãã¬ãŒã ã¬ãŒã: ãã¬ãŒã æ° (ã€ãŸããåŒã³åºãæ°)
render()
)/ç§ããŸã㯠FPSã ã²ãŒã ã¯éåžžãå°ãªããšã 60 FPS ãéæããããåªããŸãã
ãã£ãã¯ã¬ãŒã: ãµãŒããŒãã²ãŒã ã®æŽæ°ãã¯ã©ã€ã¢ã³ãã«éä¿¡ããé »åºŠã ãã¬ãŒã ã¬ãŒããããäœãå Žåãå€ãã ãã®ã²ãŒã ã§ã¯ããµãŒããŒã¯ 30 ç§ããã XNUMX ãµã€ã¯ã«ã®é »åºŠã§å®è¡ãããŸãã
ææ°ã®ã²ãŒã æŽæ°ãã¬ã³ããªã³ã°ããã ãã®å ŽåãFPS ã¯åºæ¬çã« 30 ãè¶
ããããšã¯ã§ããŸããã ãµãŒããŒãã 30 ç§ããã XNUMX ãè¶
ããæŽæ°ãååŸããããšã¯ãããŸããã é»è©±ããŠã render()
60 ç§ããã XNUMX åã®å Žåããããã®åŒã³åºãã®ååã¯åããã®ãåæç»ããã ãã§ãåºæ¬çã«ã¯äœãè¡ããŸããã åçŽãªå®è£
ã«é¢ãããã XNUMX ã€ã®åé¡ã¯ã é
延ã®å¯èœæ§ããããŸãã çæ³çãªã€ã³ã¿ãŒãããé床ã§ã¯ãã¯ã©ã€ã¢ã³ãã¯æ£ç¢ºã« 33 ããªç§ããš (30 ç§ããã XNUMX å) ã«ã²ãŒã ã®ã¢ããããŒããåä¿¡ããŸãã
æ®å¿µãªãããå®ç§ãªãã®ã¯ãããŸããã ããçŸå®çãªå³ã¯æ¬¡ã®ããã«ãªããŸãã
åçŽãªå®è£
ã¯ãã¬ã€ãã³ã·ã«é¢ããŠã¯äºå®äžææªã®ã±ãŒã¹ã§ãã ã²ãŒã ã®ã¢ããããŒãã 50 ããªç§ã®é
延ã§åä¿¡ããå Žåã ã¯ã©ã€ã¢ã³ããã¹ããŒã«ãã 以åã®ã¢ããããŒãããã®ã²ãŒã ç¶æ
ããŸã ã¬ã³ããªã³ã°ããŠãããããããã« 50 ããªç§ããããŸãã ããããã¬ã€ã€ãŒã«ãšã£ãŠã©ãã»ã©äžå¿«ã§ãããã¯æ³åã§ããã§ããããä»»æã®ãã¬ãŒããããããšãã²ãŒã ããããããããŠäžå®å®ã«æããããŸãã
7.2 ã¯ã©ã€ã¢ã³ãç¶æ ã®æ¹å
çŽ æŽãªå®è£ ã«ããã€ãã®æ¹åãå ããŸãã ãŸããç§ãã¡ã䜿çšããã®ã¯ã ã¬ã³ããªã³ã°ã®é 延 100ããªç§éã ããã¯ãã¯ã©ã€ã¢ã³ãã®ãçŸåšã®ãç¶æ ããµãŒããŒäžã®ã²ãŒã ã®ç¶æ ãããåžžã« 100 ããªç§é ããããšãæå³ããŸãã ããšãã°ããµãŒããŒäžã®æéã次ã®å Žåã 150ããã®åŸãã¯ã©ã€ã¢ã³ãã¯ãµãŒããŒããã®æç¹ã§ãã£ãç¶æ ãã¬ã³ããªã³ã°ããŸã 50:
ããã«ãããã²ãŒã ã®æŽæ°ã®äºæž¬ã§ããªãã¿ã€ãã³ã°ã«èããããã® 100 ããªç§ã®ãããã¡ãŒãåŸãããŸãã
ãã®ä»£åã¯æ°žä¹
ã«åŸãããã ãã
ãšåŒã°ããå¥ã®ãã¯ããã¯ã䜿çšããããšãã§ããŸãã
ã¯ã©ã€ã¢ã³ãåŽã®äºæž¬ ãããã¯ç¥èŠãããé 延ãæžããã®ã«å¹æçã§ããããã®æçš¿ã§ã¯åãäžããŸããã
ç§ãã¡ã䜿çšããŠãããã XNUMX ã€ã®æ¹è¯ç¹ã¯ã ç·åœ¢è£éã ã¬ã³ããªã³ã°ã®é
ãã«ãããéåžžãã¯ã©ã€ã¢ã³ãã§ã¯çŸåšæå»ããå°ãªããšã XNUMX åæŽæ°ãé²ãã§ããŸãã åŒã°ãããšã getCurrentState()
ãå®è¡ã§ããŸã
ããã«ãããã¬ãŒã ã¬ãŒãã®åé¡ã解決ãããå¿
èŠãªãã¬ãŒã ã¬ãŒãã§ç¬èªã®ãã¬ãŒã ãã¬ã³ããªã³ã°ã§ããããã«ãªããŸããã
7.3 æ¹åãããã¯ã©ã€ã¢ã³ãç¶æ ã®å®è£
ã§ã®å®è£
äŸ src/client/state.js
ã¬ã³ããªã³ã° ã©ã°ãšç·åœ¢è£éã®äž¡æ¹ã䜿çšããŸãããé·æéã¯äœ¿çšããŸããã ã³ãŒãã XNUMX ã€ã®éšåã«åå²ããŸãããã æåã®ãã®ã¯æ¬¡ã®ãšããã§ãã
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()
ã åã«èŠãããã«ããã¹ãŠã®ã²ãŒã æŽæ°ã«ã¯ãµãŒããŒã®ã¿ã€ã ã¹ã¿ã³ããå«ãŸããŠããŸãã ã¬ã³ããªã³ã°é
延ã䜿çšããŠããµãŒããŒãã 100 ããªç§é
ããŠç»åãã¬ã³ããªã³ã°ãããã®ã§ããã ãµãŒããŒäžã®çŸåšæå»ãç¥ãããšã¯ã§ããŸãããªããªããã¢ããããŒããå±ããŸã§ã«ã©ããããã®æéãããã£ãããåãããªãããã§ãã ã€ã³ã¿ãŒãããã¯äºæž¬äžå¯èœã§ããã®é床ã¯å€§ããç°ãªãå¯èœæ§ããããŸãã
ãã®åé¡ãåé¿ããã«ã¯ã次ã®ãããªåççãªè¿äŒŒã䜿çšã§ããŸãã æåã®ã¢ããããŒããå³åº§ã«å±ãããµããããã ãããæ¬åœã§ããã°ããã®ç¹å®ã®ç¬éã®ãµãŒããŒæå»ããããããšã«ãªããŸãã ãµãŒããŒã®ã¿ã€ã ã¹ã¿ã³ãã次ã®ããã«ä¿åããŸãã 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),
};
}
}
ç§ãã¡ã¯æ¬¡ã® XNUMX ã€ã®ã±ãŒã¹ãæ±ããŸãã
base < 0
çŸåšã®ã¬ã³ããªã³ã°æéãŸã§æŽæ°ããªãããšãæå³ããŸã (äžèšã®å®è£ ãåç §)getBaseUpdate()
ïŒã ããã¯ãã¬ã³ããªã³ã°ã®é 延ã«ãããã²ãŒã ã®éå§çŽåŸã«çºçããå¯èœæ§ããããŸãã ãã®å Žåãåä¿¡ããææ°ã®ã¢ããããŒãã䜿çšããŸããbase
ã¯ææ°ã®ã¢ããããŒãã§ãã ããã¯ããããã¯ãŒã¯ã®é 延ãŸãã¯ã€ã³ã¿ãŒãããæ¥ç¶ã®äžè¯ãåå ã§ããå¯èœæ§ããããŸãã ãã®å Žåãææ°ã®ã¢ããããŒãã䜿çšããŠããŸãã- çŸåšã®ã¬ã³ããªã³ã°æéã®ååŸã®äž¡æ¹ã§æŽæ°ãããããã è£éãã!
æ®ã£ãŠãããã®ã¯ãã¹ãŠ state.js
ã¯ãåçŽãª (ãããéå±ãª) æ°åŠã§ããç·åœ¢è£éã®å®è£
ã§ãã èªåã§èª¿ã¹ãŠã¿ããå Žåã¯ãéããŠãã ãã state.js
Ма
ããŒã 2. ããã¯ãšã³ããµãŒããŒ
ãã®ããŒãã§ã¯ã
1. ãµãŒããŒãšã³ããªãŒãã€ã³ã
Web ãµãŒããŒã管çããã«ã¯ãNode.js ã®äžè¬ç㪠Web ãã¬ãŒã ã¯ãŒã¯ãšåŒã°ããã䜿çšããŸãã 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 ã«ã€ããŠèª¬æããããšãèŠããŠããŸãã? ãã㧠Webpack æ§æã䜿çšããŸãã ãããã次㮠XNUMX ã€ã®æ¹æ³ã§äœ¿çšããŸãã
- 䜿çšãã
webpack-dev-ããã«ãŠã§ã¢ éçºããã±ãŒãžãèªåçã«åæ§ç¯ããããããŸã㯠- éçã«ãã©ã«ããŒã転éãã
dist/
ãå®çšŒåãã«ãåŸã« Webpack ããã¡ã€ã«ãæžã蟌ãå Žæã§ãã
ããäžã€ã®éèŠãªä»»å 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 ã²ãŒã ãäœæããŠããã®ã§ãå¿
èŠãªã³ããŒã¯ XNUMX ã€ã ãã§ã Game
(ãã²ãŒã ã) â ãã¹ãŠã®ãã¬ã€ã€ãŒãåãã¢ãªãŒãã§ãã¬ã€ããŸãã 次ã®ã»ã¯ã·ã§ã³ã§ã¯ããã®ã¯ã©ã¹ãã©ã®ããã«æ©èœããããèŠãŠãããŸãã Game
.
2. ã²ãŒã ãµãŒããŒ
ã¯ã©ã¹ Game
ãµãŒããŒåŽã®æãéèŠãªããžãã¯ãå«ãŸããŠããŸãã ããã«ã¯ XNUMX ã€ã®äž»ãªã¿ã¹ã¯ããããŸãã ãã¬ãŒã€ãŒã®ç®¡ç О ã²ãŒã ã·ãã¥ã¬ãŒã·ã§ã³.
æåã®ã¿ã¹ã¯ã§ãããã¬ãŒã€ãŒã®ç®¡çããå§ããŸãããã
ã²ãŒã .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
ã ããç§ãã¡ã¯ããã«ã€ããŠå¿é
ããå¿
èŠã¯ãããŸããã 圌ã«é»è©±ããŸã ãã¬ã€ã€ãŒID.
ããã念é ã«çœ®ããŠãã¯ã©ã¹å
ã®ã€ã³ã¹ã¿ã³ã¹å€æ°ã調ã¹ãŠã¿ãŸããã Game
:
sockets
ãã¬ãŒã€ãŒ ID ããã¬ãŒã€ãŒã«é¢é£ä»ãããããœã±ããã«ãã€ã³ããããªããžã§ã¯ãã§ãã ããã«ããããã¬ãŒã€ãŒ ID ã«ãã£ãŠäžå®æéå ã«ãœã±ããã«ã¢ã¯ã»ã¹ã§ããããã«ãªããŸããplayers
ãã¬ãŒã€ãŒ ID ãã³ãŒãã«ãã€ã³ããããªããžã§ã¯ãã§ã>Player ãªããžã§ã¯ã
bullets
ãªããžã§ã¯ãã®é
åã§ã Bullet
ãæ確ãªé åºã¯ãããŸããã
lastUpdateTime
ã²ãŒã ãæåŸã«æŽæ°ããããšãã®ã¿ã€ã ã¹ã¿ã³ãã§ãã ãããã©ã®ããã«äœ¿çšããããã¯ããã«èŠãŠã¿ãŸãããã
shouldSendUpdate
ã¯è£å©å€æ°ã§ãã ãã®äœ¿ãæ¹ã«ã€ããŠãããã«èŠãŠãããŸãã
ã¡ãœãã addPlayer()
, removePlayer()
О handleInput()
説æã®å¿
èŠã¯ãããŸãããããããã¯ä»¥äžã§äœ¿çšãããŸãã server.js
ã 埩ç¿ãå¿
èŠãªå Žåã¯ãå°ãäžã«æ»ã£ãŠãã ããã
æåŸã®è¡ constructor()
æã¡äžã æŽæ°ãµã€ã¯ã« ã²ãŒã (60 æŽæ°/ç§ã®é »åºŠ):
ã²ãŒã .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 ç§ããã XNUMX åã®ã²ãŒã æŽæ°ã¯ããªãã®æ°ã§ãã
ãªããã é»è©±ããªãã®ã§ãã
update()
30ç§éã«XNUMXåãããïŒ ã²ãŒã ã®ã·ãã¥ã¬ãŒã·ã§ã³ãæ¹åããããã ããåŒã°ããã»ã©update()
ãã²ãŒã ã·ãã¥ã¬ãŒã·ã§ã³ãããæ£ç¢ºã«ãªããŸãã ãã ãã課é¡ã®æ°ã«å€¢äžã«ãªããããªãã§ãã ãããupdate()
ãããã¯èšç®éãå€ãã¿ã¹ã¯ã§ããããã60 ç§ããã XNUMX ã§ååã§ãã
ã¯ã©ã¹ã®æ®ãã®äººã
Game
ã§äœ¿çšããããã«ã㌠ã¡ãœããã§æ§æãããŸãã update()
:
ã²ãŒã .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()
éåžžã«åçŽã§ãããã¬ã€ã€ãŒãã¹ã³ã¢ã§äžŠã¹æ¿ããäžäœ XNUMX åãååŸããããããã®ãŠãŒã¶ãŒåãšã¹ã³ã¢ãè¿ããŸãã
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
次ã®æ¡åŒµåã®ã¿:
- ããã±ãŒãžã®äœ¿çš
èãäœã ã©ã³ãã çæçš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
:
ã²ãŒã .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;
}
ãã®åçŽãªè¡çªæ€åºã¯ã次ã®äºå®ã«åºã¥ããŠããŸãã XNUMX ã€ã®åã¯ãäžå¿éã®è·é¢ãååŸã®åèšããå°ããå Žåã«è¡çªããŸããã 以äžã¯ãXNUMX ã€ã®åã®äžå¿éã®è·é¢ããããã®ååŸã®åèšã«æ£ç¢ºã«çããå Žåã§ãã
ããã§èæ
®ãã¹ãç¹ãããã«ããã€ããããŸãã
- çºå°ç©ã¯ããããäœæãããã¬ã€ã€ãŒã«åœãããªãããã«ããŠãã ããã ããã¯æ¯èŒããããšã§éæã§ããŸã
bullet.parentID
Ñplayer.id
. - è€æ°ã®ãã¬ã€ã€ãŒã«åæã«åœäžãã極端ãªå Žåãçºå°ç©ã¯ XNUMX åã ãåœäžããå¿
èŠããããŸãã æŒç®åã䜿çšããŠãã®åé¡ã解決ããŸãã
break
: çºå°ç©ãšè¡çªããŠãããã¬ã€ã€ãŒãèŠã€ãããšããã«æ€çŽ¢ãåæ¢ãã次ã®çºå°ç©ã«é²ã¿ãŸãã
çµäº
ããã ãã§ãïŒ .io Web ã²ãŒã ãäœæããããã«ç¥ã£ãŠããã¹ãããšã¯ãã¹ãŠç¶²çŸ ããŸããã 次ã¯äœã§ããïŒ ç¬èªã® .io ã²ãŒã ãæ§ç¯ããŸããã!
ãã¹ãŠã®ãµã³ãã«ã³ãŒãã¯ãªãŒãã³ãœãŒã¹ã§ããã次ã®ãµã€ãã«æ²èŒãããŠããŸãã
åºæïŒ habr.com