■補講 Canvasを使ったゲーム(横スクロールシューティングゲーム)
この補習講義では横スクロールシューティングゲームを作成します。1982年あたりは縦スクロールシューティングゲームが多かったのですが、次第に横スクロールシューティングゲームが多くなっていきました。横スクロールシューティングゲームの中で有名なものとしては以下のゲームがあります。
●グラディウス (GRADIUS)
http://ja.wikipedia.org/wiki/グラディウス_(ゲーム)
●ダライアス (DARIUS)
http://ja.wikipedia.org/wiki/ダライアス
●R-TYPE
http://ja.wikipedia.org/wiki/R-TYPE
いずれも背景は右から左へと強制的に移動(スクロール)するものです。パワーアップアイテムをとることで、自機をパワーアップさせることができます。自機がパワーアップするシステムは1983年以降のシューティングゲームにおいては一般的なものです。
そこで、今回作成するゲームはオレンジ色の敵を倒すとパワーアップアイテムが出現し、それを取ると自機が撃てるビームの数が増えるようにします。最大で12連射まで可能です。
また、横スクロールシューティングゲームの多くは地形に自機があたると爆発しやられてしまいます。ここで作成するゲームも、それにならって自機が地形にあたるとやられるようにします。
横スクロールシューティングゲームでも基本的には縦スクロールシューティングゲームと変わりません。敵や自機が移動する方向が変わっただけです。このため、ここで説明するプログラムは以前の補習講義で使用したものを、そのまま利用しています。
ここでは差分のみ解説します。なお、地形との接触判定は本書のレッスン40を参照してください。同じ手法を使って地形との接触判定を行っています。
------------------------------------------------------------------------------------------------
【コラム】
------------------------------------------------------------------------------------------------
ちなみに本書のレッスン40のゲームはDECOのB-WINGSがベースです。
●B-WINGS
http://ja.wikipedia.org/wiki/B-WING
ちなみに以下のようなゲームです。(↓自分でプレイしてます)
●B-Wings最終面〜1000万点-1点(アーケード版)
http://www.youtube.com/watch?v=50I6ig_OiO8
●最終面付近
http://www.youtube.com/watch?v=aQ_WICEFqQE
B-WINGSの基板も持ってます(ゼビウスの基板なども持ってます)。基板は2層になっています。かなり重い基板です。
------------------------------------------------------------------------------------------------
■背景画像の合成
まず、ゲームの画面構成ですが以下のように2枚のCanvasを用意し、同じ位置に重ねます。この手法は本書のレッスン40でも採用している方法です。
画面は奥にあるのが宇宙空間の背景と自機や敵を表示するCanvas(id="main")で、手前(id="gournd")にあるのが地形を表示するCanvasです。このようにすると敵や自機が地形の奥に表示されるようになります。
地形のCanvasを別にしてあるのは本書のレッスン40と同じように描画された地形のαチャンネルを読み込んで当たり判定を行っているためです。このため、描画される地形はPNG形式でαチャンネルが含まれていなければいけません。透明部分は当たり判定がなく、不透明部分に当たると接触したと見なされ自機がやられます。つまりゲームオーバーです。
------------------------------------------------------------------------------------------------
■初期化
それでは初期化部分を見てみましょう。これまでのゲームと同様に自機の座標や敵、敵弾の座標を入れる配列等を用意します。
今回のゲームでは新たにパワーアップアイテムを入れておくためのpowerData配列を用意しています。パワーアップアイテムは最大6つ出るようになっていますが、今回のサンプルではそこまで多数のパワーアップアイテムがでることはありません。パワーアップアイテムを持った敵の出現頻度を高くすれば、同時にいくつものパワーアップアイテムが出現することになります。
var game = {
fighterX : 10, // 自機のX座標
fighterY : 160, // 自機のY座標
mouseX : 0, // マウスのX座標
mouseY : 0, // マウスのY座標
score : 0, // ゲームのスコア
charSize : 32, // 画像の幅(32×32)
beamMax : 1, // ビームの最大数
beamData : [null, null, null,null, null, null,null, null, null,null, null, null], // ビームの座標などを入れる配列(最大12連射)
ikaCount : 0, // 敵の出現頻度制御
ikaMax : 6, // 敵の最大出現数
ikaData : [null, null, null, null, null, null ] ,// 敵の座標などを入れる配列(最大6)
tamaMax : 4, // 弾の最大出現数
tamaData : [null, null, null, null ],// 弾の座標などを入れる配列(最大4)
mapX : 0,
mapWidth : -2400 + 480, // 最大値
mapCounter : 0, // マップカウンタ
mapSpeed : 2, // マップがスクロールする速度
bakData : [null, null, null, null, null, null ], // 爆発データを入れる配列
bakMax : 6, // 爆発の最大数
powerMax : 6, // 同時に6つまでしかでない
powerCount : 8, // 10匹に1匹の割合でオレンジ色の敵を出すためのカウンタ
powerData : [null, null, null, null, null, null ] // パワーアップアイテム
};
------------------------------------------------------------------------------------------------
■ページ読み込み後の処理
ページが読み込まれた後には、これまでのゲームと同じようにCanvasのコンテキストの取得とマウスイベントを設定します。Canvasは2つあるので、それぞれのCanvasのコンテキストを取得しておきます。
次にマップ画像データを読み込みます。マップ画像が読み込まれたらタイマーをスタートさせます。マップ画像は前にも書いたようにPNG形式でαチャンネル付きにしておきます。不透明部分のみ当たり判定があります。
window.addEventListener("load", function(){
var canvasObj = document.getElementsByTagName("canvas")[0];
context = canvasObj.getContext("2d");
contextMap = document.getElementsByTagName("canvas")[1].getContext("2d"); // 前面の地形
window.document.addEventListener("mousemove", moveMyFighter, false);
window.document.addEventListener("mousedown", startBeam, false);
mapImage.src = "images/map.png";
mapImage.onload = function(){
timerID = setInterval("gameProc()", 50);
}
}, true);
------------------------------------------------------------------------------------------------
■地形との当たり判定
まず、地形との当たり判定を見てみましょう。このゲームではhitCheck()関数を作成しています。この関数の中身は本書のレッスン40で使ったものと同じです。αチャンネルを読み出し加算しています。加算した結果によって接触したかどうかを真偽値(trueかfalse)で返しています。Canvasのαチャンネルの読み出し方法については本書の9章とレッスン40を参照してください。
function hitCheck(ctx, x,y,w,h){
var pixel = ctx.getImageData(x, y, w, h);
var count = 0;
for(var i=0; i 0){ return true; }
return false;
}
------------------------------------------------------------------------------------------------
■ビームと地形の当たり判定
ビームと地形の当たり判定を見てみましょう。すでに用意してあるhitCheck()関数にビームのX,Y座標と判定する幅を指定します。非常にシンプルな仕組みなので、ここまでの補習講義を読んできたのであれば十分理解できるでしょう。
function hitCheck_beam_ground(){
for(var i=0; iと
です。
通常の敵は種類を"normal"に、パワーアップアイテムを持った敵は"power"にしておきます。とりあえずパワーアップアイテムを持った敵は10回に1回の割合で出現するようにします。
var ikaType = "normal";
var ikaID = "ika"; // 表示する敵のID
game.powerCount = game.powerCount + 1;
if (game.powerCount > 10){
game.powerCount = 0;
ikaType = "power"; // パワーアップアイテムを持った敵
ikaID = "ika2";
}
後は敵を出現させる際に種類(type)とIDを追加したデータを配列に入れます。
game.ikaData[i] = { type: ikaType, id : ikaID, x : 480, y : y, dx : -dx, dy : dy };
全体としては以下の関数になります。
function startIka(){
game.ikaCount = game.ikaCount + 1;
if (game.ikaCount < 32){ return; } // 32回に1回の割合で敵を出す
game.ikaCount = 0;
if (game.mapCounter > 300){ game.ikaCount = 8; } // 途中から難易度を上げる
if (game.mapCounter > 500){ game.ikaCount = 20; } // 途中から難易度を上げる
if (game.mapCounter > 700){ game.ikaCount = 24; } // 途中から難易度を上げる
var ikaType = "normal";
var ikaID = "ika"; // 表示する敵のID
game.powerCount = game.powerCount + 1;
if (game.powerCount > 10){
game.powerCount = 0;
ikaType = "power"; // パワーアップアイテムを持った敵
ikaID = "ika2";
}
for(var i=0; i 180){ dy = 2; }
if (game.mapCounter > 700){ dx = dx * 2; } // ある程度マップが進んだら速度を2倍にする
game.ikaData[i] = { type: ikaType, id : ikaID, x : 480, y : y, dx : -dx, dy : dy };
return;
}
}
}
------------------------------------------------------------------------------------------------
■パワーアップアイテムを出す
ビームと敵の当たり判定が行われた際、やっつけた敵がパワーアップアイテムを持った敵の場合、以下のようにしてパワーアップアイテムを出現させます。この仕組みは敵を出現させる、ビームを発射する仕組みと同じです。
function startPowerupItem(x, y){
for(var i=0; i (px+32)) || ((fx+32) < px) || (fy > (py+32)) || ((fy+20) < py) ){ continue; }
if (game.beamMax < 12){
game.beamMax = game.beamMax + 1; // 連射可能数を増やす
}
game.powerData[i] = null; // パワーアップアイテムを消す
}
}
ここまで読むと似たような処理がたくさんあることに気づくでしょう。当たり判定の多くは同じ仕組みですし、描画処理も何を描画するかの違いしかありません。
このサンプルのように、様々なものを個別に処理していくと処理速度の面でも不利ですし、敵を追加したり新たなゲーム要素を追加していくことが難しくなります。それでは、どうすればよいのでしょうか? それは、1つの配列で全てを管理してしまえばよいのです。そのようにすれば、当たり判定は1つの関数で済みます。描画処理も同様です。
このように、うまくまとめることができれば疑似3Dのゲーム(スペースハリアーやアフターバーナー)を作る事もできるでしょう。
●スペースハリアー
http://ja.wikipedia.org/wiki/スペースハリアー
●アフターバーナー/アフターバーナーII
http://ja.wikipedia.org/wiki/アフターバーナー_(ゲーム)
------------------------------------------------------------------------------------------------
■HTML (index.html)
------------------------------------------------------------------------------------------------
横スクロールシューティングゲーム
------------------------------------------------------------------------------------------------
■JavaScript(side.js)
------------------------------------------------------------------------------------------------
// 横スクロールシューティングゲーム
// Game用の変数
var context = null;
var contextMap = null; // 地形用のCanvasのコンテキストマップ
var timerID = null;
var mapImage = new Image();
var game = {
fighterX : 10, // 自機のX座標
fighterY : 160, // 自機のY座標
mouseX : 0, // マウスのX座標
mouseY : 0, // マウスのY座標
score : 0, // ゲームのスコア
charSize : 32, // 画像の幅(32×32)
beamMax : 1, // ビームの最大数
beamData : [null, null, null,null, null, null,null, null, null,null, null, null], // ビームの座標などを入れる配列(最大12連射)
ikaCount : 0, // 敵の出現頻度制御
ikaMax : 6, // 敵の最大出現数
ikaData : [null, null, null, null, null, null ] ,// 敵の座標などを入れる配列(最大6)
tamaMax : 4, // 弾の最大出現数
tamaData : [null, null, null, null ],// 弾の座標などを入れる配列(最大4)
mapX : 0,
mapWidth : -2400 + 480, // 最大値
mapCounter : 0, // マップカウンタ
mapSpeed : 2, // マップがスクロールする速度
bakData : [null, null, null, null, null, null ], // 爆発データを入れる配列
bakMax : 6, // 爆発の最大数
powerMax : 6, // 同時に6つまでしかでない
powerCount : 8, // 10匹に1匹の割合でオレンジ色の敵を出すためのカウンタ
powerData : [null, null, null, null, null, null ] // パワーアップアイテム
};
// ページが読み込まれた時の処理
window.addEventListener("load", function(){
var canvasObj = document.getElementsByTagName("canvas")[0];
context = canvasObj.getContext("2d");
contextMap = document.getElementsByTagName("canvas")[1].getContext("2d"); // 前面の地形
window.document.addEventListener("mousemove", moveMyFighter, false);
window.document.addEventListener("mousedown", startBeam, false);
mapImage.src = "images/map.png";
mapImage.onload = function(){
timerID = setInterval("gameProc()", 50);
}
}, true);
// 移動&表示処理
function gameProc(){
context.clearRect(0,0,480,320);
contextMap.clearRect(0,0,480,320);
// マップの移動処理
game.mapCounter = game.mapCounter + 1;
document.getElementById("stat").innerHTML = game.mapCounter;
game.mapX = game.mapX - game.mapSpeed;
if (game.mapX < game.mapWidth) { game.mapX = 0; game.mapCounter = 0; }
// 自機の移動処理
if ((game.mouseX < game.fighterX) && (game.fighterX > 4)){ game.fighterX = game.fighterX - 8; }
if ((game.mouseX > game.fighterX) && (game.fighterX < 400)){ game.fighterX = game.fighterX + 8; }
if ((game.mouseY < game.fighterY) && (game.fighterY > 4)){ game.fighterY = game.fighterY - 8; }
if ((game.mouseY > game.fighterY) && (game.fighterY < 300)){ game.fighterY = game.fighterY + 8; }
startIka(); // 敵を出現させる
moveIka(); // 敵を移動させる
moveBeam(); // ビームを移動
moveTama(); // 敵弾を移動させる
movePowerupItem(); // パワーアップアイテムを移動させる
drawTama(); // 敵弾を描画する
drawBeam(); // ビームを描画
drawPowerupItem(); // パワーアップアイテムを描画する
drawIka(); // 敵を描画する
drawBak(); // 爆発パターンを描画する
// マップの描画
contextMap.drawImage(mapImage, game.mapX, 0);
// 自機の表示
var img = document.getElementById("figter");
context.drawImage(img, game.fighterX, game.fighterY);
// スコアの表示
contextMap.fillStyle = "red";
contextMap.font = "normal bold 14px Tahoma";
contextMap.fillText("SCORE "+game.score, 5, 20);
// ビームと地形の当たり判定
hitCheck_beam_ground();
hitCheck_beam_ika(); // ビームと敵の当たり判定
// 自機とパワーアップアイテムの当たり判定
hitCheck_fighter_power();
// 自機と敵、弾、地形の当たり判定
if ((hitCheck_fighter_tama() == true) || (hitCheck_fighter_ika() == true) ||
hitCheck(contextMap, game.fighterX, game.fighterY+5, 32, 10)){
clearInterval(timerID); // タイマー解除
contextMap.fillStyle = "red";
contextMap.font = "normal bold 24px Tahoma";
contextMap.fillText("GAME OVER", 180, 160);
}
}
// 自分の移動処理
function moveMyFighter(evt){
game.mouseX = evt.clientX-20;
game.mouseY = evt.clientY-20;
}
// ビームを発射
function startBeam(){
for(var i=0; i 480){
game.beamData[i] = null; // 画面外に消えたらnullにする
}
}
}
// ビームを描画
function drawBeam(){
var beam = document.getElementById("beam");
for(var i=0; i 300){ game.ikaCount = 8; } // 途中から難易度を上げる
if (game.mapCounter > 500){ game.ikaCount = 20; } // 途中から難易度を上げる
if (game.mapCounter > 700){ game.ikaCount = 24; } // 途中から難易度を上げる
var ikaType = "normal";
var ikaID = "ika"; // 表示する敵のID
game.powerCount = game.powerCount + 1;
if (game.powerCount > 10){
game.powerCount = 0;
ikaType = "power"; // パワーアップアイテムを持った敵
ikaID = "ika2";
}
for(var i=0; i 180){ dy = 2; }
if (game.mapCounter > 700){ dx = dx * 2; } // ある程度マップが進んだら速度を2倍にする
game.ikaData[i] = { type: ikaType, id : ikaID, x : 480, y : y, dx : -dx, dy : dy };
return;
}
}
}
// 敵の移動処理(横に移動するように変更してあります)
function moveIka(){
for(var i=0; i 750)){ // ある程度マップが進んだら弾を撃つ
startTama(game.ikaData[i].x, game.ikaData[i].y+16);
}
if (game.ikaData[i].x < -40){ game.ikaData[i] = null; }
}
}
// 敵の描画処理
function drawIka(){
for(var i=0; i 480)){
game.tamaData[i] = null; // 画面外に消えたらnullにする
}
}
}
// 弾を描画
function drawTama(){
var tama = document.getElementById("tama");
for(var i=0; i tx) && (bx < (tx+game.charSize)) && (by > ty) && (by < (ty+game.charSize))){
game.beamData[i] = null; // ビームを消す
game.score = game.score + 10; // 敵を倒すと10点
startBak(tx, ty); // 爆発パターンを設定
// パワーアップアイテムの処理
if(game.ikaData[j].type == "power"){ // 敵の種類がパワーアップアイテムを持ったものだった!
startPowerupItem(tx, ty);
}
game.ikaData[j] = null; // 敵を消す
break; // ループから抜ける
}
}
}
}
// 弾と自機の当たり判定(緩く判定)
function hitCheck_fighter_tama(){
var fx = game.fighterX + 8;
var fy = game.fighterY + 8;
for(var i=0; i (tx+8)) || ((fx+16) < tx) || (fy > (ty+8)) || ((fy+16) < ty) ){ continue; }
return true; // 接触した事を知らせる
}
return false; // 当たっていない事を知らせる
}
// 敵と自機の当たり判定(緩く判定)
function hitCheck_fighter_ika(){
var fx = game.fighterX + 4;
var fy = game.fighterY + 4;
for(var i=0; i (tx+28)) || ((fx+24) < tx) || (fy > (ty+28)) || ((fy+8) < ty) ){ continue; }
return true; // 接触した事を知らせる
}
return false; // 当たっていない事を知らせる
}
// 爆発パターンを開始
function startBak(x, y){
for(var i=0; i 30){ game.bakData[i] = null; } // 爆発の半径が一定数を超えたら消す
}
context.globalAlpha = 1; // 不透明度を100%に戻す
}
// ------------------------------------------------------------------------------------------
// 接触判定。指定されたコンテキスト内のアルファチャネル値を読み出し(レッスン40参照)
function hitCheck(ctx, x,y,w,h){
var pixel = ctx.getImageData(x, y, w, h);
var count = 0;
for(var i=0; i 0){ return true; }
return false;
}
// ビームと地形の当たり判定
function hitCheck_beam_ground(){
for(var i=0; i (px+32)) || ((fx+32) < px) || (fy > (py+32)) || ((fy+20) < py) ){ continue; }
if (game.beamMax < 12){
game.beamMax = game.beamMax + 1; // 連射可能数を増やす
}
game.powerData[i] = null; // パワーアップアイテムを消す
}
}
------------------------------------------------------------------------------------------------