STGつくろー!
NO17 自機のロールで見る遷移図の実装方法3
15章、16章と自機のロールを遷移図での作成を続けています。この章では急旋回部分を作成します。そろそろ同じことの繰り返して飽きてきました(^-^;。逆にとれば、非常に単純にこの面倒な関係図を実装できるのがこの方法の利点ですね。
@ ホールドの持続時間
ホールド状態がある程度続くと、急旋回入力に移行します。移行条件は、ホールド状態が1秒(60単位)以上続いて切り返しが起こったときです。現在のホールド状態オブジェクトは、どの位ホールド状態が続いているか知りません。そこで、CRollHold::Roll関数が呼ばれるたびにカウント数を保持するようにして、その回数が60回を超えたら急旋回入力に移行するようにしましょう。そこの部分だけを示します。
RollHold.cpp CRollHold::RollHold(double initangle)
: CRollNode(initangle)
{
m_iCount = 0; // 持続カウント
}
double CRollHold::Roll(int flag, sp<CRollNode> &next)
{
// カウントをインクリメント
m_iCount++;
switch(flag)
{
case ROLL_NEUTRAL:
next.SetPtr( new CRollNormNeut( m_dAngle ) );
return next->Roll( flag, next );
break;
case ROLL_LEFT:
if(m_dAngle <= 0){
if(m_iCount < 60)
next.SetPtr( new CRollNormIpt( m_dAngle ) ); // 通常入力
else
next.SetPtr( new CRollQuickIpt( m_dAngle) ); // 急旋回入力
return next->Roll(flag, next);
}
break;
case ROLL_RIGHT:
if(m_dAngle >= 0){
if(m_iCount < 60)
next.SetPtr( new CRollNormIpt( m_dAngle ) ); // 通常入力
else
next.SetPtr( new CRollQuickIpt( m_dAngle) ); // 急旋回入力
return next->Roll(flag, next);
}
break;
}
return m_dAngle;
}
冗長です。でもこれで動きます。
A 急旋回入力は通常入力と一緒です
急旋回入力は、仕様では現在の角度を±12度単位で回転させます。ここは通常入力状態CRollNromIptと同じでして、実装も角度が違う以外は同じです。ホールド状態に移行するのまで同じです(遷移図と仕様がそうなっています)。違うのはニュートラルになった時で、急旋回ニュートラルに移行します。これらの実装も、実はもうこれまでの作業とまったく同様に行えばよいので、あえてここでは取り上げません。もちろん、製作はTDDにのっとり、テストコード→レッドシグナル→グリーンシグナル→リファクタリングの順に行います。
B リファクタリング
今の段階で、それほど保守性が悪いわけではありません。ちょっと気になると言えば、回転角度をマクロ定数にしている点でしょうか。しかし、ここは今は触りません。というのは、現段階で仕様の変更は無く、帰る理由が無いからです。微調整をする段階で変える必要性にかられた時に、初めて一般化することにしましょう。YAGNI(そんなのいらないって!)の原則は忘れてはいけません。
色々テストを繰り返したのですが、今回の実装でミスがありました。それは「次のオブジェクトの切り返し時のメモリ破壊」です。具体例を挙げましょう。
メモリ破壊 switch(flag)
{
case ROLL_NEUTRAL:
next.SetPtr( new CRollNormNeut( m_dAngle ) );
m_dAngle = next->Roll(flag, next);
return m_dAngle;
break;
}
}
上のようなコードは今回の実装の至る所で出てきました。ここに問題があります。まず、この関数はあるCRollNodeクラスの派生オブジェクトAから呼び出されます。nextスマートポインタにはAのポインタが格納されています。flagがROLL_NEUTRALの時、nextに新しいオブジェクトへのポインタが渡されますが、この段階でオブジェクトAは自動消去されます。さて、この時次の行のm_dAngleの位置は不定となってしまうのです。その残骸に無謀にも結果を格納しようとしているわけで、これはメモリ保護違反です(簡単なテストでは動いていましたが、複雑なテストにするとメモリ保護が出ました)。
これを回避するのは簡単で、次のように変更します。
メモリ破壊 switch(flag)
{
case ROLL_NEUTRAL:
next.SetPtr( new CRollNormNeut( m_dAngle ) );
return next->Roll(flag, next);
break;
}
}
2つの行を1つにしてしまったわけです。nextの先にあるRoll関数で何らかの角度を取得し、それを関数が返す形になっています。実は、これはセーフなんです。オブジェクト自体は無くなっていますが、関数ポインタの位置はコンパイル時固定なので生きています。よって、関数から何かを戻すことも可能です。これは大きなリファクタリングでしょうね。
こういう危ない橋にしない方法はあります。nextに直接新しいポインタを入れてしまうので危険になるわけですから、「変更前のnextをどこかに一時コピーしておけば参照カウントがゼロにならない」ので、空オブジェクト化を防ぐことはできます。でも、ちょっとだけ面倒にはなります。今回は、上の実装でも問題なく動くので、このままにしておきます。
最後に、今回の実装の結果出来上がったロール回転の動きをグラフとして示します。
プラス時が左に傾いている時で、マイナスが右に傾いている時です。赤いラインが通常のホールド角度(±20度)です。どうでしょうか、うまく動いていますよね。320Flipあたりにある急落は、ホールド角度に達した瞬間に通常ホールドに移行するため、角度が通常ホールドに戻っただけで、実はちゃんと連続しています。
後は、実際に動かして気持ち悪さがなくなるよう微調整をすることになるでしょう。
15章〜17章まで3章に渡って自機のロールを実装してきました。この3章で説明した遷移図を実装する方法は、他の場面でいくらでも流用できます。重要なのは、それぞれのクラスが、自分のご近所のクラスだけを知っている実装になっている点です。遷移図さえしっかりしていれば、大変実装しやすい仕組みになっています。上のような複雑な振る舞いを、誰にマネージメントされる事無く実現する。これが、オブジェクト指向の醍醐味ですよね。