最近のキャラクターでは、独自のゲージを表示したり、 SHOTを使った派手な演出をしたりするものが増えてきました。
が、しかし、演出の強化は処理の重さにも響いてきてしまいます。 SFXVIはマシンクロック15MHz推奨となってはいますが、 10MHzのユーザーもいることを忘れてはいけません。
そこで、今回は高速化のためのプログラミングについて解説していきます。
まず、忘れてはいけないのが最適化コンパイルです。コンパイラには大抵、 「コンパイルスピード重視」「最適化重視」の二つの動作モードが存在します。 当然、モードによって完成したプログラムの実行スピードには大きな差が発生します。
「それなら、常に最適化モードでコンパイルすればいいんじゃない?」という声もあるでしょう。 その通りです。「コンパイルスピード重視」モードはデバッグ中や、文法のチェックさえすればいい時のように、 少しでも速くコンパイルを終わらせたい時に使うのです。もっとも、マシンのスピードが十分速ければ必要ないわけですが。
CBの出力する make.bat では最適化オプションが記述されているので、 何も考えずとも最適化コンパイルが行われます。ですから、これに注意しなければならないのは、 コンパイルバッチや Makefile を自前で用意している人のみです。
最適化モードでコンパイルを行うには、コンパイラに最適化オプションをつけて実行すればいいのです。 そのオプションは -O です。大文字と小文字は区別されるので必ず大文字で記述しましょう。
このように記述することで最適化が行われます。
I:\SFXVI\DEVELOP\CHAR\TEST\PROGRAM\ >gcc -O action.c
もう一段階進んだ最適化を行うには、 gcc2 というコンパイラを利用する方法があります。 こちらについては今回は説明しません。ご了承ください。
さて、ここからはコーディングテクニックによる高速化です。
まずは、SFXVIシステムがどういった順番で処理を実行しているかを知っておくのがいいでしょう。 これは「えくCD」に詳しい解説がなされています。 ので、詳細を知りたい方はそちらを参照してください。今回はポイントとなる部分のみ取り上げます。
これを見て分かるとおり、SHOT関数は1サイクルに3回呼び出されています。 SHOT関数の最適化は、高速化に大きく影響します。
- OPT_サイクル
- VCT_ゲージ調整処理
- VCT_サブゲージ表示処理
- SHOT関数(SHOT_CMD_移動)
- SHOT関数(SHOT_CMD_表示)
- 動作関数
- コマンド解析
- SHOT当たり判定
- OPT_サイクル2
- 攻撃当たり判定
また、上記のオプション動作関数やベクタは、毎サイクル実行されるわけですから、 極力処理を軽くするよう努めるべきです。
というわけでSHOT関数です。SHOTのステータスには「有効」「無効」「消滅」 の3種類が存在しますが、実はこのステータスがどの状態であろうと毎サイクル呼出が行われます。 ですから登録した全てのモジュールが毎サイクル処理されるのです。
普通のSHOTモジュールは無効状態では何もせずにリターンするので、 たいしたタイムロスにはなりませんが、塵も積もれば・・・となります。 共用出来るものはなるべくひとつにまとめる努力をするしかないでしょう。
例えば「合計32発の飛び道具を乱射する」という技の時、 SHOTモジュールを32個用意するのは得策とはいえません。 画面上に最大何発まで同時に存在するかがわかれば必要なモジュールの数は減らせます。
また、演出としてSHOTを用いる場合ですが「16方向に光が飛び散る演出」の時、 SHOTモジュールを16個用意するのも(場合によりけりですが)避けるべきでしょう。
モジュールひとつで16個のSPを制御するのが吉です。
サブゲージの表示は、通常、システムが自動で行うので、 キャラクター側で気を付けるべき点は存在しません。 しかし、自前で表示させる時にはそれなりに注意が必要です。
まず、鉄則として「変化が無い時は描画を行わない」というものがあります。
というわけで、最近流行のスタンゲージを表示させる処理を例として挙げておきます。
特に難しい処理はないと思います。prm_表示値というstatic宣言をした変数に、 前のサイクルの気絶値を保存しておき、現サイクルでの値と比較。違っていれば表示を行います。
//------------------------------------------------------------ void Vct_サブゲージ表示処理( int p1, int p2, short init ) //------------------------------------------------------------ { static short prm_表示値; if( init ) { ゲージ表示_非標準( p1, 2, 27, 0, 40 ); prm_表示値 = 0; } else { if( 状態取得_気絶量( p1 ) != prm_表示値 ) { prm_表示値 = 状態取得_気絶量( p1 ); ゲージ表示_非標準( p1, 2, 27, prm_表示値, 40 ); } } } //-------------------------------------------------------------
余談ですが、状態取得_気絶量()という関数は標準APIにはありませんので注意して ください。これはWORK.Hマクロに存在する関数(正しくはマクロ)です。
コマンド解析も毎サイクル実行されるので馬鹿に出来なかったりします。 普通、必殺技コマンドには、「レバーのみで入力する」「レバー+ボタン」の2種類があります。 このうち「レバー+ボタン」のコマンドは現サイクルでボタンが押され (あるいは離され)なければ無視して構いません。
例を挙げてみましょう。以下のコマンドを持ったキャラを想定します。
- 波動拳(236P)
- 昇龍拳(623P)
- 竜巻旋風脚(214K)
- クイックフォワード(66)
- クイックバック(44)
このように記述する事で、ボタンが押されなかったサイクルでは、 クイックフォワードとクイックバックのコマンド解析しか行わなくなります。
//----------------------------------------------------------- void Cmd_地上必殺技コマンド判定( int p1, int p2 ) //----------------------------------------------------------- { if( Trg取得_直接状態( p1 ) != TRIG_N ) { Cmd_昇龍拳( p1 ); Cmd_波動拳( p1 ); Cmd_竜巻旋風脚( p1 ); } Cmd_クイックフォワード( p1 ); Cmd_クイックバック( p1 ); } //----------------------------------------------------------
更に無駄を省くなら以下のような方法もあります。
共通な部分を一ヶ所で行うようにすれば、 その分無駄な処理は少なくなりますが関数の独立性は失われていきます。 プログラムの見易さをとるか、処理速度をとるかは、難しい問題です。
//----------------------------------------------------------- void Cmd_地上必殺技コマンド判定( int p1, int p2 ) //----------------------------------------------------------- { if( Trg取得_直接状態( p1 ) != TRIG_N && Cmd条件_地上必殺技( p1, ON, OFF ) ) { Cmd_昇龍拳( p1 ); Cmd_波動拳( p1 ); Cmd_竜巻旋風脚( p1 ); } if( Cmd条件_地上必殺技( p1, OFF, OFF ) ) { Cmd_クイックフォワード( p1 ); Cmd_クイックバック( p1 ); } } //----------------------------------------------------------
OPT_サイクル、OPT_サイクル2の処理内容は高速化に大きく関わってくるのですが、 これといった対処法はありません。専用のベクタなどで出来る処理は、 なるべくそちらに記述するといったところでしょうか。
ここから先は、ワンポイントテクニックです。
☆乗除算を極力使わない
これらは非常に重い処理なので極力使わないようにしましょう。 しかし、2の累乗(2,4,8,16...)で乗除算を行うのであれば話は別です。 これらと加減算を利用すると3倍や5倍などの計算も高速に行えます。
例:a の5倍の値を計算する
b = a * 4 + a;
☆実数演算を極力使わない
実数、つまり小数点を含んだ値等を用いた計算は、加減算であっても重い処理となります。 どうしても必要な場合は下駄を履かせて整数に直して計算しましょう。
例:等加速度運動(加速度は90/256)
for( i = 0 ; i < 16 ; i ++ ) { dx += 90; 座標設定_移動( p1, dx/256, 0, DRCT_前 ); }
☆条件文は0と比較する
ほとんど気休め程度の影響しかないと思いますが
この二つを比較した場合2の方が若干速くなります。同様に
- if( a == 1 )
- if( a > 0 )
この場合も2の方が速くなります。
- if( a == 1 )
- if( a != 0 )
デフォルトのソースでよく
if( ?????? != OFF ) {
という記述がありますが、OFFには 0 が定義されているので、 実はここで解説していることと同じ記述をしているというわけです。
だからといって
if( a == 1 )→if( a - 1 == 0 )
という記述をすると a - 1 という引き算が必要になるので逆効果です。
☆再利用を積極的に
Trg取得_直接状態()の戻り値のように同一サイクル内で不変な値は、 変数に結果を保存して再利用するのがよいでしょう。賢いコンパイラではある程度無視出来るのですが、 自分でやる方が安心出来ます。
例:この場合、状態取得_動作()の戻り値はどれも同じです。 これを以下のように書き換えます。
if( 状態取得_動作(p2) == ACT_ふっとぶ || 状態取得_動作(p2) == ACT_空中連続衝撃 || 状態取得_動作(p2) == ACT_転ぶ ) {
short act = 状態取得_動作(p2); if( act == ACT_ふっとぶ || act == ACT_空中連続衝撃 || act == ACT_転ぶ ) {
☆マクロを使う
処理内容が同一であれば、関数よりマクロを使った方が高速です。 ただし、使いすぎるとプログラムの肥大化を招くのでその点には注意が必要です。 SFXVIではKYOSKE氏作の「WORK.H」マクロが有名です。
おまけとしてマクロを二つ掲載しておきます。
//絶対値の取得 #define ABS(a) ((a)>0?(a):-(a)) //符号の取得 #define SGN(a) ((a)>0?1:((a)<0?-1:0))
まわりを見まわしてみると、超高速マシンがごろごろしているため、 こういった高速化のためのテクニックというのは無用のものとなってきているかもしれません。 プログラミングの敷居を低くするという点においては喜ばしい事ですが、 なんとなく許せないと思いませんか?
しかし、まあ、68においてはこういったテクニックは重要ですから 是非とも役立てていただきたいと思います。