簡単なAIを組み込みたいと思います。
まず、ユニットに"最大の力"と"最大速度"の要素を加えます。
class Unit :public GameEntity
{
…省略…
//最大の力
double m_MaxForce;
//最大速度
double m_MaxSpeed;
…省略…
};
これはつまり、ユニットに与える力を計算するクラスです。
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; }
};
各種の”移動”関数はフラグのオン・オフで制御され、
フラグがオンである移動のみが、力として加えられます。
(いろいろあって、フラグはわざわざMyFlag構造体で管理していますが、中身はboolです)
//最終的な力の計算
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();
}
それを、大きさが最大の力になるよう調整します。
(正規化ベクトルを得て、"最大の力"倍する。)
最終的な力の計算では、
各移動関数が有効かどうかを調べ、有効ならば力に加えます。
力が最大力を越えた場合、計算を終了し、力を返します。
よって、UnitのUpdate関数は次のようになります。
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;
}
移動をさせる機構がまだ出来ていません。
プレイヤーはユニットに指示を与えるだけで、
実質的な行動に干渉することはありません。
つまり、各々のユニットは自律的に行動し、
その場の状況に応じて、移動などを行う必要があります。
つまり、ユニットひとつひとつにAIの機構が必要ですので、
これを実装することにします。
このプログラムでは、AIのシステムとして「有限オートマトン」を組み込みます。
有限オートマトンは、別名で有限ステートマシンともいい、
これを日本語で書き表すと「有限状態機械」といいます。
いまいちピンとこない単語ですが、
簡単にいうと、「状態」と「遷移」からなる「振る舞いのモデル」です。
ゲーム内のユニットで具体的に説明すると、
ユニットは常に"状態"をもっており、
「待機」という状態で、プレイヤーが移動の命令をだせば、
「待機」から「移動」に変わり、目的地に着いたら再び「待機」に変わります。
これを、例えば「待機」の状態で敵が近づいたら「攻撃」に遷移するなど、
"状態"の種類を拡張することで、AIの開発が楽になります。
文章で書くと複雑かつ難解なものに思えるかもしれませんが、
プログラム自体は割と単純で、
デザインパターンの「State」がほとんどそのまま適用できます。
コードにしてみます。
”状態”を表す"State"クラスと、それを所有する"StateMachine"クラスを設計します。
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);
}
};
ユニットのステートとして、「待機」と「移動」、
そしてグローバルステートを実装します。
グローバルステートは、あらゆる状態においても更新されるもので、
例えば「敵が接近した」、「別の指示が出された」など、
どんな状態下においても別の状態に遷移させる必要がある場合に、
そのような処理を行うためのものです。
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){}
};
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を継承させることにします。 最終的な構造 これで、ユニットは右クリックした箇所に動くようになりました。
次からは、ユニットの選択、敵味方の概念等を実装してこうと思っています。