8-12 エフェクト

エフェクトとはゲーム自体を面白くするための要素であり、
画面をにぎやかにしたり音を出して臨場感を出したりするような演出のことです。

例えばアクションゲームのようにジャンプをしたら跳ね上がる絵に切り替わるのも演出と言えます。
さらにこれに同時にピョ~ンという音も付けることで、ユーザーはそのキャラを自分で動かした
という実感を味わうことができ、このゲームは面白いと認識するわけです。

逆に言うとめちゃくちゃつまらなそうなゲームなのに、
演出がものすごく凝っていると面白く感じてしまうこともあります。

例えば見た目はしょぼいレースゲームなのに、
クラッシュしたときの爆発がものすごく激しかったりすると、
それだけで楽しくなるかもしれません。

演出なし 演出あり


どのようなゲームでも演出は大事であり、
これがあるか無いかでまったく面白さが変わるわけです。

ということでここではオブジェを弾いたときに光る演出を実装してみます。
これがあるだけで視覚的に弾いた感じがして楽しくなると思います。


■光る効果

まず光る効果とはどういう処理が必要なのかを説明します。

光る効果というのはこちらで説明した加算合成を使います。
通常のアルファ合成と加算合成の違いは以下のような感じです。

通常 加算

まずここでは光らせる部分を1箇所にしぼって考えます。
光る効果は以下のような感じでアニメーションするとします。

このアニメーションを制御するためにはアニメーションカウンタ用の変数が必要となりますが、
ここではこのカウント用にint型の変数を使い、
さらに演出時は45→0になるように値を引いていくことにします。
※ここでの45とは演出速度を考慮した値です

なぜだんだん0に減らす必要があるかというと、この変数により演出処理中かを判断するためです。
この場合は0より大きな値であれば演出中と判断出来ます。

まずはゲーム開始時にこの変数を0にリセットして置きます。
そしてゲーム中は0より大きい値が入っていれば演出中と判断し、
さらにこの値を描画時に光り具合や拡大倍率として使用します。
これにより1つの変数で2つの処理を行う一石二鳥の処理が行えます。

ちなみにプログラム的には0とは初期状態であり、0になれば全て元に戻る、
または最初の状態に戻るという概念がそのまま当てはまるため、
考え方的にもしっくりくると思います。


■アニメーションで使う数式

さて、45→0というカウンタ値で上のアニメーションのように動かすにはどうしたらよいかの前に、
面白い演出とはどういうものかについて説明します。

まず、以下のアニメーションをよく見てください。

線型補完 二次補間

左側は全てのアニメーションが平行に行われる線型補間と言われる一次関数を用いた場合のもので、
右側は加速度が徐々に変化するような動きになっています。

よくFlashなどで拡大縮小時に左の線型補完を使うものを見ますが、
動きがのっぺりしてまったくつまらない動きをします。

ただ線型補完がまったくダメというわけではなく使いどころによるのですが、
出来れば右側の二次補間のようなアニメーションをさせたほうが見た目が面白くなります。
ちなみにFlashではイージングというところでこの動きを変えることが出来ます。

この処理を使って今回の演出を行う場合、まずは二次関数を使って曲線となる部分の係数を…というのは
難しすぎるのでやめにして、ここでは二次関数に似たようなグラフが抽出可能なsinやcosを使用します。

一般的にsin、cosは以下のような波形です。

sin
cos


sinとcosは90度位相がずれただけで、離れて見ればまったく同じ波形です。
これは中学で習うと思いますが今回はこんなのはどうでもよくて、
以下のcos波形の緑の箇所に注目してください。

cos


ここではこの0~90度の範囲にある二次曲線に近い波形部分を利用します。
今度はこの緑色の部分を抜き出して上下反転してみます。

反転した値は1から引くことで求められます。
つまり「1-cos(θ)」という計算式になります。
当然θ(シータ)は0~90度であり、それ以外の角度が入ると曲線は破錠します。

次からはこれらを利用して演出の計算式を作ってみます。

■アニメーションの要素1

まず上のアニメーションは2つの要素から成り立っています。

1つは徐々に透明になっていくことで、もう1つは徐々に拡大していくことです。
この2つが同時に合わさることでもやっとした演出をすることが出来ます。


では1つ目の徐々に透明になる部分ですが、もしゆっくり半透明になるようにした場合、
連続のオブジェを光らせると前のオブジェに重なってしまい、次のオブジェが見えなくなってしまいます。

次のオブジェが分からない

そこで、ここでは光った直後にすぐ透明になるようにしてみます。
これは単純に上で説明した1-cos(θ)をそのまま使用できます。

まずアニメーションカウンタは45→0になるということが決定されています。
この45→0というのを2倍にして90→0にすればθに直接代入することが出来ます。

この曲線の右→左、つまり90→0度に対する値がアニメーション時に使われる値となります。
よく見ると90~70度付近(黄色)では急激に下がり、70~40度付近(青)まではだんだんゆっくり下がり、
それより下の40~0度付近(ピンク)はかなりゆっくりと下がることになります。

ちなみにこの計算式の利点として結果は必ず0~1の範囲となり、
ちょうどアルファ値は0~1で指定するため直接この値をアルファ値に設定することが出来ます。


■アニメーションの要素2

次に2つ目の拡大処理について説明します。

この拡大方法ですがこちらも半透明と同様に曲線を使用します。
ここではパーンと弾けるイメージで画像を拡大してみることにします。

まずは拡大率についてですが、爆発が始まる最初の大きさを1倍とし演出が終わるころに2倍となるようします。
さらに細かく言うと光始めの時は一気に拡大し、最後の方ではゆっくり拡大するような演出にします。
これでパーんと弾けるイメージに近くなると思います。

ではこのような動きはどういった波形を利用すればよいかですが、
考えられるのは90→0度の範囲で、最初は0から一気に上がり、
1になるころにはゆっくり上がる波形を探します。
すると以下のグラフの緑色の部分が使えそうだと分かります。

cos

なお、ここでの最低のサイズは1倍としなければならないため、計算結果には+1をしなければなりません。
これを計算式にすると1+cos(θ)となり、これでパーンと大きくなる演出が出来ます。


最後にこれと前の半透明の処理を合成することでもやっと光る演出の完成です。


■1つ分の演出プログラム

上記の2つの式を実際にプログラムに組み込むと以下のような感じになります。

    dd.SetPutStatus( 20,
                     1-(float)cos( (double)count*2*3.14159/180 ),
                     1+(float)cos( (double)count*2*3.14159/180 ),
                     0 );
    dd.Put2( 20,xxx,yyy );

※ここでは参考にcountが45→0になる変数としています

CDDPro90のSetPutStatusは指定の切り抜き画像の状態を変更するものです。
この関数の第2引数はアルファ値、第3引数は拡大率となっているので、
ここでこの切り抜き画像に対して計算した値をセットし、
最後に中心原点の描画関数のPut2を使って表示しています。

■同一チャンネルで同時にたくさんのオブジェが光る場合

このゲームでは短い時間にオブジェが連続で登場することもあります。
例えば下のようにいくつかのオブジェが並んで落ちてくる場合です。

こういったオブジェを1つの演出ルーチンだけで光らせようとすると、
すぐに次のオブジェがまた光るためその都度カウンタがリセットされてしまい、
以前に光っていたものが消えてしまうように見えます。

さらに光る効果とは重ねれば重ねるほどさらに光っているように見えるものなのに、
1つだけの処理を使いまわすとなると連続オブジェでも光量は同一となってしまい、
せっかくのプレイ感が抜けてしまいます。


それではこれに対応するにはどのようにしたら良いか、
さきほどから書いている1つだけという部分に気が付けばすぐに分かると思いますが、
ようは1つだけではなく演出用のカウンタをたくさん増やせば良いのです。

ただ闇雲に増やしてもCPUやメモリの無駄使いになるので、
ゲームの速度や光る効果の飽和量を考えて、
ここでは1つのチャンネルにつき3つ分のバッファを使用することにします。

そしてここが重要ですが、この3つ分のバッファを1回弾くごとに次のバッファを使うという形とし、
最後のバッファを使ったら次は最初のバッファに戻るというループバッファ処理を行います。
これにより最大で3つ分の光る効果を同時に発動出来るため、
どこかの赤いヤツみたいに通常の3倍光らせることが出来るわけです。

ただし連続するオブジェが4つ以上出てくる場合はやはり最初の光は消えてしまうことになりますが、
実際にはそのようなデータはほとんど無いのともう見えない最初の光が消えてしまっても
人間の目がこれについていけないため、基本的には問題はありません。
※プログラムをいじればいくらでも変更出来るので気になる場合は調整しましょう


■3つのバッファを全チャンネル分

1チャンネルにつき3バッファを全チャンネル分用意するということは、
合計18個のバッファが必要になります。(6チャンネル×3)

実はこれは既にCGameのヘッダに定義してあります。
以下はこれを抜き出した部分です。

    int             iFlashIndex[6];     // 次に使用されるフラッシュカウンタのインデックス
    int             iFlashCount[6][3];  // フラッシュ6x3個分のカウンタ

iFlashIndexは次にどのバッファを使用するかを6チャンネル分、
iFlashCountは実際の演出用のカウンタであり、
1チャンネルあたり3個でこれを6チャンネル分定義しています。

ゲームがスタートする前にGameInit内でこれらの変数は0クリアしています。


■描画処理

それでは上記で説明した計算式を使って描画してみます。
これは今まで説明した内容を全バッファ分ループするだけなので簡単です。

ちなみにこの処理はオブジェの上に重ねる形で表示する必要があるため、
描画処理の中で一番最後に行います。
また、この処理は光る効果となるため描画の前にデバイスに合成方法を指示しておきます。

まずこのフラッシュは各レーンの中央を原点とします。
ここでは以下のように各レーンのX座標を定義しています。


そしてこの座標は扱いやすいようにインデックスリストとして定義しておきます。

    static const float obj_fx[6]    = { 14,29,44,58,72,109 };       // フラッシュ表示X座標


次にフラッシュとして表示される画像は以下のように各レーンに合わせて3パターン用意しています。

白鍵用 黒鍵用 スクラッチ用

これらの画像の中心が各レーンの中心に合わせて表示されるようにしますが、
以前定義していた各レーンごとにどのオブジェタイプが表示されるか、
というインデックスリストがここで再度利用されます。

これはオブジェ描画のページにて以下のように定義していました。

    static const int obj_kind[6]    = { 0,1,0,1,0,2 };              // オブジェの種類


これらを使ってフラッシュ表示を行うプログラムは以下のようになります。

    // フラッシュ表示
    dd.SetRenderState( D3DRS_DESTBLEND, D3DBLEND_ONE );         // 加算合成
    for( j=0;j<6;j++ ) {
        for( i=0;i<3;i++ ) {
            if( iFlashCount[j][i]>0 ) {
                // 演出が存在する場合のみ表示
                dd.SetPutStatus(
                    obj_kind[j]+20,
                    1-(float)cos( (double)iFlashCount[j][i]*2*3.14159/180 ),
                    1+(float)cos( (double)iFlashCount[j][i]*2*3.14159/180 ),
                    0 );
                dd.Put2( obj_kind[j]+20,obj_fx[j],416 );
            }
        }
    }


最初にこれ以降の描画関数で加算合成されるようにSetRenderStateにてステートを変更しています。

次に二重のforを使ってフラッシュを1チャンネル当たり3個を全6チャンネル分ループして表示しています。

そしてカウンタ値が0より大きな値であればフラッシュ表示中と判断し、
上で説明した計算式を使ってアルファとスケール値を求めてSetPutStatusで切り抜き画像に指定し、
これを画像の中心を原点で表示出来るPut2を使って描画します。

なおフラッシュ画像は白鍵盤、黒鍵盤、スクラッチの順番で切り抜き番号20から登録されていますが、
ここではobj_kindの値に20を足すことで描画に使用する切り抜きIDを求めています。

■1フレーム制御

描画処理が出来たところで今度はフラッシュカウンタ自体の処理を行います。
これをしないといつまでたってもカウンタの値は変わらないため、結果的にアニメーションしません。

まずカウンタは前に説明した通り0より上ならば45→0になるように徐々に値を引きます。
これをプログラムにすると以下のようになります。

        // フラッシュ部
        for( j=0;j<6;j++ ) {
            for( i=0;i<3;i++ ) {
                if( iFlashCount[j][i]>0 )
                    iFlashCount[j][i] -= 2;
            }
        }


1フレームに1回この関数を呼び出すことでフラッシュアニメーションが完成ですが、
実際にはグラフィックボードによっては必ず60fpsで動作するとは限らないため、
ここでは確実に1フレームに1回、または処理落ちなどで60fps出せなかった場合を考慮して間引かれるように、
CTimerクラスから進んだフレーム数を取得して進んだ分だけ処理を行わせます。

CTimerクラスのRunの戻り値は前回呼び出し時から進んだフレーム数が返るので、
その回数分だけこのフラッシュ処理を行えば、結果的に必ず時間に合わせたフラッシュ状態が求められます。

    // 60FPSでのデータ操作
    int lp = tm.Run();
    for( k=0;k<lp;k++ ) {
        // フラッシュ部
        for( j=0;j<6;j++ ) {
            for( i=0;i<3;i++ ) {
                if( iFlashCount[j][i]>0 )
                    iFlashCount[j][i] -= 2;
            }
        }
    }



さて、ここではカウンタを-2ずつしています。
これをこのソフトの現時点での最良の速度としていますが、-3や-4などにするともっと早く光る効果が終了します。
このプログラムをよく見ると45から毎回-2をしていくと0を超えて-1になってしまいます。
しかしここでは0より上なら描画するようにしていたので、マイナスになってしまっても問題はありません。

このように数値をあとで変更出来るようになっていれば、のちのバランス調整もやりやすくなります。


ちなみに前にも書きましたが、この計算も描画処理側でやればいいと思うかもしれませんが、
ここではオブジェクト指向を取り入れているため描画と計算は別に処理します。
※これはタイマー制御の関係もあるので描画側に組み込んでしまうと面倒なことになります
 (描画もタイマーを使って常に60fpsで表示させるといった方法もありますが、
  VSYNCなどの影響でカクツキが相当目立つ恐れがあります)


■COMに組み込む

ここまで出来ればもう自動的にアニメーションされるようになっていますが、
まだこのアニメーションを実行させるトリガーを組み込んでいないため、
このままではやっぱりまだ何も表示されません。

そこで前回説明したCOMプレイにこのトリガーを組み込んでみることにします。

COMプレイに組み込むにはどうしたらよいかですが、
これは前回オブジェが判定バーに来たときに音を出すプログラムを説明しましたが、
ここで同時にエフェクトをスタートさせるだけです。

以下はCOMルーチンにフラッシュ処理を組み込んだプログラムとなります。

        /////////////////////////////////////////////////////////////////////////////////////
        // コンピュータプレイ
        /////////////////////////////////////////////////////////////////////////////////////
        for( j=0;j<6;j++ ) {
            for( i=iStartNum[index[j]+0x11+0x20];i<bms.GetObjeNum(0x11+index[j]);i++ ) {
                LPBMSDATA b = bms.GetObje( 0x11+index[j],i );
                if( now_count<b->lTime )
                    break;
                if( b->bFlag ) {
                    if( now_count>=b->lTime ) {
                        b->bFlag = FALSE;
                        ds.Reset( b->lData );
                        ds.Play( b->lData );
                        iStartNum[index[j]+0x11+0x20] = i + 1;
                        // フラッシュ開始
                        iFlashCount[index[j]][ iFlashIndex[index[j]] ] = 45;
                        // 次のインデックスへ
                        iFlashIndex[index[j]]++;
                        if( iFlashIndex[index[j]]>2 )
                            iFlashIndex[index[j]] = 0;
                    }
                }
            }
        }


まず各チャンネルごとに用意した3個分のカウンタ変数のうち、
現在選択されているカウンタ変数に対して45をセットします。

そして次に使用される配列番号も一緒に求めておきます。
この時まずその変数に1を足し、これが3個目を超える場合は先頭に戻すと言ったループを行います。
これで3個分の配列を順番に45にセットしていく処理の完成です。

ちなみにここでは3個以上かをif文で判定していますが、C言語では以下の方法で同じことが可能です。

                        // 次のインデックスへ
                        iFlashIndex[index[j]]++;
                        iFlashIndex[index[j]] %= 3;
※あまりを求めることでどのような値でも常に0~MAX-1の値に補正されます


最後にカウンタが45にセットされたということは、描画とフレーム処理にて判定が入るようになるため、
これによりアニメーションが表示されるようになるというわけです。



これでただ見ているだけでも面白いBMSプレイヤーの出来上がりです。
次は実際に人間がプレイする部分を考えます。