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 m_vecWalls; //边界墙的容器,墙由一个线段和线段的法线组成
Region* m_pPlayingArea; //描述足球场的尺寸,存有声明区域左上角、右下角和中央点位置和一个标记号ID
std::vector 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& m_PitchBoundary; //组成球场边界的墙的引用,用作碰撞检测
void TestCollisionWithWalls(const std::vector& walls); //检测球是否和墙碰撞
public:
SoccerBall(vector2D pos, double BallSize, double mass, std::vector& 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,如下图:
将这些因素权重叠加找到最佳位置,接应队员跑过去即可:计算最佳点的源码如下:
之前曾提到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能移动多远