《游戏人工智能编程》读书总结三

5、体育游戏(简单足球)模拟

一、定义球场、足球类

游戏规则:红队和蓝队,每队四个球员一个守门员。尽可能多进球,踢进球门线就算进球。



    这里很有意思的是:守门员和场上队员每人有一个状态机类,而SocccerTeam也有个状态机类,队员级别有AI,小组级别也实现AI,这就是所谓的分层AI。这类Ai经常用于RTS实施战略游戏中看到分层AI,敌人AI通常在多个层次上实现,比如部队,军队,指挥官。

赛场类SoccerPitch的定义:

class SoccerPitch					//定义赛场类
{
public:
	SoccerBall *	m_pBall;			//包含足球

	SoccerTeam*		m_pRedTeam;		//红队和蓝队
	SoccerTeam*		m_pBlueTeam;

	Goal*			m_pRedGoal;
	Goal*			m_pBlueGoal;

	std::vector<Wall2D>		m_vecWalls;     //边界墙的容器,墙由一个线段和线段的法线组成

	Region*			m_pPlayingArea;		//描述足球场的尺寸,存有声明区域左上角、右下角和中央点位置和一个标记号ID

	std::vector<Region*>	m_Regions;

	bool			m_bGameOn;		//游戏是否正在进行,如果球进了,比赛中断,所有队员回到自己的初始位置中

	bool			m_bGoalKeeperHasBall	//任意方守门员拿了球,该值为真

public:
	SoccerPitch(int cxClient, int cyClient);

	~SoccerPitch();

	void Updater();

	bool Render();
};

    函数SoccerPitch::Update和SoccerPitch::Render在更新和渲染层次的顶部,每一次更新,这些方法都会在游戏主循环中被调用,其他游戏实体对应的Render和Updater也依次被调用。


球门类Goal的定义:

class Goal						  //定义球门类
{
private:
	Vector2D		 m_vLeftPost;
	Vector2D		 m_vRightPost;

	Vector2D		 m_vFacing;        //球门的朝向

	Vector2D		 m_vCenter;

	int			m_iNumGoalsScored;//进球数

public:
	Goal(Vector2D left, Vector2D right) :m_vLeftPost(left), m_vRightPost(right), m_vCenter((left + right) / 2), m_iNumGoalsScored(0) {
		m_vFacing = Vec2DNormalize(right - left).Perp();
	}
	
	inline bool Scored(const SoccerBall*const ball);//球跨过球门线,返回真,并让m_iNumGoalsScored增1
};


足球类的定义:

class SoccerBall : public MovingEntity			   //定义足球类
{
private:
	Vector2D		 m_vOldPos;                //记录上一次更新球的位置

	PlayerBase*		 m_pOwner;                 //持球队员的指针

	const std::vector<Wall2D>&	m_PitchBoundary;   //组成球场边界的墙的引用,用作碰撞检测

	void  TestCollisionWithWalls(const std::vector<Wall2D>& walls);   //检测球是否和墙碰撞

public:
	SoccerBall(vector2D pos, double BallSize, double mass, std::vector<Wall2D>& PitchBoundary) :
		MovingEntity(pos, BallSize, Vector2D(0, 0), -1.0, Vector2D(0, 1), mass, Vector2D(1.0, 1.0), 0, 0), m_PitchBoundary(PitchBoundary), m_pOwner(NULL) {

	}

	void Update(double time_elapsed);

	void Render();

	bool HandleMessage(const Telegram& msg) { return false; }

	void Kick(Vector2D direction,double force);    //给定一个踢球的力

	double TimeToCoverDistance(Vector2D from, Vector2D to, double force)const;    //计算球经过这段距离需要花多久

	Vector2D FuturePosition(double time) const;       //计算一段时间后球的位置

	void Trap(PlayerBase* owner) { m_vVelocity.Zero(); m_pOwner = owner; }

	Vector2D OldPos() const { return m_vOldPos; }

	void PlaceAtPosition(Vector2D NewPos);

};

其中如下函数需要用到一些物理知识:

1、SoccerBall::FuturePosition

计算未来时刻球的位置,球受到地面摩擦力的减速度。使用公式:


2、SoccerBall::TimeToCoverDistance

给定两个位置A和B,以及一个踢球力,求出足球花费的时间



二、设计AI

    在游戏中分为两类足球运动员:场上队员和守门员,都继承于PlayerBase,都用到了第三章学到的SteeringBehaviors类,都有独立的有限状态机。


SoccerTeam类:

    包含组成球队队员的实例,指向场地、对方球队,自己球门,对方球门的指针,指向场上关键队员的指针,在private中的关键队员指针包括m_pReceivingPlayer(接球队员)、m_pPlayerClosestToBall(离球最近的队员)、m_pControllingPlayer(正在控球队员)、m_pSupportingPlayer(接应队员,他企图移动到前场最有利的位置)

计算最佳接应点:

    SupportSpotCalculator类通过从对方半场采样的接应点计分来计算BSS(最佳接应点),


如上图,A队员拿到球,他要将球传给接应队员,那么这个接应队员跑到哪个点比较好捏?

    答:应当将采样点选在右半场,尽量定位在接近对方球门地方。用一个累计的积分,分数高的位置就是最佳接应点BSS,接应队员移到BSS位置,准备接球。

那么如何计算这个积分捏?

    答:采用多个方式叠加来评判。

    方式一:能安全传球的位置就是好位置,该方式权值设为2,如下图大一些的点点代表能安全(距离近)传球的点


    方式二:能直接射门的点是好位置,这意味着越接近球门越是好位置,下图大一点的点点是好位置,该方式权值设为1


    方式三:和接应队员保持合适距离的位置是好位置,这意味着,距离接应队员太近的点不能充分发挥传球功效,太远造成传球有风险。权重设为2,如下图:


将这些因素权重叠加找到最佳位置,接应队员跑过去即可:计算最佳点的源码如下:


SoccerTeam球队的状态:

之前曾提到SoccterTeam有个状态机,任何时刻球队有三种状态:防守(Defending)、进攻(Attacking)、准备开球(PrepareForKickOff)。下面分别讲解每个状态:

1、PrepareForKickOff状态

进球后立刻进入这个状态。

该状态下的Enter方法将所有关键队员的指针为NULL,改变他们的初始位置为开球位置,给每个队员发送消息,请求他们回到初始位置

该状态下的Execute方法,需要所有队员都回到初始位置后,才能切换到防守状态,比赛重新开始

2、Defending状态:

该状态的enter方法让所有队员位置回到自己半场,靠近己方球门。

Defending状态的Execute方法判断球队是否获得球的控制权,一旦获得球,状态变为Attacking

void Defending::Execute(SoccerTeam* team) {
	//如果获得球,就改变状态
	if (team->InControl())
	{
		team->ChangeState(team, Attacking::Instance()); __cpp_init_captures;
	}
}

3、Attacking状态:

Enter方法和Defending原理一样,让两个队员在前半场,两个队员在后半场。只是Execute方法中,遍历所有队员,看谁能为进攻队员提供接应位置。让接应队员移动到最佳接应位置。


场上队员状态:

场上队员是场上跑动,传球,射门的人。通过FieldPlayer实例化。

场上队员的移动,通过调用SteeringBehaviors类中的Arrive或seek行为移动到目标位置。

场上队员的状态包括:

1、GlobalPlayerState(全局队员状态)

2、Wait(等待)

3、ReceiveBall(接球)

4、cKickBall(踢球)

5、Dribble(带球)

6、ChaseBall(追球)

7、ReturnToHomeRegion(回位)

8、SupportAttacker(接应)

切换状态通过以下两个方式:

1、状态逻辑本身

2、一名队员收到另一个队员的信息


GlobalPlayerState(全局状态)

    主要目的是成为一个消息路由器,虽然队员的许多行为可以在状态逻辑中实现,但是通过消息系统实现队员协作也是需要的,比如,接应队员发觉自己处在一个有利的位置,请求队友传球,为了方便通信,使用之前讲到的消息系统。

    在该游戏中message分为五种:

1、Msg_SupportAttacker(接应消息)

2、Msg_GoHome(返回初始位置消息)

3、Msg_ReceiveBall(接球消息)

4、Msg_PassToMe(传球给我消息)

5、Msg_Wait(等待消息)


ChaseBall状态


Wait状态



ReceiveBall状态



后面的几种状态不赘述了


守门员:

    用一个单独的类GoalKeeper实现,红队守门员分配到区域16,蓝队守门员分配到区域1,

守门员状态:

1、GlobalKeeperState(全局守门员状态)

2、TendGoal(守球门)

3、ReturnHome(回到初始位置)

4、PutBallBackInPlay(把球传回到赛场中)

5、InterceptBall(截球)

原理和队员的状态类似,不赘述


AI使用到的关键方法:

方法一、SoccerTeam::isPassSafeFromAllOpponents 函数

用来判断从A到B的传球过程中对方队员是否有可能把球截走

该方法传入参数为:传球的开始位置,终止位置,对方队员指针,接球队员指针,踢球力。

第一步,以AB为x轴,垂直为y轴建立局部空间坐标系,如果对手在踢球者后面,那么认为可以传球。即为W在A的后面

同时,踢出球的时刻会计算球的到达位置,该位置距离接球者位置<该位置距离对手位置,该对手排除,可以传球。



对手是否能截到球:

    对手想要截到球,需要跑到球的轨迹和自身位置垂直相交点,而且是在球经过该点之前到达。


通过SoccerBall::TimeToCoverDistance计算球从A滚到Yp需要的时间,再计算这段时间对手Y能移动多远



猜你喜欢

转载自blog.csdn.net/zhangxiaofan666/article/details/80556796