HTML5 게임에 대한 가이드

Daniel X. Moore
Daniel X. Moore

소개

캔버스와 HTML5를 사용하여 게임을 만들고 싶으신가요? 이 튜토리얼을 따라하면 금방 시작할 수 있습니다.

이 튜토리얼에서는 JavaScript에 대한 중급 수준의 지식이 있다고 가정합니다.

먼저 게임을 플레이하거나 도움말로 바로 이동하여 게임의 소스 코드를 확인할 수 있습니다.

캔버스 만들기

무언가를 그리려면 캔버스를 만들어야 합니다. 이 가이드는 초보자를 위한 가이드이므로 jQuery를 사용합니다.

var CANVAS_WIDTH = 480;
var CANVAS_HEIGHT = 320;

var canvasElement = $("<canvas width='" + CANVAS_WIDTH + 
                      "' height='" + CANVAS_HEIGHT + "'></canvas>");
var canvas = canvasElement.get(0).getContext("2d");
canvasElement.appendTo('body');

게임 루프

원활하고 연속적인 게임플레이의 모습을 시뮬레이션하려면 인간의 두뇌와 눈이 인식할 수 있는 것보다 약간 더 빠르게 게임을 업데이트하고 화면을 다시 그려야 합니다.

var FPS = 30;
setInterval(function() {
  update();
  draw();
}, 1000/FPS);

지금은 update 및 draw 메서드를 비워두어도 됩니다. 중요한 것은 setInterval()가 주기적으로 호출을 처리한다는 것입니다.

function update() { ... }
function draw() { ... }

Hello World

이제 게임 루프가 실행되고 있으므로 화면에 텍스트를 실제로 그리도록 draw 메서드를 업데이트해 보겠습니다.

function draw() {
  canvas.fillStyle = "#000"; // Set color to black
  canvas.fillText("Sup Bro!", 50, 50);
}

고정된 텍스트에는 꽤 멋진 기능이지만 게임 루프가 이미 설정되어 있으므로 쉽게 움직일 수 있습니다.

var textX = 50;
var textY = 50;

function update() {
  textX += 1;
  textY += 1;
}

function draw() {
  canvas.fillStyle = "#000";
  canvas.fillText("Sup Bro!", textX, textY);
}

이제 한번 사용해 보세요. 따라가다 보면 움직이면서도 화면에 그려진 이전 시간도 남아 있어야 합니다. 잠시 시간을 내어 그 이유를 생각해 보세요. 화면을 지우지 않기 때문입니다. 따라서 draw 메서드에 화면 지우기 코드를 추가해 보겠습니다.

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  canvas.fillStyle = "#000";
  canvas.fillText("Sup Bro!", textX, textY);
}

이제 화면에서 텍스트가 움직이므로 실제 게임을 만드는 데 한 걸음 다가간 것입니다. 컨트롤을 조정하고 게임플레이를 개선하며 그래픽을 수정하면 됩니다. 실제 게임을 만드는 데는 아직 7분의 1 정도 남았지만 좋은 소식은 튜토리얼에 더 많은 내용이 있다는 것입니다.

플레이어 만들기

플레이어 데이터를 보관하고 그리기와 같은 작업을 담당할 객체를 만듭니다. 여기서는 간단한 객체 리터럴을 사용하여 모든 정보를 보유하는 플레이어 객체를 만듭니다.

var player = {
  color: "#00A",
  x: 220,
  y: 270,
  width: 32,
  height: 32,
  draw: function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  }
};

지금은 간단한 색상 직사각형을 사용하여 플레이어를 나타냅니다. 게임을 그릴 때 캔버스를 지우고 플레이어를 그립니다.

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  player.draw();
}

키보드 컨트롤

jQuery 핫키 사용

jQuery 핫키 플러그인을 사용하면 여러 브라우저에서 키를 훨씬 더 쉽게 처리할 수 있습니다. 해독할 수 없는 교차 브라우저 keyCodecharCode 문제에 대해 걱정하는 대신 다음과 같이 이벤트를 바인딩할 수 있습니다.

$(document).bind("keydown", "left", function() { ... });

어떤 키에 어떤 코드가 있는지 세부적으로 신경 쓰지 않아도 되므로 큰 이점이 있습니다. '플레이어가 위 버튼을 누르면 뭔가를 실행합니다.'와 같이 말할 수 있으면 됩니다. jQuery Hotkeys를 사용하면 쉽게 할 수 있습니다.

플레이어 이동

JavaScript가 키보드 이벤트를 처리하는 방식은 완전히 이벤트 기반입니다. 즉, 키가 다운되었는지 확인하는 기본 제공 쿼리가 없으므로 자체 쿼리를 사용해야 합니다.

'키를 처리하는 이벤트 기반 방식을 사용하면 안 되나요?'라고 궁금할 수 있습니다. 키보드 반복 속도는 시스템마다 다르고 게임 루프의 타이밍에 연결되어 있지 않으므로 게임플레이가 시스템마다 크게 다를 수 있기 때문입니다. 일관된 환경을 만들려면 키보드 이벤트 감지를 게임 루프와 긴밀하게 통합하는 것이 중요합니다.

다행히 이벤트 쿼리를 사용할 수 있는 16줄의 JS 래퍼를 포함했습니다. 이 파일의 이름은 key_status.js이며 언제든지 keydown.left 등을 확인하여 키의 상태를 쿼리할 수 있습니다.

이제 키가 눌려 있는지 쿼리할 수 있으므로 이 간단한 업데이트 메서드를 사용하여 플레이어를 이동할 수 있습니다.

function update() {
  if (keydown.left) {
    player.x -= 2;
  }

  if (keydown.right) {
    player.x += 2;
  }
}

한번 사용해 보세요.

플레이어를 화면 밖으로 이동할 수 있습니다. 플레이어의 위치를 경계 내에 유지하도록 제한해 보겠습니다. 또한 플레이어가 약간 느린 것 같으니 속도도 높여 보겠습니다.

function update() {
  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

입력을 더 추가하는 것도 마찬가지로 쉽습니다. 일종의 발사체를 추가해 보겠습니다.

function update() {
  if (keydown.space) {
    player.shoot();
  }

  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

player.shoot = function() {
  console.log("Pew pew");
  // :) Well at least adding the key binding was easy...
};

게임 객체 추가

투사체

이제 실제로 발사체를 추가해 보겠습니다. 먼저 이를 모두 저장할 컬렉션이 필요합니다.

var playerBullets = [];

다음으로 글머리기호 인스턴스를 만드는 생성자가 필요합니다.

function Bullet(I) {
  I.active = true;

  I.xVelocity = 0;
  I.yVelocity = -I.speed;
  I.width = 3;
  I.height = 3;
  I.color = "#000";

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.active = I.active && I.inBounds();
  };

  return I;
}

플레이어가 발사하면 총알 인스턴스를 만들어 총알 컬렉션에 추가해야 합니다.

player.shoot = function() {
  var bulletPosition = this.midpoint();

  playerBullets.push(Bullet({
    speed: 5,
    x: bulletPosition.x,
    y: bulletPosition.y
  }));
};

player.midpoint = function() {
  return {
    x: this.x + this.width/2,
    y: this.y + this.height/2
  };
};

이제 업데이트 단계 함수에 글머리기호 업데이트를 추가해야 합니다. 글머리 기호 모음이 무한대로 채워지지 않도록 하려면 활성 글머리 기호만 포함하도록 글머리 기호 목록을 필터링합니다. 이렇게 하면 적과 충돌한 총알을 삭제할 수도 있습니다.

function update() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.update();
  });

  playerBullets = playerBullets.filter(function(bullet) {
    return bullet.active;
  });
}

마지막 단계는 글머리 기호를 그리는 것입니다.

function draw() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.draw();
  });
}

이제 총알을 추가한 것과 거의 동일한 방식으로 적을 추가해 보겠습니다.

  enemies = [];

function Enemy(I) {
  I = I || {};

  I.active = true;
  I.age = Math.floor(Math.random() * 128);

  I.color = "#A2B";

  I.x = CANVAS_WIDTH / 4 + Math.random() * CANVAS_WIDTH / 2;
  I.y = 0;
  I.xVelocity = 0
  I.yVelocity = 2;

  I.width = 32;
  I.height = 32;

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.xVelocity = 3 * Math.sin(I.age * Math.PI / 64);

    I.age++;

    I.active = I.active && I.inBounds();
  };

  return I;
};

function update() {
  ...

  enemies.forEach(function(enemy) {
    enemy.update();
  });

  enemies = enemies.filter(function(enemy) {
    return enemy.active;
  });

  if(Math.random() < 0.1) {
    enemies.push(Enemy());
  }
};

function draw() {
  ...

  enemies.forEach(function(enemy) {
    enemy.draw();
  });
}

이미지 로드 및 그리기

상자가 날아다니는 모습을 보는 것도 좋지만 상자에 대한 이미지가 있으면 더 좋을 것 같습니다. 캔버스에 이미지를 로드하고 그리는 작업은 일반적으로 눈물겨운 경험입니다. 이러한 고통과 불행을 방지하려면 간단한 유틸리티 클래스를 사용하면 됩니다.

player.sprite = Sprite("player");

player.draw = function() {
  this.sprite.draw(canvas, this.x, this.y);
};

function Enemy(I) {
  ...

  I.sprite = Sprite("enemy");

  I.draw = function() {
    this.sprite.draw(canvas, this.x, this.y);
  };

  ...
}

충돌 감지

화면에 여러 요소가 날아다니지만 서로 상호작용하지는 않습니다. 모든 객체에 폭발 시점을 알리려면 일종의 충돌 감지를 추가해야 합니다.

간단한 직사각형 충돌 감지 알고리즘을 사용해 보겠습니다.

function collides(a, b) {
  return a.x < b.x + b.width &&
         a.x + a.width > b.x &&
         a.y < b.y + b.height &&
         a.y + a.height > b.y;
}

몇 가지 충돌을 확인해야 합니다.

  1. 플레이어 총알 => 적 함선
  2. 플레이어 => 적 함선

업데이트 메서드에서 호출할 수 있는 충돌을 처리하는 메서드를 만들어 보겠습니다.

function handleCollisions() {
  playerBullets.forEach(function(bullet) {
    enemies.forEach(function(enemy) {
      if (collides(bullet, enemy)) {
        enemy.explode();
        bullet.active = false;
      }
    });
  });

  enemies.forEach(function(enemy) {
    if (collides(enemy, player)) {
      enemy.explode();
      player.explode();
    }
  });
}

function update() {
  ...
  handleCollisions();
}

이제 플레이어와 적에 explode 메서드를 추가해야 합니다. 이렇게 하면 삭제 대상으로 표시되고 폭발이 추가됩니다.

function Enemy(I) {
  ...

  I.explode = function() {
    this.active = false;
    // Extra Credit: Add an explosion graphic
  };

  return I;
};

player.explode = function() {
  this.active = false;
  // Extra Credit: Add an explosion graphic and then end the game
};

소리

환경을 완성하기 위해 멋진 음향 효과를 추가할 예정입니다. 이미지와 마찬가지로 소리도 HTML5에서 사용하기에는 다소 번거로울 수 있지만, 눈물 없이 마법처럼 작동하는 sound.js 덕분에 소리를 매우 간단하게 만들 수 있습니다.

player.shoot = function() {
  Sound.play("shoot");
  ...
}

function Enemy(I) {
  ...

  I.explode = function() {
    Sound.play("explode");
    ...
  }
}

이제 API가 티어링이 없지만, 현재 사운드를 추가하는 것이 애플리케이션을 비정상 종료하는 가장 빠른 방법입니다. 소리가 갑자기 끊기거나 브라우저 탭 전체가 다운되는 경우가 많으니 휴지를 준비해 두세요.

Farewell

다시 한번 작동하는 전체 게임 데모를 확인하세요. 소스 코드를 zip으로 다운로드할 수도 있습니다.

JavaScript 및 HTML5로 간단한 게임을 만드는 기본사항을 알아보는 것이 즐거웠기를 바랍니다. 적절한 추상화 수준에서 프로그래밍하면 API의 더 어려운 부분으로부터 격리할 수 있을 뿐만 아니라 향후 변경사항에 대비할 수 있습니다.

참조