2015 တွင်ထွက်ရှိခဲ့သည်။
ဤဂိမ်းများကို သင် ယခင်က တစ်ခါမျှ မကြားဖူးပါက၊ ၎င်းတို့သည် ကစားရလွယ်ကူသော အခမဲ့ multiplayer ဝဘ်ဂိမ်းများ (အကောင့်မလိုအပ်ပါ)။ များသောအားဖြင့် ၎င်းတို့သည် နယ်ပယ်တစ်ခုတည်းတွင် ဆန့်ကျင်ဘက်ကစားသမားများစွာနှင့် ရင်ဆိုင်ရလေ့ရှိသည်။ အခြားနာမည်ကြီး .io ဂိမ်းများ
ဒီ post မှာတော့ ဘယ်လိုမျိုးလဲဆိုတာ လေ့လာကြည့်ပါမယ်။ .io ဂိမ်းကို အစကနေ ပြန်ဖန်တီးပါ။. ဤအတွက်၊ Javascript အသိပညာသာလျှင် လုံလောက်လိမ့်မည်- syntax ကဲ့သို့သော အရာများကို နားလည်ရန် လိုအပ်သည်။ this
и
.io ဂိမ်းဥပမာ
သင်ယူမှုအကူအညီအတွက် ကျွန်ုပ်တို့အား ကိုးကားပါမည်။
ဂိမ်းသည်အတော်လေးရိုးရှင်းသည်- သင်သည်အခြားကစားသမားများရှိသည့်ကွင်းတစ်ခုတွင်သင်္ဘောကိုသင်ထိန်းချုပ်သည်။ သင့်သင်္ဘောသည် ဒုံးကျည်များကို အလိုအလျောက် ပစ်ခတ်ပြီး ၎င်းတို့၏ ဒုံးကျည်များကို ရှောင်ရှားနေစဉ် အခြားကစားသမားများကို ထိမှန်ရန် ကြိုးစားသည်။
1. ပရောဂျက်၏ အကျဉ်းချုပ်/ဖွဲ့စည်းပုံ
ထောက်ခံ
အရင်းအမြစ်ကုဒ်ကိုဒေါင်းလုဒ်လုပ်ပါ။ ဥပမာ ဂိမ်းမို့ မင်းငါ့ကို လိုက်ကြည့်နိုင်ပါတယ်။
ဥပမာသည် အောက်ပါတို့ကို အသုံးပြုသည်-
ထုတ်ဖော်ပြောဆို ဂိမ်း၏ဝဘ်ဆာဗာကို စီမံခန့်ခွဲသည့် လူကြိုက်အများဆုံး Node.js ဝဘ်ဘောင်ဘောင်ဖြစ်သည်။socket.io - ဘရောက်ဆာနှင့်ဆာဗာအကြားဒေတာဖလှယ်ရန်အတွက် websocket စာကြည့်တိုက်။webpack - module မန်နေဂျာ။ 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/
client နှင့် server နှစ်ခုလုံးမှ တင်သွင်းသော ကိန်းသေဖိုင်တစ်ခုပါရှိသည်။
2. စုဝေးမှုများ/ပရောဂျက်ဆက်တင်များ
အထက်တွင်ဖော်ပြခဲ့သည့်အတိုင်း၊ ကျွန်ုပ်တို့သည် ပရောဂျက်ကိုတည်ဆောက်ရန် module manager ကိုအသုံးပြုသည်။
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) client ၏ entry point ဖြစ်သည်။ Webpack သည် ဤနေရာမှ စတင်ပြီး အခြားတင်သွင်းထားသော ဖိုင်များကို ထပ်ခါတလဲလဲ ရှာဖွေပါမည်။- ကျွန်ုပ်တို့၏ Webpack build ၏ output JS သည် directory တွင် ရှိနေမည်ဖြစ်သည်။
dist/
. ဒီဖိုင်ကိုငါတို့ခေါ်မယ်။ js အထုပ်. - ငါတို့သုံးတယ်
Babel အထူးသဖြင့် ဖွဲ့စည်းမှုပုံစံ@babel/preset-env ဘရောက်ဆာအဟောင်းများအတွက် ကျွန်ုပ်တို့၏ JS ကုဒ်ကို ကူးယူရန်။ - JS ဖိုင်များမှ ကိုးကားထားသော CSS အားလုံးကို ထုတ်ယူပြီး တစ်နေရာတည်းတွင် ပေါင်းစပ်ရန် ပလပ်အင်ကို ကျွန်ုပ်တို့ အသုံးပြုနေပါသည်။ သူ့ကိုငါတို့ခေါ်မယ်။ css အထုပ်.
ထူးဆန်းသော အထုပ်ဖိုင်အမည်များကို သင် သတိပြုမိပေမည်။ '[name].[contenthash].ext'
. သူတို့ပါဝင်ပါတယ်။ [name]
input point ၏ အမည်ဖြင့် အစားထိုးမည် (ကျွန်ုပ်တို့၏ ကိစ္စတွင်၊ ဤအရာ 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
web browser ကိုသွားပါ။
3. Client ဝင်ခွင့်အမှတ်များ
ဂိမ်းကုဒ်ကို ကိုယ်တိုင်သွားကြည့်ရအောင်။ အရင်ဆုံး page တစ်ခုလိုပါတယ်။ index.html
ဝဘ်ဆိုက်ကို ဝင်ကြည့်သောအခါ၊ ဘရောက်ဆာက ၎င်းကို ဦးစွာဖွင့်ပေးလိမ့်မည်။ ကျွန်ုပ်တို့၏ page သည် အလွန်ရိုးရှင်းပါသည်။
index.html
ဥပမာ .io ဂိမ်းတစ်ခု ကစားပါ။
ဤကုဒ်နမူနာကို ရှင်းရှင်းလင်းလင်းသိရန် အနည်းငယ်ရိုးရှင်းပြီး အခြားပို့စ်နမူနာများစွာနှင့်လည်း အလားတူလုပ်ဆောင်ပါမည်။ ကုဒ်အပြည့်အစုံကို အမြဲကြည့်ရှုနိုင်ပါသည်။
ငါတို့မှာရှိတယ်:
HTML5 ကင်းဗတ်ဒြပ်စင် (<canvas>
) ဂိမ်းကို တင်ဆက်ရန် အသုံးပြုပါမည်။<link>
ကျွန်ုပ်တို့၏ CSS အထုပ်ကိုထည့်ရန်။<script>
ကျွန်ုပ်တို့၏ Javascript package ကိုထည့်ရန်။- အသုံးပြုသူအမည်နှင့်အတူ ပင်မမီနူး
<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 ပက်ကေ့ချ်တွင် ထည့်သွင်းရန် သိပါသည်)။
- ပစ်လွှတ်
connect()
ဆာဗာနှင့် ချိတ်ဆက်မှုတစ်ခုကို တည်ဆောက်ပြီး လုပ်ဆောင်ရန်downloadAssets()
ဂိမ်းကို တင်ဆက်ရန် လိုအပ်သော ပုံများကို ဒေါင်းလုဒ်လုပ်ရန်။ - အဆင့် 3 ပြီးဆုံးပြီးနောက် ပင်မမီနူးကိုပြသသည် (
playMenu
). - "PLAY" ခလုတ်ကိုနှိပ်ရန်အတွက် လက်ကိုင်ကို သတ်မှတ်ခြင်း။ ခလုတ်ကို နှိပ်လိုက်သောအခါ၊ ကုဒ်သည် ဂိမ်းကို အစပြုပြီး ကျွန်ုပ်တို့ကစားရန် အသင့်ဖြစ်နေပြီဟု ဆာဗာကို ပြောပြသည်။
ကျွန်ုပ်တို့၏ client-server logic ၏ အဓိက "အသား" သည် ဖိုင်မှတင်သွင်းသော ဖိုင်များတွင်ဖြစ်သည်။ 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. Client Rendering
မျက်နှာပြင်ပေါ်တွင် ရုပ်ပုံလွှာကို ပြသရန် အချိန်ကျရောက်ပြီဖြစ်သည်။
…ဒါပေမယ့် အဲဒါကို မလုပ်ခင်၊ ဒီအတွက် လိုအပ်တဲ့ ပုံတွေ (အရင်းအမြစ်) အားလုံးကို ဒေါင်းလုဒ်လုပ်ထားဖို့ လိုပါတယ်။ အရင်းအမြစ်မန်နေဂျာကိုရေးကြပါစို့။
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
.
အရင်းအမြစ်များကို ဒေါင်းလုဒ်လုပ်ပြီးနောက်၊ သင်သည် rendering စတင်နိုင်ပါသည်။ အထက်မှာပြောခဲ့သလိုပဲ ဝဘ်စာမျက်နှာပေါ်မှာ ဆွဲဖို့၊ <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 ဖြင့် render loop ၏ activation ကိုထိန်းချုပ်ပါ။
တစ်ဦးချင်းစီ တင်ဆက်ပေးသည့် လုပ်ဆောင်ချက်များကို အခိုင်အမာ အကောင်အထည်ဖော်ခြင်း (ဥပမာ၊ 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
!
အခြား rendering helpers များအကြောင်း လေ့လာရန် စိတ်ပါဝင်စားပါက ကျန်ကိုဖတ်ပါ။
src/client/render.js .
6. Client ထည့်သွင်းခြင်း။
ဂိမ်းတစ်ခုလုပ်ဖို့အချိန်ရောက်ပြီ။ ကစားနိုင်သော! ထိန်းချုပ်မှုအစီအစဥ်သည် အလွန်ရိုးရှင်းပါမည်- ရွေ့လျားမှုလမ်းကြောင်းကိုပြောင်းရန်၊ သင်သည် မောက်စ် (ကွန်ပြူတာပေါ်တွင်) သို့မဟုတ် စခရင် (မိုဘိုင်းကိရိယာပေါ်တွင်) ကိုထိနိုင်သည်။ ဒါကို အကောင်အထည်ဖော်ဖို့ မှတ်ပုံတင်မယ်။
ဒါတွေအားလုံး ဂရုစိုက်မယ်။ 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()
ခေါ်ဆိုသော Event Listeners များဖြစ်သည်။ updateDirection()
(၏ networking.js
) ထည့်သွင်းသည့် ဖြစ်ရပ်တစ်ခု ဖြစ်ပေါ်သည့်အခါ (ဥပမာ၊ မောက်စ်ကို ရွှေ့သည့်အခါ)။ updateDirection()
ထည့်သွင်းသည့်ဖြစ်ရပ်ကို ကိုင်တွယ်ပြီး ဂိမ်းအခြေအနေနှင့်အညီ အပ်ဒိတ်လုပ်သည့် ဆာဗာဖြင့် စာတိုပေးပို့ခြင်းကို ကိုင်တွယ်သည်။
7. Client အခြေအနေ
ဤအပိုင်းသည် ပို့စ်၏ ပထမအပိုင်းတွင် အခက်ခဲဆုံးဖြစ်သည်။ ပထမအကြိမ်ဖတ်ပြီး နားမလည်ရင် စိတ်ဓာတ်မကျပါနဲ့။ အဲဒါကို ကျော်ပြီး နောက်မှ ပြန်လာနိုင်ပါတယ်။
client/server code ဖြည့်ရန် လိုအပ်သော ပဟေဋ္ဌိ၏ နောက်ဆုံးအပိုင်းမှာ ပြည်နယ်. Client Rendering ကဏ္ဍမှ ကုဒ်အတိုအထွာကို မှတ်မိပါသလား။
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: ဤအပ်ဒိတ်ကို လက်ခံရရှိသည့် ကစားသမား၏ အချက်အလက်။
- အခြားသူများကို: တူညီသောဂိမ်းတွင် ပါ၀င်သည့် အခြားကစားသမားများအကြောင်း အချက်အလက် အစုံအလင်။
- ကျည်ဆံများ: ဂိမ်းရှိ ပရိုဂျက်စတီးလ်များအကြောင်း အချက်အလက် အစုံအလင်။
- ဦးဆောင်သူ: လက်ရှိ ဦးဆောင်သူစာရင်းဒေတာ။ ဤပို့စ်တွင် ကျွန်ုပ်တို့သည် ၎င်းတို့ကို ထည့်သွင်းစဉ်းစားမည်မဟုတ်ပါ။
7.1 Naive client အခြေအနေ
ဘာမှမသိတဲ့အကောင်အထည် getCurrentState()
မကြာသေးမီက ရရှိထားသော ဂိမ်းအပ်ဒိတ်၏ ဒေတာကို တိုက်ရိုက်သာ ပြန်ပေးနိုင်သည်။
naive-state.js
let lastGameUpdate = null;
// Handle a newly received game update.
export function processGameUpdate(update) {
lastGameUpdate = update;
}
export function getCurrentState() {
return lastGameUpdate;
}
ချစ်စရာကောင်းပြီး ရှင်းပါတယ်။ ဒါပေမယ် ့အဲဒါက ရိုးရှင်းပါတယ်။ ဤအကောင်အထည်ဖော်မှုသည် ပြဿနာရှိသော အကြောင်းရင်းများထဲမှ တစ်ခုဖြစ်သည်- ၎င်းသည် rendering frame rate ကို server clock rate တွင်ကန့်သတ်ထားသည်။.
ဘောင်နှုန်း: ဘောင်အရေအတွက် (ဆိုလိုသည်မှာ ခေါ်ဆိုမှုများ
render()
) တစ်စက္ကန့် သို့မဟုတ် FPS ။ ဂိမ်းများသည် များသောအားဖြင့် အနည်းဆုံး 60 FPS ရရှိရန် ကြိုးစားကြသည်။
အမှန်ခြစ်နှုန်း: ဆာဗာသည် သုံးစွဲသူများထံ ဂိမ်းအပ်ဒိတ်များ ပေးပို့သည့် အကြိမ်ရေ။ ၎င်းသည် frame rate ထက်နိမ့်လေ့ရှိသည်။. ကျွန်ုပ်တို့၏ဂိမ်းတွင်၊ ဆာဗာသည် တစ်စက္ကန့်လျှင် အကြိမ်ရေ 30 လည်ပတ်သည်။
အကယ်၍ ကျွန်ုပ်တို့သည် ဂိမ်း၏နောက်ဆုံးအပ်ဒိတ်ကိုသာ တင်ဆက်ပါက၊ FPS သည် အခြေခံအားဖြင့် 30 ကျော်သွားမည်မဟုတ်သောကြောင့်၊ ကျွန်ုပ်တို့သည် ဆာဗာမှ တစ်စက္ကန့်လျှင် အပ်ဒိတ် 30 ထက်ပို၍ မရပါ။. ဖုန်းဆက်ရင်တောင် render()
တစ်စက္ကန့်လျှင် အကြိမ် 60 ၊ ထို့နောက် ဤခေါ်ဆိုမှုများ၏ ထက်ဝက်သည် တူညီသောအရာကို ပြန်လည်ရေးဆွဲမည်ဖြစ်ပြီး အခြေခံအားဖြင့် ဘာမှမလုပ်ဆောင်ပါ။ နုံအသောအကောင်အထည်ဖေါ်ခြင်း၏နောက်ထပ်ပြဿနာမှာ၎င်း နှောင့်နှေးမှုများ ကျရောက်တတ်သည်။. စံပြအင်တာနက်အမြန်နှုန်းဖြင့်၊ သုံးစွဲသူသည် 33ms တိုင်း (တစ်စက္ကန့်လျှင် 30) တိတိ ဂိမ်းအပ်ဒိတ်တစ်ခု ရရှိလိမ့်မည်-
ကံမကောင်းစွာပဲ၊ ဘယ်အရာမှ ပြီးပြည့်စုံမှုမရှိပါဘူး။ ပိုလက်တွေ့ကျတဲ့ ရုပ်ပုံက-
နုံအသောအကောင်အထည်ဖော်မှုသည် latency နှင့်ပတ်သက်လာလျှင် အဆိုးဆုံးအခြေအနေဖြစ်သည်။ 50ms နှောင့်နှေးခြင်းဖြင့် ဂိမ်းအပ်ဒိတ်ကို လက်ခံရရှိပါက၊ ဖောက်သည်ဆိုင်များ ယခင်အပ်ဒိတ်မှ ဂိမ်းအခြေအနေကို တင်ဆက်နေဆဲဖြစ်သောကြောင့် 50ms အပိုဖြစ်သည်။ ကစားသမားအတွက် မည်မျှ အဆင်မပြေဖြစ်မည်ကို သင်မြင်ယောင်ကြည့်နိုင်သည်- မတရားဘရိတ်အုပ်ခြင်းက ဂိမ်းကို တုန်လှုပ်စေပြီး မတည်မငြိမ်ဖြစ်စေသည်။
7.2 တိုးတက်သော client အခြေအနေ
နုံအသော အကောင်အထည်ဖော်မှုအတွက် တိုးတက်မှုအချို့ ပြုလုပ်ပါမည်။ ပထမဦးစွာကျွန်ုပ်တို့အသုံးပြုသည်။ rendering နှောင့်နှေးခြင်း။ 100 ms အတွက် ဆိုလိုသည်မှာ ကလိုင်းယင့်၏ "လက်ရှိ" အခြေအနေသည် ဆာဗာပေါ်ရှိ ဂိမ်း၏အခြေအနေကို 100ms ဖြင့် အမြဲနောက်ကျနေမည်ဖြစ်သည်။ ဥပမာအားဖြင့်၊ ဆာဗာပေါ်ရှိအချိန်ဖြစ်ပါက 150ထို့နောက်တွင် client သည် ထိုအချိန်တွင် ဆာဗာရှိနေသည့် အခြေအနေကို တင်ဆက်မည်ဖြစ်သည်။ 50:
၎င်းသည် ကျွန်ုပ်တို့အား ခန့်မှန်းမရသော ဂိမ်းအပ်ဒိတ်အချိန်များကို ရှင်သန်ရန် 100ms ကြားခံတစ်ခု ပေးသည်-
ဤအတွက် ပေးဆပ်မှုသည် အမြဲတမ်းဖြစ်လိမ့်မည်။
အခြားနည်းပညာတစ်ခုကိုလည်း အသုံးပြုနိုင်သည်။
client-side ခန့်မှန်းချက် ထင်မြင် latency ကို လျှော့ချရန် ကောင်းမွန်သော အလုပ်ဖြစ်သည်၊ သို့သော် ဤပို့စ်တွင် အကျုံးဝင်မည်မဟုတ်ပါ။
ကျွန်ုပ်တို့အသုံးပြုနေသော အခြားတိုးတက်မှုတစ်ခုဖြစ်သည်။ linear interpolation. rendering နှောင့်နှေးမှုကြောင့်၊ ကျွန်ုပ်တို့သည် client ရှိ လက်ရှိအချိန်ထက် အနည်းဆုံး အပ်ဒိတ်တစ်ခု ပြုလုပ်လေ့ရှိပါသည်။ ခေါ်တဲ့အခါ getCurrentState()
, ငါတို့က execute နိုင်ပါတယ်။
၎င်းသည် ဖရိမ်နှုန်းပြဿနာကို ဖြေရှင်းပေးသည်- ယခုကျွန်ုပ်တို့လိုချင်သည့် မည်သည့်ဘောင်နှုန်းဖြင့် ထူးခြားသောဘောင်များကို တင်ဆက်နိုင်ပါပြီ။
7.3 မြှင့်တင်ထားသော client အခြေအနေကို အကောင်အထည်ဖော်ခြင်း။
အကောင်အထည်ဖော်ပုံဥပမာ src/client/state.js
render lag နှင့် linear interpolation နှစ်မျိုးလုံးကို အသုံးပြုသော်လည်း ကြာရှည်မခံပါ။ ကုဒ်ကို နှစ်ပိုင်းခွဲကြည့်ရအောင်။ ဤသည်မှာ ပထမတစ်ခုဖြစ်သည်။
state.js အပိုင်း ၁
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 ပုံရိပ်ကို တင်ဆက်ရန်အတွက် render latency ကို အသုံးပြုလိုသော်လည်း၊ ဆာဗာပေါ်ရှိ လက်ရှိအချိန်ကို ကျွန်ုပ်တို့ ဘယ်တော့မှ သိမည်မဟုတ်ပါ။အဘယ်ကြောင့်ဆိုသော် ကျွန်ုပ်တို့ထံ အပ်ဒိတ်များရရှိရန် အချိန်မည်မျှကြာသည်ကို ကျွန်ုပ်တို့မသိနိုင်သောကြောင့်ဖြစ်သည်။ အင်တာနက်သည် မှန်းဆ၍မရဘဲ ၎င်း၏အမြန်နှုန်းသည် အလွန်ပြောင်းလဲနိုင်သည်။
ဤပြဿနာကို ဖြေရှင်းရန်၊ ကျွန်ုပ်တို့သည် ကျိုးကြောင်းဆီလျော်သော အနီးစပ်ဆုံးကို အသုံးပြုနိုင်သည်။ ပထမဆုံး update က ချက်ချင်းရောက်လာသလို ဟန်ဆောင်တယ်။. အကယ်၍ ဤအရာသည် အမှန်ဖြစ်ပါက ဤအထူးအခိုက်အတန့်တွင် ဆာဗာအချိန်ကို ကျွန်ုပ်တို့ သိပါလိမ့်မည်။ ကျွန်ုပ်တို့သည် ဆာဗာ၏ အချိန်တံဆိပ်ကို သိမ်းဆည်းထားသည်။ firstServerTimestamp
ငါတို့ကိုစောင့်ရှောက်လော့ ပြည်တွင်း တစ်ချိန်တည်းမှာ (ဖောက်သည်) အချိန်တံဆိပ်တုံး gameStart
.
အိုးခဏ။ ဆာဗာအချိန် = ဖောက်သည်အချိန် မဟုတ်သင့်ဘူးလား။ ကျွန်ုပ်တို့သည် "ဆာဗာအချိန်တံဆိပ်" နှင့် "ဖောက်သည်အချိန်တံဆိပ်" ကို အဘယ်ကြောင့် ခွဲခြားရသနည်း။ ဒါက မေးခွန်းကောင်းတစ်ခုပါ။ ပေါ်လာတာက သူတို့ဟာ အတူတူဘဲ။ Date.now()
ကလိုင်းယင့်နှင့် ဆာဗာတွင် မတူညီသောအချိန်တံဆိပ်များကို ပြန်ပေးမည်ဖြစ်ပြီး ၎င်းသည် ဤစက်များအတွက် ဒေသဆိုင်ရာအချက်များပေါ်တွင် မူတည်သည်။ စက်အားလုံးတွင် အချိန်တံဆိပ်တုံးများ တူညီမည်ဟု ဘယ်တော့မှ မယူဆပါ။
အခု ကျွန်တော် ဘာနားလည်သလဲ။ currentServerTime()
: ပြန်လာပါတယ်။ လက်ရှိတင်ဆက်ချိန်၏ ဆာဗာအချိန်တံဆိပ်. တစ်နည်းဆိုရသော် ဤသည်မှာ ဆာဗာ၏ လက်ရှိအချိန်ဖြစ်သည် (firstServerTimestamp <+ (Date.now() - gameStart)
) အနုတ် render delay (RENDER_DELAY
).
ယခု ကျွန်ုပ်တို့သည် ဂိမ်းအပ်ဒိတ်များကို မည်သို့ကိုင်တွယ်ဖြေရှင်းသည်ကို လေ့လာကြည့်ကြပါစို့။ အပ်ဒိတ်ဆာဗာမှ လက်ခံရရှိသောအခါ၊ ၎င်းကို ခေါ်သည်။ processGameUpdate()
နှင့် ကျွန်ုပ်တို့သည် အပ်ဒိတ်အသစ်ကို ခင်းကျင်းတစ်ခုတွင် သိမ်းဆည်းပါသည်။ gameUpdates
. ထို့နောက် မန်မိုရီအသုံးပြုမှုကို စစ်ဆေးရန်၊ ကျွန်ုပ်တို့သည် အပ်ဒိတ်ဟောင်းများအားလုံးကို ဖယ်ရှားလိုက်ပါသည်။ အခြေခံမွမ်းမံမှုဘာလို့လဲဆိုတော့ ငါတို့က သူတို့ကို မလိုအပ်တော့ဘူး။
"အခြေခံမွမ်းမံမှု" ဆိုတာ ဘာလဲ။ ဒီ ဆာဗာ၏လက်ရှိအချိန်မှ နောက်သို့ရွှေ့ခြင်းဖြင့် ကျွန်ုပ်တို့တွေ့ရှိသော ပထမဆုံးအပ်ဒိတ်. ဤပုံကြမ်းကို မှတ်မိပါသလား။
ဂိမ်းအပ်ဒိတ်သည် "Client Render Time" ၏ ဘယ်ဘက်သို့ တိုက်ရိုက်မွမ်းမံမှုသည် အခြေခံမွမ်းမံမှုဖြစ်သည်။
အခြေခံအပ်ဒိတ်ကို ဘာအတွက်သုံးတာလဲ။ ကျွန်ုပ်တို့သည် အပ်ဒိတ်များကို အခြေခံစာရင်းသို့ အဘယ်ကြောင့် ချနိုင်သနည်း။ ဒါကို အဖြေရှာကြည့်ရအောင် နောက်ဆုံးတော့ အကောင်အထည်ဖော်ရန်စဉ်းစားပါ။ getCurrentState()
:
state.js အပိုင်း ၁
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()
) rendering နှောင့်နှေးမှုကြောင့် ဂိမ်းအစတွင် ၎င်းသည် ချက်ချင်းဖြစ်သွားနိုင်သည်။ ဤကိစ္စတွင်၊ ကျွန်ုပ်တို့ ရရှိထားသော နောက်ဆုံးအပ်ဒိတ်ကို အသုံးပြုပါသည်။base
ကျွန်ုပ်တို့၏နောက်ဆုံးထွက်အပ်ဒိတ်ဖြစ်ပါသည်။ ကွန်ရက်နှောင့်နှေးခြင်း သို့မဟုတ် အင်တာနက်ချိတ်ဆက်မှု အားနည်းခြင်းတို့ကြောင့် ဖြစ်နိုင်သည်။ ဤကိစ္စတွင်၊ ကျွန်ုပ်တို့သည် ကျွန်ုပ်တို့ရရှိထားသော နောက်ဆုံးအပ်ဒိတ်ကိုလည်း အသုံးပြုနေပါသည်။- လက်ရှိ တင်ဆက်ချိန်မတိုင်မီနှင့် အပြီးတွင် ကျွန်ုပ်တို့တွင် အပ်ဒိတ်တစ်ခု ရှိသည်၊ ထို့ကြောင့် ကျွန်ုပ်တို့ လုပ်ဆောင်နိုင်သည်။ interpolate!
ကျန်တာအကုန် state.js
ရိုးရှင်းသော (သို့သော် ငြီးငွေ့ဖွယ်) သင်္ချာဖြစ်သည့် linear interpolation ကို အကောင်အထည်ဖော်ခြင်း။ ကိုယ်တိုင်လေ့လာချင်ရင် ဖွင့်ကြည့်လိုက်ပါ။ state.js
အပေါ်
အပိုင်း 2. Backend ဆာဗာ
ဤအပိုင်းတွင်၊ ကျွန်ုပ်တို့ကို ထိန်းချုပ်သည့် Node.js နောက်ခံကို ကြည့်ပါမည်။
1. Server Entry Point
ဝဘ်ဆာဗာကို စီမံခန့်ခွဲရန်၊ ကျွန်ုပ်တို့သည် Node.js ဟုခေါ်သော နာမည်ကြီး ဝဘ်ဘောင်တစ်ခုကို အသုံးပြုပါမည်။ src/server/server.js
:
server.js အပိုင်း ၁
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 ကို set up လုပ်ဖို့ပါ။
server.js အပိုင်း ၁
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 ချိတ်ဆက်မှုကို အောင်မြင်စွာတည်ဆောက်ပြီးနောက်၊ socket အသစ်အတွက် ဖြစ်ရပ်ကိုင်တွယ်သူများကို ကျွန်ုပ်တို့ သတ်မှတ်ပေးပါသည်။ ပွဲစီစဉ်သူများသည် singleton အရာဝတ္တုသို့လွှဲအပ်ခြင်းဖြင့် ဖောက်သည်များထံမှရရှိသော မက်ဆေ့ဂျ်များကို ကိုင်တွယ်သည်။ game
:
server.js အပိုင်း ၁
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 အပိုင်း ၁
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 socket (စိတ်ရှုပ်နေရင် အဲဒီကို ပြန်သွားပါ။ server.js
) Socket.io ကိုယ်တိုင်က socket တစ်ခုစီကို သီးသန့်သတ်မှတ်ပေးသည်။ id
ဒါကြောင့် အဲဒါကို စိတ်ပူစရာ မလိုပါဘူး။ ငါသူ့ကိုခေါ်မယ်။ ကစားသမား ID.
အဲဒါကို စိတ်ထဲမှာထားပြီး၊ class တစ်ခုမှာရှိတဲ့ instance variable တွေကို လေ့လာကြည့်ရအောင် Game
:
sockets
ပလေယာ ID ကို ပလေယာနှင့် ဆက်စပ်နေသော socket နှင့် ချိတ်ထားသည့် အရာတစ်ခု ဖြစ်သည်။ ၎င်းသည် ကျွန်ုပ်တို့အား ၎င်းတို့၏ ကစားသမား ID များဖြင့် အချိန်အဆက်မပြတ် ဝင်ရောက်ကြည့်ရှုနိုင်စေပါသည်။players
ကစားသမား ID ကို ကုဒ်>ကစားသမားအရာဝတ္တုနှင့် ချိတ်ဆက်ထားသည့် အရာဝတ္ထုတစ်ခုဖြစ်သည်။
bullets
အရာဝတ္ထုတစ်ခုဖြစ်သည်။ Bullet
အတိအကျ အမိန့် မရှိပါ။
lastUpdateTime
ဂိမ်းကို အပ်ဒိတ်လုပ်ခဲ့သည့် နောက်ဆုံးအကြိမ်၏ အချိန်တံဆိပ်ဖြစ်သည်။ အဲဒါကို ဘယ်လိုသုံးလဲဆိုတာ မကြာခင်မှာ တွေ့ရပါလိမ့်မယ်။
shouldSendUpdate
auxiliary variable တစ်ခုဖြစ်သည်။ ၎င်း၏အသုံးပြုမှုကိုလည်း မကြာမီ မြင်တွေ့ရမည်ဖြစ်သည်။
နည်းလမ်းများ addPlayer()
, removePlayer()
и handleInput()
ရှင်းပြရန်မလိုပါ၊ ၎င်းတို့ကိုအသုံးပြုသည်။ server.js
. သင်၏မှတ်ဉာဏ်ကို ပြန်လည်ဆန်းသစ်ရန် လိုအပ်ပါက၊ အနည်းငယ်မြင့်သော ပြန်သွားပါ။
နောက်ဆုံးစာကြောင်း constructor()
စတင်သည် update သံသရာ ဂိမ်းများ (အကြိမ်ရေ 60 အပ်ဒိတ်/s)
game.js အပိုင်း ၁
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()
server-side logic ၏ အရေးကြီးဆုံးအပိုင်း ပါ၀င်သည် ။ ဤသည်မှာ အစီအစဥ်အတိုင်း လုပ်ဆောင်သည်-
- မည်မျှကြာမည်ကို တွက်ချက်သည်။
dt
လွန်ခဲ့ သော ကတည်းကupdate()
. - ဒုံးကျည်တစ်ခုစီကို ပြန်လည်ဆန်းသစ်ပြီး လိုအပ်ပါက ၎င်းတို့ကို ဖျက်ဆီးပစ်ပါ။ ဤလုပ်ဆောင်နိုင်စွမ်းကို နောက်ပိုင်းတွင် အကောင်အထည်ဖော်မှုကို ကျွန်ုပ်တို့ မြင်တွေ့ရမည်ဖြစ်သည်။ လောလောဆယ်တော့ အဲဒါကို သိဖို့ လုံလောက်ပါတယ်။
bullet.update()
ပြန်လာသည်true
အကယ်၍ ကျည်ဆန်ကို ဖျက်ဆီးပစ်ရမည်။ (သူ ကွင်းထဲက ထွက်လာခဲ့တယ်)။ - ကစားသမားတစ်ဦးစီကို အပ်ဒိတ်လုပ်ပြီး လိုအပ်ပါက ဒုံးကျည်တစ်ချောင်းကို ထုတ်ပေးသည်။ ဒီအကောင်အထည်ဖော်မှုကိုလည်း နောက်ပိုင်းမှာ တွေ့ရပါလိမ့်မယ်။
player.update()
အရာဝတ္ထုတစ်ခုကို ပြန်ပေးနိုင်ပါတယ်။Bullet
. - ဒုံးကျည်များနှင့် ကစားသမားများအကြား တိုက်မိမှုများအတွက် စစ်ဆေးမှုများ
applyCollisions()
၎င်းသည် ကစားသမားများကို ထိမှန်သော ပစ်လွှတ်မှုအခင်းအကျင်းကို ပြန်ပေးသည်။ ပြန်ပေးသည့် ကျည်ဆန်တစ်ခုစီအတွက်၊ ၎င်းကို ပစ်ခတ်သည့် ကစားသမား၏ အမှတ်များ တိုးလာသည် (အသုံးပြုသည်။player.onDealtDamage()
) ထို့နောက် array မှ projectile ကို ဖယ်ရှားပါ။bullets
. - သေဆုံးသူကစားသမားအားလုံးကို အသိပေးပြီး ဖျက်ဆီးလိုက်ပါ။
- ကစားသူအားလုံးထံ ဂိမ်းအပ်ဒိတ်တစ်ခု ပေးပို့သည်။ စက္ကန့်တိုင်း ခေါ်တဲ့အချိန်
update()
. ၎င်းက အထက်ဖော်ပြပါ auxiliary variable ကို ခြေရာခံရန် ကျွန်ုပ်တို့အား ကူညီပေးပါသည်။shouldSendUpdate
. အမျှupdate()
အကြိမ် 60 ဟုခေါ်သည်၊ ကျွန်ုပ်တို့သည် ဂိမ်းအပ်ဒိတ်များကို အကြိမ် 30/s ပေးပို့ပါသည်။ ထို့ကြောင့်, နာရီကြိမ်နှုန်း ဆာဗာနာရီသည် နာရီ 30/s (ကျွန်ုပ်တို့ပထမအပိုင်းတွင်နာရီနှုန်းထားများအကြောင်းပြောခဲ့သည်)။
ဂိမ်းအပ်ဒိတ်များကိုသာ ပေးပို့ရခြင်း အချိန်အားဖြင့် ? ချန်နယ်ကို သိမ်းဆည်းရန်။ တစ်စက္ကန့်လျှင် ဂိမ်းမွမ်းမံမှု 30 သည် အလွန်များပြားသည်။
ဘာလို့မခေါ်တာလဲ။
update()
တစ်စက္ကန့်ကို အကြိမ် 30 လား? ဂိမ်းသရုပ်သကန်ကို ပိုမိုကောင်းမွန်စေရန်။ များများခေါ်တတ်တယ်။update()
ဂိမ်းသရုပ်ဖော်မှု ပိုတိကျလေဖြစ်သည်။ ဒါပေမယ့် စိန်ခေါ်မှုအရေအတွက်နဲ့ အရမ်းကြီး လိုက်မမီလိုက်ပါနဲ့။update()
အဘယ်ကြောင့်ဆိုသော် ၎င်းသည် ကွန်ပြူတာစျေးကြီးသောအလုပ်ဖြစ်သောကြောင့် - တစ်စက္ကန့်လျှင် 60 လုံလောက်သည်။
ကျန်တဲ့အတန်း Game
အကူအညီပေးသည့်နည်းလမ်းများ ပါဝင်ပါသည်။ update()
:
game.js အပိုင်း ၁
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
အောက်ပါ extensions များကိုသာ
- အထုပ်ကိုအသုံးပြုခြင်း။
တိုတို ကျပန်းမျိုးဆက်အတွက်id
ကျည်ဆန် - အကွက်တစ်ခုထည့်ခြင်း။
parentID
ဒါမှ ဒီ projectile ကို ဖန်တီးတဲ့ ကစားသမားကို သင် ခြေရာခံနိုင်မှာ ဖြစ်ပါတယ်။ - ပြန်ပေးသည့်တန်ဖိုးကို ပေါင်းထည့်ခြင်း။
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()
အထူးသဖြင့်၊ တစ်ခုမှမကျန်ဘူးဆိုရင် အသစ်ဖန်တီးထားတဲ့ projectile ကို ပြန်ပေးတယ်။ 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 ဂိမ်းကို တည်ဆောက်ပါ။
နမူနာကုဒ်အားလုံးသည် open source ဖြစ်ပြီး တွင်တင်ထားသည်။
source: www.habr.com