思考日記:時間をかけて経過を見せなければならない問題(2)

もくじへ戻る


前回,脳内がこんな感じ↓になったわけだが……

//細切れ処理の戻り値用 bit フラグ群
enum class ResultBits : unsigned int
{
  None = 0,
  Finished =  0b0001,
  SuppressSubsequents = 0b0010
};

//細切れ処理
template< class ...Args >
using VerySmallPartOfProc = std::function< Flags<ResultBits>( Args... ) >;

//長時間要する処理=細切れ処理のシーケンス
template< class ...Args >
using TimeConsumingProc = std::list< VerySmallPartOfProc<Args...> >;

//長時間要する処理を進める関数
template< class ...Args >
void UpdateTimeConsumeingProc( TimeConsumingProc<Args...> &TgtProc, Args... args )
{
    auto i=TgtProc.begin();
    while( i!=TgtProc.end() )
    {
        const auto Result = (*i)( args... );

        if( Result.Has( ResultBits::Finished ) )
        {   i = TgtProc.erase(i);   }
        else{   i++;    }

        if( Result.Has( ResultBits::SuppressSubsequents ) )
        {   return; }
    }
}

さて,それでは具体的にこんなのをどう使えば

【攻撃行動に関する表示(アニメーション?)の後にダメージ値が表示されて,その後で表示されているHPの値が減る】という流れ

みたいなのを実現できるのか?
さらっと「表示(アニメーション?)の後に…」とか書いてるが,実際それやれんのか? っていう.


アニメーションとは何か

シーケンスを構成するすべての細切れ処理が単純に「何かデータを更新するやつ」ならば話は簡単だったと思う.
しかし実際やりたいことにはどうしても表示が絡む.「更新」だけじゃなくて「表示」が必要な やつもある のでどうにかせねばならぬ様子.
「アニメーション」というやつの実装方法を考えねばなるまい.作業が山積みです.

アニメーションとは何ですか? っていうと,まぁざっくりとこういう話だと思う:

みたいな.ものすごく簡素に書けば

//何か描画手段
struct XPainter
{
  int x,y;
  void Draw() const {  /*せっかくだから俺は(x,y)の位置に何か描画するぜ!*/  }
};

//細切れ処理は例えばこんな感じ
class XAnim
{
public:
  //対象 AnimTgt の描画位置を (dx,dy) だけ動かすことを nTimes 回だけ実施する.
  //その間は所属シーケンス内の後続の「細切れ処理」に処理を回さない.
  XAnim(
    XPainter &AnimTgt,  //アニメーション対象(座標をいじくる対象)
    int dx,  //operator()毎に座標を変化させる量
    int dy,  //operator()毎に座標を変化させる量
    int nTimes  //座標をいじくる回数
  ) : m_AnimTgt(AnimTgt), m_dx(dx), m_dy(dy), m_nRest(nTimes) {}
  
  Flags<ResultBits> operator()
  {
    if( m_nRest <= 0 )return ResultBits::Finished;  //念のため
    
    m_AnimTgt.x += m_dx;
    m_AnimTgt.y += m_dy;
    
    Flags<ResultBits> Ret = ResultBits::SuppressSubsequents;
    --m_nRest;
    if( m_nRest<=0 )Ret |= ResultBits::Finished;
    return Ret;
  }
  
private:
  XPainter &m_AnimTgt;
  int m_dx;
  int m_dy;
  int m_nRest;
};

的な何かだ.

解決すべき課題とは何か

上記例だと ctor で描画手段への参照を渡しているから「アニメーションさせるべき描画手段オブジェクトってのは既存」っていう趣きだけど,やりたいことを考えてみると,全てがそういう状況というわけではないハズだ.
例えば「ダメージ値の表示」みたいなやつだと 描画手段は表示開始タイミングで作って→表示完了タイミングで捨てたいハズ.そういうやつはどうすんの?

表示物がある日突然現れて,そして消えて行かねばならない.
前回までの脳内コードでそれは可能なのかい?

そしたら描画手段はどこにある?

えっと,つまるところ「描画手段は細切れ処理と寿命が共通」って感じなんですけど? って話だから……

みたいな?
でもそしたらその描画処理ってのはどうやって呼ぶのだろうか?
細切れ処理に「更新」を担う operator() とは別に「描画」を担うメソッドを設けたとして,誰がそれを適切に呼び出せるというのか?
描画メソッドを有する細切れ処理が

//長時間要する処理=細切れ処理のシーケンス
template< class ...Args >
using TimeConsumingProc = std::list< VerySmallPartOfProc<Args...> >;

とかいう list の一要素として管理されてしまっている時点で,その描画メソッドを呼ぶ方法が無いではないか.
解決すべき課題が増えたぞ.困った.一体どうすればいいのか.全てが闇の中だ.

細切れ処理とは「更新と描画ができるもの」だとする?

「細切れ処理」を std::function<どうのこうの> だとするんじゃなくて,何か「 Update()Draw() の両方を持つ型」なのだとしてしまえばどうですか?
そしたら「更新」も「描画」も普通に呼べるし.良かった.はい完了.

いやいや,良くねぇよな.
何かデータを更新したいだけの処理とかがあるんだよ.そいつらは描画とかしねぇんだよ.そりゃ強引に「空っぽの Draw() を持つ型」とか言って実装できるかもしれんが…… 嫌な形だ

なんだろう,すごく間違った方向に行ってる気がしてならない.
ほんとにそんなことをせねばならないのか? もう無理か? 詰んだのか?

初心に立ち返る

いやまてそこで詰むな.あきらめるな.がんばれ.初心に立ち返れ.
(初心者が「初心に立ち返れ」とか言い出す謎の状況.混乱の極みだな)

「アニメーション」的な処理が実際に何を新たに描画し始めるのかは不明であるが,とにかくそれらを全部描画すりゃいいんだろう?
最も素直に考えるなら,こうだろーが:

//例えば IPainter という型で「描画手段」を表現するとして,こんなのがあれば……
std::vector< IPainter* > Painters;

//描画時に Painters に所属するやつらの描画を行えばいい
void XXX::Draw()
{
  /*各種既存描画処理(略)*/
  
  //追加の描画処理(唐突に湧いてくるアニメーション的なやつ用)
  for( const auto *p : Painters )
  {  p->Paint();  }
}

DrawPaint とで表記の揺れが半端ない感は置いとくとして)
で,まぁそしたら「 Painters の要素が指すオブジェクトってのは実際いつどこからどうやって湧いてきて,いついなくなるの? どこでどうやって必要な期間だけ保持してるの?」っていうのが問題なわけだが, そこについては細切れ処理が描画手段の所在を知ってるって話なら,この Painters の更新作業(要素の追加や削除)は細切れ処理に任せちゃえばいいんじゃない?

//更新作業
void XXX::Update()
{
  //時間がかかる処理を進める
  //  →  ここで Painters に対する要素の 追加/削除 が起きるってことで,ひとつヨロシク
  UpdateTimeConsumeingProc( m_TimeConsumingProc );
}

はい.じゃあその方向でさっきの細切れ処理の例を書き換えるとしたら……

//追加の描画物を入れるやつ
using AdditionalPaints = std::vector< IPainter* >;

//細切れ処理
class XAnim
{
public:
  XAnim(
    AdditionalPaints &Paints  //★描画手段の 登録/登録解除 先.要はこれを渡しとけばいいって話だ.
    int dx,  //operator()毎に表示位置を変化させる量
    int dy,  //operator()毎に表示位置を変化させる量
    int nTimes  //アニメーション表示期間
  ) : m_Paints(Paints), m_dx(dx), m_dy(dy), m_nRest(nTimes) {}
  
  Flags<ResultBits> operator()
  {
    if( m_nRest <= 0 )return ResultBits::Finished;  //念のため
    
    //★初回に「以降,これを表示に用いて欲しいんです!」って登録する
    if( m_Is1stTime )
    {
      m_Paints.push_back( &m_XPainter );
      m_Is1stTime = false;
    }
    
    //(初回からこの更新処理をやるべきなのか?みたいな話も別途ありそうだが)
    m_XPainter.x += m_dx;
    m_XPainter.y += m_dy;
    
    Flags<ResultBits> Ret = ResultBits::SuppressSubsequents;
    --m_nRest;
    if( m_nRest<=0 )
    {
      Ret |= ResultBits::Finished;
      
      //★処理完了時に登録解除する
      if( auto i = std::find( m_Paints.begin(), m_Paints.end(), &m_XPainter );    i != m_Paints.end() )
      {  m_Paints.erase( i );  }
      else
      {  throw ???  }  //ここの find() が end() を返してくる状況ってのは想定外なので例外投げとくべきかな
    }
    return Ret;
  }

private:
  AdditionalPaints &m_Paints;
  XPainter m_XPainter;  //描画手段を保有.何か適切に初期化する必要があるだろうけどそこは(略)
  int m_dx;
  int m_dy;
  int m_nRest;
  bool m_Is1stTime = true;  //初回フラグ(なんかダサい実装だが…)
};

みたいな感じかな.

登録したものを必ず登録解除することは細切れ処理の責任だ.
AdditionalPaints は実際には vector 丸出しとかじゃなくて適当にコンテナをラップしたような型にでもして,そこら辺の話をその型の注釈で書いとけばよかろう.

じゃあ,まずはこの形で試していく感じで.(それで困ったらまた考えよう)


その他のどうでもいい話

処理タイミングの決め方の話

SuppressSubsequents という話は,細切れ処理の開始タイミングが他者(他の細切れ処理)によって決められるっていう話だが,自分で決めちゃダメなのか?
「何らかの基準時刻からN秒後」だとか「N回目の呼び出し」とかで自身で制御しちゃいかんのか?
あるいはタイマ処理的にどこぞに「N秒後から開始ってことでよろしく」みたいに依頼するだとか……

……そういうのでも良い気がするけども,結局その秒数とか回数ってのは「自身よりも前の処理群にかかる時間の総和」なわけで,それをどうにかして求められる必要がある.
それは結局,他者から「私はN秒間は後続をブロックしますよ」みたいな情報を引き出す必要があるわけだから話はたいして変わらない気がするし,シーケンスを用意する際にいちいち「足し算しては設定」みたいなことをやるのがだるそうな予感.

描画処理の名前

描画メソッド名が Paint() ならばそれを持つ型名は Painter で良い気がするが,メソッド名が Draw() だとどうなるの?
Drawer でググると「引き出し」とか言われるんですけど? 「描画者」的な意味は持てるの? それとも「引き出し」オンリーな世界なの?

仕方ないからそこは XXXer ではなくて XXXable にしろとかいう話なの? 何なの?