Flashな弾幕STGで60FPSを目指す

パララライザ

パララライザでどのぐらい動かせているのかというと、こんな感じ。

パララライザHardのクリアリプレイ(ネタバレ注意)

けっこうな弾量でてますよね。画面右上の数字がFPSです。PCの性能や弾の量によっても変わりますが、だいたい50〜60ぐらいは出せているかなと(ただし、IEだと遅い)。

ということで、Flash弾幕STG「パララライザ」の製作におけるポイントについてざっくり解説したいと思います。技術的にすごいことをやっているわけでもなく、これからAS3とかでゲームつくり始めたい人向けです。

弾幕モノに限らず、Flashゲームにおいてかなり困りごとなのが、FPSが安定しないのと、描画速度が遅いこと。FPSが安定しないのは、ゲームの中で精密性を要求しない(格ゲーならコマンドを簡易にする・弾幕ならイライラ棒な要素を減らす)ようにすればそんなに問題ではないのですが、描画が遅いのは本当に困ります。ガリガリ動くようなものを作ろうとおもったら、すぐに処理落ちしてくれます。なので、弾や敵を出すたびに富豪的にSpriteやBitmapなんかをフレーム毎にnewするなんてのは到底無理な話です。

幸いなことに、AS3で弾幕を高速に処理するというのは先人が開拓してくれていて、大変参考になりました。もちろん、弾幕でなくてもアクションゲームなり格闘ゲームなりに十分応用できます。

大きなポイントは、

  • 必要なBitmapDataは前もって生成しておき、処理速度の速いfillRectとcopyPixelで描画する
  • インスタンスはむやみにnewしないで使いまわす
  • 当たり判定は簡単に

といったところ。それぞれ解説します。

必要なBitmapDataは前もって生成しておく

基本的に、ゲーム中のグラフィックは全てBitmapDataを使います。ベクタにするとどうしても重くなるからです。私の場合、元々ドット絵しか作れないので好都合。

一番処理が重いのがBitmapDataの生成なのですが、一旦生成してしまえばいくらでも使いまわせます。なので、ステージの開始前などに必要なBitmapDataを一通り生成しておいて、それを元に描画します。(パララライザの場合、そんなに量もないのでステージごとではなくゲーム開始時に全部生成してる)

BitmapDataを元に描画する方法はいくつかあるのですが、その中でも一番高速なcopyPixelを使います。フレーム毎にfillRectで描画領域を塗りつぶし、弾や敵などのBitmapDataをcopyPixelしていきます。

これだけでもかなり処理速度が改善されるはずです。

ただ、copyPixelだけだと回転・拡大縮小などが出来ないので、回転角や拡大倍率ごとにまたBitmapDataを生成しておきます。大抵の場合、それほど細かく生成する必要はなくて、回転角なら15度とか30度とかごとに回転して生成するだけでも十分だったりします。どうしても細かい変化や調整が必要ならば、その部分のグラフィックだけSpriteにするなどでもなんとかなると思います。

また、描画する画面全体の大きさも処理速度に大きくかかわってくるので、必要最小限にしておきましょう。

インスタンスはむやみにnewしないで使いまわす

無駄にnewしないでインスタンスを使いまわすのは処理を軽くする基本ですね。BitmapDataを前もって生成しておくのと同じように、他のインスタンス生成もできるだけ使いまわします。

STGなんかだと敵やら弾やら、バンバンでてくるわりに消えるのもすぐです。出現するごとにnewして、必要なくなったら後はガーベージコレクタにまかせてしまうとかしていたら、大変なことになります。敵とか弾とかはどうせ似たようなモノなので、消えた後もインスタンスをブールしておいて、次の敵や弾の生成のときに使いまわしちゃいましょう。

まあ、前に作ったユキノネだとこれはやっていなかったので、弾が大量に出るシーンなどではけっこう処理落ちしたりしてましたね。ユキノネでは結局30FPSで妥協していました。

当たり判定は簡単に

ゲームにおいて、処理量が一番多く、不安定になりがちなのが当たり判定です。処理量を減らすために一つ一つの判定をできるだけ簡単にしましょう。FlashでありがちなhitTestはすごく重いので論外です。

当たり判定にもいろいろありますが、基本的には矩形判定が一番楽かつ処理も軽くていいと思います。あまり複雑にしすぎるとバグの元だし、2DSTGぐらいだとものすごく改善できるわけでもないので単純に(このあと少しだけ改善するのですが)。もちろん、当たり判定するごとに矩形を new Rectangle() するとかしてたらだめですよ?

STGで必要な当たり判定は、「自機と敵・敵弾」と「敵と自弾」があれば最低限です。あと、ゲームシステムによっては判定がもっと必要になりますね(地形判定・アイテム取得判定・弾消し判定・かすり判定などなど)

一番重要なのが「自機と敵・敵弾」で、この判定だけはしっかりやりましょう。被弾判定がおおざっぱすぎると、ストレスの元になります(某有名横シューの3作目とか)幸いなことに、1対多の判定なので、さほど処理量は多くなりません。矩形判定でやってもいいと思いますが、ここでは描画に使用したBitmapDataを利用する判定を考えます。

自機の当たり判定は1ピクセルと決めてしまって、その判定の部分に敵や敵弾が描画されているかどうかで判定するのです。

// buffer:BitmapData(敵・敵弾の描画領域), px,py:プレイヤーの当たり判定座標
if ((buffer.getPixel32(px, py) & 0xFF000000) != 0) {
	damaged(); // 被弾した!
}

といった感じ。大量の敵や敵弾との判定を一つ一つやる必要はなく、一発で判定できます。ただし、個々の敵・敵弾と判定しているわけではないので、ややおおざっぱな判定です。あと、敵弾ごとにダメージが違うとかいうことまでは判定できません。なので、「BitmapDataを利用して一気に判定 → 当たっていたらそれぞれ矩形判定する」などの方法があります。もちろん、BitmapData利用だと描画領域=当たり判定となるので、ゲームシステムによっては都合がわるいです。

ちなみに、パララライザの場合、被弾すると敵・敵弾に関わらずライフ1.0減少固定なので、BitmapDataでしか判定していません。「弾消し判定」もBitmapDataのみ。「敵と自弾」「アイテム判定」は個別に判定が必要なので、点と点との距離判定で行っています(矩形判定よりやや遅くおおざっぱですが、モノごとに当たり判定を設定する手間がほぼ無いので楽)。簡単に判定していてもまだ重い場合は、2フレームに1回しか判定しないとかするのもありだと思います。

ある程度厳密さや製作の手間は割り切ってしまって、それに合わせたゲーム内容にしてしまうというのも大切なことです。

もっと最適化

上の三つは処理に関わる一番大きいところで、これらだけでも一工夫すれば大抵は十分な速度が出せるのではと思います。

さらに、高速に処理を行うための細かいテクニックは他にも沢山あります。たいてい、AS3に限ったことではなく、他のほとんどの言語でも似たようなテクニックが通じます(むしろ逆)

あんまり最適化することを気にしすぎて製作が遅れたりするのも、わざわざFlashを作る意味があんまり無いので、最初は細かいことはほどほどでいいと思います。パララライザでもあんまりやってません。製作速度重視の構造が悪く汚い勢いで書いたコードになってます。いまはそれで十分だと思ってやってます。

まとめ

結局のところ、なんだかんだいってますが、古典的なゲーム製作テクニックそのままですね。

参考リンク

パララライザの製作にあたっても大変参考になりました。ありがとうございます。