ゲーム開発

Web ゲームを 1 時間で

Michael Oneppo

コード サンプルのダウンロード

ゲームの開発に新しいスキルはまったく必要ありません。実際、HTML、JavaScript、CSS など、既に習得している Web 開発スキルさえあれば、幅広いゲームを開発できます。さらに、ゲームのビルドに Web テクノロジを利用すると、多種多様なデバイスのブラウザーで動作するようになります。

これを実証するため、Web テクノロジと外部ライブラリだけを使ってゲームを最初からビルドしてみます。おそらく 1 時間もかかりません。今回は基本的なデザインとレイアウト、コントロール、スプライトから、簡単な対戦相手としての人工知能 (AI) まで、ゲーム開発に関連するさまざまなトピックを取り上げます。今回開発してみるゲームは、PC、タブレット、およびスマートフォンで動作します。Web 開発者として、または他の開発分野でのプログラミング経験はあっても、ゲームを作成した経験がない方なら、今回はまさにうってつけの入門編です。1 時間もあれば、必ずコツがつかめます。

はじめに

開発はすべて Visual Studio で行います。Visual Studio なら、変更を加えながら Web アプリをすばやく実行できます。コラムの説明を理解しやすいように Visual Studio は最新版 (bit.ly/1xEjEnX からダウンロード) を用意します。今回は Visual Studio Professional 2013 を使用していますが、コードは Visual Studio Community 2013 を使って更新しています。

今回のアプリにはサーバー コードは必要ないため、新しく空の Web ページ プロジェクトを Visual Studio で作成するところから始めます。使用するのは Web サイト向けの空の C# テンプレートです。[ファイル] メニューの [新規作成] をクリックし、[新しい Web サイト] を選択後、[Visual C#] オプションから [ASP.NET 空の Web サイト] を選択します。

インデックス HTML ファイルに必要なリソースは、jQuery、メインのスタイル シート、メインの JavaScript ファイルの 3 つだけです。プロジェクトに style.css という空の CSS ファイルと、ping.js という空の JavaScript ファイルを追加して、ページ読み込み時にエラーが発生しないようにします。

<!DOCTYPE html>
<html>
<head>
  <script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.1.1.min.js"></script>
  <script src="ping.js"></script>
  <link rel="stylesheet" href="style.css"></script>
</head>
<body>
</body>
</html>

基本デザイン

今回ビルドするゲームは、古い卓球ゲーム "ポン (Pong)" に少し手を加え "ピン (Ping)" とします。ピンのルールは基本的にポンと同じですが、ピンではお互いのプレイヤーが飛んできた球をつかみ、直接打ち返すか、上か下に角度をつけて打ち返すことができます。普通は、ゲームを作る前に外観をどうするか思い描きます。今回のゲームでは、目標にする全体のレイアウトを図 1 のようにします。

ピンの全体デザイン
図 1 ピンの全体デザイン

ゲームのデザイン レイアウトを考えたら、HTML に各要素を追加して、ゲームをビルドするだけです。1 つ注意点として、今回はスコアボードとコントロールをグループ化して、まとめて配置しています。追加した要素 1 つ 1 つは図 2 で確認できます。

図 2 初期 HTML レイアウト

<div id="arena">
  <div id="score">
    <h1>
      <span id="playerScore">0</span>
      -
      <span id="opponentScore">0</span>
    </h1>
  </div>
  <div id="player"></div>
  <div id="opponent"></div>
  <div id="ball"></div>
  <div id="controls-left">
    <div id="up"></div>
    <div id="down"></div>
  </div>
  <div id="controls-right">
    <div id="left"></div>
    <div id="right"></div>
  </div>
</div>

スタイルの操作

このままページを読み込んでも、スタイルを設定していないので何も表示されません。HTML にはメインの .css ファイルへのリンクを既に設定しているため、その名前の新しいファイルにすべての CSS を配置します。まず、すべての要素の画面上の位置を決めます。ページ本体 (body) は画面全体を占めるように、最初は以下のように設定します。

body {
  margin: 0px;
  height: 100%;
}

次に画面全体を試合会場の背景画像で塗りつぶす、arena を用意します (図 3 参照)。

#arena {
  background-image: url(arena.png);
  background-size: 100% 100%;
  margin: 0px;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

会場の背景画像
図 3 会場の背景画像

続いて、スコアボードの位置を決めます。これは他の要素より上の画面上部中央に表示します。コマンド position: absolute により自由な位置に配置できるようにし、left: 50% によりウィンドウ上部中央に配置していますが、中央にくるのはスコアボード要素の左端です。完全な中央揃えにするために、transform プロパティを使用します。さらに、z-index プロパティを使用してスコアボードが常に最前面に表示されるようにします。

#score {
  position: absolute;
  z-index: 1000;
  left: 50%;
  top: 5%;
  transform: translate(-50%, 0%);
}

テキストのフォントもレトロな雰囲気にします。ほとんどのモダン ブラウザーでは、独自のフォントを使用できます。今回は codeman38 (zone38.net) で、ゲームに合う"Press Start 2P" フォントを見つけました。スコアボードにフォントを追加するには、新しいフォント フェイスを作成する必要があります。

@font-face {
  font-family: 'PressStart2P';
  src: url('PressStart2P.woff');
}

ここでは、得点が h1 タグに含まれているため、すべての h1 タグにフォントを設定します。フォントを利用できない場合に備えて、次のようにいくつかバックアップ オプションを用意しておきます。

h1 {
  font-family: 'PressStart2P', 'Georgia', serif;
}

その他要素については、画像のスプライト シートを使用します。スプライト シートは、ゲームに必要な画像すべてを 1 つのファイルに含んでいます (図 4 参照)。

ピンのスプライト シート
図 4 ピンのスプライト シート

このシート上に画像を含むすべての要素に sprite クラスを 1 つ割り当てます。次に、要素ごとに、スプライト シートの中で表示する部分を、background-position を使って定義します。

.sprite {
  background-image: url("sprites.png");
  width: 128px;
  height: 128px;
}

次に、この sprite クラスをスプライト シートを使用するすべての要素に追加します。そのためには、いったん HTML に戻る必要があります。

<div id="player" class="sprite"></div>
<div id="opponent" class="sprite"></div>
<div id="ball" class="sprite"></div>
<div id="controls-left">
  <div id="up" class="sprite"></div>
  <div id="down" class="sprite"></div>
</div>
<div id="controls-right">
  <div id="left" class="sprite"></div>
  <div id="right" class="sprite"></div>
</div>

ここで、要素ごとに、シート上のスプライトそれぞれの位置を示します。もう一度 background-position を使い、この作業を行います (図 5 参照)。

図 5 スプライト シートのオフセットの追加

#player {
  position: absolute;
  background-position: 0px 128px;
}
#opponent {
  position: absolute;
  background-position: 0px 0px;
}
#ball {
  position: absolute;
  background-position: 128px 128px;
}
#right {
  background-position: 64px 192px;
}
#left {
  background-position: 64px 0px;
}
#down {
  background-position: 128px 192px;
}
#up {
  background-position: 128px 0px;
}

player、opponent、および ball に position: absolute プロパティを設定して、これらを JavaScript を使って動かせるようにしています。この時点でページを表示すると、コントロールと球に不要なピースがあるのが分かります。これはスプライトのサイズが既定の 128 ピクセルより小さいことが原因なので、これを正しいサイズに調整します。球は 1 つだけなので、次のようにサイズを直接設定します。

#ball {
  position: absolute;
  width: 64px;
  height: 64px;
  background-position: 128px 128px;
}

コントロール要素 (ユーザーがプレイヤーを動かすボタン)は 4 つあるので、特別なクラスを用意します。余白も加えて、ボタンの周囲に小さなスペースを設けます。

.control {
  margin: 16px;
  width: 64px;
  height: 64px;
}

このクラスを追加すると、ゲームのコントロールの見た目が非常に良くなります。

<div id="controls-left">
  <div id="up" class="sprite control"></div>
  <div id="down" class="sprite control"></div>
</div>
<div id="controls-right">
  <div id="left" class="sprite control"></div>
  <div id="right" class="sprite control"></div>
</div>

最後に、ページがモバイル デバイスで動作するときは、コントロールの位置をユーザーの指の近くにする必要があります。今回は、画面の下隅に固定します。

#controls-left {
  position: absolute;
  left: 0; bottom: 0;
}
#controls-right {
  position: absolute;
  right: 0; bottom: 0;
}

このデザインが優れている点は、すべてが相対位置で設定されることです。つまり、画面のサイズが大きく変わっても、ゲームの見た目は変わりません。

球の跳ね返り

今度は、球があちこち動くようにしましょう。CSS の場合と同様、HTML では ping.js というファイルを JavaScript コードとして既に参照しています。そこで球を動かすコードを、この名前の新しいファイルに追加します。ここでは、球と各プレイヤーのオブジェクトを作成しますが、このオブジェクトにはファクトリ パターンを使用します。

考え方はシンプルです。Ball 関数は、呼び出されたときに、新しい球を作成します。new キーワードを使用する必要はありません。このパターンは、オブジェクトの使用可能なプロパティを明確にすることで、この変数にまつわるあいまいさをある程度軽減します。ゲーム製作時間を 1 時間としたので、混乱を招きかねない概念は最小限に抑えます。

シンプルな Ball クラスを作りながら、このパターンの構造を図 6 に示します。

図 6 Ball クラス

var Ball = function( {
  // List of variables only the object can see (private variables).
  var velocity = [0,0];
  var position = [0,0];
  var element = $('#ball');
  var paused = false;
  // Method that moves the ball based on its velocity. This method is only used
  // internally and will not be made accessible outside of the object.
  function move(t) {
  }
  // Update the state of the ball, which for now just checks  
  // if the play is paused and moves the ball if it is not.  
  // This function will be provided as a method on the object.
  function update(t) {
    // First the motion of the ball is handled
    if(!paused) {
      move(t);
    }
  }
  // Pause the ball motion.
  function pause() {
    paused = true;
  }
  // Start the ball motion.
  function start() {
    paused = false;
  }
  // Now explicitly set what consumers of the Ball object can use.
  // Right now this will just be the ability to update the state of the ball,
  // and start and stop the motion of the ball.
  return {
    update:       update,
    pause:        pause,
    start:        start
}

新しい球を作成するには、次のようにここで定義した関数を単純に呼び出します。

var ball = Ball();

次に、球が画面中で動き回り、跳ね返るようにしましょう。まず、適当な間隔で update 関数を呼び出し、球のアニメーションを生み出す必要があります。モダン ブラウザーには、このようなアニメーションを目的に用意された関数 requestAnimationFrame があります。この関数は引数として関数を受け取り、次回のアニメーション サイクル実行時に、受け取った関数を呼び出します。これにより、ブラウザーの更新準備が整うと、球が滑らかに動き回るようになります。requestAnimationFrame 関数は受け取った関数を呼び出すときに、ページが読み込まれてからの経過時間を秒単位で指定します。これは、時間が経ってもアニメーションの一貫性を確保するために重要です。ゲームでは、requestAnimationFrame を以下のように使用します。

var lastUpdate = 0;
var ball = Ball();
function update(time) {
  var t = time - lastUpdate;
  lastUpdate = time;
  ball.update(t);
  requestAnimationFrame(update);
}
requestAnimationFrame(update);

球の更新が完了したときに、requestAnimationFrame が関数内で再度呼び出されている点に注意してください。これによって連続アニメーションになります。

上記のコードは機能しますが、ページが完全に読み込まれる前にスクリプトの動作が開始されると問題になる可能性があります。これを避けるために、ページの読み込み時に次のように jQuery を使ってコードを呼び出します。

var ball;
var lastUpdate;
$(document).ready(function() {
  lastUpdate = 0;
  ball = Ball();
  requestAnimationFrame(update);
});

球の速度 (velocity) と前回更新してからの経過時間が分かっているので、次のように簡単な物理学を使って球を動かすことができます。

var position = [300, 300];
var velocity = [-1, -1];
var move = function(t) {
  position[0] += velocity[0] * t;
  position[1] += velocity[1] * t;  
  element.css('left', position[0] + 'px');
  element.css('top', position[1] + 'px');
}

ここでコードを試してみると、球が斜めに動き、画面外に消えます。一瞬楽しくなりますが、球が画面の端から消えてしまえばそれまでです。そこで、ここからは、球が画面の端で跳ね返るようにします (図 7 参照)。このコードを追加してアプリを起動すると、球が連続して跳ね返るようになります。

図 7 単純な球の跳ね返りの物理学

var move = function(t) {
  // If the ball hit the top or bottom, reverse the vertical speed.
  if (position[1] <= 0 || position[1] >= innerHeight) {
   velocity[1] = -velocity[1];
  }
  // If the ball hit the left or right sides, reverse the horizontal speed.
  if (position[0] <= 0 || position[0] >= innerWidth) {
   velocity[0] = -velocity[0];
  }
  position[0] += velocity[0] * t;
  position[1] += velocity[1] * t; 
  element.css('left', (position[0] - 32) + 'px');
  element.css('top', (position[1] - 32) + 'px');
}

プレイヤーを動かす

今度は、プレイヤーのオブジェクトを作成しましょう。プレイヤー クラスを具体化する最初の手順として、プレイヤーの位置を変える move 関数を作成します。side 変数はどちらのコートにプレイヤーが存在するかを示し、player の水平方向の移動方法を決定します。move 関数に渡される y 値は、プレイヤーの上下の移動量を表します。

var Player = function (elementName, side) {
  var position = [0,0];
  var element = $('#'+elementName);
  var move = function(y) {
  }
  return {
    move: move,
    getSide:      function()  { return side; },
    getPosition:  function()  { return position; }
  }
}

図 8 はプレイヤーの動きをレイアウトしていて、プレイヤーのスプライトがウィンドウの上端または下端に達すると動きを止めます。

ここで、2 人のプレイヤーを作成し、それぞれ画面の適切なコートに配置します。

player = Player('player', 'left');
player.move(0);
opponent = Player('opponent', 'right');
opponent.move(0);

図 8 プレイヤーのスプライトの移動をコントロール

var move = function(y) {
  // Adjust the player's position.
  position[1] += y;
  // If the player is off the edge of the screen, move it back.
  if (position[1] <= 0)  {
    position[1] = 0;
  }
  // The height of the player is 128 pixels, so stop it before any
  // part of the player extends off the screen.
  if (position[1] >= innerHeight - 128) {
    position[1] = innerHeight - 128;
  }
  // If the player is meant to stick to the right side, set the player position
  // to the right edge of the screen.
  if (side == 'right') {
    position[0] = innerWidth - 128;
  }
  // Finally, update the player's position on the page.
  element.css('left', position[0] + 'px');
  element.css('top', position[1] + 'px');
}

キーボード入力

これで理論上はプレイヤーを動かせるようになりましたが、指示がなければ動きません。左側のプレイヤーにいくつかコントロールを追加します。プレイヤーを操作する方法は 2 つ用意します。1 つはキーボードを使う方法 (PC 用)、もう 1 つはコントロールをタップする方法 (タブレットやスマートフォン用) です。

プラットフォームが変わってもタッチ入力とマウス入力の一貫性を確保するため、今回は優れた統合フレームワーク Hand.js (handjs.codeplex.com、英語) を使用します。まず、HTML の head セクションに次のスクリプトを追加します。

 

<script src="hand.minified-1.3.8.js"></script>

図 9 は Hand.js と jQuery を使用して、A キーと Z キーの押下、またはコントロールのタップによってプレイヤーを操作する例を示しています。

図 9 タッチ コントロールとキーボード コントロールの追加

var distance = 24;  // The amount to move the player each step.
$(document).ready(function() {
  lastUpdate = 0;
  player = Player('player', 'left');
  player.move(0);
  opponent = Player('opponent', 'right');
  opponent.move(0);
  ball = Ball();
  // pointerdown is the universal event for all types of pointers -- a finger,
  // a mouse, a stylus and so on.
  $('#up')    .bind("pointerdown", function() {player.move(-distance);});
  $('#down')  .bind("pointerdown", function() {player.move(distance);});
  requestAnimationFrame(update);
});
$(document).keydown(function(event) {
  var event = event || window.event;
  // This code converts the keyCode (a number) from the event to an uppercase
  // letter to make the switch statement easier to read.
  switch(String.fromCharCode(event.keyCode).toUpperCase()) {
    case 'A':
      player.move(-distance);
      break;
    case 'Z':
      player.move(distance);
      break;
  }
  return false;
});

球をつかむ

球が跳ね回るようになったので、プレイヤーがボールをつかむようにします。つかまれた球は、保持しているプレイヤーがいることになるため、球の動きはプレイヤーの動きに従います。図 10 は、球の move メソッドに機能を追加し、球をつかんでいるプレイヤーを認識させ、球の動きをプレイヤーに従わせます。

図 10 球の動きをプレイヤーに従わせる

var move = function(t) {
  // If there is an owner, move the ball to match the owner's position.
  if (owner !== undefined) {
    var ownerPosition = owner.getPosition();
    position[1] = ownerPosition[1] + 64;
    if (owner.getSide() == 'left') {
      position[0] = ownerPosition[0] + 64;
    } else {
      position[0] = ownerPosition[0];
    }
  // Otherwise, move the ball using physics. Note the horizontal bouncing
  // has been removed -- ball should pass by a player if it
  // isn't caught.
  } else {
    // If the ball hits the top or bottom, reverse the vertical speed.
    if (position[1] - 32 <= 0 || position[1] + 32 >= innerHeight) {
      velocity[1] = -velocity[1];
    }
    position[0] += velocity[0] * t;
    position[1] += velocity[1] * t;  
  }
  element.css('left', (position[0] - 32) + 'px');
  element.css('top',  (position[1] - 32) + 'px');
}

現状では、プレイヤー オブジェクトの位置を取得する方法がないため、ここでプレイヤー オブジェクトに getPosition アクセサーと getSide アクセサーを追加します。

return {
  move: move,
  getSide:      function()  { return side; },
  getPosition:  function()  { return position; }
}

これで、プレイヤーが球を保持していれば、球がプレイヤーの動きに従うようになります。しかし、球をつかんだプレイヤーを決める方法が必要です。誰かが球をつかむ必要があります。図 11 は、プレイヤーのスプライトの一方が球に触れた瞬間を判断する方法を示しています。この状態が発生したら、球に触れたプレイヤーを保持者に設定します。

図 11 球とプレイヤーの接触検出

var update = function(t) {
// First the motion of the ball is handled.
if(!paused) {
    move(t);
}
// The ball is under control of a player, no need to update.
if (owner !== undefined) {
    return;
}
// First, check if the ball is about to be grabbed by the player.
var playerPosition = player.getPosition();
  if (position[0] <= 128 &&
      position[1] >= playerPosition[1] &&
      position[1] <= playerPosition[1] + 128) {
    console.log("Grabbed by player!");
    owner = player;
}
// Then the opponent...
var opponentPosition = opponent.getPosition();
  if (position[0] >= innerWidth - 128 &&
      position[1] >= opponentPosition[1] &&
      position[1] <= opponentPosition[1] + 128) {
    console.log("Grabbed by opponent!");
    owner = opponent;
}

この時点でゲームをプレイすると、プレイヤーを操作して画面上部で反射した球をつかめるようになっています。今度は、球を投げる方法を考えます。球の狙いを定めるには、右手のコントロールを使用します。図 12 では "fire" 関数と aim プロパティをプレイヤーに追加しています。

図 12 球の狙いを定めて打つ

var aim = 0;
var fire = function() {
  // Safety check: if the ball doesn't have an owner, don't not mess with it.
  if (ball.getOwner() !== this) {
    return;
  }
  var v = [0,0];
  // Depending on the side the player is on, different directions will be thrown.
  // The ball should move at the same speed, regardless of direction --
  // with some math you can determine that moving .707 pixels on the
  // x and y directions is the same speed as moving one pixel in just one direction.
  if (side == 'left') {
    switch(aim) {
    case -1:
      v = [.707, -.707];
      break;
    case 0:
      v = [1,0];
      break;
    case 1:
      v = [.707, .707];
    }
  } else {
    switch(aim) {
    case -1:
      v = [-.707, -.707];
      break;
    case 0:
      v = [-1,0];
      break;
    case 1:
      v = [-.707, .707];
    }
  }
  ball.setVelocity(v);
  // Release control of the ball.
  ball.setOwner(undefined);
}
// The rest of the Ball definition code goes here...
return {
  move: move,
  fire: fire,
  getSide:      function()  { return side; },
  setAim:       function(a) { aim = a; },
  getPosition:  function()  { return position; },
}

図 13 では、キーボード関数にプレイヤーが狙いを定める関数と球を打ち返す関数を追加しています。これによって、狙いの付け方が変わります。狙いを定めるキーを離すと、その狙いが単純に返されます。

図 13 プレイヤーが狙いを定める関数の設定

$(document).keydown(function(event) {
  var event = event || window.event;
  switch(String.fromCharCode(event.keyCode).toUpperCase()) {
    case 'A':
      player.move(-distance);
      break;
    case 'Z':
      player.move(distance);
      break;
    case 'K':
      player.setAim(-1);
      break;
    case 'M':
      player.setAim(1);
      break;
    case ' ':
      player.fire();
      break;
  }
  return false;
});
$(document).keyup(function(event) {
  var event = event || window.event;
  switch(String.fromCharCode(event.keyCode).toUpperCase()) {
    case 'K':
    case 'M':
      player.setAim(0);
      break;
  }
  return false;
});

最後に、すべてのコントロールにタッチ サポートを追加します。右にあるコントロールによってプレイヤーの狙いが変わるようにします。また、画面の任意の場所をタッチすると、球が打ち返されるようにします。

$('#left')  .bind("pointerdown", function() {player.setAim(-1);});
$('#right') .bind("pointerdown", function() {player.setAim(1);});
$('#left')  .bind("pointerup",   function() {player.setAim(0);});
$('#right') .bind("pointerup",   function() {player.setAim(0);});
$('body')   .bind("pointerdown", function() {player.fire();});

得点を付ける

プレイヤーが球をつかめずに通り過ぎたら得点となり、そのプレイヤーに球を与えます。カスタム イベントを使えば、既存の任意のオブジェクトからでも個別に加点できます。update 関数が長くなるので、checkScored というプライベート関数を新しく追加します。

function checkScored() {
  if (position[0] <= 0) {
    pause();
    $(document).trigger('ping:opponentScored');
  }
  if (position[0] >= innerWidth) {
    pause();
    $(document).trigger('ping:playerScored');
  }
}

図 14 のコードは上記のイベントに反応して、得点を更新し、球をプレイヤーに渡します。このコードを JavaScript ドキュメントの最後に加えます。

図 14 スコアボードの更新

$(document).on('ping:playerScored', function(e) {
  console.log('player scored!');
  score[0]++;
  $('#playerScore').text(score[0]);
  ball.setOwner(opponent);
  ball.start();
});
$(document).on('ping:opponentScored', function(e) {
  console.log('opponent scored!');
  score[1]++;
  $('#opponentScore').text(score[1]);
  ball.setOwner(player);
  ball.start();
});

これで、対戦相手が球をつかめず通り過ぎる (現時点で相手は動かないので、難しいことではありません) と、自分の得点が増え、球が相手の手に渡ります。ですが、対戦相手は球を手にしたまま動きません。

知恵を授ける

ゲームはほぼ完成です。ただし、一緒に遊んでくれる人が必要です。そこで最後の手順として、簡単な AI を使って対戦相手をコントロールする方法を紹介します。対戦相手は、球が動いているとき、球に対して平行を保とうとします。相手が球をつかむと、その後ランダムに動き、ランダムな方向に球を打ち出します。この AI をもう少し人間らしくして、すべての行動に遅延を加えます。知性の高い AI ではありませんが、一応はゲームをプレイします。

この種のシステムをデザインする際は、状態について考えることをお勧めします。対戦相手の AI には、追従、照準/発射、および待機の 3 つの状態が考えられます。より人間的な要素を加えるため、追従アクションの間に状態を追加します。最初の AI オブジェクトは次のような状態です。

function AI(playerToControl) {
  var ctl = playerToControl;
  var State = {
    WAITING: 0,
    FOLLOWING: 1,
    AIMING: 2
  }
  var currentState = State.FOLLOWING;
}

AI の状態に応じて、実行する操作を変えます。球の場合と同様、requestAnimationFrame から呼び出される update 関数を作成し、AI のアクションが状態に応じて変わるようにします。

function update() {
  switch (currentState) {
    case State.FOLLOWING:
      // Do something to follow the ball.
      break;
    case State.WAITING:
      // Do something to wait.
      break;
    case State.AIMING:
      // Do something to aim.
      break;
  }
}

FOLLOWING 状態はそのままの意味です。対戦相手は球の縦方向の動きと同じ動きをします。そしていくらか遅れた反応時間をはさむために、AI は WAITING 状態に移行します。図 15 はこれら 2 つの状態を表しています。

図 15 単純な追従 AI

function moveTowardsBall() {
  // Move the same distance the player would move, to make it fair.
  if(ball.getPosition()[1] >= ctl.getPosition()[1] + 64) {
    ctl.move(distance);
  } else {
    ctl.move(-distance);
  }
}
function update() {
  switch (currentState) {
    case State.FOLLOWING:
      moveTowardsBall();
      currentState = State.WAITING;
    case State.WAITING:
      setTimeout(function() {
        currentState = State.FOLLOWING;
      }, 400);
      break;
    }
  }
}

図 15 のコードによって AI は球に追従する状態と、一瞬待機する状態を行き来します。このコードを、ゲーム全体の update 関数に追加します。

function update(time) {
  var t = time - lastUpdate;
  lastUpdate = time;
  ball.update(t);
  ai.update();
  requestAnimationFrame(update);
}

ゲームをプレイすると対戦相手が球の動きに追従するのが分かります。30 行に満たないコードにしては悪くない AI です。当然ですが相手が球をつかんだら、そこで終わりです。この 1 時間の最後の技として、AIMING 状態での操作に取り掛かります。AI には何度かランダムな動きをさせ、その後ランダムな方向に球を打ち出させます。図 16 はそのためのプライベート関数を追加しています。AIMING case ステートメントに aimAndFire 関数を加えることで、対戦相手の機能を完全に備えた AI が完成します。

図 16 照準と発射の AI

function repeat(cb, cbFinal, interval, count) {
  var timeout = function() {
    repeat(cb, cbFinal, interval, count-1);
  }
  if (count <= 0) {
    cbFinal();
  } else {
    cb();
    setTimeout(function() {
      repeat(cb, cbFinal, interval, count-1);
    }, interval);
  }
}
function aimAndFire() {
  // Repeat the motion action 5 to 10 times.
  var numRepeats = Math.floor(5 + Math.random() * 5);
  function randomMove() {
    if (Math.random() > .5) {
      ctl.move(-distance);
    } else {
      ctl.move(distance);
    }
  }
  function randomAimAndFire() {
    var d = Math.floor( Math.random() * 3 - 1 );
    opponent.setAim(d);
    opponent.fire();
    // Finally, set the state to FOLLOWING.
    currentState = State.FOLLOWING;
  }
  repeat(randomMove, randomAimAndFire, 250, numRepeats);
}

まとめ

これで PC、スマートフォン、タブレットで動作する本格的な Web ゲームが完成です。このゲームに考えられる改善点はまだまだあります。たとえば、スマートフォンを縦向きにすると、見た目がわずかに悪くなります。そのためこのゲームを正しく動作させるには、必ずスマートフォンを横向きの状態にしておく必要があります。今回紹介したのは、Web やその後のゲーム開発の可能性のほんの小さな部分にすぎません。


Michael Oneppo はクリエイティブな技術者で、マイクロソフトの Direct3D チームで前プログラム マネージャーを務めていました。近年では、非営利技術団体 Library For All の CTO としての業務、さらにニューヨーク大学の Interactive Telecommunications Program で修士号の取得などに努めました。​

この記事のレビューに協力してくれた技術スタッフの Mohamed Ameen Ibrahim に心より感謝いたします。