Edited at

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

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 でシューティングゲームを作成することに興味がわいたら、ぜひ参加してください。待ってますよ!!


さて、つくろう


サンプルの実行結果


前回は自機キャラクターをクラスで管理することについて考えました。

今回は、この自機キャラクターを管理するクラスを拡張することで、自機キャラクターがショットを撃てるようにしてみましょう。

ちなみに、ショットはマウスのクリックを検知して発射されるようにします。


グローバル変数と定数の追加

さて、まずは今回のサンプルで追加されるいくつかの変数と定数を見てみます。


main.jsの初期化部分を抜粋

// - global -------------------------------------------------------------------

var screenCanvas, info;
var run = true;
var fps = 1000 / 30;
var mouse = new Point();
var ctx;
var fire = false;

// - const --------------------------------------------------------------------
var CHARA_COLOR = 'rgba(0, 0, 255, 0.75)';
var CHARA_SHOT_COLOR = 'rgba(0, 255, 0, 0.75)';
var CHARA_SHOT_MAX_COUNT = 10;


今回新たに追加されたのがfireという変数、さらにふたつの定数です。

変数fireは、ショットを発射するのか、あるいはしないのか――これを真偽値で保持します。

先ほども書いたように、マウスのクリックを検知してショットを発射するようにしたいわけですから、クリックイベントから呼ばれる関数側では、この変数fireの値をtrueに設定してやります。ショットをひとつ発射したら、再度fireにはfalseを設定するわけです。

定数は、前回も登場したCHARA_COLORと同じように、自機ショットの色の指定に利用するCHARA_SHOT_COLORと、もうひとつCHARA_SHOT_MAX_COUNTを定義します。

CHARA_SHOT_MAX_COUNTのほうは、自機キャラクターが発射できるショットの上限を決めるために使います。

この上限値の意味は、あくまでも 画面上に出せるショットの最大値 です。ゲーム内でショットが 10 回しか撃てないという意味ではありませんので注意しましょう。

また前回のテキストで、ちょっとだけですがあえて定数を使ったほうがいい場合がある、という話をしました。

今回は自機キャラクターのショットの最大上限を定数で 10 と指定しましたね。これを、将来的に増やしたり減らしたりする際に、定数として管理していれば修正個所は初期化している部分の一か所だけで済みます。

キャラクターに固有の値はクラスのプロパティとして設定したほうがいい場合が多いです。しかし、定数を用いたほうが都合がいい場合もありますので、よく考えたうえで必要に応じて上手に定数扱いの変数を利用しましょう。


イベント用関数の追加

続いて、マウスクリックを検知する関数を追加します。


main.jsクリック検知用関数

function mouseDown(event){

// フラグを立てる
fire = true;
}

非常に簡素です。フラグを立てているだけですね。

この関数は、onloadから呼ばれるメインプログラムの中で、その他のイベント用関数と同様に登録しておきます。

該当箇所は以下のようなコードになるわけです。


main.jsイベント処理関連

screenCanvas.addEventListener('mousemove', mouseMove, true);

screenCanvas.addEventListener('mousedown', mouseDown, true);
window.addEventListener('keydown', keyDown, true);

特別難しいことはないと思います。

一応書いておきますが、本連載のサンプルではここまでのみっつのイベントのみを使います。今後はイベント関連は増えません。


character.js 側

main.js の中身をさらに詳しく見る前に、まずは character.js 側に追加する自機ショット用のクラスから見ていきましょう。


character.js

function CharacterShot(){

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

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

// サイズ、スピードをセット
this.size = size;
this.speed = speed;

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

CharacterShot.prototype.move = function(){
// 座標を真上にspeed分だけ移動させる
this.position.y -= this.speed;

// 一定以上の座標に到達していたら生存フラグを降ろす
if(this.position.y < -this.size){
this.alive = false;
}
};


CharacterShotというショット管理用の新しいクラスを追加します。

コンストラクタでは各種プロパティの初期化のみを行います。プロパティのそれぞれの意味は以下のようになっています。


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

  • size = 自機ショットのサイズ

  • speed = 自機ショットの進むスピード

  • alive = 自機ショットの生存フラグ

ほとんどが読んで字の如く、そのままの意味です。

最後の生存フラグというのはちょっとわかりにくいかもしれませんが、ショットは自機キャラクターと違い、画面上に描かれる場合とそうでない場合があります。aliveはこの描かれるべきなのかどうかという状態を管理するために使われるのですね。

プロパティのほかには、ふたつのメソッドが書かれています。

まず最初、setメソッドは引数をみっつほど受け取り、それをもとに自機ショットを初期化します。

引数には、ショットの初期位置とサイズ、そしてスピードを与えます。このメソッドが呼び出されると、自動的に生存フラグが立つようになっているのが見てわかると思います。

続いてもうひとつのメソッドmoveですが、これも読んで字の如く、ショットの動きに関する処理を受け持ちます。


character.js自機ショットのmoveメソッド

// 座標を真上にspeed分だけ移動させる

this.position.y -= this.speed;

// 一定以上の座標に到達していたら生存フラグを降ろす
if(this.position.y < -this.size){
this.alive = false;
}


canavas 上の座標は、マウスカーソルなどの処理でも扱ったように、左上角が原点(0, 0)です。そして右に行けば行くほど X が大きな数字になり、同様に下に行けば行くほど Y が大きな数字になります。

自機ショットは、サンプルでは上の方向にまっすぐ進むように実装します。つまり、Y の値がどんどん 小さくなっていく ようにするわけです。

また、ショットは当然最後は画面外に消えていきます。

ここでショットの生存フラグを降ろしておくようにしないと、ショットを再度撃つことができなくなります。

よって、ショットの位置とサイズを考慮したうえで、画面外に出てしまったショットの生存フラグは降ろすような処理が書かれています。


main.js でのショットの初期化

さて、続いては再び main.js 側に戻って、先ほどの自機ショットのクラスを初期化するところ周辺を見ていきます。

ショットクラスのインスタンスは、冒頭で定数として定義したCHARA_SHOT_MAX_COUNTの 10 という数値をもとにして、配列として生成します。


main.jsショットの初期化

var charaShot = new Array(CHARA_SHOT_MAX_COUNT);

for(i = 0; i < CHARA_SHOT_MAX_COUNT; i++){
charaShot[i] = new CharacterShot();
}

これで、定数で指定したとおり 10 個の自機ショットを扱うことができるようになりました。


ショットの生成

無名関数のループのなかでは、変数fireの中身を見ながら、ショットを撃つべきかどうか判断します。


main.jsショットの生成

// fireフラグの値により分岐

if(fire){
// すべての自機ショットを調査する
for(i = 0; i < CHARA_SHOT_MAX_COUNT; i++){
// 自機ショットが既に発射されているかチェック
if(!charaShot[i].alive){
// 自機ショットを新規にセット
charaShot[i].set(chara.position, 3, 5);

// ループを抜ける
break;
}
}
// フラグを降ろしておく
fire = false;
}


このように、定数CHARA_SHOT_MAX_COUNTを利用したループ処理を用意します。

ループ処理のなかでは、まずaliveプロパティを参照することで、ショットが既に発射されているのかどうかを判断します。

もし、全てのショットのaliveプロパティがtrueだった場合には、既に全てのショットが発射済みということになりますので、新規にショットを撃つことはできません。

もしalivefalseが設定されているショットが見つかったら、新規にsetメソッドを呼び出してショットをセットします。ショットを生成する初期位置は、当り前ですが自機のいる座標です。サイズは 3 で速度は 5 にしてみました。

また、その新規セットの下でbreakによってループを抜けるようになっていますが、これが結構重要です。

もし、breakを書き忘れてしまうと、firetrueだったときに、一度に全てのショットが発射されてしまいます。また、最終的にはfireの値もfalseに戻しておかないと、次に無名関数が呼ばれたときに無条件で再度ショットがセットされてしまいますので、気を付けましょう。


ショットを一度に描き出す

ショットを描くためのプロセスは、基本的には自機キャラクターを描く際と変わりません。

手順としては、パス設定開始を宣言 ⇒ パスを設定 ⇒ 描画命令 という流れです。ただし、ショットは最大で 10 個あります。毎回毎回、この手順を律儀に繰り返すというのは、ちょっと大変ですね。

そこで、次のようにします。


main.jsショットを描くルーチン

// パスの設定を開始

ctx.beginPath();

// すべての自機ショットを調査する
for(i = 0; i < CHARA_SHOT_MAX_COUNT; i++){
// 自機ショットが既に発射されているかチェック
if(charaShot[i].alive){
// 自機ショットを動かす
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 = CHARA_SHOT_COLOR;

// 自機ショットを描く
ctx.fill();


ここでまず注目すべきは、beginPathが一度しかコールされていない点です。同様に、図形を描くためのfillメソッドも一度しかコールされません。自機ショットは 10 個あるはずなのに、パスの開始と描画命令はそれぞれ一度しかコールされません。

同じ形状で、なおかつ同じ色の指定となる図形に関しては、連続して一気にパスを設定してまとめて一度の描画命令で canvas 上に描くことが可能です。その鍵を握っているのが今回初登場のclosePathメソッドです。

このclosePathメソッドは、そこまでに指定されたパスの設定をいったん区切ってくれます。次にパスの設定が行われた場合には、先ほどのパスとは別のものとして個別に設定されるようにしてくれるのです。

基本的にパスはひと繋ぎになっているものなので、もしclosePathメソッドを用いずに連続してパスを設定すると、全てのパスがつながった状態で図形が描き出されます。それはそれで複雑な図形を描けるので便利な場合もあるのですが、ショット同士がつながった状態で描かれてしまうのは困ります。そこで、closePathでそれぞれを個別に区切り、最後にまとめて描画するという今回のような方法を用います。


まとめ

今回の修正で、クリックによって自機がショットを放つようになりました。

最大で画面上に配置できる自機ショットは 10 個を上限にしているので、超クリック連射でも 10 個以上は配置されません。実際にサンプルを動作させて確認してみるのもいいでしょう。

オンラインサンプル.04で今回のサンプルの動作を確認できます。