LoginSignup
10
3

More than 5 years have passed since last update.

Font AwesomeとJavaScriptでシューティングゲーム(その2)

Last updated at Posted at 2018-12-17

これは 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点行けたらスゴいです:santa:

shoot.gif

GitHubリポジトリ: https://github.com/naga3/font-awesome-shooting

アジェンダ

前回は、クラス分割・jQueryの使用をやめるところまでやりました。

今回は以下のことをやります。盛りだくさんです。

  • モジュール分割・弾の連射・敵の複数表示
  • シーン切り替え・スコア・レベル
  • 背景スクロール・敵の種類・スマホ対応など

モジュール分割・弾の連射・敵の複数表示

shoot4.png

弾と敵がひとつずつなのは寂しいので、複数表示できるようにしてみました。
また、ソースコードが長くなりそうなので、モジュールに分割しました。

ソースコードはこちら: 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.jsBulletCollectionが弾の管理をするクラスです。

// 弾コレクションクラス
class BulletCollection {
  constructor() {
    this.items = []
  }

  //・・・・・・
}

配列this.itemsに各弾Bulletのインスタンスが入ります。まずコンストラクタで最大数MAX_ITEMSまでBulletインスタンスを生成し、各弾の生存管理は表示・非表示is_showで管理しています。

弾を打つたびにBulletインスタンスを生成したほうがシンプルな構造にになりますが、DOMの追加は非常に重い処理なので、最初にまとめてやっています。

マウスのボタンを押すと、PlayerインスタンスからBulletCollection.bornメソッドがトリガーされ、弾が発生します。弾の配列を先頭から走査して、生存していない弾(is_showFalse)をBullet.bornメソッドで発生させる処理をやっています。

コレクションのアルゴリズムについて

実際は、複数キャラクターの生存管理は、連結リストを使うのが良いと思います。
また、配列を使う場合でも、今回のようなフラグでの生存管理ではなく、spliceなどによって配列自体のサイズ変更をしたほうがシンプルに実装できる場合があります。
ただ今回は、DOMを最初に全て生成しておくという関係上、配列サイズは固定にして、フラグによる生存管理方法を採っています。

敵の複数表示

こちらもやっていることは弾の連射と同じです。Enemy.jsEnemyCollectionが敵の生存管理をするクラスです。

弾の場合はマウスクリックがトリガーとなって発生しますが、敵の場合は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

シーン切り替え

メイン画面に加え、タイトル画面・ゲームオーバー画面を追加しました。

タイトル画面:
title.png
ゲームオーバー画面:
over.png

文字は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関数で緩やかにしています。

背景スクロール・敵の種類・スマホ対応など

shoot6.png

背景スクロールや、自機や敵に少しスタイリングして、完成度を上げました。

ソースコードはこちら: 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はゲームが作れるくらいキャラクターが揃っているなーという適当な思いつきでしたが、結構ちゃんとしたゲームになったのではないでしょうか。

あとは、面の概念やパワーアップなどを実装すると、シューティングゲームとしての完成度が上がると思います。

10
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
3