Edited at

[連載] javascriptで作るシューティングゲーム的な何か(5)

More than 3 years have passed since last update.


さあ、はじめよう


はじめに

本稿は掲題の通り javascript を用いて[ シューティングゲーム的な何か ]を作ろうという試みについて解説するテキストの第五回です。


想定する読者


  • 割と暇である

  • プログラミングに興味がある

  • ゲーム作りに興味がある

  • javascriptの基本をマスターしたけど特に作るものがない

  • javascriptを使った動きのある処理を実装してみたい

  • canvas でなんか作ってみたい


本連載の狙い

本連載はどちらかというと初心者向けです。

このページに検索からやってきた「ゲーム作りてえええええ」と日に三十回くらい叫んでいる小中学生諸君は、まずjavascriptの基本をお勉強してから本連載を読みましょう。

また、最終的に出来上がる[ シューティングゲーム的な何か ]は、そんなに大層なものではありません。シューティングゲームがどのような感じで作られていくのか、その過程を眺めていろいろ考えていただくキッカケを作ることが本連載の狙いです。

また、本連載では伝わりやすさ優先でテキストを書きます。たとえば javascript には厳密にはクラスはありませんが、連載内でクラスという言葉を使って解説します。このあたりは理解しやすさや、雰囲気を伝えることを優先して書きます。


オンラインサンプル

本連載は全10回を予定しています。

各回にはその時点までの[ シューティングゲーム的な何か ]のサンプルが付属します。

各テキストからリンクを張っておきますので、オンラインで実際に動作確認が行えます。

サンプルプログラム一式については著作権とかライセンスとかそういったものは一切ありません。

ちなみに、最終的に完成する[ シューティングゲーム的な何か ]はこんな感じです。

マウスによる移動、クリックによるショットが可能です。ESC キーでプログラムを停止します。

javascriptで作るシューティングゲーム的な何か(1)

javascriptで作るシューティングゲーム的な何か(2)

javascriptで作るシューティングゲーム的な何か(3)

javascriptで作るシューティングゲーム的な何か(4)

javascriptで作るシューティングゲーム的な何か(5)

javascriptで作るシューティングゲーム的な何か(6)

javascriptで作るシューティングゲーム的な何か(7)

javascriptで作るシューティングゲーム的な何か(8)

javascriptで作るシューティングゲーム的な何か(9)

javascriptで作るシューティングゲーム的な何か(最終回)


書いてる人

書いてる人はdoxasという人です。

こんな企画もやってますので、少しでも javascript でシューティングゲームを作成することに興味がわいたら、ぜひ参加してください。待ってますよ!!


さて、つくろう


サンプルの実行結果


前回は自機キャラクターから、マウスクリックでショットが発射されるようになりました。これには自機ショット専用のクラスを用意することで対応しました。

今回はいよいよ敵を画面上に登場させます。

基本的には前回のように新しいクラスを実装してやる方法を用います。


Character.jsに敵のクラスを作る

自機ショットは、いったん配置するとmoveというメソッドのなかの処理としてひたすら上方向に進むようになっていました。画面の上方向に進み続け、画面外に消えた場合には生存フラグを降ろすという管理方法になっていました。

敵キャラクターも、基本的な部分は自機ショットと同様で、まずは配置 ⇒ 動かす ⇒ 特定の条件下で生存フラグを降ろす、という一連の流れで管理します。ただ、自機ショットと同じように書いては芸がないので、敵キャラクターに関しては タイプ という概念を取り入れ、指定されたタイプによって動作が変わるようにしてみましょう。


character.jsエネミークラス

function Enemy(){

this.position = new Point();
this.size = 0;
this.type = 0;
this.param = 0;
this.alive = false;
}

Enemy.prototype.set = function(p, size, type){
// 座標をセット
this.position.x = p.x;
this.position.y = p.y;

// サイズ、タイプをセット
this.size = size;
this.type = type;

// パラメータをリセット
this.param = 0;

// 生存フラグを立てる
this.alive = true;
};

Enemy.prototype.move = function(){
// パラメータをインクリメント
this.param++;

// タイプに応じて分岐
switch(this.type){
case 0:
// X 方向へまっすぐ進む
this.position.x += 2;

// スクリーンの右端より奥に到達したら生存フラグを降ろす
if(this.position.x > this.size + screenCanvas.width){
this.alive = false;
}
break;
case 1:
// マイナス X 方向へまっすぐ進む
this.position.x -= 2;

// スクリーンの左端より奥に到達したら生存フラグを降ろす
if(this.position.x < -this.size){
this.alive = false;
}
break;
}
};


コンストラクタ内部で初期化されるプロパティは、自機ショットのときとはちょっと違っています。


  • position = Pointクラスのインスタンス

  • size = エネミーの大きさ

  • type = エネミーのタイプ

  • param = 管理用のパラメータ

  • alive = 生存フラグ

今回はタイプと管理用のパラメータが初出です。

このタイプと管理用のパラメータがどのように使われるのかは、moveメソッドの中身を見てみるとわかると思います。

moveメソッドの中身では、まず管理用パラメータをインクリメントしています。これによりmoveメソッドが呼び出されるそのたびに、paramの中身は 1 ずつ増えていくことになります。

今後連載を続けていく中でparamプロパティを利用する場面が出てきますが、今回はとりあえず使いません。このプロパティがなんのためにあるのかという点は説明しておきます。

paramプロパティは、各エネミーキャラクターごとに、登場してからどれくらい経過したのかを計る指標になります。moveメソッドが呼び出されるたびにインクリメントが行われるため、paramの中身を見れば最初の登場から何度moveメソッドが呼ばれたのかを把握できます。

このparamの特性を利用すると、登場してから一定のタイミングでショットを撃つ、あるいは移動方法を変化させるなど、キャラクターに応じて柔軟に操作することが可能になります。このために、管理用パラメータとしてparamというプロパティを持たせているわけですね。

そして、もうひとつ今回から登場したtypeというプロパティの概念は、先ほどのparamのときにも触れたように、敵キャラクターにタイプという概念を持たせることで、複数の挙動を行うようにするための仕組みです。

moveメソッドの中身を見ると、typeの値によって処理が分岐しているのがわかると思います。


敵キャラクターのタイプ別分岐

Enemy.prototype.move = function(){

// パラメータをインクリメント
this.param++;

// タイプに応じて分岐
switch(this.type){
case 0:
// X 方向へまっすぐ進む
this.position.x += 2;

// スクリーンの右端より奥に到達したら生存フラグを降ろす
if(this.position.x > this.size + screenCanvas.width){
this.alive = false;
}
break;
case 1:
// マイナス X 方向へまっすぐ進む
this.position.x -= 2;

// スクリーンの左端より奥に到達したら生存フラグを降ろす
if(this.position.x < -this.size){
this.alive = false;
}
break;
}
};


タイプ1の場合は X のプラス方向、つまり右に向かって進みます。生存フラグを降ろす部分の処理も、右端から出て行った場合を想定したつくりになっています。

タイプ2は逆に、X のマイナス方向、つまり左に向かって進みます。

このように、タイプという概念を導入するとmoveメソッドというひとつの関数のなかで、タイプによって異なる動作をさせることが可能になります。今回は単に左右の進行方向の違いしかありませんが、たとえば横から登場して上に去るタイプの敵、あるいは画面内に留まり続けて破壊されるまでは消えない敵など、いくらでもタイプを振り分けることができます。


main.js 初期化周り

さて character.js へのクラス定義が終わったら、次は main.js 内での初期化です。

自機ショットのときと同様に、最大出現可能数や色の設定には定数を使います。


main.js定数初期化

var ENEMY_COLOR = 'rgba(255, 0, 0, 0.75)';

var ENEMY_MAX_COUNT = 10;

また、今回から追加されるグローバル変数がひとつあります。


main.jsに追加する変数

var counter = 0;


このcounterという変数は、シーンを管理するために使います。

先ほど、敵キャラクターの項で登場したparamプロパティと同じように、ゲーム全体の進行具合を把握する為にcounterは使います。無名関数によるループ処理が一度実行されるたびにcounterをインクリメントしていくようにします。すると、counterの中身を見ればゲーム開始から何フレーム経過しているのかがわかるというわけです。

また、onloadから呼ばれる処理のなかでは、無名関数によるループを開始する前にEnemyクラスのインスタンスを初期化する処理を入れておきます。


main.js敵キャラクター用インスタンスの初期化

var enemy = new Array(ENEMY_MAX_COUNT);

for(i = 0; i < ENEMY_MAX_COUNT; i++){
enemy[i] = new Enemy();
}

これは前回の自機ショットのときと同じような感じですね。


出現タイミングの管理

さて先ほども少し触れたように、敵の出現には経過フレーム数を格納しているcounterをうまく使います。

サンプルから該当する場面のコードを抜粋します。


main.js敵の出現管理

// エネミーの出現管理 -------------------------------------------------

// 100 フレームに一度出現させる
if(counter % 100 === 0){
// すべてのエネミーを調査する
for(i = 0; i < ENEMY_MAX_COUNT; i++){
// エネミーの生存フラグをチェック
if(!enemy[i].alive){
// タイプを決定するパラメータを算出
j = (counter % 200) / 100;

// タイプに応じて初期位置を決める
var enemySize = 15;
p.x = -enemySize + (screenCanvas.width + enemySize * 2) * j
p.y = screenCanvas.height / 2;

// エネミーを新規にセット
enemy[i].set(p, enemySize, j);

// 1体出現させたのでループを抜ける
break;
}
}
}


まず冒頭部分に書かれているとおり、今回のサンプルの場合は 100 フレームに一度、敵キャラクターを出現させます。

パーセントの記号は除算の剰余を返します。たとえば10 % 7の解は 3 です。

つまり上記のように 100 で割ってその余りだけを取得した結果が、0 と等しくなるのは当然 100 の倍数のときだけです。結果的に 100 フレームに一度だけ、出現管理のなかの処理が行われることになります。

出現管理の処理ではまず、敵キャラクターのaliveプロパティをチェックして、対象の敵の生存フラグを確認します。登場していない敵に対して処理をしても無意味なので、まず先にチェックするわけです。

出現していない敵キャラクターが見つかったときだけ、新規に敵キャラクターを出現させます。出現させると決まったとしてもまだ安心はできません。敵のタイプを決めないといけないからです。

敵のタイプは、やはりフレーム数をうまく利用して振り分けるようにします。もちろん、ゲームの内容次第ではランダムなどにしてもいいのですが、今回は二種類のタイプがありますのでそれらが交互に出現するようにしてみます。


main.js敵のタイプ振り分け

// タイプを決定するパラメータを算出

j = (counter % 200) / 100;

// タイプに応じて初期位置を決める
var enemySize = 15;
p.x = -enemySize + (screenCanvas.width + enemySize * 2) * j
p.y = screenCanvas.height / 2;


最初の時点でcounter % 100の結果が 0 であることは確認しています。ですから、この段階ではcounterの中身は確実に 100 の倍数になっているはずです。そこでさらに(counter % 200) / 100を計算すると、その結果は 0 か 1 のいずれかにしかならないはずですね。

これをそのままタイプとして使いましょう。

タイプによって、敵の進行方向が変わるわけですから、当然登場する座標も左右に振り分けてやる必要があります。

初期位置の計算はちょっとややこしい部分もあると思いますが変数jの中身は 0 か 1 のいずれかです。双方を代入して計算してみると、どのようなことが起こっているのかわかると思います。

エネミーキャラクターは、一度出現させたら必ずループを抜けるようにしておきましょう。そうしないと、一度のループ処理で全部のエネミーが一度に登場してしまいます。これは自機ショットのときにも話した概念ですので大丈夫でしょう。


まとめ

さて今回はいよいよ敵キャラクターが登場しました。

まだ移動するだけの存在にすぎませんが、次回はこの敵キャラクターが自発的にショットを発射してくるようにしてみましょう。

オンラインサンプル.05では実際のサンプルの動作を確認できます。