مطالعه موردی - ماوس فنری

معرفی

ماوس فنری

پس از انتشار Bouncy Mouse در iOS و Android در پایان سال گذشته، چند درس بسیار مهم آموختم. کلیدی در میان آنها این بود که ورود به یک بازار تثبیت شده سخت است. در بازار کاملاً اشباع آیفون، جلب توجه بسیار سخت بود. در بازار اندروید کمتر اشباع شده، پیشرفت آسان‌تر بود، اما همچنان آسان نبود. با توجه به این تجربه، فرصت جالبی را در فروشگاه وب کروم دیدم. در حالی که فروشگاه وب به هیچ وجه خالی نیست، کاتالوگ بازی‌های با کیفیت بالای مبتنی بر HTML5 به تازگی شروع به رشد کرده است. برای یک توسعه‌دهنده برنامه جدید، این به این معنی است که ایجاد نمودارهای رتبه‌بندی و دیده شدن بسیار آسان‌تر است. با در نظر گرفتن این فرصت، تصمیم گرفتم ماوس Bouncy را به HTML5 منتقل کنم، به این امید که بتوانم آخرین تجربه بازی خود را به یک پایگاه کاربر جدید هیجان انگیز ارائه دهم. در این مطالعه موردی، من کمی در مورد فرآیند کلی انتقال Bouncy Mouse به HTML5 صحبت خواهم کرد، سپس کمی عمیق‌تر در سه حوزه که جالب بود: صدا، عملکرد و درآمدزایی را بررسی خواهم کرد.

انتقال یک بازی C++ به HTML5

Bouncy Mouse در حال حاضر در Android (C++)، iOS (C++)، Windows Phone 7 (C#) و Chrome (Javascript) در دسترس است. این گهگاه این سوال را ایجاد می کند: چگونه می توان یک بازی بنویسد که به راحتی به چندین پلتفرم منتقل شود؟ من این احساس را دارم که مردم به یک گلوله جادویی امیدوارند که بتوانند از آن برای دستیابی به این سطح از قابلیت حمل و نقل بدون توسل به درگاه دستی استفاده کنند. متأسفانه، من هنوز مطمئن نیستم که چنین راه حلی وجود داشته باشد (نزدیک ترین چیز احتمالاً فریمورک PlayN Google یا موتور Unity است، اما هیچ یک از اینها به تمام اهداف مورد علاقه من نمی رسد). رویکرد من در واقع یک پورت دستی بود. من ابتدا نسخه iOS/Android را در C++ نوشتم، سپس این کد را به هر پلتفرم جدید منتقل کردم. در حالی که ممکن است کار زیادی به نظر برسد، نسخه‌های WP7 و Chrome هر کدام بیش از ۲ هفته طول نکشید تا تکمیل شوند. حال سوال اینجاست که آیا می توان کاری کرد که یک کد پایه به راحتی قابل حمل با دست باشد؟ چند کار بود که من انجام دادم که به این امر کمک کرد:

Codebase را کوچک نگه دارید

اگرچه این ممکن است بدیهی به نظر برسد، اما واقعاً دلیل اصلی این است که من توانستم بازی را به سرعت پورت کنم. کد مشتری Bouncy Mouse فقط حدود 7000 خط C++ است. 7000 خط کد چیزی نیست، اما آنقدر کوچک است که قابل مدیریت باشد. هر دو نسخه سی شارپ و جاوا اسکریپت کد کلاینت تقریباً یک اندازه بودند. کوچک نگه داشتن پایگاه کد من اساساً به دو روش کلیدی تبدیل شد: کد اضافی ننویسید و تا حد امکان در کدهای پیش پردازش (غیر زمان اجرا) انجام دهید. ننوشتن کد اضافی ممکن است بدیهی به نظر برسد، اما این چیزی است که همیشه با خودم می جنگم. من اغلب این اصرار را دارم که برای هر چیزی که می تواند به عنوان کمک کننده در نظر گرفته شود، یک کلاس/تابع کمکی بنویسم. با این حال، مگر اینکه واقعاً قصد داشته باشید چندین بار از یک کمک کننده استفاده کنید، معمولاً کد شما را متورم می کند. با Bouncy Mouse، مراقب بودم که هرگز کمکی ننویسم مگر اینکه حداقل سه بار از آن استفاده کنم. وقتی یک کلاس کمکی نوشتم، سعی کردم آن را تمیز، قابل حمل و قابل استفاده مجدد برای پروژه های آینده خود کنم. از سوی دیگر، هنگام نوشتن کد فقط برای Bouncy Mouse، با احتمال کم استفاده مجدد، تمرکز من این بود که کار کدنویسی را به سادگی و سریع ترین زمان ممکن انجام دهم، حتی اگر این "زیباترین" راه برای نوشتن نباشد. کد بخش دوم و مهم‌تر کوچک نگه‌داشتن پایگاه کد این بود که تا حد امکان به مراحل پیش‌پردازش فشار وارد کنید. اگر بتوانید یک کار در زمان اجرا را انجام دهید و آن را به یک کار پیش پردازش منتقل کنید، نه تنها بازی شما سریعتر اجرا می شود، بلکه لازم نیست کد را به هر پلتفرم جدید پورت کنید. برای مثال، من در ابتدا داده‌های هندسه سطح خود را به عنوان یک قالب نسبتاً پردازش نشده ذخیره کردم و بافرهای رأس OpenGL/WebGL واقعی را در زمان اجرا جمع‌آوری کردم. این کار کمی تنظیمات و چند صد خط کد زمان اجرا نیاز داشت. بعداً، این کد را به مرحله پیش پردازش انتقال دادم، و در زمان کامپایل، بافرهای راس OpenGL/WebGL کاملاً بسته بندی شده را نوشتم. مقدار واقعی کد تقریباً یکسان بود، اما آن چند صد خط به مرحله پیش پردازش منتقل شده بودند، به این معنی که هرگز مجبور نبودم آنها را به هیچ پلتفرم جدیدی پورت کنم. نمونه‌های زیادی از این در Bouncy Mouse وجود دارد، و آنچه ممکن است از بازی به بازی دیگر متفاوت است، اما فقط مراقب هر چیزی باشید که نیازی نیست در زمان اجرا اتفاق بیفتد.

وابستگی هایی را که به آن نیاز ندارید نپذیرید

یکی دیگر از دلایلی که Bouncy Mouse برای پورت کردن آسان است این است که تقریباً هیچ وابستگی ندارد. نمودار زیر وابستگی های اصلی کتابخانه Bouncy Mouse را در هر پلتفرم خلاصه می کند:

اندروید iOS HTML5 WP7
گرافیک OpenGL ES OpenGL ES WebGL XNA
صدا OpenSL ES OpenAL صوتی وب XNA
فیزیک Box2D Box2D Box2D.js Box2D.xna

تقریباً همین است. از هیچ کتابخانه بزرگ شخص ثالثی استفاده نشد، به جز Box2D ، که در تمام پلتفرم ها قابل حمل است. برای گرافیک، نقشه WebGL و XNA تقریباً 1:1 با OpenGL است، بنابراین این مشکل بزرگی نبود. فقط در حوزه صدا، کتابخانه های واقعی متفاوت بودند. با این حال، کد صدا در Bouncy Mouse کوچک است (حدود صد خط کد مخصوص پلتفرم)، بنابراین این مشکل بزرگی نبود. خالی نگه داشتن Bouncy Mouse از کتابخانه های بزرگ غیر قابل حمل به این معنی است که منطق کد زمان اجرا می تواند بین نسخه ها تقریباً یکسان باشد (با وجود تغییر زبان). علاوه بر این ما را از قفل شدن در یک زنجیره ابزار غیرقابل حمل نجات می دهد. از من پرسیده شده که آیا کدنویسی در برابر OpenGL/WebGL به طور مستقیم باعث افزایش پیچیدگی در مقایسه با استفاده از کتابخانه‌هایی مانند Cocos2D یا Unity می‌شود (برخی از کمک‌کنندگان WebGL نیز وجود دارد). در واقع، من برعکس اعتقاد دارم. اکثر بازی های تلفن همراه / HTML5 (حداقل آنهایی مانند Bouncy Mouse) بسیار ساده هستند. در بیشتر موارد، بازی فقط چند اسپرایت و شاید هندسه بافتی را ترسیم می کند. مجموع کدهای اختصاصی OpenGL در Bouncy Mouse احتمالا کمتر از 1000 خط است. اگر استفاده از یک کتابخانه کمکی واقعاً این تعداد را کاهش دهد، تعجب خواهم کرد. حتی اگر این تعداد را به نصف کاهش دهد، باید زمان قابل توجهی را صرف یادگیری کتابخانه ها/ابزارهای جدید صرف کنم تا 500 خط کد را ذخیره کنم. علاوه بر این، من هنوز یک کتابخانه کمکی قابل حمل در تمام پلتفرم‌هایی که به آن‌ها علاقه‌مندم پیدا نکرده‌ام، بنابراین استفاده از چنین وابستگی به قابل‌توجهی به قابلیت حمل آسیب می‌زند. اگر من یک بازی سه بعدی می نوشتم که به نقشه های نوری، LOD پویا، انیمیشن پوسته شده و غیره نیاز داشت، قطعاً پاسخ من تغییر می کرد. در این مورد، من دوباره چرخ را اختراع می‌کنم تا سعی کنم کل موتورم را در برابر OpenGL رمزگذاری کنم. منظور من در اینجا این است که بیشتر بازی‌های موبایل/HTML5 (هنوز) در این دسته قرار نمی‌گیرند، بنابراین قبل از اینکه لازم باشد، نیازی به پیچیده‌تر کردن مسائل نیست.

شباهت های بین زبان ها را دست کم نگیرید

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

نتیجه گیری انتقال

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

سمعی

یکی از زمینه هایی که برای من (و ظاهراً همه افراد دیگر) دردسر ایجاد کرد، صدا بود. در iOS و Android، تعدادی از گزینه های صوتی جامد در دسترس هستند (OpenSL، OpenAL)، اما در دنیای HTML5، همه چیز بدتر به نظر می رسید. در حالی که HTML5 Audio در دسترس است، متوجه شدم که هنگام استفاده در بازی ها مشکلاتی برای شکستن معامله دارد. حتی در جدیدترین مرورگرها، اغلب با رفتارهای عجیب و غریب مواجه می شدم. برای مثال، به نظر می‌رسد کروم محدودیتی در تعداد عناصر صوتی ( منبع ) هم‌زمان که می‌توانید ایجاد کنید دارد. علاوه بر این، حتی زمانی که صدا پخش می‌شد، گاهی اوقات به طور غیرقابل توضیحی تحریف می‌شد. در کل کمی نگران بودم. جستجوی آنلاین نشان داد که تقریباً همه مشکل مشابهی دارند. راه حلی که در ابتدا به آن رسیدم یک API به نام SoundManager2 بود. این API در صورت موجود بودن از HTML5 Audio استفاده می‌کند و در موقعیت‌های دشوار به Flash بازمی‌گردد. در حالی که این راه حل کار می کرد، هنوز هم باگ و غیرقابل پیش بینی بود (فقط کمتر از صدای خالص HTML5). یک هفته پس از راه‌اندازی، با برخی از افراد مفید در Google صحبت کردم که به من در Webkit's Web Audio API اشاره کردند. من در ابتدا به استفاده از این API فکر کرده بودم، اما به دلیل پیچیدگی غیر ضروری (برای من) که به نظر می رسید API از آن اجتناب کرده بودم. من فقط می خواستم چند صدا را پخش کنم: با HTML5 Audio این مقدار به چند خط جاوا اسکریپت می رسد. با این حال، در نگاه کوتاهی که به Web Audio داشتم، از مشخصات عظیم آن (70 صفحه)، تعداد کمی نمونه در وب (معمولی برای یک API جدید)، و حذف «پخش»، «مکث» شگفت زده شدم. ، یا عملکرد "توقف" در هر نقطه از مشخصات. با تضمین گوگل مبنی بر اینکه نگرانی‌های من به خوبی پایه‌گذاری نشده است، دوباره به API پرداختم. پس از بررسی چند نمونه دیگر و کمی تحقیق بیشتر، متوجه شدم که گوگل درست می‌گوید – API قطعاً می‌تواند نیازهای من را برآورده کند، و می‌تواند این کار را بدون اشکالاتی که سایر APIها را آزار می‌دهند، انجام دهد. مقاله شروع به کار با Web Audio API بسیار مفید است، که اگر می‌خواهید درک عمیق‌تری از API کسب کنید، مکان بسیار خوبی برای رفتن است. مشکل واقعی من این است که حتی پس از درک و استفاده از API، همچنان به نظر من مانند یک API است که برای "تنها پخش چند صدا" طراحی نشده است. برای دور زدن این تردید، یک کلاس کمکی کوچک نوشتم که به من اجازه داد از API درست همانطور که می‌خواستم استفاده کنم - پخش، مکث، توقف، و پرس و جو از وضعیت یک صدا. من این کلاس کمکی را AudioClip نامیدم. منبع کامل در GitHub تحت مجوز Apache 2.0 در دسترس است و من در مورد جزئیات کلاس در زیر بحث خواهم کرد. اما ابتدا، برخی پس‌زمینه‌های Web Audio API:

نمودارهای صوتی وب

اولین چیزی که Web Audio API را پیچیده‌تر (و قدرتمندتر) از عنصر HTML5 Audio می‌کند، توانایی آن در پردازش / ترکیب صدا قبل از خروجی آن برای کاربر است. در حالی که قدرتمند است، این واقعیت که هر پخش صوتی شامل یک نمودار است، کارها را در سناریوهای ساده کمی پیچیده‌تر می‌کند. برای نشان دادن قدرت Web Audio API، نمودار زیر را در نظر بگیرید:

نمودار پایه صوتی وب
نمودار پایه صوتی وب

در حالی که مثال بالا قدرت Web Audio API را نشان می دهد، من در سناریوی خود به بیشتر این قدرت نیاز نداشتم. فقط میخواستم یه صدا بزنم در حالی که این هنوز به یک نمودار نیاز دارد، نمودار بسیار ساده است.

نمودارها می توانند ساده باشند

اولین چیزی که Web Audio API را پیچیده‌تر (و قدرتمندتر) از عنصر HTML5 Audio می‌کند، توانایی آن در پردازش / ترکیب صدا قبل از خروجی آن برای کاربر است. در حالی که قدرتمند است، این واقعیت که هر پخش صوتی شامل یک نمودار است، کارها را در سناریوهای ساده کمی پیچیده‌تر می‌کند. برای نشان دادن قدرت Web Audio API، نمودار زیر را در نظر بگیرید:

نمودار صوتی وب بی اهمیت
نمودار صوتی وب بی اهمیت

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

اما بیایید حتی نگران نمودار نباشیم

در حالی که درک نمودار خوب است، این چیزی نیست که بخواهم هر بار که صدایی را پخش می کنم با آن مقابله کنم. بنابراین، من یک کلاس بسته بندی ساده "AudioClip" نوشتم. این کلاس این نمودار را به صورت داخلی مدیریت می کند، اما یک API بسیار ساده تر برای کاربر ارائه می دهد.

کلیپ صوتی
کلیپ صوتی

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

// At startup time
var sound = new AudioClip("ping.wav");

// Later
sound.play();

جزئیات پیاده سازی

بیایید نگاهی گذرا به کد کلاس کمکی بیندازیم: سازنده - سازنده بارگذاری داده های صوتی را با استفاده از XHR انجام می دهد. اگرچه در اینجا نشان داده نشده است (برای ساده نگه داشتن مثال)، یک عنصر صوتی HTML5 نیز می تواند به عنوان گره منبع استفاده شود. این به ویژه برای نمونه های بزرگ مفید است. توجه داشته باشید که Web Audio API ایجاب می کند که این داده ها را به عنوان یک "آرایه بافر" واکشی کنیم. پس از دریافت داده ها، یک بافر Web Audio از این داده ها ایجاد می کنیم (آن را از فرمت اصلی خود به فرمت PCM زمان اجرا رمزگشایی می کنیم).

/**
* Create a new AudioClip object from a source URL. This object can be played,
* paused, stopped, and resumed, like the HTML5 Audio element.
*
* @constructor
* @param {DOMString} src
* @param {boolean=} opt_autoplay
* @param {boolean=} opt_loop
*/
AudioClip = function(src, opt_autoplay, opt_loop) {
// At construction time, the AudioClip is not playing (stopped),
// and has no offset recorded.
this.playing_ = false;
this.startTime_ = 0;
this.loop_ = opt_loop ? true : false;

// State to handle pause/resume, and some of the intricacies of looping.
this.resetTimout_ = null;
this.pauseTime_ = 0;

// Create an XHR to load the audio data.
var request = new XMLHttpRequest();
request.open("GET", src, true);
request.responseType = "arraybuffer";

var sfx = this;
request.onload = function() {
// When audio data is ready, we create a WebAudio buffer from the data.
// Using decodeAudioData allows for async audio loading, which is useful
// when loading longer audio tracks (music).
AudioClip.context.decodeAudioData(request.response, function(buffer) {
    sfx.buffer_ = buffer;
    
    if (opt_autoplay) {
    sfx.play();
    }
});
}

request.send();
}

پخش - پخش صدای ما شامل دو مرحله است: تنظیم نمودار پخش، و فراخوانی نسخه ای از "noteOn" در منبع نمودار. یک منبع فقط یک بار قابل پخش است، بنابراین باید هر بار که بازی می کنیم منبع/گراف را دوباره ایجاد کنیم. بیشتر پیچیدگی این تابع ناشی از الزامات مورد نیاز برای از سرگیری یک کلیپ متوقف شده است ( this.pauseTime_ > 0 ). برای از سرگیری پخش یک کلیپ متوقف شده، از noteGrainOn استفاده می کنیم که امکان پخش یک منطقه فرعی از بافر را فراهم می کند. متأسفانه noteGrainOn برای این سناریو به شکل دلخواه با حلقه کردن ارتباط برقرار نمی کند (منطقه فرعی را حلقه می کند، نه کل بافر). بنابراین، ما باید با پخش باقیمانده کلیپ با noteGrainOn ، آن را حل کنیم، سپس کلیپ را از ابتدا با فعال کردن حلقه شروع کنیم.

/**
* Recreates the audio graph. Each source can only be played once, so
* we must recreate the source each time we want to play.
* @return {BufferSource}
* @param {boolean=} loop
*/
AudioClip.prototype.createGraph = function(loop) {
var source = AudioClip.context.createBufferSource();
source.buffer = this.buffer_;
source.connect(AudioClip.context.destination);

// Looping is handled by the Web Audio API.
source.loop = loop;

return source;
}

/**
* Plays the given AudioClip. Clips played in this manner can be stopped
* or paused/resumed.
*/
AudioClip.prototype.play = function() {
if (this.buffer_ && !this.isPlaying()) {
// Record the start time so we know how long we've been playing.
this.startTime_ = AudioClip.context.currentTime;
this.playing_ = true;
this.resetTimeout_ = null;

// If the clip is paused, we need to resume it.
if (this.pauseTime_ > 0) {
    // We are resuming a clip, so it's current playback time is not correctly
    // indicated by startTime_. Correct this by subtracting pauseTime_.
    this.startTime_ -= this.pauseTime_;
    var remainingTime = this.buffer_.duration - this.pauseTime_;

    if (this.loop_) {
    // If the clip is paused and looping, we need to resume the clip
    // with looping disabled. Once the clip has finished, we will re-start
    // the clip from the beginning with looping enabled
    this.source_ = this.createGraph(false);
    this.source_.noteGrainOn(0, this.pauseTime_, remainingTime)

    // Handle restarting the playback once the resumed clip has completed.
    // *Note that setTimeout is not the ideal method to use here. A better 
    // option would be to handle timing in a more predictable manner,
    // such as tying the update to the game loop.
    var clip = this;
    this.resetTimeout_ = setTimeout(function() { clip.stop(); clip.play() },
                                    remainingTime * 1000);
    } else {
    // Paused non-looping case, just create the graph and play the sub-
    // region using noteGrainOn.
    this.source_ = this.createGraph(this.loop_);
    this.source_.noteGrainOn(0, this.pauseTime_, remainingTime);
    }

    this.pauseTime_ = 0;
} else {
    // Normal case, just creat the graph and play.
    this.source_ = this.createGraph(this.loop_);
    this.source_.noteOn(0);
}
}
}

پخش به عنوان جلوه صوتی - عملکرد پخش بالا اجازه نمی دهد که کلیپ صوتی چندین بار با همپوشانی پخش شود (بازپخش دوم فقط زمانی امکان پذیر است که کلیپ تمام شده یا متوقف شود). گاهی اوقات یک بازی می خواهد یک صدا را چندین بار بدون انتظار برای تکمیل هر پخش (جمع آوری سکه در یک بازی و غیره) پخش کند. برای فعال کردن این، کلاس AudioClip یک متد playAsSFX() دارد. از آنجایی که چندین پخش می‌توانند به طور همزمان انجام شوند، پخش از playAsSFX() با AudioClip 1:1 محدود نمی‌شود. بنابراین، پخش را نمی توان متوقف کرد، مکث کرد یا برای وضعیت پرس و جو کرد. حلقه زدن نیز غیرفعال است، زیرا هیچ راهی برای متوقف کردن صدای حلقه ای که به این روش پخش می شود وجود ندارد.

/**
* Plays the given AudioClip as a sound effect. Sound Effects cannot be stopped
* or paused/resumed, but can be played multiple times with overlap.
* Additionally, sound effects cannot be looped, as there is no way to stop
* them. This method of playback is best suited to very short, one-off sounds.
*/
AudioClip.prototype.playAsSFX = function() {
if (this.buffer_) {
var source = this.createGraph(false);
source.noteOn(0);
}
}

حالت توقف، مکث و پرس و جو – بقیه توابع کاملاً مستقیم هستند و نیازی به توضیح زیادی ندارند:

/**
* Stops an AudioClip , resetting its seek position to 0.
*/
AudioClip.prototype.stop = function() {
if (this.playing_) {
this.source_.noteOff(0);
this.playing_ = false;
this.startTime_ = 0;
this.pauseTime_ = 0;
if (this.resetTimeout_ != null) {
    clearTimeout(this.resetTimeout_);
}
}
}

/**
* Pauses an AudioClip. The offset into the stream is recorded to allow the
* clip to be resumed later.
*/
AudioClip.prototype.pause = function() {
if (this.playing_) {
this.source_.noteOff(0);
this.playing_ = false;
this.pauseTime_ = AudioClip.context.currentTime - this.startTime_;
this.pauseTime_ = this.pauseTime_ % this.buffer_.duration;
this.startTime_ = 0;
if (this.resetTimeout_ != null) {
    clearTimeout(this.resetTimeout_);
}
}
}

/**
* Indicates whether the sound is playing.
* @return {boolean}
*/
AudioClip.prototype.isPlaying = function() {
var playTime = this.pauseTime_ +
            (AudioClip.context.currentTime - this.startTime_);

return this.playing_ && (this.loop_ || (playTime < this.buffer_.duration));
}

نتیجه گیری صوتی

امیدواریم این کلاس کمکی برای توسعه دهندگانی که با مشکلات صوتی مشابه من دست و پنجه نرم می کنند مفید باشد. همچنین، کلاسی مانند این مکان معقولی برای شروع به نظر می رسد، حتی اگر لازم باشد برخی از ویژگی های قدرتمندتر Web Audio API را اضافه کنید. در هر صورت، این راه حل نیازهای Bouncy Mouse را برآورده کرد و به بازی اجازه داد تا یک بازی واقعی HTML5 بدون هیچ رشته ای باشد!

کارایی

یکی دیگر از زمینه هایی که من را در رابطه با پورت جاوا اسکریپت نگران کرد عملکرد بود. پس از اتمام نسخه 1 پورت، متوجه شدم که همه چیز در دسکتاپ چهار هسته ای من درست کار می کند. متأسفانه، اوضاع در نت‌بوک یا کروم‌بوک کمی کمتر از حالت عادی بود. در این مورد، نمایه‌ساز کروم با نشان دادن اینکه دقیقاً در کجا زمان تمام برنامه‌های من صرف می‌شود، من را نجات داد. تجربه من اهمیت نمایه سازی قبل از انجام هر گونه بهینه سازی را برجسته می کند. انتظار داشتم فیزیک Box2D یا شاید کد رندر منبع اصلی کاهش سرعت باشد. با این حال، بیشتر وقت من در تابع Matrix.clone() من صرف می‌شود. با توجه به ماهیت ریاضی سنگین بازی من، می‌دانستم که ایجاد/کلون‌سازی ماتریس زیادی انجام داده‌ام، اما هرگز انتظار نداشتم که این گلوگاه باشد. در پایان، مشخص شد که یک تغییر بسیار ساده به بازی اجازه می‌دهد تا استفاده از CPU خود را بیش از 3 برابر کاهش دهد و از 6-7٪ CPU روی دسکتاپ من به 2٪ برسد. شاید این برای توسعه دهندگان جاوا اسکریپت رایج باشد، اما به عنوان یک توسعه دهنده ++C، این مشکل من را شگفت زده کرد، بنابراین کمی بیشتر به جزئیات می پردازم. اساساً، کلاس ماتریس اصلی من یک ماتریس 3x3 بود: یک آرایه 3 عنصری، که هر عنصر حاوی یک آرایه 3 عنصری است. متأسفانه، این بدان معنی بود که وقتی زمان شبیه سازی ماتریس فرا رسید، مجبور شدم 4 آرایه جدید ایجاد کنم. تنها تغییری که باید انجام می‌دادم این بود که این داده‌ها را به یک آرایه 9 عنصری منتقل کنم و ریاضیاتم را بر این اساس به‌روزرسانی کنم. این یک تغییر کاملاً مسئول کاهش 3 برابری CPU بود که من دیدم، و پس از این تغییر عملکرد من در تمام دستگاه های آزمایشی من قابل قبول بود.

بهینه سازی بیشتر

در حالی که عملکردم قابل قبول بود، هنوز چند سکسکه جزئی می دیدم. پس از کمی نمایه سازی، متوجه شدم که این به خاطر مجموعه زباله جاوا اسکریپت است. برنامه من با سرعت 60 فریم در ثانیه اجرا می شد، به این معنی که هر فریم فقط 16 میلی ثانیه برای کشیدن داشت. متأسفانه، هنگامی که جمع‌آوری زباله با دستگاهی کندتر شروع می‌شود، گاهی اوقات حدود 10 میلی‌ثانیه می‌خورد. این منجر به لکنت در چند ثانیه شد، زیرا بازی تقریباً به 16 میلی‌ثانیه کامل برای رسم یک فریم کامل نیاز داشت. برای اینکه بفهمم چرا این همه زباله تولید می‌کنم، از نمایه‌گر هیپ کروم استفاده کردم. با ناامیدی من، مشخص شد که اکثریت قریب به اتفاق زباله (بیش از 70٪) توسط Box2D تولید می شود. حذف زباله در جاوا اسکریپت کار دشواری است، و نوشتن مجدد Box2D غیرممکن بود، بنابراین متوجه شدم که خودم را به گوشه ای رسانده بودم. خوشبختانه، من هنوز یکی از قدیمی‌ترین ترفندهای کتاب را در دسترس داشتم: وقتی نمی‌توانید 60 فریم بر ثانیه بزنید، با سرعت 30 فریم در ثانیه اجرا کنید. کاملاً موافق است که دویدن با سرعت 30 فریم در ثانیه به مراتب بهتر از دویدن با سرعت 60 فریم بر ثانیه است. در واقع من هنوز یک شکایت یا نظر دریافت نکرده‌ام که بازی با سرعت 30 فریم در ثانیه اجرا می‌شود (تعریف آن واقعاً سخت است مگر اینکه این دو نسخه را در کنار هم مقایسه کنید). این 16 میلی‌ثانیه اضافی در هر فریم به این معنی بود که حتی در مورد یک جمع‌آوری زباله زشت، من هنوز زمان زیادی برای رندر کردن قاب داشتم. در حالی که اجرای با سرعت 30 فریم در ثانیه به صراحت توسط API زمانی که من استفاده می‌کردم فعال نمی‌شود ( RequestAnimationFrame عالی WebKit)، می‌توان آن را به روشی بسیار پیش پاافتاده انجام داد. اگرچه شاید به زیبایی یک API صریح نباشد، 30 فریم در ثانیه را می توان با دانستن اینکه فاصله RequestAnimationFrame با VSYNC مانیتور (معمولاً 60 فریم در ثانیه) تراز شده است، انجام داد. این بدان معنی است که ما فقط باید هر تماس دیگر را نادیده بگیریم. اساساً، اگر شما یک «تیک» برگشتی دارید که هر بار که «RequestAnimationFrame» اجرا می‌شود، فراخوانی می‌شود، این کار را می‌توان به صورت زیر انجام داد:

var skip = false;

function Tick() {
skip = !skip;
if (skip) {
return;
}

// OTHER CODE
}

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

توزیع و کسب درآمد

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

Bouncy Mouse در پایان ماه اکتبر در فروشگاه وب کروم راه اندازی شد. با انتشار در فروشگاه وب Chrome، می‌توانم از سیستم موجود برای قابلیت کشف، مشارکت جامعه، رتبه‌بندی و سایر ویژگی‌هایی که در پلتفرم‌های تلفن همراه به آن عادت کرده بودم، استفاده کنم. چیزی که من را شگفت زده کرد این بود که دسترسی به فروشگاه چقدر گسترده بود. در عرض یک ماه پس از انتشار، من به نزدیک به چهارصد هزار نصب رسیده بودم و قبلاً از مشارکت جامعه (گزارش اشکال، بازخورد) بهره می بردم. یکی دیگر از چیزهایی که من را شگفت زده کرد، پتانسیل یک برنامه وب برای کسب درآمد بود.

Bouncy Mouse یک روش ساده برای کسب درآمد دارد - یک بنر تبلیغاتی در کنار محتوای بازی. با این حال، با توجه به گستردگی بازی، متوجه شدم که این بنر تبلیغاتی توانسته درآمد قابل توجهی ایجاد کند و در طول دوره اوج آن، اپلیکیشن درآمدی قابل مقایسه با موفق ترین پلتفرم من، اندروید داشت. یکی از عواملی که در این امر نقش دارد این است که تبلیغات AdSense بزرگتر نشان داده شده در نسخه HTML5 نسبت به تبلیغات Admob کوچکتر نشان داده شده در Android درآمد قابل توجهی بیشتری را به ازای هر نمایش ایجاد می کند. نه تنها این، بلکه تبلیغات بنری در نسخه HTML5 بسیار کمتر از نسخه اندرویدی مزاحم است و امکان تجربه گیم پلی تمیزتری را فراهم می کند. به طور کلی من از این نتیجه بسیار شگفت زده شدم.

درآمد عادی در طول زمان
درآمد عادی در طول زمان

در حالی که درآمد حاصل از بازی بسیار بهتر از حد انتظار بود، شایان ذکر است که دسترسی به فروشگاه وب کروم هنوز کمتر از پلتفرم های بالغ تر مانند Android Market است. در حالی که Bouncy Mouse توانست به سرعت به رتبه نهم محبوب ترین بازی در فروشگاه وب کروم برسد، نرخ ورود کاربران جدید به سایت از زمان انتشار اولیه به میزان قابل توجهی کاهش یافت. با این اوصاف، بازی همچنان شاهد رشد ثابتی است، و من هیجان زده هستم که ببینم پلتفرم به چه شکلی توسعه می‌یابد!

نتیجه

من می‌توانم بگویم که انتقال ماوس Bouncy به کروم بسیار روان‌تر از آن چیزی است که انتظار داشتم. به غیر از برخی مشکلات جزئی صدا و عملکرد، متوجه شدم که Chrome یک پلتفرم کاملاً توانا برای یک بازی تلفن هوشمند موجود است. من هر توسعه‌دهنده‌ای را تشویق می‌کنم که از این تجربه اجتناب کرده‌اند تا آن را امتحان کنند. من هم از فرآیند انتقال و هم از مخاطبان جدید بازی که داشتن یک بازی HTML5 مرا به آن متصل کرده است، بسیار راضی بودم. در صورت داشتن هرگونه سوال می توانید به من ایمیل بزنید. یا فقط یک نظر در زیر بنویسید، من سعی خواهم کرد این موارد را به طور منظم بررسی کنم.