مطالعه موردی - داخل پیچ و خم جهانی

World Wide Maze یک بازی است که در آن از گوشی هوشمند خود برای هدایت یک توپ در حال چرخش در پیچ و خم های سه بعدی ایجاد شده از وب سایت ها استفاده می کنید تا سعی کنید به اهداف آنها برسید.

پیچ و خم جهانی

این بازی دارای استفاده فراوان از ویژگی های HTML5 است. برای مثال، رویداد DeviceOrientation داده‌های شیب را از تلفن هوشمند بازیابی می‌کند، که سپس از طریق WebSocket به رایانه شخصی ارسال می‌شود، جایی که بازیکنان راه خود را از طریق فضاهای سه بعدی ساخته شده توسط WebGL و Web Workers پیدا می‌کنند.

در این مقاله، نحوه استفاده دقیق از این ویژگی ها، روند کلی توسعه و نکات کلیدی برای بهینه سازی را توضیح خواهم داد.

جهت گیری دستگاه

رویداد DeviceOrientation ( مثال ) برای بازیابی داده‌های شیب از تلفن هوشمند استفاده می‌شود. هنگامی که addEventListener با رویداد DeviceOrientation استفاده می شود، یک تماس با شی DeviceOrientationEvent به عنوان آرگومان در فواصل زمانی منظم فراخوانی می شود. خود فواصل با دستگاه مورد استفاده متفاوت است. به عنوان مثال، در iOS + Chrome و iOS + Safari، تماس پاسخ تقریباً هر 1/20 ثانیه فراخوانی می‌شود، در حالی که در Android 4 + Chrome تقریباً هر 1/10 ثانیه فراخوانی می‌شود.

window.addEventListener('deviceorientation', function (e) {
  // do something here..
});

شی DeviceOrientationEvent حاوی داده های شیب برای هر یک از محورهای X ، Y و Z بر حسب درجه (نه رادیان) است ( در HTML5Rocks بیشتر بخوانید ). با این حال، مقادیر بازگشتی نیز با ترکیب دستگاه و مرورگر مورد استفاده متفاوت است. محدوده مقادیر بازگشتی واقعی در جدول زیر نشان داده شده است:

جهت گیری دستگاه

مقادیری که در بالا با رنگ آبی مشخص شده اند، مقادیری هستند که در مشخصات W3C تعریف شده اند. مواردی که با رنگ سبز مشخص شده اند با این مشخصات مطابقت دارند، در حالی که آنهایی که با رنگ قرمز مشخص شده اند منحرف می شوند. با کمال تعجب، تنها ترکیب Android-Firefox مقادیری را برگرداند که با مشخصات مطابقت داشت. با این وجود، وقتی نوبت به اجرا می رسد، تطبیق مقادیری که مکرراً اتفاق می افتد، منطقی تر است. بنابراین World Wide Maze از مقادیر بازگشتی iOS به عنوان استاندارد استفاده می کند و بر این اساس برای دستگاه های Android تنظیم می شود.

if android and event.gamma > 180 then event.gamma -= 360

با این حال، هنوز از Nexus 10 پشتیبانی نمی کند. اگرچه Nexus 10 همان محدوده ای از مقادیر را با سایر دستگاه های اندرویدی برمی گرداند، اما یک اشکال وجود دارد که مقادیر بتا و گاما را معکوس می کند. به این موضوع جداگانه پرداخته می شود. (شاید جهت افقی پیش فرض باشد؟)

همانطور که این نشان می دهد، حتی اگر API های مربوط به دستگاه های فیزیکی دارای مشخصات تنظیم شده باشند، هیچ تضمینی وجود ندارد که مقادیر بازگشتی با آن مشخصات مطابقت داشته باشند. بنابراین آزمایش آنها بر روی همه دستگاه های آینده بسیار مهم است. همچنین به این معنی است که مقادیر غیرمنتظره ای ممکن است وارد شود، که نیاز به ایجاد راه حل دارد. World Wide Maze از بازیکنانی که برای اولین بار می خواهند دستگاه خود را مطابق مرحله 1 آموزش خود کالیبره کنند، می خواهد، اما اگر مقادیر شیب غیرمنتظره ای دریافت کند، به درستی در موقعیت صفر کالیبره نمی شود. بنابراین دارای یک محدودیت زمانی داخلی است و از پخش کننده می خواهد تا در صورتی که نمی تواند در آن محدودیت زمانی کالیبره شود، به کنترل های صفحه کلید سوئیچ کند.

وب سوکت

در World Wide Maze، تلفن هوشمند و رایانه شخصی شما از طریق WebSocket متصل می شوند. به طور دقیق تر، آنها از طریق یک سرور رله بین آنها، یعنی گوشی هوشمند به سرور به رایانه شخصی متصل می شوند. این به این دلیل است که WebSocket توانایی اتصال مستقیم مرورگرها به یکدیگر را ندارد. (استفاده از کانال های داده WebRTC امکان اتصال همتا به همتا را فراهم می کند و نیاز به سرور رله را از بین می برد، اما در زمان اجرا این روش فقط با Chrome Canary و Firefox Nightly قابل استفاده بود .)

من استفاده از کتابخانه ای به نام Socket.IO (v0.9.11) را انتخاب کردم که شامل ویژگی هایی برای اتصال مجدد در صورت قطع یا قطع شدن اتصال است. من از آن همراه با NodeJS استفاده کردم، زیرا این ترکیب NodeJS + Socket.IO بهترین عملکرد سمت سرور را در چندین آزمایش پیاده سازی WebSocket نشان داد.

جفت شدن بر اساس اعداد

  1. کامپیوتر شما به سرور متصل می شود.
  2. سرور یک عدد به طور تصادفی تولید شده به کامپیوتر شما می دهد و ترکیب عدد و کامپیوتر را به خاطر می آورد.
  3. از دستگاه تلفن همراه خود، شماره ای را مشخص کرده و به سرور متصل شوید.
  4. اگر شماره مشخص شده با یک رایانه متصل یکسان باشد، دستگاه تلفن همراه شما با آن رایانه شخصی جفت شده است.
  5. اگر رایانه شخصی مشخصی وجود نداشته باشد، خطایی رخ می دهد.
  6. هنگامی که داده از دستگاه تلفن همراه شما وارد می شود، به رایانه شخصی که با آن جفت شده است ارسال می شود و بالعکس.

همچنین می توانید به جای آن اتصال اولیه را از دستگاه تلفن همراه خود برقرار کنید. در این حالت دستگاه ها به سادگی معکوس می شوند.

همگام سازی برگه

ویژگی Tab Sync مخصوص کروم فرآیند جفت شدن را آسان تر می کند. با استفاده از آن، صفحاتی که در رایانه شخصی باز هستند را می توان به راحتی در دستگاه تلفن همراه باز کرد (و بالعکس). کامپیوتر شماره اتصال صادر شده توسط سرور را می گیرد و با استفاده از history.replaceState به URL صفحه اضافه می کند.

history.replaceState(null, null, '/maze/' + connectionNumber)

اگر Tab Sync فعال باشد، URL پس از چند ثانیه همگام‌سازی می‌شود و همان صفحه را می‌توان در دستگاه تلفن همراه باز کرد. دستگاه تلفن همراه URL صفحه باز شده را بررسی می کند و اگر شماره ای اضافه شود، بلافاصله شروع به اتصال می کند. با این کار نیازی به وارد کردن اعداد به صورت دستی یا اسکن کدهای QR با دوربین نیست.

تاخیر

از آنجایی که سرور رله در ایالات متحده قرار دارد، دسترسی به آن از ژاپن منجر به تأخیر تقریباً 200 میلی‌ثانیه قبل از رسیدن اطلاعات شیب گوشی هوشمند به رایانه شخصی می‌شود. زمان‌های پاسخ‌دهی به وضوح در مقایسه با زمان‌های محیط محلی که در طول توسعه استفاده می‌شد کند بود، اما قرار دادن چیزی مانند فیلتر پایین‌گذر (من از EMA استفاده کردم) این را به سطوح غیرمحجوبی بهبود بخشید. (در عمل، یک فیلتر پایین گذر برای اهداف ارائه نیز مورد نیاز بود؛ مقادیر برگشتی از سنسور شیب شامل مقدار قابل توجهی نویز بود، و اعمال آن مقادیر روی صفحه نمایش باعث لرزش زیاد می‌شد.) با پرش‌ها کار کنید، که به وضوح کند بودند، اما هیچ کاری برای حل این مشکل نمی‌توان انجام داد.

از آنجایی که از همان ابتدا انتظار مشکلات تاخیر را داشتم، به فکر راه اندازی سرورهای رله در سرتاسر جهان افتادم تا مشتریان بتوانند به نزدیکترین موارد موجود متصل شوند (در نتیجه تاخیر را به حداقل می رساند). با این حال، من از موتور محاسباتی گوگل (GCE) استفاده کردم، که در آن زمان فقط در ایالات متحده وجود داشت، بنابراین این امکان وجود نداشت.

مسئله الگوریتم ناگل

الگوریتم Nagle معمولاً برای ارتباط کارآمد از طریق بافر در سطح TCP در سیستم‌های عامل گنجانده می‌شود، اما متوجه شدم که نمی‌توانم در زمان فعال بودن این الگوریتم داده‌ها را در زمان واقعی ارسال کنم. (به ویژه هنگامی که با تأیید تأخیر TCP ترکیب می شود. حتی با عدم تأخیر ACK ، اگر ACK به دلیل عواملی مانند قرار گرفتن سرور در خارج از کشور تا حدی معین به تأخیر بیفتد، همان مشکل رخ می دهد.)

مشکل تأخیر Nagle با WebSocket در Chrome for Android که شامل گزینه TCP_NODELAY برای غیرفعال کردن Nagle است، رخ نداد، اما با WebKit WebSocket مورد استفاده در Chrome برای iOS که این گزینه را فعال نکرده است، رخ داد. (سافاری که از همان WebKit استفاده می کند نیز این مشکل را داشت. این مشکل از طریق گوگل به اپل گزارش شده و ظاهراً در نسخه توسعه دهنده WebKit حل شده است .

هنگامی که این مشکل رخ می دهد، داده های شیب ارسال شده در هر 100 میلی ثانیه در تکه هایی ترکیب می شوند که فقط هر 500 میلی ثانیه به رایانه شخصی می رسند. بازی تحت این شرایط نمی‌تواند کار کند، بنابراین با ارسال داده‌های سمت سرور در فواصل زمانی کوتاه (هر 50 میلی‌ثانیه یا بیشتر) از این تأخیر جلوگیری می‌کند. من معتقدم که دریافت ACK در فواصل زمانی کوتاه، الگوریتم Nagle را فریب می دهد و فکر می کند که ارسال داده ها مشکلی ندارد.

الگوریتم ناگل 1

نمودار بالا فواصل داده های واقعی دریافت شده را نشان می دهد. فواصل زمانی بین بسته ها را نشان می دهد. سبز نشان دهنده فواصل خروجی و قرمز نشان دهنده فواصل ورودی است. حداقل 54 میلی‌ثانیه، حداکثر 158 میلی‌ثانیه و وسط نزدیک به 100 میلی‌ثانیه است. در اینجا من از یک آیفون با سرور رله واقع در ژاپن استفاده کردم. خروجی و ورودی هر دو حدود 100 میلی‌ثانیه هستند و عملکرد روان است.

الگوریتم ناگل 2

در مقابل، این نمودار نتایج استفاده از سرور در ایالات متحده را نشان می دهد. در حالی که فواصل خروجی سبز در ۱۰۰ میلی‌ثانیه ثابت می‌مانند، فواصل ورودی بین کم‌ترین و حداکثر ۵۰۰ میلی‌ثانیه در نوسان هستند، که نشان می‌دهد رایانه در حال دریافت داده‌ها به صورت تکه‌ای است.

ALT_TEXT_HERE

در نهایت، این نمودار نتایج اجتناب از تأخیر را با فرستادن داده‌های متغیر مکان توسط سرور نشان می‌دهد. در حالی که عملکرد آن به خوبی استفاده از سرور ژاپنی نیست، واضح است که فواصل ورودی تقریباً در حدود 100 میلی‌ثانیه ثابت می‌مانند.

یک اشکال؟

با وجود اینکه مرورگر پیش‌فرض Android 4 (ICS) دارای یک WebSocket API است، نمی‌تواند متصل شود و در نتیجه یک رویداد Socket.IO connect_failed رخ می‌دهد. در داخل زمان آن تمام می شود و سمت سرور نیز نمی تواند اتصال را تأیید کند. (من این را تنها با WebSocket تست نکرده ام، بنابراین ممکن است مشکل از Socket.IO باشد.)

مقیاس پذیری سرورهای رله

از آنجایی که نقش سرور رله چندان پیچیده نیست، افزایش مقیاس و افزایش تعداد سرورها تا زمانی که اطمینان حاصل کنید که رایانه شخصی و دستگاه تلفن همراه همیشه به یک سرور متصل هستند، نباید دشوار باشد.

فیزیک

حرکت توپ در بازی (غلت زدن در سراشیبی، برخورد با زمین، برخورد با دیوارها، جمع آوری اقلام و غیره) همه با یک شبیه ساز فیزیک سه بعدی انجام می شود. من از Ammo.js - پورتی از موتور پرکاربرد فیزیک Bullet در جاوا اسکریپت با استفاده از Emscripten - به همراه Physijs برای استفاده از آن به عنوان "Web Worker" استفاده کردم.

کارگران وب

Web Workers یک API برای اجرای جاوا اسکریپت در موضوعات جداگانه است. جاوا اسکریپت که به‌عنوان Web Worker راه‌اندازی می‌شود، به‌عنوان یک رشته مجزا از رشته‌ای که در ابتدا آن را نامیده بود اجرا می‌شود، بنابراین می‌توان کارهای سنگین را در حالی که صفحه پاسخگو نگه داشت انجام داد. Physijs به طور موثر از Web Workers برای کمک به عملکرد نرمال موتور فیزیک سه بعدی فشرده استفاده می کند. World Wide Maze موتور فیزیک و رندر تصویر WebGL را با نرخ فریم کاملا متفاوت مدیریت می کند، بنابراین حتی اگر نرخ فریم در یک دستگاه با مشخصات پایین به دلیل بار رندر سنگین WebGL کاهش یابد، خود موتور فیزیک کم و بیش 60 فریم در ثانیه را حفظ می کند و مانعی نخواهد داشت. کنترل های بازی

FPS

این تصویر نرخ فریم حاصل را در لنوو G570 نشان می دهد. کادر بالا نرخ فریم WebGL (رندر تصویر) را نشان می‌دهد و کادر پایین نرخ فریم موتور فیزیک را نشان می‌دهد. GPU یک تراشه Intel HD Graphics 3000 یکپارچه است، بنابراین نرخ فریم رندر تصویر به 60 فریم بر ثانیه مورد انتظار نمی رسد. با این حال، از آنجایی که موتور فیزیک به نرخ فریم مورد انتظار دست یافت، گیم‌پلی آن چندان متفاوت از عملکرد یک دستگاه با مشخصات بالا نیست.

از آنجایی که رشته‌های دارای Web Workers فعال دارای اشیاء کنسول نیستند، داده‌ها باید از طریق postMessage به رشته اصلی ارسال شوند تا گزارش‌های اشکال زدایی ایجاد شود. استفاده از console4Worker معادل یک شیء کنسول در Worker ایجاد می کند و فرآیند اشکال زدایی را به میزان قابل توجهی آسان می کند.

کارگران خدماتی

نسخه‌های اخیر Chrome به شما امکان می‌دهد هنگام راه‌اندازی Web Workers، نقاط شکست را تعیین کنید، که برای اشکال‌زدایی نیز مفید است. این را می توان در پانل "Workers" در Developer Tools پیدا کرد.

کارایی

مراحل با تعداد چند ضلعی بالا گاهی اوقات از 100000 چند ضلعی فراتر می رود، اما عملکرد حتی زمانی که به طور کامل به صورت Physijs.ConcaveMesh ( btBvhTriangleMeshShape در Bullet) تولید می شدند، به ویژه آسیب نمی بیند.

در ابتدا، با افزایش تعداد اجسامی که نیاز به تشخیص برخورد داشتند، نرخ فریم کاهش یافت، اما حذف پردازش های غیر ضروری در Physijs باعث بهبود عملکرد شد. این بهبود در فورکی از Physijs اصلی انجام شد.

اشیاء ارواح

اجسامی که دارای تشخیص برخورد هستند اما هیچ تاثیری بر برخورد ندارند و در نتیجه هیچ تاثیری بر سایر اجسام ندارند در Bullet "اشیاء روح" نامیده می شوند. اگرچه Physijs رسماً از اشیاء ارواح پشتیبانی نمی کند، اما می توان پس از ایجاد یک Physijs.Mesh ، آنها را با سرهم کردن پرچم ها در آنجا ایجاد کرد. ماز جهانی از اشیاء ارواح برای تشخیص برخورد اقلام و نقاط هدف استفاده می کند.

hit = new Physijs.SphereMesh(geometry, material, 0)
hit._physijs.collision_flags = 1 | 4
scene.add(hit)

برای collision_flags , 1 CF_STATIC_OBJECT , و 4 CF_NO_CONTACT_RESPONSE است . برای اطلاعات بیشتر، انجمن Bullet، Stack Overflow یا مستندات Bullet را جستجو کنید. از آنجایی که Physijs یک پوشش برای Ammo.js است و Ammo.js اساساً با Bullet یکسان است، بیشتر کارهایی که می‌توان در Bullet انجام داد در Physijs نیز قابل انجام است.

مشکل فایرفاکس 18

به روز رسانی فایرفاکس از نسخه 17 به 18 نحوه تبادل داده ها توسط Web Workers را تغییر داد و در نتیجه Physijs از کار افتاد. این مشکل در GitHub گزارش شد و پس از چند روز حل شد. در حالی که این کارایی منبع باز من را تحت تاثیر قرار داد، این حادثه همچنین به من یادآوری کرد که چگونه World Wide Maze از چندین چارچوب متن باز مختلف تشکیل شده است. من این مقاله را می نویسم به این امید که نوعی بازخورد ارائه کنم.

asm.js

اگرچه این به طور مستقیم به World Wide Maze مربوط نمی شود، اما Ammo.js قبلاً از asm.js اخیراً اعلام شده موزیلا پشتیبانی می کند (تعجب آور نیست زیرا asm.js اساساً برای سرعت بخشیدن به جاوا اسکریپت تولید شده توسط Emscripten ایجاد شده است و خالق Emscripten نیز خالق Ammo.js). اگر کروم از asm.js نیز پشتیبانی کند، بار محاسباتی موتور فیزیک باید به میزان قابل توجهی کاهش یابد. هنگام آزمایش با Firefox Nightly، سرعت به طور قابل توجهی سریعتر بود. شاید بهتر باشد بخش هایی را بنویسیم که به سرعت بیشتری در C/C++ نیاز دارند و سپس با استفاده از Emscripten آنها را به جاوا اسکریپت پورت کنیم؟

WebGL

برای پیاده سازی WebGL از فعال ترین کتابخانه توسعه یافته، three.js (r53) استفاده کردم. اگرچه نسخه 57 قبلاً در مراحل آخر توسعه منتشر شده بود، تغییرات عمده ای در API ایجاد شده بود، بنابراین من به نسخه اصلی برای انتشار ادامه دادم.

جلوه درخشش

افکت درخشش اضافه شده به هسته توپ و موارد با استفاده از یک نسخه ساده از به اصطلاح " Kawase Method MGF " اجرا می شود. با این حال، در حالی که روش Kawase همه مناطق روشن را شکوفا می کند، ماز جهانی برای مناطقی که نیاز به درخشش دارند، اهداف رندر جداگانه ایجاد می کند. این به این دلیل است که یک اسکرین شات وب سایت باید برای بافت های صحنه استفاده شود، و به عنوان مثال، اگر پس زمینه سفید داشته باشد، به سادگی استخراج تمام مناطق روشن منجر به درخشان شدن کل وب سایت می شود. من همچنین در نظر گرفتم همه چیز را در HDR پردازش کنم، اما این بار تصمیم گرفتم این کار را نکنم زیرا پیاده سازی بسیار پیچیده شده بود.

درخشش

بالا سمت چپ اولین پاس را نشان می‌دهد، جایی که نواحی درخشنده به طور جداگانه ارائه شده و سپس یک تاری اعمال شده است. پایین سمت راست، پاس دوم را نشان می دهد، جایی که اندازه تصویر 50 درصد کاهش یافته و سپس یک تاری اعمال می شود. بالا سمت راست پاس سوم را نشان می دهد، جایی که تصویر دوباره 50 درصد کاهش یافته و سپس تار شده است. سپس این سه مورد روی هم قرار گرفتند تا تصویر ترکیبی نهایی نشان داده شده در پایین سمت چپ ایجاد شود. برای تاری از VerticalBlurShader و HorizontalBlurShader استفاده کردم که در three.js گنجانده شده است، بنابراین هنوز جا برای بهینه سازی بیشتر وجود دارد.

توپ انعکاسی

بازتاب روی توپ بر اساس نمونه‌ای از three.js است. همه جهت ها از موقعیت توپ رندر شده و به عنوان نقشه های محیطی استفاده می شوند. نقشه های محیطی باید هر بار که توپ حرکت می کند به روز شوند، اما از آنجایی که به روز رسانی با سرعت 60 فریم در ثانیه بسیار فشرده است، در عوض هر سه فریم به روز می شوند. نتیجه به اندازه به روز رسانی هر فریم صاف نیست، اما تفاوت عملاً محسوس است مگر اینکه به آن اشاره شود.

سایه‌زن، سایه‌زن، سایه‌زن…

WebGL برای همه رندرها به سایه‌زن‌ها (راس‌های سایه‌زن، سایه‌زن‌های قطعه) نیاز دارد. در حالی که سایه‌زن‌های موجود در three.js در حال حاضر طیف وسیعی از افکت‌ها را امکان‌پذیر می‌کنند، نوشتن اثر خود برای سایه‌زنی و بهینه‌سازی دقیق‌تر اجتناب‌ناپذیر است. از آنجایی که World Wide Maze CPU را با موتور فیزیکی خود مشغول نگه می‌دارد، من سعی کردم به جای آن از GPU با نوشتن تا حد امکان به زبان سایه (GLSL) استفاده کنم، حتی زمانی که پردازش CPU (از طریق جاوا اسکریپت) آسان‌تر بود. اثرات موج اقیانوس به طور طبیعی به سایه زن ها متکی است، همانطور که آتش بازی در نقاط دروازه و اثر مش که هنگام ظاهر شدن توپ استفاده می شود.

توپ های سایه زن

موارد فوق مربوط به آزمایشات اثر مش است که هنگام ظاهر شدن توپ استفاده می شود. مورد سمت چپ همان مورد استفاده شده در بازی است که از 320 چند ضلعی تشکیل شده است. یکی در مرکز از حدود 5000 چند ضلعی استفاده می کند و دیگری در سمت راست از حدود 300000 چند ضلعی استفاده می کند. حتی با این تعداد چند ضلعی، پردازش با سایه زن می تواند نرخ فریم ثابت 30 فریم در ثانیه را حفظ کند.

مش سایه زن

اقلام کوچک پراکنده شده در سراسر صحنه، همه در یک شبکه یکپارچه هستند، و حرکت فردی متکی به شیدرهایی است که هر یک از نوک های چند ضلعی را حرکت می دهند. این از آزمایشی است برای اینکه ببینیم آیا عملکرد با تعداد زیادی از اشیاء موجود آسیب می بیند یا خیر. حدود 5000 شی در اینجا چیده شده است که از تقریباً 20000 چند ضلعی تشکیل شده است. عملکرد اصلا لطمه ای ندید.

poly2tri

مراحل بر اساس اطلاعات کلی دریافت شده از سرور و سپس چند ضلعی توسط جاوا اسکریپت تشکیل می شوند. مثلث‌سازی، بخش کلیدی این فرآیند، توسط three.js ضعیف اجرا می‌شود و معمولاً با شکست مواجه می‌شود. بنابراین تصمیم گرفتم خودم یک کتابخانه مثلثی متفاوت به نام poly2tri را ادغام کنم. همانطور که مشخص است، three.js آشکارا در گذشته همین کار را انجام داده بود، بنابراین من آن را به سادگی با اظهار نظر بخشی از آن انجام دادم. در نتیجه خطاها به میزان قابل توجهی کاهش یافت و مراحل قابل بازی بیشتری را امکان پذیر کرد. خطای گاه به گاه باقی می ماند و بنا به دلایلی poly2tri با صدور هشدارها خطاها را کنترل می کند، بنابراین من آن را به گونه ای تغییر دادم که به جای آن استثناهایی ایجاد کند.

poly2tri

شکل بالا نشان می دهد که چگونه طرح آبی مثلثی شده و چند ضلعی های قرمز ایجاد می شود.

فیلتر ناهمسانگرد

از آنجایی که نگاشت استاندارد MIP ایزوتروپیک تصاویر را در محورهای افقی و عمودی کوچک می کند، مشاهده چند ضلعی ها از زوایای مایل باعث می شود که بافت ها در انتهای مراحل ماز جهانی مانند بافت های افقی کشیده و با وضوح پایین به نظر برسند. تصویر بالا سمت راست در این صفحه ویکی پدیا نمونه خوبی از این موضوع را نشان می دهد. در عمل، وضوح افقی بیشتری مورد نیاز است که WebGL (OpenGL) با استفاده از روشی به نام فیلتر ناهمسانگرد آن را حل می کند. در three.js، تنظیم یک مقدار بیشتر از 1 برای THREE.Texture.anisotropy فیلتر ناهمسانگرد را فعال می کند. با این حال، این ویژگی یک افزونه است و ممکن است توسط همه GPU ها پشتیبانی نشود.

بهینه سازی کنید

همانطور که این مقاله بهترین شیوه های WebGL نیز اشاره می کند، مهم ترین راه برای بهبود عملکرد WebGL (OpenGL) به حداقل رساندن تماس های قرعه کشی است. در طول توسعه اولیه World Wide Maze، همه جزایر درون بازی، پل ها و ریل های نگهبانی اشیاء جداگانه ای بودند. این گاهی اوقات منجر به بیش از 2000 تماس قرعه کشی می شد که مراحل پیچیده را سخت می کرد. با این حال، هنگامی که من همان نوع اشیاء را در یک شبکه قرار دادم، فراخوانی‌ها به پنجاه یا بیشتر کاهش یافت و عملکرد را به طور قابل توجهی بهبود بخشید.

من از ویژگی ردیابی کروم برای بهینه سازی بیشتر استفاده کردم. نمایه‌های موجود در ابزارهای توسعه‌دهنده کروم می‌توانند زمان‌های کلی پردازش روش را تا حدی تعیین کنند، اما ردیابی می‌تواند به شما بگوید که هر قسمت چقدر طول می‌کشد، تا 1/1000 ثانیه. برای جزئیات بیشتر در مورد نحوه استفاده از ردیابی به این مقاله نگاهی بیندازید.

بهينه سازي

موارد فوق نتایج حاصل از ایجاد نقشه های محیطی برای انعکاس توپ هستند. قرار دادن console.time و console.timeEnd در مکان‌های به ظاهر مرتبط در three.js نموداری شبیه این به ما می‌دهد. زمان از چپ به راست جریان دارد و هر لایه چیزی شبیه پشته تماس است. قرار دادن یک console.time در یک console.time امکان اندازه گیری بیشتر را فراهم می کند. نمودار بالا پیش بهینه سازی و پایین پس از بهینه سازی است. همانطور که نمودار بالا نشان می دهد، updateMatrix (اگرچه کلمه کوتاه شده است) برای هر یک از رندرهای 0-5 در طول پیش بهینه سازی فراخوانی شد. من آن را طوری تغییر دادم که فقط یک بار فراخوانی شود، زیرا این فرآیند فقط زمانی لازم است که اشیا موقعیت یا جهت خود را تغییر دهند.

فرآیند ردیابی به طور طبیعی منابعی را اشغال می‌کند، بنابراین درج بیش از حد console.time می‌تواند باعث انحراف قابل‌توجهی از عملکرد واقعی شود و تعیین مناطق برای بهینه‌سازی را دشوار کند.

تنظیم کننده عملکرد

با توجه به ماهیت اینترنت، بازی احتمالا بر روی سیستم هایی با مشخصات بسیار متفاوت اجرا می شود. Find Your Way to Oz ، که در اوایل فوریه منتشر شد، از کلاسی به نام IFLAutomaticPerformanceAdjust برای کاهش افکت ها بر اساس نوسانات نرخ فریم استفاده می کند و به اطمینان از پخش روان کمک می کند. World Wide Maze بر اساس همان کلاس IFLAutomaticPerformanceAdjust ساخته شده و افکت‌ها را به ترتیب زیر کاهش می‌دهد تا گیم‌پلی را تا حد امکان روان کند:

  1. اگر نرخ فریم کمتر از 45 فریم بر ثانیه باشد، نقشه های محیطی به روز رسانی متوقف می شوند.
  2. اگر همچنان کمتر از 40 فریم در ثانیه باشد، وضوح رندر به 70٪ (50٪ نسبت سطح) کاهش می یابد.
  3. اگر باز هم کمتر از 40 فریم در ثانیه باشد، FXAA (ضد الایاسینگ) حذف می شود.
  4. اگر همچنان کمتر از 30 فریم در ثانیه باشد، جلوه های درخشندگی حذف می شوند.

نشت حافظه

حذف دقیق اشیا به نوعی دردسرساز با three.js است. اما رها کردن آنها بدیهی است که منجر به نشت حافظه می شود، بنابراین روش زیر را ابداع کردم. @renderer به THREE.WebGLRenderer اشاره دارد. (آخرین ویرایش three.js از یک روش توزیع کمی متفاوت استفاده می‌کند، بنابراین احتمالاً این روش با آن کار نمی‌کند.)

destructObjects: (object) =>
  switch true
    when object instanceof THREE.Object3D
      @destructObjects(child) for child in object.children
      object.parent?.remove(object)
      object.deallocate()
      object.geometry?.deallocate()
      @renderer.deallocateObject(object)
      object.destruct?(this)

    when object instanceof THREE.Material
      object.deallocate()
      @renderer.deallocateMaterial(object)

    when object instanceof THREE.Texture
      object.deallocate()
      @renderer.deallocateTexture(object)

    when object instanceof THREE.EffectComposer
      @destructObjects(object.copyPass.material)
      object.passes.forEach (pass) =>
        @destructObjects(pass.material) if pass.material
        @renderer.deallocateRenderTarget(pass.renderTarget) if pass.renderTarget
        @renderer.deallocateRenderTarget(pass.renderTarget1) if pass.renderTarget1
        @renderer.deallocateRenderTarget(pass.renderTarget2) if pass.renderTarget2

HTML

من شخصاً فکر می کنم بهترین چیز در مورد برنامه WebGL توانایی طراحی صفحه آرایی در HTML است. ساختن رابط های دوبعدی مانند نمایش امتیاز یا متن در Flash یا OpenFrameworks (OpenGL) نوعی دردسر است. فلش حداقل یک IDE دارد، اما openFrameworks اگر به آن عادت نداشته باشید سخت است (استفاده از چیزی مانند Cocos2D ممکن است کار را آسان‌تر کند). از طرف دیگر HTML امکان کنترل دقیق تمام جنبه های طراحی ظاهری با CSS را فراهم می کند، درست مانند هنگام ساخت وب سایت. اگرچه اثرات پیچیده ای مانند متراکم شدن ذرات در یک لوگو غیرممکن است، برخی از جلوه های سه بعدی با قابلیت های CSS Transforms امکان پذیر است. جلوه‌های متنی «GOAL» و «TIME IS UP» World Wide Maze با استفاده از مقیاس در CSS Transition (پیاده‌شده با Transit ) متحرک می‌شوند. (بدیهی است که درجه بندی پس زمینه از WebGL استفاده می کند.)

هر صفحه در بازی (عنوان، RESULT، RANKING، و غیره) فایل HTML مخصوص به خود را دارد، و هنگامی که آنها به عنوان الگو بارگذاری شدند، $(document.body).append() با مقادیر مناسب در زمان مناسب فراخوانی می شود. . یک مشکل این بود که رویدادهای ماوس و صفحه کلید را نمی‌توان قبل از الحاق تنظیم کرد، بنابراین تلاش برای el.click (e) -> console.log(e) قبل از الحاق کارساز نبود.

بین المللی سازی (i18n)

کار در HTML برای ایجاد نسخه انگلیسی زبان نیز راحت بود. من استفاده از i18next را انتخاب کردم، یک کتابخانه وب i18n، برای نیازهای بین المللی خود، که توانستم بدون تغییر از آن استفاده کنم.

ویرایش و ترجمه متن درون بازی در صفحه گسترده Google Docs انجام شد. از آنجایی که i18next به فایل های JSON نیاز دارد، من صفحات گسترده را به TSV صادر کردم و سپس آنها را با یک مبدل سفارشی تبدیل کردم. من درست قبل از انتشار به‌روزرسانی‌های زیادی انجام دادم، بنابراین خودکار کردن فرآیند صادرات از Google Docs Spreadsheet کارها را بسیار آسان‌تر می‌کرد.

ویژگی ترجمه خودکار کروم نیز به طور معمول عمل می کند زیرا صفحات با HTML ساخته شده اند. با این حال، گاهی اوقات نمی تواند زبان را به درستی تشخیص دهد، در عوض آن را با زبانی کاملاً متفاوت (مثلاً ویتنامی) اشتباه می گیرد، بنابراین این ویژگی در حال حاضر غیرفعال است. ( با استفاده از متا تگ می توان آن را غیرفعال کرد .)

RequireJS

من RequireJS را به عنوان سیستم ماژول جاوا اسکریپت انتخاب کردم. 10000 خط کد منبع بازی به حدود 60 کلاس (= فایل های قهوه) تقسیم شده و در فایل های js مجزا کامپایل شده است. RequireJS این فایل های فردی را به ترتیب مناسب بر اساس وابستگی بارگیری می کند.

define ->
  class Hoge
    hogeMethod: ->

کلاس تعریف شده در بالا (hoge.coffee) را می توان به صورت زیر استفاده کرد:

define ['hoge'], (Hoge) ->
  class Moge
    constructor: ->
      @hoge = new Hoge()
      @hoge.hogeMethod()

برای کار کردن، hoge.js باید قبل از moge.js بارگیری شود، و از آنجایی که "hoge" به عنوان اولین آرگومان "define" تعیین شده است، hoge.js همیشه ابتدا بارگیری می شود (پس از بارگیری hoge.js دوباره فراخوانی می شود). این مکانیسم AMD نامیده می‌شود و هر کتابخانه شخص ثالثی تا زمانی که از AMD پشتیبانی می‌کند، می‌تواند برای همان نوع تماس مجدد استفاده شود. حتی آنهایی که این کار را نمی کنند (به عنوان مثال، three.js) تا زمانی که d وابستگی ها از قبل مشخص شده باشند، عملکرد مشابهی خواهند داشت.

این شبیه به واردات AS3 است، بنابراین نباید آنقدرها عجیب به نظر برسد. اگر در نهایت با فایل های وابسته بیشتری مواجه شدید، این یک راه حل ممکن است.

r.js

RequireJS شامل یک بهینه ساز به نام r.js است. این js اصلی را با همه فایل‌های js وابسته در یک دسته جمع می‌کند، سپس با استفاده از UglifyJS (یا Closure Compiler) آن را کوچک می‌کند. این باعث کاهش تعداد فایل‌ها و کل داده‌هایی می‌شود که مرورگر برای بارگیری نیاز دارد. حجم کل فایل جاوا اسکریپت برای World Wide Maze حدود 2 مگابایت است و با بهینه سازی r.js می توان آن را به حدود 1 مگابایت کاهش داد. اگر بازی را بتوان با استفاده از gzip توزیع کرد، این حجم به 250 کیلوبایت کاهش می یابد. (GAE مشکلی دارد که اجازه انتقال فایل‌های gzip 1 مگابایتی یا بزرگتر را نمی‌دهد، بنابراین بازی در حال حاضر بدون فشرده‌سازی به‌عنوان 1 مگابایت متن ساده توزیع می‌شود.)

سازنده صحنه

داده های مرحله به صورت زیر تولید می شوند و به طور کامل بر روی سرور GCE در ایالات متحده انجام می شوند:

  1. آدرس وب سایت برای تبدیل به مرحله از طریق WebSocket ارسال می شود.
  2. PhantomJS یک اسکرین شات می گیرد و موقعیت های تگ div و img بازیابی شده و با فرمت JSON خروجی می گیرند.
  3. بر اساس اسکرین شات از مرحله 2 و داده های موقعیت یابی عناصر HTML، یک برنامه سفارشی C++ (OpenCV، Boost) نواحی غیر ضروری را حذف می کند، جزیره ها را تولید می کند، جزیره ها را با پل ها به هم متصل می کند، موقعیت های ریل نگهبان و آیتم ها را محاسبه می کند، نقطه هدف را تعیین می کند و غیره. نتایج با فرمت JSON خارج شده و به مرورگر بازگردانده می شود.

فانتوم جی اس

PhantomJS مرورگری است که نیازی به صفحه نمایش ندارد. این می تواند صفحات وب را بدون باز کردن پنجره بارگیری کند، بنابراین می توان از آن در تست های خودکار یا گرفتن اسکرین شات در سمت سرور استفاده کرد. موتور مرورگر آن WebKit است، همان موتوری که کروم و سافاری از آن استفاده می‌کنند، بنابراین طرح‌بندی و نتایج اجرای جاوا اسکریپت نیز کم و بیش مشابه نتایج مرورگرهای استاندارد است.

با PhantomJS، جاوا اسکریپت یا کافی اسکریپت برای نوشتن فرآیندهایی که می خواهید اجرا شوند استفاده می شود. همانطور که در این نمونه نشان داده شده است، گرفتن اسکرین شات بسیار آسان است. من روی یک سرور لینوکس (CentOS) کار می‌کردم، بنابراین باید فونت‌هایی را برای نمایش ژاپنی ( M+ FONTS ) نصب کنم. حتی در این صورت، رندر فونت به طور متفاوتی نسبت به سیستم عامل ویندوز یا مک انجام می شود، بنابراین همان فونت می تواند در ماشین های دیگر متفاوت به نظر برسد (هر چند تفاوت حداقل است).

بازیابی موقعیت های تگ img و div اساساً مانند صفحات استاندارد انجام می شود. jQuery نیز بدون هیچ مشکلی قابل استفاده است.

صحنه_ساز

من در ابتدا استفاده از یک رویکرد مبتنی بر DOM را برای تولید مراحل (شبیه به بازرس 3 بعدی فایرفاکس ) در نظر گرفتم و چیزی شبیه به تجزیه و تحلیل DOM در PhantomJS انجام دادم. با این حال، در پایان، من روی یک رویکرد پردازش تصویر مستقر شدم. برای این منظور یک برنامه C++ نوشتم که از OpenCV و Boost به نام "stage_builder" استفاده می کند. موارد زیر را انجام می دهد:

  1. اسکرین شات و فایل(های) JSON را بارگیری می کند.
  2. تصاویر و متن را به "جزایر" تبدیل می کند.
  3. پل هایی را برای اتصال جزایر ایجاد می کند.
  4. پل های غیر ضروری را برای ایجاد پیچ ​​و خم حذف می کند.
  5. اقلام بزرگ را قرار می دهد.
  6. وسایل کوچک را قرار می دهد.
  7. نرده های محافظ را قرار می دهد.
  8. داده های موقعیت یابی را در قالب JSON خروجی می دهد.

هر مرحله به تفصیل در زیر آمده است.

بارگیری اسکرین شات و فایل(های) JSON

از cv::imread معمولی برای بارگیری اسکرین شات استفاده می شود. چندین کتابخانه را برای فایل های JSON آزمایش کردم، اما picojson ساده ترین کار به نظر می رسید.

تبدیل تصاویر و متن به "جزیره"

ساخت صحنه

تصویر بالا یک اسکرین شات از بخش اخبار سایت aid-dcc.com است (برای مشاهده اندازه واقعی کلیک کنید). تصاویر و عناصر متن باید به جزیره تبدیل شوند. برای جداسازی این بخش‌ها، باید رنگ پس‌زمینه سفید - به عبارت دیگر رایج‌ترین رنگ در تصویر را حذف کنیم. پس از انجام این کار به نظر می رسد:

ساخت صحنه

بخش های سفید جزایر بالقوه هستند.

متن خیلی ظریف و واضح است، بنابراین آن را با cv::dilate ، cv::GaussianBlur و cv::threshold ضخیم می کنیم. محتوای تصویر نیز وجود ندارد، بنابراین بر اساس خروجی داده تگ img از PhantomJS، آن مناطق را با رنگ سفید پر می کنیم. تصویر حاصل به این صورت است:

ساخت صحنه

اکنون متن توده های مناسبی را تشکیل می دهد و هر تصویر یک جزیره مناسب است.

ایجاد پل هایی برای اتصال جزایر

پس از آماده شدن جزایر، با پل هایی به هم متصل می شوند. هر جزیره به دنبال جزایر مجاور چپ، راست، بالا و پایین می‌گردد، سپس یک پل را به نزدیک‌ترین نقطه نزدیک‌ترین جزیره متصل می‌کند و نتیجه‌ای شبیه به این است:

ساخت صحنه

از بین بردن پل های غیر ضروری برای ایجاد یک پیچ و خم

نگه داشتن تمام پل ها باعث می شود که حرکت در صحنه بسیار آسان شود، بنابراین برای ایجاد یک پیچ و خم باید برخی از آنها را حذف کرد. یک جزیره (به عنوان مثال، یکی در بالا سمت چپ) به عنوان نقطه شروع انتخاب می شود، و همه به جز یک پل (به طور تصادفی انتخاب شده) که به آن جزیره متصل می شوند حذف می شوند. سپس همین کار را برای جزیره بعدی که با پل باقی مانده متصل می شود انجام می شود. هنگامی که مسیر به بن‌بست می‌رسد یا به جزیره‌ای که قبلاً بازدید کرده‌اید منتهی می‌شود، به نقطه‌ای برمی‌گردد که امکان دسترسی به جزیره جدید را فراهم می‌کند. زمانی که تمام جزایر به این روش پردازش شوند، پیچ و خم تکمیل می شود.

ساخت صحنه

قرار دادن وسایل بزرگ

یک یا چند آیتم بزرگ در هر جزیره بسته به ابعاد آن قرار داده می شود و از نقاط دورتر از لبه های جزیره انتخاب می شود. اگرچه خیلی واضح نیست، اما این نکات با رنگ قرمز در زیر نشان داده شده اند:

ساخت صحنه

از بین تمام این نقاط ممکن، نقطه ای که در بالا سمت چپ قرار دارد به عنوان نقطه شروع (دایره قرمز)، نقطه پایین سمت راست به عنوان هدف (دایره سبز) و حداکثر شش مورد از بقیه برای بزرگ انتخاب می شود. قرار دادن آیتم (دایره بنفش).

قرار دادن وسایل کوچک

ساخت صحنه

تعداد مناسبی از اقلام کوچک در امتداد خطوط در فواصل تعیین شده از لبه های جزیره قرار می گیرند. تصویر بالا (نه از aid-dcc.com) خطوط قرارگیری پیش بینی شده را به رنگ خاکستری، افست و در فواصل منظم از لبه های جزیره نشان می دهد. نقاط قرمز نشان می دهد که اقلام کوچک در کجا قرار می گیرند. از آنجایی که این تصویر مربوط به یک نسخه متوسط ​​​​است، موارد در خطوط مستقیم قرار گرفته اند، اما نسخه نهایی موارد را کمی نامنظم تر به دو طرف خطوط خاکستری پراکنده می کند.

قرار دادن نرده های محافظ

نرده‌های محافظ اساساً در امتداد مرزهای بیرونی جزایر قرار می‌گیرند، اما باید در پل‌ها قطع شوند تا امکان دسترسی فراهم شود. کتابخانه Boost Geometry برای این کار مفید بود و محاسبات هندسی مانند تعیین محل تلاقی داده های مرز جزیره با خطوط دو طرف پل را ساده کرد.

ساخت صحنه

خطوط سبزی که جزایر را مشخص می کنند، نرده های محافظ هستند. شاید دیدن آن در این تصویر دشوار باشد، اما هیچ خط سبزی در محل پل ها وجود ندارد. این تصویر نهایی مورد استفاده برای اشکال زدایی است، که در آن تمام اشیایی که باید به JSON خروجی شوند گنجانده شده است. نقاط آبی روشن موارد کوچکی هستند و نقاط خاکستری نقاط شروع مجدد پیشنهادی هستند. هنگامی که توپ به اقیانوس می افتد، بازی از نزدیکترین نقطه شروع مجدد از سر گرفته می شود. نقاط شروع مجدد کم و بیش به همان روشی که اقلام کوچک هستند، در فواصل زمانی معین در فاصله مشخصی از لبه جزیره چیده می شوند.

خروجی داده های موقعیت یابی در فرمت JSON

من از picojson برای خروجی نیز استفاده کردم. داده ها را در خروجی استاندارد می نویسد، که سپس توسط تماس گیرنده (Node.js) دریافت می شود.

ایجاد یک برنامه C++ در مک برای اجرا در لینوکس

این بازی بر روی Mac توسعه داده شد و در لینوکس مستقر شد، اما از آنجایی که OpenCV و Boost برای هر دو سیستم عامل وجود داشت، پس از ایجاد محیط کامپایل، توسعه خود دشوار نبود. من از Command Line Tools در Xcode برای اشکال زدایی بیلد در مک استفاده کردم، سپس یک فایل پیکربندی با استفاده از automake/autoconf ایجاد کردم تا بیلد بتواند در لینوکس کامپایل شود. سپس مجبور شدم از "configure && make" در لینوکس برای ایجاد فایل اجرایی استفاده کنم. من به دلیل تفاوت در نسخه کامپایلر با برخی از اشکالات خاص لینوکس مواجه شدم اما توانستم آنها را به راحتی با استفاده از gdb حل کنم.

نتیجه

بازی مانند این را می توان با Flash یا Unity ایجاد کرد که مزایای زیادی را به همراه خواهد داشت. با این حال، این نسخه به هیچ پلاگینی نیاز ندارد و ویژگی های طرح بندی HTML5 + CSS3 بسیار قدرتمند است. قطعاً داشتن ابزار مناسب برای هر کار مهم است. من شخصاً از اینکه چگونه بازی برای یک بازی کاملاً در HTML5 ساخته شده بود شگفت زده شدم، و اگرچه هنوز در بسیاری از زمینه ها کمبود دارد، من مشتاقانه منتظرم تا ببینم در آینده چگونه توسعه می یابد.