初心者(俺)の感覚で言うと,ゲームの実装で何が一番厄介かというと
「めちゃくちゃ遅く処理が進む必要がある」
ってところだと思うんだよね.
普通(?)のプログラムならば「可能な限り早く処理が終わる」ことが求められると思うんだけど,ゲームを作る場合にはある意味逆に「どうやって遅らせるか」というところで苦労しなきゃならない.
ちょっと何言ってるかわからないだろうから,例を挙げるとすると……
マス目単位で移動するRPGの移動処理を考えてみる.
「右キーを押されたら右隣のマスに移動するぜ!」という処理はどう実装するのか?
めちゃくちゃ素直に考えればこんな↓程度の話でしかないから,悩む要素なんてどこにも無いハズだけど……
//更新処理
void XXX::Update()
{
//PositionX はマス目単位のX座標値なのだとして……
if( 右キーが押されていたら )
{ ++PositionX; } //これで右隣のマスに移動完了だ
}
残念ながらこの実装では全くダメ(不十分)なんだな.
この処理は本当に「一瞬」なレベルの処理時間しか要しないと思うけど,これで表示処理の側が
//表示処理
void XXX::Paint() const { /* PositionX の位置にキャラクタを描画する */ }
みたいなことになってるとしたら「キーを押したらキャラクタが瞬間的に隣接マスにワープする」という動作になるよね.
でも,実際のゲームってそういう動きをしてない(まぁものすごく太古のゲームとかであれば,そういう動きをする物もあるかもしれないけども).
「隣のマスに移動」するのに(0.2秒か0.3秒かわからんけども)相応の時間をかけて「なめらかに」動くよね.
つまりはこういうことだ.
また,時間を要する処理にすることで,本来存在しなかったハズの以下のような付随的な課題もごちゃごちゃと生じてくるじゃないか.
Update()
の中に「長時間かけて物事を進めるループ処理」とかを書くわけにはいかない.毎回メインループに処理を戻すかたちで実装せねばならない.PositionX
を更新」っていう実装ができない(!?)って話ならば,そしたらデータ更新はいつどうやってやるというのか?上記の例に限らず,とにかく万事この調子だ.
今回作るゲームの戦闘はごくふつーのターン性のやつなんだけど「攻撃を受けたキャラクタのHPが減る」みたいな話でも「HPが減る」のはいつなのか?
【攻撃行動に関する表示(アニメーション?)の後にダメージ値が表示されて,その後で表示されているHPの値が減る】という流れにしたいのであれば,
少なくとも表示されているHPの値が攻撃処理が始まったタイミング(:アニメーション?の開始時点)で変わってしまうのでは早すぎるというわけだ.
「表示されているHP」と「実際のHPデータ」が同じなのか違うのか? っていう話もちょっと出てきそうだ.
表示処理の実装を
みたいな話.
前者側は例えば
//自身を描画する手段を持ってるやつ
class AAA
{
m_Data; //何かデータ
SomeData public:
void Paint() const { /* m_Data の内容に即した描画を行う */ }
};
だとか,あるいは AAA
は Paint()
を持っていなくて,何か別の描画処理をするやつ:
//AAAを描画するやつ
class AAAPainter
{
//描画する対象を指すポインタを持っていて……
const AAA *m_pDataToShow;
public:
void Paint() const { /* m_pDataToShow が指す対象の状況を描画する */ }
};
みたいなのがあるみたいな.
こういうのはデータを変更すれば表示がそれに合わせて(自動的に)変化するというところに便利さがあるように思えるが,
描画内容の更新タイミングを遅らせるとかしようとする場合には扱い難いようにも思える.
後者側はなんというかとても普通の形だな.
要するに,ある処理を「 XXX::Update()
の呼び出し毎にちまちまと進んでいく」ように実装する必要があるわけだ.
そういうことができるような仕組みについて考える.
ある「長時間を要する処理」を時間方向に細切れに切り分けて,それを
//時間方向に細切れになった処理
using VerySmallPartOfProc = std::function< SomeRetVal(SomeArgs) >; //引数や戻り値は実際に必要なものにする
とかで表現するとしよう.そしたら「長時間を要する処理」というのは
//長時間を要する処理 = 細切れ処理群のシーケンス
//(※ `list` が良いのかどうかはわからんが)
using TimeConsumingProc = std::list< VerySmallPartOfProc >;
みたいな感じになり,更新処理は
void XXX::Update( SomeArgs args )
{
//実施中の「長時間を要する処理」があって,それがまだ終わっていない場合はそれを進める
if( !m_TimeConsumingProc.empty() )
{
//m_TimeConsumingProcの先頭にある処理を実施する
if( m_TimeConsumingProc.front()( args ) == 完了したという意味合いの値 )
{ m_TimeConsumingProc.pop_front(); }
}
if( !m_TimeConsumingProc.empty() )return; //以降に書かれている他の更新処理を抑止
//他の更新処理
//...
}
とかなんとかすることで,とりあえずは「長時間を要する処理をちまちまと進めていく」ことができそう.
ただし,こんな実装だと「細切れ処理」がその内容によらず必ず
XXX::Update()
1回分の時間を要してしまうということになってしまいそうだから,もうちょっとまともな形が必要であろう.
そこらへんをどうすべきなのかという情報を戻り値 SomeRetVal
で表すとしたら,どのくらいの物が必要だろうか?
「細切れ処理が時間を要さない」というのは,すなわち「1回の
XXX::Update()
で複数の細切れ処理が実行され得る」という話だ.
そういうことをやれるように↑のコード内に書いた
完了したという意味合いの値
とかいうぼんやりとした部分を以下の2つの要素に分けてみよう.
戻り値 SomeRetVal
のところをbitフラグの組み合わせ値とかで済ませるなら,例えば
//bitフラグ群
enum class ResultBits : unsigned int
{
= 0, //下記のすべてに該当しない場合用
None = 0b0001, //完了した(シーケンスから除去すべき)
Finished = 0b0010 //後続の細切れ処理を実施してはならない
SuppressSubsequents };
みたいなのを定義しとけばよいかな.
……しかし enum class
をbitフラグ値の定義に使おうとすると「組み合わせ値」を書くのがいちいち面倒だなコレ.
unsigned int Result = (unsigned int)ResultBits::Finished | (unsigned int)ResultBits::SuppressSubsequents;
とかなんとかいちいちキャストを書く必要があるし,
そのキャストも「 C++ なんだから
static_cast<unsigned int>( ResultBits::Finished )
とか書こうぜ!」とか言い出すと ただの2つの値の bitwise or
を書いてるだけの行がすっごい横に長いものに……っていう.
2,3回書いたあたりで嫌になってきたので,ちょっとしたヘルパを用意することに.
//見苦しいキャストを目に触れないようにするだけの型
template< class Enum_t > //Enum_t は bitフラグな enum class 型
class Flags
{
public:
using Val_t = std::underlying_type_t<Enum_t>;
constexpr Flags() : m_Val(0) {}
constexpr Flags( Enum_t v ) : m_Val( static_cast<Val_t>(v) ) {}
constexpr Flags( const Flags<Enum_t> &rhs ) : m_Val( rhs.m_Val ) {}
constexpr Flags<Enum_t> &operator |=( const Flags<Enum_t> &rhs ){ m_Val |= rhs.m_Val; return *this; }
//フラグが立ってるかチェック
constexpr bool Has( Enum_t flag ) const { return ( m_Val & static_cast<Val_t>(flag) ); }
private:
Val_t m_Val;
};
//なんか必要そうな bitwise or 手段を用意しとく
template <typename Enum_t>
constexpr Flags<Enum_t> operator |( const Flags<Enum_t> &lhs, const Flags<Enum_t> &rhs )
{ return Flags<Enum_t>{ lhs } |= rhs; }
template <typename Enum_t>
constexpr Flags<Enum_t> operator |( const Flags<Enum_t> &lhs, Enum_t rhs )
{ return lhs | Flags<Enum_t>(rhs); }
template <typename Enum_t>
constexpr Flags<Enum_t> operator |( Enum_t lhs, Enum_t rhs )
{ return Flags<Enum_t>(lhs) | Flags<Enum_t>(rhs); }
うん,こんな30行程度のコード書くのに1時間くらい要したぞ……
とりあえずこれを使って細切れ処理の戻り値の型を
Flags<ResultBits>
ということにすれば……
void XXX::Update( SomeArgs args )
{
//「長時間を要する処理」を進める
auto i=m_TimeConsumingProc.begin();
while( i!=m_TimeConsumingProc.end() )
{
const auto Result = (*i)( args );
if( Result.Has( ResultBits::Finished ) )
{ i = m_TimeConsumingProc.erase(i); }
else
{ i++; }
if( Result.Has( ResultBits::SuppressSubsequents ) )
{ return; }
}
#if 0 //これが必要かどうかは場所(XXXの都合)次第かな
if( !m_TimeConsumingProc.empty() )return; //以降に書かれている他の更新処理を抑止
#endif
//他の更新処理
//...
}
みたいになる感じか.
「長時間を要する処理」を進める
の部分は関数にでもしとけばよいのかな.
SomeArgs
については……何が必要なのかわからんから template
にする?
というわけで,以上をまとめると,何かこんな感じか:
//細切れ処理の戻り値用 bit フラグ群
enum class ResultBits : unsigned int
{
= 0,
None = 0b0001,
Finished = 0b0010
SuppressSubsequents };
//細切れ処理
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; }
}
}
...
をどっち側に書くんだっけ? とかがどうにも覚えられなくてつらい.
template
を書くこと自体が初心者的に難易度高杉であり,身の丈に合ってない感みたいなのが半端ないので,できれば避けたいのだが.std::function
じゃなくてやりたいことに合う型を用意して使うことになりそうな気がする.
SuppressSubsequents
的なのは「アニメーション的な表示を待つ」みたいなのを時系列処理にねじ込むような場合には更新処理と描画処理の双方でチェックしたい気がするから,処理の戻り値ではなくてその型のメソッドにした方が使いやすいとか.list
なら大丈夫かな?)