我梦dream以求的项目之一就是带有内存的模块化任务机器人。 该项目的最终目标是用能够独立和集体行动的生物创造一个世界。
我曾经对世界生成器进行编程,所以我想用简单的机器人来填充世界,这些机器人使用AI来确定其行为和交互。 因此,由于演员对世界的影响,可以增加其细节。
我已经实现了基本的Javascript任务管道系统(因为这简化了我的生活),但是我想要更可靠,更可扩展的东西,所以我用C ++编写了这个项目。 在subreddit / r /过程生成中执行过程花园的竞争使我想到了这一点(因此有相应的主题)。
在我的系统中,模拟由三个部分组成:世界,人口以及将它们联系起来的一系列动作。 因此,我需要创建三个模型,本文将对此进行讨论。
为了增加难度,我希望演员们保留有关以前与世界的经历的信息,并在以后的动作中使用有关这些交互作用的知识。
创建世界模型时,我选择了一条简单的路径,并使用Perlin噪声将其放置在水面。 世界上所有其他物体都是绝对随机放置的。
对于总体模型(及其“内存”),我仅创建了一个具有多个特征和坐标的类。 这应该是低分辨率的模拟。 内存是一个队列,机器人被环顾四周,保存有关其环境的信息,写入队列并管理该队列以解释其内存。
为了连接这两个动作系统,我想在任务队列的分层系统内创建一个原始任务框架,以便各个实体可以实现世界上的复杂行为。
样本图。 水完全无意间变成了河流。 所有其他元素都是随机放置的,包括蚁丘,蚁丘在种子中移到边缘的位置太远(但河流看上去很美)。我认为草丛中的一堆蚂蚁将是一个很好的测试模型,它可以保证基本功能(以及整个任务队列系统)实现的可靠性,并防止内存泄漏(有很多)。
我想更详细地描述任务系统和内存的结构,并说明如何从(主要是)原始基本功能创建复杂性。 我还想展示一些有趣的“蚂蚁的内存泄漏”,当蚂蚁开始疯狂地转圈以寻找草丛或静止不动并使程序变慢时,您可能会遇到这些有趣的现象。
一般结构
我用C ++编写了此模拟,并使用SDL2进行渲染(之前我已经为SLD2编写了一个小型表示类)。 我还使用了我在github上找到的A *实现(略作修改),因为
我的实现速度很慢,而且我不明白为什么。
地图只是一个100×100的网格,具有两层-土壤层(用于搜索路径)和填充层(用于完成交互和搜索路径)。 世界一流的服装还具有多种美容功能,例如草和植物的生长。 我现在正在谈论这一点,因为这些是本文中不会描述的唯一部分。
人口
机器人在一类具有描述单个生物的属性的类中。 其中一些是装饰性的,其他一些则影响了动作的执行(例如,飞行的能力,视野范围,所吃的东西以及该生物可以穿什么)。
这里最重要的是确定行为的辅助值。 即:一个包含其当前路径A *的矢量,这样就不必在每个时钟周期中对其进行计数(这样可以节省计算时间,并允许您模拟更多的机器人),以及一个内存队列,用于定义生物对环境的解释。
内存队列
内存队列是一个简单队列,其中包含一组受bot属性限制大小的内存对象。 每次添加新的记忆时,它们都会被推进,并且超出边界的所有内容都会被切断。 因此,某些记忆可能比其他记忆更“新鲜”。
如果该机器人希望从内存中调用信息,那么他将创建一个内存对象(请求)并将其与内存中的内容进行比较。 然后,撤回函数返回与查询中指定的任何或所有条件匹配的内存向量。
class Memory{ public:
内存由一个包含多个属性的简单对象组成。 这些内存属性被认为是彼此“关联”的。 每个内存还被赋予一个“ recallScore”值,该值在每次由调用函数记住该内存时进行迭代。 机器人每次记住内存时,如果旧内存的callbackScore高于新内存的recallScore,它将从背面开始依次执行单遍排序,更改内存的顺序。 因此,某些内存可能更“重要”(内存大小较大),并且在队列中的存储时间更长。 随着时间的流逝,它们将被新的替换。
内存队列
我还向此类添加了一些重载运算符,以便可以执行内存队列和查询之间的直接比较,比较“ any”或“ all”属性,以便在覆盖内存时仅覆盖指定的属性。 因此,我们可以将对象的内存与某个位置相关联,但是如果我们再次查看该位置而该对象不存在,则可以使用包含该位置的查询来覆盖包含新填充图块的内存,从而更新该内存。
void Bot::updateMemory(Memory &query, bool all, Memory &memory){
在为该系统创建代码的过程中,我学到了很多东西。
任务系统
游戏循环或渲染的本质是,在每种情况下都重复相同的功能,但是,我想在我的机器人中实现非周期性行为。
在本节中,我将解释为应对这种影响而设计的任务系统结构的两种观点。
自下而上的结构
我决定从下而上,创建一系列机器人应该执行的“原始动作”。 每个动作仅持续一拍。 有了良好的原始函数库,我们可以将它们组合成由几个原始函数组成的复杂动作。
此类原始动作的示例:
请注意,这些操作包含对世界和人口的引用,您可以对其进行更改。
- 等待会导致该生物在此循环中不执行任何操作。
- Look解析环境并排队新的记忆。
- 交换会抓住生物手中的一个物体,并将其替换为躺在地上的一个物体。
- 消耗会消灭生物手中的物品。
- 步骤采用当前计算出的到达目的地的路径,然后执行一个步骤(进行一系列错误检查)。
- ...等等。
所有任务功能都是我的任务类的成员; 经过严格的测试,他们已经证明了其可靠性和结合到更复杂任务中的能力。
在这些辅助函数中,我们通过简单地链接其他任务来构造函数:
- 步行任务仅几步(带有错误处理)
- 取任务是外观和交换任务(由于蚂蚁内存处理,所以需要它,我将在后面解释)
- 空闲任务是选择一个随机的位置并移动到该位置(使用步行),等待几个周期(使用等待),然后将该循环重复指定的次数
- ...等等
其他任务更加复杂。 搜索任务执行一个内存查询,以搜索包含“ food”对象(可用于这种类型的bot)的地方的任何内存。 她下载了这些记忆,然后遍历所有这些,“寻找”食物(对于蚂蚁来说,这就是草)。 如果没有食物记忆,该任务将使该生物随机漫游世界并环顾四周。 通过观察和研究(通过用viewRadius = 1进行“观察”;即仅看着下面的瓷砖),该生物可以使用有关周围环境的信息来更新其记忆,从而有目的地聪明地寻找食物。
更为广泛的饲草任务包括寻找食物,捡拾食物,检查(以更新内存并在附近寻找食物),回家和存放食物。
您可能会注意到,蚂蚁离开蚁丘,有目的地寻找食物。 由于初始化,蚂蚁的初始路径指向一个随机点,因为它们在t = 0处的内存为空。 然后,他们被赋予了在觅食任务中捡食物的命令,并且他们也环顾四周,确保没有更多的食物。 他们不时开始流浪,因为他们用光了看不到食物的地方(不祥的近视)。最后,该机器人具有一个“视图”,该视图确定分配给它的AI的类型。 每个视图都与一个定义其所有行为的控制任务相关联:它由一系列越来越小的任务组成,这些任务由一组内存队列和原始任务轻松确定。 这些任务就像蚂蚁和蜜蜂一样。
自上而下的结构
如果从上到下查看,系统将包含一个任务主类,该类负责协调地图上每个机器人的控制任务及其执行。
Taskmaster有一个控制任务向量,每个控制任务都与一个机器人相关。 每个控制任务又具有一个子任务队列,这些任务在具有相关任务功能的任务对象的首次初始化期间加载。
class Task{ public:
队列中的每个任务对象都存储一个参数数组,并将其传递给关联的函数处理程序。 这些参数确定了尽可能简单地创建的这些原始任务的行为。 参数是通过引用传递的,因此队列中的任务对象可以存储其参数并允许其子函数进行更改,因此您可以实现诸如迭代等操作,以等待一定数量的滴答声或请求以收集一定数量的项,等等。 子函数通过引用更改父函数的迭代器(参数[n])的值,并使其成功条件取决于其值。
在每种度量中,任务管理器都会遍历控制任务列表,并通过调用它们的perform方法来执行它们。 反过来,perform方法将查看任务中队列的顶部元素,并使用任务中的参数执行该元素。 因此,您可以级联任务队列,始终执行最高任务。 然后,任务的返回值确定任务的完成。
当原始任务返回true时,它已经达到了稳定点,或者至少不应重复执行(例如,当生物到达终点时step返回true)。 也就是说,它的返回条件得到满足,并将其从队列中删除,以便可以在下一个度量中完成下一个任务。
队列为空后,包含任务队列的任务将返回true。 因此,可以使用队列和子队列的结构创建复杂的任务,在这些队列和子队列中,经常调用相同的功能,但是每次调用都会一步一步迭代游戏状态和任务状态。
最后,控制任务使用简单的结构-在每个循环中调用它们,仅当它们为空时才加载任务,否则执行队列中加载的任务。
借助我的队列循环(参见代码),我可以重复执行一个函数,并且每次执行其队列中的顶层元素时,如果调用它们的perform方法返回true,则将元素从其中推出。
结果
所有这些都包装在libconfig中,因此仿真参数非常容易更改。 您可以编写许多控制任务而没有问题(我创建了蚂蚁和蜜蜂),并且使用libconfig定义和加载新物种非常简单。
将它们优雅地加载到模拟中。 由于有了新的改进的路径搜索功能,我可以模拟大量在二维平面上收集食物的主动机器人。
模拟40只蚂蚁同时收草。 它们在沙子中形成的路径归因于分配给“未破坏”土地的重量增加。 这导致创建有特色的“蚂蚁公路”。 它们也可以解释为信息素,但是如果蚂蚁实际上交换了记忆,则更像是事实。该系统的模块化确保快速创建新物种,其行为由简单的控制任务决定。 在上面的代码中,您可以看到我是通过简单地更改它们的颜色,路径搜索限制(它们不能飞行),可见性范围和内存大小来创建蠕虫和AI蜜蜂的。 同时,我更改了它们的一般行为,因为所有这些参数均由原始任务的功能使用。
调试蚂蚁的记忆
复杂任务和内存的结构导致了无法预料的困难,并且需要处理异常。
这是三个使我重做子系统的特别复杂的内存错误:
蚂蚁围成一圈
我必须面对的第一个错误:蚂蚁疯狂地沿着广场上的图案跑去,在裸露的地面上寻找草。 之所以出现此问题,是因为那时我还没有实现内存更新。 蚂蚁对食物的位置有记忆,一旦他们拾起草再次环顾四周,就形成了新的记忆。
问题在于新的内存在同一时间,但是旧的内存被保留了。 这意味着在寻找食物的过程中,蚂蚁会记住并保留不再有效的食物的位置,但是这些旧的记忆得以保留并取代了新的记忆(他们记得这种美味的药草)。
我将其修复如下:如果我们看到的位置相同并且对象已更改,则对象的数据只会在旧的内存中被覆盖(例如,该生物看到那里不再有草,但不记得曾经是草)。 也许将来我会简单地在内存中添加“无效”属性,以便机器人可以记住可能很重要的旧信息,但是不再有效的信息会“出现”(“我在这里看到了一只熊,但现在已经消失了”)。
蚂蚁捡起其他蚂蚁下的物体
时不时地(尤其是有大量蚂蚁和高密度的草),两只蚂蚁可以用一种措施在一块草上捡起来。 这意味着第一只蚂蚁进入瓷砖,环顾四周,并分三步拿走了该物品。 反过来,第二只蚂蚁做了同样的事情,只是在提起物体之前,另一只蚂蚁从他的鼻子下面抢了下来。 他从容地继续他的任务,检查与上一措施中的另一只蚂蚁相同的环境,并以类似的方式处理他的记忆线(因为在此阶段它们的记忆是相同的)。 这导致第二只蚂蚁复制第一只蚂蚁,从不拾取对象,而跟随第一只蚂蚁,实际上完成了所有工作。 我注意到了这一点,因为在模拟五只蚂蚁时,只有三只可见。 找到原因花了很长时间。
我通过将交换任务设为原始并创建了接管任务来解决了这个问题,该任务首先查看地面以查看是否存在对象。 如果是,它将“交换”,否则,将“等待”两次移动,以便另一只蚂蚁一定会离开。 在一种情况下,此操作适用于两种措施,在另一种情况下-适用于一种措施。
无法到达的位置
另一个迫使我重新进行内存处理的令人不快的错误是蚂蚁无法看到的某些地方。 它们之所以出现,是因为我在土地上懒散地放置了“草叉”,这些草有时挂在水面上。 这使我概括了步骤任务。
在发送食物搜索请求时,蚂蚁通常会记住他们真正无法到达的地方(他们看到水上方的草,
疯狂地想要收集它)。 如果在内存中未标记该变量(例如,布尔变量“ reachable”),则他们将继续记住该变量并将其写入队列,直到该动作是唯一的动作为止。 这导致了严重的抑制,因为他们
在每个度量中不断执行路径查找操作,试图到达该位置,但失败了 。
解决方案是,如果找不到任务的路径,则在步骤任务中更新内存,将其标记为无法访问。 另外,搜索任务仅查询有食物的地方以获取可访问的记忆。
系统一般
总的来说,我想说-是的,很遗憾,我花了一个星期的时间参加编程马拉松比赛,因为我受到启发去创造出能够执行我告诉他们的事情(以及他们想要做的事情)的机器人。 我不得不做一些技巧,学到了很多东西。
我创建的系统不是100%可靠的,并且我仍然注意到一些工件。 例如,作为解析外观的方向,该动作上下左右使用,也就是说,最后一个内存在右下角。 当召回信息以搜索物品时,这意味着生物将倾向于向东南移动。 在大型模拟中,当草长得快并且向东南方向略微弯曲(与种子无关)时,这一点尤其明显。
增强功能
我认为需要进行重大改进才能模拟更多复杂生物的更复杂记忆。
这包括提高内存处理功能的可靠性,以及添加新的原语(例如“ think”)和高级任务的派生(例如“ decide”或“ dream”)。 “思考”可以是存储器请求的原始动作。 反过来,一个“梦”可以由几个“思考”调用组成:选择一个随机存储器,获取一个随机属性,并重复进行重复以加强共同的主题或重要的关联。
为了将来,我计划添加三个特定的功能:
- 添加中断处理和任务优先级
- 在实体之间添加通讯
- 添加组结构,以便实体可以正式识别彼此
实体之间的交互可能需要中断处理并确定任务的优先级,因为该漫游器在与之通信时不能盲目地继续其活动(必须以某种方式“侦听”)或受到攻击(“逃跑”或“战斗”) )
实体之间的通信可能包含一个或两个原始任务,用于交换内存或向其他漫游器的内存发出请求(例如,“说”或“问”)。 通过这种方式,可以传输诸如食物或其他资源的位置之类的信息。
我希望执行这些任务,并绘制一个有或没有通信的大型团队的资源积累速率图表。 人口已经在跟踪每种度量收集的食物数量。 有趣的是,分享记忆会影响效率。
未来
模拟社区的最重要功能将是添加组结构,并使这些组具有宏观属性,例如其共同的“目标和责任”。 这为我们提供了一种“种子”,从中我们可以获取高层任务,这些任务在组结构的层次结构中委派为直接影响整个世界的“较低”高层任务。 它还允许您创建一种政治结构形式。
这样的系统非常自给自足,可视化只是叠加在其之上。 用类人动物代替昆虫,收集资源并将它们存储在某个地方将非常简单,这样它的大小就会增加。
例如,他们家的成长性质可以非常依赖于机器人,也可以完全独立于机器人的行为。不同的物种可能有不同的部落,具有不同的特征和趋势。另外,我可以将此系统与以前创建的地图生成器(扩展世界级)结合使用,以使世界更加真实。总结
在不久的将来,我计划将人类替换为生物并实现某些最后的功能。当我提高系统质量时,也许我会发布完整的源代码(在某些地方代码相当混乱)。等待下一篇文章。同时,这是一段视频,蜜蜂在花中寻找花粉;它们使用相同的框架进行编码。我选择此种子是因为起点位于一个小岛上。但是,没有将蜜蜂编程为返回蜂巢,而只是不断地收集花粉。您可能会注意到他们的视线更高,有时他们非常有意地移到了刚刚看到的花朵上。...这是Bee Task成员函数: bool Task::Bee(Garden &garden, Population &population, int (&arguments)[10]){