RTS製作日記。その3

アバター
MNS
記事: 35
登録日時: 15年前

RTS製作日記。その3

投稿記事 by MNS » 14年前

今回は、ユニットの移動を実装し、
簡単なAIを組み込みたいと思います。


まず、ユニットに"最大の力"と"最大速度"の要素を加えます。

CODE:

class	Unit :public GameEntity
{

	…省略…

	//最大の力
	double	m_MaxForce;
	//最大速度
	double	m_MaxSpeed;

	…省略…
};
次に、"ユニットの移動を制御する"クラスを作成します。
これはつまり、ユニットに与える力を計算するクラスです。

CODE:

class	MyFlag
{
	bool	flag;

public:

	MyFlag():flag(false){}

	void	ON(){ flag = true; }
	void	OFF(){ flag = false; }

	operator bool(){ return flag; }
};



class	Unit;

class	MotionManager
{

	Unit*	m_pUnit;

	//ターゲット地点
	Vec2D	m_Target;

public:

	MotionManager(Unit*	unit)
		:m_pUnit(unit)
	{}

	~MotionManager(){}


	//最終的な力を計算
	Vec2D	Calculate();


	//移動の種類
	Vec2D	Go(Vec2D Target);

	
	//--移動のフラグ
	MyFlag	flag_Go;


	//--アクセサ
	Vec2D	GetTarget(){ return m_Target; }
	void	SetTarget(Vec2D target){ m_Target = target; } 

};
このクラスは、各ユニットが各々のものを持ちます。

CODE:

class	Unit :public GameEntity
{
	…省略…
	MotionManager* m_MotionManager;
	…省略…
};
現状では、『目的地に移動するための力を計算する』関数であるGo関数だけが定義されています。
各種の”移動”関数はフラグのオン・オフで制御され、
フラグがオンである移動のみが、力として加えられます。
(いろいろあって、フラグはわざわざMyFlag構造体で管理していますが、中身はboolです)

CODE:

//最終的な力の計算
Vec2D	MotionManager::Calculate()
{
	Vec2D	Force;

	//記録しておくと便利
	double	maxF = m_pUnit->MaxForce();
	
	if(flag_Go)
	{
		Force += Go(m_Target);

		if(Force.LengthSQ() > maxF*maxF)
		{
			Force = getNormalize(Force) * maxF;
			return Force;
		}
	}

	return Force;
}


Vec2D	MotionManager::Go(Vec2D Target)
{
	Vec2D F = Target - m_pUnit->Pos(); 
	
	return
		getNormalize(F) * m_pUnit->MaxForce();
}
Go関数では、自分の位置から目的地までのベクトルを得て、
それを、大きさが最大の力になるよう調整します。
(正規化ベクトルを得て、"最大の力"倍する。)

最終的な力の計算では、
各移動関数が有効かどうかを調べ、有効ならば力に加えます。
力が最大力を越えた場合、計算を終了し、力を返します。

よって、UnitのUpdate関数は次のようになります。

CODE:

void	Unit::Update()
{
	Vec2D	Force;

	Force = m_MotionManager->Calculate();

	m_Acceleration += Force / m_Mass;

	m_Velocity += m_Acceleration;

	//最大速度を超えていないかどうか
	if(m_Velocity.LengthSQ() > m_MaxSpeed*m_MaxSpeed)
	{
		m_Velocity = getNormalize(m_Velocity) * m_MaxSpeed;
	}

	m_Pos += m_Velocity;
}
―ここまでの構造
tunagari.png
tunagari.png (20.62 KiB) 閲覧数: 112 回
さて、これで移動の計算を行う処理はできましたが、
移動をさせる機構がまだ出来ていません。

プレイヤーはユニットに指示を与えるだけで、
実質的な行動に干渉することはありません。
つまり、各々のユニットは自律的に行動し、
その場の状況に応じて、移動などを行う必要があります。

つまり、ユニットひとつひとつにAIの機構が必要ですので、
これを実装することにします。

このプログラムでは、AIのシステムとして「有限オートマトン」を組み込みます。
有限オートマトンは、別名で有限ステートマシンともいい、
これを日本語で書き表すと「有限状態機械」といいます。
いまいちピンとこない単語ですが、
簡単にいうと、「状態」と「遷移」からなる「振る舞いのモデル」です。
ゲーム内のユニットで具体的に説明すると、
ユニットは常に"状態"をもっており、
「待機」という状態で、プレイヤーが移動の命令をだせば、
「待機」から「移動」に変わり、目的地に着いたら再び「待機」に変わります。
これを、例えば「待機」の状態で敵が近づいたら「攻撃」に遷移するなど、
"状態"の種類を拡張することで、AIの開発が楽になります。

文章で書くと複雑かつ難解なものに思えるかもしれませんが、
プログラム自体は割と単純で、
デザインパターンの「State」がほとんどそのまま適用できます。

コードにしてみます。
”状態”を表す"State"クラスと、それを所有する"StateMachine"クラスを設計します。

CODE:

class	State
{
public:

	virtual void	Enter(Unit* owner) = 0;
	virtual void	Update(Unit* owner) = 0;
	virtual void	Exit(Unit* owner) = 0;
};


class	StateMachine
{
	//以前の状態
	State*	m_PreviousState;
	//現在の状態
	State*	m_CurrentState;
	//常にある状態
	State*	m_GlobalState;

	//保持者
	Unit*	m_pOwner;

public:

	StateMachine(Unit*	owner,
				 State* firstState,
				 State* globalState)
		:m_pOwner(owner),
		 m_CurrentState(firstState),
		 m_GlobalState(globalState)
	{}

	~StateMachine()
	{
		if(m_PreviousState)	delete m_PreviousState;
		if(m_CurrentState)	delete m_CurrentState;
		if(m_GlobalState)	delete m_GlobalState;
	}


	void	Execute()
	{
		if(m_CurrentState) m_CurrentState->Update(m_pOwner);

		if(m_GlobalState) m_GlobalState->Update(m_pOwner);
	}

	void	ChangeState(State* newState)
	{
		m_PreviousState = m_CurrentState;

		m_CurrentState->Exit(m_pOwner);

		delete	m_CurrentState;

		m_CurrentState = newState;

		m_CurrentState->Enter(m_pOwner);
	}
};
すべての”状態”はこのStateクラスから派生します。
ユニットのステートとして、「待機」と「移動」、
そしてグローバルステートを実装します。
グローバルステートは、あらゆる状態においても更新されるもので、
例えば「敵が接近した」、「別の指示が出された」など、
どんな状態下においても別の状態に遷移させる必要がある場合に、
そのような処理を行うためのものです。

CODE:

class	STATE_StandBy :public State
{
public:

	//なにもしない!
	void	Enter(Unit* owner){}
	void	Update(Unit* owner){}
	void	Exit(Unit* owner){}
};


class	STATE_GoAhead :public State
{
public:
	
	void	Enter(Unit* owner);
	void	Update(Unit* owner);
	void	Exit(Unit* owner);
};


class	STATE_Global :public State
{
public:
	
	void	Enter(Unit* owner){}
	void	Update(Unit* owner);
	void	Exit(Unit* owner){}
};

CODE:

const	double	FullyNearDistSQ = 2.0;

void	STATE_GoAhead::Enter(Unit* owner)
{
	owner->getMotionManager()->flag_Go.ON();
}

void	STATE_GoAhead::Update(Unit* owner)
{
	//目的地との距離
	Vec2D Dist = owner->getMotionManager()->GetTarget()
			    	- owner->Pos();
	
	//目的地に近づいたら、移動を終了する
	if(Dist.LengthSQ() ChangeState(new STATE_StandBy);
	}
}

void	STATE_GoAhead::Exit(Unit* owner)
{
	owner->getMotionManager()->flag_Go.OFF();
	//ユニットの行動を止める。
	owner->Stop();
}






void	STATE_Global::Update(Unit* owner)
{
	//もし右クリックされたら…
	if(GetMouseInput() & MOUSE_INPUT_RIGHT)
	{
		owner->ChangeState(new STATE_GoAhead);

		//マウスの座標を記録
		int mx, my;
		GetMousePoint(&mx, &my);
		//ターゲットを設定
		owner->getMotionManager()->SetTarget(Vec2D(mx,my));
	}
}
これで、ステートマシーンを定義することができました。
あとは、これをユニットに組み込むだけです。

ユニットは、ステートマシーンを所有するというよりは、
ステートマシーンそのものであるといえるので、
UnitにStateMachineを継承させることにします。

CODE:

class	Unit :public GameEntity, public StateMachine
{
    ~省略~
};

CODE:

void	Unit::Update()
{
	//ステートマシーンを動かす
	Execute();

	~省略~
}
最終的な構造
tunagari2.png
tunagari2.png (31.34 KiB) 閲覧数: 117 回
これで、ユニットは右クリックした箇所に動くようになりました。
次からは、ユニットの選択、敵味方の概念等を実装してこうと思っています。

コメントはまだありません。