これは scouty Advent Calendar 2018 の18日目の記事です。
はじめに
前回 Font AwesomeとJavaScriptでシューティングゲーム(その1) の続きです。
完成バージョンをやってみる: https://naga3.github.io/font-awesome-shooting/shoot6/
ランキング搭載バージョン: https://naga3.github.io/font-awesome-shooting/shoot7/
こちらはfirebaseを使っていますが、今回は解説していないです。
1000点行けたらスゴいです
GitHubリポジトリ: https://github.com/naga3/font-awesome-shooting
アジェンダ
前回は、クラス分割・jQueryの使用をやめるところまでやりました。
今回は以下のことをやります。盛りだくさんです。
- モジュール分割・弾の連射・敵の複数表示
- シーン切り替え・スコア・レベル
- 背景スクロール・敵の種類・スマホ対応など
モジュール分割・弾の連射・敵の複数表示
弾と敵がひとつずつなのは寂しいので、複数表示できるようにしてみました。
また、ソースコードが長くなりそうなので、モジュールに分割しました。
ソースコードはこちら: https://github.com/naga3/font-awesome-shooting/tree/master/shoot4
モジュール分割
モジュール分割でよく使われているのが、webpackなどでモジュールを統合することですが、多少前準備が必要になるので、使っていません。また、最近のJavaScriptにはモジュール分割する機構がありますが、CORSに引っかかってローカルで気軽に試せなくなるので、今回は使っていません。昔ながらのscriptタグでの分割をしています。
<script src="Mouse.js"></script>
<script src="Character.js"></script>
<script src="Bullet.js"></script>
<script src="Enemy.js"></script>
<script src="Player.js"></script>
<script src="main.js"></script>
クラス毎に分割しています。
弾の連射
一画面に弾を複数表示するには、各弾の生存管理をしなければなりません。
Bullet.js
のBulletCollection
が弾の管理をするクラスです。
// 弾コレクションクラス
class BulletCollection {
constructor() {
this.items = []
}
//・・・・・・
}
配列this.items
に各弾Bullet
のインスタンスが入ります。まずコンストラクタで最大数MAX_ITEMS
までBullet
インスタンスを生成し、各弾の生存管理は表示・非表示is_show
で管理しています。
弾を打つたびにBullet
インスタンスを生成したほうがシンプルな構造にになりますが、DOMの追加は非常に重い処理なので、最初にまとめてやっています。
マウスのボタンを押すと、Player
インスタンスからBulletCollection.born
メソッドがトリガーされ、弾が発生します。弾の配列を先頭から走査して、生存していない弾(is_show
がFalse
)をBullet.born
メソッドで発生させる処理をやっています。
コレクションのアルゴリズムについて
実際は、複数キャラクターの生存管理は、連結リストを使うのが良いと思います。
また、配列を使う場合でも、今回のようなフラグでの生存管理ではなく、splice
などによって配列自体のサイズ変更をしたほうがシンプルに実装できる場合があります。
ただ今回は、DOMを最初に全て生成しておくという関係上、配列サイズは固定にして、フラグによる生存管理方法を採っています。
敵の複数表示
こちらもやっていることは弾の連射と同じです。Enemy.js
のEnemyCollection
が敵の生存管理をするクラスです。
弾の場合はマウスクリックがトリガーとなって発生しますが、敵の場合はmain.js
の中で毎フレームEnemyCollection.born
メソッドが呼ばれます。毎フレーム敵が発生するとワチャワチャしてしまうので、interval
変数で発生頻度を抑えています。
敵が自機を追跡する
Enemy.vy
変数を追加していますが、これは敵の上下方法の速度です。Enemy.move
メソッドに自機の座標を渡し、自機を追いかけてくるようなロジックにしています。
this.vy += 0.0002 * (py - this.y)
if (this.vy > 4) this.vy = 4
if (this.vy < -4) this.vy = -4
this.y += this.vy
自機と敵の座標の差分を速度にしていますが、そのままでは速すぎるので、ある程度のしきい値を設けています。
敵と弾との当たり判定
敵が複数で、弾も複数なので、複数×複数の当たり判定が必要になります。シンプルに二重ループでも良いのですが、今回はBulletCollection.hit_enemy
(すべての弾と敵1体の当たり判定)をEnemyCollection.hit_bullets
内からすべての敵に対して呼び出すことによって、当たり判定を行っています。
シーン切り替え・スコア・レベル
タイトル画面とゲームオーバー画面を追加し、自機が敵に当たったらゲームオーバー画面に移行するようにします。ゲームオーバー画面ではスコアを表示します。
さらに、レベルを追加し、敵の攻撃がだんだん激しくなるようにしています。
ソースコードはこちら: https://github.com/naga3/font-awesome-shooting/tree/master/shoot5
シーン切り替え
メイン画面に加え、タイトル画面・ゲームオーバー画面を追加しました。
文字はGoogle Fontsの Fredoka One を使っています。HTMLに以下の一文を書き、CSSでフォントの指定をするだけで使えます。便利な時代になったものです。
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Fredoka+One">
body {
font-family: 'Fredoka One', cursive;
}
シーン切り替えを実装する場合は、シーンが始まるときにオブジェクトやイベントリスナの初期化を正しく行い、次のシーンに切り替わるときに正しく後始末をするのが重要になります。
main.js
内でクラスとしてシーンを分割し、その中で初期化と後始末の責務を果たしています。initTitle
がタイトル画面、initMain
がメイン画面、initOver
がゲームオーバー画面のシーンの初期化をしていますになっています。
自機と敵との当たり判定
シーンを作らないとゲームオーバーに移行できないので、自機と敵の当たり判定をここでやっと実装しました。
Player.hit_enemy
メソッドで当たり判定を行っていますが、弾と敵との当たり判定とは若干処理が違います。
if (this.x >= enemy.x - enemy.width / 2
&& this.x < enemy.x + enemy.width / 2
&& this.y >= enemy.y - enemy.height / 2
&& this.y < enemy.y + enemy.height / 2) return true
return false
自機の中心の1ピクセルのみ、当たり判定があります。弾と敵との当たり判定はなるべく大きく、自機と敵との当たり判定はなるべく小さくすることによって、ゲーム中のストレスを少なくする狙いがあります。
レベル
毎フレームにレベルをカウントアップして行き、敵の出現間隔を少しずつ狭めています。EnemyCollection.born
を参照してみてください。
this.interval += Math.log(level) * 5
if (this.interval > 2000) {
// born
レベルをそのまま出現間隔にすると勢いがつきすぎるのでlog関数で緩やかにしています。
背景スクロール・敵の種類・スマホ対応など
背景スクロールや、自機や敵に少しスタイリングして、完成度を上げました。
ソースコードはこちら: https://github.com/naga3/font-awesome-shooting/tree/master/shoot6
背景スクロール
もちろん背景もFont Awesomeのアイコンを使います。
Background
クラスに背景に使うアイコン・色・サイズの配列を入れておき、毎フレームBackground.scroll
メソッドを呼びスクロールしています。
敵の種類
今回は3種類の敵を作りました。
-
EnemyBird
は今までと同じ、Y軸方向を合わせて自機に向かってくる敵です。CSSのroatateで自機の方向に首を傾けるようにしています。 -
EnemyHippo
はたまにジャンプする敵です。最初にマイナスの速度を設定し、毎フレーム一定の値をプラスすることによって放物線のような効果を出しています。CSSのrotateを使って、ジャンプするときは首を上げ、落ちるときは首を下げるようにして、多少自然な動きになるようにしています。 -
EnemyDice
はX軸方向もY軸方向も執拗に自機を追いかけてくる敵です。Font Awesomeの機能で、classにfa-spin
を追加すると、くるくる回るようになります。
3クラスとも共通でborn
メソッドとmove
メソッドを持つことによって、独自の動きを演出可能で、さらにEnemyCollection
側からはインスタンスがどのクラスかを知る必要がありません。C++などではポリモーフィックな設定をしっかりしなければいけませんが、JavaScriptでは何も考えずただ呼べるので楽です(その分、バグも混入しやすくなりますが)。
スマホ対応
Mouse
クラスを拡張してタッチイベントを取るようにしたので、ある程度スマホでも動くようになったと思います。スマホの場合は、タッチした場所に自機を移動させると指でキャラクターが隠れてしまうので、タッチした場所からの増分で自機を動かすようにしました。
const mx = e.changedTouches[0].pageX
const my = e.changedTouches[0].pageY
this.x += mx - this.tx
this.y += my - this.ty
this.tx = mx
this.ty = my
TouchEvent.changedTouchesにはタッチした場所の情報が入ります。スマホはマルチタッチ可能なので配列になっています。this.tx
, this.ty
には直前のフレームでタッチした場所を保存しておきます。プレイヤーの座標に、(現在タッチした場所 - 直前でタッチした場所)を足すことによって、タッチした場所からの増分でプレイヤーを動かすようにしています。
終わりに
最初はFont Awesomeはゲームが作れるくらいキャラクターが揃っているなーという適当な思いつきでしたが、結構ちゃんとしたゲームになったのではないでしょうか。
あとは、面の概念やパワーアップなどを実装すると、シューティングゲームとしての完成度が上がると思います。