表示ってどうやって作ってんの?

アバター
usao
記事: 1889
登録日時: 11年前

表示ってどうやって作ってんの?

投稿記事 by usao » 1年前

神は言われた.「光Array」.
すると,
まばゆく輝く配列「index out of range」

…とか何とか言う啓示を受けた気がしないでもないので,プログラミングの話をしよう.そうしよう.
例によって,まともにゲームプログラミングをしたことのない者の妄言というかそんな.

---

RPG でも SLG でも何でもいいけど,「AがBを攻撃する」という処理があるとしよう.
この処理だけを考えれば,

何か攻撃手段に応じたダメージ計算をして,必要なら追加でBが状態異常になるとかいう処理をしたり,BのHPが0になったらどうのこうの…

っていうのをやることになるのだろうけど,この処理って,単に処理(計算)するだけなら一瞬で終わるよね.
で,ふつーの(仕事とかの?)処理ってのは,まぁ一瞬で終わるならそれに越したことは無いわけで,「OK,できた」ってなるんだけども,これがゲームだと話が違ってくる:

何か攻撃のエフェクトを表示して,次にダメージの表示があって,Bが消える演出が…

とか何とかいうことになって,これは「一瞬」で済ませるわけにはいかない.面倒な限りだ.
つまり,処理内容的には一瞬で済むような話なんだけども,その様子を時間をかけて,言わば「スロー再生」して見せなければならなくて,且つ「それが終わるまでは処理を次に進めない」っていう制御が必要になるわけだ.
こういうのって,どうやってるんだろう?(何かしらのかっこいい実装方法的な意味で) っていう話.
最後に編集したユーザー usao on 2022年8月29日(月) 18:29 [ 編集 1 回目 ]

アバター
usao
記事: 1889
登録日時: 11年前

Re: 表示ってどうやって作ってんの?

投稿記事 by usao » 1年前

STGみたいなのが入門とされていることが多いのは,こういう話をとりあえず考えなくて良いから的な理由なのかな? とか思ったり.

さて,とりあえず素人が考えると,以下の2つの形態が考えられるけど…
  • ちょっとずつ処理(計算)を進める.表示と平行して.
  • 処理(計算)は即終わってるんだけど,表示側が終わるまで待つ.
前者側は,表示側の都合で処理側のタイミングを支配するっていうのが,何だろう,因果関係が逆とでもいうか,個人的に割と気持ち悪い感.
それに,元々その場で全部書けた程度の「一瞬」な処理を,ちまちま進むように分割する(?)っていう実装も何だか難しそうな予感だぞ.

なので,どちらかと言えば,「処理(計算)の部分は細切れにせずにそのまま自然体で書ける」という意味で後者側の方がやさしい予感がするね.

「処理(計算)」時に見せるべき表示物(の列みたいなの)を生成して表示器に与え,表示器は与えられたのをひたすら表示して,「処理(計算)」の側はその完了までは何もしない的な話にすれば良いのかな.
攻撃処理の例で言えば,
攻撃のエフェクト→ダメージ表示のエフェクト→Bの消滅のエフェクト みたいな3つの表示物のシーケンスを表示器に放り込む,という感じ?

どうなん?
最後に編集したユーザー usao on 2022年8月29日(月) 16:50 [ 編集 1 回目 ]

アバター
usao
記事: 1889
登録日時: 11年前

Re: 表示ってどうやって作ってんの?

投稿記事 by usao » 1年前

何はともあれ,「表示物」が要るから,とりあえずこんなのを用意するとしよう.
(最近,カタコトのイングリッシュで注釈を書くのが謎のマイブーム)

CODE:

//Object displaying something.
//Each concrete-type object may handle some part of the entire view, therefore the view may consist of a group of IDispItem.
class IDispItem
{
public:
	virtual ~IDispItem(){}
public:
	//Returns true if this object has been finished its work.
	//If this method returns true, it means that callers of this object's paint() method should stop making further calls.
	//
	//The value returned by this method should only change due to the execution of Update() :
	//if this method returns X at some time, this method must returns X until the next update() call.
	virtual bool Finished() const = 0;

	//Returns whether need to redraw or not : In other words, returning true indicates that Paint() should be called.
	//So this method must return true if some data to show are changed.
	virtual bool Update() = 0;

	//If this method is called under the state that finished() returning true, the behavior of this method is undefined.
	virtual void Paint( /*XXX*/ ) const = 0;
};
よくある,Update() と Paint() を持ってる形だね.
例えば,何らかのアニメーションを Update() で進めていき,最後まで行ったなら Finished() がtrueを返す感じ.
この仕様だと,アニメーションの「最後の絵の次」にいっちゃったタイミングで Finished() がtrueを返すようにする感じかな.

アバター
usao
記事: 1889
登録日時: 11年前

Re: 表示ってどうやって作ってんの?

投稿記事 by usao » 1年前

さて,そしたら,これのシーケンスを扱ってくれる奴(表示器)が必要だけど,ここには本件の課題である「待つ」を実装する必要があるから,まずはそれをどうすればいいのか?ってのを考える.



うーん,とりあえず Observer Pattern で,ってことにしようか.
…っていうわけで,こんなのを用意する.
(世間での Observer Pattern の説明に出てくる単語がどうにも個人的にしっくりこないので,クラス名はこの場でてきとーに付けた)

CODE:

//Observer pattern implementation.
class Notifier
{
public:
	class INotified
	{
	public:
		virtual ~INotified(){}
	public:
		//Returns whether need to unregister or not.
		//if true, this object will be unregistered from the Notifier.
		virtual bool OnNotify() = 0;
	};

public:
	Notifier(){}
	Notifier( const Notifier & ) = delete;
	Notifier &operator=( const Notifier & ) = delete;

	//Note : invalid usage such as null or duplication don't be checked.
	void Register( std::weak_ptr<INotified> pObserver ){	m_Observers.push_back(pObserver);	}

	void Clear(){	m_Observers.clear();	}
	bool Empty() const {	return m_Observers.empty();	}

	void Notify()
	{
		for( auto iter=m_Observers.begin(); iter!=m_Observers.end(); /*NOP*/ )
		{
			auto sp = iter->lock();
			if( !sp  ||  sp->OnNotify() )
			{	iter = m_Observers.erase( iter );	}
			else
			{	++iter;	}
		}
	}
private:
	std::list< std::weak_ptr<INotified> > m_Observers;
};
通知を受ける者(INotified)が内部クラス扱いってのはどうなん? っていうのが迷うところだが,まぁ本筋ではないからOK.
こんなのがあれば,きっと待てるっしょ.

アバター
usao
記事: 1889
登録日時: 11年前

Re: 表示ってどうやって作ってんの?

投稿記事 by usao » 1年前

そしたらもう,表示器はこんな感じ.
表示物のリスト(シーケンス)を抱えていて,全部やり切ったら待ってる奴に通知する,と.

表示器自体も「表示物」っていう形にして Composite Pattern な雰囲気を演出.
複数の表示器を束(配列か何か)にすることで,表示順序を(ペインタアルゴリズム的に)扱ったりとかそういう感じになるのかも感,みたいな.

CODE:

//The "caller" for a IDispItem sequence.
//Has finish notification mechanism.
class DisplayMechanism : public IDispItem
{
public:
	DisplayMechanism(){}
	DisplayMechanism( const DisplayMechanism & ) = delete;
	DisplayMechanism &operator=( const DisplayMechanism & ) = delete;

public:
	void AddDispItem( std::shared_ptr<IDispItem> pItem )
	{	m_Items.push_back( pItem );	}

	//Register observer which waits the state that this object's Finish() returns true.
	void RegisterWaiter( std::weak_ptr< Notifier::INotified > pWaiter )
	{	m_FinishNotifier.Register(pWaiter);	}

	//---
	// IDispItem implementation

	virtual bool Finished() const override {	return m_Items.empty();	}

	virtual bool Update() override
	{
		bool NeedToRedraw = false;
		for( auto iter=m_Items.begin();	iter!=m_Items.end();	/*NOP*/ )
		{
			NeedToRedraw |= (*iter)->Update();
			if( (*iter)->Finished() )
			{	iter = m_Items.erase( iter );	}
			else
			{	++iter;	}
		}

		if( Finished() ){	m_FinishNotifier.Notify();	}

		return NeedToRedraw;
	}

	virtual void Paint( /*XXX*/ ) const override
	{
		for( const auto &pItem : m_Items )
		{	pItem->Paint( /*XXX*/ );	}
	}

private:
	std::list< std::shared_ptr<IDispItem> > m_Items;
	Notifier m_FinishNotifier;
};
こいつの Update() やら Paint() がメインループ的な所から呼ばれるのだとして,
処理(計算)側から必要に応じて,AddDispItem() を用いて表示物のシーケンスを組み立てればいいんじゃね? っていう.
最後に編集したユーザー usao on 2022年8月29日(月) 18:35 [ 編集 1 回目 ]

アバター
usao
記事: 1889
登録日時: 11年前

Re: 表示ってどうやって作ってんの?

投稿記事 by usao » 1年前

これだと,待てるのは「表示器」の単位であって,「IDispItem」なる表示物ではない,という形だけども,まぁそこは問題ないんじゃないかな.

大抵は複数個の表示物で作られたシーケンスを待つんじゃないか,すなわち各表示物が受け持つのはかなり小さい単位になるかな,とか想像するので.
本当に表示物1個を待つ必要があるならばその1個だけ持つ表示器を作ればいけるっしょ.
(実際に使い方の頻度が逆転するようなら考える必要があるね)

---

待つ側は,「俺は今待ってるぜー」っていうフラグでも1個用意して,

CODE:

void Update()
{
  if( Waiting )return;  //待ってたら即return
  
  //処理
}
みたいなことにでもすれば良いのかな.
(もちろん通知が来たらフラグをおろす.)

アバター
usao
記事: 1889
登録日時: 11年前

Re: 表示ってどうやって作ってんの?

投稿記事 by usao » 1年前

こういう機構みたいな(?)のを考えているときに重要なのは,
「その機構を使う範囲はプログラム全体じゃなくてもよいのだ」という意識をちゃんと持つことかもしれないね.

こういう話を考えていると,どうしても「うまいことプログラム全体で統一した動作機構みたいなのができねぇかなぁ?」とか欲が出ちゃうんだけど(私だけかな?),そういうことを考え出すと「これだけだとあれができないし,こういうのもできないと…」っていう無限の泥沼に突入しかねない.

もちろん,全体を通して使える物が本当に構築できれば素敵だろうけども,自分自身の利便性のために考えたはずの機構が実装する上での制約に成り下がらないようにするのは,私みたいな素人には難しすぎるなぁ.

だから,「自分が今現在考えている範囲は全体じゃないぞ」,「本当にメインループ直下にこの表示器が鎮座していなければならないというわけではなくて,もっと細かい,「ある特定のシーン」の実装だとか,そういったローカルな範囲で通用すれば良い話を考えているんだ」という意識を保つことがきっと大切なんじゃなかろうか,と.
最初から立派に纏まった実装を目指すんじゃなくて,「何だかんだ実際に実装してみたら,あっちとこっちが結果として何かいい感じに纏められたわ」みたいなことになればうれしいな的な,そんな気構えで.

アバター
usao
記事: 1889
登録日時: 11年前

Re: 表示ってどうやって作ってんの?

投稿記事 by usao » 1年前

> IDispItem

我ながらダサい名称だよなぁコレ.
「表示物」とか「表示項目」だとかいう意味合いなんだけども…
"Display" を半端に "Disp" と略しているのもかっこわるい感.(しかも他のクラスの側では何故か略していないぞ!)


> INotified

っていうのも,通知する側の名称 "Notifier" との対比というかそんな感じなんだけど,引数名は "Observer" ってなってるとかもうね.

あと,この実装だと通知をもらうためには weak_ptr 型で登録しなきゃならないんだけども,通知が欲しい型自体を INotified を継承した実装にしてしまうと,自身が shared_ptr で管理されてること前提になってしまい,そしたら「 shared_from_this がどうのこうの」とか言い出す嫌な方向に直行することになってしまいそうだから,自身ではなくて INotified を継承する内部型でも用意してそいつに仲介させるとかそういう形にしないといかんのかなぁ.

アバター
usao
記事: 1889
登録日時: 11年前

Re: 表示ってどうやって作ってんの?

投稿記事 by usao » 1年前

…で,そんな感じで絵に描いた餅を考えてみたわけだが,「ちょっと試しに使ってみるか」ってなると,やはりというかなんというか,すこぶる使いにくいわけで.

まず,予想通り(?)に,std::weak_ptr<INotified> とかいう型がつらいんだわ.
INotified と weak_ptr なる型の縛りが2重につらい.
「std::function とかでよくね?」ということで変更してみる:

CODE:

class Notifier
{
public:
	//Returns whether need to unregister or not.
	//If true, this object will be unregistered from the Notifier.
	using Observer = std::function< bool(void) >;

public:
	Notifier(){}
	Notifier( const Notifier & ) = delete;
	Notifier &operator=( const Notifier & ) = delete;

	//Note : invalid usage such as null or duplication don't be checked.
	void Register( const Observer &rObserver ){	m_Observers.push_back(rObserver);	}

	void Clear(){	m_Observers.clear();	}
	bool Empty() const {	return m_Observers.empty();	}

	void Notify()
	{
		for( auto iter=m_Observers.begin(); iter!=m_Observers.end(); /*NOP*/ )
		{
			if( (*iter)() ){	iter = m_Observers.erase( iter );	}
			else {	++iter;	}
		}
	}
private:
	std::list< Observer > m_Observers;
};
"INotified" という名前にしてたところが何気に "Observer" になっちゃってるけど気にしない方向で.
最後に編集したユーザー usao on 2022年9月07日(水) 16:51 [ 編集 1 回目 ]

アバター
usao
記事: 1889
登録日時: 11年前

Re: 表示ってどうやって作ってんの?

投稿記事 by usao » 1年前

あと,やっぱり

> 表示物のリスト(シーケンス)を抱えていて,全部やり切ったら待ってる奴に通知する

っていう2つの要素がいきなり合体しているのは不便.
シーケンスはシーケンスであり,通知は通知.分けたい.

本当に必要なのは「終わったら通知を出してくれる表示物」だと思うので,それを素直にやるならばこうじゃね?

CODE:

//IDispItem with finish notification.
class IWaitableDispItem : public IDispItem
{
public:
	//Register observer which waits the state that this Object's Finish() returns true.
	virtual void RegisterWaiter( const Notifier::Observer &Waiter ) = 0;
};
「表示物であって,通知も出せる」っていう.うん.
継承階層になるのがなんとなく気に入らない感がなくもないが,まぁとりあえずは素直な雰囲気だと思う.

そしたら,シーケンスは単にシーケンスでしかないので,通知がどうのいう要素を取っ払えば,単純にこんなのになるだけ:

CODE:

//Sequence of IDispItem.
class DispItemSeq : public IDispItem
{
public:
	DispItemSeq(){}
	DispItemSeq( const DispItemSeq & ) = delete;
	DispItemSeq &operator=( const DispItemSeq & ) = delete;
	virtual ~DispItemSeq(){}

public:
	//Add to the tail of sequence.
	//When the added item's Finished() returns true, the item would be automatically removed from this sequence.
	void AddDispItem( std::shared_ptr<IDispItem> spItem ){	m_Items.push_back( spItem );	}

public:	//IDispItem implementation
	virtual bool Finished() const override {	return m_Items.empty();	}

	virtual bool Update() override
	{
		bool NeedToRedraw = false;
		for( auto iter=m_Items.begin();	iter!=m_Items.end();	/*NOP*/ )
		{
			NeedToRedraw |= (*iter)->Update();
			if( (*iter)->Finished() ){	iter = m_Items.erase( iter );	}
			else {	++iter;	}
		}
		return NeedToRedraw;
	}

	virtual void Paint( HDC hdc, int W, int H ) const override
	{	for( const auto &pItem : m_Items ){	pItem->Paint( hdc,W,H );	}	}

protected:
	std::list< std::shared_ptr<IDispItem> > m_Items;
};
単にシーケンスへの登録と解除に関するルールを定めているだけの代物.

アバター
usao
記事: 1889
登録日時: 11年前

Re: 表示ってどうやって作ってんの?

投稿記事 by usao » 1年前

で,IWaitableDispItem の具体実装を例えばこんな感じで用意すれば良いのではあるまいか.

CODE:

//A concrete implementation of IWaitableDispItem
class WaitableDispItem : public IWaitableDispItem
{
private:
	std::unique_ptr<IDispItem> m_upDispItem;
	Notifier m_FinishNotifier;
public:
	WaitableDispItem( std::unique_ptr<IDispItem> &&upDispItem ) : m_upDispItem{ std::move(upDispItem) } {}
	WaitableDispItem( std::unique_ptr<IDispItem> &&upDispItem, const Notifier::Observer &Waiter ) : WaitableDispItem{ std::move(upDispItem) } {	RegisterWaiter( Waiter );	}
public:	//IWaitableDispItem implementation
	void RegisterWaiter( const Notifier::Observer &Waiter ){	m_FinishNotifier.Register(Waiter);	}
public:	//IDispItem implementation
	virtual bool Finished() const override {	return m_upDispItem->Finished();	}
	virtual void Paint( HDC hdc, int W, int H ) const override {	m_upDispItem->Paint( hdc,W,H );	}
	virtual bool Update() override
	{
		bool Ret = m_upDispItem->Update();
		if( Finished() ){	m_FinishNotifier.Notify();	}
		return Ret;
	}
};
こういうのを「デコレータ」とか呼ぶのか,呼ぶのは間違ってるのか,そこら辺はわからんけども,何かそんな雰囲気で表示物と通知を合体させてみた.
単に両者をメンバに抱えて,表示物が Finished() で true を返して来たら通知者の Notify() してやるぜ,っていうだけだ.