Nghiên cứu điển hình – Bouncy Mouse

Giới thiệu

Chuột nảy

Sau khi phát hành Bouncy Mouse trên iOS và Android vào cuối năm ngoái, tôi đã rút ra được một vài bài học rất quan trọng. Trong đó, quan trọng nhất là việc khó thâm nhập vào một thị trường đã có chỗ đứng. Trên thị trường iPhone đã bão hoà, việc thu hút người dùng rất khó khăn; trên Android Marketplace ít bão hoà hơn, tiến trình dễ dàng hơn nhưng vẫn không dễ dàng. Từ trải nghiệm này, tôi nhận thấy một cơ hội thú vị trên Cửa hàng Chrome trực tuyến. Mặc dù Cửa hàng web không hề trống, nhưng danh mục trò chơi chất lượng cao dựa trên HTML5 của cửa hàng này mới chỉ bắt đầu phát triển và hoàn thiện. Đối với nhà phát triển ứng dụng mới, điều này có nghĩa là việc tạo biểu đồ xếp hạng và tăng khả năng hiển thị sẽ dễ dàng hơn nhiều. Với cơ hội này, tôi bắt đầu chuyển Bouncy Mouse sang HTML5 với hy vọng có thể mang đến trải nghiệm chơi trò chơi mới nhất cho một cơ sở người dùng mới đầy thú vị. Trong nghiên cứu điển hình này, tôi sẽ nói một chút về quy trình chung để chuyển Bouncy Mouse sang HTML5, sau đó sẽ tìm hiểu sâu hơn một chút về ba khía cạnh thú vị: Âm thanh, Hiệu suất và Kiếm tiền.

Chuyển trò chơi C++ sang HTML5

Bouncy Mouse hiện có trên Android(C++), iOS (C++), Windows Phone 7 (C#) và Chrome (Javascript). Điều này đôi khi khiến bạn phải đặt câu hỏi: Làm cách nào để viết một trò chơi có thể dễ dàng chuyển sang nhiều nền tảng? Tôi có cảm giác rằng mọi người hy vọng có một giải pháp thần kỳ để có thể đạt được mức độ di động này mà không cần phải chuyển đổi thủ công. Đáng buồn là tôi không chắc liệu có giải pháp nào như vậy hay không (có lẽ thứ gần nhất là khung PlayN của Google hoặc công cụ Unity, nhưng cả hai đều không đáp ứng được tất cả các mục tiêu mà tôi quan tâm). Thực tế, cách tiếp cận của tôi là một cổng tay. Trước tiên, tôi đã viết phiên bản iOS/Android bằng C++, sau đó chuyển mã này sang từng nền tảng mới. Mặc dù nghe có vẻ như rất nhiều việc phải làm, nhưng mỗi phiên bản WP7 và Chrome chỉ mất không quá 2 tuần để hoàn thành. Vậy câu hỏi đặt ra là có thể làm gì để dễ dàng di chuyển cơ sở mã không? Tôi đã làm một vài việc giúp ích cho việc này:

Giữ cho cơ sở mã nhỏ

Mặc dù điều này có vẻ hiển nhiên, nhưng đây thực sự là lý do chính khiến tôi có thể chuyển đổi trò chơi nhanh chóng như vậy. Mã ứng dụng của Bouncy Mouse chỉ có khoảng 7.000 dòng mã C++. 7.000 dòng mã không phải là con số nhỏ, nhưng đủ nhỏ để có thể quản lý. Cả phiên bản C# và Javascript của mã ứng dụng đều có kích thước gần như nhau. Về cơ bản, việc giữ cho cơ sở mã của tôi nhỏ gọn là nhờ hai phương pháp chính: Không viết bất kỳ mã thừa nào và làm nhiều việc nhất có thể trong mã xử lý trước (không phải thời gian chạy). Việc không viết mã thừa có vẻ như là điều hiển nhiên, nhưng đó là một điều mà tôi luôn phải tự đấu tranh với chính mình. Tôi thường có nhu cầu viết một lớp/hàm trợ giúp cho mọi thứ có thể được đưa vào một trình trợ giúp. Tuy nhiên, trừ phi bạn thực sự có kế hoạch sử dụng trình trợ giúp nhiều lần, thì việc này thường chỉ làm mã của bạn trở nên cồng kềnh. Với Bouncy Mouse, tôi cẩn thận không bao giờ viết trình trợ giúp trừ phi tôi sẽ sử dụng trình trợ giúp đó ít nhất ba lần. Khi viết một lớp trợ giúp, tôi đã cố gắng làm cho lớp đó gọn gàng, dễ di chuyển và có thể sử dụng lại cho các dự án trong tương lai. Mặt khác, khi chỉ viết mã cho Bouncy Mouse, với khả năng tái sử dụng thấp, tôi tập trung vào việc hoàn thành nhiệm vụ lập trình một cách đơn giản và nhanh chóng nhất có thể, ngay cả khi đây không phải là cách viết mã "đẹp nhất". Phần thứ hai và quan trọng hơn để giữ cho cơ sở mã nhỏ là đẩy càng nhiều càng tốt vào các bước xử lý trước. Nếu có thể lấy một tác vụ trong thời gian chạy và chuyển tác vụ đó sang một tác vụ xử lý trước, thì trò chơi của bạn không chỉ chạy nhanh hơn mà bạn cũng không phải chuyển mã sang từng nền tảng mới. Để minh hoạ, ban đầu tôi đã lưu trữ dữ liệu hình học cấp độ dưới dạng một định dạng khá chưa được xử lý, tập hợp các vùng đệm đỉnh OpenGL/WebGL thực tế tại thời gian chạy. Quá trình này mất một chút thời gian thiết lập và vài trăm dòng mã thời gian chạy. Sau đó, tôi đã chuyển mã này sang bước xử lý trước, ghi các vùng đệm đỉnh OpenGL/WebGL được đóng gói đầy đủ tại thời điểm biên dịch. Lượng mã thực tế vẫn như cũ, nhưng vài trăm dòng đó đã được chuyển sang bước xử lý trước, nghĩa là tôi không bao giờ phải chuyển các dòng đó sang bất kỳ nền tảng mới nào. Có rất nhiều ví dụ về điều này trong Bouncy Mouse và những gì có thể sẽ khác nhau tuỳ theo trò chơi, nhưng bạn chỉ cần chú ý đến mọi thứ không cần xảy ra trong thời gian chạy.

Không lấy các phần phụ thuộc bạn không cần

Một lý do khác khiến Bouncy Mouse dễ dàng được chuyển đổi là vì ứng dụng này gần như không có phần phụ thuộc nào. Biểu đồ sau đây tóm tắt các phần phụ thuộc thư viện chính của Bouncy Mouse theo nền tảng:

Android iOS HTML5 WP7
Đồ hoạ OpenGL ES OpenGL ES WebGL XNA
Âm thanh OpenSL ES OpenAL Âm thanh trên web XNA
Vật Lý Box2D Box2D Box2D.js Box2D.xna

Đó là tất cả. Không có thư viện lớn nào của bên thứ ba được sử dụng, ngoại trừ Box2D có thể di chuyển trên tất cả nền tảng. Đối với đồ hoạ, cả WebGL và XNA đều ánh xạ gần như 1:1 với OpenGL, vì vậy đây không phải là vấn đề lớn. Chỉ trong lĩnh vực âm thanh, các thư viện thực tế mới khác nhau. Tuy nhiên, mã âm thanh trong Bouncy Mouse rất nhỏ (khoảng một trăm dòng mã dành riêng cho nền tảng), vì vậy, đây không phải là vấn đề lớn. Việc không để Bouncy Mouse chứa các thư viện lớn không thể di chuyển có nghĩa là logic của mã thời gian chạy có thể gần giống nhau giữa các phiên bản (mặc dù ngôn ngữ thay đổi). Ngoài ra, việc này giúp chúng ta không bị mắc kẹt trong một chuỗi công cụ không di động. Tôi đã được hỏi liệu việc lập trình trực tiếp trên OpenGL/WebGL có làm tăng độ phức tạp so với việc sử dụng thư viện như Cocos2D hoặc Unity hay không (cũng có một số trình trợ giúp WebGL). Thực tế, tôi tin rằng điều ngược lại mới đúng. Hầu hết các trò chơi dành cho điện thoại di động / HTML5 (ít nhất là những trò chơi như Bouncy Mouse) đều rất đơn giản. Trong hầu hết các trường hợp, trò chơi chỉ vẽ một vài sprite và có thể là một số hình học có kết cấu. Tổng số mã dành riêng cho OpenGL trong Bouncy Mouse có thể dưới 1000 dòng. Tôi sẽ rất ngạc nhiên nếu việc sử dụng thư viện trợ giúp thực sự làm giảm số lượng này. Ngay cả khi giảm số lượng này xuống một nửa, tôi vẫn cần dành nhiều thời gian để tìm hiểu các thư viện/công cụ mới chỉ để tiết kiệm 500 dòng mã. Ngoài ra, tôi chưa tìm thấy thư viện trợ giúp nào có thể di chuyển được trên tất cả các nền tảng mà tôi quan tâm, vì vậy, việc sử dụng phần phụ thuộc như vậy sẽ làm giảm đáng kể khả năng di chuyển. Nếu tôi đang viết một trò chơi 3D cần bản đồ ánh sáng, LOD động, ảnh động được tạo hình, v.v., thì chắc chắn câu trả lời của tôi sẽ thay đổi. Trong trường hợp này, tôi sẽ phải phát minh lại để cố gắng tự viết mã cho toàn bộ công cụ của mình dựa trên OpenGL. Ý tôi ở đây là hầu hết các trò chơi dành cho thiết bị di động/HTML5 (chưa) thuộc danh mục này, vì vậy, bạn không cần phải làm phức tạp mọi thứ trước khi cần thiết.

Đừng đánh giá thấp sự tương đồng giữa các ngôn ngữ

Một mẹo cuối cùng giúp tiết kiệm nhiều thời gian trong việc chuyển đổi cơ sở mã C++ sang một ngôn ngữ mới là nhận ra rằng hầu hết mã đều gần giống nhau giữa các ngôn ngữ. Mặc dù một số thành phần chính có thể thay đổi, nhưng số lượng thành phần không thay đổi sẽ nhiều hơn nhiều. Trên thực tế, đối với nhiều hàm, việc chuyển từ C++ sang JavaScript chỉ cần chạy một vài thay thế biểu thức chính quy trên cơ sở mã C++ của tôi.

Kết luận về việc chuyển đổi

Đó là tất cả những gì bạn cần làm trong quy trình chuyển đổi. Tôi sẽ đề cập đến một số thách thức cụ thể về HTML5 trong vài phần tiếp theo, nhưng thông điệp chính là nếu bạn giữ mã của mình đơn giản, thì việc chuyển đổi sẽ là một vấn đề nhỏ chứ không phải là một cơn ác mộng.

Âm thanh

Một khía cạnh khiến tôi (và dường như mọi người khác) gặp một số vấn đề là âm thanh. Trên iOS và Android, có một số lựa chọn âm thanh chắc chắn (OpenSL, OpenAL), nhưng trong thế giới HTML5, mọi thứ có vẻ ảm đạm hơn. Mặc dù có Âm thanh HTML5, nhưng tôi nhận thấy rằng tính năng này có một số vấn đề nghiêm trọng khi được sử dụng trong trò chơi. Ngay cả trên các trình duyệt mới nhất, tôi thường xuyên gặp phải hành vi lạ. Ví dụ: Chrome có vẻ như có giới hạn về số lượng phần tử Âm thanh (nguồn) đồng thời mà bạn có thể tạo. Ngoài ra, ngay cả khi âm thanh phát, đôi khi âm thanh đó vẫn bị méo một cách không thể giải thích. Nhìn chung, tôi hơi lo lắng. Sau khi tìm kiếm trên mạng, tôi nhận thấy hầu hết mọi người đều gặp phải vấn đề tương tự. Giải pháp ban đầu mà tôi sử dụng là một API có tên SoundManager2. API này sử dụng Âm thanh HTML5 khi có sẵn, quay lại sử dụng Flash trong các tình huống khó khăn. Mặc dù giải pháp này hoạt động, nhưng vẫn gặp lỗi và khó dự đoán (chỉ ít hơn so với Âm thanh HTML5 thuần tuý). Một tuần sau khi ra mắt, tôi đã trò chuyện với một số nhân viên hữu ích tại Google. Họ đã chỉ cho tôi Web Audio API của Webkit. Ban đầu, tôi đã cân nhắc sử dụng API này, nhưng đã tránh sử dụng do API có vẻ phức tạp (đối với tôi) và không cần thiết. Tôi chỉ muốn phát một vài âm thanh: với Âm thanh HTML5, việc này chỉ mất vài dòng mã JavaScript. Tuy nhiên, khi xem qua Web Audio, tôi đã bị ấn tượng bởi thông số kỹ thuật khổng lồ (70 trang), số lượng mẫu nhỏ trên web (thường thấy đối với một API mới) và việc thiếu chức năng "phát", "tạm dừng" hoặc "dừng" ở bất kỳ đâu trong thông số kỹ thuật. Nhờ Google đảm bảo rằng những lo lắng của tôi là không có cơ sở, tôi đã tìm hiểu lại API này. Sau khi xem xét thêm một vài ví dụ và nghiên cứu thêm một chút, tôi nhận thấy Google đã đúng – API này chắc chắn có thể đáp ứng nhu cầu của tôi mà không gặp phải lỗi như các API khác. Đặc biệt hữu ích là bài viết Bắt đầu sử dụng Web Audio API. Đây là một nơi tuyệt vời để bạn tham khảo nếu muốn hiểu rõ hơn về API này. Vấn đề thực sự của tôi là ngay cả sau khi hiểu và sử dụng API, tôi vẫn thấy API này không được thiết kế để "chỉ phát một vài âm thanh". Để giải quyết sự nghi ngờ này, tôi đã viết một lớp trợ giúp nhỏ cho phép tôi sử dụng API theo cách tôi muốn – phát, tạm dừng, dừng và truy vấn trạng thái của âm thanh. Tôi gọi lớp trợ giúp này là AudioClip. Bạn có thể xem toàn bộ nguồn trên GitHub theo giấy phép Apache 2.0. Tôi sẽ thảo luận chi tiết về lớp này ở bên dưới. Trước tiên, hãy tìm hiểu một số thông tin cơ bản về API Web âm thanh:

Biểu đồ âm thanh trên web

Điều đầu tiên khiến Web Audio API phức tạp hơn (và mạnh mẽ hơn) so với phần tử Âm thanh HTML5 là khả năng xử lý / kết hợp âm thanh trước khi xuất âm thanh đó cho người dùng. Mặc dù mạnh mẽ, nhưng việc phát âm thanh nào cũng liên quan đến biểu đồ khiến mọi thứ trở nên phức tạp hơn một chút trong các tình huống đơn giản. Để minh hoạ sức mạnh của Web Audio API, hãy xem xét biểu đồ sau:

Biểu đồ âm thanh cơ bản trên web
Biểu đồ âm thanh cơ bản trên web

Mặc dù ví dụ trên cho thấy sức mạnh của API Âm thanh trên web, nhưng tôi không cần đến hầu hết sức mạnh này trong trường hợp của mình. Tôi chỉ muốn phát một âm thanh. Mặc dù vẫn cần có biểu đồ, nhưng biểu đồ này rất đơn giản.

Biểu đồ có thể đơn giản

Điều đầu tiên khiến Web Audio API phức tạp hơn (và mạnh mẽ hơn) so với phần tử Âm thanh HTML5 là khả năng xử lý / kết hợp âm thanh trước khi xuất âm thanh đó cho người dùng. Mặc dù mạnh mẽ, nhưng việc phát âm thanh nào cũng liên quan đến biểu đồ khiến mọi thứ trở nên phức tạp hơn một chút trong các tình huống đơn giản. Để minh hoạ sức mạnh của Web Audio API, hãy xem xét biểu đồ sau:

Biểu đồ âm thanh Web không quan trọng
Biểu đồ âm thanh web nhỏ

Biểu đồ nhỏ hiển thị ở trên có thể thực hiện mọi thao tác cần thiết để phát, tạm dừng hoặc dừng âm thanh.

Nhưng đừng lo lắng về biểu đồ

Mặc dù việc hiểu biểu đồ là rất tốt, nhưng đó không phải là điều tôi muốn xử lý mỗi khi phát âm thanh. Do đó, tôi đã viết một lớp trình bao bọc đơn giản "AudioClip". Lớp này quản lý biểu đồ này nội bộ, nhưng trình bày một API dành cho người dùng đơn giản hơn nhiều.

AudioClip
AudioClip

Lớp này không gì khác ngoài một biểu đồ Web Audio và một số trạng thái trợ giúp, nhưng cho phép tôi sử dụng mã đơn giản hơn nhiều so với khi phải tạo biểu đồ Web Audio để phát từng âm thanh.

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

// Later
sound.play();

Chi tiết triển khai

Hãy xem nhanh mã của lớp trình trợ giúp: Hàm khởi tạo – Hàm khởi tạo xử lý việc tải dữ liệu âm thanh bằng XHR. Mặc dù không được hiển thị ở đây (để giữ cho ví dụ đơn giản), nhưng bạn cũng có thể sử dụng phần tử Âm thanh HTML5 làm nút nguồn. Điều này đặc biệt hữu ích đối với các mẫu lớn. Xin lưu ý rằng API Âm thanh trên web yêu cầu chúng ta tìm nạp dữ liệu này dưới dạng "arraybuffer". Sau khi nhận được dữ liệu, chúng ta sẽ tạo một bộ đệm Âm thanh trên web từ dữ liệu này (giải mã dữ liệu từ định dạng ban đầu thành định dạng PCM trong thời gian chạy).

/**
* 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();
}

Phát – Việc phát âm thanh của chúng ta bao gồm hai bước: thiết lập biểu đồ phát và gọi phiên bản "noteOn" trên nguồn của biểu đồ. Bạn chỉ có thể phát lại một nguồn một lần, vì vậy, chúng ta phải tạo lại nguồn/biểu đồ mỗi khi phát. Phần lớn sự phức tạp của hàm này đến từ các yêu cầu cần thiết để tiếp tục phát một đoạn video đã tạm dừng (this.pauseTime_ > 0). Để tiếp tục phát một đoạn video đã tạm dừng, chúng ta sử dụng noteGrainOn, cho phép phát một vùng phụ của vùng đệm. Rất tiếc, noteGrainOn không tương tác với vòng lặp theo cách mong muốn cho trường hợp này (vòng lặp sẽ lặp lại vùng phụ, chứ không phải toàn bộ vùng đệm). Do đó, chúng ta cần giải quyết vấn đề này bằng cách phát phần còn lại của đoạn video bằng noteGrainOn, sau đó bắt đầu lại đoạn video từ đầu với chế độ lặp lại được bật.

/**
* 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);
}
}
}

Phát dưới dạng hiệu ứng âm thanh – Hàm phát ở trên không cho phép phát đoạn âm thanh nhiều lần với âm thanh chồng chéo (chỉ có thể phát lần thứ hai khi đoạn âm thanh kết thúc hoặc dừng). Đôi khi, trò chơi sẽ muốn phát âm thanh nhiều lần mà không cần chờ mỗi lần phát hoàn tất (thu thập tiền xu trong trò chơi, v.v.). Để bật tính năng này, lớp AudioClip có một phương thức playAsSFX(). Vì nhiều lượt phát có thể xảy ra đồng thời, nên lượt phát từ playAsSFX() không được liên kết 1:1 với AudioClip. Do đó, bạn không thể dừng, tạm dừng hoặc truy vấn trạng thái phát. Tính năng phát lặp lại cũng bị tắt vì không có cách nào để dừng âm thanh phát lặp lại theo cách này.

/**
* 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);
}
}

Dừng, tạm dừng và truy vấn trạng thái – Các hàm còn lại khá đơn giản và không cần giải thích nhiều:

/**
* 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));
}

Kết luận bằng âm thanh

Hy vọng lớp trình trợ giúp này sẽ hữu ích cho các nhà phát triển đang gặp phải những vấn đề về Âm thanh giống như tôi. Ngoài ra, một lớp như thế này có vẻ là một nơi hợp lý để bắt đầu ngay cả khi bạn cần thêm một số tính năng mạnh mẽ hơn của API Âm thanh trên web. Dù thế nào đi nữa, giải pháp này cũng đáp ứng được nhu cầu của Bouncy Mouse và cho phép trò chơi trở thành một trò chơi HTML5 thực sự, không có điều kiện ràng buộc!

Hiệu suất

Một khía cạnh khác khiến tôi lo ngại về cổng JavaScript là hiệu suất. Sau khi hoàn tất phiên bản 1 của cổng, tôi nhận thấy mọi thứ đều hoạt động tốt trên máy tính để bàn 4 nhân. Rất tiếc, mọi thứ không được như ý trên máy tính xách tay hoặc Chromebook. Trong trường hợp này, trình phân tích tài nguyên của Chrome đã giúp tôi bằng cách cho biết chính xác thời gian mà tất cả các chương trình của tôi đang tiêu tốn. Kinh nghiệm của tôi cho thấy tầm quan trọng của việc lập hồ sơ trước khi thực hiện bất kỳ hoạt động tối ưu hoá nào. Tôi dự kiến rằng vật lý Box2D hoặc có thể là mã kết xuất sẽ là nguyên nhân chính gây ra tình trạng chậm; tuy nhiên, phần lớn thời gian của tôi thực sự đã dành cho hàm Matrix.clone(). Do trò chơi của tôi có tính chất toán học cao, nên tôi biết rằng mình đã tạo/nhân bản rất nhiều ma trận, nhưng tôi không ngờ đây lại là nút thắt cổ chai. Cuối cùng, một thay đổi rất đơn giản đã giúp trò chơi giảm mức sử dụng CPU hơn 3 lần, từ 6-7% CPU trên máy tính để bàn xuống còn 2%. Có thể đây là kiến thức phổ biến đối với các nhà phát triển JavaScript, nhưng với tư cách là một nhà phát triển C++, tôi đã rất ngạc nhiên khi gặp phải vấn đề này. Vì vậy, tôi sẽ trình bày chi tiết hơn một chút. Về cơ bản, lớp ma trận ban đầu của tôi là một ma trận 3x3: một mảng 3 phần tử, mỗi phần tử chứa một mảng 3 phần tử. Rất tiếc, điều này có nghĩa là khi đến lúc nhân bản ma trận, tôi phải tạo 4 mảng mới. Thay đổi duy nhất tôi cần thực hiện là di chuyển dữ liệu này vào một mảng 9 phần tử và cập nhật toán học cho phù hợp. Thay đổi này hoàn toàn chịu trách nhiệm cho việc giảm CPU 3 lần mà tôi thấy. Sau khi thay đổi này, hiệu suất của tôi đã ở mức chấp nhận được trên tất cả thiết bị thử nghiệm.

Tối ưu hoá khác

Mặc dù hiệu suất của tôi chấp nhận được, nhưng tôi vẫn gặp một vài sự cố nhỏ. Sau khi phân tích thêm một chút, tôi nhận ra rằng điều này là do tính năng Thu gom rác của Javascript. Ứng dụng của tôi đang chạy ở tốc độ 60 khung hình/giây, tức là mỗi khung hình chỉ có 16 mili giây để vẽ. Rất tiếc, khi quá trình thu gom rác bắt đầu trên một máy chạy chậm hơn, đôi khi quá trình này sẽ tiêu tốn khoảng 10 mili giây. Điều này dẫn đến tình trạng giật vài giây một lần, vì trò chơi cần gần như toàn bộ 16 mili giây để vẽ một khung hình đầy đủ. Để hiểu rõ hơn lý do tạo ra nhiều rác, tôi đã sử dụng trình phân tích tài nguyên vùng nhớ khối xếp của Chrome. Tôi rất thất vọng khi phát hiện ra rằng phần lớn rác (hơn 70%) là do Box2D tạo ra. Việc loại bỏ rác trong Javascript là một việc khó khăn và việc viết lại Box2D là không thể, vì vậy, tôi nhận ra mình đã tự đưa mình vào một góc. May mắn thay, tôi vẫn có một trong những thủ thuật lâu đời nhất trong sách: Khi không thể đạt được 60 khung hình/giây, hãy chạy ở tốc độ 30 khung hình/giây. Có thể đồng ý rằng việc chạy ở tốc độ 30 khung hình/giây ổn định sẽ tốt hơn nhiều so với việc chạy ở tốc độ 60 khung hình/giây bị giật. Trên thực tế, tôi vẫn chưa nhận được một đơn khiếu nại hoặc nhận xét nào về việc trò chơi chạy ở tốc độ 30 khung hình/giây (thực sự rất khó để nhận ra trừ phi bạn so sánh song song hai phiên bản). 16 mili giây bổ sung cho mỗi khung hình có nghĩa là ngay cả trong trường hợp thu gom rác không hiệu quả, tôi vẫn có đủ thời gian để kết xuất khung hình. Mặc dù API thời gian mà tôi đang sử dụng (requestAnimationFrame tuyệt vời của WebKit) không bật rõ ràng chế độ chạy ở tốc độ 30 khung hình/giây, nhưng bạn có thể thực hiện việc này theo cách rất đơn giản. Mặc dù có thể không tinh tế như API rõ ràng, nhưng bạn có thể đạt được tốc độ 30 khung hình/giây bằng cách biết rằng khoảng thời gian của RequestAnimationFrame được căn chỉnh với VSYNC của màn hình (thường là 60 khung hình/giây). Điều này có nghĩa là chúng ta chỉ cần bỏ qua mọi lệnh gọi lại khác. Về cơ bản, nếu bạn có lệnh gọi lại "Tick" được gọi mỗi khi "RequestAnimationFrame" được kích hoạt, bạn có thể thực hiện việc này như sau:

var skip = false;

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

// OTHER CODE
}

Nếu muốn thận trọng hơn, bạn nên kiểm tra để đảm bảo VSYNC của máy tính không ở mức 30 khung hình/giây trở xuống khi khởi động và tắt tính năng bỏ qua trong trường hợp này. Tuy nhiên, tôi chưa thấy điều này trên bất kỳ cấu hình máy tính để bàn/máy tính xách tay nào mà tôi đã kiểm thử.

Phân phối và kiếm tiền

Một khía cạnh cuối cùng khiến tôi ngạc nhiên về phiên bản Bouncy Mouse dành cho Chrome là khả năng kiếm tiền. Khi bắt tay vào dự án này, tôi đã hình dung trò chơi HTML5 là một thử nghiệm thú vị để tìm hiểu các công nghệ sắp ra mắt. Điều tôi không ngờ là bản chuyển đổi này sẽ tiếp cận được một lượng lớn khán giả và có tiềm năng kiếm tiền đáng kể.

Bouncy Mouse được ra mắt vào cuối tháng 10 trên Cửa hàng Chrome trực tuyến. Bằng cách phát hành trên Cửa hàng Chrome trực tuyến, tôi có thể tận dụng hệ thống hiện có để tăng khả năng khám phá, mức độ tương tác của cộng đồng, thứ hạng và các tính năng khác mà tôi đã quen dùng trên các nền tảng di động. Điều khiến tôi ngạc nhiên là phạm vi tiếp cận rộng lớn của cửa hàng. Trong vòng một tháng kể từ khi phát hành, tôi đã đạt gần 400.000 lượt cài đặt và đã hưởng lợi từ sự tương tác của cộng đồng (báo cáo lỗi, phản hồi). Một điều khác khiến tôi ngạc nhiên là tiềm năng kiếm tiền của ứng dụng web.

Bouncy Mouse có một phương thức kiếm tiền đơn giản – quảng cáo biểu ngữ bên cạnh nội dung trò chơi. Tuy nhiên, do phạm vi tiếp cận rộng của trò chơi, tôi nhận thấy quảng cáo biểu ngữ này có thể tạo ra thu nhập đáng kể. Trong thời gian cao điểm, ứng dụng này đã tạo ra thu nhập tương đương với nền tảng thành công nhất của tôi là Android. Một yếu tố góp phần vào điều này là quảng cáo AdSense lớn hơn hiển thị trên phiên bản HTML5 tạo ra doanh thu cao hơn đáng kể trên mỗi lượt hiển thị so với quảng cáo AdMob nhỏ hơn hiển thị trên Android. Không chỉ vậy, quảng cáo biểu ngữ trên phiên bản HTML5 ít gây phiền toái hơn nhiều so với phiên bản Android, mang đến trải nghiệm chơi trò chơi rõ ràng hơn. Nhìn chung, tôi rất ngạc nhiên và hài lòng với kết quả này.

Thu nhập được chuẩn hoá theo thời gian.
Thu nhập chuẩn hoá theo thời gian

Mặc dù thu nhập từ trò chơi này tốt hơn nhiều so với dự kiến, nhưng đáng chú ý là phạm vi tiếp cận của Cửa hàng Chrome trực tuyến vẫn nhỏ hơn so với các nền tảng phát triển hơn như Android Market. Mặc dù Bouncy Mouse có thể nhanh chóng vươn lên vị trí thứ 9 trong danh sách trò chơi phổ biến nhất trên Cửa hàng Chrome trực tuyến, nhưng tốc độ người dùng mới truy cập vào trang web đã chậm lại đáng kể kể từ lần phát hành đầu tiên. Tuy nhiên, trò chơi vẫn đang phát triển đều đặn và tôi rất hào hứng được xem nền tảng này phát triển như thế nào!

Kết luận

Tôi có thể nói rằng việc chuyển Bouncy Mouse sang Chrome diễn ra suôn sẻ hơn tôi mong đợi. Ngoài một số vấn đề nhỏ về âm thanh và hiệu suất, tôi nhận thấy Chrome là một nền tảng hoàn toàn phù hợp cho một trò chơi hiện có trên điện thoại thông minh. Tôi khuyến khích mọi nhà phát triển từng tránh xa trải nghiệm này nên thử sức. Tôi rất hài lòng với cả quá trình chuyển đổi cũng như đối tượng chơi trò chơi mới mà việc có một trò chơi HTML5 đã kết nối tôi với họ. Vui lòng gửi email cho tôi nếu bạn có câu hỏi. Hoặc bạn chỉ cần để lại bình luận bên dưới, tôi sẽ cố gắng kiểm tra các bình luận này thường xuyên.