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

Giới thiệu

Chuột nhún

Sau khi xuất bản Bouncy Mouse trên iOS và Android vào cuối năm ngoái, tôi đã rút ra được một số bài học rất quan trọng. Điểm mấu chốt trong số đó là việc tham gia vào một thị trường lâu đời là điều rất khó. Trên thị trường iPhone đã bão hoà triệt để, việc tăng sức hút là 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. Dựa trên trải nghiệm này, tôi thấy được 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 trực tuyến hoàn toàn trống, nhưng danh mục trò chơi dựa trên HTML5 chất lượng cao của cửa hàng này chỉ mới bắt đầu phát triển. Đối với một nhà phát triển ứng dụng mới, điều này có nghĩa là việc lập bảng xếp hạng và tăng khả năng hiển thị sẽ dễ dàng hơn nhiều. Nắm bắt cơ hội này, tôi quyết định đưa Bouncy Mouse sang HTML5 với hy vọng có thể cung cấp trải nghiệm chơi trò chơi mới nhất cho cơ sở người dùng mới 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 khi chuyển Bouncy Mouse sang HTML5, sau đó tôi sẽ tìm hiểu sâu hơn một chút về ba lĩnh vực được cho là 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 sẽ đặt ra câu hỏi: Làm thế nào để viết một trò chơi có thể dễ dàng chuyển đổi sang nhiều nền tảng?. Tôi có cảm giác rằng mọi người hy vọng vào một gợi ý kỳ diệu mà họ có thể sử dụng để đạt được mức độ linh hoạt này mà không cần phải dùng tay. Tiếc là tôi không chắc liệu có một giải pháp như vậy hay không (giải pháp gần giống nhất có thể là khung PlayN của Google hoặc công cụ Unity, nhưng cả hai giải pháp này đều không đáp ứng được tất cả các mục tiêu mà tôi quan tâm). Trên thực tế, cách tiếp cận của tôi là một cổng tay. Lần đầu tiên tôi viết phiên bản iOS/Android trong C++, sau đó chuyển mã này sang từng nền tảng mới. Mặc dù việc này nghe có vẻ mất nhiều công sức, nhưng mỗi phiên bản WP7 và Chrome đều mất không quá 2 tuần để hoàn tất. Vì vậy, câu hỏi đặt ra là có thể làm gì để cơ sở mã có thể dễ dàng di chuyển bằng tay không? Có một số việc tôi đã làm để giúp ích cho vấn đề này:

Giữ cơ sở mã nhỏ

Mặc dù điều này có vẻ hiển nhiên, nhưng đó thực sự là lý do chính khiến tôi có thể chuyển đổi trò chơi nhanh chóng đến vậy. Mã khách hàng của Bouncy Mouse chỉ có khoảng 7.000 dòng C++ và 7.000 dòng mã không phải là gì, nhưng nó đủ 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 tương tự nhau. Việc giữ cho cơ sở mã của tôi nhỏ gọn về cơ bản có hai phương pháp chính: Không viết bất kỳ mã thừa nào và làm nhiều nhất có thể trong quá trình xử lý trước (không phải thời gian chạy). Việc không viết bất kỳ mã thừa nào có vẻ hiển nhiên, nhưng đó là một điều tôi luôn chiến đấu với bản thân mình. Tôi thường có nhu cầu viết một lớp/hàm trợ giúp cho bất kỳ yếu tố nào có thể được đưa vào một trình trợ giúp. Tuy nhiên, trừ phi bạn thực sự có ý định sử dụng trình trợ giúp nhiều lần, việc này thường sẽ làm tăng mã của bạn. Với Bouncy Mouse, tôi đã cẩn thận để không bao giờ viết một trình trợ giúp trừ phi tôi định sử dụng nó í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 này gọn gàng, dễ di chuyển và có thể sử dụng lại cho các dự án sau này. Mặt khác, khi chỉ viết mã cho Bouncy Mouse, khả năng sử dụng lại 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 nhất có thể, ngay cả khi đây không phải là cách viết mã "đẹp nhất". Việc thứ hai và quan trọng hơn để giữ cho cơ sở mã nhỏ gọn là cố gắng đưa nhiều nhất có thể vào các bước xử lý trước. Nếu bạn có thể thực hiện tác vụ trong thời gian chạy và chuyển tác vụ đó sang tác vụ tiền xử lý, thì trò chơi không chỉ chạy nhanh hơn mà bạn sẽ không phải chuyển mã sang từng nền tảng mới. Để làm ví dụ, ban đầu tôi lưu trữ dữ liệu hình học ở cấp độ của mình ở dạng định dạng chưa được xử lý khá rõ ràng, tập hợp các vùng đệm đỉnh OpenGL/WebGL thực tế trong thời gian chạy. Quá trình này 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, viết ra các vùng đệm đỉnh OpenGL/WebGL được đóng gói đầy đủ tại thời điểm biên dịch. Số lượng mã thực tế tương đương, 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 chúng sang bất kỳ nền tảng mới nào. Có rất nhiều ví dụ về vấn đề này trong Bouncy Mouse và những khả năng có thể sẽ thay đổi theo từng trò chơi, nhưng hãy chú ý đến những điều không cần xảy ra trong thời gian chạy.

Không dùng các phần phụ thuộc mà bạn không cần

Một lý do khác khiến Bouncy Mouse có thể dễ dàng chuyển đổi là vì nó hầu như không có phần phụ thuộc. Biểu đồ sau đây tóm tắt các phần phụ thuộc chính trong thư viện của Bouncy Mouse trên mỗi 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ý Hộp 2D Hộp 2D Box2D.js Box2D.xna

Vậy là xong rồi. Không có thư viện lớn nào của bên thứ ba được sử dụng, ngoại trừ Box2D, một giải pháp di động trên tất cả các nền tảng. Đối với đồ hoạ, cả WebGL và XNA đều lập bản đồ theo tỷ lệ 1:1 với OpenGL, vì vậy đây không phải là một vấn đề lớn. Chỉ khác trong lĩnh vực âm thanh. Tuy nhiên, mã âm thanh trong Bouncy Mouse có kích thước nhỏ (khoảng một trăm dòng mã dành riêng cho nền tảng) nên đây không phải là một vấn đề lớn. Việc giữ cho Bouncy Mouse không chứa các thư viện lớn không di động có nghĩa là logic của mã thời gian chạy có thể gần như giống nhau giữa các phiên bản (mặc dù có thay đổi về ngôn ngữ). Ngoài ra, việc này còn giúp chúng ta không bị bó buộc với một chuỗi công cụ không di động được. Cho tôi biết có phải việc lập trình dựa trên OpenGL/WebGL trực tiếp làm tăng độ phức tạp so với việc sử dụng một thư viện như Cocos2D hoặc Unity (cũng có một số trình trợ giúp WebGL). Thực tế, tôi tin điều ngược lại. Hầu hết trò chơi trên điện thoại / HTML5 trên điện thoại di động (ít nhất là những trò chơi như Bouncy Mouse) 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 ảnh sprite và có thể là một số hình dạng hoạ tiết. Tổng số mã dành riêng cho OpenGL trong Bouncy Mouse có thể dưới 1.000 dòng. Tôi sẽ ngạc nhiên nếu việc sử dụng thư viện trợ giúp lại thực sự làm giảm được con số này. Kể cả khi con số này giảm đi một nửa, tôi vẫn cần phải dành thời gian đáng kể để 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 vẫn chưa tìm thấy thư viện trợ giúp có thể di chuyển 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ẽ ảnh hưởng đáng kể đến 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, hoạt ảnh có giao diện, v.v., câu trả lời của tôi chắc chắn sẽ thay đổi. Trong trường hợp này, tôi muốn phát minh lại vô lăng để cố gắng viết mã thủ công toàn bộ công cụ của mình dựa trên OpenGL. Quan điểm của tôi ở đây là hầu hết trò chơi dành cho Thiết bị di động/HTML5 vẫn chưa thuộc danh mục này, vì vậy, bạn không cần phải phức tạp hóa mọi thứ trước khi thực sự 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 rất nhiều thời gian trong việc chuyển cơ sở mã C++ của tôi sang ngôn ngữ mới là nhận ra rằng hầu hết các mã gần như giống hệt nhau giữa mỗi ngôn ngữ. Tuy một số yếu tố chính có thể thay đổi, nhưng đó ít hơn rất nhiều so với những yếu tố không thay đổi. Trên thực tế, đối với nhiều hàm, việc đi từ C++ sang JavaScript chỉ đơn giản là 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 chuyển đổi

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

Biểu đồ âm thanh web

Điều đầu tiên làm cho API Web Audio 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 ra cho người dùng. Mặc dù mạnh mẽ nhưng thực tế là mọi bản phát âm thanh đều 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 API Web âm thanh, hãy xem xét biểu đồ sau:

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

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

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

Điều đầu tiên làm cho API Web Audio 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 ra cho người dùng. Mặc dù mạnh mẽ nhưng thực tế là mọi bản phát âm thanh đều 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 API Web âm thanh, hãy xem xét biểu đồ sau:

Đồ thị âm thanh trên web ít quan trọng
Biểu đồ âm thanh web ba chiều

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

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

Mặc dù việc hiểu biểu đồ rất tốt, nhưng tôi không muốn xử lý vấn đề này 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 trong nội bộ, nhưng trình bày một API giao diện người dùng đơn giản hơn nhiều.

AudioClip
AudioClip

Lớp này chỉ là một biểu đồ Âm thanh web 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 trường hợp tôi phải xây dựng một biểu đồ Âm thanh web để phát từng âm thanh.

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

// Later
sound.play();

Thông tin triển khai

Hãy xem nhanh mã của lớp 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 (để đơn giản hoá ví dụ), phần tử Âm thanh HTML5 cũng có thể được sử dụng làm nút nguồn. Điều này đặc biệt hữu ích đối với các mẫu có kích thước lớn. Xin lưu ý rằng API Web âm thanh yêu cầu chúng tôi tìm nạp dữ liệu này dưới dạng "arraybuffer". Sau khi nhận được dữ liệu, chúng tôi sẽ tạo vùng đệm Âm thanh 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 bao gồm hai bước: thiết lập biểu đồ phát và gọi một phiên bản “noteOn” trên nguồn của biểu đồ. Mỗi nguồn chỉ có thể được phát lại một lần, vì vậy chúng ta phải tạo lại nguồn/biểu đồ mỗi lần phát. Sự phức tạp nhất của hàm này xuất phát từ các yêu cầu cần thiết để tiếp tục một đoạn video bị tạm dừng (this.pauseTime_ > 0). Để tiếp tục phát một đoạn video bị tạm dừng, chúng ta dùng noteGrainOn để cho phép phát một vùng phụ của bộ đệm. Rất tiếc, noteGrainOn không tương tác với vòng lặp theo cách bạn muốn trong tình huống này (điều này sẽ lặp lại khu vực con, chứ không phải toàn bộ vùng đệm). Do đó, chúng ta cần xử lý việc 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à bật tính năng lặp lại.

/**
* 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 – Chức năng phát ở trên không cho phép phát đoạn âm thanh nhiều lần bị 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 lại). Đôi khi, một trò chơi muốn phát âm thanh nhiều lần mà không cần đợi mỗi lần phát xong (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ể diễn ra đồng thời, nên nội dung phát từ playAsSFX() không bị ràng buộc 1:1 với AudioClip. Do đó, không thể dừng, tạm dừng hoặc truy vấn trạng thái phát. Tính năng lặp lại cũng bị tắt vì sẽ không có cách nào để dừng âm thanh lặp lại được phát 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);
}
}

Trạng thái dừng, tạm dừng và truy vấn – Phần còn lại của các hàm 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 âm thanh

Hy vọng lớp học trợ giúp này sẽ có ích cho các nhà phát triển đang gặp khó khăn về âm thanh như tôi. Ngoài ra, lớp học như thế này có vẻ là nơi phù hợp để 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 Web âm thanh. Dù bằng cách nào, giải pháp này vẫn đá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ự mà không cần chuỗi nào!

Hiệu suất

Một vấn đề khác khiến tôi lo lắng 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 thấy rằng mọi thứ đều hoạt động tốt trên máy tính để bàn quad-core của mình. Rất tiếc, mọi thứ trên máy tính xách tay hoặc Chromebook đều không ổn định một chút. Trong trường hợp này, trình phân tích tài nguyên của Chrome đã giúp tôi lưu bằng cách hiển thị chính xác nơi tôi sử dụng tất cả thời gian cho các chương trình của mình. Trải nghiệm của tôi nêu bật tầm quan trọng của việc phân tích tài nguyên trước khi thực hiện bất kỳ hoạt động tối ưu hoá nào. Tôi đã kỳ vọng rằng vật lý Box2D hoặc mã kết xuất có thể là nguyên nhân chính gây ra tình trạng chậm trễ; tuy nhiên, phần lớn thời gian của tôi thực sự lại được dành cho hàm Matrix.clone(). Do tính chất toán học nặng nề về toán học trong trò chơi của mình, tôi biết rằng mình đã thực hiện rất nhiều thao tác tạo/nhân bản ma trận, nhưng tôi chưa bao giờ nghĩ việc này lại trở thành nút thắt cổ chai. Cuối cùng, hoá ra một sự thay đổi rất đơn giản đã cho phép trò chơi cắt 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 của tôi xuống còn 2%. Đây có thể là kiến thức phổ biến đối với các nhà phát triển JavaScript, nhưng là một nhà phát triển C++, vấn đề này khiến tôi ngạc nhiên, vì vậy tôi sẽ đi vào 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ử. Thật không may, điều này có nghĩa là khi đến lúc sao chép 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 của tôi cho phù hợp. Một 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 và sau thay đổi này, hiệu suất của tôi chấp nhận được trên tất cả các thiết bị thử nghiệm.

Tối ưu hoá thêm

Mặc dù hiệu suất ở mức chấp nhận được, nhưng tôi vẫn thấy một vài vấn đề nhỏ. Sau khi phân tích kỹ hơn 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ẽ. Thật không may, khi tính năng thu gom rác hoạt động trên một má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án đoạn vài giây vì trò chơi cần gần 16 mili giây để vẽ toàn bộ khung hình. Để hiểu rõ hơn về lý do tại sao tôi lại tạo ra nhiều rác như vậy, tôi đã sử dụng trình phân tích vùng nhớ khối xếp của Chrome. Thật tuyệt vọng, hoá ra 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 công việc phức tạp và việc viết lại Box2D không có gì bất thường, vì vậy tôi nhận ra mình đã rơi vào một góc. Thật may là tôi vẫn còn một trong những thủ thuật cũ nhất trong cuốn sách có sẵn: Khi bạn không thể đạt tốc độ 60 khung hình/giây, hãy chạy ở tốc độ 30 khung hình/giây. Mọi người cũng đồng ý rằng chạy ở tốc độ 30 khung hình/giây đều đặn sẽ tốt hơn nhiều so với chạy ở tốc độ 60 khung hình/giây có dao động. Trên thực tế, tôi vẫn chưa nhận được một lời phàn nàn hay nhận xét nào về việc trò chơi chạy ở tốc độ 30 khung hình/giây (rất khó để nhận biết trừ khi bạn so sánh hai phiên bản cạnh nhau). Thêm 16 mili giây trên mỗi khung hình này có nghĩa là ngay cả trong trường hợp thu gom rác xấu, tôi vẫn còn nhiều thời gian để kết xuất khung hình này. Mặc dù không được bật rõ ràng bởi API thời gian mà tôi đã sử dụng (requestAnimationFrame xuất sắc của WebKit), dù đang chạy ở tốc độ 30 khung hình/giây), nhưng điều này có thể được thực hiện theo cách rất đơn giản. Mặc dù có thể không thanh lịch như API rõ ràng, nhưng có thể thực hiện 30 khung hình/giây nếu 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ỉ phải 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” (Tắc) được gọi mỗi khi “RequestAnimationFrame” được kích hoạt, thì 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 rằng VSYNC của máy tính chưa ở mức hoặc dưới 30 khung hình/giây khi khởi động và tắt chế độ 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 tra.

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

Một vấn đề cuối cùng khiến tôi ngạc nhiên về cổng Bouncy Mouse trên Chrome là kiếm tiền. Trong 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ệ mới nổi. Tôi đã không nhận ra là cổng này sẽ tiếp cận rất nhiều đối tượng 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 một hệ thống hiện có cho khả năng phát hiện, mức độ tương tác với cộng đồng, thứ hạng và các tính năng khác mà tôi đã từng sử dụng trên nền tảng di động. Điều khiến tôi ngạc nhiên là phạm vi tiếp cận của cửa hàng rất rộng. Trong vòng một tháng kể từ khi phát hành, tôi đã đạt được gần 400 nghìn lượt cài đặt và hưởng lợi từ sự tham gia của cộng đồng (báo cáo lỗi, phản hồi). Một điều khác làm 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, đó là quảng cáo biểu ngữ bên cạnh nội dung trò chơi. Tuy nhiên, với 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ể và trong giai đoạn 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 ảnh hưởng đến việc 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 cho mỗi lượt hiển thị cao hơn đáng kể 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 xâm phạm hơn nhiều so với phiên bản Android, cho phép trải nghiệm chơi trò chơi rõ ràng hơn. Nhìn chung, tôi rất 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 bình thường hoá theo thời gian

Mặc dù thu nhập từ trò chơi 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 phạm vi tiếp cận của các nền tảng đã phát triển lớ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 trò chơi phổ biến nhất trên Cửa hàng Chrome trực tuyến, nhưng tỷ lệ người dùng mới truy cập trang web đã chậm lại đáng kể kể từ lần phát hành đầu tiên. Dù vậy, trò chơi vẫn đang phát triển ổn định và tôi rất hào hứng xem nền tảng này sẽ phát triển như thế nào!

Kết luận

Tôi thấy rằng quá trình chuyển Bouncy Mouse sang Chrome diễn ra suôn sẻ hơn nhiều so với kỳ vọng của tôi. Ngoài một số vấn đề nhỏ về hiệu suất và âm thanh, tôi thấy rằng Chrome là một nền tảng hoàn hảo cho trò chơi hiện có trên điện thoại thông minh. Tôi khuyến khích bất kỳ nhà phát triển nào đang từ chối trải nghiệm này thử nghiệm. 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 đã kết nối tôi với trò chơi HTML5. Đừng ngại gửi email cho tôi nếu bạn có bất kỳ câu hỏi nào. 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 những báo cáo này thường xuyên.