HTML5 게임을 위한 간단한 애셋 관리

세스 래드

소개

HTML5는 브라우저에서 반응성이 뛰어나고 강력한 최신 웹 애플리케이션을 개발하는 데 유용한 여러 API를 제공합니다. 좋은 기능이지만 실제로 게임을 빌드하고 플레이하고 싶을 겁니다. 다행히도 HTML5는 캔버스와 같은 API 및 강력한 JavaScript 엔진을 사용하여 플러그인 없이 브라우저에서 바로 게임을 제공하는 게임 개발의 새로운 시대를 열었습니다.

이 도움말에서는 HTML5 게임을 위한 간단한 애셋 관리 구성요소를 빌드하는 방법을 안내합니다. 애셋 관리자가 없으면 게임에서 알 수 없는 다운로드 시간과 비동기 이미지 로드를 보완하기가 어렵습니다. 따라 하면서 HTML5 게임을 위한 간단한 애셋 관리자의 예를 확인하세요.

문제

HTML5 게임은 HTTP를 통해 다운로드된 애셋이 있는 웹브라우저에서 플레이되는 것을 암시하기 때문에 HTML5 게임은 이미지 또는 오디오와 같은 애셋이 플레이어의 로컬 시스템에 있을 것이라고 가정할 수 없습니다. 네트워크가 관련되어 있기 때문에 브라우저는 게임의 애셋이 언제 다운로드되고 사용 가능한지 알 수 없습니다.

웹브라우저에서 프로그래매틱 방식으로 이미지를 로드하는 기본 방법은 다음 코드입니다.

var image = new Image();
image.addEventListener("success", function(e) {
  // do stuff with the image
});
image.src = "/some/image.png";

이제 게임이 시작될 때 로드 및 표시해야 하는 100개의 이미지가 있다고 가정해 보겠습니다. 이미지 100개가 모두 준비되었다는 것을 어떻게 알 수 있을까요? 모두 성공적으로 로드되었나요? 실제로 언제 게임을 시작해야 할까요?

해결 방법

애셋 관리자가 애셋의 대기열을 처리하고 모든 것이 준비되면 게임에 다시 보고하도록 합니다. 자산 관리자는 네트워크를 통해 자산을 로드하는 로직을 일반화하며 상태를 쉽게 확인할 수 있는 방법을 제공합니다.

간단한 애셋 관리자의 요구사항은 다음과 같습니다.

  • 다운로드 대기열에 추가
  • 다운로드 시작
  • 성공 및 실패 추적
  • 모든 작업이 완료되었을 때 신호
  • 손쉬운 자산 검색

큐에 추가하는 중

첫 번째 요구사항은 다운로드를 대기열에 추가하는 것입니다. 이러한 디자인을 사용하면 필요한 애셋을 실제로 다운로드하지 않고도 선언할 수 있습니다. 이 방법은 예를 들어 구성 파일에서 게임 레벨의 모든 애셋을 선언하려는 경우에 유용할 수 있습니다.

생성자 및 큐의 코드는 다음과 같습니다.

function AssetManager() {
  this.downloadQueue = [];
}

AssetManager.prototype.queueDownload = function(path) {
    this.downloadQueue.push(path);
}

다운로드 시작

다운로드할 모든 애셋을 대기열에 추가한 후 애셋 관리자에게 모든 애셋을 다운로드하도록 요청할 수 있습니다.

웹 브라우저는 다운로드를 병렬 처리할 수 있습니다. 다행히 일반적으로 호스트당 최대 4개의 연결이 가능합니다. 애셋 다운로드 속도를 높이는 한 가지 방법은 애셋 호스팅에 다양한 도메인 이름을 사용하는 것입니다. 예를 들어 assets.example.com의 모든 항목을 게재하는 대신 assets1.example.com, assets2.example.com, assets3.example.com 등을 사용합니다. 이러한 각 도메인 이름이 단순히 동일한 웹 서버에 대한 CNAME이라 할지라도, 웹 브라우저는 이를 별도의 서버로 간주하여 자산 다운로드에 사용되는 연결의 수를 증가시킵니다. 웹사이트 속도 향상을 위한 권장사항의 도메인 간 구성요소 분할에서 이 기법에 대해 자세히 알아보세요.

다운로드 초기화를 위한 메서드는 downloadAll()라고 합니다. 시간이 지나면서 만들어 질 것입니다. 지금은 다음과 같이 다운로드를 시작하는 첫 번째 로직이 있습니다.

AssetManager.prototype.downloadAll = function() {
    for (var i = 0; i < this.downloadQueue.length; i++) {
        var path = this.downloadQueue[i];
        var img = new Image();
        var that = this;
        img.addEventListener("load", function() {
            // coming soon
        }, false);
        img.src = path;
    }
}

위 코드에서 볼 수 있듯이 downloadAll()는 간단히 downloadQueue를 통해 반복하고 새 Image 객체를 만듭니다. 로드 이벤트에 대한 이벤트 리스너가 추가되고 이미지의 src가 설정되어 실제 다운로드를 트리거합니다.

이 방법으로 다운로드를 시작할 수 있습니다.

성공 및 실패 추적

또 다른 요구사항은 성공과 실패를 모두 추적하는 것입니다. 안타깝게도 모든 것이 완벽하게 작동하지는 않기 때문입니다. 지금까지의 코드는 성공적으로 다운로드된 애셋만 추적합니다. 오류 이벤트에 대한 이벤트 리스너를 추가하면 성공 및 실패 시나리오를 모두 캡처할 수 있습니다.

AssetManager.prototype.downloadAll = function(downloadCallback) {
  for (var i = 0; i < this.downloadQueue.length; i++) {
    var path = this.downloadQueue[i];
    var img = new Image();
    var that = this;
    img.addEventListener("load", function() {
        // coming soon
    }, false);
    img.addEventListener("error", function() {
        // coming soon
    }, false);
    img.src = path;
  }
}

애셋 관리자는 우리가 얼마나 많은 성공과 실패를 경험했는지 알아야 하며, 그러지 않으면 게임이 언제 시작될 수 있는지 결코 알지 못합니다.

먼저 생성자의 객체에 카운터를 추가합니다. 이제 다음과 같이 표시됩니다.

function AssetManager() {
<span class="highlight">    this.successCount = 0;
    this.errorCount = 0;</span>
    this.downloadQueue = [];
}

이제 다음과 같이 이벤트 리스너의 카운터를 증가시킵니다.

img.addEventListener("load", function() {
    <span class="highlight">that.successCount += 1;</span>
}, false);
img.addEventListener("error", function() {
    <span class="highlight">that.errorCount += 1;</span>
}, false);

이제 Asset Manager는 성공적으로 로드된 애셋과 실패한 애셋을 모두 추적합니다.

완료 시 알림

게임이 다운로드를 위해 애셋을 대기열에 추가하고 애셋 관리자에게 모든 애셋을 다운로드하도록 요청한 후에는 모든 애셋이 다운로드될 때 게임에 알려야 합니다. 게임에서 애셋이 다운로드되었는지 반복해서 묻는 대신 애셋 관리자가 게임에 다시 신호를 보낼 수 있습니다.

애셋 관리자는 먼저 모든 애셋이 완료된 시점을 알아야 합니다. 이제 isDone 메서드를 추가합니다.

AssetManager.prototype.isDone = function() {
    return (this.downloadQueue.length == this.successCount + this.errorCount);
}

애셋 관리자는 성공 횟수 + errorCount와 downloadQueue의 크기를 비교하여 모든 애셋이 성공적으로 완료되었는지 또는 일종의 오류가 발생했는지 파악합니다.

물론, 완료 여부를 아는 것은 전 과정의 절반에 불과하므로 자산 관리자도 이 방법을 확인해야 합니다. 아래 코드와 같이 두 이벤트 핸들러 내에 이 검사를 추가합니다.

img.addEventListener("load", function() {
    console.log(this.src + ' is loaded');
    that.successCount += 1;
    if (that.isDone()) {
        // ???
    }
}, false);
img.addEventListener("error", function() {
    that.errorCount += 1;
if (that.isDone()) {
        // ???
    }
}, false);

카운터가 증가된 후 큐의 마지막 애셋인지 확인합니다. Asset Manager가 실제로 다운로드를 완료한 경우 정확히 어떻게 해야 하나요?

애셋 관리자가 모든 애셋 다운로드를 완료하면 당연히 콜백 메서드가 호출됩니다. downloadAll()를 변경하고 콜백 매개변수를 추가해 보겠습니다.

AssetManager.prototype.downloadAll = function(downloadCallback) {
    ...

이벤트 리스너 내에서 downloadCallback 메서드를 호출합니다.

img.addEventListener("load", function() {
    that.successCount += 1;
    if (that.isDone()) {
        downloadCallback();
    }
}, false);
img.addEventListener("error", function() {
    that.errorCount += 1;
    if (that.isDone()) {
        downloadCallback();
    }
}, false);

애셋 관리자가 드디어 마지막 요구사항을 충족합니다.

간편한 애셋 검색

게임이 시작될 수 있다는 신호를 받으면 게임이 이미지를 렌더링하기 시작합니다. 애셋 관리자는 애셋을 다운로드하고 추적하는 것은 물론 이를 게임에 제공하는 역할도 담당합니다.

최종 요구사항에는 일종의 getAsset 메서드가 포함되어 있으므로 지금 추가하겠습니다.

AssetManager.prototype.getAsset = function(path) {
    return this.cache[path];
}

이 캐시 객체는 생성자에서 초기화되며 이제 다음과 같이 표시됩니다.

function AssetManager() {
    this.successCount = 0;
    this.errorCount = 0;
    this.cache = {};
    this.downloadQueue = [];
}

캐시는 아래와 같이 downloadAll()의 끝에 채워집니다.

AssetManager.prototype.downloadAll = function(downloadCallback) {
  ...
      img.addEventListener("error", function() {
          that.errorCount += 1;
          if (that.isDone()) {
              downloadCallback();
          }
      }, false);
      img.src = path;
      <span class="highlight">this.cache[path] = img;</span>
  }
}

보너스: 버그 수정

버그를 발견하셨나요? 위에서 설명한 것처럼 isDone 메서드는 로드 또는 오류 이벤트가 트리거될 때만 호출됩니다. 하지만 애셋 관리자에 다운로드 대기열에 추가된 애셋이 없는 경우에는 어떻게 해야 할까요? isDone 메서드가 트리거되지 않고 게임이 시작되지 않습니다.

downloadAll()에 다음 코드를 추가하여 이 시나리오를 처리할 수 있습니다.

AssetManager.prototype.downloadAll = function(downloadCallback) {
    if (this.downloadQueue.length === 0) {
      downloadCallback();
  }
 ...

대기 중인 애셋이 없으면 콜백이 즉시 호출됩니다. 버그가 수정되었습니다.

사용 예

HTML5 게임에서 이 애셋 관리자를 사용하는 방법은 매우 간단합니다. 다음은 라이브러리를 사용하는 가장 기본적인 방법입니다.

var ASSET_MANAGER = new AssetManager();

ASSET_MANAGER.queueDownload('img/earth.png');

ASSET_MANAGER.downloadAll(function() {
    var sprite = ASSET_MANAGER.getAsset('img/earth.png');
    ctx.drawImage(sprite, x - sprite.width/2, y - sprite.height/2);
});

위의 코드는 다음을 보여줍니다.

  1. 새 애셋 관리자를 만듭니다.
  2. 다운로드할 애셋을 대기열에 추가합니다.
  3. downloadAll()로 다운로드 시작
  4. 콜백 함수를 호출하여 애셋이 준비되면 알림
  5. getAsset()로 애셋 검색

개선이 필요한 영역

게임을 개발하다 보면 이 단순한 애셋 관리자보다 더 나아질 거예요. 물론 기본적인 시작이 되었길 바랍니다. 향후 기능에는 다음이 포함될 수 있습니다.

  • 오류 메시지 표시
  • 진행 상황을 나타내는 콜백
  • File System API에서 애셋 검색

아래 댓글에 개선사항, 포크, 코드 링크를 게시하세요.

전체 소스

이 애셋 관리자 및 애셋 관리자의 소스는 Apache 라이선스에 따른 오픈소스이며 Bad Aliens GitHub 계정에서 확인할 수 있습니다. Bad Aliens 게임은 HTML5 호환 브라우저에서 플레이할 수 있습니다. 이 게임은 Super Browser 2 Turbo HD Remix: Introduction to HTML5 Game Development (슬라이드, 동영상)라는 제목의 Google IO 토크의 주제였습니다.

요약

대부분의 게임에는 일종의 애셋 관리자가 있지만, HTML5 게임에는 네트워크를 통해 애셋을 로드하고 실패를 처리하는 애셋 관리자가 필요합니다. 이 도움말에서는 간편하게 사용하고 다음 HTML5 게임에 맞게 조정할 수 있는 간단한 애셋 관리자에 대해 설명합니다. 즐거운 시간 보내시고 아래의 댓글에 의견을 남겨 주세요. 감사합니다.