记一次robocup2d比赛

1. 概述

在前东家参加了一个robocup2d的比赛,感兴趣的可以搜索一下这个比赛。主要是通过代码控制二维足球员踢球对抗。这篇文章记录一下比赛中的资料和感悟。

2. 详述

2.1. 关键问题

  • 无球队员的跑位
  • 截球问题
  • 射门技术
  • 铲球模型
  • 动作决策搜索

2.2. 仿真模型

2.2.1. 感知模型

  • 听觉模型:接受球员、教练喊话,或裁判发送的信息。
  • 视觉模型:视觉范围和视觉质量。
  • 身体感知模型:球员自身(位置、速度、身体方向等)。

2.2.2. 运动模型

  • 全局坐标系:以球场中心为(0,0),右边为x轴正方向,长度为52.5;下边为y轴正方向,长度为34.0。
  • 球员坐标系:以身体方向为y轴正方向。

2.2.3. 动作模型和体能模型

动作包括互斥动作和非互斥动作。

  • 互斥动作包括:Kick、Dash、Turn、Tackle、Catch、Move
  • 非互斥动作包括:Say、AttentionTo、PointTo、TurnNeck、ChangeView、Score、SenseBody

2.2.4. 球员异构模型

18种异构类型,每场server随机11种

2.2.5. 裁判模型

判断出界、角球、越位等情形的判断

  • Before_kick_off 比赛开始之前
  • Play_on 比赛进行中
  • Time_over 比赛结束
  • Kick_off_side 声明比赛开始
  • Kick_in_side
  • Free_kick
  • Corner_kick_side
  • CGoal_kick_side
  • Drop_ball
  • Offside_side
  • Goal_side_n
  • Foul_side
  • Goalie_catch_ball_side
  • Back_pass_side
  • Free_kick_fault_side
  • Time_up_without_a_team
  • Half_time
  • Time_up
  • Time_extented

3. 阵型设计和跑位

3.1. 阵型建模方法

UVA的阵型因子建模、agent2d的Delaunay三角剖分法

3.2. 阵型参数

  • Formation_id 阵型编号
  • Player_num 球员编号,1~11
  • Player_role 球员角色
  • Player_x 球员初始状态x坐标
  • Player_y 球员初始状态y坐标
  • Attr_x 球对球员x坐标的吸引因子
  • Attr_y 球对球员y坐标的吸引因子
  • Behind_ball 是否限制球员位于球的后方
  • X_min 球员x坐标的最小值
  • X_max 球员x坐标的最大值

3.3. 跑位点

1
2
3
x=min(max(Player_x+(Ball_x*Attr_x), X_min), X_max)
if Behind_ball=True Then x=min(x,Ball_x)
y=Player_y+(Ball_y*Attr_y)

其中(Ball_x, Ball_y)表示球的位置

4. 代码解读

4.1. librcsc

1
2
3
4
5
6
7
8
9
10
11
12
rcsc/action         动作类(重点)   
rcsc/ann 人工神经网络类
rcsc/coach 在线教练类
rcsc/common 公共的类
rcsc/formation 一些阵型类(agent2d只使用了DT跑位)
rcsc/geom 一些几何类
rcsc/net 一些与server交换数据的类
rcsc/param 一些参数类
rcsc/player 一些球员类(重点)
rcsc/time 时间类(一般用不到)
rcsc/trainer 一些离线教练类
rcsc/util game_mode math version

4.2. src

  • 以bhv开头的都是在各种情况下的动作执行类(重要)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bhv_basic_move 球员基本跑位
bhv_basic_offensive_kick 基本进攻踢球
bhv_basic_tackle 阻截对手的球(铲球)
bhv_custom_before_kick_off 开球前的习惯(模式)
bhv_go_to_static_ball 跑向静态球
bhv_goalie_basic_move 守门员基本跑位
bhv_goalie_chase_ball 守门员追球
bhv_goalie_free_kick 守门员任意球
bhv_penalty_kick 罚球
bhv_prepare_set_play_kick 准备踢比赛
bhv_set_play_free_kick 设置任意球
bhv_set_play_goal_kick 设置踢球门球
bhv_set_play_indirect_free_kick 设置打间接任意球
bhv_set_play_kick_in 设置踢界外球
bhv_set_play_kick_off 设置开球
bhv_their_goal_kick_move 对手踢球跑位
  • 以role开头的都是角色类
  • 以sample开头的都是示例,可以模仿其结构修改代码
  • 以intertion开头的是意图类
  • 以neck开头的是转脖子动作

4.3. formations-dt

保存了一些阵型的数据

4.4. chain_action

agent2d底层的决策核心

4.5. 球员角色分配

具体代码在“ROLE_角色名”中查看;具体角色划分在strategy.cpp里面;异构球员的顺序在sample_coach.cpp里面;具体的阵型角色文件通常以Begin Roles开始,以End Roles结束。

1
2
3
4
5
6
CenterForward 中锋      (11号)
SideForward 边锋 (9,10号)
SideBack 边后卫 (2,5号)
CenterBack 中后卫 (3,4号)
DefensiveHalf 防守型中场 (6号)
OffensiveHalf 进攻型中场 (7,8号)

4.6. 代码执行流程

  • 由Main()函数开始
  • 首先是一些环境变量设置,启动球员类。
  • 进入BasicClient类中,执行Run()函数。
  • RunOnline()调用PlayerAgent类的 HandleMessage()函数处理获得的信息。
  • HandleMessage()函数调用在PlayerAgent类中的Action()函数进行动作决策和Server参数的解析parse()函数。
  • 在Action()函数中依次执行ActionImpl()函数,DoArmAction()函数,DoViewAction()函数,DoNeckAction()函数以及CommunicationImpl()函数。
  • 其中ActionImpl()函数是主要的决策函数的框架。基于球员在场上的角色(Role)以及场上位置(Home Position),执行相应的Role策略,这种基于角色的策略增加了球员的灵活性,使不同类型的球员具有不同的策略,对于球场动态环境具有更强的自适应性。

5. 示例

(1)bhv_basic_move.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// agent代表一个智能体 球员

// 执行函数 判断BasicMove是否执行
bool Bhv_BasicMove::execute( PlayerAgent * agent )
{
/*Logger类:为了输出玩家代理的动作内容,特别是有关决策的记录,
通过使用Logger,可以在比赛时间的同时将任意消息输出到外部文件,
可以设置日志级别(具体看开发文档)
*/

// dlog是logger.{h,cpp}中宣布的全局变量,是Logger类的实体。
// addText()的第一参数中指定了日志级别的Id
dlog.addText( Logger::TEAM,
__FILE__": Bhv_BasicMove" );//输出TEAM的日志信息
//-----------------------------------------------
// tackle拦截(铲球)
if ( Bhv_BasicTackle( 0.8, 80.0 ).execute( agent ) )
{
return true;
}
const WorldModel & wm = agent->world();
/*--------------------------------------------------------*/
// chase ball 追球-->拦截
// InterceptTable类是根据保存在World Model内的信息,
// 来预测各玩家捕捉球所需的循环数,并将其结果保存起来的

// SelfObject(播放器代理自身)的预测球捕捉周期数。(自己到达球的周期)
const int self_min = wm.interceptTable()->selfReachCycle();

// 我方玩家的预测球捕捉周期数中最小的值。(队友到达球的周期)
const int mate_min = wm.interceptTable()->teammateReachCycle();

// 敌玩家的预测球捕捉周期数中最小的值。(对手到达球的周期)
const int opp_min = wm.interceptTable()->opponentReachCycle();

// bool existKickableTeammate() const;
// 如果队里估计有一个可以踢球的玩家,返回真。
if ( ! wm.existKickableTeammate()
&& ( self_min <= 3
|| ( self_min <= mate_min
&& self_min < opp_min + 3 )
)
)
{

dlog.addText( Logger::TEAM,
__FILE__": intercept" );
// 执行 球员去追逐(截断)球
Body_Intercept().execute( agent );

// 转向函数,猜测是拦截成功后调整转向为进攻方向
agent->setNeckAction( new Neck_OffensiveInterceptNeck() );
return true;
}

// 获取目标点(球)的位置
const Vector2D target_point =
Strategy::i().getPosition( wm.self().unum() );

// 获取冲刺力(冲刺功率)
const double dash_power =
Strategy::get_normal_dash_power( wm );

// 运球时与球的距离
double dist_thr = wm.ball().distFromSelf() * 0.1;

// 调节球与球员之间的距离
if ( dist_thr < 1.0 ) dist_thr = 1.0;

dlog.addText( Logger::TEAM,
__FILE__": Bhv_BasicMove target=(%.1f %.1f) dist_thr=%.2f",
target_point.x, target_point.y,
dist_thr );

// debugClient类 会向调试服务器发送信息作为第一个客户端功能的实现

// 添加球员信息
agent->debugClient().addMessage( "BasicMove%.0f", dash_power );

// 设定球员行动目标位置.一个周期只能使用一次。
agent->debugClient().setTarget( target_point );
agent->debugClient().addCircle( target_point, dist_thr );

if ( ! Body_GoToPoint( target_point, dist_thr, dash_power
).execute( agent ) )
{
// 身体转向球可以联系设置的体力直接冲向球(动作执行)
Body_TurnToBall().execute( agent );
}

if ( wm.existKickableOpponent()
&& wm.ball().distFromSelf() < 18.0 )
{
// Neck_TurnToBall是将视角对向球,把视线投向下一个循环的预测球位置。
agent->setNeckAction( new Neck_TurnToBall() );
}
else
{
// Neck_TurnToBallOrScan是将视角对向球或者进行扫描球的位置,
// 也就是说并不清楚球的位置。
// 当球信息不可靠时,将视线转向下一个循环的预测球位置。
// 当球信息可靠性高时,环视周围。
// 如果存在可以踢除自己以外的球的玩家,则考虑球移动的范围,决定是否注意球。
agent->setNeckAction( new Neck_TurnToBallOrScan() );
}
return true;
}

(2)一些类和函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

BallObject Ball; 球类
AgentObject agentObject; agent类
PlayerObject Teammates[MAX_TEAMMATES]; 球员类,三个数组存放不同的球员
PlayerObject Opponents[MAX_OPPONENTS];
PlayerObject UnknownPlayers[MAX_TEAMMATES+MAX_OPPONENTS];
Time 时间类(int类型属性:周期数,循环次数)
Circle 圆类(圆心,半径)
Line 直线类(包含三个double属性,分别是直线ay + bx + c = 0的三个系数)
VecPosition 坐标信息类(包含两个double属性,分别对应x轴和y轴的坐标值)
ObjectT 枚举类,包括场上可能出现的对象,例如OBJECT_BALL,OBJECT_GOAL_L,球,左侧球门等等

函数
可通过WM-> 调用函数
(1)球的坐标位置
getGlobalPosition( ObjectT o ) 获取o的坐标(返回类型VecPosition)
getBallPos() 获取球的坐标(通过调用getGlobalPosition间接完成)
getBallPos().getX() 获取球的x轴坐标
getBallPos().getY() 获取球的y轴坐标(均为double类型)
getPosOwnGoal() 获取自己的球门位置(返回VecPosition)
getPosOpponentGoal( ) 获取对手球门的位置(返回VecPosition)

(2)球的速度,方向
getGlobalVelocity( ObjectT o ) 获取o的速度(返回类型VecPosition)
getBallSpeed() 获取球的速度(返回值为double类型,通过调用getGlobalVelocity()间接完成)
getBallDirection()获取球的运动方向(返回AngDeg(弧度角),也就是double)

(3)球员,场上的状态
getPlayerNumber() const 获取球员的号码(返回int)
getPlayMode() 获取状态
PlayModeT 枚举类,包含不同的状态,例如PM_BEFORE_KICK_OFF,PM_KICK_OFF_LEFT
getClosestInSetTo这是一个重载函数,根据参数的不同功能的实现也不同

(4)距离,角度
getPosClosestOpponentTo( double *dDist, ObjectT o ) (返回VecPosition)
getRelAngleOpponentGoal() 获取自己与对方球门之间的相对角(返回double)
getRelativeDistance( ObjectT o )获取自己与o之间的相对距离(返回double)
getRelDistanceOpponentGoal() 获取自己与对方球门之间的相对距离(返回double)

(5)判断
isBeforeKickOff( PlayModeT pm ) 判断是否在开球之前(返回bool)
isKickOffUs( PlayModeT pm ) 判断当前的play模式下是否由我方开球
isOffsideUs( PlayModeT pm ) 判断当前的play模式下我方是否越位(返回bool)
isBallKickable() 判断球是否可踢(返回bool)
isBallCatchable() 判断球是否可抓(返回bool,该方法只适用于守门员)
isBallHeadingToGoal( )判断当前球是否朝向我方球门(返回bool)
isBallInOurPossesion( ) 判断球是否在我方手中(返回bool)
isBeforeGoal( VecPosition pos )判断位置参数pos是否在对方球门前(返回bool)

(6)周期
getCurrentTime() 获取当前的时间(返回Time对象)
getCurrentCycle() 获取当前的周期(返回int)

6. 相关开源项目和开发人员

7. 修改方向

  • 改阵型,配合fedit2来调整
  • 动作链
  • 无球跑位策略
  • 评估器,难度最大
  • 角色
  • 其他:原子动作、行为策略、教练

8. 参考文章

9. 总结

比赛给了大家一个月的时间来学习、修改代码,由于种种原因,我们组在前期还取得不错的成绩,但是后期却掉落下来。我们当时大多数的组采用的都是暴力调参,实战对抗,毫无悬念最后也只能在一定范围内提升。动手去修改算法的组还是比较少的。不过有兄弟组采用RL search的方式,使用机器学习模型替代手工调参,最后的确搜索到了很不错的参数组合,最终好像是第一还是第二来着,让人眼前一亮。