اليوم ، عندما تكون Node.js على قدم وساق ، يمكننا بالفعل استخدامها للقيام بجميع أنواع الأشياء. منذ بعض الوقت ، شارك مضيف UP في حدث Geek Song. خلال هذا الحدث ، نعتزم إنشاء لعبة تتيح لـ "الأشخاص المتجولين" التواصل أكثر. الوظيفة الأساسية هي التفاعل متعدد الأشخاص في الوقت الفعلي لمفهوم حزب الشبكة المحلية. لم يكن سباق المهوسون سوى 36 ساعة قصيرة ، مما يتطلب كل شيء سريعًا وسريعًا. في ظل هذه الفرضية ، يبدو الإعداد الأولي "طبيعيًا" قليلاً. حل التطبيق عبر المنصات ، اخترنا عقدة webkit ، وهو أمر بسيط بما فيه الكفاية ويفي بمتطلباتنا.
وفقًا للمتطلبات ، يمكن تنفيذ تطورنا بشكل منفصل وفقًا للوحدات النمطية. تصف هذه المقالة على وجه التحديد عملية تطوير فطائر (إطار لعبة متعددة اللاعبين في الوقت الفعلي) ، بما في ذلك سلسلة من الاستكشافات والمحاولات ، وكذلك حلول لبعض القيود على node.js ومنصة WebKit نفسها ، واقتراح الحلول.
ابدء
نظرة فاحشة
في البداية ، كان تصميم Spaceroom مدفوعًا بالتأكيد. نأمل أن يتمكن هذا الإطار من توفير الوظائف الأساسية التالية:
يمكن تمييز مجموعة من المستخدمين بناءً على غرف (أو قنوات)
قادر على تلقي إرشادات من المستخدمين في مجموعة التجميع
عند المطابقة بين كل عميل ، يمكن بث بيانات اللعبة بدقة وفقًا للفاصل الزمني المحدد
يمكن القضاء على تأثير زمن انتقال الشبكة قدر الإمكان
بطبيعة الحال ، في المراحل اللاحقة من الترميز ، قمنا بتزويد فاصل بمزيد من الوظائف ، بما في ذلك إيقاف اللعبة ، وتوليد أرقام عشوائية متسقة بين كل عميل ، وما إلى ذلك (بالطبع ، يمكن تنفيذها بمفردها وفقًا للمتطلبات ، وليس من الضروري استخدام فطائر ، إطار عمل يعمل على مستوى الاتصال ، وهو أكثر من مستوى الاتصال).
واجهات برمجة التطبيقات
يتم تقسيم الفاصل إلى جزأين: الطرف الأمامي والخلفي. يتضمن العمل المطلوب من قبل الخادم الحفاظ على قائمة الغرف وتوفير وظائف إنشاء الغرفة والانضمام إليها. يبدو واجهات برمجة التطبيقات العميل لدينا هكذا:
Spaceroom.connect (العنوان ، رد الاتصال) الاتصال بالخادم
Spaceroom.createroom (رد الاتصال) إنشاء غرفة
Spaceroom.joinroom (RoomId) انضم إلى غرفة
Spaceroom.on (الحدث ، رد الاتصال) يستمع للأحداث
...
بعد اتصال العميل بالخادم ، يتلقى أحداث مختلفة. على سبيل المثال ، قد يتلقى المستخدم في غرفة حدثًا ينضم إليه لاعب جديد ، أو حدثًا تبدأ فيه اللعبة. نعطي العميل "دورة حياة" ، والتي ستكون في واحدة من الولايات التالية في أي وقت:
يمكنك الحصول على الحالة الحالية للعميل من خلال spaceroom.state.
باستخدام إطار من جانب الخادم أمر بسيط نسبيًا. إذا كنت تستخدم ملف التكوين الافتراضي ، فيمكنك تشغيل إطار العمل من جانب الخادم مباشرة. لدينا شرط أساسي: يمكن تشغيل رمز الخادم مباشرة في العميل دون الحاجة إلى خادم منفصل. يجب أن يعرف اللاعبون الذين لعبوا PS أو PSP ما أتحدث عنه. بالطبع ، من الممتاز أيضًا التشغيل على خادم خاص.
تنفيذ الكود المنطقي موجز هنا. أكمل الجيل الأول من Spaceroom وظيفة خادم المقبس ، الذي يحافظ على قائمة الغرف ، بما في ذلك حالة الغرفة ، واتصالات وقت اللعبة المقابلة لكل غرفة (مجموعة تعليمات ، بث دلو ، إلخ). للتنفيذ المحدد ، يرجى الرجوع إلى رمز المصدر.
خوارزمية متزامنة
لذا ، كيف يمكننا عرض الأشياء بين كل عميل متسقة في الوقت الفعلي؟
هذا الشيء يبدو مثيرًا للاهتمام. فكر في الأمر بعناية ، ماذا نحتاج إلى الخادم لمساعدتنا في تسليمها؟ بطبيعة الحال ، سوف تفكر في التسبب في تناقضات منطقية بين مختلف العملاء: تعليمات المستخدم. نظرًا لأن الرموز التي تتعامل مع منطق اللعبة هي نفسها ، بالنظر إلى نفس الشروط ، فإن الكود يعمل نفس النتيجة. الفرق الوحيد هو أوامر اللاعب المختلفة التي تم استلامها أثناء اللعبة. صحيح ، نحن بحاجة إلى طريقة لمزامنة هذه التعليمات. إذا تمكن جميع العملاء من الحصول على نفس التعليمات ، فيمكن لجميع العملاء نظريًا نفس نتيجة العملية.
خوارزميات التزامن للألعاب عبر الإنترنت كلها أنواع من السيناريوهات الغريبة والقابلة للتطبيق. يستخدم Spaceroom خوارزميات التزامن مماثلة لمفهوم قفل الإطار. نقسم الجدول الزمني إلى فترات واحدة تلو الأخرى ، ويطلق على كل فاصل دلو. يتم استخدام دلو لتحميل التعليمات ويتم الحفاظ عليها من جانب الخادم. في نهاية كل فترة زمنية دلو ، يقوم الخادم ببث الجرافة لجميع العملاء. بعد أن يحصل العميل على الدلو ، يتم استرداد التعليمات منه ، ثم تنفذ بعد التحقق.
من أجل تقليل تأثير تأخير الشبكة ، سيتم تسليم كل تعليمات تلقاها الخادم من العميل إلى الدلو المقابل وفقًا لخوارزمية معينة ، ويتم اتباع الخطوات التالية على وجه التحديد:
دع Order_start يكون وقت الأمر الذي يحمله الأمر ، و T هو وقت بدء الجرافة حيث يوجد order_start.
إذا كان T + DEHED_TIME <= وقت البدء للدلو الذي يقوم حاليًا بجمع التعليمات ، قم بتسليم الأمر إلى الدلو الذي يقوم بجمع التعليمات حاليًا ، وإلا تابع الخطوة 3
قم بتسليم التعليمات إلى الدلو المقابل لـ T + Delay_time
عندما يكون التأخير في وقت تأخير الخادم المتفق عليه ، والذي يمكن اعتباره متوسط التأخير بين العملاء. تبلغ القيمة الافتراضية في الفضاء 80 ، والقيمة الافتراضية لطول الجرافة هي 48. في نهاية كل فترة زمنية دلو ، يقوم الخادم ببث هذا الدلو لجميع العملاء ويبدأ تلقي التعليمات للدلو التالي. يتحكم العميل في خطأ الوقت ضمن نطاق مقبول عند تنفيذ المطابقة تلقائيًا في المنطق بناءً على فاصل الجرافة المستلم.
هذا يعني أنه في ظل الظروف العادية ، سيتلقى العميل دلوًا يتم إرساله من الخادم كل 48 مللي ثانية. عندما يكون الوقت الذي يحتاج فيه الدلو معالجة معالجته ، سيتعامل العميل مع ذلك وفقًا لذلك. على افتراض أن FPS العميل = 60 ، سيتلقى دلوًا كل 3 إطارات أو نحو ذلك ، وسيتم تحديث المنطق وفقًا لهذا الجرافة. إذا لم يتم استلام الدلو بعد انتهاء الوقت بسبب تقلبات الشبكة ، فإن العميل يتوقف مؤقتًا عن منطق اللعبة وينتظر. في وقت واحد داخل دلو ، يمكن تحديث المنطق باستخدام طريقة LERP.
في حالة التأخير = 80 ، bucket_size = 48 ، سيتم تأخير أي تعليمية بتنفيذ 96 مللي ثانية على الأقل. قم بتغيير هاتين المعلمتين ، على سبيل المثال ، في حالة Delay_time = 60 ، bucket_size = 32 ، سيتم تأخير أي تعليمة بمقدار 64 مللي ثانية على الأقل.
حادثة دموية تسببها مؤقت
بالنظر إلى الأمر برمته ، يحتاج إطار عملنا إلى الحصول على مؤقت دقيق عند تشغيله. تنفيذ بث الدلو تحت فاصل ثابت. بالطبع ، فكرنا أولاً في استخدام SetInterval () ، ولكن في الثانية التالية أدركنا مدى عدم موثوقية هذه الفكرة: يبدو أن SetInterval () لا يوجد لديه أخطاء خطيرة للغاية. وما هو سيء للغاية هو أن كل خطأ سوف يتراكم ، مما يسبب عواقب وخيمة بشكل متزايد.
لذلك فكرنا على الفور في استخدام setTimeOut () لجعل منطقنا مستقرًا تقريبًا حول الفاصل الزمني المحدد عن طريق تصحيح وقت الوصول التالي ديناميكيًا. على سبيل المثال ، هذا الوقت SetTimeOut () أقل من 5 مللي ثانية من المتوقع ، لذلك سوف نسمح لها 5 مللي ثانية في وقت مبكر في المرة القادمة. ومع ذلك ، فإن نتائج الاختبار ليست مرضية ، وهذا ليس أنيقًا بما يكفي بغض النظر عن كيفية النظر إليه.
لذلك نحن بحاجة إلى تغيير تفكيرنا. هل من الممكن أن تنتهي صلاحية SetTimeOut () في أسرع وقت ممكن ، ثم نتحقق مما إذا كان الوقت الحالي قد وصل إلى الوقت المستهدف. على سبيل المثال ، في حلقتنا ، باستخدام setTimeout (رد الاتصال ، 1) لمواصلة التحقق من الوقت ، والتي تبدو فكرة جيدة.
مؤقت مخيب للآمال
كتبنا على الفور قطعة من الكود لاختبار أفكارنا ، وكانت النتائج مخيبة للآمال. في أحدث إصدار مستقر Node.js (v0.10.32) ونظام التشغيل Windows ، قم بتشغيل هذه الرمز:
نسخة الكود كما يلي:
var sum = 0 ، count = 0 ؛
اختبار الوظيفة () {
var now = date.now () ؛
setTimeout (function () {
var diff = date.now () - الآن ؛
SUM += Diff ؛
count ++ ؛
امتحان()؛
}) ؛
}
امتحان()؛
بعد فترة من الزمن ، أدخل SUM/COUNT في وحدة التحكم ويمكنك رؤية نتيجة ، على غرار:
نسخة الكود كما يلي:
> SUM / COUNT
15.624555160142348
ماذا؟! أريد 1 مللي ثانية فاصل ، لكنك تخبرني أن الفاصل الزمني الفعلي هو 15.625 مللي ثانية! هذه الصورة ببساطة جميلة جدا. لقد أجرينا نفس الاختبار على Mac وكانت النتيجة 1.4 مللي ثانية. لذلك كنا في حيرة من أمرنا: ماذا بحق الجحيم هذا؟ إذا كنت من محبي Apple ، فربما خلصت إلى أن Windows كانت سلة المهملات وتنازلت عن Windows. لحسن الحظ ، كنت مهندسًا صارمًا في الواجهة الأمامية ، لذلك بدأت في الاستمرار في التفكير في هذا الرقم.
انتظر ، لماذا هذا الرقم مألوف جدا؟ هل سيكون هذا الرقم مشابهًا جدًا للفاصل الزمني الموقت القصوى تحت Windows؟ قمت على الفور بتنزيل Clockres للاختبار ، وبعد تشغيل وحدة التحكم ، حصلت على النتائج التالية:
نسخة الكود كما يلي:
الفاصل الزمني الأقصى الموقت: 15.625 مللي ثانية
فاصل الموقت الأدنى: 0.500 مللي ثانية
الفاصل الزمني للوقت الحالي: 1.001 مللي ثانية
بالتأكيد بما فيه الكفاية! بالنظر إلى دليل node.js ، يمكننا أن نرى وصفًا لـ setTimeout مثل هذا:
يعتمد التأخير الفعلي على العوامل الخارجية مثل تفاصيل مؤقت OS وحمل النظام.
ومع ذلك ، تظهر نتائج الاختبار أن هذا التأخير الفعلي هو الحد الأقصى لفاصل مؤقت (لاحظ أن الفاصل الزمني للوقت الحالي للنظام هو 1.001 مللي ثانية فقط) ، وهو أمر غير مقبول في أي حال. إن الفضول القوي يدفعنا إلى النظر من خلال رمز المصدر لـ Node.js للحصول على لمحة عن الحقيقة.
علة في node.js
أعتقد أن معظمكم ولدي فهم معين لآلية حلقة حتى Node.js. بالنظر إلى الكود المصدري لتنفيذ الموقت ، يمكننا أن نفهم مبدأ التنفيذ تقريبًا. لنبدأ بالحلقة الرئيسية لحلقة الحدث:
نسخة الكود كما يلي:
بينما (r! = 0 && loop-> stop_flag == 0) {
/* تحديث الوقت العالمي*/
UV_UPDATE_TIME (حلقة) ؛
/* تحقق مما إذا كان الموقت ينتهي وتنفيذ رد الاتصال الموقت المقابل*/
UV_PROCESS_TIMERS (حلقة) ؛
/* استدعاء عمليات الاسترجاعات الخاملة إذا لم يكن هناك شيء. */
if (loop-> pending_reqs_tail == null &&
loop-> endgame_handles == null) {
/* منع حلقة الحدث من الخروج*/
UV_IDLE_INVOKE (حلقة) ؛
}
UV_PROCESS_REQS (حلقة) ؛
UV_PROCESS_ENDGAMES (حلقة) ؛
UV_PREPARE_INVOKE (حلقة) ؛
/* جمع أحداث IO*/
(*استطلاع) (حلقة ، حلقة-> Idle_handles == null &&
loop-> pending_reqs_tail == null &&
حلقة-> endgame_handles == null &&
! loop-> stop_flag &&
(loop-> active_handles> 0 ||
! ngx_queue_empty (& loop-> active_reqs)) &&
! (MODE & UV_RUN_NOWAIT)) ؛
/* setimmediate () الخ*/
UV_CHECK_INVOKE (حلقة) ؛
r = uv__loop_alive (حلقة) ؛
if (mode & (uv_run_once | uv_run_nowait))
استراحة؛
}
الكود المصدري لدالة UV_UPDATE_TIME كما يلي: (https://github.com/joyent/libuv/blob/v0.10/src/win/timer.c))))
نسخة الكود كما يلي:
void uv_update_time (uv_loop_t* loop) {
/* احصل على وقت النظام الحالي*/
dword ticks = getTickCount () ؛
/ * يتم الافتراض بأن chare_integer.quadpart لديه نفس النوع */
/* loop-> الوقت ، والذي يحدث. هل هناك أي طريقة لتأكيد هذا؟ */
large_integer* time = (large_integer*) & loop-> time ؛
/* إذا كان الموقت ملفوفًا ، فأضف 1 إلى DWORD عالي الترتيب. */
/ * يجب أن تتأكد UV_POLL من أن الموقت لا يمكن أن يفيض أكثر من */
/* مرة واحدة بين اثنين من مكالمات UV_UPDATE_TIME. */
if (tricks <tim-> lowpart) {
time-> Highpart += 1 ؛
}
time-> lowpart = ticks ؛
}
يستخدم التنفيذ الداخلي لهذه الوظيفة وظيفة getTickCount () لنظام التشغيل Windows لتعيين الوقت الحالي. ببساطة ، بعد استدعاء وظيفة setTimeout ، بعد سلسلة من النضالات ، سيتم ضبط المؤقت الداخلي-> على Time Time + Timeout الحالي. في حلقة الحدث ، قم أولاً بتحديث وقت الحلقة الحالية من خلال UV_UPDATE_TIME ، ثم تحقق مما إذا كان الموقت ينتهي في UV_PROCESS_TIMES. إذا كان الأمر كذلك ، أدخل عالم JavaScript. بعد قراءة المقال بأكمله ، تشبه حلقة الحدث هذه العملية تقريبًا:
تحديث الوقت العالمي
تحقق من المؤقت. إذا انتهت صلاحية المؤقت ، قم بتنفيذ رد الاتصال.
تحقق من قائمة انتظار REQS وتنفيذ طلب الانتظار
أدخل وظيفة الاستطلاع لجمع أحداث IO. في حالة وصول حدث IO ، أضف وظيفة المعالجة المقابلة إلى قائمة انتظار REQS للتنفيذ في حلقة الحدث التالية. داخل وظيفة الاستطلاع ، يتم استدعاء طريقة النظام لجمع أحداث IO. ستؤدي هذه الطريقة إلى حظر العملية حتى يصل حدث IO أو الوصول إلى وقت المهلة المحددة. عندما يتم استدعاء هذه الطريقة ، يتم ضبط وقت المهلة على الوقت الذي ينتهي فيه الموقت الأخير. وهذا يعني أن أحداث IO يتم جمعها عن طريق الحظر وأن الحد الأقصى لوقت الحظر هو الوقت الأخير للموقت التالي.
الكود المصدري لأحد وظائف الاستطلاع تحت Windows:
نسخة الكود كما يلي:
باطل ثابت UV_POLL (UV_LOOP_T* LOOP ، int block) {
بايت بايت ، مهلة ؛
ulong_ptr مفتاح ؛
متداخلة* متداخلة.
UV_REQ_T* req ؛
if (block) {
/* خذ وقت انتهاء صلاحية آخر مؤقت*/
timeout = uv_get_poll_timeout (loop) ؛
} آخر {
مهلة = 0 ؛
}
getQueuedCompletionstatus (حلقة-> IOCP ،
& بايت ،
&مفتاح،
وتداخل ،
/* في معظم الحظر حتى انتهاء الموقت التالي*/
نفذ الوقت)؛
إذا (متداخل) {
/ * تم إلغاء الحزمة */
req = uv_overlopped_to_req (تداخل) ؛
/* أدخل أحداث IO في قائمة الانتظار*/
UV_INSERT_PENDING_REQ (loop ، req) ؛
} آخر إذا (getLasterRor ()! = wait_timeout) {
/ * خطأ جاد */
UV_FATAL_ERROR (getLasterror () ، "getQueuedCompletionStatus") ؛
}
}
باتباع الخطوات المذكورة أعلاه ، على افتراض أننا قمنا بتعيين Timeout = 1MS Timer ، ستقوم وظيفة الاستطلاع بمنع 1ms على الأكثر ثم استرداد بعد الاسترداد (إذا لم تكن هناك أحداث IO خلال الفترة). عند الاستمرار في إدخال حلقة الحدث ، ستجد UV_UPDATE_TIME الوقت ، ثم يجد UV_PROCESS_TIMERS أن مؤقتنا ينتهي وتنفيذ رد الاتصال. وبالتالي فإن التحليل الأولي هو أن UV_UPDATE_TIME لديه مشكلة (لا يتم تحديث الوقت الحالي بشكل صحيح) ، أو تنتظر وظيفة الاقتراح 1 مللي ثانية ثم تتعافى. هناك مشكلة في انتظار 1Ms.
بالنظر إلى MSDN ، اكتشفنا بشكل مدهش وصفًا لوظيفة GetTickCount:
يقتصر دقة وظيفة getTickCount على حل مؤقت النظام ، والذي يتراوح عادة ما يتراوح بين 10 ملايين ثانية إلى 16 مليون ثانية.
دقة GetTickCount صعبة للغاية! افترض أن وظيفة الاستطلاع تحظر بشكل صحيح وقت 1 مللي ثانية ، ولكن في المرة التالية التي يتم فيها تنفيذ UV_UPDATE_TIME ، لا يتم تحديث وقت الحلقة الحالي بشكل صحيح! لذلك لم يتم الحكم على مؤقتنا على انتهاء صلاحيته ، لذا انتظر استطلاع الرأي 1 مللي ثانية ودخل حلقة الحدث التالية. حتى يتم تحديث GetTickCount أخيرًا بشكل صحيح (يتم تحديث ما يسمى 15.625 مللي ثانية مرة واحدة) ، يتم تحديث الوقت الحالي للحلقة ، ويتم الحكم على مؤقتنا في UV_PROCESS_TIMERS.
اطلب المساعدة من WebKit
هذا الكود المصدري لـ Node.js عاجز للغاية: لقد استخدم وظيفة الوقت بدقة منخفضة ولم يفعل أي شيء. لكننا اعتقدنا على الفور أنه نظرًا لأننا نستخدم Node-Webkit ، بالإضافة إلى node.js 'setTimeout ، لدينا أيضًا setTimeOut من Chromium. اكتب رمز اختبار واستخدم متصفحنا أو العقدة webkit لتشغيله: http://marks.lrednight.com/test.html#1 (# يليه الرقم يشير إلى الفاصل الزمني المراد قياسه). النتيجة على النحو التالي:
وفقًا لمواصفات HTML5 ، يجب أن تكون النتيجة النظرية هي أن النتائج الخمس الأولى هي 1 مللي ثانية ، والنتائج التالية هي 4 مللي ثانية. تبدأ النتائج المعروضة في حالة الاختبار من المرة الثالثة ، مما يعني أن البيانات الواردة في الجدول يجب أن تكون نظريًا 1 مللي ثانية للمرات الثلاث الأولى ، والنتائج بعد ذلك هي 4 مللي ثانية. والنتيجة لها بعض الأخطاء ، ووفقًا للوائح ، فإن أصغر نتيجة نظرية يمكننا الحصول عليها هي 4 مللي ثانية. على الرغم من أننا غير راضين ، من الواضح أنه أكثر إرضاءً من نتيجة Node.js. اتجاه الفضول القوي ، دعنا نلقي نظرة على رمز المصدر للكروم لنرى كيف يتم تنفيذه. (https://chromium.googlesource.com/chromium/src.git/+/38.0.2125.101/base/time/time_win.cc)
أولاً ، يستخدم Chromium وظيفة TimeGetTime () في تحديد الوقت الحالي للحلقة. من خلال النظر إلى MSDN ، يمكنك أن تجد أن دقة هذه الوظيفة تتأثر بالفاصل الزمني الموقت الحالي للنظام. على جهاز الاختبار الخاص بنا ، هو نظريًا 1.001 ملليًا أعلاه. ومع ذلك ، افتراضيًا ، فإن الفاصل الزمني الموقت هو الحد الأقصى لقيمته (15.625 مللي ثانية على جهاز الاختبار) ، ما لم يعدل التطبيق الفاصل الزمني للمؤتمر العالمي.
إذا اتبعت الأخبار في صناعة تكنولوجيا المعلومات ، فيجب أن تكون قد رأيت مثل هذه الأخبار. يبدو أن الكروم لدينا قد وضع الفاصل الزمني الموقت صغير جدا! يبدو أنه لا داعي للقلق بشأن فاصل توقيت النظام؟ لا تكن سعيدًا جدًا في وقت مبكر جدًا ، فقد أعطانا مثل هذا الإصلاح ضربة. في الواقع ، تم إصلاح هذه المشكلة في Chrome 38. هل يجب أن نستخدم إصلاح العقدة السابقة webkit؟ من الواضح أن هذا ليس أنيقًا بما فيه الكفاية ويمنعنا من استخدام نسخة أكثر أداءً من الكروم.
عند النظر إلى رمز مصدر الكروم ، يمكننا أن نجد أنه عندما يكون هناك مؤقت ومهلة المهلة <32 مللي ثانية ، سيغير Chromium الفاصل الزمني للمصنف العالمي للنظام لتحقيق مؤقت بدقة أقل من 15.625 مللي ثانية. (عرض رمز المصدر) عند بدء تشغيل المؤقت ، سيتم تمكين شيء يسمى HighresolutionTimerManager. سوف تستدعي هذه الفئة وظيفة enableHighresolutionTimer استنادًا إلى نوع طاقة الجهاز الحالي. على وجه التحديد ، عندما يستخدم الجهاز الحالي البطارية ، فسوف يستدعي enableHighresolutionTimer (false) ، وسيتم تمرير True عند استخدام الطاقة. إن تنفيذ وظيفة enableHighresolutionTimer هو كما يلي:
نسخة الكود كما يلي:
وقت الفراغ :: enableHighresolutionTimer (Bool Enable) {
BASE :: Autolock Lock (g_high_res_lock.get ()) ؛
if (g_high_res_timer_enabled == تمكين)
يعود؛
g_high_res_timer_enabled = enable ؛
if (! g_high_res_timer_count)
يعود؛
// منذ g_high_res_timer_count! = 0 ، ActivateHighresolutionTimer (صحيح)
// تم استدعاؤه الذي يسمى timebeginperiod مع g_high_res_timer_enabled
// مع قيمة هي عكس | تمكين |. مع هذه المعلومات نحن
// call timeendperiod مع نفس القيمة المستخدمة في الوقت المحدد و
// لذلك التراجع عن تأثير الفترة.
إذا (تمكين) {
timeendperiod (KmintimerInterVallowResms) ؛
timebeginperiod (kmintimerintervalhighresms) ؛
} آخر {
timeendperiod (kmintimerintervalhighresms) ؛
timebeginperiod (KmintimerInterVallowResms) ؛
}
}
حيث ، kmintimerInterVallowResms = 4 و KmintimerIntervalhighresms = 1. الوقت timebeginperiod و timeendperiod هي وظائف توفرها Windows لتعديل فاصل توقيت النظام. وهذا يعني ، عند الاتصال بمصدر الطاقة ، أصغر فاصل مؤقت يمكن أن نحصل عليه هو 1 مللي ثانية ، أثناء استخدام البطارية ، يكون 4 مللي ثانية. نظرًا لأن حلقتنا تستدعي بشكل مستمر setTimeOut ، وفقًا لمواصفات W3C ، فإن الفاصل الزمني الدنيا هو أيضًا 4 مللي ثانية ، لذلك أشعر بالارتياح ، وهذا ليس له تأثير ضئيل علينا.
مشكلة دقيقة أخرى
بالعودة إلى البداية ، وجدنا أن نتائج الاختبار تظهر أن الفاصل الزمني لـ SetTimeout ليس مستقرًا عند 4 مللي ثانية ، ولكنه يتقلب بشكل مستمر. توضح نتائج اختبار http://marks.lrednight.com/test.html#48 أيضًا أن الفواصل الزمنية تتغلب بين 48 مللي ثانية و 49 مللي ثانية. والسبب هو أنه في حلقة الحدث من Chromium و Node.js ، تتأثر دقة استدعاء وظيفة Windows في انتظار حدث IO بجهاز توقيت النظام الحالي. يتطلب تنفيذ منطق اللعبة وظيفة requestAnimationFrame (تحديث القماش باستمرار) ، والتي يمكن أن تساعدنا على ضبط الفاصل الزمني الموقت على الأقل kmintimerintervallowresms (لأنه يحتاج إلى مؤقت 16ms ، والذي يؤدي إلى متطلبات مؤقت للغاية). عندما تستخدم جهاز الاختبار الطاقة ، يكون فاصل مؤقت النظام 1 مللي ثانية ، وبالتالي فإن نتيجة الاختبار لها خطأ ± 1Ms. إذا لم يغير جهاز الكمبيوتر الخاص بك الفاصل الزمني لتوقيت النظام وقم بإجراء اختبار #48 أعلاه ، فقد يصل Max إلى 48+16 = 64ms.
باستخدام تطبيق SetTimeout الخاص بـ Chromium ، يمكننا التحكم في خطأ SetTimeOut (FN ، 1) إلى حوالي 4 مللي ثانية ، في حين أن خطأ SetTimeOut (FN ، 48) يمكنه التحكم في خطأ SetTimeout (FN ، 48) إلى حوالي 1 مللي ثانية. لذلك ، لدينا مخطط جديد في أذهاننا ، مما يجعل رمزنا يبدو مثل هذا:
نسخة الكود كما يلي:
/ * احصل على زخرفة الفاصل الزمني الأقصى */
var decoration = getMaxIntervaldeviation (bucketsize) ؛ // bucketssize = 48 ، الانحراف = 2 ؛
وظيفة gameloop () {
var now = date.now () ؛
if (previourbucket + bucketsize <= الآن) {
السابق bucket = الآن ؛
dologic () ؛
}
if (previourbucket + bucketsize - date.now ()> decoration) {
// انتظر 46 مللي ثانية. التأخير الفعلي أقل من 48 مللي ثانية.
setTimeout (gameloop ، bucketsize - design) ؛
} آخر {
// مشغول الانتظار. استخدم setImmediate بدلاً من Process.nextTick لأن الأول لا يحظر أحداث IO.
setImmediate (gameloop) ؛
}
}
يتيح لنا الرمز أعلاه الانتظار لوقت مع خطأ أقل من Bucket_Size (تعريف bucket_size) بدلاً من معادلة دلو. حتى إذا حدث الحد الأقصى للخطأ في تأخير 46 مللي ثانية ، وفقًا للنظرية أعلاه ، فإن الفاصل الزمني الفعلي أقل من 48 مللي ثانية. بقية الوقت نستخدم طريقة الانتظار المزدحمة للتأكد من تنفيذ Gameloop لدينا تحت فاصل بدقة كافية.
بينما قمنا بحل المشكلة إلى حد ما مع الكروم ، من الواضح أن هذا ليس أنيقًا بما فيه الكفاية.
تذكر طلبنا الأولي؟ يجب أن يكون رمز جانب الخادم الخاص بنا قادرًا على التشغيل مباشرة على جهاز كمبيوتر مع بيئة node.js بدون عميل العقدة webkit. إذا قمت بتشغيل الكود أعلاه مباشرة ، فإن قيمة التعريف هي على الأقل 16 مللي ثانية ، مما يعني أنه في كل 48 مللي ثانية ، علينا أن ننتظر 16 مللي ثانية. ارتفع معدل استخدام وحدة المعالجة المركزية.
مفاجأة غير متوقعة
إنه أمر مزعج للغاية. ألم يلاحظ أحد مثل هذا الخطأ الكبير في node.js؟ الجواب يجعلنا سعداء للغاية. تم إصلاح هذا الخطأ في الإصدار v.0.11.3. يمكنك أيضًا رؤية النتائج المعدلة عن طريق عرض الفرع الرئيسي لرمز LIBUV مباشرة. يتمثل النهج المحدد في إضافة مهلة إلى الوقت الحالي للحلقة بعد أن تنتظر وظيفة الاستطلاع. وبهذه الطريقة ، حتى لو لم يتفاعل GetTickCount ، ما زلنا أضفنا وقت الانتظار هذا بعد انتظار الاستطلاع. لذلك يمكن أن تنتهي الموقت بسلاسة.
بمعنى آخر ، تم حل المشكلة التي تم عملها بجد لفترة طويلة في v.0.11.3. ومع ذلك ، لم تكن جهودنا سدى. لأنه حتى إذا تم القضاء على وظيفة getTickCount ، فإن وظيفة الاستطلاع نفسها تتأثر بموقت النظام. أحد الحلول هو كتابة المكون الإضافي Node.js لتغيير فترات توقيت النظام.
ومع ذلك ، فإن الإعدادات الأولية للعبأ لدينا هذه المرة ليست خالية من الخادم. بعد إنشاء العميل غرفة ، يصبح خادمًا. يمكن أن يتم تشغيل رمز الخادم في بيئة Node-Webkit ، وبالتالي فإن أولوية مشكلات المؤقت على أنظمة Windows ليست هي الأعلى. باتباع الحل الذي قدمناه أعلاه ، فإن النتائج كافية لإرضاءنا.
النهاية
بعد حل مشكلة المؤقت ، لن يتم إعاقة تنفيذ الإطار لدينا بشكل أساسي. نحن نقدم دعم WebSocket (في بيئات HTML5 الخالصة) ، وتخصيص بروتوكول الاتصال لتحقيق دعم مقبس أداء أعلى (في بيئات العقدة webkit). بالطبع ، كانت وظائف Spaceroom بسيطة نسبيًا في البداية ، ولكن مع اقتراح الطلب وزاد الوقت ، نحسن هذا الإطار تدريجياً.
على سبيل المثال ، عندما وجدنا أنه عندما نحتاج إلى توليد أرقام عشوائية متسقة في لعبتنا ، أضفنا هذه الوظيفة إلى فطائر. في بداية اللعبة ، سيقوم Spaceroom بتوزيع بذور الأرقام العشوائية. يوفر Spaceroom للعميل طريقة لاستخدام العشوائية في MD5 لإنشاء أرقام عشوائية بمساعدة بذور الأرقام العشوائية.
يبدو مرتاحا جدا. لقد تعلمت أيضًا الكثير في عملية كتابة مثل هذا الإطار. إذا كنت مهتمًا بتطهير الفضاء ، فيمكنك أيضًا المشاركة فيه. أعتقد أن الفضاء سوف يستخدم قبضته وقدمه في المزيد من الأماكن.