シューティングゲーム、インベーダー・グラディウス・東方projectの弾幕STGなど様々な種類があり大変楽しいものです。
もちろんjavascriptで再現することができますが、難しい説明になりがちです。
「初学者向け」×「できるだけ丁寧に解説」が目標の記事となります。
理解の助けになれば幸いでございますので、よろしくお願いいたします。
各回一覧
パート毎に分けておりますが、どの回も「完成品の確認⇒コードの解説」の流れで進みます。
各回の一覧はこちら⇩
・第1回(マウスで移動できる自機)
・第2回(今回)
・第3回(敵機を画面に表示する)
・第4回(敵機の弾を発射できる)
・第5回(弾が衝突したら自機/敵機を消す)
完成品の確認
この記事では、キーを押したら弾を発射できる自機を作るまでが目標です。
左クリックをするとショットをうつことができます。
第1回からの修正点はjavaScript側だけです。
早速コードを見ていきましょう!
main.js
// - 変数 - //
var Canvas;
var info;
var ctx;
var Run = true;
var mouse = new Point();
var fire = false;
var Chara_Shot_Count = 10;
// - クラス - //
// Character
function Chara(){
this.position = new Point();
this.size = 0;
}
Chara.prototype = {
init:function(size){
//サイズ設定
this.size = size;
}
}
// Point
function Point(){
this.x = 0;
this.y = 0;
}
function Chara_Shot(){
this.position = new Point();
this.size = 0;
this.speed = 0;
this.alive = false;
}
Chara_Shot.prototype = {
set:function(position,size,speed){
//座標
this.position.x = position.x
this.position.y = position.y
//サイズ
this.size = size;
//スピード
this.speed = speed;
// 生存フラグを立てる
this.alive = true;
},
move:function(){
// 座標を真上にspeed分だけ移動
this.position.y -= this.speed;
// 一定以上の座標に到達⇒false
if(this.position.y < -this.size){
this.alive = false;
}
}
};
// - メイン - //
window.onload = function(){
// スクリーンの初期化
Canvas = document.getElementById('screen');
Canvas.width = 256;
Canvas.height = 256;
// 2dコンテキスト
ctx = Canvas.getContext('2d');
//Chara_Shot インスタンス化
var charaShot = new Array(Chara_Shot);
for(i = 0; i < Chara_Shot_Count; i++){
charaShot[i] = new Chara_Shot();
}
console.log(Chara_Shot)
// イベントの登録
Canvas.addEventListener('mousemove', mouseMove);
window.addEventListener('keydown', keyDown);
Canvas.addEventListener('mousedown', mouseDown);
// その他のエレメント関連
info = document.getElementById('info');
// 自機初期化
var chara = new Chara();
chara.init(10);
// 画面を繰り返し呼び出す
(function(){
// HTMLを更新
info.innerHTML = mouse.x + ' : ' + mouse.y;
// screenクリア
ctx.clearRect(0, 0, Canvas.width, Canvas.height);
// パスの設定を開始
ctx.beginPath();
// 自機の位置を設定
chara.position.x = mouse.x;
chara.position.y = mouse.y;
// 自機を描くパスを設定
ctx.rect(chara.position.x, chara.position.y,30,30);
// 自機の色を設定する
ctx.fillStyle = "black";
// 自機を描く
ctx.fill();
// fireフラグ trueなら処理が進む
if(fire===true){
// すべての自機ショットを調査する
for(i = 0; i < Chara_Shot_Count; i++){
// 自機ショットが既に発射されているかチェック
if(charaShot[i].alive === false){
// 自機ショットを新しくセット
charaShot[i].set(chara.position, 5, 10);
// ループ抜ける
break;
}
}
// フラグをfalseにする
fire = false;
}
// パスの設定を開始
ctx.beginPath();
// すべての自機ショット(=10)を調査する
for(i = 0; i < Chara_Shot_Count; i++){
// 自機ショットが既に発射されているかチェック
if(charaShot[i].alive===true){
// 自機ショットを動かす
charaShot[i].move();
// 自機ショットを描くパスを設定
ctx.arc(
charaShot[i].position.x,
charaShot[i].position.y,
charaShot[i].size,
0, Math.PI * 2, false
);
// パスをセーブ
ctx.closePath();
}
}
// 自機ショットの色を設定する
ctx.fillStyle = "red";
// 自機ショットを描く
ctx.fill();
// フラグにより再帰呼び出し
if(Run){setTimeout(arguments.callee, 1000/30);}
})();
};
// 関数
function mouseMove(event){
// マウスカーソル座標の更新
mouse.x = event.clientX;
mouse.y = event.clientY;
}
function keyDown(event){
// キーコード取得
var ck = event.key;
// spaceKeyで画面が止まる
if(ck === " "){Run = false;}
}
function mouseDown(event){
// フラグを立てる
fire = true;
}
今回の主役、javascriptのコードについて解説していきます。
ポイントは以下4つ
・変数
・関数 + イベント登録
・クラス
・自機ショットの描画
まずは分かりやすい変数から見て行きましょう。
変数
今回追加された変数は2つです。
//今回で追加されたものだけ抜粋//
var fire = false
var Chara_Shot_Count = 10
fireはtrue/falseを判定する為だけのもの
「ボタンを押したらtrueになって弾が発射される。だから初期値はfalseにしている」
という理解でOKです。
Chara_Shot_Countは画面上に存在できる弾の上限です。
弾それぞれを描画する時、以下の処理をする為に設定しています。
「全ての自機ショットが発射されてるかif文で判定⇒10より上なら描画しない。10より下なら描画する」
続いて、関数の設定をみていきましょう。
関数 + イベント登録
今回はmousedown関数(左クリックしたら発動する)を登録しました。
こちらはイベント登録
// イベントの登録
Canvas.addEventListener('mousemove', mouseMove)
window.addEventListener('keydown', keyDown)
Canvas.addEventListener('mousedown', mouseDown)//⇦⇦⇦これが追加された
そしてこちらがfire=trueにして発射の条件を満たすための関数です。
function mouseDown(event){
// フラグを立てる
fire = true;
}
⇒左クリックする
⇒mousedown関数が発動
⇒fireが「true」になる
⇒「fireがtrueの時は~」のif文で判定
★ゴール :「弾を大きさ○○で今の座標から□□動かして描画する」で処理する
なんとなく頭にいれて次のクラス設定の解説に進みましょう。
クラス
ここではChara_Shotクラスを設定しています。
コンストラクタとメソッドに分けてみてみましょう。
Chara_Shot コンストラクタ
function Chara_Shot(){
this.position = new Point();
this.size = 0;
this.speed = 0;
this.alive = false;
}
ここでは4つのプロパティを設定しています。
自機ショットについて、それぞれ以下のような意味になります。
size :大きさ
speed:スピード
alive :生存フラグ
最後の生存フラグについて説明します。
自機ショットは「画面上に描画される・されない場合」があります。
弾の発射ボタンを押した時のみ描画されないといけない ということですね。
これを管理するためにaliveを定義しています。
初期値=ゲームスタート時点は何も触れていない のでfalseとしているわけです。
Chara_Shot メソッド
Chara_Shot.prototype = {
//setメソッド//
set:function(position,size,speed){
//座標
this.position.x = position.x
this.position.y = position.y
//サイズ・スピード
this.size = size;
this.speed = speed;
// 生存フラグを立てる
this.alive = true;
},
//moveメソッド//
move:function(){
// 座標を真上にspeed分だけ移動させる
this.position.y -= this.speed;
// 一定以上の座標に到達していたら生存フラグを降ろす
if(this.position.y < -this.size){
this.alive = false;
}
}
}
2つのメソッド(set/move)を設定しています。
Chara_Shot:setメソッド
setメソッドは position:位置・size:大きさ・speed:速さ を受けとります。
同時にthis alive=true、弾を描画していい状態ですよ を設定しています。
『setメソッドで描画に必要な情報と条件を満たす』と理解いただくとよい思います。
Chara_Shot:moveメソッド
moveメソッドは弾が動いているように見せるためのものです。
「this.position.y -= this.speed」はこちらの画像を見るとイメージしやすいと思います。
縦の座標は左上が(0,0)です。
つまり、弾がまっすぐ真上に飛んでほしい時は「position.y」の値が小さくなればOK
小さくするために「this.speed」を呼び出すごとに「position.y」から引き算すれば画面の上に動いてくれます。
また、弾の位置が画面よりマイナスの座標(=画面外)になった時は生存フラグを戻します。その弾の生存フラグだけをfalseにするということです。
この処理がないと、画面上に存在できる弾の上限(=Chara_Shot_Count)をすぐに超えてしまい発射ができなくなってしまうからです。
ショットが画面に反映するまで
ここまで変数~クラス~メソッドの説明でした。
後は、ショットを画面に反映させるだけです!流れとしては
・生成 :既に10発以上発射されているかチェック
・描画 :ctx.beginPath~ctx.fill()
となります。
ショットの初期化からみていきましょう。
ショットの初期化
初期化とは「Chara_Shot_Countの数だけ初期化して配列を作ります」という意味です。
var charaShot = new Array(CHARA_SHOT_MAX_COUNT)
for(i = 0; i < CHARA_SHOT_MAX_COUNT; i++){
charaShot[i] = new Chara_Shot()
}
new Array()で配列のインスタンスを作っていますね。
Chara_Shot_Countの数(=10)だけfor文で回します。
配列の[i]番目それぞれにnew Chara_Shot()を作成していきます。
これでChara_Shot_Countの数だけ、自機ショットを初期化して生成する準備ができました。
ショットの生成
続いてはショットの生成です。
// fireフラグ trueなら処理が進む
if(fire===true){
// すべての自機ショットを調査する
for(i = 0; i < Chara_Shot_Count; i++){
// 自機ショットが既に発射されているかチェック
if(charaShot[i].alive === false){
// 自機ショットを新規にセット
charaShot[i].set(chara.position, 5, 10);
// ループを抜ける
break;
}
}
// フラグを降ろしておく
fire = false;
}
難しそうですが…具体的な処理はこの1か所だけです。
⇒『配列に入ってるcharaShotのi番目にsetメソッド(サイズ:3/速さ:5)使ってね』
この処理にたどり着くための前後2か所を解説しましょう。
ショットの生成の条件
// fireフラグ trueなら処理が進む
if(fire===true){
// すべての自機ショットを調査する
for(i = 0; i < CHARA_SHOT_MAX_COUNT; i++){
// 自機ショットが既に発射されているかチェック
if(charaShot[i].alive === false){
抜粋すると2つのif文があります。
間にあるfor文は『Chara_Shot_Countの数(=10)だけ○○しろ』という意味
○○には「if(charaShot[i].alive=== false){} 」が入ります。
⇒『fire=trueなら{}していいよ』
②if(charaShot[i].alive === false)
⇒『chara_Shotのi番目にaliveがfalseなら{}していいよ』
①は左クリックした時にtrueになるだけですね。
②については…
・aliveにfalseが設定されているショットがある⇒setメソッドが発動
・aliveにtrueが(全てのショットに)入っている ⇒全てのショットが発射済=弾はでない
↑のようになり、aliveの真偽値で発射するしないを判定します。
for文の中にbreakのある理由
for文の中にbreakがあります。
このようにbreakはループ処理を抜ける為のものです。
これがないと、alive=true だった時にまとめて全てのショットが発射されてしまいますのでご注意ください。
ショットの描画
ついに最後です!もう少しだけ頑張りましょう!
// すべての自機ショット(=10)を調べる
for(i = 0; i < Chara_Shot_Count; i++){
// 自機ショットが既に発射されているかチェック
if(charaShot[i].alive===true){
// 自機ショットを動かす
charaShot[i].move();
// 自機ショットを描くパスを設定
ctx.arc(
charaShot[i].position.x,
charaShot[i].position.y,
charaShot[i].size,
0, Math.PI * 2, false
);
// パスをセーブ
ctx.closePath();
}
}
// 自機ショットの色を設定する
ctx.fillStyle = "red";
// 自機ショットを描く
ctx.fill();
自機キャラクター描画する時と流れは同じです。
・何を描画するかパスを設定
・描画命令
今回はショット(=○)を描画するわけですが、毎回ショット10回分をbegin.path~fill()では面倒ですしミスの原因になってしまいます。
そこで今回はclosePathを使います。
『closePath()と書いたところまでのpathのみセーブする』
これがclosePathの効力で、ない場合は全てのショットが一筆書きのように描画されてしまいます。
⇩のように全てのショットの座標がつながったpathで描画されてしまうわけですね。
closePathの効果で「ショット1つづつのpathをセーブ⇒まとめて描画」することができるわけです。
まとめ
最後にポイントをおさらいしましょう。
②関数:左クリックでfire=trueになるイベント登録をする
③クラス:ショットの座標・大きさ・速さを設定
④描画 :ショットの上限と描画条件をaliveで管理⇒最後はclosePathを使う
特に③④のボリュームが大きかったと思いますが、ここを理解できれば「敵機を描画する」「敵のショットを描画する」コードにもイメージがつきやすいと思います。
当記事がjavascriptでゲームを作ったりする時の理解の助けになれば幸いです。
また、初学者向けのゲームの作り方と一行づつのコード解説をしております。
【JavaScript】 15パズルの作り方を初学者向けに1行づつ解説![前編]【ゲーム】
ゲームを作るのはとても勉強になりますし、モチベーション維持にもつながります。
興味があればご活用ください。