何故自分が困る書き方を好むのか

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

何故自分が困る書き方を好むのか

投稿記事 by usao » 1年前

立て続けに2つの質問{ これこれ }を見たけど…

なんか,結構な期間このサイトを見てきた感じ,
  • Scene とかいう謎の基底
  • Task とかいう(同上)
  • XXXManager とかいうクソみてぇな名称の何か
みたいな謎の設計の話を割とよく見かけたような気がする.
何故彼らはこれらを好むのだろうか?
もちろん,それで何かが楽になるのであれば良いのだが,楽になってない方々は何なの?っていう.

意味わかんねぇ謎の実装形態を(どこかから持ってきて)採用したら直後に実装作業に行き詰るであろうことは明白であると思うのだが.
マゾなのか?

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

Re: 何故自分が困る書き方を好むのか

投稿記事 by usao » 1年前

(文体を戻す)

話は変わって,これは単なる個人的な好みの話だけど,なるべくなら

上位「おいお前,ちょっとやってみろや」
シーンA「はい」
シーンA「完了しました!」
上位「じゃあ結果よこせや」
シーンA「どうぞ,こんなん出ました」
上位「そうなったか…ならば……おい,そこのお前,今度はお前がやれや」
シーンB「はい」


みたいな形にしたいなぁ.
流れは上位の管轄.
なんつーか,下位(ここでは「シーン」)というのは上位側から見たら例えるなら MessageBox みたいな存在,とでもいうか.

※先のミニマムな例で,シーンが上位側に向けて「シーンを変えろ」と言わない話になっているのは,このような思想による.
> 「ゲームオーバー条件を満たしてしまいましてよ」
と「言う」のも,唐突に/能動的に シーンの側から伝達してくるのではなくて,↑でいうところの
> シーンA「どうぞ,こんなん出ました」
に相当.
最後に編集したユーザー usao on 2022年7月28日(木) 14:34 [ 編集 1 回目 ]

アバター
もるも
記事: 54
登録日時: 8年前

Re: 何故自分が困る書き方を好むのか

投稿記事 by もるも » 1年前

ウィンドウを自作したことのある人なら、コールバックだのプロシージャだのでなんとなく、
ゲームを更新しながら応答待ちすればいいとわかるけど、初心者はDXライブラリでラッパーされていてなじみがなくて戸惑ったりする。
こういう差はけっこう大きい。

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

Re: 何故自分が困る書き方を好むのか

投稿記事 by usao » 1年前

少しばかり具体実装を考えてみましたわ.

例えば,こんな感じの物を書いていくことになるのだと思いましてよ.

CODE:

///<summary>
///ゲームの実装をこの中に書いていく的なやつ:
///これよりも外側にメインループがあって,そこから Update(), Paint() が呼ばれる想定ですわよ.
///</summary>
class GameLogic : public Toyger::Painter
{
public:	//※このゲームの表示サイズ.(ウィンドウ作る側がクライアント領域サイズを決める用)
	static int ViewW(){	return 400;	}
	static int ViewH(){	return 300;	}
public:
	GameLogic();
	~GameLogic();
	GameLogic( const GameLogic & ) = delete;
	GameLogic &operator=( const GameLogic & ) = delete;
public:	//Update() と Paint()
	UpdateResult Update( const Input_b &rInput, RndGenerator &Rnd );
	virtual bool Paint( HDC hdc, int W, int H ) override;
private:	//「シーン」関連Data
	enum class SceneIndex{	Title, GamePlay	} m_CurrScene;
	class TitleScene;
	class GameScene;
	std::unique_ptr< TitleScene > m_upTitleScene;
	std::unique_ptr< GameScene > m_upGameScene;
private:
	void TransToTitleScene();
	void TransToGamePlayScene();
};
※謎の public 継承と Paint() が virtual になっている点については,こちらで実際に動かしてみる用に書いた外側(メインループとかウィンドウの実装がある側)の都合ですから,ここでは考えなくてもよろしいですわ.

Update() の引数は
  • キーとかマウスとかの入力状態を知るための何か
  • 乱数生成器
となっていますわ.最低限このくらいの物を上位側から渡していただければ,ゲームを進行できるかと思いましてよ.
戻り値の方は,こんな感じですわね.

CODE:

///<summary>
///	<see cref="GameLogic::Update"/>の戻り値用.
///	呼び出し側(メインループ側)に対して必要事項を伝達する.
///</summary>
struct UpdateResult
{
	bool NeedToRedraw;	//再描画が必要か?(Update()で表示内容を変えるべき変化が起きたかどうか)
	bool AppQuitRequested;	//「APP自体を終了させたい」ということになったか?

	UpdateResult( bool Redraw, bool Quit=false ) : NeedToRedraw(Redraw), AppQuitRequested(Quit) {}
};
NeedToRedraw がtrueな物を返すと,上位側から Paint() が呼ばれる,という機構としていますわ.
(別に毎回 Paint() を呼ぶのでもよくね? のようにお考えの場合には,常にtrueにすればよろしくってよ.)

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

Re: 何故自分が困る書き方を好むのか

投稿記事 by usao » 1年前

このように,「classですが何か?」とだけ宣言したものの unique_ptr を保有する形にした場合,
コンストラクタやデストラクタの実装をそのclassの型が解決されている場所に書く必要がある,という点に注意が必要ですわね.
…という部分と「シーンの遷移」の実装はこんな感じですこと?

CODE:

GameLogic::GameLogic(){	TransToTitleScene();	}  //最初は「タイトルシーン」にしておくのですわ
GameLogic::~GameLogic(){}

void GameLogic::TransToTitleScene()  //「タイトルシーン」への遷移処理ですわ
{
	m_CurrScene = SceneIndex::Title;
	m_upTitleScene = std::make_unique< TitleScene >();
}

void GameLogic::TransToGamePlayScene()  //「ゲーム中シーン」への遷移処理ですわ
{
	m_CurrScene = SceneIndex::GamePlay;
	if( !m_upGameScene ){	m_upGameScene = std::make_unique< GameScene >();	}
	else {	m_upGameScene->InitializeGameState();	}
}
「タイトルシーン」のインスタンスは毎回作り直し,「ゲーム中シーン」の方は同一インスタンスを使い回す形に書いてみましたわ.

m_CurrScene が「カレントのシーンはどっちか?」を示すメンバ変数ですわ.
あとは,この値に基づいた分岐を Update() と Paint() に実装すれば良いのですわね.

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

Re: 何故自分が困る書き方を好むのか

投稿記事 by usao » 1年前

Update() を実装しましたわ.

CODE:

UpdateResult GameLogic::Update( const Input_b &rInput, RndGenerator &Rnd )
{
	if( rInput.KeyStateHist( 'Q' ) & 0x01 )  //シーンに依らず,Qキーで終了するための処理ですわ.
	{	return UpdateResult(false,true);	}

	switch( m_CurrScene )  //「シーン」毎の処理をするための分岐ですわね
	{
	case SceneIndex::Title:
		{
			if( m_upTitleScene->Update( rInput ) )  //「ゲームを開始したい」場合にはtrueが返される想定としておりますの.
			{
				TransToGamePlayScene();
				return UpdateResult( true );
			}
		}
		break;
	case SceneIndex::GamePlay:
		{
			auto Req = m_upGameScene->Update( rInput, Rnd );  //こちらの戻り値は構造体な想定ですわ.
			if( Req.ResetRequested )
			{//「リセット」が要求された場合には「タイトルシーン」に戻すこととしていますわ
				TransToTitleScene();
				return UpdateResult( true );
			}
			else
			{	return UpdateResult( Req.NeedToRedraw );	}
		}
		break;
	default:
		break;
	}
	return UpdateResult( false );
}
先頭に「Qキーが押されていたら問答無用でAPP終了」とするための記述があり,
あとは switch で各シーンの処理を実施しているだけですわ.
それぞれの case が何をやっているかは…字面から明白ですわね.
どちらのシーンも Update() の戻り値で必要な情報を返すというシンプルな形としていますわ.
戻り値の意味についてはそれぞれのシーンが独自に必要な形に定めればよいのですわ.

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

Re: 何故自分が困る書き方を好むのか

投稿記事 by usao » 1年前

Paint() の方も同じ感じですわね.
(この例は, Win32 で GDI な世界で書いていましてよ.)

CODE:

//※引数(W,H) は hdc に Select されている BMP のサイズということにしていますわ.
// (=クライアント領域のサイズ という感じでしてよ)
bool GameLogic::Paint(HDC hdc, int W, int H)
{
	RECT ViewRect;
	ViewRect.left = ViewRect.top = 0;
	ViewRect.right = W;	ViewRect.bottom = H;

	//とりあえず常に全面Clearしていますわ.
	//でも,これは「シーン」に任せるべき仕事かもしれませんわね.
	::FillRect( hdc, &ViewRect, (HBRUSH)::GetStockObject(BLACK_BRUSH) );
	
	switch( m_CurrScene )  //ここは「シーン」に描画を任せるだけですわね
	{
	case SceneIndex::Title:
		m_upTitleScene->Paint( hdc, ViewRect );	break;
	case SceneIndex::GamePlay:
		m_upGameScene->Paint( hdc, ViewRect );	break;
	default:
		break;
	}
	return true;
}

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

Re: 何故自分が困る書き方を好むのか

投稿記事 by usao » 1年前

タイトルシーンの実装ですわ.

CODE:

class GameLogic::TitleScene
{
public:
	//ゲーム開始するならtrueを返す
	bool Update( const Input_b &rInput )
	{	return ( (rInput.KeyStateHist( VK_LBUTTON ) & 0x03) == 0x01 );	}

	void Paint( HDC hdc, const RECT &ViewRect ) const
	{
		::SetTextColor( hdc, RGB(220,230,255) );
		::SetBkMode( hdc, TRANSPARENT );
		RECT r = ViewRect;
		::DrawText( hdc, _T("= L-Click to Start ="), -1, &r, DT_CENTER|DT_SINGLELINE|DT_VCENTER );
	}
};
最低限すぎて説明するまでもない状態ですわね…
単に「クリックされるまで待つ」を実現しているだけになっていますわ.
肝心の「ゲームの内容」が決まっていない状態では「タイトルシーン」の役割も定まらないので,当然こうなってしまいますわね.

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

Re: 何故自分が困る書き方を好むのか

投稿記事 by usao » 1年前

と,そのようなわけで,「ゲーム中シーン」の内容がありませんわ…困りましたわね……
最初に引用した質問に

> 敵が上から落としてくる弾をかわすといったゲーム

という話がありましたから,なんとなくそのような内容を実装してみることに致しますわ.
(しかしアバウトな文章ですわね…)

まずは,「敵」と「弾」は度外視して,最低限「かわす」の主体が存在する状態を作ってみますわね.

CODE:

//-------------------------------------------
//これらは現状,不明ですわ.
class Bullet{};	//「弾」
class Enemy{};	//「敵」

//-------------------------------------------
//GameLogic::GameScene
class GameLogic::GameScene
{
public:
	GameScene(){	InitializeGameState();	}

public:
	//ゲーム状態を初期状態にするメソッドですわ.
	void InitializeGameState()
	{
		m_bGameOver = false;
		m_AvoiderPos.Assign( ViewW()*0.5f, ViewH()*0.75f );
		m_Enemies.clear();
		m_Bullets.clear();
	}
	
	//Updateの戻り値:呼び出し側への要求を返すものですわね.
	struct Request
	{
		bool NeedToRedraw;	//再描画が必要か?(Update()で表示内容を変えるべき変化が起きたかどうか)
		bool ResetRequested;	//「リセット」したいということになったか?
		Request( bool Redraw, bool Reset=false ) : NeedToRedraw(Redraw), ResetRequested(Reset) {}
	};
	
	Request Update( const Input_b &rInput, RndGenerator &Rnd );
	void Paint( HDC hdc, const RECT &ViewRect ) const;
private:	//Data
	bool m_bGameOver;	//GameOverフラグ
	Vec2f m_AvoiderPos;	//「避ける者」の位置
	std::vector< std::shared_ptr<Enemy> > m_Enemies;	//敵
	std::vector< std::shared_ptr<Bullet> > m_Bullets;	//弾
private:
	bool MoveAvoider( const Input_b &rInput );	//「避ける者」の移動処理
};
public なメソッドはどれも呼び出し側コードで既出のものですわね.

> Vec2f m_AvoiderPos;
の部分の型(Vec3f)は,よくある2次元のベクトル(要素値の型がfloatのもの)の実装ですわ.

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

Re: 何故自分が困る書き方を好むのか

投稿記事 by usao » 1年前

「ゲーム中シーン」のUpdateですわ.

CODE:

GameLogic::GameScene::Request GameLogic::GameScene::Update( const Input_b &rInput, RndGenerator &Rnd )
{
	Request Ret( false, false );
	
	//「避ける者」の移動処理ですわ.
	if( !m_bGameOver )
	{	Ret.NeedToRedraw |= MoveAvoider( rInput );	}

	{//※これはゲームオーバー状態にするためのダミー処理ですわ
		if( !m_bGameOver && (rInput.KeyStateHist( VK_MBUTTON )) )
		{	m_bGameOver=true;	Ret.NeedToRedraw=true;	}
	}

	//【ゲームオーバーになっているときに,マウスクリック】で「リセット」ということにいたしますわね.
	if( m_bGameOver )
	{
		Ret.ResetRequested = ( (rInput.KeyStateHist( VK_LBUTTON ) & 0x03) == 0x01 );
	}

	return Ret;
}
想定としては…

「避ける者」が「弾」に当たってしまったら「ゲームオーバー」となり,
ゲームオーバー状態でやれることは「リセット」だけ

…という感じでしてよ.
現状,「避ける者」がいるだけですから,動作テスト用にゲームオーバーにするためのダミーの処理を入れてありますわ.
  • MoveAvoider() メソッドの中身は,入力に応じて座標更新するだけのものですから,割愛致しますわね.
  • Paint() も同様に割愛ですわ.「避ける者」を何かしら描画するだけですもの.
とりあえずこれで通しで動かすことはできますわね.まずは一段落,といったところかしら.
「敵」と「弾」については,まだ実装がありませんの.
これらは今後追加することといたしますわ.

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

Re: 何故自分が困る書き方を好むのか

投稿記事 by usao » 1年前

「敵」と「弾」を入れてみましてよ.
Test.png
四角がすごい勢いで丸を吐き出してくる
Test.png (5.58 KiB) 閲覧数: 34 回
> 敵が上から落としてくる弾をかわす

…という文言からでは,「敵」とは「弾」の排出者,くらいのことしかわかりませんわね.
「かわす」対象とはされていないようにも読めますから,「敵」には当たり判定は無いものといたしましたの.

こうですわね.

CODE:

class Bullet
{
public:
	virtual ~Bullet(){}
	//Returns false if this bullet must be disabled (removed from GameField).
	virtual bool Move( int GameFieldW, int GameFieldH, const Vec2f &AvoiderPos, RndGenerator &Rnd ) = 0;
	virtual const bool HitCheck( const Vec2f &CheckTgtPos ) const = 0;
	virtual void Paint( HDC hdc ) const = 0;
};

class Enemy
{
public:
	virtual ~Enemy(){}
	//Returns false if this enemy must be disabled (removed from GameField).
	virtual bool Move( int GameFieldW, int GameFieldH, const Vec2f &AvoiderPos, RndGenerator &Rnd ) = 0;
	//Returns a newly fired bullet or nullptr.
	virtual std::shared_ptr<Bullet> Fire( const Vec2f &AvoiderPos, RndGenerator &Rnd ) = 0;
	virtual void Paint( HDC hdc ) const = 0;
};
そろそろ説明も面倒になってきましたので,雰囲気でお察しくださいませ.
「弾」には当たり判定の処理が,「敵」には「弾」を生成する処理が設けられている,と言うだけの話ですことよ.