JavaScript基礎ワークショップ

 0. はじめに
 1. 自機をつくろう! - オブジェクトを知る
 2. 隕石をとばそう!! - クラスを使う
 3. ゲームの開始だ!!! - 相互作用を考える

0. はじめに

今日は、隕石が飛んでくるのをよける、かんたんなゲームをつくります。

見た目はとても簡素ですが、いざつくってみると、いろいろなところで迷ってしまうことに気付くでしょう。
クラスやオブジェクトは使うんだろうか?
どのような単位で全体を分けたらいいんだろう?
各オブジェクトをどんなふうに結び付けたらいいのかしら?

ゲームには、たくさんのキャラクターが登場し、これらが協調して動作することでユーザーを楽しませます。
グラフィカルユーザーインターフェースを持つ一般的なアプリケーションや、仕事で使うシステムも、基本的にはゲームと同じです。
テキストボックスやボタン、表やダイアログが共に連携をとりながら、ひとつのミッションをこなしていきます。

このワークショップでは、ゲームづくりを通じて、JavaScriptの基本やオブジェクトの使い方を学びます。
これらの知識は、きっと幅広く応用することができるでしょう。

0-1. 環境準備

開発/実行環境として、Firefox + Firebugを使います。
インストールされていない場合は、あらかじめご用意を。

演習に使うファイルをダウンロードし展開してください。

asteroid.htm が、これからコードを追加していく元のファイルです。
asteroid-3-3.htm のようにナンバリングされているファイルは、各セクション終了後の結果ファイルです。
コードがサンプルと大きく食い違ってしまったり、進行に間に合わなかった場合は、セクション開始時にこちらへ乗り換えてしまってもかまいません。

0-2. リンク

1. 自機をつくろう! - オブジェクトを知る

はじめのセクションでは、主人公の自機をつくります。
キー押下イベントをハンドリングし、上下に動かせるようにしましょう。
さあ、今日一日のウォーミングアップです。

このセクションでは、以下を学習することをねらいとしています。

  • イベントのハンドリング
  • オブジェクトの使い方
  • thisとは何か
  • 名前空間のメリット

1-1. 自機を動かす (サンプル)

1. 自機を表示する

まずは暗黒の宇宙空間に、真っ白な自機を表示させましょう。
div#spaceに、新たにdiv要素を追加します。

<div id="ship" />

CSSでスタイルをつけて見えるように。

#ship {
  background-color: white;
  position: absolute;

  width: 60px;
  height: 60px;

  left: 60px;
  top: 0px;
}

2. 自機を動かす

div#shipを移動しましょう。
特定のidの要素を取得するのは、MochiKitのgetElementを使います。
document.getElementByIdを使ってもかまいません。

getElement( id );

要素の位置の変更は、MochiKitのsetElementPositionを使います。
座標はxプロパティとyプロパティを持つオブジェクトを渡して指定します。

setElementPosition( エレメント, 座標 );

以下のコードで、自機は左端から60ピクセル、上から100ピクセルの位置に移動します。

setElementPosition(getElement('ship'), {x:60, y:100});

上下に移動できるように一つの関数にまとめて、現在位置も覚えておくようにしておきましょう。

var position = {x:60, y:0};

function move(up){
  position.y += (up? -20 : 20);
  setElementPosition(getElement('ship'), position);
}

3. キー押下で動かす

イベントの取得は、MochiKitのconnectを使います。
クロスブラウザで動き、IEのメモリリークなども考慮されています。
イベント名は onkeydown のように、先頭にonをつけて小文字に揃えます。

connect(ソース, イベント名, ハンドラ);

connectのハンドラに渡される引数は、MochiKit独自のものです。
Firebugに出力し中身を確認し、上下キーを押して自機を動かせるようにしましょう。

connect(document, 'onkeydown', function(evt){
  console.log(evt);
  console.log(evt.key());
});

上下キーは、ブラウザの上下スクロールに割り当てられているため、時々ガタガタと画面が動いてしまいます。
以下のコードで、これをキャンセルしておきましょう。

evt.preventDefault();

4. 完成

1-2. オブジェクトを使って整理する (サンプル)

1-1のように、グローバル領域にすべてを置いてしまうと、モノが増えてきたときに名前が重なってしまいます。
これでは、隕石やゲームを追加していくことができません。
自機に関係する変数や関数を、オブジェクトにまとめてしまいましょう。

一度に大きな変更を加えると、長い間動作しなくなってしまう可能性があります。
細かく修正し、動きを確認しながら進めていきましょう。

1. shipオブジェクトを定義する

shipオブジェクトを変数として定義し準備します。

var ship = {};

2. キー押下イベントハンドラを移動する。

connect呼び出し部分で直接書かれていた関数を、shipへ移動しメソッドにしましょう。
ひとまずonKeyDownという名前にするなら、以下のような書き方になります。

connect(document, 'onkeydown', ship, 'onKeyDown');

3. position変数とmove関数を移動する

残りの要素も自機に関係する要素なので、shipオブジェクトにくっつけちゃいましょう。
moveメソッドの中が、this.positionのように変わることに注意。

4. 完成

発展演習

2. 隕石をとばそう!! - クラスを使う

このセクションでは、主人公に襲いかかる敵、隕石をつくります。
自機は、一台しかなかったので、ひとつのオブジェクトで間に合いました。
しかし、隕石はたくさんあります。
クラスとインスタンスを使って効率的につくりましょう。

このセクションでは、以下を学習することをねらいとしています。

  • コンストラクタ関数とnew演算子
  • thisと関数のスコープ

2-1. 隕石を表示する (サンプル)

1. Asteroidクラスを定義する

functionを使って、Asteroidクラスを定義しましょう。
クラス名は大文字からはじまり、オブジェクト名は小文字からはじまるのが一般的です。

function Asteroid(){};

2. 隕石を表示する

隕石となるdiv要素を、spaceに追加しましょう。
隕石はたくさんあるので、CSSのclassを設定し、見た目を整えます。
基本的に、画面の中でその要素が一つしかない場合はid、複数存在する場合はclassを使ってcssと結びつけます。

.asteroid {
  background-color: gold;
}

div要素は、document.createElementかMochiKitのDOM生成関数を使います。
MochiKitのDOM生成関数は、第一引数にDOM要素の属性値、第二引数以降に子要素を並べます。

DIV({
  class : 'asteroid',
  style : 'position: absolute;',
})

要素のサイズを変更するのは、setElementDimensions関数です。
これを使わず、element.style.widthまたはheightで設定することもできます。

setElementDimensions(element, dimensions)

隕石を画面の右端に表示するために、画面のサイズを得るのはgetViewportDimensionsです。
Firebugで実行し、返り値の内容を確認し利用してみてください。

getViewportDimensions()

ランダムな位置に隕石を出現させるため、Math.randomを使って移動してください。

Math.random()*画面の高さ

3. 隕石をちょっと動かしてみる

最後に、つくった隕石を動かすことができるか、コンストラクタの中でmoveメソッドを定義して試してみましょう。
ここでは、moveを呼ぶ度に15ピクセルずつ左に移動していくようにしました。

this.move = function(){
  this.position.x -= 15;
  ...
};

こんなコードで、いくつかの隕石を動かしてみましょう。

var a1 = new Asteroid();
a1.move();
a1.move();
a1.move();
a1.move();

var a2 = new Asteroid();
a2.move();

何回かリロードし、バラバラな位置に表示されることを確認しましょう。

4. 完成

2-2. 隕石を動かす (サンプル)

1. prototypeを使う

prototypeにmoveメソッドを動かしましょう。
多少の問題もありますが、ここではprototypeに直接オブジェクトを設定する簡単な書き方でOKです。

Asteroid.prototype = {
  move : function(){
    ...
  }
};

2. アニメーションさせる

setIntervalで定期的にmoveメソッドを呼び出し、隕石をアニメーションさせましょう。
この処理はコンストラクタの中に追加し、オブジェクトが生成されたときに自動的にスタートするようにします。
ここでは50msごとに再描画を行うようにしました。

this.iid = setInterval(function(){
 ...
}, 50);

setIntervalの返り値は、タイマーを止めるときに必要なので、オブジェクトに付けて保存しておきましょう。

こんなコードで、いくつかの隕石を飛ばし、うまく動くことを確認してみます。

var a1 = new Asteroid();
var a2 = new Asteroid();
var a3 = new Asteroid();

3. 画面外に出たら消す

隕石が画面の外に消え見えなくなったら、DOM要素を消しタイマーを止めましょう。
DOM要素を消すのは、MochiKit.DOM.removeElement関数を使います。

removeElement(element);

タイマーを止めるのは、clearIntervalです。

clearInterval(タイマーID);

4. 完成

発展演習

1. 自機を画面の端で止める

getViewportDimensionsを使い、画面の外へ行かないようにしてみましょう。

2. 自機をクラスに変更する

現在、自機はオブジェクトでそのまま書かれています。
これを隕石と同様にクラスとインスタンスの構成に変えてみましょう。

3. ゲームの開始だ!!! - 相互作用を考える

最後のセクションでは、ゲームをつくり、実際に遊べるようにします。
クラスとオブジェクトの相互作用を検討し、すっきりと見通しのよいプログラムを設計しましょう。
またsignal/connectの考え方を学び、GUIプログラミングのひとつのアイディアを確認します。

このセクションでは、以下を学習することをねらいとしています。

  • オブジェクトの相互作用設計
  • signal/connectの考え方

3-1. ゲーム登場! (サンプル)

はじめに、ゲーム全体をコントロールするGameクラスをつくります。
Gameクラスは、隕石をとばしたり、ゲームの難易度を調整したり、ゲームの流れを制御する仕事を行います。
映画で例えると、自機や隕石は俳優で、ゲームは監督のような役割ですね。
各オブジェクトは自分の仕事はきちんと自分で行い、出現のタイミングなど上位からのコントロールが必要な部分は監督の指示を仰ぎます。

1. Gameクラスをつくる

Gameクラスをつくり、定期的にAsteroidを生成しましょう。
こんなコードで、gameオブジェクトを生成し、動作を確認してみます。

var game = new Game();
setTimeout(function(){
  game.stop();
}, 5000);

ヒントは少ないですが、ここまでに覚えたことを組みあわせれば、きっとできます。

2. 自機に衝突の処理を追加する

次のセクションで使うため、自機に衝突と消去の処理を追加しておきます。
各々hitメソッドとremoveメソッドに分けて書きます。
3ポイントのライフを持っていることにして、これが無くなると自分でremoveメソッドを呼び出し消えるようにしましょう。

3. 完成

3-2. 衝突!! (サンプル)

さあ、今日一番の考えどころです。
これまでは、隕石は自機に干渉せず、お互いはバラバラでした。
ここでは、自機、隕石、ゲームの三者を結びつけ、協調して動くようにします。

1. 相互作用を考える

ちょっとむずかしいので、まずはコードではなく、紙の上で考えてみましょう。
衝突の仕様は以下のようになります。

  • 隕石は、自機と同じ領域に達すると衝突し消える
  • 自機は、3回衝突すると消える
  • ゲームは、自機が消えると隕石の発生を止める

各オブジェクトでイベントが発生し、それを他のオブジェクトへ伝える、または他のオブジェクトが検知して次のアクションが起こるということが連続していますね。

衝突は誰がチェックしたらいいのでしょうか?
また、どのように他のオブジェクトへそれを伝えたらよいのでしょうか?
どうすれば変更要求に強い、柔軟な構造にできるでしょうか?

2. signalを送る

イベントの発生は、MochiKitのsignalで伝えることができます。
signalの引数は、どのオブジェクトで(ソース)、どんなイベントが起きたか(シグナル名)、そのイベントの詳細な内容(シグナル引数)で構成されています。
シグナル名は、独自に決めてかまいません。

signal( ソース, シグナル名, シグナル引数... );

例えば、自機の衝突で hit というシグナルを送る場合は以下のようになります。

signal(this, 'hit', this);

ここでは、自機の衝突と消去、隕石の移動と消去の計4カ所にsignalを追加しておきます。
受け取る人の有無に関係なく、とりあえず何かを発信しておき、後でそれを使おうと思った人が勝手に使えるような形はブログのフィードに似ていますね。

3. connectで結びつける

さあ、準備は整いました。
シグナルを受け取り、3つのオブジェクトをつなぎましょう!
シグナルに接続するのはconnect関数です。
connectは始めの二つの引数で、どのオブジェクトの(ソース)どのシグナルに(シグナル名)繋ぐかを指定し、残りの二つの引数でどのオブジェクトの(送信先)どのメソッドが(メソッド名)受け取るかを指定します。

connect( ソース, シグナル名, 送信先, メソッド名 );

これは、今日の初めにキー押下イベントを受け取るときに使った形と同じですね。
MochiKitのsignalは、アプリケーションのオブジェクトとDOMを同一のモノとして扱い、同じ書き方でオブジェクト同士を繋ぐことができます。
DOMやオブジェクトなど多数のオブジェクトが繋がり、その間をsignalが飛び交っている様子がイメージできるでしょうか?

自機と隕石が衝突する箇所は、以下のコードでチェックしてください。

function isOverlapped(posA, dimA, posB, dimB){
  function within(a1, a2, b1, b2){
    a2 = a1 + a2;
    b2 = b1 + b2;

    return (a1<=b1 && b1<=a2) || (a1<=b2 && b2<=a2);
  }

  return within(posA.x, dimA.w, posB.x, dimB.w) && within(posA.y, dimA.h, posB.y, dimB.h);
}

3. connectを解放する

現在は、ゲームオーバーになり自機が消えた後も、キー押下を受け取りエラーが発生してしまっています。
不要になったconnectは外し、オブジェクトの参照を解放し、ガベージコレクトを促す必要があります。
消去やゲーム停止のタイミングで、diconnectAllなどを使って、connectを一括解放しておきましょう。

diconnectAll(ソース);
diconnectAllTo(送信先);

4. 完成

3-3. ライフパネルも無いとね (サンプル)

三者で構成されているゲームに、もう一人の登場人物、残りライフを表示するパネルを追加しましょう。
アプリケーションに新たなパーツや機能を追加するようなイメージです。

1. ライフパネルを追加する

ライフパネルは、自機のライフが減ったときに表示が更新されます。
これは隕石が衝突したときですね。
このポイントをsignalとconnectで結び、新しい部品を組み込んでみましょう。

2. 完成

発展演習

1. 点数を表示してみよう

ライフパネルと同様に点数を表示するパネルを追加してみましょう。
隕石が消滅したときに、その隕石の点数が足されるようにつくってみましょう。

changed October 9, 2009