このページの本文へ

知ってる?Web開発者がJavaScriptでゲームを作るときのはじめの一歩

2016年08月23日 10時28分更新

文●Mark Brown

  • この記事をはてなブックマークに追加
本文印刷
Web開発者が初めてゲームを作るときに戸惑うのが、「ゲームループ」という考え方。普通のWebアプリを作るのとどう違うのか、シンプルなゲームのプログラム構造に触れてみましょう。

「ゲームループ」は、時間をかけて状態を変化させることでアニメーションやゲームをレンダリングするために使う技術につけられた名前です。本来は、ユーザー入力を受け取り、経過時間の状態を更新してからフレームをできるだけ多くの回数描く関数です。

この短い記事では、基本的な技術がどう機能するかを説明します。基本的な技術が分かれば、ブラウザベースのゲームやアニメーションの制作を始められます。

ゲームループは、JavaScriptでは次のようになります。

function update(progress) {
  // Update the state of the world for the elapsed time since last render
}

function draw() {
  // Draw the state of the world
}

function loop(timestamp) {
  var progress = timestamp - lastRender

  update(progress)
  draw()

  lastRender = timestamp
  window.requestAnimationFrame(loop)
}
var lastRender = 0
window.requestAnimationFrame(loop)

requestAnimationFrameメソッドは、次の再描画の直前に、指定した関数を呼び出すようにブラウザーに要求します。アニメーションのレンダリング専用のAPIですが、タイムアウトの短いsetTimeoutでも同様の結果を得られます。 requestAnimationFrameはコールバックが発生したときにタイムスタンプを渡されます。タイムスタンプはperformance.now()と同等で、ウィンドウが読み込まれてからのミリ秒数が含まれています。

progress値、または各レンダリング間の時間は、スムーズなアニメーションの作成に重要です。update関数でxとyの位置の調整に使用することで、アニメーションが一貫した速度で動くことを確認します。

位置の更新

最初のアニメーションはとてもシンプルです。赤い正方形がキャンバスの右端まで移動し、開始地点へ戻ることを繰り返します。

正方形の位置を記憶し、update関数のx位置をインクリメントする必要があります。境界に到達したときに、開始地点へ戻るキャンバスの幅を差し引きます。

var width = 800
var height = 200

var state = {
  x: width / 2,
  y: height / 2
}

function update(progress) {
  state.x += progress

  if (state.x > width) {
    state.x -= width
  }
}

新しいフレームを描画する

この例ではグラフィックスをレンダリングする<canvas>要素を使用していますが、ゲームループはHTMLやSVGドキュメントなどのほかの出力でも使用できます。

draw関数は、現在の領域の状態を単にレンダリングします。各フレームでキャンバスをクリアし、そのあとstateオブジェクトに記憶された位置を中心として10pxの赤い正方形を描画します。

var canvas = document.getElementById("canvas")
var width = canvas.width
var height = canvas.height
var ctx = canvas.getContext("2d")
ctx.fillStyle = "red"

function draw() {
  ctx.clearRect(0, 0, width, height)

  ctx.fillRect(state.x - 5, state.y - 5, 10, 10)
}

これで動きます!

注意:デモでは、キャンバスのサイズが、CSSとHTML要素のwidth属性とheight属性の両方に設定されていることがわかります。CSSスタイルは要素がページに描画されるキャンバスの実際の大きさを設定し、HTMLの属性はAPIが使用するキャンバスの座標系や「グリッド」の大きさを設定します。詳しくは、スタックオーバーフローの質問を参照してください。

ユーザー入力への反応

次に、オブジェクトの位置をコントロールするキーボード入力を取得します。state.pressedKeysが、どのキーが押されたかを追跡します。

var state = {
  x: (width / 2),
  y: (height / 2),
  pressedKeys: {
    left: false,
    right: false,
    up: false,
    down: false
  }
}

キーが上下するイベントすべてに従い、state.pressedKeysを適切に更新します。使用するキーは、右がD、左がA、上がW、下がSです。キーコードのリストはここにあります。

var keyMap = {
  68: 'right',
  65: 'left',
  87: 'up',
  83: 'down'
}
function keydown(event) {
  var key = keyMap[event.keyCode]
  state.pressedKeys[key] = true
}
function keyup(event) {
  var key = keyMap[event.keyCode]
  state.pressedKeys[key] = false
}

window.addEventListener("keydown", keydown, false)
window.addEventListener("keyup", keyup, false)

そのあと押されたキーに基づいてxとyの値を更新し、オブジェクトを境界内に維持していることを確認します。

function update(progress) {
  if (state.pressedKeys.left) {
    state.x -= progress
  }
  if (state.pressedKeys.right) {
    state.x += progress
  }
  if (state.pressedKeys.up) {
    state.y -= progress
  }
  if (state.pressedKeys.down) {
    state.y += progress
  }

  // Flip position at boundaries
  if (state.x > width) {
    state.x -= width
  }
  else if (state.x < 0) {
    state.x += width
  }
  if (state.y > height) {
    state.y -= height
  }
  else if (state.y < 0) {
    state.y += height
  }
}

そしてユーザーに入力させます。

アステロイド

ここで、もっと興味深いことをするための基本を完成しようとしています。

有名なゲームアステロイドのような船を造るのは、それほど複雑ではありません。

stateには、移動のための追加のベクトル(x、yの組み合わせ)だけでなく、船の方向転換を記憶させる必要もあります。

var state = {
  position: {
    x: (width / 2),
    y: (height / 2)
  },
  movement: {
    x: 0,
    y: 0
  },
  rotation: 0,
  pressedKeys: {
    left: false,
    right: false,
    up: false,
    down: false
  }
}

update関数は、次の3つを更新します。

  • 左右キーを押すことによる回転
  • 上下キーと回転による動き
  • 動きのベクトルやカンバスの境界に基づく位置
function update(progress) {
  // Make a smaller time value that's easier to work with
  var p = progress / 16

  updateRotation(p)
  updateMovement(p)
  updatePosition(p)
}

function updateRotation(p) {
  if (state.pressedKeys.left) {
    state.rotation -= p * 5
  }
  else if (state.pressedKeys.right) {
    state.rotation += p * 5
  }
}

function updateMovement(p) {
  // Behold! Mathematics for mapping a rotation to it's x, y components
  var accelerationVector = {
    x: p * .3 * Math.cos((state.rotation-90) * (Math.PI/180)),
    y: p * .3 * Math.sin((state.rotation-90) * (Math.PI/180))
  }

  if (state.pressedKeys.up) {
    state.movement.x += accelerationVector.x
    state.movement.y += accelerationVector.y
  }
  else if (state.pressedKeys.down) {
    state.movement.x -= accelerationVector.x
    state.movement.y -= accelerationVector.y
  }

  // Limit movement speed
  if (state.movement.x > 40) {
    state.movement.x = 40
  }
  else if (state.movement.x < -40) {
    state.movement.x = -40
  }
  if (state.movement.y > 40) {
    state.movement.y = 40
  }
  else if (state.movement.y < -40) {
    state.movement.y = -40
  }
}

function updatePosition(p) {
  state.position.x += state.movement.x
  state.position.y += state.movement.y

  // Detect boundaries
  if (state.position.x > width) {
    state.position.x -= width
  }
  else if (state.position.x < 0) {
    state.position.x += width
  }
  if (state.position.y > height) {
    state.position.y -= height
  }
  else if (state.position.y < 0) {
    state.position.y += height
  }
}

draw関数は、矢印の形を描画する前に、キャンバスの原点を変換し回転させます。

function draw() {
  ctx.clearRect(0, 0, width, height)

  ctx.save()
  ctx.translate(state.position.x, state.position.y)
  ctx.rotate((Math.PI/180) * state.rotation)

  ctx.strokeStyle = 'white'
  ctx.lineWidth = 2
  ctx.beginPath ()
  ctx.moveTo(0, 0)
  ctx.lineTo(10, 10)
  ctx.lineTo(0, -20)
  ctx.lineTo(-10, 10)
  ctx.lineTo(0, 0)
  ctx.closePath()
  ctx.stroke()
  ctx.restore()
}

アステロイドのような船を再現するために必要なコードです。このデモで使用しているキーは、前述と同じです(右がD、左がA、上がW、下がS)。

あとは、小惑星、弾丸、衝突判定を追加してください。

レベルアップ

この記事を興味深いと思った人は、さらに複雑な例になるメアリー・ローズ・クックのライブ―スペースインベーダーをゼロから作るコードを楽しんでください。数年前のものですが、ブラウザーにゲームを組み立てる入門として優れています。

※本記事はAndrew RaySebastian Seitzが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。

(原文:Quick Tip: How to Make a Game Loop in JavaScript

[翻訳:柴田理恵]
[編集:Livit

Web Professionalトップへ

WebProfessional 新着記事