소개
작년 말에 iOS 및 Android용 Bouncy Mouse를 게시한 후 몇 가지 중요한 교훈을 얻었습니다. 그중에서도 기존 시장에 진입하기가 어렵다는 점이 가장 중요했습니다. 포화 상태에 이른 iPhone 시장에서는 관심을 얻기가 매우 어려웠습니다. 포화 상태가 덜한 Android 마켓플레이스에서는 진행이 더 쉬웠지만 여전히 쉽지는 않았습니다. 이러한 경험을 바탕으로 Chrome 웹 스토어에서 흥미로운 기회를 발견했습니다. 웹 스토어에 게임이 없는 것은 아니지만 고품질 HTML5 기반 게임 카탈로그는 이제 막 성숙해지기 시작했습니다. 신규 앱 개발자의 경우 순위 차트를 만들고 가시성을 높이는 것이 훨씬 쉬워집니다. 이 기회를 활용하여 Bouncy Mouse를 HTML5로 포팅하여 새로운 사용자층에 최신 게임플레이 환경을 제공하고자 했습니다. 이 사례 연구에서는 Bouncy Mouse를 HTML5로 포팅하는 일반적인 프로세스에 대해 간단히 설명한 다음, 흥미로운 것으로 입증된 세 가지 영역인 오디오, 성능, 수익 창출에 대해 자세히 살펴보겠습니다.
C++ 게임을 HTML5로 포팅
Bouncy Mouse는 현재 Android(C++), iOS (C++), Windows Phone 7 (C#), Chrome (Javascript)에서 사용할 수 있습니다. 이때 다음과 같은 질문이 제기됩니다. 여러 플랫폼으로 쉽게 이식할 수 있는 게임을 작성하려면 어떻게 해야 하나요? 사람들은 수동 포팅에 의존하지 않고도 이 정도의 휴대성을 달성할 수 있는 마법의 총알을 원하고 있는 것 같습니다. 안타깝게도 아직 이러한 솔루션이 있는지 모르겠습니다. 가장 근접한 것은 Google의 PlayN 프레임워크 또는 Unity 엔진일 수 있지만, 이 두 가지 모두 제가 관심을 두는 모든 타겟을 충족하지는 않습니다. 사실 저는 수동 포팅 방식을 사용했습니다. 먼저 C++로 iOS/Android 버전을 작성한 다음 이 코드를 각 새 플랫폼으로 포팅했습니다. 많은 작업처럼 들릴 수 있지만 WP7 및 Chrome 버전은 각각 2주 이내에 완료되었습니다. 이제 코드베이스를 손쉽게 휴대용으로 만들 수 있는 방법이 있을까요? 이 문제를 해결하는 데 도움이 된 몇 가지 방법이 있습니다.
코드베이스 작게 유지
이 점이 당연해 보이지만, 이 덕분에 게임을 이렇게 빠르게 포팅할 수 있었습니다. Bouncy Mouse의 클라이언트 코드는 C++로 약 7,000줄입니다. 7,000줄의 코드는 적지 않지만 관리할 수 있을 만큼 작습니다. 클라이언트 코드의 C# 버전과 JavaScript 버전은 모두 거의 동일한 크기가 되었습니다. 코드베이스를 작게 유지하는 방법은 기본적으로 두 가지 주요 관행으로 요약할 수 있습니다. 불필요한 코드를 작성하지 마세요. 그리고 사전 처리 (런타임 외) 코드에서 최대한 많은 작업을 하세요. 불필요한 코드를 작성하지 않는 것이 당연해 보이지만 항상 나와 싸우는 부분입니다. 도우미로 분해할 수 있는 모든 항목에 대해 도우미 클래스/함수를 작성하고 싶은 충동이 자주 생깁니다. 그러나 도우미를 실제로 여러 번 사용할 계획이 아니라면 일반적으로 코드가 팽창하게 됩니다. Bouncy Mouse의 경우 헬퍼를 세 번 이상 사용할 생각이 아니라면 절대 작성하지 않았습니다. 도우미 클래스를 작성할 때는 향후 프로젝트에서 사용할 수 있도록 깔끔하고 이식 가능하며 재사용 가능한 클래스로 만들려고 노력했습니다. 반면에 재사용 가능성이 낮은 Bouncy Mouse용 코드를 작성할 때는 코드를 작성하는 '가장 예쁜' 방법이 아니더라도 코딩 작업을 최대한 간단하고 빠르게 완료하는 데 중점을 두었습니다. 코드베이스를 작게 유지하는 데 있어 두 번째로 중요한 부분은 최대한 많은 작업을 사전 처리 단계로 푸시하는 것이었습니다. 런타임 작업을 가져와서 사전 처리 작업으로 이동할 수 있다면 게임이 더 빠르게 실행될 뿐만 아니라 코드를 각 새 플랫폼으로 포팅할 필요가 없습니다. 예를 들어 원래는 수준 도형 데이터를 상당히 처리되지 않은 형식으로 저장하여 런타임에 실제 OpenGL/WebGL 정점 버퍼를 조합했습니다. 이를 위해 약간의 설정과 수백 줄의 런타임 코드가 필요했습니다. 나중에 이 코드를 전처리 단계로 이동하여 컴파일 시 완전히 패킹된 OpenGL/WebGL 정점 버퍼를 작성했습니다. 실제 코드의 양은 거의 동일했지만 수백 줄이 사전 처리 단계로 이동되었으므로 새 플랫폼으로 포팅할 필요가 없었습니다. Bouncy Mouse에는 이러한 예가 많이 있으며, 가능한 작업은 게임마다 다르지만 런타임에 실행될 필요가 없는 작업은 주의하세요.
필요하지 않은 종속 항목 사용하지 않기
Bouncy Mouse를 쉽게 포팅할 수 있는 또 다른 이유는 종속 항목이 거의 없기 때문입니다. 다음 차트에는 플랫폼별로 Bouncy Mouse의 주요 라이브러리 종속 항목이 요약되어 있습니다.
대략적인 내용은 이 정도입니다. 모든 플랫폼에서 이식 가능한 Box2D 외에는 큰 서드 파티 라이브러리가 사용되지 않았습니다. 그래픽의 경우 WebGL과 XNA 모두 OpenGL과 거의 1:1로 매핑되므로 큰 문제가 되지 않았습니다. 사운드 영역에서만 실제 라이브러리가 다릅니다. 하지만 Bouncy Mouse의 사운드 코드는 작기 때문에 (플랫폼별 코드 약 100줄) 큰 문제는 아닙니다. Bouncy Mouse에서 이식 불가능한 대규모 라이브러리를 사용하지 않으면 언어 변경사항에도 불구하고 런타임 코드의 로직이 버전 간에 거의 동일할 수 있습니다. 또한 이로 인해 휴대 불가능한 도구 체인에 갇히지 않을 수 있습니다. OpenGL/WebGL을 직접 코딩하면 Cocos2D 또는 Unity와 같은 라이브러리를 사용하는 것보다 복잡성이 증가하는지 문의하는 경우가 있습니다 (일부 WebGL 도우미도 있음). 사실 그 반대라고 생각합니다. 대부분의 휴대전화 / HTML5 게임 (적어도 Bouncy Mouse와 같은 게임)은 매우 간단합니다. 대부분의 경우 게임은 스프라이트 몇 개와 텍스처가 적용된 도형을 그리는 데 그칩니다. Bouncy Mouse의 OpenGL 관련 코드의 총합은 1,000줄 미만일 수 있습니다. 도우미 라이브러리를 사용해도 이 숫자가 실제로 줄어들지는 않을 것 같습니다. 이 숫자를 절반으로 줄여도 코드 500줄을 절약하기 위해 새로운 라이브러리/도구를 배우는 데 상당한 시간을 소비해야 합니다. 또한 관심 있는 모든 플랫폼에서 이식 가능한 도우미 라이브러리를 아직 찾지 못했습니다. 따라서 이러한 종속 항목을 사용하면 이식성이 크게 저하됩니다. 라이트맵, 동적 LOD, 스킨 애니메이션 등이 필요한 3D 게임을 작성하는 경우라면 답변이 달라질 것입니다. 이 경우 OpenGL에 맞게 전체 엔진을 직접 코딩하려고 하면서 헛수고를 하게 됩니다. 요점은 대부분의 모바일/HTML5 게임이 아직 이 카테고리에 속하지 않으므로 필요하지 않은 시점에 일을 복잡하게 만들 필요가 없다는 것입니다.
언어 간의 유사성을 과소평가하지 마세요
C++ 코드베이스를 새 언어로 포팅하는 데 많은 시간을 절약할 수 있었던 마지막 비결은 대부분의 코드가 각 언어 간에 거의 동일하다는 사실을 깨달은 것입니다. 일부 주요 요소는 변경될 수 있지만 변경되지 않는 요소에 비해 훨씬 적습니다. 실제로 많은 함수의 경우 C++에서 JavaScript로 전환하는 작업은 C++ 코드베이스에서 정규식 대체를 몇 번 실행하는 것뿐이었습니다.
포팅 결론
이 정도면 이전 프로세스를 완료한 것입니다. 다음 몇 섹션에서는 HTML5 관련 몇 가지 문제를 다루겠지만, 코드를 간단하게 유지하면 포팅이 악몽이 아닌 약간의 골칫거리가 될 것이라는 것이 핵심 메시지입니다.
오디오
저와 다른 모든 사용자에게 문제를 일으킨 부분은 오디오였습니다. iOS 및 Android에서는 여러 가지 확실한 오디오 옵션 (OpenSL, OpenAL)을 사용할 수 있지만 HTML5에서는 상황이 더 좋지 않았습니다. HTML5 오디오는 사용할 수 있지만 게임에 사용하면 심각한 문제가 발생하는 것으로 확인되었습니다. 최신 브라우저에서도 이상한 동작이 자주 발생했습니다. 예를 들어 Chrome에서는 동시에 만들 수 있는 오디오 요소 (소스) 수에 제한이 있는 것 같습니다. 또한 소리가 재생되더라도 설명할 수 없는 이유로 왜곡되는 경우가 있었습니다. 전반적으로 조금 걱정스러웠습니다. 온라인에서 검색한 결과 거의 모든 사용자에게 동일한 문제가 있는 것으로 확인되었습니다. 처음에 도달한 솔루션은 SoundManager2라는 API였습니다. 이 API는 사용 가능한 경우 HTML5 오디오를 사용하고 어려운 상황에서는 플래시로 대체합니다. 이 솔루션은 작동했지만 버그가 많고 예측할 수 없었습니다 (순수 HTML5 오디오보다는 버그가 적음). 출시 일주일 후 Google의 도움을 주는 직원과 대화를 나눴는데, 그 직원이 Webkit의 Web Audio API를 알려주었습니다. 원래 이 API를 사용해 보려고 했지만, 나에게 불필요한 복잡성이 많아서 사용하지 않았습니다. 몇 가지 소리를 재생하고 싶었습니다. HTML5 오디오를 사용하면 JavaScript 몇 줄이면 됩니다. 하지만 Web Audio를 간단히 살펴본 결과, 방대한 사양 (70페이지), 웹에 있는 소수의 샘플 (새로운 API의 일반적인 특징), 사양 어디에도 '재생', '일시중지', '중지' 함수가 누락된 점이 눈에 띄었습니다. 걱정은 근거가 없다는 Google의 확인을 받고 API를 다시 살펴봤습니다. 몇 가지 예시를 더 살펴보고 조사한 결과, Google의 말이 맞았습니다. 이 API는 확실히 내 요구사항을 충족할 수 있으며 다른 API를 괴롭히는 버그 없이도 이를 실행할 수 있습니다. 특히 Web Audio API 시작하기 도움말이 유용합니다. 이 도움말은 API에 관해 자세히 알아보려는 경우 유용한 정보가 많습니다. 실제 문제는 API를 이해하고 사용한 후에도 '몇 가지 사운드만 재생'하도록 설계되지 않은 API처럼 보인다는 점입니다. 이 문제를 해결하기 위해 사운드의 재생, 일시중지, 중지, 상태 쿼리 등을 원하는 방식으로 API를 사용할 수 있는 작은 도우미 클래스를 작성했습니다. 이 도우미 클래스를 AudioClip이라고 이름을 지었습니다. 전체 소스는 Apache 2.0 라이선스에 따라 GitHub에서 확인할 수 있으며, 아래에서 클래스의 세부정보를 설명하겠습니다. 먼저 Web Audio API에 관한 배경 정보를 알아보겠습니다.
웹 오디오 그래프
Web Audio API가 HTML5 Audio 요소보다 더 복잡하고 강력한 첫 번째 이유는 오디오를 사용자에게 출력하기 전에 오디오를 처리 / 믹스할 수 있다는 점입니다. 강력하지만 오디오 재생에 그래프가 포함되므로 간단한 시나리오에서는 상황이 조금 더 복잡해집니다. Web Audio API의 강력함을 보여주는 다음 그래프를 살펴보세요.
위 예에서는 Web Audio API의 강력한 기능을 보여줍니다. 하지만 제 시나리오에서는 이 기능의 대부분이 필요하지 않았습니다. 소리를 재생하고 싶었을 뿐입니다. 그래프가 여전히 필요하지만 그래프는 매우 간단합니다.
그래프는 간단할 수 있습니다.
Web Audio API가 HTML5 Audio 요소보다 더 복잡하고 강력한 첫 번째 이유는 오디오를 사용자에게 출력하기 전에 오디오를 처리 / 믹스할 수 있다는 점입니다. 강력하지만 오디오 재생에 그래프가 포함되므로 간단한 시나리오에서는 상황이 조금 더 복잡해집니다. Web Audio API의 강력함을 보여주는 다음 그래프를 살펴보세요.
위에 표시된 사소한 그래프로 소리를 재생, 일시중지 또는 중지하는 데 필요한 모든 작업을 실행할 수 있습니다.
그래프는 걱정하지 마세요.
그래프를 이해하는 것은 좋지만 사운드를 재생할 때마다 처리하고 싶지는 않습니다. 따라서 간단한 래퍼 클래스 'AudioClip'을 작성했습니다. 이 클래스는 이 그래프를 내부적으로 관리하지만 훨씬 더 간단한 사용자 대상 API를 제공합니다.
이 클래스는 Web Audio 그래프와 몇 가지 도우미 상태에 불과하지만 각 사운드를 재생하기 위해 Web Audio 그래프를 빌드해야 하는 것보다 훨씬 간단한 코드를 사용할 수 있습니다.
// At startup time
var sound = new AudioClip("ping.wav");
// Later
sound.play();
구현 세부정보
도우미 클래스의 코드를 간단히 살펴보겠습니다. constructors – 생성자는 XHR을 사용하여 소리 데이터 로드를 처리합니다. 예를 간단하게 유지하기 위해 여기에 표시되지는 않지만 HTML5 오디오 요소를 소스 노드로 사용할 수도 있습니다. 이는 대규모 샘플에 특히 유용합니다. Web Audio API에서는 이 데이터를 'arraybuffer'로 가져와야 합니다. 데이터가 수신되면 이 데이터에서 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 게임이 될 수 있도록 했습니다.
성능
JavaScript 포트와 관련하여 걱정되는 또 다른 영역은 성능입니다. 포트 v1을 완료한 후 쿼드 코어 데스크톱에서 모든 것이 제대로 작동하는 것을 확인했습니다. 안타깝게도 넷북이나 Chromebook에서는 문제가 조금 있었습니다. 이 경우 Chrome의 프로파일러가 모든 프로그램 시간이 어디에 소비되고 있는지 정확하게 보여줌으로써 문제를 해결했습니다.
최적화하기 전에 프로파일링하는 것이 중요하다는 점을 경험을 통해 알 수 있었습니다. Box2D 물리학 또는 렌더링 코드가 느려짐의 주요 원인이라고 생각했지만, 실제로는 Matrix.clone()
함수에서 대부분의 시간이 소요되었습니다. 게임이 수학을 많이 사용하는 특성을 고려할 때 많은 행렬 생성/클론을 실행한다는 사실을 알고 있었지만 이것이 병목 현상일 것이라고는 예상하지 못했습니다. 결국 매우 간단한 변경으로 게임의 CPU 사용량을 3배 이상 줄일 수 있었습니다. 데스크톱에서 CPU 사용량이 6~7% 에서 2%로 줄었습니다.
자바스크립트 개발자에게는 상식일 수도 있지만 C++ 개발자인 저는 이 문제에 놀랐습니다. 자세히 살펴보겠습니다. 기본적으로 원래 행렬 클래스는 3x3 행렬이었습니다. 즉, 3개 요소 배열로, 각 요소에는 3개 요소 배열이 포함되어 있습니다. 안타깝게도 매트릭스를 클론할 때마다 새 배열 4개를 만들어야 했습니다. 변경해야 할 유일한 사항은 이 데이터를 단일 9개 요소 배열로 이동하고 수학을 적절하게 업데이트하는 것이었습니다. 이 한 가지 변경사항으로 인해 CPU 사용량이 3배나 줄었습니다. 이 변경사항을 적용한 후 모든 테스트 기기에서 성능이 허용 가능했습니다.
추가 최적화
성능은 허용 가능한 수준이었지만 몇 가지 사소한 문제가 있었습니다. 프로파일링을 좀 더 진행한 결과, 이 문제가 JavaScript의 가비지 컬렉션 때문이라는 것을 알게 되었습니다. 앱이 60fps로 실행되고 있었으므로 각 프레임은 그리기에 16ms밖에 걸리지 않았습니다. 안타깝게도 느린 머신에서 가비지 컬렉션이 시작되면 때때로 약 10ms가 소모되었습니다. 이로 인해 게임이 전체 프레임을 그리는 데 거의 16ms가 소요되어 몇 초마다 끊김이 발생했습니다. 왜 이렇게 많은 가비지가 생성되는지 더 잘 파악하기 위해 Chrome의 힙 프로파일러를 사용했습니다. 안타깝게도 대부분의 가비지 (70% 이상)가 Box2D에서 생성되고 있었습니다. JavaScript에서 가비지를 제거하는 것은 까다로운 작업이며 Box2D를 다시 작성하는 것은 불가능하므로 난감한 상황에 처했음을 깨달았습니다. 다행히도 가장 오래된 트릭 중 하나를 사용할 수 있었습니다. 60fps를 달성할 수 없는 경우 30fps로 실행합니다. 끊김이 있는 60fps로 실행하는 것보다 일관된 30fps로 실행하는 것이 훨씬 낫다는 데는 거의 모든 사람이 동의합니다. 사실 게임이 30fps로 실행된다는 불만이나 의견은 아직 한 건도 접수되지 않았습니다 (두 버전을 나란히 비교하지 않는 한 알기가 정말 어렵습니다). 프레임당 16ms가 추가되어 가비지 컬렉션이 불안정해도 프레임을 렌더링할 시간이 충분했습니다. 사용 중인 타이밍 API (WebKit의 우수한 requestAnimationFrame)에서 30fps로 실행하는 것이 명시적으로 사용 설정되지는 않지만 매우 간단한 방식으로 실행할 수 있습니다. 명시적 API만큼 우아하지는 않지만 RequestAnimationFrame의 간격이 모니터의 VSYNC (일반적으로 60fps)에 맞춰져 있는지 알면 30fps를 실행할 수 있습니다. 즉, 다른 모든 콜백을 무시하면 됩니다. 기본적으로 'RequestAnimationFrame'이 실행될 때마다 호출되는 콜백 'Tick'이 있는 경우 다음과 같이 실행할 수 있습니다.
var skip = false;
function Tick() {
skip = !skip;
if (skip) {
return;
}
// OTHER CODE
}
특히 주의해야 하는 경우 시작 시 컴퓨터의 VSYNC가 30fps 이하가 아닌지 확인하고 이 경우 건너뛰기를 사용 중지해야 합니다. 하지만 테스트한 데스크톱/노트북 구성에서는 아직 이 문제가 발생하지 않았습니다.
배포 및 수익 창출
Bouncy Mouse의 Chrome 포트에서 놀랐던 마지막 부분은 수익 창출이었습니다. 이 프로젝트를 시작할 때 HTML5 게임을 떠오르는 기술을 배우는 흥미로운 실험으로 생각했습니다. 이 포팅이 매우 많은 시청자에게 도달하고 상당한 수익 창출 잠재력이 있다는 사실을 몰랐습니다.
Bouncy Mouse는 10월 말에 Chrome 웹 스토어에 출시되었습니다. Chrome 웹 스토어에 출시함으로써 모바일 플랫폼에서 익숙해진 검색 가능성, 커뮤니티 참여도, 순위, 기타 기능을 위한 기존 시스템을 활용할 수 있었습니다. 매장의 도달범위가 얼마나 넓은지 놀랐습니다. 출시 한 달 만에 설치 수가 40만 개에 가까워졌고 이미 커뮤니티 참여 (버그 신고, 의견)의 혜택을 누리고 있었습니다. 또 하나 놀랐던 점은 웹 앱의 수익 창출 잠재력입니다.
Bouncy Mouse에는 게임 콘텐츠 옆에 배너 광고를 게재하는 간단한 수익 창출 방법이 하나 있습니다. 하지만 게임의 광범위한 도달범위를 고려할 때 이 배너 광고는 상당한 수익을 창출할 수 있었습니다. 최대 실적을 올리던 시기에 앱은 가장 성공적인 플랫폼인 Android와 비슷한 수준의 수익을 창출했습니다. 여기에는 HTML5 버전에 게재되는 더 큰 애드센스 광고가 Android에 게재되는 더 작은 Admob 광고보다 노출당 수익이 훨씬 더 높다는 점이 한 가지 원인으로 작용합니다. 뿐만 아니라 HTML5 버전의 배너 광고는 Android 버전보다 훨씬 눈에 잘 띄지 않아 더 깔끔한 게임플레이 환경을 제공합니다. 전반적으로 이번 결과에 매우 만족했습니다.

게임에서 발생한 수익은 예상보다 훨씬 많았지만, Chrome 웹 스토어의 도달범위는 아직 Android 마켓과 같은 더 성숙한 플랫폼보다 작습니다. Bouncy Mouse는 Chrome 웹 스토어에서 가장 인기 있는 게임 9위까지 빠르게 올라갔지만, 초기 출시 이후 사이트를 방문하는 신규 사용자의 비율이 크게 느려졌습니다. 하지만 게임은 여전히 꾸준히 성장하고 있으며 앞으로 이 플랫폼이 어떻게 발전할지 기대됩니다.
결론
Bouncy Mouse를 Chrome으로 포팅하는 작업이 예상보다 훨씬 원활하게 진행되었습니다. 몇 가지 사소한 오디오 및 성능 문제 외에도 Chrome은 기존 스마트폰 게임을 실행하기에 완벽한 플랫폼이라는 것을 알게 되었습니다. 이 환경을 꺼려했던 개발자는 한 번 시도해 보시기 바랍니다. 포팅 프로세스와 HTML5 게임을 통해 만난 새로운 게임 시청자 모두에게 만족하고 있습니다. 궁금한 점이 있으면 언제든지 이메일을 보내주세요. 또는 아래에 댓글을 남겨 주시면 정기적으로 확인해 보겠습니다.