博客园仿真足球竞赛平台的物理模型以及球员命令的分析
在本文中,我将向大家介绍平台的物理模型(如场地的规格,角度的计算标准等等)以及球队的命令的分析(5种命令如何使用,效果如何)。
我们可以根据下面这个图片来了解平台的比赛场地模型。
平台的左上角是场上坐标的原点(0,0),在原点的右边是X轴的正方向,原点的下方是Y轴的正方向。
角度的计算是按照上图中的r角的方向来的。(顺时针方向为正方向)
被蓝色标记的区域是禁区(左边的没有标记出来)。
在实际的编程中,我们可以通过调用CnblogsDotNetSDK.Settings.FieldSettings获得下面的一系列代表球场上规格的具体数值:
/// <summary> /// 比赛场地的长度 /// </summary> public double FieldLength; /// <summary> /// 比赛场地的宽度 /// </summary> public double FieldWidth; /// <summary> /// 禁区的长度 /// </summary> public double FieldPNZLength; /// <summary> /// 禁区的宽度 /// </summary> public double FieldPNZWidth; /// <summary> /// 中场圆的半径 /// </summary> public double FieldMidCircleRadius; /// <summary> /// 球门的宽度 /// </summary> public double FieldDoorWidth; /// <summary> /// 球门的Y坐标(较小的) /// </summary> public double FieldDoorY1; /// <summary> /// 球门的Y坐标(较大的) /// </summary> public double FieldDoorY2; /// <summary> /// 比赛场地的左顶点位置 /// </summary> public Vector2f FieldLeftTopPos /// <summary> /// 比赛场地的右底点位置 /// </summary> public Vector2f FieldRightBottomPos /// <summary> /// 比赛场地的矩形区域 /// </summary> public Rectangle Field /// <summary> /// 比赛场地的左半场区域 /// </summary> public Rectangle LeftField /// <summary> /// 比赛场地的右半场区域 /// </summary> public Rectangle RightField /// <summary> /// 比赛场地的左禁区区域 /// </summary> public Rectangle LeftPNZ /// <summary> /// 比赛场地的右禁区区域 /// </summary> public Rectangle RightPNZ
下面,我们来看看球员可以执行的命令。
Stay:执行该命令的球队本周期不会作出反应(相当于无命令)
平台实现代码:
/// <summary> /// 执行Stay命令,这是一个空命令,什么也不做,Just stay;) /// </summary> /// <param name="wm">场上的信息</param> /// <param name="command">命令参数</param> /// <param name="num">执行该命令的球员编号</param> /// <returns>命令是否执行成功</returns> public bool Perform(WorldModel wm, Command command, int num) { return true; }
Turn:执行该命令的球员身体朝向指定的方向(由Command.Parameter1指定),该命令的有效转身角度在[-180,180]之间。所以在一个周期内,你能转向任何你需要的角度。注意:这里是希望转身以后的角度,而不是转身的角度。
平台实现代码:
/// <summary> /// 执行Turn命令。 /// 这个命令中只有Command.parameter1是有效的,代表该球员希望转身以后的角度。 /// </summary> /// <param name="wm">场上的信息</param> /// <param name="command">命令参数</param> /// <param name="num">执行该命令的球员编号</param> /// <returns>命令是否执行成功</returns> public bool Perform(WorldModel wm, Command command, int num) { //确保转身后的角度在有效的范围内 double turnAng =AngleHelper.NormalizeAngle(command.Parameter1); wm.agents[num].Dir = turnAng; return true; }
Dash:执行该命令的球员将在当前身体朝向的方向中前进或者后退一定的距离(由Command.Parameter1指定),如果希望前进,则有效的距离在[0, RuleSettings.Instance.MaxDashVel]之间,如果希望后退(给的值是负数),则有效的距离在[RuleSettings.Instance.MinDashVel,0]之间。注意:这里移动的距离方向按照球员当前的身体朝向来的,这也是为什么要有Turn命令的原因。
平台实现代码:
/// <summary> /// 执行Dash命令。可以让指定的球员在一个周期内向前或向后移动一定的距离 /// 这个命令中只有Command.parameter1是有效的,代表该球员希望获得的速度。 /// </summary> /// <param name="wm">场上的信息</param> /// <param name="command">命令参数</param> /// <param name="num">执行该命令的球员编号</param> /// <returns>命令是否执行成功</returns> public bool Perform(WorldModel wm, Command command, int num) { double dashVel = command.Parameter1; //确保球员的速度不越界 if (dashVel > RuleSettings.Instance.MaxDashVel) { dashVel = RuleSettings.Instance.MaxDashVel; } else if (dashVel < RuleSettings.Instance.MinDashVel) { dashVel = RuleSettings.Instance.MinDashVel; } Vector2f dashPos = new Vector2f(dashVel, wm.agents[num].Dir, true) + wm.agents[num].Pos; //球员出了球场,计算反弹后的位置 if (!FieldSettings.Instance.Field.IsInside(dashPos)) { Line l = Line.MakeLineFromPositionAndAngle(wm.agents[num].Pos, wm.agents[num].Dir); Line[] ls = new Line[4]; ls[0] = Line.MakeLineFromTwoPoints(FieldSettings.Instance.FieldLeftTopPos, new Vector2f(FieldSettings.Instance.FieldLength, 0)); ls[1] = Line.MakeLineFromTwoPoints(new Vector2f(0, FieldSettings.Instance.FieldWidth), FieldSettings.Instance.FieldRightBottomPos); ls[2] = Line.MakeLineFromTwoPoints(FieldSettings.Instance.FieldLeftTopPos, new Vector2f(0, FieldSettings.Instance.FieldWidth)); ls[3] = Line.MakeLineFromTwoPoints(new Vector2f(FieldSettings.Instance.FieldLength, 0), FieldSettings.Instance.FieldRightBottomPos); double minDis = 9999; Vector2f newPos = new Vector2f(wm.agents[num].Pos); foreach (Line line in ls) { Vector2f collidePos = l.GetIntersection(line); if (collidePos.GetDistanceTo(wm.agents[num].Pos) < minDis) { newPos = collidePos; minDis = collidePos.GetDistanceTo(wm.agents[num].Pos); } } wm.agents[num].Pos = newPos; return false; } else { wm.agents[num].Pos = dashPos; return true; } }
Kick:执行该命令的球员将给足球施加一个速度(Vector2f)。速度的大小由Command.Parameter1决定,有效值的范围在[RuleSettings.Instance.MinKickBallVelocity,RuleSettings.Instance.MaxKickBallVelocity]之间,速度的方向由Command.Parameter2决定,有效的范围在[RuleSettings.Instance.MinKickBallAngle,RuleSettings.Instance.MaxKickBallAngle]之间。注意:球和球员之间的距离必须不大于RuleSettings.Instance.MaxKickBallDistance,如果多个人同时踢球,则给球的速度取这些速度的和(初中的2维向量相加)。
平台实现代码:
/// <summary> /// 执行Dash命令。可以让指定的球员在本周期给足球添加一个速度 /// Command.parameter1 代表速度的大小 /// Command.parameter2 代表速度的方向 /// </summary> /// <param name="wm">场上的信息</param> /// <param name="command">命令参数</param> /// <param name="num">执行该命令的球员编号</param> /// <returns>命令是否执行成功</returns> public bool Perform(WorldModel wm, Command command, int num) { //如果球员于足球的距离大于MaxKickBallDistance,则踢球失败 if (wm.agents[num].Pos.GetDistanceTo(wm.ball.Pos) > RuleSettings.Instance.MaxKickBallDistance) { return false; } double kickVelocity = command.Parameter1; //确保踢球的角度在有效的范围内 double kickAngle = AngleHelper.NormalizeAngle(command.Parameter2); //确保踢球的速度在有效的范围内 if (kickVelocity > RuleSettings.Instance.MaxKickBallVelocity) { kickVelocity = RuleSettings.Instance.MaxKickBallVelocity; } else if (kickVelocity < RuleSettings.Instance.MinKickBallVelocity) { kickVelocity = RuleSettings.Instance.MinKickBallVelocity; } wm.ball.Vel += new Vector2f(kickVelocity, kickAngle, true); return true; }
Catch:执行该命令的球员将球的速度变为0,同时将在自己禁区中的非本队的球员朝X方向移除自己的禁区。注意:执行该命令的球员必须是1号,足球和球员直接的距离不能大于RuleSettings.Instance.MaxCatchBallDistance,并且球员在自己的禁区扑球一次以后,必须等球出了自己的禁区才能再次使用该命令。
平台实现代码:
/// <summary> /// 扑球的命令类,可以让守门员在本周期把足球停止住,同时让对方的球员移出自己边的禁区 /// 球员在自己的禁区扑球一次以后,必须等球出了自己的禁区才能再次使用该命令 /// </summary> /// <param name="wm">场上的信息</param> /// <param name="command">命令参数</param> /// <param name="num">执行该命令的球员编号</param> /// <returns>命令是否执行成功</returns> public bool Perform(WorldModel wm, Command command, int num) { //只有守门员可以执行这个命令 if ((num % RuleSettings.Instance.AgentNum + 1) != RuleSettings.Instance.CatchNum) { return false; } //判断是否连续扑球 if ( (num < RuleSettings.Instance.AgentNum && !CanLeftTeamCatch) || !CanRightTeamCatch) { return false; } //判断扑球的距离是否合理 if (wm.ball.Pos.GetDistanceTo(wm.agents[num].Pos) > RuleSettings.Instance.MaxCatchBallDistance) { return false; } Rectangle PNZ; int startIndex; bool left = false; if (num < RuleSettings.Instance.AgentNum) { startIndex = RuleSettings.Instance.AgentNum; PNZ = FieldSettings.Instance.LeftPNZ; left = true; } else { startIndex = 0; PNZ = FieldSettings.Instance.RightPNZ; left = false; } //球必须在禁区中 if (!PNZ.IsInside(wm.ball.Pos)) { return false; } //让球停止运动 wm.ball.Stop(); //将对方的球员移除自己的禁区 for (int i = startIndex; i < startIndex + RuleSettings.Instance.AgentNum; i++) { if (PNZ.IsInside(wm.agents[i].Pos)) { if (left) { wm.agents[i].Pos = new Vector2f(PNZ.PosRightBottom.X, wm.agents[i].Pos.Y); } else { wm.agents[i].Pos = new Vector2f(PNZ.PosLeftTop.X, wm.agents[i].Pos.Y); } } } //不能连续扑球的设置 if (left) { CanLeftTeamCatch = false; } else { CanRightTeamCatch = false; } return true; }
以上就是球队的运动模型和命令分析。
最后一项是足球的运动模型:
足球下一个周期的位置 = 这一个周期的位置 + 这一个周期的速度
足球下一个周期的速度 = 这一个周期的速度 * 速度的衰减系数(RuleSettings.Instance.BallDelayRate)
平台的实现代码如下:
/// <summary> /// 让足球在当前的速度下运动一个周期 /// </summary> /// <returns>返回球的状态,通过球的状态,可以知道是否进球。</returns> public void DoMove() { //如果球的速度过大,则让球的速度大小变为BallMaxVelocity,方向不变 if (_vel.GetLength() > RuleSettings.Instance.BallMaxVelocity) { _vel.SetLength(RuleSettings.Instance.BallMaxVelocity); } Vector2f newPos = _pos + _vel; //球到了左边球门中,右方进球 if (newPos.X <= 0 && newPos.IsBetweenY(FieldSettings.Instance.FieldDoorY1, FieldSettings.Instance.FieldDoorY2)) { _pos = newPos; _state = BallState.RightGoal; return; } //球到了右边球门中,左方进球 else if (newPos.X >= FieldSettings.Instance.FieldLength && newPos.IsBetweenY(FieldSettings.Instance.FieldDoorY1, FieldSettings.Instance.FieldDoorY2)) { this.Pos = newPos; _state = BallState.LeftGoal; return; } //球出了球场,计算反弹后的位置和速度(位置和速度取反) if (!FieldSettings.Instance.Field.IsInside(newPos)) { if (newPos.X < 0) { newPos.X = -newPos.X; _vel.X = -_vel.X; } else if (newPos.X > FieldSettings.Instance.FieldLength) { newPos.X = FieldSettings.Instance.FieldLength - (newPos.X - FieldSettings.Instance.FieldLength); _vel.X = -_vel.X; } if (newPos.Y < 0) { newPos.Y = -newPos.Y; _vel.Y = -_vel.Y; } else if (newPos.Y > FieldSettings.Instance.FieldWidth) { newPos.Y = FieldSettings.Instance.FieldWidth - (newPos.Y - FieldSettings.Instance.FieldWidth); _vel.Y = -_vel.Y; } } //设置球的位置 _pos = newPos; //球的速度衰减 _vel *= RuleSettings.Instance.BallDelayRate; if (!FieldSettings.Instance.LeftPNZ.IsInside(_pos)) { CatchCommand.CanLeftTeamCatch = true; } else if (!FieldSettings.Instance.RightField.IsInside(_pos)) { CatchCommand.CanRightTeamCatch = true; } }
最后,我们可以看看这些命令究竟是如何影响球员和足球的:
在下一篇文章中,我们将讲解整个比赛的执行流程。