በ2015 ተለቋል
ስለእነዚህ ጨዋታዎች ከዚህ በፊት ሰምተህ የማታውቀው ከሆነ፣ ለመጫወት ቀላል የሆኑ ነጻ ባለብዙ ተጫዋች የድር ጨዋታዎች ናቸው (ምንም መለያ አያስፈልግም)። ብዙውን ጊዜ በተመሳሳይ መድረክ ብዙ ተቃራኒ ተጫዋቾችን ይጋፈጣሉ። ሌሎች ታዋቂ የ.io ጨዋታዎች፡-
በዚህ ጽሑፍ ውስጥ, እንዴት እንደሆነ እንመረምራለን ከባዶ የ.io ጨዋታ ይፍጠሩ. ለዚህ, የጃቫስክሪፕት እውቀት ብቻ በቂ ይሆናል: እንደ አገባብ ያሉ ነገሮችን መረዳት ያስፈልግዎታል this
и
.io ጨዋታ ምሳሌ
ለትምህርት እርዳታ፣ እንጠቅሳለን።
ጨዋታው በጣም ቀላል ነው፡ ሌሎች ተጫዋቾች ባሉበት መድረክ ላይ መርከብን ትቆጣጠራላችሁ። የእርስዎ መርከብ በራስ-ሰር ፕሮጄክተሮችን ያቃጥላል እና ሌሎች ተጫዋቾችን ፕሮጄክተሮችን በማስወገድ ላይ እያሉ ለመምታት ይሞክራሉ።
1. የፕሮጀክቱ አጭር መግለጫ / መዋቅር
እኔ አመሰግናለሁ
ምንጭ ኮድ አውርድ እኔን መከተል እንዲችሉ የምሳሌ ጨዋታ።
ምሳሌው የሚከተሉትን ይጠቀማል:
ይግለጹ የጨዋታውን የድር አገልጋይ የሚያስተዳድር በጣም ታዋቂው የ Node.js የድር ማዕቀፍ ነው።ሶኬት.io - በአሳሽ እና በአገልጋይ መካከል ውሂብ ለመለዋወጥ የዌብሶኬት ቤተ-መጽሐፍት።Webpack። - ሞጁል አስተዳዳሪ. ለምን Webpack እንደሚጠቀሙ ማንበብ ይችላሉ.እዚህ .
የፕሮጀክት ማውጫ መዋቅር ምን እንደሚመስል እነሆ፡-
public/
assets/
...
src/
client/
css/
...
html/
index.html
index.js
...
server/
server.js
...
shared/
constants.js
የህዝብ/
በአቃፊ ውስጥ ሁሉም ነገር public/
በአገልጋዩ በስታቲስቲክስ ገቢ ይደረጋል። ውስጥ public/assets/
በእኛ ፕሮጀክት ጥቅም ላይ የዋሉ ምስሎችን ይዟል.
src /
ሁሉም የምንጭ ኮድ በአቃፊው ውስጥ ነው። src/
. ርዕሶች client/
и server/
ለራሳቸው ይናገሩ እና shared/
በደንበኛው እና በአገልጋዩ የሚመጣ ቋሚ ፋይል ይይዛል።
2. ስብሰባዎች / የፕሮጀክት ቅንጅቶች
ከላይ እንደተጠቀሰው ፕሮጀክቱን ለመገንባት ሞጁሉን ሥራ አስኪያጅ እንጠቀማለን.
webpack.common.js፡
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: {
game: './src/client/index.js',
},
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ['@babel/preset-env'],
},
},
},
{
test: /.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
'css-loader',
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'src/client/html/index.html',
}),
],
};
በጣም አስፈላጊዎቹ መስመሮች እዚህ አሉ-
src/client/index.js
የጃቫስክሪፕት (JS) ደንበኛ መግቢያ ነጥብ ነው። ዌብፓክ ከዚህ ይጀምራል እና ሌሎች ወደ ሀገር ውስጥ የሚገቡ ፋይሎችን በየጊዜው ይፈልጋል።- የእኛ የዌብፓክ ግንባታ ውጤት JS በማውጫው ውስጥ ይቀመጣል
dist/
. ይህንን ፋይል የኛ እደውላለሁ። js ጥቅል. - እኛ እንጠቀማለን
ባቤል , እና በተለይም አወቃቀሩ@babel/preset-env ለአሮጌ አሳሾች የኛን JS ኮድ ለመፃፍ። - በJS ፋይሎች የተጠቀሱ ሁሉንም CSS ለማውጣት እና በአንድ ቦታ ለማጣመር ፕለጊን እየተጠቀምን ነው። የኛ እለዋለሁ። css ጥቅል.
እንግዳ የጥቅል ፋይል ስሞችን አስተውለህ ይሆናል። '[name].[contenthash].ext'
. ይይዛሉ [name]
በግቤት ነጥቡ ስም ይተካዋል (በእኛ ሁኔታ ይህ game
) ፣ እና [contenthash]
በፋይሉ ይዘት ሃሽ ይተካል። እናደርገዋለን contenthash
). የመጨረሻው ውጤት የእይታ ፋይል ስም ይሆናል game.dbeee76e91a97d0c7207.js
.
ፋይል webpack.common.js
ወደ ግንባታ እና የተጠናቀቁ የፕሮጀክት ውቅሮች የምናስገባው የመሠረት ውቅር ፋይል ነው። የልማት ውቅር ምሳሌ ይኸውና፡
webpack.dev.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'development',
});
ለውጤታማነት, በልማት ሂደት ውስጥ እንጠቀማለን webpack.dev.js
, እና ወደ ይቀይራል webpack.prod.js
ወደ ምርት በሚሰራጭበት ጊዜ የጥቅል መጠኖችን ለማመቻቸት.
የአካባቢ ቅንብር
በዚህ ጽሑፍ ውስጥ የተዘረዘሩትን ደረጃዎች መከተል እንዲችሉ ፕሮጀክቱን በአካባቢው ማሽን ላይ እንዲጭኑ እመክራለሁ. ማዋቀሩ ቀላል ነው: በመጀመሪያ, ስርዓቱ መጫን አለበት
$ git clone https://github.com/vzhou842/example-.io-game.git
$ cd example-.io-game
$ npm install
እና እርስዎ ለመሄድ ዝግጁ ነዎት! የልማት አገልጋዩን ለመጀመር፣ በቀላሉ ያሂዱ
$ npm run develop
እና ወደ ድር አሳሽ ይሂዱ
3. የደንበኛ መግቢያ ነጥቦች
ወደ ጨዋታው ኮድ እራሱ እንውረድ። መጀመሪያ ገጽ እንፈልጋለን index.html
, ጣቢያውን ሲጎበኙ, አሳሹ መጀመሪያ ይጭነዋል. ገጻችን በጣም ቀላል ይሆናል፡-
index.html
ምሳሌ .io ጨዋታ ተጫወት
ይህ የኮድ ምሳሌ ግልጽ ለማድረግ በትንሹ ቀለል ያለ ነው፣ እና ከብዙ ሌሎች የልጥፍ ምሳሌዎች ጋር ተመሳሳይ ነገር አደርጋለሁ። ሙሉ ኮድ ሁል ጊዜ በ ላይ ሊታይ ይችላል።
እና አለነ:
HTML5 ሸራ አባል (<canvas>
) ጨዋታውን ለመስራት የምንጠቀምበት።<link>
የእኛን CSS ጥቅል ለመጨመር።<script>
የእኛን ጃቫስክሪፕት ጥቅል ለመጨመር።- ዋና ምናሌ ከተጠቃሚ ስም ጋር
<input>
እና የ PLAY ቁልፍ (<button>
).
የመነሻ ገጹን ከጫኑ በኋላ አሳሹ ከመግቢያ ነጥብ JS ፋይል ጀምሮ የጃቫስክሪፕት ኮድን መተግበር ይጀምራል። src/client/index.js
.
index.js
import { connect, play } from './networking';
import { startRendering, stopRendering } from './render';
import { startCapturingInput, stopCapturingInput } from './input';
import { downloadAssets } from './assets';
import { initState } from './state';
import { setLeaderboardHidden } from './leaderboard';
import './css/main.css';
const playMenu = document.getElementById('play-menu');
const playButton = document.getElementById('play-button');
const usernameInput = document.getElementById('username-input');
Promise.all([
connect(),
downloadAssets(),
]).then(() => {
playMenu.classList.remove('hidden');
usernameInput.focus();
playButton.onclick = () => {
// Play!
play(usernameInput.value);
playMenu.classList.add('hidden');
initState();
startCapturingInput();
startRendering();
setLeaderboardHidden(false);
};
});
ይህ ውስብስብ ሊመስል ይችላል፣ ነገር ግን እዚህ ብዙ እየተካሄደ አይደለም፡
- ሌሎች በርካታ JS ፋይሎችን በማስመጣት ላይ።
- CSS ማስመጣት (ስለዚህ ዌብፓክ በCSS ጥቅል ውስጥ እንደሚያካትታቸው ያውቃል)።
- Запуск
connect()
ከአገልጋዩ ጋር ግንኙነት ለመመስረት እና ለማሄድdownloadAssets()
ጨዋታውን ለመስራት የሚያስፈልጉ ምስሎችን ለማውረድ። - ደረጃ 3 ከተጠናቀቀ በኋላ ዋናው ምናሌ ይታያል (
playMenu
). - የ"PLAY" ቁልፍን ለመጫን ተቆጣጣሪውን በማዘጋጀት ላይ። ቁልፉ ሲጫን ኮዱ ጨዋታውን ይጀምራል እና እኛ ለመጫወት ዝግጁ መሆናችንን ለአገልጋዩ ይነግረዋል።
የእኛ የደንበኛ-አገልጋይ አመክንዮ ዋናው "ስጋ" በፋይሉ ወደ ሀገር ውስጥ በገቡት ፋይሎች ውስጥ ነው index.js
. አሁን ሁሉንም በቅደም ተከተል እንመለከታለን.
4. የደንበኛ ውሂብ መለዋወጥ
በዚህ ጨዋታ ከአገልጋዩ ጋር ለመገናኘት የታወቀ ቤተ-መጽሐፍትን እንጠቀማለን።
አንድ ፋይል ይኖረናል src/client/networking.js
ማን ይንከባከባል ሁሉም ሰው ከአገልጋዩ ጋር መገናኘት;
አውታረ መረብ.js
import io from 'socket.io-client';
import { processGameUpdate } from './state';
const Constants = require('../shared/constants');
const socket = io(`ws://${window.location.host}`);
const connectedPromise = new Promise(resolve => {
socket.on('connect', () => {
console.log('Connected to server!');
resolve();
});
});
export const connect = onGameOver => (
connectedPromise.then(() => {
// Register callbacks
socket.on(Constants.MSG_TYPES.GAME_UPDATE, processGameUpdate);
socket.on(Constants.MSG_TYPES.GAME_OVER, onGameOver);
})
);
export const play = username => {
socket.emit(Constants.MSG_TYPES.JOIN_GAME, username);
};
export const updateDirection = dir => {
socket.emit(Constants.MSG_TYPES.INPUT, dir);
};
ይህ ኮድ ለግልጽነትም በትንሹ እንዲታጠር ተደርጓል።
በዚህ ፋይል ውስጥ ሶስት ዋና ተግባራት አሉ፡-
- ከአገልጋዩ ጋር ለመገናኘት እየሞከርን ነው።
connectedPromise
ግንኙነት ስንፈጥር ብቻ ነው የሚፈቀደው። - ግንኙነቱ ስኬታማ ከሆነ የመልሶ መደወል ተግባራትን እንመዘግባለን (
processGameUpdate()
иonGameOver()
) ከአገልጋዩ ልንቀበላቸው ለሚችሉ መልእክቶች። - ወደ ውጭ እንልካለን
play()
иupdateDirection()
ሌሎች ፋይሎችን መጠቀም እንዲችሉ.
5. የደንበኛ አቀራረብ
ምስሉን በስክሪኑ ላይ ለማሳየት ጊዜው አሁን ነው!
ነገር ግን ያንን ከማድረጋችን በፊት፣ ለዚህ የሚያስፈልጉትን ሁሉንም ምስሎች (ሃብቶች) ማውረድ አለብን። የሀብት አስተዳዳሪን እንፃፍ፡-
ንብረቶች.js
const ASSET_NAMES = ['ship.svg', 'bullet.svg'];
const assets = {};
const downloadPromise = Promise.all(ASSET_NAMES.map(downloadAsset));
function downloadAsset(assetName) {
return new Promise(resolve => {
const asset = new Image();
asset.onload = () => {
console.log(`Downloaded ${assetName}`);
assets[assetName] = asset;
resolve();
};
asset.src = `/assets/${assetName}`;
});
}
export const downloadAssets = () => downloadPromise;
export const getAsset = assetName => assets[assetName];
የሀብት አስተዳደር ለመተግበር ያን ያህል ከባድ አይደለም! ዋናው ሀሳብ እቃ ማከማቸት ነው assets
, ይህም የፋይል ስም ቁልፍን ከእቃው ዋጋ ጋር ያስራል Image
. ሀብቱ ሲጫን በአንድ ዕቃ ውስጥ እናከማቻለን assets
ለወደፊቱ ፈጣን መዳረሻ. እያንዳንዱ ግለሰብ መገልገያ መቼ ማውረድ ይፈቀድለታል (ማለትም፣ ሁሉም ሀብቶች) እንፈቅዳለን። downloadPromise
.
ንብረቶቹን ካወረዱ በኋላ ማሳየት መጀመር ይችላሉ። ቀደም ሲል እንደተናገረው, በድረ-ገጽ ላይ ለመሳል, እንጠቀማለን <canvas>
). የኛ ጨዋታ በጣም ቀላል ነው ስለዚህ የሚከተለውን መሳል ብቻ ያስፈልገናል።
- ጀርባ
- የተጫዋች መርከብ
- በጨዋታው ውስጥ ያሉ ሌሎች ተጫዋቾች
- ዛጎሎች
አስፈላጊዎቹ ቅንጥቦች እዚህ አሉ። src/client/render.js
ከላይ የተዘረዘሩትን አራት ነገሮች በትክክል የሚያቀርብ፡-
ማቅረብ.js
import { getAsset } from './assets';
import { getCurrentState } from './state';
const Constants = require('../shared/constants');
const { PLAYER_RADIUS, PLAYER_MAX_HP, BULLET_RADIUS, MAP_SIZE } = Constants;
// Get the canvas graphics context
const canvas = document.getElementById('game-canvas');
const context = canvas.getContext('2d');
// Make the canvas fullscreen
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
function render() {
const { me, others, bullets } = getCurrentState();
if (!me) {
return;
}
// Draw background
renderBackground(me.x, me.y);
// Draw all bullets
bullets.forEach(renderBullet.bind(null, me));
// Draw all players
renderPlayer(me, me);
others.forEach(renderPlayer.bind(null, me));
}
// ... Helper functions here excluded
let renderInterval = null;
export function startRendering() {
renderInterval = setInterval(render, 1000 / 60);
}
export function stopRendering() {
clearInterval(renderInterval);
}
ይህ ኮድ ግልጽ ለማድረግም አጠረ።
render()
የዚህ ፋይል ዋና ተግባር ነው። startRendering()
и stopRendering()
የ render loop ን እንቅስቃሴ በ60 FPS ይቆጣጠሩ።
የግለሰባዊ ረዳት ተግባራት ተጨባጭ ትግበራዎች (ለምሳሌ ፣ renderBullet()
) ያን ያህል አስፈላጊ አይደሉም፣ ግን አንድ ቀላል ምሳሌ እዚህ አለ፡-
ማቅረብ.js
function renderBullet(me, bullet) {
const { x, y } = bullet;
context.drawImage(
getAsset('bullet.svg'),
canvas.width / 2 + x - me.x - BULLET_RADIUS,
canvas.height / 2 + y - me.y - BULLET_RADIUS,
BULLET_RADIUS * 2,
BULLET_RADIUS * 2,
);
}
ዘዴውን እየተጠቀምን መሆኑን ልብ ይበሉ getAsset()
, ቀደም ሲል በ ውስጥ ታይቷል asset.js
!
ስለ ሌሎች አጋዥ ረዳቶች ለመማር ፍላጎት ካሎት የቀረውን ያንብቡ።
src/ደንበኛ/render.js .
6. የደንበኛ ግቤት
ጨዋታ ለመስራት ጊዜው አሁን ነው። ሊጫወት የሚችል! የቁጥጥር መርሃግብሩ በጣም ቀላል ይሆናል-የእንቅስቃሴውን አቅጣጫ ለመለወጥ, አይጤውን (በኮምፒተር ላይ) መጠቀም ወይም ማያ ገጹን (በሞባይል መሳሪያ ላይ) መንካት ይችላሉ. ይህንን ተግባራዊ ለማድረግ እንመዘግባለን።
ይህንን ሁሉ ይንከባከባል src/client/input.js
:
ግብዓት.js
import { updateDirection } from './networking';
function onMouseInput(e) {
handleInput(e.clientX, e.clientY);
}
function onTouchInput(e) {
const touch = e.touches[0];
handleInput(touch.clientX, touch.clientY);
}
function handleInput(x, y) {
const dir = Math.atan2(x - window.innerWidth / 2, window.innerHeight / 2 - y);
updateDirection(dir);
}
export function startCapturingInput() {
window.addEventListener('mousemove', onMouseInput);
window.addEventListener('touchmove', onTouchInput);
}
export function stopCapturingInput() {
window.removeEventListener('mousemove', onMouseInput);
window.removeEventListener('touchmove', onTouchInput);
}
onMouseInput()
и onTouchInput()
ጥሪ የሚያደርጉ የክስተት አድማጮች ናቸው። updateDirection()
(የ networking.js
) የግቤት ክስተት ሲከሰት (ለምሳሌ, አይጥ ሲንቀሳቀስ). updateDirection()
የግብአት ክስተትን የሚያስተናግድ እና የጨዋታውን ሁኔታ የሚያሻሽለው ከአገልጋዩ ጋር መልእክትን ያስተናግዳል።
7. የደንበኛ ሁኔታ
ይህ ክፍል በፖስታው የመጀመሪያ ክፍል ውስጥ በጣም አስቸጋሪው ነው. ለመጀመሪያ ጊዜ ስታነቡት ካልተረዳችሁት ተስፋ አትቁረጡ! እንዲያውም መዝለል ትችላለህ እና በኋላ ወደ እሱ መመለስ ትችላለህ።
የደንበኛው/የአገልጋይ ኮድ ለማጠናቀቅ የሚያስፈልገው የመጨረሻው የእንቆቅልሽ ክፍል ነው። ግዛት. ከደንበኛ አተረጓጎም ክፍል የተገኘውን የኮድ ቅንጣቢ አስታውስ?
ማቅረብ.js
import { getCurrentState } from './state';
function render() {
const { me, others, bullets } = getCurrentState();
// Do the rendering
// ...
}
getCurrentState()
በደንበኛው ውስጥ የጨዋታውን ወቅታዊ ሁኔታ ሊሰጠን ይገባል በማንኛውም ጊዜ ከአገልጋዩ በተቀበሉት ዝመናዎች ላይ በመመስረት። አገልጋዩ ሊልክ የሚችለው የጨዋታ ማሻሻያ ምሳሌ ይኸውና፡
{
"t": 1555960373725,
"me": {
"x": 2213.8050880413657,
"y": 1469.370893425012,
"direction": 1.3082443894581433,
"id": "AhzgAtklgo2FJvwWAADO",
"hp": 100
},
"others": [],
"bullets": [
{
"id": "RUJfJ8Y18n",
"x": 2354.029197099604,
"y": 1431.6848318262666
},
{
"id": "ctg5rht5s",
"x": 2260.546457727445,
"y": 1456.8088728920968
}
],
"leaderboard": [
{
"username": "Player",
"score": 3
}
]
}
እያንዳንዱ የጨዋታ ዝመና አምስት ተመሳሳይ መስኮችን ይይዛል።
- tይህ ዝማኔ መቼ እንደተፈጠረ የሚጠቁም የአገልጋይ የጊዜ ማህተም።
- meይህንን ዝመና ስለተቀበለው ተጫዋች መረጃ።
- ሌሎች: ሌሎች ተጫዋቾች በተመሳሳይ ጨዋታ ውስጥ ስለሚሳተፉ መረጃ ድርድር።
- ጥይቶችበጨዋታው ውስጥ ስለ ፕሮጄክቶች መረጃ ድርድር።
- የመሪዎች ሰሌዳአሁን ያለው የመሪዎች ሰሌዳ ውሂብ። በዚህ ጽሑፍ ውስጥ, እኛ አንመለከታቸውም.
7.1 Naive ደንበኛ ሁኔታ
የዋህ አተገባበር getCurrentState()
በቅርቡ የተቀበለውን የጨዋታ ዝመና ውሂብ ብቻ ነው መመለስ የሚችለው።
naive-state.js
let lastGameUpdate = null;
// Handle a newly received game update.
export function processGameUpdate(update) {
lastGameUpdate = update;
}
export function getCurrentState() {
return lastGameUpdate;
}
ቆንጆ እና ግልጽ! ግን ያን ያህል ቀላል ቢሆን ኖሮ። የዚህ አተገባበር አንዱ ምክንያት ችግር ያለበት ነው፡- የፍሬም ፍጥነቱን በአገልጋይ ሰዓት ፍጥነት ይገድባል.
የፍሬም መጠንየክፈፎች ብዛት (ማለትም ጥሪዎች
render()
) በሰከንድ ወይም FPS. ጨዋታዎች አብዛኛውን ጊዜ ቢያንስ 60 FPS ለማግኘት ይጥራሉ.
የምልክት መጠን: አገልጋዩ የጨዋታ ዝመናዎችን ለደንበኞች የሚልክበት ድግግሞሽ። ብዙውን ጊዜ ከክፈፉ ፍጥነት ያነሰ ነው. በእኛ ጨዋታ አገልጋዩ በሰከንድ በ30 ዑደቶች ድግግሞሽ ይሰራል።
የጨዋታውን የቅርብ ጊዜ ዝመና ብቻ ካቀረብን፣ FPS በመሠረቱ ከ 30 በላይ አይበልጥም ፣ ምክንያቱም ከአገልጋዩ በሰከንድ ከ30 በላይ ዝማኔዎችን አናገኝም።. ብንጠራም render()
በሰከንድ 60 ጊዜ፣ ከዚያ ከእነዚህ ጥሪዎች ውስጥ ግማሾቹ አንድ አይነት ነገር እንደገና ይቀይራሉ፣ በመሠረቱ ምንም ነገር አያደርጉም። ሌላው የዋህ አተገባበር ችግር ነው። ለመዘግየት የተጋለጠ. በጥሩ የኢንተርኔት ፍጥነት፣ ደንበኛው በየ33ሚሴ (በሴኮንድ 30 በሰከንድ) የጨዋታ ዝማኔ ይቀበላል።
በሚያሳዝን ሁኔታ, ምንም ነገር ፍጹም አይደለም. የበለጠ ትክክለኛ ምስል የሚከተለው ይሆናል-
የዋህነት አተገባበር ወደ መዘግየት ሲመጣ በተግባር እጅግ የከፋ ነው። የጨዋታ ዝመና በ 50ms መዘግየት ከደረሰ ፣ ከዚያ የደንበኛ መሸጫዎች ተጨማሪ 50 ሚሴ ምክንያቱም አሁንም የጨዋታውን ሁኔታ ከቀደመው ዝማኔ እያቀረበ ነው። ይህ ለተጫዋቹ ምን ያህል የማይመች እንደሆነ መገመት ትችላላችሁ፡ የዘፈቀደ ብሬኪንግ ጨዋታውን ያሸበረቀ እና ያልተረጋጋ ያደርገዋል።
7.2 የተሻሻለ የደንበኛ ሁኔታ
በአፈፃፀሙ ላይ አንዳንድ ማሻሻያዎችን እናደርጋለን። በመጀመሪያ, እንጠቀማለን መዘግየት ማሳየት ለ 100 ms. ይህ ማለት የደንበኛው "የአሁኑ" ሁኔታ ሁል ጊዜ በአገልጋዩ ላይ ካለው የጨዋታ ሁኔታ በ100 ሚ.ሴ. ለምሳሌ, በአገልጋዩ ላይ ያለው ጊዜ ከሆነ 150, ከዚያም ደንበኛው አገልጋዩ በወቅቱ የነበረውን ሁኔታ ያቀርባል 50:
ይህ የማይገመት የጨዋታ ዝመና ጊዜዎችን ለመትረፍ የ100ms ቋት ይሰጠናል፡
ለዚህ ያለው ክፍያ ዘላቂ ይሆናል
ሌላ የሚባል ቴክኒክ መጠቀም እንችላለን
የደንበኛ-ጎን ትንበያ , ይህም የታሰበውን መዘግየትን በመቀነስ ጥሩ ስራ ይሰራል, ነገር ግን በዚህ ጽሑፍ ውስጥ አይካተትም.
ሌላው እየተጠቀምንበት ያለው ማሻሻያ ነው። መስመራዊ ጣልቃገብነት. በማሳየት መዘግየት ምክንያት፣ እኛ ብዙውን ጊዜ በደንበኛው ውስጥ ካለው የአሁኑ ጊዜ በፊት ቢያንስ አንድ ዝመና እንሆናለን። ሲጠራ getCurrentState()
, መፈጸም እንችላለን
ይሄ የፍሬም ተመን ችግርን ይፈታል፡ አሁን ልዩ ፍሬሞችን በምንፈልገው የፍሬም ፍጥነት መስራት እንችላለን!
7.3 የተሻሻለ የደንበኛ ሁኔታን በመተግበር ላይ
የትግበራ ምሳሌ በ src/client/state.js
ሁለቱንም render lag እና linear interpolation ይጠቀማል፣ ግን ለረጅም ጊዜ አይደለም። ኮዱን በሁለት ክፍሎች እንከፋፍል። የመጀመሪያው ይኸውና፡-
state.js ክፍል 1
const RENDER_DELAY = 100;
const gameUpdates = [];
let gameStart = 0;
let firstServerTimestamp = 0;
export function initState() {
gameStart = 0;
firstServerTimestamp = 0;
}
export function processGameUpdate(update) {
if (!firstServerTimestamp) {
firstServerTimestamp = update.t;
gameStart = Date.now();
}
gameUpdates.push(update);
// Keep only one game update before the current server time
const base = getBaseUpdate();
if (base > 0) {
gameUpdates.splice(0, base);
}
}
function currentServerTime() {
return firstServerTimestamp + (Date.now() - gameStart) - RENDER_DELAY;
}
// Returns the index of the base update, the first game update before
// current server time, or -1 if N/A.
function getBaseUpdate() {
const serverTime = currentServerTime();
for (let i = gameUpdates.length - 1; i >= 0; i--) {
if (gameUpdates[i].t <= serverTime) {
return i;
}
}
return -1;
}
የመጀመሪያው እርምጃ ምን እንደሆነ ማወቅ ነው currentServerTime()
. ቀደም ሲል እንዳየነው እያንዳንዱ የጨዋታ ዝመና የአገልጋይ ጊዜ ማህተምን ያካትታል። ምስሉን ከአገልጋዩ ጀርባ 100ms ለማቅረብ የምስል ስራ መዘግየትን መጠቀም እንፈልጋለን ነገር ግን በአገልጋዩ ላይ ያለውን ጊዜ ፈጽሞ አናውቅም፣ ምክንያቱም ማሻሻያዎቹ ወደ እኛ ለመድረስ ምን ያህል ጊዜ እንደወሰደ ማወቅ አንችልም። በይነመረቡ የማይታወቅ እና ፍጥነቱ በጣም ሊለያይ ይችላል!
በዚህ ችግር ዙሪያ ለማግኘት, እኛ ምክንያታዊ approximation መጠቀም ይችላሉ: እኛ የመጀመሪያው ዝማኔ ወዲያውኑ እንደደረሰ አስመስሎ. ይህ እውነት ከሆነ፣ በዚህ ጊዜ የአገልጋዩን ጊዜ እናውቅ ነበር! የአገልጋዩን የጊዜ ማህተም እናከማቻለን። firstServerTimestamp
እና የእኛን ጠብቅ አካባቢያዊ (ደንበኛ) የጊዜ ማህተም በተመሳሳይ ቅጽበት ውስጥ gameStart
.
ቆይ. የአገልጋይ ጊዜ = የደንበኛ ጊዜ መሆን የለበትም? ለምንድን ነው በ"አገልጋይ ጊዜ ማህተም" እና "ደንበኛ የጊዜ ማህተም" መካከል የምንለየው? ይህ ትልቅ ጥያቄ ነው! አንድ አይነት ነገር እንዳልሆኑ ታወቀ። Date.now()
በደንበኛው እና በአገልጋዩ ውስጥ የተለያዩ የጊዜ ማህተሞችን ይመልሳል ፣ እና በእነዚህ ማሽኖች አካባቢያዊ ሁኔታዎች ላይ የተመሠረተ ነው። የጊዜ ማህተሞች በሁሉም ማሽኖች ላይ አንድ አይነት ይሆናሉ ብላችሁ አታስቡ።
አሁን ምን እንደሚሰራ ተረድተናል currentServerTime()
: ይመለሳል የአሁኑ የምስል ጊዜ የአገልጋይ ጊዜ ማህተም. በሌላ አነጋገር ይህ የአገልጋዩ የአሁኑ ጊዜ ነው (firstServerTimestamp <+ (Date.now() - gameStart)
የዘገየ መዘግየት ()RENDER_DELAY
).
አሁን የጨዋታ ዝመናዎችን እንዴት እንደምንይዝ እንመልከት። ከዝማኔ አገልጋይ ሲደርሰው ይባላል processGameUpdate()
እና አዲሱን ዝመና ወደ ድርድር እናስቀምጠዋለን gameUpdates
. ከዚያ የማህደረ ትውስታውን አጠቃቀም ለመፈተሽ ከዚህ በፊት ሁሉንም የቆዩ ዝመናዎችን እናስወግዳለን። የመሠረት ማሻሻያምክንያቱም እኛ ከእንግዲህ አንፈልጋቸውም።
"መሰረታዊ ማሻሻያ" ምንድን ነው? ይህ ከአገልጋዩ ወቅታዊ ጊዜ ወደ ኋላ በመንቀሳቀስ የምናገኘው የመጀመሪያው ማሻሻያ. ይህን ሥዕላዊ መግለጫ አስታውስ?
የጨዋታው ማሻሻያ በቀጥታ በ "የደንበኛ መስጫ ጊዜ" በስተግራ የመሠረት ማሻሻያ ነው።
የመሠረት ዝማኔው ለምን ጥቅም ላይ ይውላል? ለምንድነው ማሻሻያዎችን ወደ መነሻ መስመር መጣል የምንችለው? ይህንን ለማወቅ እንሞክር በመጨረሻ አተገባበሩን አስቡበት getCurrentState()
:
state.js ክፍል 2
export function getCurrentState() {
if (!firstServerTimestamp) {
return {};
}
const base = getBaseUpdate();
const serverTime = currentServerTime();
// If base is the most recent update we have, use its state.
// Else, interpolate between its state and the state of (base + 1).
if (base < 0) {
return gameUpdates[gameUpdates.length - 1];
} else if (base === gameUpdates.length - 1) {
return gameUpdates[base];
} else {
const baseUpdate = gameUpdates[base];
const next = gameUpdates[base + 1];
const r = (serverTime - baseUpdate.t) / (next.t - baseUpdate.t);
return {
me: interpolateObject(baseUpdate.me, next.me, r),
others: interpolateObjectArray(baseUpdate.others, next.others, r),
bullets: interpolateObjectArray(baseUpdate.bullets, next.bullets, r),
};
}
}
ሶስት ጉዳዮችን እንይዛለን-
base < 0
ይህ ማለት አሁን እስካለበት ጊዜ ድረስ ምንም ዝማኔዎች የሉም (ከላይ ያለውን ትግበራ ይመልከቱgetBaseUpdate()
). ይህ በጨዋታው መጀመሪያ ላይ በሂደት መዘግየት ምክንያት ሊከሰት ይችላል። በዚህ አጋጣሚ, የተቀበለውን የቅርብ ጊዜ ዝመና እንጠቀማለን.base
ያለን የቅርብ ጊዜ ዝመና ነው። ይህ በኔትወርክ መዘግየት ወይም ደካማ የበይነመረብ ግንኙነት ምክንያት ሊሆን ይችላል. በዚህ አጋጣሚ፣ ያለንን የቅርብ ጊዜ ዝመና እየተጠቀምን ነው።- ከአሁኑ የመስሪያ ጊዜ በፊትም ሆነ በኋላ ማሻሻያ አለን፣ ስለዚህ እንችላለን ጣልቃ መግባት!
የቀረው ሁሉ state.js
ቀላል (ነገር ግን አሰልቺ) ሒሳብ የሆነ የመስመራዊ ጣልቃገብነት ትግበራ ነው። እራስዎ ማሰስ ከፈለጉ ከዚያ ይክፈቱት። state.js
ላይ
ክፍል 2. የጀርባ አገልጋይ
በዚህ ክፍል የእኛን የሚቆጣጠረውን የ Node.js ጀርባ እንመለከታለን
1. የአገልጋይ መግቢያ ነጥብ
የድር አገልጋዩን ለማስተዳደር ለ Node.js የሚታወቅ ታዋቂ የድር ማዕቀፍ እንጠቀማለን። src/server/server.js
:
አገልጋይ.js ክፍል 1
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackConfig = require('../../webpack.dev.js');
// Setup an Express server
const app = express();
app.use(express.static('public'));
if (process.env.NODE_ENV === 'development') {
// Setup Webpack for development
const compiler = webpack(webpackConfig);
app.use(webpackDevMiddleware(compiler));
} else {
// Static serve the dist/ folder in production
app.use(express.static('dist'));
}
// Listen on port
const port = process.env.PORT || 3000;
const server = app.listen(port);
console.log(`Server listening on port ${port}`);
በመጀመሪያው ክፍል ስለ Webpack እንደተነጋገርን አስታውስ? የኛን የዌብፓክ አወቃቀሮችን የምንጠቀምበት ይህ ነው። በሁለት መንገዶች እንጠቀማቸዋለን፡-
- ተጠቀም
የዌብፓክ-ዴቭ-ሚድልዌር የእኛን የእድገት ፓኬጆችን በራስ ሰር እንደገና ለመገንባት ወይም - በስታቲስቲክስ አቃፊ ያስተላልፉ
dist/
ከምርቱ ግንባታ በኋላ ፋይሎቻችንን በየትኛው የዌብፓክ እንጽፋለን።
ሌላው አስፈላጊ ተግባር server.js
አገልጋዩን ማዋቀር ነው።
አገልጋይ.js ክፍል 2
const socketio = require('socket.io');
const Constants = require('../shared/constants');
// Setup Express
// ...
const server = app.listen(port);
console.log(`Server listening on port ${port}`);
// Setup socket.io
const io = socketio(server);
// Listen for socket.io connections
io.on('connection', socket => {
console.log('Player connected!', socket.id);
socket.on(Constants.MSG_TYPES.JOIN_GAME, joinGame);
socket.on(Constants.MSG_TYPES.INPUT, handleInput);
socket.on('disconnect', onDisconnect);
});
የ socket.io ግንኙነት ከአገልጋዩ ጋር በተሳካ ሁኔታ ካቋቋምን በኋላ ለአዲሱ ሶኬት የክስተት ተቆጣጣሪዎችን አዘጋጅተናል። የክስተት ተቆጣጣሪዎች ከደንበኞች የተቀበሉትን መልእክት ለአንድ ነጠላ ዕቃ በማስተላለፍ ያስተናግዳሉ። game
:
አገልጋይ.js ክፍል 3
const Game = require('./game');
// ...
// Setup the Game
const game = new Game();
function joinGame(username) {
game.addPlayer(this, username);
}
function handleInput(dir) {
game.handleInput(this, dir);
}
function onDisconnect() {
game.removePlayer(this);
}
የ.io ጨዋታ እየፈጠርን ነው፣ ስለዚህ አንድ ቅጂ ብቻ እንፈልጋለን Game
("ጨዋታ") - ሁሉም ተጫዋቾች በአንድ መድረክ ውስጥ ይጫወታሉ! በሚቀጥለው ክፍል ይህ ክፍል እንዴት እንደሚሰራ እንመለከታለን. Game
.
2. የጨዋታ አገልጋዮች
ክፍል Game
በአገልጋዩ በኩል በጣም አስፈላጊ የሆነውን አመክንዮ ይዟል. ሁለት ዋና ተግባራት አሉት፡- የተጫዋች አስተዳደር и የጨዋታ ማስመሰል.
ከመጀመሪያው ተግባር ማለትም የተጫዋች አስተዳደር እንጀምር።
game.js ክፍል 1
const Constants = require('../shared/constants');
const Player = require('./player');
class Game {
constructor() {
this.sockets = {};
this.players = {};
this.bullets = [];
this.lastUpdateTime = Date.now();
this.shouldSendUpdate = false;
setInterval(this.update.bind(this), 1000 / 60);
}
addPlayer(socket, username) {
this.sockets[socket.id] = socket;
// Generate a position to start this player at.
const x = Constants.MAP_SIZE * (0.25 + Math.random() * 0.5);
const y = Constants.MAP_SIZE * (0.25 + Math.random() * 0.5);
this.players[socket.id] = new Player(socket.id, username, x, y);
}
removePlayer(socket) {
delete this.sockets[socket.id];
delete this.players[socket.id];
}
handleInput(socket, dir) {
if (this.players[socket.id]) {
this.players[socket.id].setDirection(dir);
}
}
// ...
}
በዚህ ጨዋታ ተጫዋቾቹን በሜዳ እንለያቸዋለን id
their socket.io socket (ግራ ከተጋቡ፣ ከዚያ ይመለሱ server.js
). Socket.io ራሱ ለእያንዳንዱ ሶኬት ልዩ ይመድባል id
ስለዚህ ስለዚህ ጉዳይ መጨነቅ አያስፈልገንም. እደውልለታለሁ። የተጫዋች መታወቂያ.
ያንን በአእምሯችን ይዘን፣ በአንድ ክፍል ውስጥ ያሉ የአብነት ተለዋዋጮችን እንመርምር Game
:
sockets
የተጫዋች መታወቂያውን ከተጫዋቹ ጋር ከተገናኘው ሶኬት ጋር የሚያገናኝ ነገር ነው። በቋሚ ጊዜ ውስጥ ሶኬቶችን በተጫዋች መታወቂያቸው እንድንደርስ ያስችለናል።players
የተጫዋች መታወቂያውን ከኮዱ>ተጫዋች ነገር ጋር የሚያገናኝ ዕቃ ነው።
bullets
የነገሮች ስብስብ ነው። Bullet
, እሱም የተወሰነ ቅደም ተከተል የሌለው.
lastUpdateTime
ጨዋታው ለመጨረሻ ጊዜ የተዘመነበት የጊዜ ማህተም ነው። እንዴት ጥቅም ላይ እንደሚውል በቅርቡ እንመለከታለን።
shouldSendUpdate
ረዳት ተለዋዋጭ ነው. አጠቃቀሙንም በቅርቡ እንመለከታለን።
ዘዴዎች addPlayer()
, removePlayer()
и handleInput()
ማብራራት አያስፈልግም, በ ውስጥ ጥቅም ላይ ይውላሉ server.js
. የማስታወስ ችሎታዎን ማደስ ከፈለጉ ትንሽ ከፍ ብለው ይመለሱ።
የመጨረሻው መስመር constructor()
ይጀምራል የዝማኔ ዑደት ጨዋታዎች (ከ60 ዝመናዎች ድግግሞሽ ጋር)
game.js ክፍል 2
const Constants = require('../shared/constants');
const applyCollisions = require('./collisions');
class Game {
// ...
update() {
// Calculate time elapsed
const now = Date.now();
const dt = (now - this.lastUpdateTime) / 1000;
this.lastUpdateTime = now;
// Update each bullet
const bulletsToRemove = [];
this.bullets.forEach(bullet => {
if (bullet.update(dt)) {
// Destroy this bullet
bulletsToRemove.push(bullet);
}
});
this.bullets = this.bullets.filter(
bullet => !bulletsToRemove.includes(bullet),
);
// Update each player
Object.keys(this.sockets).forEach(playerID => {
const player = this.players[playerID];
const newBullet = player.update(dt);
if (newBullet) {
this.bullets.push(newBullet);
}
});
// Apply collisions, give players score for hitting bullets
const destroyedBullets = applyCollisions(
Object.values(this.players),
this.bullets,
);
destroyedBullets.forEach(b => {
if (this.players[b.parentID]) {
this.players[b.parentID].onDealtDamage();
}
});
this.bullets = this.bullets.filter(
bullet => !destroyedBullets.includes(bullet),
);
// Check if any players are dead
Object.keys(this.sockets).forEach(playerID => {
const socket = this.sockets[playerID];
const player = this.players[playerID];
if (player.hp <= 0) {
socket.emit(Constants.MSG_TYPES.GAME_OVER);
this.removePlayer(socket);
}
});
// Send a game update to each player every other time
if (this.shouldSendUpdate) {
const leaderboard = this.getLeaderboard();
Object.keys(this.sockets).forEach(playerID => {
const socket = this.sockets[playerID];
const player = this.players[playerID];
socket.emit(
Constants.MSG_TYPES.GAME_UPDATE,
this.createUpdate(player, leaderboard),
);
});
this.shouldSendUpdate = false;
} else {
this.shouldSendUpdate = true;
}
}
// ...
}
ዘዴ update()
ምናልባት በጣም አስፈላጊ የሆነውን የአገልጋይ-ጎን አመክንዮ ይዟል። የሚሠራው ይኸውና፣ በቅደም ተከተል፡-
- ለምን ያህል ጊዜ ያሰላል
dt
ካለፈው ጀምሮ አልፏልupdate()
. - እያንዳንዱን ፕሮጀክት ያድሳል እና አስፈላጊ ከሆነ ያጠፋቸዋል. የዚህን ተግባር አተገባበር በኋላ እንመለከታለን. ለአሁኑ ይህን ማወቅ በቂያችን ነው።
bullet.update()
ይመለሳልtrue
ፕሮጀክቱ መጥፋት ካለበት (ከመድረኩ ወጣ)። - እያንዳንዱን ተጫዋች ያዘምናል እና አስፈላጊ ከሆነ ፕሮጄክትን ያበቅላል። ይህንን ትግበራ በኋላ እንመለከታለን -
player.update()
ዕቃ መመለስ ይችላል።Bullet
. - በፕሮጀክቶች እና በተጫዋቾች መካከል ግጭቶችን ይፈትሻል
applyCollisions()
, ይህም በተጫዋቾች ላይ የሚደርሱ የፕሮጀክቶች ስብስብ ይመልሳል. ለእያንዳንዱ የተመለሰ ፕሮጀክት፣ የተኮሰውን ተጫዋች ነጥቦችን እንጨምራለን (በመጠቀምplayer.onDealtDamage()
) እና ከዚያም ፕሮጄክቱን ከድርድሩ ላይ ያስወግዱትbullets
. - ሁሉንም የተገደሉ ተጫዋቾች ያሳውቃል እና ያጠፋል።
- የጨዋታ ዝማኔ ለሁሉም ተጫዋቾች ይልካል በየሰከንዱ በሚጠሩበት ጊዜ ጊዜያት
update()
. ይህ ከላይ የተጠቀሰውን ረዳት ተለዋዋጭ ለመከታተል ይረዳናል.shouldSendUpdate
. ምክንያቱምupdate()
60 ጊዜ / ሰ ይባላል ፣ የጨዋታ ዝመናዎችን 30 ጊዜ / ሰ እንልካለን። ስለዚህም የሰዓት ድግግሞሽ የአገልጋይ ሰዓት 30 ሰዓት / ሰ ነው (በመጀመሪያው ክፍል ውስጥ ስለ የሰዓት ዋጋዎች ተነጋገርን).
ለምን የጨዋታ ዝመናዎችን ብቻ ይላኩ በጊዜ በኩል ? ቻናል ለማስቀመጥ። በሰከንድ 30 የጨዋታ ዝመናዎች ብዙ ናቸው!
ለምን ዝም ብለህ አትደውል።
update()
በሰከንድ 30 ጊዜ? የጨዋታውን ማስመሰል ለማሻሻል። ብዙ ጊዜ የሚጠራው።update()
፣ የጨዋታው ማስመሰል የበለጠ ትክክለኛ ይሆናል። ነገር ግን በፈተናዎች ብዛት ብዙ አትወሰዱ።update()
, ምክንያቱም ይህ ስሌት ውድ የሆነ ስራ ነው - 60 በሰከንድ በቂ ነው.
የቀረው ክፍል Game
ውስጥ ጥቅም ላይ የዋሉ የረዳት ዘዴዎችን ያካትታል update()
:
game.js ክፍል 3
class Game {
// ...
getLeaderboard() {
return Object.values(this.players)
.sort((p1, p2) => p2.score - p1.score)
.slice(0, 5)
.map(p => ({ username: p.username, score: Math.round(p.score) }));
}
createUpdate(player, leaderboard) {
const nearbyPlayers = Object.values(this.players).filter(
p => p !== player && p.distanceTo(player) <= Constants.MAP_SIZE / 2,
);
const nearbyBullets = this.bullets.filter(
b => b.distanceTo(player) <= Constants.MAP_SIZE / 2,
);
return {
t: Date.now(),
me: player.serializeForUpdate(),
others: nearbyPlayers.map(p => p.serializeForUpdate()),
bullets: nearbyBullets.map(b => b.serializeForUpdate()),
leaderboard,
};
}
}
getLeaderboard()
በጣም ቀላል - ተጫዋቾቹን በውጤት ይመድባል፣ አምስቱን ይወስዳል እና የተጠቃሚ ስም እና ለእያንዳንዱ ነጥብ ይመልሳል።
createUpdate()
ውስጥ ጥቅም ላይ ውሏል update()
ለተጫዋቾች የሚከፋፈሉ የጨዋታ ዝመናዎችን ለመፍጠር። ዋናው ሥራው ዘዴዎችን መጥራት ነው serializeForUpdate()
ለክፍሎች የተተገበረ Player
и Bullet
. ለእያንዳንዱ ተጫዋች መረጃን ብቻ እንደሚያስተላልፍ ልብ ይበሉ ቅርብ ተጫዋቾች እና ፕሮጄክቶች - ከተጫዋቹ ርቀው ስለሚገኙ የጨዋታ ዕቃዎች መረጃ ማስተላለፍ አያስፈልግም!
3. በአገልጋዩ ላይ የጨዋታ እቃዎች
በእኛ ጨዋታ ውስጥ ፕሮጄክተሮች እና ተጫዋቾች በጣም ተመሳሳይ ናቸው፡ እነሱ ረቂቅ፣ ክብ፣ ተንቀሳቃሽ የጨዋታ እቃዎች ናቸው። በተጫዋቾች እና በፕሮጀክቶች መካከል ያለውን ተመሳሳይነት ለመጠቀም የመሠረት ክፍሉን በመተግበር እንጀምር Object
:
ነገር.js
class Object {
constructor(id, x, y, dir, speed) {
this.id = id;
this.x = x;
this.y = y;
this.direction = dir;
this.speed = speed;
}
update(dt) {
this.x += dt * this.speed * Math.sin(this.direction);
this.y -= dt * this.speed * Math.cos(this.direction);
}
distanceTo(object) {
const dx = this.x - object.x;
const dy = this.y - object.y;
return Math.sqrt(dx * dx + dy * dy);
}
setDirection(dir) {
this.direction = dir;
}
serializeForUpdate() {
return {
id: this.id,
x: this.x,
y: this.y,
};
}
}
እዚህ ምንም የተወሳሰበ ነገር የለም. ይህ ክፍል ለቅጥያው ጥሩ መልህቅ ይሆናል. ክፍሉ እንዴት እንደሆነ እንይ Bullet
ይጠቀማል Object
:
ጥይት.js
const shortid = require('shortid');
const ObjectClass = require('./object');
const Constants = require('../shared/constants');
class Bullet extends ObjectClass {
constructor(parentID, x, y, dir) {
super(shortid(), x, y, dir, Constants.BULLET_SPEED);
this.parentID = parentID;
}
// Returns true if the bullet should be destroyed
update(dt) {
super.update(dt);
return this.x < 0 || this.x > Constants.MAP_SIZE || this.y < 0 || this.y > Constants.MAP_SIZE;
}
}
ትግበራ Bullet
በጣም አጭር! ጨምረናል። Object
የሚከተሉት ቅጥያዎች ብቻ:
- ጥቅሉን በመጠቀም
shortid ለአጋጣሚ ትውልድid
ፕሮጄክት. - መስክ መጨመር
parentID
ይህንን ፕሮጀክት የፈጠረውን ተጫዋች መከታተል እንድትችሉ። - የመመለሻ እሴት በማከል ላይ
update()
, ይህም ጋር እኩል ነውtrue
ፕሮጀክቱ ከመድረኩ ውጭ ከሆነ (በመጨረሻው ክፍል ላይ ስለዚህ ጉዳይ እንደተነጋገርን ያስታውሱ?)
ወደዚህ እንሂድ Player
:
ተጫዋች.js
const ObjectClass = require('./object');
const Bullet = require('./bullet');
const Constants = require('../shared/constants');
class Player extends ObjectClass {
constructor(id, username, x, y) {
super(id, x, y, Math.random() * 2 * Math.PI, Constants.PLAYER_SPEED);
this.username = username;
this.hp = Constants.PLAYER_MAX_HP;
this.fireCooldown = 0;
this.score = 0;
}
// Returns a newly created bullet, or null.
update(dt) {
super.update(dt);
// Update score
this.score += dt * Constants.SCORE_PER_SECOND;
// Make sure the player stays in bounds
this.x = Math.max(0, Math.min(Constants.MAP_SIZE, this.x));
this.y = Math.max(0, Math.min(Constants.MAP_SIZE, this.y));
// Fire a bullet, if needed
this.fireCooldown -= dt;
if (this.fireCooldown <= 0) {
this.fireCooldown += Constants.PLAYER_FIRE_COOLDOWN;
return new Bullet(this.id, this.x, this.y, this.direction);
}
return null;
}
takeBulletDamage() {
this.hp -= Constants.BULLET_DAMAGE;
}
onDealtDamage() {
this.score += Constants.SCORE_BULLET_HIT;
}
serializeForUpdate() {
return {
...(super.serializeForUpdate()),
direction: this.direction,
hp: this.hp,
};
}
}
ተጫዋቾች ከፕሮጀክቶች የበለጠ ውስብስብ ናቸው, ስለዚህ ጥቂት ተጨማሪ መስኮች በዚህ ክፍል ውስጥ መቀመጥ አለባቸው. የእሱ ዘዴ update()
ብዙ ስራዎችን ይሰራል, በተለይም, ምንም ካልቀሩ አዲስ የተፈጠረውን ፕሮጀክት ይመልሳል fireCooldown
(በቀደመው ክፍል ስለዚህ ጉዳይ እንደተነጋገርን አስታውስ?) እንዲሁም ዘዴውን ያራዝመዋል serializeForUpdate()
, ምክንያቱም በጨዋታ ዝማኔ ውስጥ ለተጫዋቹ ተጨማሪ መስኮችን ማካተት አለብን.
የመሠረት ክፍል መኖር Object
- ኮድን መድገም ለማስወገድ አስፈላጊ እርምጃ. ለምሳሌ, ክፍል የለም Object
እያንዳንዱ የጨዋታ ነገር አንድ አይነት አተገባበር ሊኖረው ይገባል distanceTo()
, እና እነዚህን ሁሉ ትግበራዎች በበርካታ ፋይሎች ላይ መቅዳት ቅዠት ይሆናል. ይህ በተለይ ለትላልቅ ፕሮጀክቶች አስፈላጊ ይሆናል.የሚሰፋው ቁጥር ሲጨምር Object
ክፍሎች እያደጉ ናቸው.
4. ግጭትን መለየት
ለእኛ የቀረን ብቸኛው ነገር ፕሮጀክቱ ተጫዋቾቹን ሲመቱ መለየት ነው! ይህንን ኮድ ከስልቱ ውስጥ ያስታውሱ update()
በክፍል ውስጥ Game
:
ጨዋታ.js
const applyCollisions = require('./collisions');
class Game {
// ...
update() {
// ...
// Apply collisions, give players score for hitting bullets
const destroyedBullets = applyCollisions(
Object.values(this.players),
this.bullets,
);
destroyedBullets.forEach(b => {
if (this.players[b.parentID]) {
this.players[b.parentID].onDealtDamage();
}
});
this.bullets = this.bullets.filter(
bullet => !destroyedBullets.includes(bullet),
);
// ...
}
}
ዘዴውን ተግባራዊ ማድረግ አለብን applyCollisions()
, ይህም ተጫዋቾችን የሚመቱትን ሁሉንም ፕሮጄክቶች ይመልሳል. እንደ እድል ሆኖ, ይህን ማድረግ ከባድ አይደለም ምክንያቱም
- ሁሉም የሚጋጩ ነገሮች ክበቦች ናቸው, ይህም የግጭት ማወቂያን ለመተግበር ቀላሉ ቅርጽ ነው.
- አስቀድመን ዘዴ አለን
distanceTo()
, በክፍል ውስጥ ባለፈው ክፍል ውስጥ ተግባራዊ ያደረግነውObject
.
የእኛ የግጭት ማወቂያ አተገባበር ምን ይመስላል።
ግጭቶች.js
const Constants = require('../shared/constants');
// Returns an array of bullets to be destroyed.
function applyCollisions(players, bullets) {
const destroyedBullets = [];
for (let i = 0; i < bullets.length; i++) {
// Look for a player (who didn't create the bullet) to collide each bullet with.
// As soon as we find one, break out of the loop to prevent double counting a bullet.
for (let j = 0; j < players.length; j++) {
const bullet = bullets[i];
const player = players[j];
if (
bullet.parentID !== player.id &&
player.distanceTo(bullet) <= Constants.PLAYER_RADIUS + Constants.BULLET_RADIUS
) {
destroyedBullets.push(bullet);
player.takeBulletDamage();
break;
}
}
}
return destroyedBullets;
}
ይህ ቀላል የግጭት ግኝት በእውነታው ላይ የተመሰረተ ነው በማዕከሎቻቸው መካከል ያለው ርቀት ከ ራዲዮቻቸው ድምር ያነሰ ከሆነ ሁለት ክበቦች ይጋጫሉ. በሁለት ክበቦች ማዕከሎች መካከል ያለው ርቀት በትክክል ከራዲያቸው ድምር ጋር እኩል የሆነበት ሁኔታ እዚህ አለ።
እዚህ ሊታሰብባቸው የሚገቡ ሁለት ተጨማሪ ገጽታዎች አሉ-
- ፕሮጀክቱ የፈጠረውን ተጫዋች መምታት የለበትም። ይህ በማነፃፀር ሊገኝ ይችላል
bullet.parentID
сplayer.id
. - ብዙ ተጫዋቾች በተመሳሳይ ጊዜ ሲጋጩ ፕሮጀክቱ አንድ ጊዜ ብቻ መምታት አለበት። ኦፕሬተሩን በመጠቀም ይህንን ችግር እንፈታዋለን
break
: ከፕሮጀክቱ ጋር የሚጋጭ ተጫዋቹ እንደተገኘ ፍለጋውን አቁመን ወደሚቀጥለው ፕሮጀክት እንቀጥላለን።
መጨረሻው
ይኼው ነው! የ.io ድር ጨዋታ ለመፍጠር ማወቅ ያለብዎትን ሁሉንም ነገር ሸፍነናል። ቀጥሎ ምን አለ? የራስዎን የ.io ጨዋታ ይገንቡ!