#さあ、はじめよう#
##はじめに##
本稿は掲題の通り 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 でシューティングゲームを作成することに興味がわいたら、ぜひ参加してください。待ってますよ!!
#さて、つくろう#
前回までで、敵キャラクターが画面上に登場するようになりました。
敵キャラクターにはタイプという概念を取り入れ、タイプによって動作が異なる設計ができるようにしました。
また、敵キャラクターの登場を管理するためにフレーム数をカウントしてタイミングを計る仕組みも実装しました。今回は登場した敵キャラクターがショットを撃ってくるようにしてみます。
##common.jsを改造する##
さて今回はまず、第一回で登場して以来一度も修正を加えていない汎用クラス用の common.js を修正します。
もう少し具体的に言うと、Point
クラスを拡張するのが目的です。
function Point(){
this.x = 0;
this.y = 0;
}
Point.prototype.distance = function(p){
var q = new Point();
q.x = p.x - this.x;
q.y = p.y - this.y;
return q;
};
Point.prototype.length = function(){
return Math.sqrt(this.x * this.x + this.y * this.y);
};
Point.prototype.normalize = function(){
var i = this.length();
if(i > 0){
var j = 1 / i;
this.x *= j;
this.y *= j;
}
};
今までは単純に X と Y の座標情報を格納するだけだったPoint
クラスにメソッドをみっつ追加しました。
実は、この修正は非常に重要なものですが同時にちょっと数学的な話になるので、少々難しいかもしれません。
わかりにくい部分もあるかと思いますが最低限イメージだけでもつかんでください。
まずdistance
メソッドですが、このメソッドは同じPoint
クラスのデータを引数として受け取って、自分自身の X Y 座標と引数で受け取った X Y 座標との間の差分を取ります。コードを見ると、減算処理が行われているのがわかると思います。
雰囲気をつかむ上ではこのメソッドは、ふたつのPoint
クラスの位置関係を求めるもの、と考えるといいと思います。実際の使い方は後述します。
次に登場するlength
メソッドは、自分自身に設定されている X Y 座標のデータから 大きさ を計算します。これも先ほどのdistance
同様にいまいち意味がわかりにくいと思いますが、簡単に言うと、現在設定されている X と Y がどのくらい大きいデータなのかを求める、というふうにイメージしてください。
そして、追加した最後のメソッド、normalize
ですが、これもまたちょっと難しい。
これは、正確言うとベクトルを正規化する処理なのですが、簡単に言うなら 比率を一定にする ための処理を行っています。
さて、数学にあまり詳しくないと、ここまで読んでもさっぱりわからないと思いますので、次項から補足していきます。
##ベクトルという概念##
Point
クラスに新しく追加したみっつのメソッド。
いずれも、ちょっとわかりにくい内容です。そして、これがなんの役に立つのやら想像もつかない――という人もいるでしょう。
これらを正しく理解するためには、 ベクトル についてちょっとだけお勉強しないといけません。
ベクトルについて既に知識のある方は、本項を読む必要はありません。また、今回は厳密な意味でのベクトルの解説はしません。ニュアンスが伝わりやすいように書きます。
ベクトルについてまったくわからないという人も、ぜひ軽い気持ちで読み進めてみてください。
あらかじめ知識のある方は、突っ込みたい部分もあると思いますがイメージをつかみやすくするために書いたものですので、その点ご理解ください。
さて、ベクトルは数学的にはいろいろ小難しい部分もあるのですが、わかりやすく言うと 向き と 大きさ を表すための表現方法です。
たとえば[ 10cm ]という言葉を見たとき、大抵の人はそれが何を表しているのかすぐにわかりますね。cm はセンチメートルのことであり、それが長さを表現しているものだろうということは一見してわかると思います。
これと同じように、ベクトルという概念を用いると、向き、そしてその方向に掛かる力の大きさ、このふたつを表現できます。
ベクトルの表現には、二次元であれば X と Y を用います。三次元なら、X Y Z のみっつを使います。本連載は二次元で話を進めてきているので、X と Y があれば十分です。(0, 0)
と書いた場合には、X と Y のいずれの方向にもまったく力が掛かっていない状態と表現できます。
(10, 5)
であれば、X 方向に 10 の力が、Y 方向に 5 の力が掛かっているということになります。
さて、ここでもう一度先ほどのメソッドを考えてみます。
Point.prototype.distance = function(p){
var q = new Point();
q.x = p.x - this.x;
q.y = p.y - this.y;
return q;
};
distance
メソッドは、ふたつのベクトルの差分を計算しています。これは、お互いのベクトルを比較することで、その力関係がどうなっているのかを表す新しいベクトルを作っていることと同じです。
このメソッドは後で実際に登場しますが、結論から書いてしまうと 敵キャラクターから自機へ向かってショットを飛ばす ときに必要になります。
敵キャラクターの座標と、自機キャラクターの座標、このふたつを比較してベクトルを作ると、そのベクトルは敵から自機への方向を表すベクトルになるのです。ちょっと不思議な感じもしますね。
敵キャラクターや自機キャラクターの位置は、ゲームの進行中常に絶えず変化し続けます。ですから、ベクトルという概念を用いることでより簡単に、双方の位置関係を把握できるようにしておくと都合がよいのです。
続いてはlength
メソッドです。
Point.prototype.length = function(){
return Math.sqrt(this.x * this.x + this.y * this.y);
};
さてこのメソッドは何をしているのでしょうか。
先ほども書いたように、このメソッドはベクトルの大きさを求めるためのメソッドです。ベクトルには、向きのほかに大きさという概念があります。たとえば(10, 5)
というベクトルと(100, 50)
というベクトル、いずれも向きは同じですが大きさが違います。
同様に(2, 1)
というベクトルもまた、(10, 5)
というベクトルと向きは同じです。
これを見るとわかると思いますが、ベクトルの向きとはすなわち X と Y の比率だと考えるとわかりやすいでしょう。ベクトルの大きさは、もうそのまま数字の大きさに表れているのでわかると思いますが、これを数学的にちゃんと計算するには、上記のlength
メソッドでやっているように平方根などを用いて計算してやる必要があります。
さて、三つ目のnormalize
メソッドについても考えてみます。
このメソッドは、ベクトルを正規化します。正規化というのは、ベクトルの大きさが 1 ちょうどになるように調整する作業のことです。
Point.prototype.normalize = function(){
var i = this.length();
if(i > 0){
var j = 1 / i;
this.x *= j;
this.y *= j;
}
};
先ほども書いたように、ベクトルには向きと大きさがあります。しかし、実際には向きの情報だけが必要で、大きさは無視したい場合があります。大きさを無視したいときは純粋に 向きだけがわかればいい ので、ベクトルを正規化して大きさをぴったり 1 にしてやります。
メソッドのなかで何をやっているのかを詳細まで理解する必要はありませんが、上記のような計算を行うと、ベクトルの大きさを計算するとちょうど 1 になるように X と Y の値が調整されます。これにより、単純な向きだけを表現したベクトルを得ることができます。
ここで紹介した、ややこしく難しく面倒くさい新メソッドたちは、このあと実際に登場します。
ここまで解説してきたものを読んでも、もしかしたら意味が全然わからない方もいるとは思います。その場合は、いずれ理解できることを信じてとりあえず使い方だけでも、この先を呼んでマスターしておきましょう。というわけで、次項からは使い方を実際に見ていきます。
##エネミーショットクラス##
character.js に、敵のショットを管理するためのクラスを追加します。
まずはそのコードを見てみます。
function EnemyShot(){
this.position = new Point();
this.vector = new Point();
this.size = 0;
this.speed = 0;
this.alive = false;
}
EnemyShot.prototype.set = function(p, vector, size, speed){
// 座標、ベクトルをセット
this.position.x = p.x;
this.position.y = p.y;
this.vector.x = vector.x;
this.vector.y = vector.y;
// サイズ、スピードをセット
this.size = size;
this.speed = speed;
// 生存フラグを立てる
this.alive = true;
};
EnemyShot.prototype.move = function(){
// 座標をベクトルに応じてspeed分だけ移動させる
this.position.x += this.vector.x * this.speed;
this.position.y += this.vector.y * this.speed;
// 一定以上の座標に到達していたら生存フラグを降ろす
if(
this.position.x < -this.size ||
this.position.y < -this.size ||
this.position.x > this.size + screenCanvas.width ||
this.position.y > this.size + screenCanvas.height
){
this.alive = false;
}
};
従来と同じように、コンストラクタで値を初期化していますね。
そして今回初めて登場したプロパティがあります。それがvector
です。
そう、これが先ほどから何度も登場しているベクトルを扱うために用意したプロパティです。
敵のショットを新規にセットする際に呼ばれるset
メソッドでは、引数として、初期位置だけでなくベクトルも受け取るようになっているのがわかりますね。
そしてこのベクトルが活躍するのが、move
メソッドのなかです。
// 座標をベクトルに応じてspeed分だけ移動させる
this.position.x += this.vector.x * this.speed;
this.position.y += this.vector.y * this.speed;
// 一定以上の座標に到達していたら生存フラグを降ろす
if(
this.position.x < -this.size ||
this.position.y < -this.size ||
this.position.x > this.size + screenCanvas.width ||
this.position.y > this.size + screenCanvas.height
){
this.alive = false;
}
自機キャラクターのショットのときには、移動は Y 方向にまっすぐ進むだけでした。
しかし、敵キャラクターのショットは、発射された時点で自機キャラクターがいた場所に向かって飛んでいきます。
そのようなショットの軌道を実現するには、以下の抜粋箇所のようにして計算します。
this.position.x += this.vector.x * this.speed;
this.position.y += this.vector.y * this.speed;
自分自身の現在地に、ベクトルとスピードを掛け合わせたものを加算するのです。
そして、勘のいい人なら気が付いたと思いますが、ここではベクトルが正規化されていたほうが都合がいいですね。どうしてだか、わかりますか?
たとえば上記のコードが実際に動作するとき、vector
の中身が(10, 5)
だった場合でも(1, 0.5)
だった場合でも、進む方向は同じです。ベクトルの X と Y の比率が同じだからです。しかし進む量は確実に前者のほうが大きいですね。
こういったことが起こるので、ベクトルは正規化して大きさを 1 ぴったりにしておいたほうが都合がいい場合が出てくるわけです。先ほどまではベクトルを正規化してなにが嬉しいの? という感じだった方も、これを見るとベクトルが正規化されていて嬉しい場合があることを、なんとなくイメージできるのではないでしょうか。
さて、敵のショットは、先述の通りその時点での自機キャラクターのいる向きに進む軌道となります。となると、自機キャラクターのショットのときのように、必ずしも上に向かって飛び去っていくとは限りません。
敵のショットはあらゆる方向に消えていく可能性があるので、それを踏まえて生存フラグを操作する処理を書いておきます。
// 一定以上の座標に到達していたら生存フラグを降ろす
if(
this.position.x < -this.size ||
this.position.y < -this.size ||
this.position.x > this.size + screenCanvas.width ||
this.position.y > this.size + screenCanvas.height
){
this.alive = false;
}
これで上下左右のあらゆる方向に対してチェックできます。
##やっと main.js##
さて、やっと main.js まで来ました。
ただ、今まで順番に本連載を読み進めてきた人なら、たぶんエネミーショットの追加はそれほど難しくないと思います。
まず追加する定数から。
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;
var ENEMY_COLOR = 'rgba(255, 0, 0, 0.75)';
var ENEMY_MAX_COUNT = 10;
var ENEMY_SHOT_COLOR = 'rgba(255, 0, 255, 0.75)';
var ENEMY_SHOT_MAX_COUNT = 100;
今回増えたのは、下から数えた二行分ですね。敵のショットの色を表す定数と、敵のショットの画面上に出せる上限値を表す定数です。今回のようにすると、最大で 100 まで敵のショットを画面上に出せることになります。
敵ショットのインスタンスの初期化も、今までと似たような感じで行います。
// エネミーショット初期化
var enemyShot = new Array(ENEMY_SHOT_MAX_COUNT);
for(i = 0; i < ENEMY_SHOT_MAX_COUNT; i++){
enemyShot[i] = new EnemyShot();
}
特に難しいことはないですね。
##敵に自発的にショットを撃たせる##
さて、それでは敵キャラクターのmove
メソッドなどを呼び出している箇所を修正して、自発的にショットを撃つように改造しましょう。
// すべてのエネミーを調査する
for(i = 0; i < ENEMY_MAX_COUNT; i++){
// エネミーの生存フラグをチェック
if(enemy[i].alive){
// エネミーを動かす
enemy[i].move();
// エネミーを描くパスを設定
ctx.arc(
enemy[i].position.x,
enemy[i].position.y,
enemy[i].size,
0, Math.PI * 2, false
);
// ショットを打つかどうかパラメータの値からチェック
if(enemy[i].param % 30 === 0){
// エネミーショットを調査する
for(j = 0; j < ENEMY_SHOT_MAX_COUNT; j++){
if(!enemyShot[j].alive){
// エネミーショットを新規にセットする
p = enemy[i].position.distance(chara.position);
p.normalize();
enemyShot[j].set(enemy[i].position, p, 5, 5);
// 1個出現させたのでループを抜ける
break;
}
}
}
// パスをいったん閉じる
ctx.closePath();
}
}
ポイントになるのは、中盤に出てくるパラメータのチェックを行っているところ。
if(enemy[i].param % 30 === 0)
というようにして、パラメータをチェックしている場所です。
敵キャラクターのparam
プロパティはmove
メソッドが呼ばれた際、その中でインクリメントされて毎フレーム 1 ずつ増えていきます。つまり上記のような条件でチェックすると、30 フレームに一回だけ、該当するフレームが出てくる計算になります。
そして、ショットをいざ撃つとなった場合には、ベクトルを算出してやる必要がありますね。
ここも、該当箇所を抜粋してみますのでよく考えてみましょう。
p = enemy[i].position.distance(chara.position);
p.normalize();
enemyShot[j].set(enemy[i].position, p, 5, 5);
変数p
はPoint
クラスのインスタンスです。
まずは敵キャラクターの位置と自機キャラクターの位置、このふたつの座標を用いてdistance
メソッドを実行して、戻り値をp
に取得します。
続いて、その取得したベクトルを正規化します。normalize
メソッドですね。
正規化したベクトルを、set
メソッドの引数に渡します。
これで、敵キャラクターから自機へ向かってまっすぐに飛んでいく敵ショットがセットできました。
#まとめ#
さて、今回は途中でベクトルが登場したことで今まで以上に難しい内容になりました。
シューティングゲームでは、敵の動きや弾道を計算する都合上、どうしてもベクトルに対する知識が必要になる、あるいはベクトルを用いたほうが都合がいい場面があります。
今回のテキストを読んでもいまいち理解できない場合が十分にあるでしょう。しかし、なにかしら興味を持ってもらえるキッカケにはなったのではないかと思います。
ベクトルなんてものはわかってしまえばそんなに難しくないのですが、どうしても最初は概念がつかみづらいです。
なんにしても、今回のサンプルを実行すると敵キャラクターが登場してから 30 フレームごとにショットを撃つようになりました。ベクトルなどについてはウェブ上にたくさん資料がありますので、わからないことはどんどん探してみてください。
オンラインサンプル.06で実際のサンプルの動作を確認できます。
次回は役者がおおよそ揃いましたので、ついに衝突判定を行うことになります。