作为一个比赛,我们的比赛任务是在模拟的地震火灾现场火势蔓延和建筑倒塌的恶劣环境下,
救援人员包括警察(蓝色),救护员(白色),消防员(红色)
普通市民的颜色为绿色
各自的任务:
对人类而言,有以下几个常用的重要属性,罗列如下:
属性名 | 含义 |
---|---|
buriedness | 掩埋深度,埋得越深,需要的救援时间越久 |
damage | 每个周期损失了多少生命值 |
HP | 生命值,初始为10000,受伤的话则会降低,到达避难所则会保持不变,不会上升 |
position | 记录当前实体所在地的EntityID,可能是在建筑内、道路上、避难所内、救护员身上 |
X、Y | 当前实体所在位置的笛卡尔坐标 |
区域包括两种类型:建筑和道路
一座建筑有一个EntityID值,而道路则不同,一段道路可能是有多个道路段拼接而成,道路段形状不一定一样,形状不规则
一些属性:
属性名 | 含义 |
---|---|
blockades | 该区域的障碍的list,因为路障形状不规则,可能一个area内有多个路障 |
edges | 该区域边界线的列表,一个区域是一个多边形,有边界的存在,edges存储的便是这些边(项目里有一个Edge类)的List |
neighbours | 存储的是与当前区域相邻的Area们,可供之后操作 |
X、Y | 区域中心所在位置的笛卡尔坐标 |
路障是对比赛成绩影响极大地一个方面,我们需要尽快安排警察将主干道路清理通畅,这样市民、消防、救护员的动作才能正常执行。
清理路障主要有两种方式:区域清理和矩形框清理方式
区域清理: 警察对目标路障执行清理动作,路障开始以一定速度逐渐向中心收缩变小,最终消失(耗时很久)
矩形框清理 对当前目标路障,警察行进前方生成一个矩形框,在矩形框范围内的障碍会消失,没有被矩形框覆盖的障碍则不会变化(速度相对很快)
代码策略:
仿真初期: 由于此时的主要目标是尽快将各大主干道疏通,让消防能尽快前往着火建筑灭火、救火员尽快前去救人,所以我们采用矩形框清障方式,疏通主干道路
仿真中后期:由于此时大部分的道路已经可以通行了,但是由于矩形框清障时可能会在路上留下毛刺型障碍,可能还是会卡住其余人类,此时我们将清障方式改为区域清障,保证每条道路障碍被完全清理干净
关键问题在于:如何判断什么时候开始转换为区域清障方式,这也是亟待我们解决的问题
属性名 | 含义 |
---|---|
temperature | 掩埋深度,埋得越深,需要的救援时间越久 |
damage | 每个周期损失了多少生命值 |
HP | 生命值,初始为10000,受伤的话则会降低,到达避难所则会保持不变,不会上升 |
position | 记录当前实体所在地的EntityID,可能是在建筑内、道路上、避难所内、救护员身上 |
X、Y | 当前实体所在位置的笛卡尔坐标 |
几种特殊的不会着火的建筑:
名称 | 作用 |
---|---|
避难所 | 人类实体在里面生命值不会降低,消防员们可以在里面补水,救护员会将受伤市民移动到此处 |
救护中心 | 实现救护员的总体统筹管理 |
消防中心 | 实现消防员的总体统筹管理 |
警察局 | 实现警察的总体统筹管理 |
消防栓 | 水量无限,数量较多,随机分布在地图内,同一时间仅可以供一个消防员补水 |
汽油站 | 重点关照目标,如果火势蔓延到汽油站的话会导致汽油站爆炸,瞬间汽油站周围的建筑都会剧烈着火** |
一般的,每场比赛的仿真周期为300,300周期结束之后仿真器停止工作
这是非对抗性比赛,所以比赛结果按照谁跑的分高来评判。
比赛的时候一支队伍需要跑很多张图,以总的积分来决定谁更优秀。
比赛得分的计算方法:
由上面的表达式可知:
Kernel 是整个系统的核心模块。Kernel 负责调度各个模块之间的通信,转发他们之间发送的消息,管理协调各个模块。
仿真每个仿真周期分为两部分:
前半周期 Sub-simulator 发来各种仿真数据,Kernel 对这些数据进行分析和计算并发送给 GIS 和 Viewer,Viewer 会进行更新;
后半周期用于与 Client 端的Agent 进行交互,等待 Agent 动作命令响应。Kernel 必须在后半周期内接收到 Client 端发来
的动作命令消息,否则该动作将不会被执行。
Sub-simulator 模块模拟现实灾害情况,如房屋倒塌、建筑物起火等,同时响应救援队伍的救援行为。
Sub-simulator 主要由火灾仿真器、交通环境仿真器、道路阻塞仿真器、建筑倒塌仿真器和其他仿真器等构成。
各个仿真器根据自身内部的状态信息以及从 Kernel 接收到的信息和命令来计算世界模型将要发生的事件,其中包括 Agent 的各种动作以及它们的动作带来的环境变化,然后把计算结果反馈回 Kernel。
Viewer 动态地映射仿真系统内发生的所有仿真行为动作和事物变化,通过 2D 图形界面形式直观、清晰地表现出系统世界模型的运动发展。建筑物坍塌和着火燃烧、道路阻塞、市民被掩埋和受伤以及各种异构智能体的不同救援行为
Viewer 界面随着每个仿真周期而更新一次。
智能体的感知器用于获取自身以及环境信息,是做决策的核心部分。Agent 通过感知器获取自己所处的位置信息、建筑物信息、道路信息等。
为提高模拟仿真的真实度,感知器从Kernel 获得的信息都是带有噪音的,并且所有感知能力都受到距离的限制:视觉范围一般为 10米,而听觉范围一般为 30 米。
下图是仿真系统中 Viewer 表现下的 Agent 视线情况。在仿真系统实际运行时,Agent 的视觉、听觉感知范围都会受到物体阻挡等因素影响而出现不同程度上的浮动。
以下列出救援仿真系统运行过程中各模块之间消息传递步骤,救援仿真系统的运行就是这些步骤的不断循环迭代。
可移动救援智能体在灾难空间中可以自由移动,在执行救援任务时,它们需要移动到特定目标地点。智能体在灾难空间中的移动需要某种路径规划方法来实现从当前所处位置到目的地的移动,同时满足路径最短即耗时最短的要求。
我们项目代码中是用的是A*寻路算法,采用启发式搜索算法来寻找最优路径。
路径规划具体流程为:以智能体当前所处位置为起始点,以目的地为终点,每一条路径中所经过所有道路的长度之和为评价依据。 如果路径中存在有路障的道路,该路径的长度将变为无穷大,下一轮路径搜索时将不再考虑该路径。所有路径计算完成后按照评价值进行从小到大排序,智能体使用长度最短的路径作为从当前所处位置到目的地的路径。通过这种动态的启发式搜索方法,智能体能够及时找到一条到达目的地的最短通路。
EntityID - 是仿真平台对于当前世界中所有东西进行的编号,比如当前智能体编号为001,前面那个建筑编号为8888,编号的目的是便于区分和使用
-AgentInfo 智能体自身世界观 主要封装了一些与智能体相关的函数,可以返回智能体的信息
包括:
1.设置以及返回自身记录的当前时间周期
2.返回当前智能体的entityID即编号、在地图中的X、Y坐标、自身所在地区的编号
3.听到的信息,看见的东西
4.自身的一些属性,比如自己是否有灭火能力、当前水量、当前身上背的人的编号
WorldInfo 世界模型 主要封装了一些与地图属性相关的函数,可以返回地图的具体的信息
1. public StandardEntity getEntity() 根据函数名来判断,此函数是传入一个编号,返回对应的实体
2 public Collection getEntitiesOfType() 此函数是传入类别,返回对应的实体集合,包括
ROAD BLOCKADE BUILDING REFUGE HYDRANT GAS_STATION
三种指挥中心 市民 三种智能体 等
3. public Collection getObjectIDsInRange 返回所给区域内的实体ID
4.public Collection getFireBuildings 返回着火建筑的集合
5.public Collection getBuriedHumans 传入某建筑,返回建筑内被掩埋的人类集合
6.public Collection getBlockades 返回某条路上的所有路障的集合
7.public StandardEntity getPosition 返回某东西所在位置的实体
…
剩下的函数可以自己去探索,列出的函数都是之后会经常用到的,调用时注意观察传入参数的返回类型以及函数作用,这就是我们编写代码的一般思路---对于想实现的某个功能,先搜索看看是否已经有相关的函数存在了,通过调用函数以及编写辅助代码,这样才能较快地写出稳健可运行的代码
ScenarioInfo 场景模型,主要返回的是场景相关的一些属性,用的较少
目前我们可以对代码进行优化的部分对应的位置在src-adf.sample 包下面
下面以extaction 下面的 ActionExtClear 作为例子,简单讲下一些基础的、通用的知识
一些变量的初始化
private PathPlanning pathPlanning;//路径规划
private int clearDistance;//清理过的路径长度
private int forcedMove;//强制移动,如果警察在某一区域停留的时间过长,则代码强制让其前往下一个目标
private int thresholdRest;//需要休息时的临界值
private int kernelTime;//内核时间 即1-300个仿真周期
private EntityID target;//本类所要得到的最终结果-清障目标
private Map<EntityID, Set<Point2D>> movePointCache;//移动时经过的位置编号、中心X、Y坐标所形成的map
private int oldClearX;//上一个周期的位置的X坐标
private int oldClearY;//上一个周期的位置的X坐标
private int count;//计数变量
初始化函数
public ActionExtClear(AgentInfo ai, WorldInfo wi, ScenarioInfo si, ModuleManager moduleManager, DevelopData developData)
关于这几个Info 之前已经讲过了
//预计算,预计算后代码跑的更好
public ExtAction precompute(PrecomputeData precomputeData)
//直接开始 无预计算
public ExtAction resume(PrecomputeData precomputeData)
//准备好了的
public ExtAction preparate()
//每周期更新一下仿真平台的信息
public ExtAction updateInfo(MessageManager messageManager)
以上这四行代码基本在每一个类里面都有,有初始化功能之意,大家不用纠结,可以暂时不予修改
然后是
public ExtAction calc()
calc是一个类中最为重要的函数,其计算结果直接或间接影响仿真结果的好坏,注意在分析代码的时候多多看看 if 结构的{ 和 }两个括号的匹配
public ExtAction calc() {
this.result = null;
//policeForce 即为我自己
PoliceForce policeForce = (PoliceForce)this.agentInfo.me();
//注意if结构出现了
//如果我需要休息,我先把我的目标存储一下,然后调用calcRest函数进行动作的计算
// private Action calcRest(Human human, PathPlanning pathPlanning, Collection targets)
//calcRest 这个函数字面意思是在我休息前进行一次计算,参数为 人类、路径规划方式类、警察的目标们 所以在调用时 人类就是我自己 路径规划方式类即本类的属性this.pathPlanning,目标为我之前存储的目标
if(this.needRest(policeForce)) {
List<EntityID> list = new ArrayList<>();
if(this.target != null) {
list.add(this.target);
}
this.result = this.calcRest(policeForce, this.pathPlanning, list);
//如果我计算结果不为空,表明我有一个好的动作可以去做了,于是return, 跳出calc,终止计算(一次计算只返回一次结果)
if(this.result != null) {
return this;
}
}
//如果我自身没有目标要去做,早点renurn 吧,计算也算不出什么结果的
if(this.target == null) {
return this;
}
//找到我自己所在位置的编号、目标的实体、我所在位置的实体
EntityID agentPosition = policeForce.getPosition();
StandardEntity targetEntity = this.worldInfo.getEntity(this.target);
StandardEntity positionEntity = Objects.requireNonNull(this.worldInfo.getEntity(agentPosition));
//目标位置实体为空或者不是区域 - 无法操作,早点返回,节省时间
if(targetEntity == null || !(targetEntity instanceof Area)) {
return this;
}
//如果我在路上,就返回清障目标,目标非空就去吧,return 不再计算
if (positionEntity instanceof Road) {
this.result = this.getRescueAction(policeForce, (Road) positionEntity);
if (this.result != null) {
return this;
}
}
//如果我与目标在同一区域了,则清理区域内的障碍(getAreaClearAction)
if(agentPosition.equals(this.target)) {
this.result = this.getAreaClearAction(policeForce, targetEntity);
}
//如果目的地和我所在地有边相连,说明我们相邻了,调用有邻边相连区域的清障函数
else if(((Area)targetEntity).getEdgeTo(agentPosition) != null) {
this.result = this.getNeighbourPositionAction(policeForce, (Area)targetEntity);
}
//再不然说明我与目标很远啊,就规划路径
else {
// path 内存储的是从智能体所在地到目标的路径
List<EntityID> path = this.pathPlanning.getResult(agentPosition, this.target);
//路径规划成功
if (path != null && path.size() > 0) {
//index 存储的是警察所在地在path中的位置
int index = path.indexOf(agentPosition);
//如果警察所在地不在路径规划的路径之中
if(index == -1) {
//area即为警察所在的地区
Area area = (Area)positionEntity;
//找到规划得到路径的某一段,我与它相邻,那我可以从这一段过去,我的位置也就相应的是这段路的位置,因为可能我在建筑里,而路径规划只规划道路,所以我要先到路上去
for(int i = 0; i < path.size(); i++) {
if(area.getEdgeTo(path.get(i)) != null) {
index = i;
break;
}
}
}
//如果警察所在地在路径规划的路径之中
else if(index >= 0){
index++;
}
//警察所在地在规划的路径之中且不为终点
if(index >= 0 && index < (path.size())) {
//entity为警察所在地
StandardEntity entity = this.worldInfo.getEntity(path.get(index));
//判断下我现在是否到达与目的地相邻的区域了,如果是,getNeighbourPositionAction返回的有值,如果不相邻,getNeighbourPositionAction返回为空
this.result = this.getNeighbourPositionAction(policeForce, (Area) entity);
if (this.result != null && this.result.getClass() == ActionMove.class) {
if(!((ActionMove)this.result).getUsePosition()) {
this.result = null;
}
}
}
//结果设置为空,沿着path移动
if(this.result == null) {
this.result = new ActionMove(path);
}
}
}
return this;
}
在类里面一般会有一些工具函数的存在,比如
private boolean intersect
这种函数,其代码量多,晦涩,而且细看无用,较难进行修改,因为了解到其用法和作用就要停下来,不要纠结于弄懂每个函数,可以直接通过其函数名、参数、返回值 大致了解其功能,如果不影响代码的阅读自然最好,实在是对代码理解需要的话再看,切记,不要太过深入,仿真项目一共寥寥数万行代码,深入看的话根本难以实现!!!
加油吧!!!!!!!!!!!!!!!!!!!!