■補講 Canvasを使ったゲーム(集金ゲーム)  この補習講義ではドットイートタイプのゲームを作成します。ドットイートタイプのゲームとして有名なのはパックマンでしょう。パックマンは1980年に発売され大ヒットしたゲームです。 ●パックマン http://ja.wikipedia.org/wiki/パックマン ●パックマン(Google) http://www.google.com/pacman/ ドットイートタイプの元祖はセガのヘッドオンで、車で画面上にあるドットを全て取っていくというものです。 ●ヘッドオン http://ja.wikipedia.org/wiki/ヘッドオン ヘッドオンはパックマンと違い敵が1つだけです。 ということで(?)ここでは、さらにシンプルにして敵がないドットイートタイプのゲームを作成します。操作方法は、ブロック崩しなどとは異なりキーボードでキャラクタを移動させます。今回はカーソルキーでキャラクタが上下左右に移動します。 ここでは画面上にあるお金を拾うようにし、全てのお金を拾うとラウンドクリアとします。さらに、ラウンドは全部で3つあり、3つのラウンドをクリアすると全面クリアとなりゲームオーバーになります。 ゲームと言っても敵もいない上に何も競うものがないので、最初に改良するのであればタイムアタック型にしてみるとよいかもしれません。敵がいた方が面白いのですが、このゲームをヒントにして自分で作成してみるとよいでしょう。 それではドットイート型のゲームについて説明します。 ------------------------------------------------------------------------------------------------ ■ラウンドデータの準備  まず、プログラムを作成する前に3つのラウンドのデータを用意しましょう。ここではラウンドデータはJavaScriptデータとして格納しdata.jsファイルに入れておきます。ラウンドのデータはroundMap配列に入れておきます。roundMap[1]がラウンド1のデータになります。roundMap[2]ならラウンド2です。  ラウンドのデータは0、1、2の文字で示し以下のように対応しています。 【表】 0 何もない空間 1 壁。キャラはこれをすり抜けることはできない 2 お金 ラウンドデータは012の文字で表現し横は10文字、縦は15行で用意します。1つのブロックが32×32ピクセルですから、横10文字、縦15行にすると320×480ピクセルになります。 今回のゲームでは、これまでのブロック崩しなどとは異なり壁との当たり判定が必要になります。そこで、外側は全て壁にしておき、画面からキャラクタが出てしまわないようにします。 お金の数はプログラムで自動的に計算されるので特別に計算して設定する必要はあります。 オンラインであることが前提なら非同期通信(Ajax)を使ってラウンドごとにデータを読み込むようにするのもよいでしょう。山間部などデータ通信ができない場合でもプレイしてもらうのであれば、今回のようにまとめてしまった方がよいでしょう。 ------------------------------------------------------------------------------------------------ 【コラム】ラウンドごとに敵のアルゴリズムが違う場合は 今回は敵がいませんが、ラウンドごとに敵の種類や動かし方を違うものにしたいこともあります。このような場合、メインとなるプログラムに全部入れておくという方法以外にデータ(今回の例で言えばラウンドデータ)に敵のデータとプログラムを入れておく方法があります。 制作側が全てコントロールできる場合は前者でもよいのですが、将来が想定しにくい場合には後者のデータにプログラムを含めておく方が融通がききます。例えば日本ファルコムのRPGであるソーサリアンは後者の方法を採用しています。 ●ソーサリアン http://ja.wikipedia.org/wiki/ソーサリアン ------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------ ■初期化  それでは初期化部分を見てみましょう。自分のキャラクタの座標と集金したお金の数、ラウンドデータ等を入れるプロパティを設定しています。これまでとは異なりキーボードで操作しますが、keydownイベントを利用してキー入力を行います。このため、入力時とキャラクタの表示処理などが非同期になっています。そこで、keyプロパティを用意し、そこに押されたキーの値(キーコード)を一時的に入れておくことにします。  登場するキャラクタ(自分、ブロック、お金)は32×32ピクセルサイズで固定されています(charSize)。  また、今回は3つのラウンドがあるのでroundプロパティを用意し、ラウンド数を設定しておきます。 var game = { round : 1, // ゲームの面(ラウンド) manX : 1, // 自分のX座標 manY : 13, // 自分のY座標 key : 0, // 押されたキーのコード roundData : new Array(), // ラウンドマップのデータを格納する配列 moneyCount : 0, // 金の総数 charSize : 32 // 画像の幅(32×32) }; ------------------------------------------------------------------------------------------------ ■ページ読み込み後の処理  ページが読み込まれた後には敵の座標の初期化とキーダウンイベントを設定します。 イベントの設定が終わったらラウンドデータを元に画面を描画します。この処理を行うのがsetRoundData()関数です。setRoundData()関数のパラメータに描画したいラウンド数を指定すると、そのラウンドが描画されます。 window.addEventListener("load", function(){ var canvasObj = document.getElementsByTagName("canvas")[0]; context = canvasObj.getContext("2d"); window.document.addEventListener("keydown", moveMyChar, false); setRoundData(game.round); // 最初はステージ1 timerID = setInterval("moveChar()", 100); }, true); ------------------------------------------------------------------------------------------------ ■ラウンドデータを元に画面を描画  setRoundData関数内でラウンドデータを描画します。まず、最初に集めるお金の合計数を0にしておきます。 game.moneyCount = 0; // お金の合計  次にラウンドデータを描画します。今回はあらかじめHTMLファイル内にキャラクタの画像を表示してありますので、その画像のID名を利用します。以下のようにtype配列に入れておきます。012の3つしかないのでifやswitchで処理してもよいのですが、10〜30くらいになると配列に入れておいた方が楽です。1000とかであれば画像ファイル名などを、そのままID名に対応させて利用する方がよいでしょう。 var type = ["", "block", "money" ]; ラウンドデータを描く前にCanvas内容を消去します。 context.clearRect(0,0, 320, 480); // Canvasを消去 これで準備ができました。後はfor命令を使って繰り返しデータを読み出しながら1ブロックずつ描画します。描画と同時にラウンドデータをgame.roundMap配列に入れていきます。game.roundMapには0, 1, 2のいずれかの文字列が入ることになります。game.roundMap配列の何番目の要素に入れるかは以下の計算式で求めることができます。 var mp = x+y*10; また、読み出すデータは以下のようにして求めることができます。nはsetRoundData()関数に渡されたラウンド数です。 roundMap[n][y].charAt(x) ここから読み出した012のに応じてCanvasに壁とお金を描画します。お金の場合は描画したらgame.moneyCountに1を加算していきます。これで自動的に集金すべきお金の総数を求めることができます。 for(var y=0; y<15; y++){ for(var x=0; x<10; x++){ var mp = x+y*10; // 設定するマップの位置を算出。10はマップ内での横幅 var c = game.roundData[mp] = roundMap[n][y].charAt(x); if (c > 0){ var imageObj = document.getElementById(type[c]); context.drawImage(imageObj, x*game.charSize, y*game.charSize, 32, 32); if (c == "2"){ game.moneyCount = game.moneyCount + 1; } } } } 今回のゲームはラウンドが3つあります。プレイしている時に、どのラウンドなのかを表示した方が親切です。そこで、画面の左下に現在のラウンド数を描画します。 ラウンド数は文字で描画しますから、7章のレッスン26で説明したfillText()、strokeText()を使います。単純にfillText()で文字を描画すると、すでに描画されたブロック上に重なってしまい見にくい状態になります。そこで、文字に枠を付けるようにします。文字に枠をつけるには最初にstrokeText()を使って文字を描画します。この時に、どのくらいのサイズの枠にするかはコンテキストのlineWidthプロパティで設定できます。ここでは4ピクセルに設定しました。  strokeText()を使って描画した後にfillText()を使って描画すれば文字に枠がついた状態になります。 // 画面の左下にラウンド数を表示する context.font = "normal bold 20px Tahoma"; context.lineWidth = 4; context.strokeStyle = "black"; context.strokeText("Round "+n, 5, 470); context.fillStyle = "red"; context.fillText("Round "+n, 5, 470); ------------------------------------------------------------------------------------------------ ■自分のキャラクタの描画処理  それでは自分のキャラクタの描画処理について説明します。まず、最初にやることは自分のキャラクタを消すことです。game.charSizeはキャラクタのサイズで32ピクセルになっています。 context.clearRect(game.manX*game.charSize, game.manY*game.charSize, game.charSize, game.charSize); 長いので以下のように32にした方が分かりやすいかもしれません。 context.clearRect(game.manX*32, game.manY*32, 32, 32); 自分のキャラクタは1ピクセルずつ動くのではなくブロック単位、つまり32ピクセルずつ移動します。やはり1ピクセル単位で動かしたいという方は改良してみてください。 自分のキャラクタを消したら移動処理を行います。移動処理については後ほど説明します。移動処理が終わったら自分のキャラクタを描画します。自分のキャラクタはページ上に表示済みですので、document.getElementById()を使ってimg要素でIDがmanになっているものを指定します。後は、これまでのゲームと同様にdrawImage()を使って描画します。 var man = document.getElementById("man"); context.drawImage(man, game.manX*game.charSize, game.manY*game.charSize); ------------------------------------------------------------------------------------------------ ■自分のキャラクタの移動(1)  それでは自分のキャラクタの移動処理について説明します。このゲームではブロック崩しなどとは異なり移動先にブロックがある場合には移動することができません。問題は、どのようにして移動できるかできないかを判定するか、です。これは、どのようにするかというと移動先にブロックがあるかないかで判断します。具体的には以下のように一時的に移動先を入れる変数tx, tyを用意しておき、キー入力に応じて座標を変更します。この時、tx, tyの座標を動かすだけで実際の自分のキャラクタの座標を操作するわけではない点に注意してください。 var tx = game.manX, ty = game.manY; // 自分の座標(一時的に利用する) if (game.key == 37){ tx = tx - 1; } if (game.key == 39){ tx = tx + 1; } if (game.key == 38){ ty = ty - 1; } if (game.key == 40){ ty = ty + 1; } 暫定の移動先が決まったら、このtx, tyの座標を元に移動先にブロックがあるかどうかを調べます。そのために、まずラウンドデータから文字を読み出す必要があります。その読み出す要素の番号は以下のようにして求めることができます。10を乗算しているのは、横のブロックの数(マスの数)が10になっているからです。 var mp = tx+ty*10; // マップ上での自分の位置を算出。10はマップ内での横幅 読み出す番号が分かったら、そこにブロックがあるかどうか調べます。ブロックは文字列の1です。そこで、以下のようにブロック以外だったら移動処理を行うようにします。これで、何もない空間とお金の場合に自分のキャラクタが移動することになります。 移動することが可能だと判明したのでtx, tyを実際の自分のキャラクタの座標値として設定します。 if (game.roundData[mp] != "1"){ game.manX = tx; game.manY = ty; なお、ここではキー入力は基本的にリピートしないようになっています。つまり、一度上のカーソルキーを押した後、キーを離しても、ずっと上に移動しつづけないということです。リピートしないようにするため以下の一行を入れてありますが、パックマンなどのように移動したままにしたい場合には、この行は削除してください。 game.key = 0; // オートリピートを禁止 ------------------------------------------------------------------------------------------------ ■自分のキャラクタの移動(2)/集金処理  移動先が何も無い空間ではなく、お金の場合には別の処理が必要です。お金だったら移動先のラウンドデータは文字列の2になります。お金だったら、game.moneyCountから1を引きます。その際、集金済みなのでラウンドデータ内の配列要素には0を設定しておきます。つまり、0なので何もないことになります。  game.moneyCountには拾うべき、お金の総数が入っています。これが0になればラウンドクリアになります。  ラウンドをクリアしたらラウンド数に1を足します。 if (game.roundData[mp] == "2"){ game.roundData[mp] = "0"; // お金を集金したので0にして空にしておく if (game.moneyCount < 1){ context.clearRect(game.manX*game.charSize, game.manY*game.charSize, game.charSize, game.charSize); // 自分のキャラを消去 alert("ラウンドクリア"); game.round = game.round + 1; // ラウンド数に1を足す 今回のゲームではラウンド数は3しかありません(これはroundMap.lengthと同じ値になります)。そこで、設定されているラウンド数を超えた場合にはゲームオーバーの処理を行います。ゲームオーバー時には、これまでのブロック崩しなどと同様にタイマーをクリアしアラートダイアログを表示します。 if (game.round >= roundMap.length){ // 最終面をクリアした clearInterval(timerID); // タイマーをクリア alert("全面クリアしました"); return; } 最終面をクリアしていない場合には次のラウンドを描画、設定しなければいけません。これは以下のようにsetRoundData()を呼び出した後、自分のキャラクタの位置を初期化します。つまり、自分のキャラクタの位置を左下に戻します。 setRoundData(game.round); game.manX = 1; // キャラクタの位置を再度設定する game.manY = 13; return; これでドットイートタイプのゲームの完成です。ここでは012の文字列で処理していますが、日本語の■や●にした方がラウンドデータとしては分かりやすいでしょう。今は、日本語も、ほぼ問題なく処理されますから、分かりやすくしておいた方がいいかもしれません。 ------------------------------------------------------------------------------------------------ ■HTML (index.html) ------------------------------------------------------------------------------------------------ 集金ゲーム Canvasが使えるブラウザでどうぞ
カーソルキー(←→↑↓)を動かすと操作できます
------------------------------------------------------------------------------------------------ ■JavaScript(doteat.js) ------------------------------------------------------------------------------------------------ // 集金ゲーム(ドットイートタイプのゲーム) // Game用の変数 var context = null; var timerID = null; var game = { round : 1, // ゲームの面(ラウンド) manX : 1, // 自分のX座標 manY : 13, // 自分のY座標 key : 0, // 押されたキーのコード roundData : new Array(), // ラウンドマップのデータを格納する配列 moneyCount : 0, // 金の総数 charSize : 32 // 画像の幅(32×32) }; // ページが読み込まれた時の処理 window.addEventListener("load", function(){ var canvasObj = document.getElementsByTagName("canvas")[0]; context = canvasObj.getContext("2d"); window.document.addEventListener("keydown", moveMyChar, false); setRoundData(game.round); // 最初はステージ1 timerID = setInterval("moveChar()", 100); }, true); // 移動&表示処理 function moveChar(){ context.clearRect(game.manX*game.charSize, game.manY*game.charSize, game.charSize, game.charSize); // 自分のキャラを消去 var tx = game.manX, ty = game.manY; // 自分の座標(一時的に利用する) // 自機の移動処理(マップ内で移動させる) if (game.key == 37){ tx = tx - 1; } if (game.key == 39){ tx = tx + 1; } if (game.key == 38){ ty = ty - 1; } if (game.key == 40){ ty = ty + 1; } var mp = tx+ty*10; // マップ上での自分の位置を算出。10はマップ内での横幅 if (game.roundData[mp] != "1"){ game.manX = tx; game.manY = ty; game.key = 0; // オートリピートを禁止 if (game.roundData[mp] == "2"){ game.roundData[mp] = "0"; // お金を集金したので0にして空にしておく game.moneyCount = game.moneyCount - 1; if (game.moneyCount < 1){ context.clearRect(game.manX*game.charSize, game.manY*game.charSize, game.charSize, game.charSize); // 自分のキャラを消去 alert("ラウンドクリア"); game.round = game.round + 1; // ラウンド数に1を足す if (game.round >= roundMap.length){ // 最終面をクリアした clearInterval(timerID); // タイマーをクリア alert("全面クリアしました"); return; } // 次にラウンドデータを表示する setRoundData(game.round); game.manX = 1; // キャラクタの位置を再度設定する game.manY = 13; return; } } } // 自分のキャラクタを描画 var man = document.getElementById("man"); context.drawImage(man, game.manX*game.charSize, game.manY*game.charSize); } // 自分の移動処理 function moveMyChar(evt){ game.key = evt.keyCode; } // ラウンドマップを読み込み function setRoundData(n){ game.moneyCount = 0; // お金の合計 var type = ["", "block", "money" ]; context.clearRect(0,0, 320, 480); // Canvasを消去 for(var y=0; y<15; y++){ for(var x=0; x<10; x++){ var mp = x+y*10; // 設定するマップの位置を算出。10はマップ内での横幅 var c = game.roundData[mp] = roundMap[n][y].charAt(x); if (c > 0){ var imageObj = document.getElementById(type[c]); context.drawImage(imageObj, x*game.charSize, y*game.charSize, 32, 32); if (c == "2"){ game.moneyCount = game.moneyCount + 1; } } } } // 画面の左下にラウンド数を表示する context.font = "normal bold 20px Tahoma"; context.lineWidth = 4; context.strokeStyle = "black"; context.strokeText("Round "+n, 5, 470); context.fillStyle = "red"; context.fillText("Round "+n, 5, 470); } ------------------------------------------------------------------------------------------------ ■JavaScript (data.js) // ラウンドのデータ(オンライン環境前提なら非同期通信で読み込ませる方法がグッド) var roundMap = new Array(); // 配列を用意 roundMap[1] = [ "1111111111", "1212222221", "1210002121", "1211122121", "1220022201", "1200021021", "1201021201", "1121001211", "1111001111", "1222221221", "1210022221", "1212111121", "1212122121", "1012112221", "1111111111"]; roundMap[2] = [ "1111111111", "1010000001", "1010110101", "1012200101", "1011111101", "1200000001", "1011121001", "1012021001", "1012101001", "1211111101", "1002200001", "1211111121", "1200000201", "1011200001", "1111111111"]; roundMap[3] = [ "1111111111", "1200000021", "1111111121", "1200000021", "1211111111", "1200000001", "1222222221", "1111111121", "1200000021", "1211111111", "1200000001", "1222222221", "1211110111", "1010000021", "1111111111"]; ------------------------------------------------------------------------------------------------