HTML5 게임에 대한 가이드

다니엘 X. Moore
다니엘 X. 무어

소개

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

본 가이드에서는 적어도 중급 이상의 자바스크립트에 대해 알고 있다고 가정합니다.

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

캔버스 만들기

항목을 그리려면 캔버스를 만들어야 합니다. 이 가이드는 눈물 없는 가이드이므로 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);

지금은 업데이트 및 그리기 메서드를 비워 둘 수 있습니다. 알아야 할 중요한 점은 setInterval()가 주기적으로 호출을 처리한다는 것입니다.

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

Hello World

이제 게임 루프가 준비되었으므로 그리기 메서드를 업데이트하여 실제로 화면에 텍스트를 그려 보겠습니다.

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

이제 지휘봉을 빙글빙글 돌려 보세요. 따라가는 경우라면 이동하되 이전에 화면에 그려진 시간도 그대로 두어야 합니다. 잠시 시간을 내어 그 이유를 추측해 보세요. 이는 Google에서 화면을 지우지 않기 때문입니다. 이제 화면 삭제 코드를 그리기 메서드에 추가해 보겠습니다.

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 핫키가 이를 잘 처리해줍니다.

플레이어 움직임

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

이제 플레이어와 적에게 폭발 메서드를 추가해야 합니다. 그러면 제거하도록 신고되고 폭발을 일으킵니다.

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에서는 이미지와 마찬가지로 사운드를 사용하기가 다소 어려울 수 있지만, Google의 마법 같은 '눈물 없이(no-tears)' 형식인 Sound.js 덕분에 사운드를 매우 간단하게 만들 수 있습니다.

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

function Enemy(I) {
  ...

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

API는 이제 찢어지지 않지만 사운드를 추가하는 것이 현재 애플리케이션을 다운시키는 가장 빠른 방법입니다. 사운드가 잘리거나 브라우저 탭 전체가 삭제되는 경우가 드물지 않으므로 화장대를 준비합니다.

작별

정상적으로 작동하는 게임 데모를 확인해 보세요. 소스 코드를 ZIP 파일로 다운로드할 수도 있습니다.

자바스크립트 및 HTML5로 간단한 게임을 만드는 기본적인 방법을 배웠길 바랍니다. 적절한 수준의 추상화에서 프로그래밍하면 API의 더 어려운 부분에서 벗어날 수 있을 뿐만 아니라 향후 변화에 대비할 수 있습니다.

참조