Game AI Pro

《星球大战》的绝地武士AI设计

Posted by LudoArt on November 23, 2020

《星球大战》中的绝地武士AI

示例代码可以通过网站 http://www.gameaipro.com 下载demo。

存储

绝地武士AI需要的第一件东西是关于整个游戏世界的内部知识图谱,为此我们需要一个世界状态模型。它包含了在游戏世界中,可以通过动作(Action)操作的所有事物,包括绝地武士自身、绝地武士当前正在攻击的目标、附近的敌人和任何即将到来的威胁等。

它包含了两个函数:simulate()simulateDamage()

  • simulate()函数允许任何动作来更新存储中的部分数据,这是实时改变的;
  • simulateDamage()函数允许任何行为可以简单地模拟对给定敌人的伤害。

以下是绝地AI存储(世界状态)的一个示例,它使得AI可以跟踪当前的世界状态并模拟行为的发生。

class CJediAiMemory {
public:
	//simulate this AI memory over a given timestep
	void simulate(float dt);
	//simulate damage to an actor
	void simulateDamage(float dmg, SJediAiActorState &actor);
	//data about my self’s current state
	struct SSelfState {
		float skillLevel, hitPoints;
		CVector pos, frontDir, rightDir;
	} selfState;
	//knowledge container for world entities
	struct SJediAiEntityState {
		CVector pos, velocity;
		CVector frontDir, rightDir, toSelfDir;
		float distanceToSelf, selfFacePct, faceSelfPct;
	};
	//knowledge container for other actors
	struct SJediAiActorState : SJediAiEntityState {
		EJediEnemyType type;
		float hitpoints;
	};
	//victim state
	SJediAiEntityState *victimState;
	//enemy state list
	enum { kEnemyStateListSize = 8 };
	int enemyStateCount;
	SJediAiActorState enemyStates[kEnemyStateListSize];
	//knowledge container for threats
	struct SJediAiThreatState : SJediAiEntityState {
		EJediThreatType type;
		float damage;
	};
	//threat state list
	enum { kThreatStateListSize = 8 };
	int threatStateCount;
	SJediAiThreatState threatStates[kThreatStateListSize];
};

行为树

行为树上的所有节点,都可以称之为动作(Action),动作提供了标准的行为树节点接口——开始/更新/结束。这些节点返回一个值EJediAiActionResult给他们的父节点,使得父节点可以获取一个动作当前的状态。

返回值——动作结果代码示例。

//jedi ai action results
enum EJediAiActionResult {
eJediAiActionResult_Success = 0,
eJediAiActionResult_InProgress,
eJediAiActionResult_Failure,
eJediAiActionResult_Count
};

动作还提供一个检查约束的方法checkConstraints(),遍历一个列表中的限制条件。限制条件还提供了是否可以在动作的执行期间或者模拟期间被跳过的选项,用于防止动作被打断。

限制条件示例代码。

//base class for all jedi ai constraints
class CJediAiActionConstraint {
public:
	//next constraint in the list
	CJediAiActionConstraint *nextConstraint;
	//don’t check this constraint while in progress
	bool skipWhileInProgress;
	//don’t check this constraint while simulating
	bool skipWhileSimulating;
	//check our constraint
	virtual EJediAiActionResult checkConstraint(
		const CJediAiMemory &memory,
		const CJediAiAction &action,
		bool simulating) const = 0;
};

最后,动作提供了一个模拟的接口。每一个动作都包含了一个模拟摘要(simulation summary,里面含有所有启发式函数所关心的东西。该摘要包含了一个EJediAiActionSimResult的值,这个值由启发式函数计算得出,指出了该动作的可获取性。

//jedi ai action simulation result
enum EJediAiActionSimResult {
	eJediAiActionSimResult_Impossible,
	eJediAiActionSimResult_Hurtful,
	eJediAiActionSimResult_Irrelevant,
	eJediAiActionSimResult_Cosmetic,
	eJediAiActionSimResult_Beneficial,
	eJediAiActionSimResult_Urgent,
	eJediAiActionSimResult_Count
};
//jedi ai action simulation summary data
struct SJediAiActionSimSummary {
	EJediAiActionSimResult result;
	float selfHitPoints, victimHitPoints, threatLevel;
};

现在已经有了所有动作的片段代码,现在将它们一起放在类CJediAiAction中。类CJediAiAction中提供了标准的开始/更新/结束的接口、模拟接口和限制接口。

class CJediAiAction {
public:
	//standard begin/update/end interface
	virtual EJediAiActionResult onBegin();
	virtual EJediAiActionResult update(float dt) = 0;
	virtual void onEnd();
	//simulate this action on the specified memory object
	virtual void simulate(
		CJediAiMemory &simMemory,
		SJediAiActionSimSummary &simSummary) = 0;
	//check my constraints
	virtual EJediAiActionResult checkConstraints(
		const CJediAiMemory &memory, bool simulating) const;
};

组合动作(Composite Action)是所有由子节点组合而成的节点的基类,提供一些通用的接口,用于获取子节点

class CJediAiActionComposite : public CJediAiAction {
public:
	//child actions accessors
	CJediAiAction *getAction(int index);
	virtual CJediAiAction **getActionTable(int *count) = 0;
};

序列动作(Sequence Action)将简单地按顺序运行它底下的所有子动作。如果有任何的子动作执行失败了,则整个序列动作也会失败。在模拟期间,每一个子节点将会使用上一个子节点模拟的世界状态结果作为当前模拟的状态基础进行模拟,当所有的子节点都模拟结束了,整个序列将会对所有的模拟结果进行叠加计算。序列动作还提供一些参数使其可以定制它的操作方式。

class CJediAiActionSequence : public CJediAiActionComposite {
public:
	//parameters
	struct {
		//specify a delay between each action in the Sequence
		float timeBetweenActions;
		//allows the Sequence to loop on completion
		bool loop;
		//allows the Sequence to skip over failed actions
		bool allowActionFailure;
		//specify what action result is considered a failure
		EJediAiActionSimResult minFailureResult;
	} sequenceParams;
	//get the next available action in the sequence, starting with the specified index
	virtual CJediAiAction *getNextAction(int &nextActionIndex);
	//begin the next available action in the sequence
	virtual EJediAiActionResult beginNextAction();
};

选择动作(Selector Action)决定了AI该做什么,不该做什么。选择动作通过给定的世界状态,模拟每个子节点的行为,从而得出其模拟摘要,再通过对比每个动作的模拟摘要,选出最好结果的那个动作行为。

class CJediAiActionSelector : public CJediAiActionComposite {
public:
	//parameters
	struct SSelectorParams {
		//specify how often we reselect an action
		float selectFrequency;
		//prevents the selected action from being reselected
		bool debounceActions;
		//allow hurtful actions to be selected
		bool allowNegativeActions;
		//if results are equal, reselect the selected action
		bool ifEqualUseCurrentAction;//default is true
	} selectorParams;
	//simulate each action and select which one is best
	virtual CJediAiAction *selectAction(CJediAiMemory *memory);
	//compare action simulation summaries and select one
	virtual int compareAndSelectAction(
		int actionCount, CJediAiAction *const actionTable[]);
};

模拟

当我们开始模拟一个动作的时候,首先我们要对当前的世界状态创建摘要。在模拟完成后,我们将创建结果世界状态的摘要,并将此新状态与初始状态的摘要进行比较,计算其可取性。最后的摘要被传递回父操作,行为树将在从一组其他操作中选择此操作时使用。

//condense the specified memory into a summary
void setSimSummaryMemoryData(
	SJediAiActionSimSummary &summary,
	const CJediAiMemory &memory);
//initialize a summary from the specified memory
void initSimSummary(
	SJediAiActionSimSummary &summary,
	const CJediAiMemory &memory)
{
	summary.result = eJediAiActionSimResult_Impossible;
	setSimSummaryMemoryData(summary, memory);
}
//compute the resultant world state summary
void setSimSummary(
	SJediAiActionSimSummary &summary,
	const CJediAiMemory &memory)
{
	summary.result = computeSimResult(summary, memory);
	setSimSummaryMemoryData(summary, memory);
}

规划启发式计算模拟的结果,它代表了AI的当前目标。启发函数通过对动作模拟后的世界状态进行分类,将其分为EJediAiActionSimResult中的某一个值

//determine the result of a simulation by comparing a summary of the initial state to the post-simulation state
EJediAiActionSimResult computeSimResult(
	SJediAiActionSimSummary &summary,
	const CJediAiMemory &memory)
{
	//if we are more hurt than before, the action is hurtful
	//if we are dead, the action is deadly
	if (memory.selfState.hitPoints < summary.selfHitPoints) {
		if (memory.selfState.hitPoints <= 0.0f) {
			return eJediAiActionSimResult_Deadly;
		} else {
			return eJediAiActionSimResult_Hurtful;
		}
	//if our threat level increased, the action is hurtful
	} else if (memory.threatLevel > summary.threatLevel) {
		return eJediAiActionSimResult_Hurtful;
	//if our threat level decreased, the action is helpful
	//if it decreased by a lot, the action is urgent
	} else if (memory.threatLevel < summary.threatLevel) {
		float d = (summary.threatLevel - memory.threatLevel);
		if (d < 0.05f) {
			return eJediAiActionSimResult_Safe;
		} else {
			return eJediAiActionSimResult_Urgent;
		}
	//if victim was hurt, the action is helpful
	} else if (memory.victimState->hitPoints < summary.victimHitPoints) {
		return eJediAiActionSimResult_Beneficial;
	}
	//otherwise, the sim was irrelevant
	return eJediAiActionSimResult_Irrelevant;
}

现在我们已经定义了AI的模拟结果是如何计算的,以下代码展示了实际的模拟步骤SwingSaber动作)。

void CJediAiActionSwingSaber::simulate(
	CJediAiMemory &simMemory,
	SJediAiActionSimSummary &simSummary)
{
	initSimSummary(simSummary, simMemory);
	EJediAiActionResult result;
	for (int i = data.swingCount; i < params.numSwings; ++i)
	{
		//simulate a single swing’s duration
		CJediAiMemory::SSimulateParams simParams;
		simMemory.simulate(kJediSwingSaberDuration, simParams);
		//apply damage to my target
		simMemory.simulateDamage(simMemory.selfState.saberDamage,
			*simMemory.victimState);
		//if my target is dead, I’m done
		if (simMemory.victimState->hitPoints <= 0.0f)
			break;
	}
	setSimSummary(simSummary, simMemory);
}