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

소개

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

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

문제

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

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

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);

이제 애셋 관리자가 로드에 성공한 애셋과 실패한 애셋을 모두 추적합니다.

완료 시 신호

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

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

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

애셋 관리자는 successCount + 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);

카운터가 증가하면 대기열의 마지막 애셋인지 확인합니다. 애셋 관리자의 다운로드가 실제로 완료된 경우 정확히 어떻게 해야 하나요?

애셋 관리자가 모든 애셋을 다운로드한 경우 콜백 메서드가 호출됩니다. 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: HTML5 게임 개발 소개'라는 제목의 Google IO 강연 (슬라이드, 동영상)의 주제였습니다.

요약

대부분의 게임에는 일종의 애셋 관리자가 있지만 HTML5 게임에는 네트워크를 통해 애셋을 로드하고 오류를 처리하는 애셋 관리자가 필요합니다. 이 도움말에서는 다음 HTML5 게임에 쉽게 사용하고 적용할 수 있는 간단한 애셋 관리자를 간략히 설명했습니다. 즐겁게 사용해 보시고 아래 댓글을 통해 의견을 알려주세요. 감사합니다.