تم إصدار
Agar.io في عام 2015 ، وأصبح
منشئ هذا النوع الجديد من
الألعاب .io ، التي نمت شعبيتها بشكل كبير منذ ذلك الحين. نمو شعبية ألعاب .io التي جربتها على نفسي: على مدى السنوات الثلاث الماضية ، قمت
بإنشاء وبيع مباراتين من هذا النوع. .
في حالة عدم سماعك لمثل هذه الألعاب من قبل: فهذه ألعاب مجانية متعددة اللاعبين على الويب يسهل مشاركتها فيها (لا يلزم حساب). عادة ما يدفعون العديد من اللاعبين المعارضين في نفس الساحة. الألعاب الشهيرة الأخرى من النوع .io هي:
Slither.io و
Diep.io.في هذا
المنشور ، سنكتشف كيفية
إنشاء لعبة .io من البداية . لهذا ، ستكون معرفة Javascript فقط كافية: تحتاج إلى فهم أشياء مثل بناء جملة
ES6 ،
this
والوعود . حتى لو كنت لا تعرف Javascript تمامًا ، فلا يزال بإمكانك معرفة معظم النشر.
مثال لعبة
لمساعدتك على التعلم ، سوف نشير إلى
مثال .io game . حاول تشغيلها!
اللعبة بسيطة للغاية: يمكنك التحكم في سفينة في ساحة يوجد بها لاعبون آخرون. تقوم سفينتك بإطلاق قذائف تلقائيًا وتحاول ضرب لاعبين آخرين ، مع تجنب قذائفهم.
1. نظرة عامة / هيكل المشروع
أوصي بتنزيل الكود المصدري للعبة سبيل المثال حتى تتمكن من متابعتي.
يستخدم المثال ما يلي:
- Express هو أكثر أطر الويب شعبية لـ Node.js التي تدير خادم الويب الخاص باللعبة.
- socket.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/
folder. اسماء
client/
و
server/
يتحدثان عنهما ، و
shared/
يحتوي على ملف ثابت مستورد من قبل كل من العميل و الخادم.
2. الجمعيات / معلمات المشروع
كما هو مذكور أعلاه ، نستخدم مدير وحدة
Webpack لبناء المشروع. دعونا نلقي نظرة على تكوين Webpack لدينا:
webpack.common.js:
const path = require('path'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = { entry: { game: './src/client/index.js', }, output: { filename: '[name].[contenthash].js', path: path.resolve(__dirname, 'dist'), }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: "babel-loader", options: { presets: ['@babel/preset-env'], }, }, }, { test: /\.css$/, use: [ { loader: MiniCssExtractPlugin.loader, }, 'css-loader', ], }, ], }, plugins: [ new MiniCssExtractPlugin({ filename: '[name].[contenthash].css', }), new HtmlWebpackPlugin({ filename: 'index.html', template: 'src/client/html/index.html', }), ], };
الأهم هنا هي السطور التالية:
src/client/index.js
هي نقطة الإدخال لعميل Javascript (JS). سيبدأ Webpack من هنا والبحث بشكل متكرر عن الملفات المستوردة الأخرى.- سيتم وضع الإخراج JS الخاص بتجميع Webpack الخاص بنا في
dist/
directory. سأتصل بهذا الملف حزمة JS الخاصة بنا. - نستخدم Babel ، وبصفة خاصة تكوين @ babel / preset-env لنقل رمز JS الخاص بنا للمتصفحات القديمة.
- نحن نستخدم البرنامج المساعد لاستخراج جميع CSS المشار إليها بواسطة ملفات JS ولجمعها في مكان واحد. سأسميها حزمة CSS الخاصة بنا.
ربما لاحظت أسماء ملفات حزمة غريبة
'[name].[contenthash].ext'
. أنها تحتوي على
استبدال أسماء ملفات Webpack: سيتم استبدال
[name]
باسم نقطة الإدخال (في حالتنا ، هذه
game
) ، وسيتم استبدال
[contenthash]
لمحتويات الملف. إننا نقوم بذلك من أجل
تحسين المشروع للتجزئة - يمكنك إخبار المتصفحات بتخزين مؤقت لحزم JS الخاصة بنا إلى أجل غير مسمى ، لأنه
إذا تغيرت الحزمة ، فسيتم تغيير
اسم الملف الخاص بها (يتغير
contenthash
). ستكون النتيجة النهائية اسم ملف للنموذج
game.dbeee76e91a97d0c7207.js
.
ملف
webpack.common.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.dev.js
التطوير ،
webpack.prod.js
إلى
webpack.prod.js
لتحسين أحجام الحزمة عند نشرها في الإنتاج.
الإعداد المحلي
أوصي بتثبيت المشروع على جهاز محلي حتى تتمكن من اتباع الخطوات المذكورة في هذا المنشور. الإعداد بسيط: أولاً ، يجب تثبيت
Node و
NPM على النظام. القادمة تحتاج إلى أداء
$ git clone https:
وأنت مستعد للذهاب! لبدء خادم التطوير ، فقط قم بتشغيل
$ npm run develop
والوصول إلى
المضيف المحلي: 3000 في متصفح الويب. سيعمل خادم التطوير تلقائيًا على إعادة إنشاء حزم JS و CSS مع تغيير الرمز - ما عليك سوى تحديث الصفحة لمشاهدة جميع التغييرات!
3. نقاط دخول العملاء
دعنا ننكب على رمز اللعبة نفسه. أولاً ، نحتاج إلى صفحة
index.html
، عندما تزور الموقع ، سيقوم المتصفح بتحميله أولاً. ستكون صفحتنا بسيطة للغاية:
index.html و
<! DOCTYPE html>
<HTML>
<رئيس>
<title> مثال على لعبة .io </title>
<link type = "text / css" rel = "stylesheet" href = "/ game.bundle.css">
</ رئيس>
<body>
<canvas id = "game-canvas"> </canvas>
<script async src = "/ game.bundle.js"> </script>
<div id = "play-menu" class = "hidden">
<input type = "text" id = "username-input" placeholder = "Username" />
<button id = "play-button"> PLAY </button>
</ div>
</ الهيئة>
</ HTML>
تم تبسيط مثال التعليمات البرمجية هذا قليلاً من أجل الوضوح ، سأفعل نفس الشيء مع العديد من الأمثلة الأخرى للنشر. يمكن دائمًا عرض الرمز الكامل على جيثب .لدينا:
<canvas>
HTML5 Canvas ( <canvas>
) الذي سنستخدمه لتقديم اللعبة.<link>
لإضافة حزمة CSS الخاصة بنا.<script>
لإضافة حزمة Javascript الخاصة بنا.- القائمة الرئيسية مع اسم المستخدم
<input>
<button>
"اللعب" ( <button>
).
بعد تحميل الصفحة الرئيسية ، سيبدأ تنفيذ شفرة Javascript في المتصفح ، بدءًا من ملف JS لنقطة الدخول:
src/client/index.js
index.js
import { connect, play } from './networking'; import { startRendering, stopRendering } from './render'; import { startCapturingInput, stopCapturingInput } from './input'; import { downloadAssets } from './assets'; import { initState } from './state'; import { setLeaderboardHidden } from './leaderboard'; import './css/main.css'; const playMenu = document.getElementById('play-menu'); const playButton = document.getElementById('play-button'); const usernameInput = document.getElementById('username-input'); Promise.all([ connect(), downloadAssets(), ]).then(() => { playMenu.classList.remove('hidden'); usernameInput.focus(); playButton.onclick = () => {
قد يبدو هذا الأمر معقدًا ، ولكن في الواقع لا توجد أعمال كثيرة تحدث هنا:
- استيراد عدة ملفات JS أخرى.
- قم باستيراد CSS (حتى يعرف Webpack تضمينها في حزمة CSS الخاصة بنا).
- تشغيل
connect()
لتأسيس اتصال بالخادم وتشغيل downloadAssets()
لتنزيل الصور اللازمة لتقديم اللعبة. - بعد الانتهاء من الخطوة 3 ، يتم
playMenu
القائمة الرئيسية ( playMenu
). - تكوين المعالج للضغط على زر اللعب. عند الضغط على الزر ، يقوم الكود بتهيئة اللعبة ويخبر الخادم بأننا مستعدون للعب.
إن "اللحوم" الرئيسية لمنطق خادم العميل لدينا هي تلك الملفات التي تم استيرادها بواسطة ملف
index.js
. الآن سننظر فيها جميعًا بالترتيب.
4. تبادل بيانات العملاء
في هذه اللعبة ، نستخدم مكتبة
socket.io المعروفة للتواصل مع الخادم. يوفر Socket.io دعمًا
مدمجًا لـ
WebSockets ، وهو مناسب تمامًا للاتصال ثنائي الاتجاه: يمكننا إرسال رسائل إلى الخادم ويمكن للخادم إرسال رسائل إلينا عبر نفس الاتصال.
سيكون لدينا ملف
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(() => {
هذا الرمز هو أيضا انخفاض طفيف للوضوح.هناك ثلاثة إجراءات رئيسية في هذا الملف:
- نحن نحاول الاتصال بالخادم.
connectedPromise
يُسمح به فقط عندما ننشئ اتصالًا. - إذا تم إنشاء الاتصال بنجاح ، فإننا نقوم بتسجيل وظائف رد الاتصال (
processGameUpdate()
و onGameOver()
) للرسائل التي يمكن أن نتلقاها من الخادم. - نحن نصدر
play()
و updateDirection()
حتى تتمكن الملفات الأخرى من استخدامها.
5. تقديم العملاء
لقد حان الوقت لعرض صورة على الشاشة!
... ولكن قبل أن نتمكن من القيام بذلك ، نحتاج إلى تنزيل جميع الصور (الموارد) اللازمة لذلك. دعنا نكتب مدير الموارد:
assets.js
const ASSET_NAMES = ['ship.svg', 'bullet.svg']; const assets = {}; const downloadPromise = Promise.all(ASSET_NAMES.map(downloadAsset)); function downloadAsset(assetName) { return new Promise(resolve => { const asset = new Image(); asset.onload = () => { console.log(`Downloaded ${assetName}`); assets[assetName] = asset; resolve(); }; asset.src = `/assets/${assetName}`; }); } export const downloadAssets = () => downloadPromise; export const getAsset = assetName => assets[assetName];
إدارة الموارد ليست صعبة للغاية لتنفيذ! النقطة الأساسية هي تخزين كائن
assets
، والذي سيربط مفتاح اسم الملف بقيمة كائن
Image
. عند تحميل المورد ، نقوم بحفظه في كائن
assets
لاسترجاعه سريعًا في المستقبل. عندما يُسمح بالتنزيل لكل مورد فردي (أي ، سيتم تنزيل
جميع الموارد) ، فإننا نقوم بتمكين
downloadPromise
.
بعد تنزيل الموارد ، يمكنك البدء في العرض. كما ذكرنا سابقًا ، نستخدم
HTML5 Canvas (
<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;
يتم تقصير هذا الرمز أيضًا من أجل الوضوح.render()
هو الوظيفة الرئيسية لهذا الملف.
startRendering()
و
stopRendering()
في تنشيط دورة العرض عند 60 إطارًا في الثانية.
لا تعتبر التطبيقات المحددة لوظائف التقديم المساعدة الفردية (مثل
renderBullet()
) مهمة بهذا القدر ، ولكن هذا مثال بسيط واحد:
render.js
function renderBullet(me, bullet) { const { x, y } = bullet; context.drawImage( getAsset('bullet.svg'), canvas.width / 2 + x - me.x - BULLET_RADIUS, canvas.height / 2 + y - me.y - BULLET_RADIUS, BULLET_RADIUS * 2, BULLET_RADIUS * 2, ); }
لاحظ أننا نستخدم طريقة
getAsset()
التي شوهدت سابقًا في
asset.js
!
إذا كنت مهتمًا باستكشاف وظائف التقديم الإضافية ، فاقرأ بقية src / client / render.js .
6. إدخال العميل
حان الوقت لجعل اللعبة
قابلة للعب ! سيكون نظام التحكم بسيطًا للغاية: يمكنك استخدام الماوس (على الكمبيوتر) أو لمس الشاشة (على الجهاز المحمول) لتغيير اتجاه الحركة. لتنفيذ ذلك ، سنقوم بتسجيل
مستمعي الأحداث لأحداث الماوس واللمس.
سيقوم
src/client/input.js
بعمل كل هذا:
input.js
import { updateDirection } from './networking'; function onMouseInput(e) { handleInput(e.clientX, e.clientY); } function onTouchInput(e) { const touch = e.touches[0]; handleInput(touch.clientX, touch.clientY); } function handleInput(x, y) { const dir = Math.atan2(x - window.innerWidth / 2, window.innerHeight / 2 - y); updateDirection(dir); } export function startCapturingInput() { window.addEventListener('mousemove', onMouseInput); window.addEventListener('touchmove', onTouchInput); } export function stopCapturingInput() { window.removeEventListener('mousemove', onMouseInput); window.removeEventListener('touchmove', onTouchInput); }
onMouseInput()
و
onTouchInput()
مستمعي الأحداث الذين
updateDirection()
(من
updateDirection()
) عند حدوث حدث إدخال (على سبيل المثال ، عند تحريك الماوس).
updateDirection()
في المراسلة مع خادم يقوم بمعالجة حدث الإدخال ويقوم بتحديث حالة اللعبة وفقًا لذلك.
7. حالة العملاء
هذا القسم هو الأصعب في الجزء الأول من المنشور. لا تحبط إذا لم تفهمها من القراءة الأولى! يمكنك حتى تخطيها والعودة إليها في وقت لاحق.
الحالة الأخيرة من اللغز المطلوب لإكمال كود خادم العميل هي
الحالة . هل تتذكر مقتطف الشفرة من قسم تقديم العميل؟
render.js
import { getCurrentState } from './state'; function render() { const { me, others, bullets } = getCurrentState();
يجب أن يكون
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 : الطابع الزمني للخادم للوقت الذي تم فيه إنشاء هذا التحديث.
- لي : معلومات حول اللاعب الذي يتلقى هذا التحديث.
- الآخرين : مجموعة من المعلومات حول اللاعبين الآخرين المشاركين في نفس اللعبة.
- الرصاص : مجموعة من المعلومات حول الأصداف الموجودة في اللعبة.
- المتصدرين : بيانات المتصدرين الحالية. في هذا المنشور ، لن نأخذها في الاعتبار.
7.1 الحالة الساذجة للعميل
يمكن للتطبيق الساذج لـ
getCurrentState()
أن يعرض البيانات مباشرة من آخر تحديث للعبة تم استلامه.
state.js-ساذج
let lastGameUpdate = null;
جميل وواضح! ولكن إذا كان كل شيء في غاية البساطة. أحد الأسباب التي تجعل هذا التطبيق إشكاليًا:
فهو يحد من معدل إطار العرض على تردد ساعة الخادم .
معدل الإطارات : عدد الإطارات (مثل render()
المكالمات) في الثانية ، أو FPS. تميل الألعاب عادةً إلى الوصول إلى 60 إطارًا في الثانية على الأقل.
معدل التجزئة : هو التردد الذي يرسل فيه الخادم تحديثات اللعبة للعملاء. غالبا ما يكون أقل من معدل الإطار . في لعبتنا ، يعمل الخادم بتردد 30 دورة في الثانية.
إذا قمنا للتو بتقديم آخر تحديث للعبة ، فلن يتمكن FPS في الواقع من تجاوز 30 ، لأننا
لن نتلقى أكثر من 30 تحديثًا في الثانية من الخادم . حتى لو قمنا بالاتصال
render()
60 مرة في الثانية ، فإن نصف هذه المكالمات سيعيد رسم نفس الشيء ببساطة ، ولا يفعل شيئًا بشكل أساسي. مشكلة أخرى للتنفيذ الساذج هو أنه
عرضة للتأخير . مع سرعة إنترنت مثالية ، سيتلقى العميل تحديث لعبة كل 33 مللي ثانية بالضبط (30 في الثانية):
لسوء الحظ ، لا يوجد شيء مثالي. ستكون الصورة الأكثر واقعية هي:
يعتبر التنفيذ الساذج هو أسوأ الحالات عندما يتعلق الأمر بالتأخير. إذا تم استلام تحديث اللعبة مع تأخير قدره 50 مللي ثانية ،
فسيتباطأ العميل لمدة 50 ثانية إضافية ، لأنه لا يزال يعرض حالة اللعبة من التحديث السابق. يمكنك أن تتخيل مدى عدم ملاءمته للاعب: نظرًا للفرملة التعسفية ، ستبدو اللعبة غائرة وغير مستقرة.
7.2 تحسين حالة العملاء
سنقوم بإجراء بعض التحسينات على التنفيذ الساذج. أولاً ، نستخدم
تأخير تقديم قدره 100 مللي ثانية. هذا يعني أن الحالة "الحالية" للعميل ستتخلف دائمًا عن حالة اللعبة على الخادم بمقدار 100 مللي ثانية. على سبيل المثال ، إذا كان الوقت على الخادم هو
150 ، فسيتم عرض الحالة التي كان الخادم فيها في الوقت
50 على العميل:
هذا يعطينا مؤقتًا قدره 100 مللي ثانية ، مما يسمح لنا بالبقاء على قيد الحياة في الوقت غير المتوقع لتلقي تحديثات اللعبة:
الدفع مقابل هذا سيكون
تأخير إدخال مستمر
(تأخر الإدخال) قدره 100 مللي ثانية. هذه تضحية بسيطة من أجل اللعب السلس - معظم اللاعبين (خاصة غير العاديين) لن يلاحظوا هذا التأخير. من الأسهل بالنسبة للناس التكيف مع تأخير ثابت قدره 100 مللي ثانية بدلاً من التأخير غير المتوقع.
يمكننا استخدام تقنية أخرى تسمى "التنبؤ من جانب العميل" ، والتي تقوم بعمل جيد في تقليل التأخيرات المتوقعة ، لكن لن يتم أخذها في الاعتبار في هذا المنشور.
تحسين آخر نستخدمه هو
الاستيفاء الخطي . نظرًا للتأخير في التقديم ، عادة ما نتجاوز الوقت الحالي في العميل عن طريق تحديث واحد على الأقل. عند
getCurrentState()
، يمكننا إجراء
الاستيفاء الخطي بين تحديثات اللعبة فورًا وبعد الوقت الحالي في العميل:
هذا يحل المشكلة مع معدل الإطار: الآن يمكننا تقديم إطارات فريدة مع أي تردد نحتاجه!
7.3 تطبيق حالة العميل المحسنة
يستخدم تطبيق مثال في
src/client/state.js
كلاً من تأخير التقديم والاستيفاء الخطي ، لكن هذا ليس طويلاً. دعنا نقسم الرمز إلى قسمين. هنا أول واحد:
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);
الخطوة الأولى هي معرفة ما يفعله
currentServerTime()
. كما رأينا سابقًا ، يتم تضمين الطابع الزمني للخادم في كل تحديث للألعاب. نريد استخدام تأخير العرض لتقديم الصورة 100 مللي ثانية خلف الخادم ،
لكننا لن نعرف الوقت الحالي على الخادم ، لأنه لا يمكننا معرفة المدة التي وصلنا بها أي من التحديثات. الانترنت لا يمكن التنبؤ به وسرعته يمكن أن تختلف اختلافا كبيرا!
للتغلب على هذه المشكلة ، يمكنك استخدام تقريب معقول: سوف
ندعي أن التحديث الأول وصل على الفور . إذا كان هذا صحيحًا ، فسنعرف وقت الخادم في هذه اللحظة بالذات! نقوم بحفظ الطابع الزمني للخادم في
firstServerTimestamp
وحفظ الطابع الزمني
المحلي (العميل) الخاص بنا في نفس الوقت في
gameStart
.
أوه انتظر.
لا ينبغي أن يكون هناك وقت على الخادم = الوقت في العميل؟ لماذا نميز بين "الطابع الزمني للخادم" و "الطابع الزمني للعميل"؟ هذا سؤال عظيم! اتضح أن هذا ليس هو الشيء نفسه.
Date.now()
الطوابع الزمنية المختلفة في العميل والخادم ، وهذا يعتمد على العوامل المحلية لهذه الأجهزة.
لا تفترض أبدًا أن الطوابع الزمنية هي نفسها على جميع الآلات.الآن نحن نفهم ما يفعله
currentServerTime()
: فهو يعرض
الطابع الزمني للخادم لوقت العرض الحالي .
بمعنى آخر ، هذا هو وقت الخادم الحالي ( firstServerTimestamp <+ (Date.now() - gameStart)
) مطروحًا منه تأخر العرض ( RENDER_DELAY
).الآن دعونا نرى كيف نتعامل مع تحديثات اللعبة. عند تلقي تحديث من الخادم ، يتم استدعاؤه processGameUpdate()
، ونحفظ التحديث الجديد في صفيف gameUpdates
. بعد ذلك ، للتحقق من استخدام الذاكرة ، نقوم بحذف جميع التحديثات القديمة إلى التحديث الأساسي ، لأننا لم نعد بحاجة إليها.ما هو "التحديث الأساسي"؟ هذا هو التحديث الأول الذي نجده يتحرك للخلف من وقت الخادم الحالي . تذكر هذه الدائرة؟«Client Render Time» .
? ? ,
- getCurrentState()
:
state.js, 2
export function getCurrentState() { if (!firstServerTimestamp) { return {}; } const base = getBaseUpdate(); const serverTime = currentServerTime();
:
base < 0
, (. getBaseUpdate()
). - . .base
— , . - . , .- , , !
,
state.js
— , ( ) . ,
state.js
Github .
2. -
Node.js,
.io .
1.
- - Node.js
Express .
src/server/server.js
:
server.js, 1
const express = require('express'); const webpack = require('webpack'); const webpackDevMiddleware = require('webpack-dev-middleware'); const webpackConfig = require('../../webpack.dev.js');
, Webpack? Webpack. :
server.js
socket.io , Express:
server.js, 2
const socketio = require('socket.io'); const Constants = require('../shared/constants');
socket.io . -
game
:
server.js, 3
const Game = require('./game');
.io,
Game
(«Game») – ! ,
Game
.
2. Game
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;
id
socket.io ( ,
server.js
). Socket.io
id
, .
ID .
,
Game
:
sockets
— , ID , . ID .players
— , ID code>Player
bullets
هي مجموعة من الكائنات Bullet
التي ليس لها ترتيب محدد.lastUpdateTime
- هذا هو الطابع الزمني لآخر مرة تم فيها تحديث اللعبة. قريبا سنرى كيف يتم استخدامه.shouldSendUpdate
هو متغير مساعد. سوف نرى استخدامه قريبا جدا.الأساليب addPlayer()
، removePlayer()
وليس handleInput()
هناك حاجة لشرح ، يتم استخدامها في server.js
. إذا كنت بحاجة إلى تحديث ذاكرتك ، فارجع إلى أعلى قليلاً. يبدأالسطر الأخير في دورة تحديث اللعبة (بتردد 60 تحديث / تحديثات):constructor()
game.js ، الجزء 2
const Constants = require('../shared/constants'); const applyCollisions = require('./collisions'); class Game {
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()
– , .
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; }
Bullet
!
Object
:
- shortid
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; }
, , .
update()
, , ,
fireCooldown
(, ?).
serializeForUpdate()
, .
Object
— , . ,
Object
distanceTo()
, .
,
Object
.
4.
, – , !
update()
Game
:
game.js
const applyCollisions = require('./collisions'); class Game {
applyCollisions()
, , . , ,
- , .
distanceTo()
, Object
.
:
collisions.js
const Constants = require('../shared/constants');
,
, . , :
:
- . ,
bullet.parentID
player.id
. - .
break
: , , .
! , - .io. ما التالي؟
بناء لعبة .io الخاصة بك!جميع رمز عينة مفتوح المصدر ونشرها على جيثب .