ゲームの表示待ちがどうの

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

ゲームの表示待ちがどうの

投稿記事 by usao » 1年前

そんなこんなで 書き散らかした素人考えの実装 を使ってみるちょっとしたプログラムを作ってみよう,と.

ただ,題材としては,最初に例にあげた「攻撃してどうの」いうやつだと,そういう内容の物を作ること自体がかなり大変そうなので,もっとシンプルなやつがいいなぁ.

…というわけで,
2D見下ろしRPGみたいなやつの「歩く」を題材にしてみることにする.
隣のマスに移動する処理は,データ上だと「単に(マスの位置を示す)座標値を +1 するだけ」なんだけど,表示としては結構な時間をかけてマスからマスへと移動するよね.
で,その間は別の入力は受け付けない=待ち状態なわけだ.
あれをやってみよう. うん.

でも,ただ隣に歩くだけだと,表示物としては「マスからマスへ移動中の表示をするやつ」という単一の物だけで完結しちゃいそうで,それだと「シーケンス」をテストできないから,「歩く→何かが起きる」みたいな連続的な事柄を表示したい.
なので,「マップのいくつかのマスの中には地雷が埋まっていて,そこに移動したら爆発する」ということにしよう.
そしたら「歩く→爆発」っていう連続した表示が発生するから最低限の「シーケンシャルな表示が終わるのを待つ」という形になるね.

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

Re: ゲームの表示待ちがどうの

投稿記事 by usao » 1年前

というわけで,最初の仕事は何か…? っていうと,それはもちろん,地雷を踏んだ時の爆発アニメーションを描くことになるよね(!).

早速ペイントでざっくりと描いてみたよ.
32x32 で 6 パターンアニメーション.

アニメーションとしてちゃんと繋がってるか? 爆発に見えるか? 等の調整とかはできていないけども(そういうのを簡単に見れる良いツールが欲しいなぁ),自分的にはそこそこ描けた感.
Explosion.png
たのしいおえかき
Explosion.png (1.92 KiB) 閲覧数: 370 回
「怪しい伝説」っていう,ほぼ毎回何かを盛大に爆破する番組を見てた感じだと,実際の爆発ってこんな感じではなかったけど.
最初のフレーム(一番左の丸いの)みたいなのは超スロー再生でもしないと全く見えないし,上の方に上って行ったやつは煙色で,この絵みたいに炎っぽい色(赤や黄色)ってことは無いみたい.
…でも,ゲームのグラフィックの爆発だとこんな色で描かれていることが多い気がするし,煙色で描くってのは自分には難易度高すぎなので,こんな感じに描いてみた.
最後に編集したユーザー usao on 2022年9月09日(金) 17:58 [ 編集 1 回目 ]

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

Re: ゲームの表示待ちがどうの

投稿記事 by usao » 1年前

「マップ」って何だよ? っていう……
最低限こんなのがあれば,マップに関する処理は書いていけるのかな?

CODE:

class IMapData
{
public:
	virtual ~IMapData(){}
public:
	virtual int Width() const = 0;
	virtual int Height() const = 0;
	virtual unsigned char At( int x, int y ) const = 0;
};
マップの広さ(縦横何マスあるのか)と,各マスの属性(地形)を得る手段があるだけだ.
で,コレの具体実装に関しては,ガチのゲームを作るわけじゃないのだから,超てきとーに用意する.

CODE:

enum MapChipType
{
	Wall,	//壁.侵入不可
	Free,	//何もない場所.侵入可.
	Mine	//地雷がある場所.侵入可.
};

class MapData : public IMapData
{
private:
	static constexpr int W = 20;
	static constexpr int H = 16;
	unsigned char m_Data[H][W];
public:
	MapData()
	{
		for( int y=0; y<H; ++y )
		{
			for( int x=0; x<W; ++x )
			{	m_Data[y][x] = Free;	}
		}
		m_Data[4][4] = m_Data[3][5] = m_Data[3][6] = m_Data[3][8] = m_Data[4][8] = Wall;
		m_Data[2][2] = m_Data[6][10] = Mine;
	}
public:	//IMapData Implementation
	virtual int Width() const override {	return W;	}
	virtual int Height() const override {	return H;	}
	virtual unsigned char At(int x, int y) const override
	{
		if( x<0 || x>=W || y<0 || y>=H )return Wall;
		return m_Data[y][x];
	}
public:
	unsigned char At( const Vec2i &Pos ) const {	return At( Pos[0], Pos[1] );	}
};
コンストラクタで,すっごいてきとーにマップの中身を設定.
At() メソッドは範囲外の座標については「そこは壁です」って返すようにしとく.(「範囲外」に対するいわゆる「番兵」的な役割だね)
最後に編集したユーザー usao on 2022年9月09日(金) 19:17 [ 編集 1 回目 ]

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

Re: ゲームの表示待ちがどうの

投稿記事 by usao » 1年前

で,次にやっつけるべきは,コレ系のやつに必須の要素,「スクロール」だ.

↑の具体実装だとマップは相当に狭いけども,それでも表示域(e.g. ウィンドウのクライアント領域)の中にマップ全体を常に表示できるとは限らないから,マップの一部だけを表示するような仕組みが必要.

見下ろしRPGみたいなやつだと,基本的には「キャラクターが表示域の中心に見えるように」スクロールさせるだろうけど,常にそうなるわけじゃないよね.
というのは,マップの端にいるときにも「キャラクターが表示域の中心」にくるように描画するとしたら,表示域の中にマップが描画されない部分(マップデータが無い箇所)ができてしまうから.「スクロールはマップの端が表示域の端に来た時点で止まる」みたいな形だよね.

…っていう話でのスクロール計算を(1次元について,単位が Pixel な世界で)やるヘルパを書く.

CODE:

//表示スクロール関係
//	てきとーな言葉の定義:
//		表示したい物全体のことを "Content", その一部を "Image" として見る(描画する)っていう話を考える.
//		(e.g. マップ全体のうちの一部だけを描画するよね,っていう話)

//Content の指定箇所を着目するようなスクロール量を求む.
//	「スクロール量」とは Image 上での座標0 に対応した Content 上の座標値のこと.
//典型的には Image の中心に着目点が来るようなスクロール量を返すが,
//着目点が Content の端付近である場合には,なるべく Image 内に Content の内容を多く含むように調整した値を求む.
//(e.g. 着目点として座標0を指定した場合,そこを Image の中央に捉えるのではなく,Contentの端(0)とImageの端(0)を一致させるようにする)
inline int CalcScrollAmountToFocus( int ContentSize, int FocusPosOnContent, int ImageSize )
{
	int RawScroll = FocusPosOnContent - ImageSize/2;
	if( RawScroll<0 )return 0;
	if( RawScroll + ImageSize > ContentSize )return (std::max)( 0, ContentSize-ImageSize);
	return RawScroll;
}

//Content上の座標に対応したImage内の座標
inline int ImagePos( int ContentPos, int ScrollAmount ){	return ContentPos - ScrollAmount;	}
//Image内の座標に対応したContent上の座標
inline int ContentPos( int ImagePos, int ScrollAmount ){	return ImagePos + ScrollAmount;	}
まぁ,簡素な算数だよね.
で,これを使って実装することになるだろう物とは何か?っていうと,それは【マップのマス ←→ Content ←→ Image】っていう3つの2次元座標系の間の座標変換処理のあたり.
マップの描画処理は「(3,2) のマスってのは,画像のどこに描画すればいいのか?」みたいな計算が必要だから,そういった演算を解決してくれるやつが必要っていう話.
マップ描画処理としては,↓みたいなのにそこらへんを移譲しちゃえば良いだろう.

CODE:

//Mapの描画に必要な座標変換手段
//	{スクロール,1マスをどんなpixelサイズで描画するのか}というあたりの計算を解決する.
template< int MAP_CELL_PIXEL_SIZE >	//マップの1マスの描画サイズ(縦横共通)[pixel]
class ICoordinateTrans
{
public:
	virtual ~ICoordinateTrans(){}
public:
	//ContentPix : スクロールと無関係なPixel座標(≒マップ全体を一枚の絵に描画した場合の座標)
	//ImagePix : スクロールの影響を受けたPixel座標(表示用画像の上での座標)

		//マップのマスの左上に対する ContentPix
	static Vec2i MapCellTL_to_ContentPix( const Vec2i &MapPos ){	return MapPos*MAP_CELL_PIXEL_SIZE;	}
		//マップのマスの中心に対する ContentPix
	static Vec2i MapCellCenter_to_ContentPix( const Vec2i &MapPos )
	{	return MapCellTL_to_ContentPix(MapPos) + Vec2i(MAP_CELL_PIXEL_SIZE/2, MAP_CELL_PIXEL_SIZE/2);	}
		//ContentPix がどのマップのマス内か
	static Vec2i ContentPix_to_MapCell( const Vec2i &ContentPixPos ){	return ContentPixPos/MAP_CELL_PIXEL_SIZE;	}

	//ContentPix ←→ ImagePix (スクロールに関する演算)
	virtual Vec2i ContentPix_to_ImagePix( const Vec2i &ContentPixPos ) const = 0;
	virtual Vec2i ImagePix_to_ContentPix( const Vec2i &ImagePixPos ) const = 0;
};
> ContentPix ←→ ImagePix (スクロールに関する演算)

の部分の具体実装で,さっきのヘルパ関数を使うことになる感じ.

これで,マップ描画関数を書けるね.
具体実装は示さないけど,こんな形の関数になるね.

CODE:

//Mapの描画処理.
//スクロール状態的に見ていている範囲を描画する処理
template< int MAP_CELL_PIXEL_SIZE >	//マップの1マスの描画サイズ(縦横共通)[pixel]
void DrawMap(
	HDC hdc, int W, int H,	//描画先hdc と,そこに選択されているbitmapのサイズ(=描画対象画像のサイズ)
	const IMapData &MapData,
	const ICoordinateTrans<MAP_CELL_PIXEL_SIZE> &CT,
	const COLORREF *Colors	//※IMapData::At()の値に対する描画色を得るための配列
);
最後の引数は,今回はマップの各マスをてきとーに四角形で描画することを考えているから,四角形の描画色を指定してる.
(ガチのゲームなら,ここはマップチップ画像を解決するような何かになるだろうか.)
最後に編集したユーザー usao on 2022年9月09日(金) 19:20 [ 編集 1 回目 ]

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

Re: ゲームの表示待ちがどうの

投稿記事 by usao » 1年前

ふーやれやれ,これでやっと本題の実装に入れそうだ.

まず,「表示器」の側だけど,
マップもキャラクターもエフェクトもとにかく「これを表示しとけや」って表示物を突っ込めばまとめて描画してくれる,っていう形にしてしまおう.

CODE:

//ゲーム処理側から表示器に対して表示物を登録する手段
class IDispItemRegistration
{
public:
	~IDispItemRegistration(){}
public:
	enum class Layer
	{
		Constant=0,	//Mapとか,基本ずっと表示しているような物
		Effect=1	//エフェクト用
	};

	//表示物を登録.
	//* 同一 layer に複数登録した場合,Paint()メソッドの呼び出し順は登録した順になる.
	//* Finished()がtrueを返す状態になった物は登録解除される.
	virtual void RegisterDisplayItem( Layer layer, std::shared_ptr<IDispItem> DispItem ) = 0;
};
注釈として書かれてないけど,enum Layer の値が小さい側から順に描画する,ってことにする:
"Constant" の側にマップ描画やキャラクター描画を突っ込んで,
"Effect" の側に「歩く→地雷が爆発」みたいなのを突っ込む,っていう形を考えてる.

これの具体実装は超シンプル.
2つの layer に相当するデータ構造を持っていて,こいつらに全ての処理を移譲すればOK.
DispItemList 型は,前の日記に出てきた「パラレルすぎる」やつ(単に表示物のリストを抱えていて,リストの全要素のメソッドをforで呼ぶ仕事をするようなやつだよ).

CODE:

//表示物取り扱いの具体実装
class Painter : public IDispItemRegistration
{
private:
	DispItemList m_Layers[2];

public:	//IDispItemRegistration Implementation
	virtual void RegisterDisplayItem( Layer layer, std::shared_ptr<IDispItem> DispItem ) override {	m_Layers[(int)layer].AddDispItem(DispItem);	}
public:
	bool Update()
	{
		bool Ret = false;
		for( auto &L : m_Layers ){	Ret |= L.Update();	}
		return Ret;
	}

	void Paint( HDC hdc, int W, int H ) const
	{
		{//clear with Black
			RECT rect{ 0,0, W, H };
			::FillRect( hdc, &rect, (HBRUSH)::GetStockObject(BLACK_BRUSH) );
		}
		for( const auto &L : m_Layers ){	L.Paint( hdc, W,H );	}
	}
};
マップを表示するには,先に用意したマップ描画関数を呼ぶだけの IDispItem 型具体実装を用意して,それをこいつに突っ込めばOK,と.

同様に,キャラクターの座標に対して何かを描画する物も用意して突っ込めばいい.
キャラクターの座標(マップ上のマス座標)に対する pixel 位置の演算処理はマップ表示で使ってる物と同じだから,それを使って何かをてきとーに描画すれば完了だ.丸でも描いとけば事足りるっしょ.
最後に編集したユーザー usao on 2022年9月09日(金) 19:21 [ 編集 1 回目 ]

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

Re: ゲームの表示待ちがどうの

投稿記事 by usao » 1年前

ここまでの内容で,とりあえず必要なデータ構造(まぁデータって,マップとスクロール量とキャラクターの座標しかないんだけど)とそれを使って動く部分は用意できた.
とりあえずマップとキャラクターが描画されるぞ.

丸いのがキャラクター.
マップ端の方に来た時には右側の絵のようになる.
ちょっと濃い(暗い)緑のマスには地雷が埋まっているぞ!
Fig.png
美麗なグラフィック
Fig.png (7.8 KiB) 閲覧数: 350 回

あとは,ここに移動エフェクトとか,見えている地雷を踏みに行ったらどうの~ みたいな部分を追加していくわけだ.
力作の爆発アニメーションの具合を確認できる時が迫りつつある…!

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

Re: ゲームの表示待ちがどうの

投稿記事 by usao » 1年前

移動のエフェクト中は入力を受け付けないように,ゲームの進行処理に「待ってたら何もしない」という処理を加える.一行書くだけだ.
待ってるか否か?という状態を持つ方法は bool でも何でも良いんだけど,なんとなくカウンタにした.

CODE:

//ゲームの進行処理
UpdateResult Update( const Input_b &rInput, IDispItemRegistration &IDIR )
{
	//エフェクト待ち状態ならreturn
	if( m_EffectWaitCounter )return UpdateResult(false);

	bool NeedToRedraw = false;
	//キー入力による移動処理
	if( Walk( rInput, IDIR ) ){	NeedToRedraw = true;	}

	return UpdateResult( NeedToRedraw );
}
で,Walk() の中で「移動させるキーが押されている」と判断されたときにやる処理がこんな感じになる.

CODE:

//UnitDir には,押したキーに対応した方向の単位ベクトルが与えられる.
//例えば,右方向なら (1,0) になる.
bool StartWalking( const Vec2i &UnitDir, IDispItemRegistration &IDIR )
{
	//移動先のマスの属性を調べる.「壁」なら移動できないからreturn.
	Vec2i TgtPos = m_WalkerPos + UnitDir;
	unsigned char TgtMapCellAttr = m_Map.At( TgtPos );
	if( TgtMapCellAttr == Wall )return false;

	{//Effect
		auto upRawEffectSeq = std::make_unique< DispItemSeq >();
		upRawEffectSeq->PushDispItem( std::make_unique<WalkingEffect>( *this, m_WalkerPos, TgtPos, 4 ) );

		if( TgtMapCellAttr == Mine )  //地雷を踏んだら
		{	upRawEffectSeq->PushDispItem( std::make_unique<ExplosionEffect<32,6> >( *this, TgtPos, 12 ) );	}

		IDIR.RegisterDisplayItem(
			IDispItemRegistration::Layer::Effect,
			std::make_unique<WaitableDispItem>( std::move(upRawEffectSeq), [this]()->bool{	return this->OnWalkFinished();	} )
		);

		//「待ち」状態にするためにカウンタを更新
		++m_EffectWaitCounter;
		//通常時にキャラクターを丸で描いているやつの描画を止める
		m_spWalkerPainter->SetVisibility(false);
	}

	//ゲーム進行としての必要な処理はコレだけ.
	m_WalkerPos = TgtPos;	//キャラクターの座標を更新
	return true;
}
> //Effect

って注釈が入っているブロックの中身がエフェクト関係の仕事.

移動中の表示を行う WalkingEffect ,地雷の爆発表示を行う ExplosionEffect をシーケンスにして,シーケンス終了時に this->OnWalkFinished() が呼ばれるようにコールバックを設定して,表示機に登録してる.

通常時のキャラクタ表示と移動中エフェクトとで二重にキャラクタが描画されると困るから,通常時側は描画するかどうかを切り替えられるようにして,移動を開始する際に無効化→コールバック内で再び有効化する,ってことをやってる.

CODE:

//移動のエフェクト表示が終わったとき
bool OnWalkFinished()
{
	m_spWalkerPainter->SetVisibility(true);  //通常時のキャラクタ描画を再開
	--m_EffectWaitCounter;  //待ちカウンタを減らす
	FocusTo( MapCellCenter_to_ContentPix( m_WalkerPos ) );  //念のためスクロール位置をキャラクタ位置に基づいて設定
	return true;
}
最後に編集したユーザー usao on 2022年9月11日(日) 10:40 [ 編集 1 回目 ]

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

Re: ゲームの表示待ちがどうの

投稿記事 by usao » 1年前

エフェクトクラスは(ゲーム処理進行を書いているクラスの)内部クラスとして書いたから,生成してる箇所では

CODE:

std::make_unique<WalkingEffect>( *this, m_WalkerPos, TgtPos, 4 )
っていう感じで this が突っ込まれている.
この WalkingEffect の場合だと this からたどってスクロール関係の処理を使っている(丸を描くpixel座標を得たり,逆にエフェクトの進行に合わせスクロールさせたりしてる.)
ちなみに残りの引数は「現在位置から目標位置まで4ステップかけて動かせ」っていうだけの話.

何で内部クラスなん? …っていうと,「面倒だったので^^」という面もあるけども,「具体実装の一部をたまたまクラスとして書いたのだから」とか考えれば良いんじゃないかな.
上手く言えないけど,例えば,ゲーム処理進行を書いているクラスそのものに Paint() みたいなメソッドがあるような(ふつーの?)実装形態を考えると,各要素の描画はそのクラスの privete メソッドとかになってるかもしれないよね.この private メソッドが今回は内部クラスになった,っていうような感じかな.
最後に編集したユーザー usao on 2022年9月11日(日) 09:53 [ 編集 1 回目 ]

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

Re: ゲームの表示待ちがどうの

投稿記事 by usao » 1年前

ところで,移動中の表示は「現在位置から目標位置まで4ステップかけて動かせ」っていう話だったけど,爆発のアニメーションの方は「6枚の画像パターンがあるから,それを12ステップかけて表示しろ」って話になってる.

これって,どっちも同じような話だよね:
前者は2次元のpixel座標を,後者は「何枚目の絵」っていう値を扱う,っていう違いはあれども,「値を初期値から目標値までNステップかけて変化させろ」っていう根本部分は一緒.

なので,エフェクトの実装に際しては,この「値を線形的に変化させる」っていう処理をまずは用意したよ.

CODE:

//何らかの値を N ステップで初期値から目標値まで動かす
template< class POS_T >
class LinearTransition
{
private:
	POS_T m_From;
	POS_T m_To;
	int m_nSteps = 0;

	POS_T m_Curr;
	int m_StepCounter;
public:
	LinearTransition( const POS_T &From, const POS_T &To, int nSteps )
		: m_From(From), m_To(To), m_nSteps( (std::max)(nSteps,1) )
		, m_Curr(From), m_StepCounter(0)
	{}

	bool Reached() const {	return (m_StepCounter >= m_nSteps);	}
	const POS_T &CurrPos() const {	return m_Curr;	}

	void Step()
	{
		if( Reached() )return;
		++m_StepCounter;
		m_Curr = m_From  +  m_StepCounter*(m_To-m_From)/m_nSteps;
	}
};
そしたら,エフェクトの Update() の中身は,ほぼこいつを使うだけだから楽だよね.

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

Re: ゲームの表示待ちがどうの

投稿記事 by usao » 1年前

全ての実装が終わったんで,念願の爆発アニメーションを拝めるようになったんだけど…

うーん,イマイチかなぁ.
動きとしてうまく繋がってないというか,それぞれの絵の間の時間間隔が揃ってないというでもいうか,何か不自然さがある気がする.

---

といったところで,考えてみた実装をちょいと使ってみたわけなんだけど,どうなんだろうね.

表示したい要素毎にいちいち class って書かなきゃならないのはちょっとした手間だなぁ.
(あと,内部クラスなんてのはその場に全部書いてしまいたいんだけど,そうするとどんなに小さくても class { ... }; っていう記述は相応に行数を食うんで,コードが見難くなる.個々をどこに書くかっていう整理が必要そう.)

「ゲームデータ側の進行 と 表示」っていう分け方をするかどうかは別としても,「ある時点(入力か何かがあった時点)でやるべき事のスケジュールをだーっと作って List なり queue なりに突っ込む → 以降はそれを再生していく」っていう方式は,まぁまぁ考えやすい気もするね.