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

アバター
usao
記事: 1889
登録日時: 12年前
連絡を取る:

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

投稿記事 by usao » 3年前

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

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

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

アバター
usao
記事: 1889
登録日時: 12年前
連絡を取る:

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

投稿記事 by usao » 3年前

「敵」と「弾」を扱う処理を「ゲーム中シーン」の Update() メソッド内にこのように追加しましたの.

CODE:

GameLogic::GameScene::Request GameLogic::GameScene::Update( const Input_b &rInput, RndGenerator &Rnd )
{
	Request Ret( false, false );
	{
		//これは「避ける者」の移動処理ですわね.
		Ret.NeedToRedraw |= MoveAvoider( rInput );
		//以下が増えましてよ.
		//これらのメソッドは何かデータに変化があればtrueを返してくるというルールですわ.
			//ランダムに「敵」を生成して,メンバ変数のコンテナ m_Enemies に追加する処理ですわ.
		Ret.NeedToRedraw |= CreateNewEnemyAtRandom( Rnd );
			//「敵」の移動と破棄,敵が生成した「弾」をメンバ変数のコンテナ m_Bullets に追加する処理ですわ.
		Ret.NeedToRedraw |= UpdateEnemies( Rnd );
			//「弾」の移動と破棄ですわね.
		Ret.NeedToRedraw |= MoveBullets( Rnd );
			//「弾」と「避ける者」との当たり判定ですことよ.
			//当たった場合は,ゲームオーバーフラグ m_bGameOver がtrueに変更されるだけのことですわね.
		Ret.NeedToRedraw |= HitCheck();
	}
	if( m_bGameOver )
	{
		Ret.ResetRequested = ( (rInput.KeyStateHist( VK_LBUTTON ) & 0x03) == 0x01 );
	}
	return Ret;
}
各作業メソッドは割愛いたしますわ.いずれも数行の処理でしかありませんもの.
もちろん Paint() メソッドにも「敵」と「弾」の Paint() 呼び出し処理を追加していましてよ.

アバター
usao
記事: 1889
登録日時: 12年前
連絡を取る:

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

投稿記事 by usao » 3年前

「弾」の具体実装は,こんな感じですわね.
単純に小さな円が等速直線運動をするだけの,見どころのない実装ですわ.

CODE:

class SimpleBullet : public Bullet
{
private:
	Vec2f m_CenterPos;	//中心座標
	float m_Radius;	//半径
	float m_SqRadius;	//半径の2乗
	Vec2f m_Velocity;	//速度
public:
	SimpleBullet( const Vec2f &InitCenterPos, float Radius, const Vec2f &Velocity )
		: m_CenterPos{ InitCenterPos }, m_Radius{ Radius }, m_SqRadius{ Radius*Radius }, m_Velocity{ Velocity }
	{}
	virtual bool Move(int GameFieldW, int GameFieldH, const Vec2f & AvoiderPos, RndGenerator & Rnd) override
	{
		m_CenterPos += m_Velocity;
		//寿命判定も,いわゆる「画面外に出たら」という話でしかありませんわ.
		if( m_CenterPos[0] < -m_Radius  ||  m_CenterPos[0] >= GameFieldW+m_Radius )return false;
		if( m_CenterPos[1] < -m_Radius  ||  m_CenterPos[1] >= GameFieldH+m_Radius )return false;
		return true;
	}
	//当たり判定は簡素に「円 vs 点」で済ましていましてよ.
	virtual const bool HitCheck(const Vec2f & CheckTgtPos) const override
	{	return ( (CheckTgtPos - m_CenterPos).SqL2Norm() < m_SqRadius );	}

	virtual void Paint(HDC hdc) const override { ここは円を描画するだけですから割愛ですわ }
};

アバター
usao
記事: 1889
登録日時: 12年前
連絡を取る:

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

投稿記事 by usao » 3年前

「敵」の具体実装に際しては,まず「武器」という概念を用意してみましたの.

CODE:

//要は,Enemy::Fire() の移譲先ですわね
class Weapon
{
public:
	virtual ~Weapon(){}
	//Returns a newly fired bullet or nullptr.
	virtual std::unique_ptr<Bullet> Fire( const Vec2f &OwnerPos, const Vec2f &TargetPos, RndGenerator &Rnd ) = 0;
};

//小さな弾をばらまく武器ができましたわ!
class MG : public Weapon
{
private:
	int m_FirePercentage;
public:
	MG( int FirePercentage ) : m_FirePercentage(FirePercentage) {}
	virtual std::unique_ptr<Bullet> Fire( const Vec2f &OwnerPos, const Vec2f &TargetPos, RndGenerator &Rnd ) override
	{
		if( Rnd.Int( 1, 100 ) > m_FirePercentage )return nullptr;

		Vec2f V;
		{//弾の向かう先をランダムに少しばらけさせているだけのことですわ
			auto D = GetUnit(TargetPos - OwnerPos);
			float theta = Rnd.Float( -PI, PI );
			D += Rnd.Float( 0.0f, D.L2Norm()*0.25f ) * Vec2f( cos(theta), sin(theta) );
			if( D.SqL2Norm() < 0.01f )return nullptr;
			V = GetUnit(D) * Rnd.Float( 3.0f, 5.0f );
		}
		return std::make_unique<SimpleBullet>( OwnerPos, 3.0f, V );
	}
};
これを敵の具体クラスのコンストラクタに引き渡して「装備」させてみましてよ.

CODE:

//「ゲーム中シーン」の敵生成処理で実施していましてよ.
bool GameLogic::GameScene::CreateNewEnemyAtRandom( RndGenerator &Rnd )
{
	if( m_bGameOver )return false;

	if( Rnd.Int( 1, 100 ) <= 5 )
	{	//「敵」の具体クラス SimpleEnemy に武器 MG を提供していますわ.
		//これで弾丸の雨あられが期待できますわね! うふふっ!
		m_Enemies.push_back(
			std::make_unique<SimpleEnemy>(
				std::make_unique<MG>( Rnd.Int(3,18) ), GameFieldW(), GameFieldH(), Rnd
				)
		);
		return true;
	}
	return false;
}

アバター
usao
記事: 1889
登録日時: 12年前
連絡を取る:

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

投稿記事 by usao » 3年前

正直,このゲーム内容で「敵」がどこからどうやって出てきてどう動くのが良いのか,わたくし,さっぱりわかりませんの.
なんとなく適当に作りましたわ.

CODE:

//これが「敵」の具体実装ですわ
class SimpleEnemy : public Enemy
{
public:
	//武器の受け取りの型がこれで妥当なのかはちょっとわかりませんの.勉強不足が露呈ですわね…
	SimpleEnemy( std::unique_ptr<Weapon> &&Weapon, int GameFieldW, int GameFieldH, RndGenerator &Rnd )
		: m_upWeapon{ std::move(Weapon) }
	{
		//適当に画面外上側から降りてきて,弾をばらまいた後,上側に去っていく,という動きにしていますわ.
		m_Pos.Assign( Rnd.Float( ms_HalfSize, GameFieldW-ms_HalfSize ), -ms_HalfSize );

		float y = Rnd.Float( GameFieldH*0.1f, GameFieldH*0.5f );	//どこまで降りてくるかに個体差を持たせてみましてよ.
		m_Schedule.push( std::make_unique<StraightMove>( Vec2f(0,2), [y]( const Vec2f &P )->bool{	return P[1]>=y;	} ) );
		m_Schedule.push( std::make_unique<Stay>( Rnd.Int( 32, 128 ) ) );	//留まる期間も個体ごとにランダムですわ
		m_Schedule.push( std::make_unique<StraightMove>( Vec2f(0,-5), []( const Vec2f &P )->bool{	return P[1] < -ms_HalfSize;	} ) );
	}
	
	virtual bool Move( int GameFieldW, int GameFieldH, const Vec2f &AvoiderPos, RndGenerator &Rnd ) override
	{//コンストラクタで決めた動作を再生していくだけのことですわね.
		if( !m_Schedule.empty()  &&  !m_Schedule.front()->Control( m_Pos ) )
		{	m_Schedule.pop();	}

		return !m_Schedule.empty();
	}

	virtual std::shared_ptr<Bullet> Fire( const Vec2f &AvoiderPos, RndGenerator &Rnd ) override
	{
		if( m_Schedule.empty()  ||  !m_Schedule.front()->CanFire() )return nullptr;
		return m_upWeapon->Fire( m_Pos, AvoiderPos, Rnd );	//ここは武器に処理移譲するだけで解決ですわね
	}

	virtual void Paint( HDC hdc ) const override { ここは割愛ですわね.単に四角形で描画しているだけのことですもの. }

private:
	//コンストラクタで動作をスケジューリングするのに使っている型ですわ.
	class State	//型名はちょっと…微妙かもしれませんわね.
	{
	public:
		virtual ~State(){}
		//位置を更新するメソッドですわ.このステートが終わった場合にはfalseを返しますの.
		virtual bool Control( Vec2f &Pos ) = 0;
		//このステートで発砲するか否か,ですわね.
		virtual bool CanFire() const {	return false;	}
	};
	//降りてくるところと,帰っていくところの実装ですわ
	class StraightMove : public State
	{
	private:
		Vec2f m_Velocity;
		std::function<bool(const Vec2f &)> m_TargetCondition;
	public:
		StraightMove( const Vec2f &V, std::function<bool(const Vec2f &)> TgtCondition )
			: m_Velocity{V}, m_TargetCondition{TgtCondition}
		{}

		virtual bool Control( Vec2f &Pos ) override {	Pos += m_Velocity;	return !m_TargetCondition(Pos);	}
	};
	//その場に留まって発砲する期間の実装ですことよ.
	class Stay : public State
	{
	private:
		int m_Counter;
	public:
		Stay( int WaitCount ) : m_Counter{WaitCount} {}
		virtual bool Control( Vec2f &Pos ) override {	return ( --m_Counter > 0 );	}
		virtual bool CanFire() const override {	return true;	}
	};

private:
	Vec2f m_Pos;
	std::queue< std::unique_ptr<State> > m_Schedule;
	std::unique_ptr< Weapon > m_upWeapon;
private:
	static const float ms_HalfSize;	//この敵のサイズ(面倒なのでstaticな定数)ですわ.
};
const float SimpleEnemy::ms_HalfSize = 7.0f;

アバター
usao
記事: 1889
登録日時: 12年前
連絡を取る:

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

投稿記事 by usao » 3年前

以上で,ゲームとしてプレイすることができるようになりましてよ!

…ですが,わたくし… 正直,これの何が面白いのかちっともわかりませんわ.
ひらすら「かわす」だけの内容では 5 秒で飽きてしまいましてよ!
(これでは,「敵」や「弾」の具体実装を増やす意欲もわきませんわね.)


できた物が面白いかはともかくとして,「とにかく素朴に順に必要なものだけを追加していく」というスタイルを実践してみた,という日記ですわ.
コードの設計の良し悪しはともかく,このようなやり方なら,何も動かない段階でどこぞの質問掲示板で愚にも付かない質問をするような事態には陥らないのではなくって?

= 完遂しましてよ! =

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

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

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

usao さんが書きました:
3年前
…ですが,わたくし… 正直,これの何が面白いのかちっともわかりませんわ.
ひらすら「かわす」だけの内容では 5 秒で飽きてしまいましてよ!
(これでは,「敵」や「弾」の具体実装を増やす意欲もわきませんわね.)
ああ、一言二言多いのが玉に瑕なお嬢様ですこと。

アバター
usao
記事: 1889
登録日時: 12年前
連絡を取る:

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

投稿記事 by usao » 2年前

(はい.お嬢様ってこう口が悪いイメージ ← 個人の感想です!!!)

で,個人的嗜好の問題も多分にあるのでしょうけど,
特に展開があるわけでもなく,とれるリアクションもひたすら移動するだけ,ってのは,どうにも面白いと思えないっす.
それは「クソゲー」みたいな方向の話ではなくて,まずそれ以前にこれだけだと「ゲーム」になっていない気がします.
んなこと言うと,「ゲームって何だろう?」みたいなことになってよくわからなくなるけども.
圧倒的に「足りない感」というか.

想像するに,これを作ろうと思う人も,この形を最終形としているのではなく「最初の形として」考えているだけなんじゃないかな.
そうだとしたら,やはり実装をまとめるのはその先の内容を追加して全体像が具体的に見えてからでも遅くはない気がします.(早すぎる最適化,イクナイ,的な意味で)
…とはいえ,単一の方法だけをシミュレートしただけでは片手落ち感もありますので,時間的余裕ができたら「Sceneインタフェースありき」みたいな形から実装しはじめる側も試行してみたいところではあります.
(ちょうどいい題材があれば良いけど.話的にはある程度「シーン」の個数があるべきなんだけど,かといって各シーンの具体的な中身の実装の側に延々時間取られるのでは,何やってるのかわからなくなるし… 今回のだって,一番コード量多いのは敵の具体実装っていう.)
最後に編集したユーザー usao on 2022年8月01日(月) 10:28 [ 編集 1 回目 ]

アバター
usao
記事: 1889
登録日時: 12年前
連絡を取る:

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

投稿記事 by usao » 2年前

一応,今回の実装の悪い(そこまで気をまわしていない/てきとーに済ませた)点も述べておくと…

Enemy と GameScene との間には暗黙的な仕様の共有が存在している…とでもいうか:
敵および弾の初回 Update() はいつ呼ばれるのか? という点がグレーゾーンになってしまっている.
(敵や弾が新規に追加されたとき,そのタイムステップではそれらは初期位置に発生するだけなのか,それともいきなり Update() がかかるのか? っていう.)

ここのところの仕様みたいなはコード的には何も表現されていない(コメントすら無いぞ).
この実装では,GameScene::Update() 内で各処理を呼ぶ順序が変わったら,「敵」や「弾」の動作に影響が及んでしまうわけだ.

アバター
usao
記事: 1889
登録日時: 12年前
連絡を取る:

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

投稿記事 by usao » 2年前

SimpleEnemy がコンストラクタ内で作っている動作スケジュールも,最初は「武器」と同様に外側から突っ込む形で考えていた.
…んだけど,「動き」は「その敵の形/大きさ」と割と切っても切れない関係あって「めんどくさい」ってなった.
(「画面外から出てくる」として,その初期位置はどこやねん? とか)

「一般的な/単純な(:この型で扱う)」敵のサイズというのは固定の何種類かにする,とかそういう話があれば,この辺は楽にやれるかな.
(FC時代のSTGとかって,そんな感じだよね.)

アバター
usao
記事: 1889
登録日時: 12年前
連絡を取る:

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

投稿記事 by usao » 2年前

ちなみに今回のやつに Scene なるインタフェースクラスを導入しても特に良いことは何もないと思いますが,一応やってみますと,

まず,TitleScene や GameScene の生成/破棄やメソッド呼び出しをしているやつ(GameLogicクラス)の保持するデータの雰囲気が変わります.

CODE:

private:	//「シーン」に関する保持データ
	enum class SceneIndex{	Title=0, GamePlay=1 } m_CurrSceneIndex;
	std::unique_ptr<Scene> m_Scenes[2];
private:  //「カレントシーン」
	std::unique_ptr<Scene> &CurrScene(){	return m_Scenes[ (int)m_CurrSceneIndex ];	}
TitleScene とか GameScene とかいう具体型名は消え,全ては Scene である,ということしかわからない形です.
※もちろん,素朴実装と同様に具体型で保持しても良いが,具体型で保持するということは何かしら具体型としてアクセスしないと仕事ができないという話であるわけで,そんな話であれば Scene なるインタフェースは何のための存在なのかわらかないということになる,と思う.

恩恵を受けるのは Update() と Paint() の実装です.switch だった分岐が消えて,単にカレントシーンに移譲するだけになります.(逆に言えば,恩恵はそれだけである)
だいたい以下のような感じに変わります.

CODE:

UpdateResult GameLogic::Update( const Input_b &rInput, RndGenerator &Rnd )
{
	if( rInput.KeyStateHist( 'Q' ) & 0x01 )
	{	return UpdateResult(false,true);	}

	return UpdateResult( CurrScene()->Update( *this, rInput, Rnd ) );
}

bool GameLogic::Paint(HDC hdc, int W, int H)
{	CurrScene()->Paint( hdc, *this );	return true;	}
ここで,Scene::Update() や Scene::Paint() の仕様をどう定めるのか? というのが問題になりますね.
全てのシーンで共通にせねばならないので,元々やれていたことをどうやって実現するのか? を考える必要があります.