AI分享站 http://www.aisharing.com 学习,思考,领悟,分享,收获,我是游戏AI程序员 Thu, 19 Apr 2018 11:13:12 +0000 zh-CN hourly 1 https://wordpress.org/?v=4.7.11 当人工智能遇到游戏 http://www.aisharing.com/archives/974 http://www.aisharing.com/archives/974#comments Thu, 19 Apr 2018 11:07:31 +0000 http://www.aisharing.com/?p=974 最近有编辑约稿写一篇关于介绍游戏人工智能的入门文章,构思再三,成文如下,可以点击原文查看,欢迎各位看官批评指正 (摘要) 在人工智能技术高度普及的今天,各个领域的应用中都要加入些人工智能技术好像才能更符合当下的业务产品需求。那么,人工智能技术是否有影响到游戏领域呢?当人工智能技术和游戏技术结合的时候,会迸发出什么样的火花呢? 我们可以先来看一个游戏角色,这个游戏角色就是风靡全球的马里奥大叔。 第一代的马里奥游戏是一个典型的平台类游戏。在整个游戏场景中,马里奥需要通过在各种平台上移动、跳跃、躲避敌人(当然,也可以主动攻击)的方式,一直跑到最后,拉起小旗子来完成这个关卡。在整个过程中,玩家能控制的就是马里奥这个角色,其他敌人,包括场景的机关,都是由系统来控制的。整个游戏最开始的一个场景是,马里奥会遇到第一个敌人,一个长着蘑菇样子的怪物 这个敌人会不停地移动,直直地冲向玩家,为什么说是直直的呢?因为这个坏蘑菇真的是除了往前走,什么都不会做,它碰到物体就会折返,遇到台阶就会掉下去,在玩家看来,它就是一个完全不会思考的傻瓜,它甚至不关心玩家在哪里,唯一的目标就是傻傻地不停往前走。如果我说这就是游戏的人工智能,你是不是会大跌眼镜?但不管怎么样,这的确就是游戏人工智能的雏形! ...... (点击这里,阅读原文) ————————————————————————————— 作者:Finney Blog:AI分享站(http://www.aisharing.com/) Email:finneytang@gmail.com 本文欢迎转载和引用,请保留本说明并注明出处 —————————————————————————————]]>

最近有编辑约稿写一篇关于介绍游戏人工智能的入门文章,构思再三,成文如下,可以点击原文查看,欢迎各位看官批评指正

(摘要)

在人工智能技术高度普及的今天,各个领域的应用中都要加入些人工智能技术好像才能更符合当下的业务产品需求。那么,人工智能技术是否有影响到游戏领域呢?当人工智能技术和游戏技术结合的时候,会迸发出什么样的火花呢?

我们可以先来看一个游戏角色,这个游戏角色就是风靡全球的马里奥大叔。

第一代的马里奥游戏是一个典型的平台类游戏。在整个游戏场景中,马里奥需要通过在各种平台上移动、跳跃、躲避敌人(当然,也可以主动攻击)的方式,一直跑到最后,拉起小旗子来完成这个关卡。在整个过程中,玩家能控制的就是马里奥这个角色,其他敌人,包括场景的机关,都是由系统来控制的。整个游戏最开始的一个场景是,马里奥会遇到第一个敌人,一个长着蘑菇样子的怪物

这个敌人会不停地移动,直直地冲向玩家,为什么说是直直的呢?因为这个坏蘑菇真的是除了往前走,什么都不会做,它碰到物体就会折返,遇到台阶就会掉下去,在玩家看来,它就是一个完全不会思考的傻瓜,它甚至不关心玩家在哪里,唯一的目标就是傻傻地不停往前走。如果我说这就是游戏的人工智能,你是不是会大跌眼镜?但不管怎么样,这的确就是游戏人工智能的雏形!

……

点击这里,阅读原文

—————————————————————————————
作者:Finney
Blog:AI分享站(http://www.aisharing.com/)
Email:finneytang@gmail.com
本文欢迎转载和引用,请保留本说明并注明出处
—————————————————————————————


评论

  • 2018 年 4 月 23 日, 匿名 评论到: 最喜欢看你的文章了。。前段时间还邮件请教了几个问题,感谢你的解答啊。。文章看完了,想问下最后跳转的那本本书如何?还不错就买来看看啊
  • 2018 年 4 月 23 日, Finney 评论到: 那本书属于文章集合,不是很系统的AI教程,比较适合有一定游戏AI编程经验的人来拓展一下思路,你可以去网店里看看这本书的目录,再决定适不适合你
知识共享许可协议
本博客作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
捐赠本站 ]]>
http://www.aisharing.com/archives/974/feed 2
离散事件模拟在游戏中的应用 http://www.aisharing.com/archives/821 http://www.aisharing.com/archives/821#comments Wed, 10 May 2017 15:25:46 +0000 http://www.aisharing.com/?p=821 离散事件模拟(discrete event simulation),这个东西可能在游戏领域用得并不是很多,它是模拟仿真领域的一个仿真模型,用来模拟在时间轴上一系列离散事件后,整个系统的变化情况,这么说,可能还是有点抽象,给大家举一个使用离散事件模拟的一个经典的例子,如何计算银行柜台排队的平均等待时间。 先简要描述一下这个场景,假设一个银行有两个柜台,每个柜台都可以办理业务,每隔3分钟都会有一位客户进来办理业务,如果有空的柜台,那他就直接办理,如果没有,就排队,每个人办理的业务时间都不同,可能是1~10分钟的任意一个时间,整个银行的营业时间是8小时,那请计算,在8小时所接待的所有客户中,客户的平均等待时间是多少? 这个问题,如果靠数学方法来计算,并不是很容易,那换一个思路就是,我可不可以把整个这个8小时的过程模拟一遍,把每一个客户的等待时间记录下来,然后求平均,这样就可以得到这个平均等待时间了。当然,真的把模拟的过程跑8个小时,太没有效率了,所以这个时候,就可以用离散事件模拟的方法,对这个问题去快速的进行仿真。 离散事件模拟有几个基本的概念,我们可以结合上面的这个例子来一个个看
  • 时钟(Clock):整个模拟是按照时间去前进的,也就是有一个虚拟的时间去跟踪当前的时间,在这里,这个时钟就是从银行开门,一直到银行关门,总长8小时
  • 结束条件(Ending Condition):模拟结束所要满足的条件,因为不可能无限模拟下去,在这里,结束条件,就是时间到了8小时
  • 事件(Event):在时间轴上需要处理的事件,每个事件都有发生的时间,并且事件是按照时间的先后排列在时间轴上的,在这里,有几个事件,客户到达银行,客户结束办理业务。
整个模拟过程是怎么样的呢?简单的一句话,就是不断的从时间轴上处理一个个的事件,直至满足结束条件或者所有的事件都处理完毕。由于在相邻的两个事件与事件之间,是没有任何事情发生的,所以时钟是可以从当前的事件触发的时间直接跳到下一个事件触发的时间,这也就是“离散事件”的含义,因为这些“事件”在整个时间轴上是离散分布的,从时间的角度来看,整个模拟过程是“跳跃”前进的。 新的事件可以通过在处理老的事件的时候来不断的产生,并且添加到时间轴上,继续用上面的例子,我们把“客户n在时间t到达银行”这个事件记为E(Arrive, n, t),把“客户n在柜台m用了t分钟结束了业务办理”这个事件记为E(Finish, n, m, t),第一个事件,就是E(Arrive, Tom, 0),也就是银行一开门,来了一个用户叫Tom,这样在时间轴上,现在是这样的 这样我们初始化就好了,现在银行开门,我们开始进行8小时的模拟 我们先处理第一个事件E(Arrive, Tom, 0),当前时钟设为事件的时间,第0分钟,这个事件处理的时候,因为现在柜台是空的,所以Tom选择了1号柜台,并且需要用了8分钟办理业务,那我们就在时间轴上再添加一个时间E(Finish, Tom, 1, 8),并且,我们还需要往时间轴再添加下一个“客户到达事件”,假设3分钟后会来一个客户叫Jerry,这样时间轴上又加了一个事件E(Arrive, Jerry, 3),这个事件会加在E(Finish, Tom, 1, 8)前面,因为它会先发生 然后我们开始处理第二个事件,因为事件是按照时间轴排序的,所以我们一定能拿到一个最近要发生的事件,现在的例子里,这个事件是E(Arrive, Jerry, 3),又来了一个客户Jerry,当前时钟设为事件的时间,第3分钟,处理的过程和上面一样,不过Jerry选择2号柜台,我们会压两个事件,E(Finish, Jerry, 2, 3+6),E(Arrive, Mary, 3+3),值得注意的是,事件发生的时刻,我用了3+6这样的形式,也就是表示,当前时钟是第3分钟,这个事件需要在当前时钟的往后的6分钟发生 按照我们的时间轴,第三个事件,是E(Arrive, Mary, 3+3),当前时钟设为事件的时间,第6分钟,这个时候,我们不能直接为Mary添加E(Finish)事件,因为没有空的柜台,所以Mary只能选择排队,但下一个E(Arrive)事件还是需要添加 第四个事件,总算轮到很早就添加的E(Finish, Tom, 1, 8),所以添加的早,不一定执行的早,因为这个模拟是按照时间来推进的,当前时钟到了第8分钟,这个时候Tom的业务办完了,在处理E(Finish)事件的时候,我们就要去看,是不是有人在排队,如果有人在排队,那就要为排在第一个的用户,添加E(Finish)事件,现在这个人是Mary,那我们就添加E(Finish, Mary, 1, 8+6),在1号柜台,预计花6分钟的时间结束, 这样基本上所有可能的情况我们都简单描述了,剩下的,就是让这个模拟过程一直跑,整个过程,就是不断的有事件被处理,然后又不断的有事件产生,到最后8小时的时候,银行关门,模拟结束。基于这个模拟过程,平均等待时间就很容易算了,客户来的时间记t1(也就是处理E(Arrive)的时间点),客户开始办业务的时间记t2(也就是添加E(Finish)事件的时间点),然后t2-t1,就是单个客户的等待时间,然后把所有等待时间求和,再除以所有的客户数,就是平均等待时间了。 这个东西说起来好累,其实实现起来并不复杂,在我维护的那个TsiU的库中,已经实现了一个离散事件模拟的工具库,也就100多行代码,非常简单。在项目中,我用这个方法,做过一个工具,用来模拟在特定的同时在线数量的情况下玩家匹配游戏的平均等待时间,这样可以帮助运营人员去确定需要导入多少的用户量,来尽可能的减少匹配的等待。离散事件模拟,对于这种情景可以说是一个非常好用的利器,概念简单,实现快捷。不过在实际的游戏层面,它可不可以发挥作用呢?或者说,有没有什么游戏情景可以用这个方法去架构呢?答案是,可以有! 说了一大圈,总算到了这次分享的正题了。离散事件模拟是可以用在某些特定的游戏情境中,比如有一些游戏是战报类的,也就是说你点击了战斗之后,后台系统就已经把整个战斗的过程算好了,然后客户端就是把整个战斗结果来回放一遍,这种类型的战斗就很适合用离散事件模拟来架构(不知道类似的游戏有没有采用这样的方法),假设我们采用类似于早期最终幻想的ATB战斗系统,也就是每个参加角色都有个自己的时间条,谁先长满了就谁先行动,速度高的角色时间条长得快一点,速度低的角色时间条就长得慢一点。 要模拟这样一场战斗就可以用离散事件模拟的结构,假设我们先简化为只能进行攻击的行动,我们就会有一个事件就是攻击,E(Attack, t),在战斗的一开始,我们把速度最快的那个人的E(Attack, t)添加到时间轴上(有点类似于上面银行的例子里,排在最前面等处理业务的那个人),然后开始进行模拟,在处理E(Attack, t)时,需要进行以下几步
  • 处理伤害
  • 添加下一个行动的人的E(Attack, t)
  • 把自己加入行动的队列
一直进行模拟,直到己方胜出或者团灭。再复杂一点,如果再攻击中改变了时间条的增长速度,那么就需要对行动队列进行调整,也就是重新按照速度排序。这样当这场战斗模拟完毕的时候,就可以把所有的模拟过程的数据发送给客户端,由客户端进行战斗表现。 不过,在这种模拟过程中,游戏内是不能进行操作的,也就说,整个战斗是自动进行的,那如果需要支持操作,那个应该怎么处理呢?对于使用离散模拟的游戏来说,是没有办法做到“实时操作,实时生效”的,不过可以做到“实时操作,延时生效”,这在某些游戏中是很常见的,比如像类似于足球经理类的游戏,在你看比赛模拟的时候,你可以进行战术改变,换人等操作,这种操作对于玩家来说并不需要很高的实时性,只要在一段时间后能生效就可以了,再比如上面说的ATB的战斗系统,假设玩家可以在观看战斗的时候,能够使用一些全局魔法,这个时候,就可以用一些“施法时间”的概念来告诉玩家,你的施法是需要时间的,来掩盖无法实时生效的问题,但是又保证了服务器快速的模拟计算,节省服务器资源。 用离散事件模拟做“实时操作,延时生效”的关键,就是采用“分段模拟”,也就是不是一下子模拟整个过程,而是模拟一段时间,等待一段时间,再模拟一段时间,这样,如果玩家的操作发生在上一个时间段,那我就可以在下一个时间段模拟的时候,加入玩家操作的影响。 采用“分段模拟”的时候,和客户端通信会采用一段一段数据发送的方式,服务器先模拟一段时间的数据,比如模拟10秒钟逻辑,然后推送给客户端,之后等待一段时间,这段时间不能超过每段的模拟时间,一般可以等待50%~80%的模拟时间,比如等待5~8秒钟,如果等待超过10秒钟(甚至接近10秒钟),就会导致客户端那边播放完了,新的数据还没到,从而客户端的表现产生卡顿的情况,后续就一直循环这个过程,直到整个模拟结束。 离散事件模拟在游戏中虽然不是很常用,不过也是可以作为一个知识点,储备在那里,当遇到适合的场合,就可以作为选择之一加以应用。具体的例子我就不写了,大家有兴趣可以用TsiU里的那个库,自己做一个模拟银行平均等待时间的程序,来体会一下,有任何问题,和指教,欢迎讨论。 ————————————————————————————— 作者:Finney Blog:AI分享站(http://www.aisharing.com/) Email:finneytang@gmail.com 本文欢迎转载和引用,请保留本说明并注明出处 —————————————————————————————]]>
离散事件模拟(discrete event simulation),这个东西可能在游戏领域用得并不是很多,它是模拟仿真领域的一个仿真模型,用来模拟在时间轴上一系列离散事件后,整个系统的变化情况,这么说,可能还是有点抽象,给大家举一个使用离散事件模拟的一个经典的例子,如何计算银行柜台排队的平均等待时间。

先简要描述一下这个场景,假设一个银行有两个柜台,每个柜台都可以办理业务,每隔3分钟都会有一位客户进来办理业务,如果有空的柜台,那他就直接办理,如果没有,就排队,每个人办理的业务时间都不同,可能是1~10分钟的任意一个时间,整个银行的营业时间是8小时,那请计算,在8小时所接待的所有客户中,客户的平均等待时间是多少?

这个问题,如果靠数学方法来计算,并不是很容易,那换一个思路就是,我可不可以把整个这个8小时的过程模拟一遍,把每一个客户的等待时间记录下来,然后求平均,这样就可以得到这个平均等待时间了。当然,真的把模拟的过程跑8个小时,太没有效率了,所以这个时候,就可以用离散事件模拟的方法,对这个问题去快速的进行仿真。

离散事件模拟有几个基本的概念,我们可以结合上面的这个例子来一个个看

  • 时钟(Clock):整个模拟是按照时间去前进的,也就是有一个虚拟的时间去跟踪当前的时间,在这里,这个时钟就是从银行开门,一直到银行关门,总长8小时
  • 结束条件(Ending Condition):模拟结束所要满足的条件,因为不可能无限模拟下去,在这里,结束条件,就是时间到了8小时
  • 事件(Event):在时间轴上需要处理的事件,每个事件都有发生的时间,并且事件是按照时间的先后排列在时间轴上的,在这里,有几个事件,客户到达银行,客户结束办理业务。

整个模拟过程是怎么样的呢?简单的一句话,就是不断的从时间轴上处理一个个的事件,直至满足结束条件或者所有的事件都处理完毕。由于在相邻的两个事件与事件之间,是没有任何事情发生的,所以时钟是可以从当前的事件触发的时间直接跳到下一个事件触发的时间,这也就是“离散事件”的含义,因为这些“事件”在整个时间轴上是离散分布的,从时间的角度来看,整个模拟过程是“跳跃”前进的。

新的事件可以通过在处理老的事件的时候来不断的产生,并且添加到时间轴上,继续用上面的例子,我们把“客户n在时间t到达银行”这个事件记为E(Arrive, n, t),把“客户n在柜台m用了t分钟结束了业务办理”这个事件记为E(Finish, n, m, t),第一个事件,就是E(Arrive, Tom, 0),也就是银行一开门,来了一个用户叫Tom,这样在时间轴上,现在是这样的

这样我们初始化就好了,现在银行开门,我们开始进行8小时的模拟

我们先处理第一个事件E(Arrive, Tom, 0),当前时钟设为事件的时间,第0分钟,这个事件处理的时候,因为现在柜台是空的,所以Tom选择了1号柜台,并且需要用了8分钟办理业务,那我们就在时间轴上再添加一个时间E(Finish, Tom, 1, 8),并且,我们还需要往时间轴再添加下一个“客户到达事件”,假设3分钟后会来一个客户叫Jerry,这样时间轴上又加了一个事件E(Arrive, Jerry, 3),这个事件会加在E(Finish, Tom, 1, 8)前面,因为它会先发生

然后我们开始处理第二个事件,因为事件是按照时间轴排序的,所以我们一定能拿到一个最近要发生的事件,现在的例子里,这个事件是E(Arrive, Jerry, 3),又来了一个客户Jerry,当前时钟设为事件的时间,第3分钟,处理的过程和上面一样,不过Jerry选择2号柜台,我们会压两个事件,E(Finish, Jerry, 2, 3+6),E(Arrive, Mary, 3+3),值得注意的是,事件发生的时刻,我用了3+6这样的形式,也就是表示,当前时钟是第3分钟,这个事件需要在当前时钟的往后的6分钟发生

按照我们的时间轴,第三个事件,是E(Arrive, Mary, 3+3),当前时钟设为事件的时间,第6分钟,这个时候,我们不能直接为Mary添加E(Finish)事件,因为没有空的柜台,所以Mary只能选择排队,但下一个E(Arrive)事件还是需要添加

第四个事件,总算轮到很早就添加的E(Finish, Tom, 1, 8),所以添加的早,不一定执行的早,因为这个模拟是按照时间来推进的,当前时钟到了第8分钟,这个时候Tom的业务办完了,在处理E(Finish)事件的时候,我们就要去看,是不是有人在排队,如果有人在排队,那就要为排在第一个的用户,添加E(Finish)事件,现在这个人是Mary,那我们就添加E(Finish, Mary, 1, 8+6),在1号柜台,预计花6分钟的时间结束,

这样基本上所有可能的情况我们都简单描述了,剩下的,就是让这个模拟过程一直跑,整个过程,就是不断的有事件被处理,然后又不断的有事件产生,到最后8小时的时候,银行关门,模拟结束。基于这个模拟过程,平均等待时间就很容易算了,客户来的时间记t1(也就是处理E(Arrive)的时间点),客户开始办业务的时间记t2(也就是添加E(Finish)事件的时间点),然后t2-t1,就是单个客户的等待时间,然后把所有等待时间求和,再除以所有的客户数,就是平均等待时间了。

这个东西说起来好累,其实实现起来并不复杂,在我维护的那个TsiU的库中,已经实现了一个离散事件模拟的工具库,也就100多行代码,非常简单。在项目中,我用这个方法,做过一个工具,用来模拟在特定的同时在线数量的情况下玩家匹配游戏的平均等待时间,这样可以帮助运营人员去确定需要导入多少的用户量,来尽可能的减少匹配的等待。离散事件模拟,对于这种情景可以说是一个非常好用的利器,概念简单,实现快捷。不过在实际的游戏层面,它可不可以发挥作用呢?或者说,有没有什么游戏情景可以用这个方法去架构呢?答案是,可以有!

说了一大圈,总算到了这次分享的正题了。离散事件模拟是可以用在某些特定的游戏情境中,比如有一些游戏是战报类的,也就是说你点击了战斗之后,后台系统就已经把整个战斗的过程算好了,然后客户端就是把整个战斗结果来回放一遍,这种类型的战斗就很适合用离散事件模拟来架构(不知道类似的游戏有没有采用这样的方法),假设我们采用类似于早期最终幻想的ATB战斗系统,也就是每个参加角色都有个自己的时间条,谁先长满了就谁先行动,速度高的角色时间条长得快一点,速度低的角色时间条就长得慢一点。

要模拟这样一场战斗就可以用离散事件模拟的结构,假设我们先简化为只能进行攻击的行动,我们就会有一个事件就是攻击,E(Attack, t),在战斗的一开始,我们把速度最快的那个人的E(Attack, t)添加到时间轴上(有点类似于上面银行的例子里,排在最前面等处理业务的那个人),然后开始进行模拟,在处理E(Attack, t)时,需要进行以下几步

  • 处理伤害
  • 添加下一个行动的人的E(Attack, t)
  • 把自己加入行动的队列

一直进行模拟,直到己方胜出或者团灭。再复杂一点,如果再攻击中改变了时间条的增长速度,那么就需要对行动队列进行调整,也就是重新按照速度排序。这样当这场战斗模拟完毕的时候,就可以把所有的模拟过程的数据发送给客户端,由客户端进行战斗表现。

不过,在这种模拟过程中,游戏内是不能进行操作的,也就说,整个战斗是自动进行的,那如果需要支持操作,那个应该怎么处理呢?对于使用离散模拟的游戏来说,是没有办法做到“实时操作,实时生效”的,不过可以做到“实时操作,延时生效”,这在某些游戏中是很常见的,比如像类似于足球经理类的游戏,在你看比赛模拟的时候,你可以进行战术改变,换人等操作,这种操作对于玩家来说并不需要很高的实时性,只要在一段时间后能生效就可以了,再比如上面说的ATB的战斗系统,假设玩家可以在观看战斗的时候,能够使用一些全局魔法,这个时候,就可以用一些“施法时间”的概念来告诉玩家,你的施法是需要时间的,来掩盖无法实时生效的问题,但是又保证了服务器快速的模拟计算,节省服务器资源。

用离散事件模拟做“实时操作,延时生效”的关键,就是采用“分段模拟”,也就是不是一下子模拟整个过程,而是模拟一段时间,等待一段时间,再模拟一段时间,这样,如果玩家的操作发生在上一个时间段,那我就可以在下一个时间段模拟的时候,加入玩家操作的影响。

采用“分段模拟”的时候,和客户端通信会采用一段一段数据发送的方式,服务器先模拟一段时间的数据,比如模拟10秒钟逻辑,然后推送给客户端,之后等待一段时间,这段时间不能超过每段的模拟时间,一般可以等待50%~80%的模拟时间,比如等待5~8秒钟,如果等待超过10秒钟(甚至接近10秒钟),就会导致客户端那边播放完了,新的数据还没到,从而客户端的表现产生卡顿的情况,后续就一直循环这个过程,直到整个模拟结束。

离散事件模拟在游戏中虽然不是很常用,不过也是可以作为一个知识点,储备在那里,当遇到适合的场合,就可以作为选择之一加以应用。具体的例子我就不写了,大家有兴趣可以用TsiU里的那个库,自己做一个模拟银行平均等待时间的程序,来体会一下,有任何问题,和指教,欢迎讨论。

—————————————————————————————
作者:Finney
Blog:AI分享站(http://www.aisharing.com/)
Email:finneytang@gmail.com
本文欢迎转载和引用,请保留本说明并注明出处
—————————————————————————————


评论

知识共享许可协议
本博客作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
捐赠本站 ]]>
http://www.aisharing.com/archives/821/feed 1
黑板和共享数据 http://www.aisharing.com/archives/801 http://www.aisharing.com/archives/801#comments Mon, 25 Jul 2016 05:37:48 +0000 http://www.aisharing.com/?p=801 基于上面的描述,我们可以看到黑板有几个功能:
  • 记录:每个人可以写下自己的看法。
  • 更新:调整已有的看法。
  • 删除:删除对于过时的,或者错误的看法。
  • 读取:黑板上的内容谁都能自由阅读。
所以从本质上来说,黑板就是这样一个共享数据的结构,它对于多个系统间通信是很有帮助的,从程序设计的角度上来说,它提供一种数据传递的方式,有助于系统的封装和解耦合。对于各个子系统而言,只需要把自己的运算的结果数据记录在黑板上,至于这个数据谁会去用,并不需要关心,反过来也是一样,对于自己的运算时需要用到的数据,可以从黑板上去获取,至于这个数据是谁提供的,也不需要关心,只要这个数据在黑板上,就够可以认为是合法数据。这就提供的了一种灵活性,各个子系统的设计也会相对独立。 当然黑板也有些不足的地方,比如RWD(读写删)操作相对随意,特别是WD操作,容易造成数据被破坏,或者产生子系统间的竞争,比如,系统A和系统B都会去修改data1,那到底以谁的值为准呢?还有一个情况就是产生非法数据,一般认为,只要在黑板上的数据,就是合法的数据,在读取的时候,不需要判断它是否合法,但如果一个子系统没有很好的维护它自己产生的数据(比如,该删除的时候没删除,或者赋值错误),那别人读取该数据的系统时候,就会产生错误的运算结果。 博客上有一篇较早的文章就讨论过这样的问题,像黑板这样的共享数据结构,既是黄金屋,又是垃圾堆,用好不容易,所以在黑板原有的功能中,我们可以加一些额外的功能:
  • 数据过期时间:对于写入黑板的数据,可以加一个过期时间的功能,比如3秒后,该数据过期,这很实用,可以提高数据维护的便利程度。
  • 数据作用域:我们可以规定可以读写该数据子系统,默认情况下,黑板的数据都是全局可见的,就像程序中的全局变量一样,但如果我们希望某些数据只有对个别子系统开放,就可以通过作用域字段来指定。
说了一大堆,反过来,我们还是要讨论游戏,现在游戏中,也大量的使用黑板(或者类黑板)系统,因为游戏系统的模块间通信的需求也是很多的,AI,动画,物理,实体与实体间,等等,他们都需要彼此交换数据,我想,大家经常碰到的一个头疼的问题就是,这个数据应该存在哪里?存在这里也可以,存在那里也可以,或者索性做个Data类来存,所以在Player类里,变量会越来越多,变量列表越来越长。 针对这种情况黑板可以帮助解决一部分问题,特别是对于在多模块之间需要通信的数据,我们再来看一下它几个好处:
  • 解耦合:黑板做为独立的数据模块,可以"超然"于所有的模块之外,提供一些额外的数据维护和管理的功能,这个让我想到了那些内存数据库,比如redis和memcached,从某种程度上,黑板就像程序内的数据库。
  • 共享性:黑板的数据是共享的,比如我们要去拿一个数据,我们不需要先拿到它的实例(还需要考虑是否为null),然后再通过get方法去取数据,我们只需要存一个黑板的实例,然后通过黑板获取数据的方法来获取。这就类似设计模式中的Facade方法,黑板提供了这样一个facade层,使得RWD的接口保持统一。
  • 数据的维护和管理:黑板提供数据的RWD,生命期,作用域等内容,让我们可以从管理数据的漩涡中解脱出来,让专业的人做专业的事。
举个一个很简单的游戏中的例子来说明最后一点,比如我们在游戏中有一个技能,可以给角色提供一种狂暴状态,持续10秒,游戏中很多别的系统在计算中,需要检查该角色是否有这样的一个狂暴的状态,然后做一些后续的判断。在这样一个例子中,常规的做法可能是,在角色上存一个变量,技能触发的时候,置成True,然后维护一个计时器,设为10秒,每帧检查这个计时器,当时间到了,就把这个值再置成False,再提供一个get方法给外部系统调用。这样的逻辑正确,但相对繁琐,不够优雅,如果我们换用黑板系统来维护这个数据应该怎么写呢?就一句话
player.GetBB().SetValue(BBKEY_FURIOUS, true).SetExpiredTiime(10)
我们先获取了黑板的实例(GetBB),然后设置了变量为True(SetValue),然后再设置了过期时间为10秒(SetExpiredTime),这样在10秒内如果访问这个变量,会返回True,但如果过了10秒,这个变量就会返回False,而所有对于数据的管理就被完整的封装在了黑板的实现中。 当然,黑板可以有很多块,像我上面的例子,我就是在角色身上建了一块黑板,用来存储与角色相关的数据,还可以建一块全局的黑板,用来存储整个游戏层面上的数据通信。不管建了几块这样的黑板,它的原理都是一样的,具体如何选择,还是取决于实际情况。 有人可能会说,我把变量一个一个具体定义,和存在黑板中用key-value的结构好像区别也不大,确实,用黑板确实能带来一些好处,但好处还不够多。但黑板有一个另外的优势,那就是支持可视化编程和数据驱动,结合现在的引擎来看,这样的好处真是大大的。现在主流的引擎,都会提供一个强大的可视化的编辑器,通过一些UI上的操作,就能完成一些复杂的游戏逻辑,像行为树和状态机在游戏行业的经久不衰,一方面是因为它的概念比较简单和直观,另一方面也是因为它在可视化编程和数据驱动方面的优势。黑板在这样的潮流中,也是一点不落后。 首先它采用的存储方式是key-value的字典结构,很通用,可以通过配置文件简单定义,通过范型和反射很容易去创建,修改和读取。其次它作为共享数据,可以很好的和类似行为树和状态机这样的系统协同工作。 我用行为树举一个例子(不熟悉行为树的,可以查看博客其他介绍行为树的文章),行为树的节点间也是存在通信的需求的,最常见的就是序列节点,比如我们有一个简单的攻击序列节点,第一个节点是选择目标,第二个节点是攻击,这里就存在一个节点间通信的需求,在"选择目标"的节点里会选择一个攻击目标,然后在攻击的节点里会对这个目标实施攻击。所以"攻击目标"这个数据就会在两个节点间进行通信,第一个节点输出,第二个节点输入,那这个数据应该存在哪里呢?存在角色身上是一个选择,还有一个选择,就是存在与这个行为树绑定的黑板上面,在Unity的Behaivor Design这个行为树插件里,这样的变量就叫共享变量,它的概念其实就是和黑板类似的(它在两个节点中分别创建了一个指向这个共享变量的引用,主要是方便编辑器操作和代码上的访问),在编辑器中,我们就可以创建这样一个变量,然后把它拖到第一个和第二个节点的相应变量里。 seq-share 状态机也是一样的,当各个状态跳转的时候,势必也会带来一些数据的通信,这个时候,黑板就能很好的帮助这样的系统进行共享数据的管理,关于状态机的例子,大家可以看Unity上一个状态机的插件PlayMaker。 所以黑板是一个很好的共享数据系统,我很推荐大家在自己的代码库中加一个黑板的库,并应用到你核心游戏部分的实现中,这个小小的东西,会带来很大的思维和代码质量的提升。如果还不是很熟悉的同学,可以去用用看我刚刚说到Unity的那两个插件,这样你就会对数据通信,共享数据,黑板等概念更为清楚。 参考: https://en.wikipedia.org/wiki/Blackboard_system http://www.hutonggames.com/ http://www.opsive.com/ ———————————————————————— 作者:Finney Blog:AI分享站(http://www.aisharing.com/) Email:finneytang@gmail.com 本文欢迎转载和引用,请保留本说明并注明出处 ————————————————————————]]>
“黑板”(Blackboard)在人工智能领域已经是一个很古老的东西了,它基于一种很直观的概念,就是一群人为了解决一个问题,在黑板前聚集,每个人都可以发表自己的意见,然后在黑板上写下自己的看法,当然你也可以基于别人记录在黑板上的看法,来发表和更新自己的看法,在这样不断的意见交换,看法更新的过程中,越来越趋向于对于问题的最终解答。一开始的黑板系统就是这样一个由多个子系统来共同协作的人工智能解决方案。

blackboard

基于上面的描述,我们可以看到黑板有几个功能:

  • 记录:每个人可以写下自己的看法。
  • 更新:调整已有的看法。
  • 删除:删除对于过时的,或者错误的看法。
  • 读取:黑板上的内容谁都能自由阅读。

所以从本质上来说,黑板就是这样一个共享数据的结构,它对于多个系统间通信是很有帮助的,从程序设计的角度上来说,它提供一种数据传递的方式,有助于系统的封装和解耦合。对于各个子系统而言,只需要把自己的运算的结果数据记录在黑板上,至于这个数据谁会去用,并不需要关心,反过来也是一样,对于自己的运算时需要用到的数据,可以从黑板上去获取,至于这个数据是谁提供的,也不需要关心,只要这个数据在黑板上,就够可以认为是合法数据。这就提供的了一种灵活性,各个子系统的设计也会相对独立。

当然黑板也有些不足的地方,比如RWD(读写删)操作相对随意,特别是WD操作,容易造成数据被破坏,或者产生子系统间的竞争,比如,系统A和系统B都会去修改data1,那到底以谁的值为准呢?还有一个情况就是产生非法数据,一般认为,只要在黑板上的数据,就是合法的数据,在读取的时候,不需要判断它是否合法,但如果一个子系统没有很好的维护它自己产生的数据(比如,该删除的时候没删除,或者赋值错误),那别人读取该数据的系统时候,就会产生错误的运算结果。

博客上有一篇较早的文章就讨论过这样的问题,像黑板这样的共享数据结构,既是黄金屋,又是垃圾堆,用好不容易,所以在黑板原有的功能中,我们可以加一些额外的功能:

  • 数据过期时间:对于写入黑板的数据,可以加一个过期时间的功能,比如3秒后,该数据过期,这很实用,可以提高数据维护的便利程度。
  • 数据作用域:我们可以规定可以读写该数据子系统,默认情况下,黑板的数据都是全局可见的,就像程序中的全局变量一样,但如果我们希望某些数据只有对个别子系统开放,就可以通过作用域字段来指定。

说了一大堆,反过来,我们还是要讨论游戏,现在游戏中,也大量的使用黑板(或者类黑板)系统,因为游戏系统的模块间通信的需求也是很多的,AI,动画,物理,实体与实体间,等等,他们都需要彼此交换数据,我想,大家经常碰到的一个头疼的问题就是,这个数据应该存在哪里?存在这里也可以,存在那里也可以,或者索性做个Data类来存,所以在Player类里,变量会越来越多,变量列表越来越长。

针对这种情况黑板可以帮助解决一部分问题,特别是对于在多模块之间需要通信的数据,我们再来看一下它几个好处:

  • 解耦合:黑板做为独立的数据模块,可以”超然”于所有的模块之外,提供一些额外的数据维护和管理的功能,这个让我想到了那些内存数据库,比如redis和memcached,从某种程度上,黑板就像程序内的数据库。
  • 共享性:黑板的数据是共享的,比如我们要去拿一个数据,我们不需要先拿到它的实例(还需要考虑是否为null),然后再通过get方法去取数据,我们只需要存一个黑板的实例,然后通过黑板获取数据的方法来获取。这就类似设计模式中的Facade方法,黑板提供了这样一个facade层,使得RWD的接口保持统一。
  • 数据的维护和管理:黑板提供数据的RWD,生命期,作用域等内容,让我们可以从管理数据的漩涡中解脱出来,让专业的人做专业的事。

举个一个很简单的游戏中的例子来说明最后一点,比如我们在游戏中有一个技能,可以给角色提供一种狂暴状态,持续10秒,游戏中很多别的系统在计算中,需要检查该角色是否有这样的一个狂暴的状态,然后做一些后续的判断。在这样一个例子中,常规的做法可能是,在角色上存一个变量,技能触发的时候,置成True,然后维护一个计时器,设为10秒,每帧检查这个计时器,当时间到了,就把这个值再置成False,再提供一个get方法给外部系统调用。这样的逻辑正确,但相对繁琐,不够优雅,如果我们换用黑板系统来维护这个数据应该怎么写呢?就一句话

player.GetBB().SetValue(BBKEY_FURIOUS, true).SetExpiredTiime(10)

我们先获取了黑板的实例(GetBB),然后设置了变量为True(SetValue),然后再设置了过期时间为10秒(SetExpiredTime),这样在10秒内如果访问这个变量,会返回True,但如果过了10秒,这个变量就会返回False,而所有对于数据的管理就被完整的封装在了黑板的实现中。

当然,黑板可以有很多块,像我上面的例子,我就是在角色身上建了一块黑板,用来存储与角色相关的数据,还可以建一块全局的黑板,用来存储整个游戏层面上的数据通信。不管建了几块这样的黑板,它的原理都是一样的,具体如何选择,还是取决于实际情况。

有人可能会说,我把变量一个一个具体定义,和存在黑板中用key-value的结构好像区别也不大,确实,用黑板确实能带来一些好处,但好处还不够多。但黑板有一个另外的优势,那就是支持可视化编程和数据驱动,结合现在的引擎来看,这样的好处真是大大的。现在主流的引擎,都会提供一个强大的可视化的编辑器,通过一些UI上的操作,就能完成一些复杂的游戏逻辑,像行为树和状态机在游戏行业的经久不衰,一方面是因为它的概念比较简单和直观,另一方面也是因为它在可视化编程和数据驱动方面的优势。黑板在这样的潮流中,也是一点不落后。

首先它采用的存储方式是key-value的字典结构,很通用,可以通过配置文件简单定义,通过范型和反射很容易去创建,修改和读取。其次它作为共享数据,可以很好的和类似行为树和状态机这样的系统协同工作。

我用行为树举一个例子(不熟悉行为树的,可以查看博客其他介绍行为树的文章),行为树的节点间也是存在通信的需求的,最常见的就是序列节点,比如我们有一个简单的攻击序列节点,第一个节点是选择目标,第二个节点是攻击,这里就存在一个节点间通信的需求,在”选择目标”的节点里会选择一个攻击目标,然后在攻击的节点里会对这个目标实施攻击。所以”攻击目标”这个数据就会在两个节点间进行通信,第一个节点输出,第二个节点输入,那这个数据应该存在哪里呢?存在角色身上是一个选择,还有一个选择,就是存在与这个行为树绑定的黑板上面,在Unity的Behaivor Design这个行为树插件里,这样的变量就叫共享变量,它的概念其实就是和黑板类似的(它在两个节点中分别创建了一个指向这个共享变量的引用,主要是方便编辑器操作和代码上的访问),在编辑器中,我们就可以创建这样一个变量,然后把它拖到第一个和第二个节点的相应变量里。

seq-share

状态机也是一样的,当各个状态跳转的时候,势必也会带来一些数据的通信,这个时候,黑板就能很好的帮助这样的系统进行共享数据的管理,关于状态机的例子,大家可以看Unity上一个状态机的插件PlayMaker

所以黑板是一个很好的共享数据系统,我很推荐大家在自己的代码库中加一个黑板的库,并应用到你核心游戏部分的实现中,这个小小的东西,会带来很大的思维和代码质量的提升。如果还不是很熟悉的同学,可以去用用看我刚刚说到Unity的那两个插件,这样你就会对数据通信,共享数据,黑板等概念更为清楚。

参考:

https://en.wikipedia.org/wiki/Blackboard_system

http://www.hutonggames.com/

http://www.opsive.com/

————————————————————————
作者:Finney
Blog:AI分享站(http://www.aisharing.com/)
Email:finneytang@gmail.com
本文欢迎转载和引用,请保留本说明并注明出处
————————————————————————


评论

  • 2016 年 8 月 15 日, 匿名 评论到: Nice
  • 2016 年 10 月 20 日, 风清扬笑 评论到: "存在这里也可以,存在那里也可以,或者索性做个Data类来存,所以在Player类里,变量会越来越多,变量列表越来越长。" 如果单纯是数据的话,确实可以用黑板,如果这个数据是某个模块中的数据的话,我倾向于用《子类沙盒模式》,把相关的接口和数据都封装到一个小模块中,比如段位赛模块,会封装一个rankpk模块,然后作为一个成员放到player下。 "比如我们在游戏中有一个技能,可以给角色提供一种狂暴状态,持续10秒,游戏中很多别的系统在计算中,需要检查该角色是否有这样的一个狂暴的状态,然后做一些后续的判断"。一般实现这种都不会简单的用变量来存,会用一个状态管理器来做,这个状态持续多久,对应的数值多少都应该有一套机制。
  • 2017 年 3 月 5 日, 迪杰斯特拉 评论到: 博主你好,我很喜欢你的文章,我最近想出一个关于行为树的视频教程,想使用你那个共享型行为树的例子和代码,我在视频中会表明出处,请问下可以吗
  • 2017 年 3 月 8 日, Finney 评论到: 你好,很高兴能对你有帮助,我博客上的文章是遵循“知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。”,如果不违反这里的要求,可以自由使用,注明出处即可
  • 2017 年 3 月 16 日, 迪杰斯特拉 评论到: 谢谢
  • 2017 年 7 月 19 日, 匿名 评论到: 对于MMO的战斗房间,我会把数据存在人物身上。比如进入战斗时,每个人的装备、附属系统加成等。而对于主城,偏向于UI系统的,一般是对每个功能做全局的数据单例。这样网络收发、界面填充,都共享使用这个单例。
知识共享许可协议
本博客作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
捐赠本站 ]]>
http://www.aisharing.com/archives/801/feed 6
核心游戏系统架构设计 http://www.aisharing.com/archives/769 http://www.aisharing.com/archives/769#comments Sun, 14 Feb 2016 10:28:17 +0000 http://www.aisharing.com/?p=769 过段时间会在公司做一个讲座课程,这个文章就是在准备过程中,用来整理思路的,里面的一些内容,在以前的博客上也有提及,有兴趣的可以去翻翻,这次系统整理一下,自己的思路也更清晰了,予人玫瑰,手留余香。 首先先来定义一下什么是我这里说的核心游戏系统,一般来说,游戏可以大致分为两个部分,一个部分是我这里指的核心游戏部分,比如FPS里的射击战斗部分,或者如LOL里的战斗对抗部分,又或者是体育类游戏里的比赛部分等等。 fps lol fifa 这些是游戏里的主要玩的点,核心游戏部分可以很重,占到玩家80%以上的游戏时间,也可以很轻,甚至没有,像现在很火的列王的纷争(COK),几乎就是没有什么核心游戏部分。另一部分就是外围的辅助系统,比如装备,任务,社交等等,这部分也有玩点和设计用意,这两个部分相辅相成组成了大部分游戏的主体框架。而今天要聊的就是第一部分的核心游戏系统。 从开始做游戏到现在,我大部分的工作是专注在引擎以及核心游戏系统部分,所以今天就想来聊聊如何来设计核心游戏系统。当然这个设计不一定适用于所有的游戏,仅仅是我个人的经验之谈,希望能给大家一些参考的价值。 大多数情况下,核心游戏系统都比较复杂,牵涉到很多系统之间的协作,也和策划的需求有相当紧密的联系,国外公司一般称这类程序员为Gameplay programmer,国内公司这种职位相对较少,一般就以泛指的客户端程序员代替了,但和做外围系统的程序员不同,真正的Gameplay programmer需要对于AI系统,动画系统,物理系统都有一定的了解,因为这是核心游戏部分都会涉及的领域。正因为核心游戏系统的复杂性,所以必须要有一个适合的,灵活的架构来支持,抛开一些基本的优点,诸如可扩展性,低耦合等等要求不说,我最直观的感受,就是以下两点好处:
  • 多人合作:一个好的架构可以将系统进行合理的拆分,这样的话就便于多人协作,对于核心游戏系统来说,一般是不可能一个人单枪匹马的去完成的,所以如何去拆分任务让更多的人参与,对于项目而言是相当有利的。对于现在的AAA的游戏来说,单单一个主角,可能就会有将近10个程序员在一起制作,包括AI,行为,动画等等,所以好的架构可以保证工作效率随着人数的增加而得到提升
  • 留有“挥霍”的空间:架构是很难完美的,因为在开始设计的时候,所有的需求并不明确,特别是核心游戏系统,可能会推倒重来,重构很多次。而当游戏方向定下来了之后,一些策划的改动或者扩展,也会使得以前的架构在某些情况下变得不是很适用,这个时候就会需要一些对于特殊情况的处理,也就是所谓的“hack”,好的架构会让我们在开发后期,在不重构的情况下,有余地进行适当的“hack”,而不是在一开始就“hack”到底,导致bug满天飞。
好,接下来开始说说设计思路。 解构一个复杂的系统,有一个很好(不是唯一)的办法,就是“分层结构”(Layered structure),也就是把一个复杂系统,分成一层一层的结构,每一层都做每一层自己的事情,并且每一层都是单向依赖。这样可以把一个网状的,如同乱线团一样的复杂系统,梳理的非常的清楚。这样的例子其实很多,比如学计算机的人都很熟悉的OSI网络七层架构,这就是一个非常好的,把复杂问题层次化的典型例子,它使得每一层都可以独立设计,而且可以有明确的设计目标,层与层之间的接口也变得非常清晰。 osi 还有一个游戏的例子,就是游戏的架构,游戏其实也是遵照这层次化的设计思路来设计的,虽然不像OSI那样有一个标准化的结构,但是大部分游戏可以分为核心层(Core),引擎层(Engine),游戏类型层(Game Genre),游戏层(Game),像现在一般的商用的游戏引擎,基本就做到核心层和引擎层,再往上就是使用引擎的人自己设计和实现了,像一些大公司可能会有一些积累,就会根据不同的游戏类型在引擎层的之上抽象出游戏类型层,比如体育类游戏,射击类游戏等等,然后再开始开发实际的游戏产品。这种分层的架构设计就可以帮助我们把复杂的系统进行解构,从而实现每个子系统或者模块的功能单一化。 engine 核心游戏系统架构也可以用这样的思路来设计,这样每一层都可以由不同的人来负责,如果实现的话,在一层当中也可以进行任务分工,那下面我就根据执行顺序,自上而下一层一层来描述 总共的架构分为五层 main

第一层:更新/收集世界信息

这部分主要是要设计两个部分,一个是知识池(Knowledge Pool),另一个就是感知器(Sensor)。听上去很高大上,其实概念上很简单,知识池可以理解为就是世界信息的数据存储,比如某一个智能体需要一个这样的数据,“谁是离我最近的人”,这个数据就可以存下来,方便获取,写成代码的话,就类似于这样:
KnowledgePool().Get(WHO_IS_NEAREST_TO_ME, meEntity)
这种数据存储的数据结构,可以根据不同的情况去选择,用key-value的黑板格式,或者自定义的数据类型都可以,也可以分成多个知识池来管理不同类型的数据,设计的关键就是要有清晰的世界信息获取方式。 感知器的话,就可以理解为具体的获取数据的方法,可以定义一个感知器的接口类:
interface ISensor {
    Update()
}
这样就可以把这个感知器注册到一个感知器的管理器中,当收集所有世界信息的时候,只要遍历一遍这些感知器,就可以完成对于世界信息的收集工作:
foreach(s in sensers){
    s.Update()
}
感知器也可以分为两种,一种是全局的感知器,这种可以看成是对于游戏整个世界,或者关卡的抽象,比如势力图 im_pic_4 还有一种是个体感知器,比如听力,视野等等 sensor 当然,这只是一种思路,也可以不定义接口,而是写成不同的数据更新方法。这就是第一层,主要的功能就是为下层预备数据。 第二层往下,都是针对单个智能体的更新,也就是说需要对每一个智能体执行更新操作。关于智能体的行为,很多时候容易写的一团糟,又要决策,又要运动,又有动画,还要处理物理,有些系统呢,需要每帧更新,比如位置,有些呢,又不需要更新的这么勤快,比如决策,所以在设计上我把它分成几个层次,决策层,请求层,行为层,运动层。

第二层:更新决策层(做什么)- What to do

决策层就是负责来决策此时该智能体应该要做什么,比如我要走到某个位置,我要攻击,放技能等等,可以说,这就是传统所说的人工智能AI部分,这部分只根据当前所有的世界信息,产生“做什么”的决策,决策的内容会封装在一个“请求”(Request)的结构中,继续向下传递: 这部分的结构可以有多种选择,状态机,行为树,甚至神经网络都可以,但是有两个要点
  • 决策的时机:也就是什么时候进行决策,这里就可以用来控制决策的频率,比如离玩家很远的人可以降低决策频度,离玩家近的人,可以提高决策频度等等,类似与这种的控制都应该在这一层中得到支持和实现。
  • 决策请求的类型:这一层的输出可以看成是所有该智能体可以做的决策的总和,所以千万不要把一些下层的行为放在这里,比如寻路,接下来会说到,寻路并不是决策层的决策行为,它只是来处理“移动”这个决策的一个方法。还有比如选择动画,也不应该是决策层所要关心的内容。
还有一种特殊的模块是属于这一层,那就是玩家输入,玩家的输入说起来,其实也是一种决策,只是这个决策是通过玩家来做出的。行为树就很容易处理这个情况,将玩家输入和AI决策可以融合成一体让所有的智能体共用: btstrategy

第三层:更新请求层

上面说到,决策层产生的输出是“请求”,请求是一种自定义的数据结构,包含所以该决策所需要传递的决策信息。那为什么要更新请求层呢?直接把当前请求传递给下一层不就好了吗?这一层的功能抽象,也是我在实践中的经验,总结一句话,“请求层”就类似于一个“防火墙”,由它来“过滤”,那些请求会被继续往下传递到行为层。 我们可以先来看一下请求层的设计,请求层的设计,借鉴了渲染中的“双缓冲”结构,把请求分为,前端请求(Foreground Request),后端请求(Background Request),前端请求就是当前智能体正在执行的请求,因为一个决策请求可能需要多帧才能完成(想象一下,移动到某一个点这个请求,就需要一段时间才能完成),后端请求就是准备执行决策请求,当一定条件满足后,就可以做了一个“Flip”的操作(前后互换),把后端请求变成前端请求,这样的话,后一层就会执行这个请求,从而改变行为了。 layered-ai-architecture-1 细心的同学会发现,在上面的描述中,有一个地方值得回味,就是Flip操作是,“当一定条件满足后”,而这个条件的监测,就是这里更新请求层的时候需要做的事情了,其实每一个决策是存在潜在的优先级的,这个优先级和策划的设计有关,比如我当前正在执行一个攻击的请求,这个时候,新的请求是一个释放技能,此时策划要求,技能释放能够打断当前的攻击行为,那这个判断逻辑就可以写在这一层中,使得当这个条件满足时,可以立即切换请求。这里的设计一般采用配优先级表,和基于规则的(Rule-based)的实现方式。 有了这一层的逻辑抽象,就可以保证它的上层和下层都不需要关心决策能否被执行,而只要关心自身的决策/行为逻辑就可以了,大大降低了上下层的实现复杂度。由于这部分逻辑相对比较繁琐,所以把这些繁琐的逻辑集中在一起,也是一种理想的设计思路。

第四层:更新行为层(怎么做)- How to do

就像补充里所描述的,行为层的职责就是怎么做,也就是如何去完成上层经过决策,经过规则的逻辑判定,最终“胜出”的那个前端请求。像前面提到的寻路,或者选择需要播放的动画,都是在这一层所完成的工作,这层的实现同样可以用行为树,或者状态机,不过我还是推荐用行为树,因为行为树可以扩展和处理更复杂的行为逻辑,比如随机,序列,并行等等。某些请求可能不是用单个行为可以完成的,需要多个行为的配合,比如完成一个技能释放,需要先集气,然后再释放,类似这样的行为,就可以用行为树来实现了,更棒的是,集气这个行为还能被共用。 skillinbev 在这一层中,会产生一系列的输出,有特效,有动画,可能还有声音等等,有一个很重要,也是必不可少的,那就是运动信息(Kinematic Information),这也是智能体最终呈现的样子,这部分内容包括空间信息(位移,旋转,缩放)和动画信息,某些情况下,智能体的空间信息可以通过物理计算在这一层直接更新,动画信息直接调用引擎的播放接口即可,但有时候这种处理还不够,那就需要第五层,运动层的参与。

第五层:更新运动层

游戏中物体的移动有两种方式,一种是动画跟随物理,比如为了解决移动中的滑步问题,我们可以做多个移动的动画,然后根据速度做融合,这样可以调出一个滑步不明显的表现,还有一种就是root motion,也就是物理跟随动画,就是物体的移动和旋转完全跟随动画中的效果,这样可以解决一些物理没有办法模拟的复杂运动。 有些时候,我们需要混合使用这两种方式,并且在位移过程中需要加上一些修正(比如为了解决同步问题),这个时候,就需要用运动层来实现。这一层的输入,就是行为层产生的运动信息,输出自然就是智能体最终的空间信息和动画了。 在实现上,建议对这两种方式进行封装,这样可以对于上层来说,接口就相对统一了。 有了这五层的设计,整个核心游戏系统的更新循环就完成了,并且每一层的功能职责和输入输出都有了明确的定义,如下图: 5th 每一层具体的设计可以仁者见仁,智者见智,并且也和具体的游戏有关,但是整体的架构基本就可以参考这样的思路,至少以我的实践来看,不会导致结构混乱,也可以更好的进行分工和合作。
最后再聊一个关于核心游戏部分网络同步和回放系统的问题(同步和回放是差不多的东西,回放只是把同步的东西存下来而已)。 其实如果有了上面的架构设计,理解网络同步就很简单了。
  • 如果同步放在第二层,那就是采用的“同步输入”(Input synchronization)的方式
inputsync
  • 如果在第三层,那就是“同步命令”(Command synchronization)的方式
cmdsync
  • 如果放在第四层/第五层,那就是“同步状态”(State synchronization)的方式
statesync   这三种方式是越往下传输的数据越多,但是“失同步”(Out of synchronization)的风险就越小。当然不同的方式在具体实现上,还是有很多值得讨论的地方,这里就不多说了。如果客户端和游戏服务器采用相同的语言,那就可以很方便的在单机游戏和网络游戏间切换,在单机模式下,只是本地和本地通信罢了,FPS游戏很多都是这样去实现的,其实在单机模式下,内部也是一个CS的架构,而如果需要一个服务器的版本,只是加一个宏去编译而已。 好了,就说这么多,欢迎讨论,也可以下载那个僵尸的demo,里面也采用了类似的层次结构,可以参考一下。 ———————————————————————— 作者:Finney Blog:AI分享站(http://www.aisharing.com/) Email:finneytang@gmail.com 本文欢迎转载和引用,请保留本说明并注明出处 ————————————————————————]]>

过段时间会在公司做一个讲座课程,这个文章就是在准备过程中,用来整理思路的,里面的一些内容,在以前的博客上也有提及,有兴趣的可以去翻翻,这次系统整理一下,自己的思路也更清晰了,予人玫瑰,手留余香。

首先先来定义一下什么是我这里说的核心游戏系统,一般来说,游戏可以大致分为两个部分,一个部分是我这里指的核心游戏部分,比如FPS里的射击战斗部分,或者如LOL里的战斗对抗部分,又或者是体育类游戏里的比赛部分等等。

fps lol fifa

这些是游戏里的主要玩的点,核心游戏部分可以很重,占到玩家80%以上的游戏时间,也可以很轻,甚至没有,像现在很火的列王的纷争(COK),几乎就是没有什么核心游戏部分。另一部分就是外围的辅助系统,比如装备,任务,社交等等,这部分也有玩点和设计用意,这两个部分相辅相成组成了大部分游戏的主体框架。而今天要聊的就是第一部分的核心游戏系统。

从开始做游戏到现在,我大部分的工作是专注在引擎以及核心游戏系统部分,所以今天就想来聊聊如何来设计核心游戏系统。当然这个设计不一定适用于所有的游戏,仅仅是我个人的经验之谈,希望能给大家一些参考的价值。

大多数情况下,核心游戏系统都比较复杂,牵涉到很多系统之间的协作,也和策划的需求有相当紧密的联系,国外公司一般称这类程序员为Gameplay programmer,国内公司这种职位相对较少,一般就以泛指的客户端程序员代替了,但和做外围系统的程序员不同,真正的Gameplay programmer需要对于AI系统,动画系统,物理系统都有一定的了解,因为这是核心游戏部分都会涉及的领域。正因为核心游戏系统的复杂性,所以必须要有一个适合的,灵活的架构来支持,抛开一些基本的优点,诸如可扩展性,低耦合等等要求不说,我最直观的感受,就是以下两点好处:

  • 多人合作:一个好的架构可以将系统进行合理的拆分,这样的话就便于多人协作,对于核心游戏系统来说,一般是不可能一个人单枪匹马的去完成的,所以如何去拆分任务让更多的人参与,对于项目而言是相当有利的。对于现在的AAA的游戏来说,单单一个主角,可能就会有将近10个程序员在一起制作,包括AI,行为,动画等等,所以好的架构可以保证工作效率随着人数的增加而得到提升
  • 留有“挥霍”的空间:架构是很难完美的,因为在开始设计的时候,所有的需求并不明确,特别是核心游戏系统,可能会推倒重来,重构很多次。而当游戏方向定下来了之后,一些策划的改动或者扩展,也会使得以前的架构在某些情况下变得不是很适用,这个时候就会需要一些对于特殊情况的处理,也就是所谓的“hack”,好的架构会让我们在开发后期,在不重构的情况下,有余地进行适当的“hack”,而不是在一开始就“hack”到底,导致bug满天飞。

好,接下来开始说说设计思路。

解构一个复杂的系统,有一个很好(不是唯一)的办法,就是“分层结构”(Layered structure),也就是把一个复杂系统,分成一层一层的结构,每一层都做每一层自己的事情,并且每一层都是单向依赖。这样可以把一个网状的,如同乱线团一样的复杂系统,梳理的非常的清楚。这样的例子其实很多,比如学计算机的人都很熟悉的OSI网络七层架构,这就是一个非常好的,把复杂问题层次化的典型例子,它使得每一层都可以独立设计,而且可以有明确的设计目标,层与层之间的接口也变得非常清晰。

osi

还有一个游戏的例子,就是游戏的架构,游戏其实也是遵照这层次化的设计思路来设计的,虽然不像OSI那样有一个标准化的结构,但是大部分游戏可以分为核心层(Core),引擎层(Engine),游戏类型层(Game Genre),游戏层(Game),像现在一般的商用的游戏引擎,基本就做到核心层和引擎层,再往上就是使用引擎的人自己设计和实现了,像一些大公司可能会有一些积累,就会根据不同的游戏类型在引擎层的之上抽象出游戏类型层,比如体育类游戏,射击类游戏等等,然后再开始开发实际的游戏产品。这种分层的架构设计就可以帮助我们把复杂的系统进行解构,从而实现每个子系统或者模块的功能单一化。

engine

核心游戏系统架构也可以用这样的思路来设计,这样每一层都可以由不同的人来负责,如果实现的话,在一层当中也可以进行任务分工,那下面我就根据执行顺序,自上而下一层一层来描述

总共的架构分为五层

main


第一层:更新/收集世界信息

这部分主要是要设计两个部分,一个是知识池(Knowledge Pool),另一个就是感知器(Sensor)。听上去很高大上,其实概念上很简单,知识池可以理解为就是世界信息的数据存储,比如某一个智能体需要一个这样的数据,“谁是离我最近的人”,这个数据就可以存下来,方便获取,写成代码的话,就类似于这样:

KnowledgePool().Get(WHO_IS_NEAREST_TO_ME, meEntity)

这种数据存储的数据结构,可以根据不同的情况去选择,用key-value的黑板格式,或者自定义的数据类型都可以,也可以分成多个知识池来管理不同类型的数据,设计的关键就是要有清晰的世界信息获取方式。

感知器的话,就可以理解为具体的获取数据的方法,可以定义一个感知器的接口类:

interface ISensor {
    Update()
}

这样就可以把这个感知器注册到一个感知器的管理器中,当收集所有世界信息的时候,只要遍历一遍这些感知器,就可以完成对于世界信息的收集工作:

foreach(s in sensers){
    s.Update()
}

感知器也可以分为两种,一种是全局的感知器,这种可以看成是对于游戏整个世界,或者关卡的抽象,比如势力图

im_pic_4

还有一种是个体感知器,比如听力,视野等等

sensor

当然,这只是一种思路,也可以不定义接口,而是写成不同的数据更新方法。这就是第一层,主要的功能就是为下层预备数据。

第二层往下,都是针对单个智能体的更新,也就是说需要对每一个智能体执行更新操作。关于智能体的行为,很多时候容易写的一团糟,又要决策,又要运动,又有动画,还要处理物理,有些系统呢,需要每帧更新,比如位置,有些呢,又不需要更新的这么勤快,比如决策,所以在设计上我把它分成几个层次,决策层,请求层,行为层,运动层。

第二层:更新决策层(做什么)- What to do

决策层就是负责来决策此时该智能体应该要做什么,比如我要走到某个位置,我要攻击,放技能等等,可以说,这就是传统所说的人工智能AI部分,这部分只根据当前所有的世界信息,产生“做什么”的决策,决策的内容会封装在一个“请求”(Request)的结构中,继续向下传递:

这部分的结构可以有多种选择,状态机,行为树,甚至神经网络都可以,但是有两个要点

  • 决策的时机:也就是什么时候进行决策,这里就可以用来控制决策的频率,比如离玩家很远的人可以降低决策频度,离玩家近的人,可以提高决策频度等等,类似与这种的控制都应该在这一层中得到支持和实现。
  • 决策请求的类型:这一层的输出可以看成是所有该智能体可以做的决策的总和,所以千万不要把一些下层的行为放在这里,比如寻路,接下来会说到,寻路并不是决策层的决策行为,它只是来处理“移动”这个决策的一个方法。还有比如选择动画,也不应该是决策层所要关心的内容。

还有一种特殊的模块是属于这一层,那就是玩家输入,玩家的输入说起来,其实也是一种决策,只是这个决策是通过玩家来做出的。行为树就很容易处理这个情况,将玩家输入和AI决策可以融合成一体让所有的智能体共用:

btstrategy

第三层:更新请求层

上面说到,决策层产生的输出是“请求”,请求是一种自定义的数据结构,包含所以该决策所需要传递的决策信息。那为什么要更新请求层呢?直接把当前请求传递给下一层不就好了吗?这一层的功能抽象,也是我在实践中的经验,总结一句话,“请求层”就类似于一个“防火墙”,由它来“过滤”,那些请求会被继续往下传递到行为层。

我们可以先来看一下请求层的设计,请求层的设计,借鉴了渲染中的“双缓冲”结构,把请求分为,前端请求(Foreground Request),后端请求(Background Request),前端请求就是当前智能体正在执行的请求,因为一个决策请求可能需要多帧才能完成(想象一下,移动到某一个点这个请求,就需要一段时间才能完成),后端请求就是准备执行决策请求,当一定条件满足后,就可以做了一个“Flip”的操作(前后互换),把后端请求变成前端请求,这样的话,后一层就会执行这个请求,从而改变行为了。

layered-ai-architecture-1

细心的同学会发现,在上面的描述中,有一个地方值得回味,就是Flip操作是,“当一定条件满足后”,而这个条件的监测,就是这里更新请求层的时候需要做的事情了,其实每一个决策是存在潜在的优先级的,这个优先级和策划的设计有关,比如我当前正在执行一个攻击的请求,这个时候,新的请求是一个释放技能,此时策划要求,技能释放能够打断当前的攻击行为,那这个判断逻辑就可以写在这一层中,使得当这个条件满足时,可以立即切换请求。这里的设计一般采用配优先级表,和基于规则的(Rule-based)的实现方式。

有了这一层的逻辑抽象,就可以保证它的上层和下层都不需要关心决策能否被执行,而只要关心自身的决策/行为逻辑就可以了,大大降低了上下层的实现复杂度。由于这部分逻辑相对比较繁琐,所以把这些繁琐的逻辑集中在一起,也是一种理想的设计思路。

第四层:更新行为层(怎么做)- How to do

就像补充里所描述的,行为层的职责就是怎么做,也就是如何去完成上层经过决策,经过规则的逻辑判定,最终“胜出”的那个前端请求。像前面提到的寻路,或者选择需要播放的动画,都是在这一层所完成的工作,这层的实现同样可以用行为树,或者状态机,不过我还是推荐用行为树,因为行为树可以扩展和处理更复杂的行为逻辑,比如随机,序列,并行等等。某些请求可能不是用单个行为可以完成的,需要多个行为的配合,比如完成一个技能释放,需要先集气,然后再释放,类似这样的行为,就可以用行为树来实现了,更棒的是,集气这个行为还能被共用。

skillinbev

在这一层中,会产生一系列的输出,有特效,有动画,可能还有声音等等,有一个很重要,也是必不可少的,那就是运动信息(Kinematic Information),这也是智能体最终呈现的样子,这部分内容包括空间信息(位移,旋转,缩放)和动画信息,某些情况下,智能体的空间信息可以通过物理计算在这一层直接更新,动画信息直接调用引擎的播放接口即可,但有时候这种处理还不够,那就需要第五层,运动层的参与。

第五层:更新运动层

游戏中物体的移动有两种方式,一种是动画跟随物理,比如为了解决移动中的滑步问题,我们可以做多个移动的动画,然后根据速度做融合,这样可以调出一个滑步不明显的表现,还有一种就是root motion,也就是物理跟随动画,就是物体的移动和旋转完全跟随动画中的效果,这样可以解决一些物理没有办法模拟的复杂运动。

有些时候,我们需要混合使用这两种方式,并且在位移过程中需要加上一些修正(比如为了解决同步问题),这个时候,就需要用运动层来实现。这一层的输入,就是行为层产生的运动信息,输出自然就是智能体最终的空间信息和动画了。

在实现上,建议对这两种方式进行封装,这样可以对于上层来说,接口就相对统一了。

有了这五层的设计,整个核心游戏系统的更新循环就完成了,并且每一层的功能职责和输入输出都有了明确的定义,如下图:

5th

每一层具体的设计可以仁者见仁,智者见智,并且也和具体的游戏有关,但是整体的架构基本就可以参考这样的思路,至少以我的实践来看,不会导致结构混乱,也可以更好的进行分工和合作。


最后再聊一个关于核心游戏部分网络同步和回放系统的问题(同步和回放是差不多的东西,回放只是把同步的东西存下来而已)。

其实如果有了上面的架构设计,理解网络同步就很简单了。

  • 如果同步放在第二层,那就是采用的“同步输入”(Input synchronization)的方式

inputsync

  • 如果在第三层,那就是“同步命令”(Command synchronization)的方式

cmdsync

  • 如果放在第四层/第五层,那就是“同步状态”(State synchronization)的方式

statesync

 

这三种方式是越往下传输的数据越多,但是“失同步”(Out of synchronization)的风险就越小。当然不同的方式在具体实现上,还是有很多值得讨论的地方,这里就不多说了。如果客户端和游戏服务器采用相同的语言,那就可以很方便的在单机游戏和网络游戏间切换,在单机模式下,只是本地和本地通信罢了,FPS游戏很多都是这样去实现的,其实在单机模式下,内部也是一个CS的架构,而如果需要一个服务器的版本,只是加一个宏去编译而已。

好了,就说这么多,欢迎讨论,也可以下载那个僵尸的demo,里面也采用了类似的层次结构,可以参考一下。

————————————————————————
作者:Finney
Blog:AI分享站(http://www.aisharing.com/)
Email:finneytang@gmail.com
本文欢迎转载和引用,请保留本说明并注明出处
————————————————————————


评论

  • 2016 年 4 月 17 日, 匿名 评论到: 很棒
  • 2016 年 5 月 19 日, 匿名 评论到: 学习了!谢谢
  • 2017 年 2 月 8 日, 马三小伙儿 评论到: 最近在接触游戏AI,国内也没有什么太好的书籍,偶然间搜到了博主的网站,细细读来真是受益匪浅啊! 希望博主继续写出更优秀的游戏AI技术文章!大力支持!
  • 2017 年 2 月 14 日, Finney 评论到: 谢谢你的关注!很高兴对你有所帮助
  • 2017 年 4 月 25 日, 匿名 评论到: 学习了,谢谢!
  • 2017 年 6 月 2 日, karl 评论到: 博主,你好,下载僵尸demo用Unity4.6.9打开,demo1的scenes中没有内容
  • 2017 年 6 月 5 日, Finney 评论到: hi,这个demo我是用unity5做的,你用新的unity打开试试
  • 2017 年 7 月 15 日, 匿名 评论到: 感觉学到了很多东西,谢谢博主!
  • 2017 年 10 月 23 日, 匿名 评论到: 博主写的很赞,我也是一名游戏AI开发者,很多博主说的东西,在我的游戏中都有具体的使用,特别是共享型行为树,我对Planner这种AI比较感兴趣,如果博主有空可以聊聊这种AI如何在游戏中应用
  • 2017 年 10 月 26 日, Allen雅伦 评论到: 博主您好,我看了你的很多文章,很多地方让我茅塞顿开,非常感谢。有一个问题我想请教一下。 就是更新/收集世界信息我没有理解透彻。 逻辑是不是这样的 知识池是一个库,里面包含各种感知器感知后的结果,当感知器感知到的元素发生变化时,通知知识库变更,知识库对对应数据进行更新,并通知所有监听这个知识的智能体,是这样的么
  • 2017 年 11 月 8 日, Finney 评论到: 嗯,好的
  • 2017 年 11 月 8 日, Finney 评论到: “通知所有监听这个知识的智能体”,不需要通知,可以存在那里,谁需要自己去取就可以了
  • 2018 年 3 月 29 日, Eric 评论到: 博主,您好,对于“留有‘挥霍’的空间”这部分不是很明白。 我的理解是:在架构的设计时预留一个特殊空间来处理在架构设计时没有考虑周到且后期无法整合到架构中的内容,也就是用于实现一些架构不支持的需求。这样可以保证在不重构架构且不破坏当前整体架构的前提下,满足策划的需求。那是否可以认为如果可能的情况下,在重构架构时最好将“挥霍”空间中的特殊需求考虑进新的架构中,以保证满足当前的所有需求? (“挥霍”空间的需求做为之后优化架构的参考) 如果理解不正确希望博主指出,如果理解完全不着边也希望博主提点~~~~ 谢谢!
  • 2018 年 4 月 3 日, Finney 评论到: 因为实践中经常发生的是,你没有办法预计后面游戏会做成什么样子,在一开始的时候,你得到的需求是不明确的,所以我们只能凭经验去设想以后可能会有什么样的需求,把能考虑到的,尽量做到架构设计中,所以一开始的设计很重要,还有一个原因是,游戏不是一个人开发的,别人在理解你的框架的时候会有偏差,经常也会把框架写乱,设计得好一点的框架,可以把这种情况控制住,让每一个人可以在使用中慢慢理解,而且即使写乱了,不至于太乱:)
知识共享许可协议
本博客作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
捐赠本站 ]]>
http://www.aisharing.com/archives/769/feed 14
共享性行为树的新实践-C#描述 http://www.aisharing.com/archives/750 http://www.aisharing.com/archives/750#comments Thu, 03 Dec 2015 02:18:08 +0000 http://www.aisharing.com/?p=750 这次和大家分享一下,前段时间学习unity和c#的时候,用到的一个行为树的实现方式,使用到了c#语言,并且选择了共享性行为树的方式。行为树的构建,还是用纯代码的方式,没有做工具,也没有做数据驱动的方式来编辑和加载。做工具的话,可能需要很多时间,至于数据驱动,难度不大,如果大家有需要,稍加改进,就应该就能自己实现。 行为树的概念这里就不再赘述,博客上已经有很多了,至于共享性行为树,以前写过两篇文章(12),也做过一个例子,这里再简要提一下,原本的行为树的实现是一个智能体上绑定一个行为树的实例,这个实例里面就包括了构建行为树的结构数据,已经行为树在运行过程中的运行时数据,而对于同一种行为树来说,它的结构数据是一样的,多个实例的话,其实是多个同样的拷贝,这个在内存上是不划算的,特别如果这个智能体是运行在服务器上,比如要跑1000个使用同一种行为树AI的怪,那其实它们可以共享这颗行为树的结构,而只需要实例化运行时数据就可以了,具体可以看下面的图 SharedBevtreeNode.png 这就是共享性行为树的基本概念,可以说这种改进对于内存消耗是有利的(虽然内存很便宜,但能省一点是一点)。我在上次文章中的实现方式是参考了aigamedev.com上的内容,没有真正在项目里用过,这个实现主要由几个问题:
  • 存在大量的内存分配和释放:需要用内存池来管理,增加了复杂度
  • 增加了复杂性:这个实现写起来不够简洁,如果为了省一些内存,平白增加了复杂性,有些得不偿失。
所以我就想用共享性行为树的概念,重新实现一下,其实客户端用不用共享性行为树没什么大关系,因为客户端一般很少会存在同时有大量智能体的情况,对于移动平台,cpu和gpu也会首先撑不住。那为什么我会选用呢?原因有两个:
  • 直接移植了服务器的代码:也是因为学习golang的关系,我先写了个golang语言版本的共享性行为树,所以客户端其实是移植于这个版本的。
  • 新的实现基本和原来的行为树使用方式一致:由于我用的方法,没有很大的增加使用的复杂性,所以秉着“能省一点是一点”的原则,就都用了共享性行为树的方式
这次我还是做了一个untiy能跑的demo,demo里演示的是和原本的行为树demo里类似的行为,不过换成了3D表现,感觉一下子高大上了很多~ old-demo new-demo
基本行为描述如下,首先会每隔一段时间会产生一个移动目标点(白色立方体),然后智能体(僵尸)会旋转并朝向目标点后,朝目标点移动,当到达目标点后,会开始攻击,整个攻击行为会持续5秒钟,最后倒地结束,一直到新的目标点产生,智能体循环执行上述行为。 整个demo可以在untiy5里面执行,打开Scenes目录下面的demo1,执行后,屏幕左上角有一个按钮,每点一次,就会加一个僵尸到场景中,还有一个滑动条,可以加速和减速整个游戏。 demo_debugui 整个demo工程可以点击这里下载 首先介绍一下这次新写的行为树的一些东西。 整个计算要做到结构上的共享,最重要的部分就是做到结构和数据的分离,也就是说整个树的计算是无态的,只要做到了这一点,就可以做到共享行为树的结构,所以在设计上,我把单个智能体在执行的状态数据都放到了传入的Workingdata的结构中,包括控制节点和行为节点上所有定义的状态数据。在这个结构中,我维护了一个字典,当新建行为树每一个节点的时候,都会附带生成一个当前行为树唯一的id值,把这个id做为这个节点的标示,也作为索引状态数据的key值。这样我就可以方便的从WorkingData里取出当前节点所对应的状态数据了 仔细看每一个控制节点的代码的时候,可以发现,所有的运行时数据都包在了一个context的数据结构里,当我要进行行为树计算的时候,就需要先从WorkingData中取出当前节点的context数据,然后根据context数据再进行下一步的计算操作。用户定义的行为节点也一样,例子的话,大家可以看AIBehavior.cs里的NOD_Attack类,看我是如何让僵尸攻击5秒钟的,唯一有一点小小的区别是,在用户的行为节点中所定义的运行时数据是放在TBTActionLeaf::TBTActionLeafContext里的userdata字段中的,我也提供了封装了方法,可以安全的访问。 在这样的封装和改造后,大家可以发现,现在对于行为树行为节点的写法和原本的行为树的写法几乎没有什么区别,但结构上已经变成了共享性的行为树结构。所以,不管我在场景中放了多少个僵尸,其实他们所使用的行为树实例只有一份,具体的可以参看AIBehavior.cs里面的AIEntityBehaviorTreeFactory类 行为树的部分就说这么多了,因为其实大部分的内容和以前分享的差不多,只是这次在结构上做了一些改进,具体的大家还是直接看代码更直观一点。接下来再可以聊聊这个demo所演示的另外一些东西
  • 分层的架构:在GameUpdater这个类里面,我演示了如何分离决策层和行为层,目前的决策层只有一个逻辑,就是产生新的移动目标点,如果当决策层比较复杂的时候,在决策层也可以使用行为树来产生决策,我在现在的项目里就是这样用的,这就是层次化的行为树结构,这样的分离带来的一个额外的好处就是可以方便的分割服务器和客户端的逻辑分离点,换句话说,我们可以很容易的决定,哪些逻辑应该写在服务器上,哪些逻辑应该写在客户端,关于这个话题可以以后继续展开。
  • 决策层和行为层的"粘合剂"--请求:在决策层和行为层的中间,我定义了请求层,在博客的两篇文章(12)中,我也提到过,关于请求层的作用,大家可以去翻一下,在这个demo里,我演示了如何在决策层中产生一个请求,以及如何在行为层中处理一个请求,当然,一般来说,我们会在WorkingData中把请求直接传递到行为层,而不是像我这里所写的,仅仅把一个值通过黑板传递过去
  • 使用黑板:在AI库中,我定义了一个简单的黑板的结构,在demo中,也演示了如何利用黑板传递一些数据,一般我们会将一些行为树需要用的额外数据结构存在黑板中,比如从世界中收集的信息,从其他模块传来的数据等等,黑板的结构非常适合作为这些数据的存储器来使用
关于这个AIToolkit,有时间,我会持续的更新,我的目标是把一些AI中常常会用到的东西做成一个个模块,彼此相对独立,并且尽量轻量化。欢迎多多讨论,也欢迎来信,或者在讨论区交流。 今天就先聊到这里,最后,感谢所有关注和支持我的朋友们,很开心写的东西能对大家有所帮助。
———————————————————————— 作者:Finney Blog:AI分享站(http://www.aisharing.com/) Email:finneytang@gmail.com 本文欢迎转载和引用,请保留本说明并注明出处 ————————————————————————
]]>

这次和大家分享一下,前段时间学习unity和c#的时候,用到的一个行为树的实现方式,使用到了c#语言,并且选择了共享性行为树的方式。行为树的构建,还是用纯代码的方式,没有做工具,也没有做数据驱动的方式来编辑和加载。做工具的话,可能需要很多时间,至于数据驱动,难度不大,如果大家有需要,稍加改进,就应该就能自己实现。

行为树的概念这里就不再赘述,博客上已经有很多了,至于共享性行为树,以前写过两篇文章(12),也做过一个例子,这里再简要提一下,原本的行为树的实现是一个智能体上绑定一个行为树的实例,这个实例里面就包括了构建行为树的结构数据,已经行为树在运行过程中的运行时数据,而对于同一种行为树来说,它的结构数据是一样的,多个实例的话,其实是多个同样的拷贝,这个在内存上是不划算的,特别如果这个智能体是运行在服务器上,比如要跑1000个使用同一种行为树AI的怪,那其实它们可以共享这颗行为树的结构,而只需要实例化运行时数据就可以了,具体可以看下面的图

SharedBevtreeNode.png

这就是共享性行为树的基本概念,可以说这种改进对于内存消耗是有利的(虽然内存很便宜,但能省一点是一点)。我在上次文章中的实现方式是参考了aigamedev.com上的内容,没有真正在项目里用过,这个实现主要由几个问题:

  • 存在大量的内存分配和释放:需要用内存池来管理,增加了复杂度
  • 增加了复杂性:这个实现写起来不够简洁,如果为了省一些内存,平白增加了复杂性,有些得不偿失。

所以我就想用共享性行为树的概念,重新实现一下,其实客户端用不用共享性行为树没什么大关系,因为客户端一般很少会存在同时有大量智能体的情况,对于移动平台,cpu和gpu也会首先撑不住。那为什么我会选用呢?原因有两个:

  • 直接移植了服务器的代码:也是因为学习golang的关系,我先写了个golang语言版本的共享性行为树,所以客户端其实是移植于这个版本的。
  • 新的实现基本和原来的行为树使用方式一致:由于我用的方法,没有很大的增加使用的复杂性,所以秉着“能省一点是一点”的原则,就都用了共享性行为树的方式

这次我还是做了一个untiy能跑的demo,demo里演示的是和原本的行为树demo里类似的行为,不过换成了3D表现,感觉一下子高大上了很多~

old-demo

new-demo

基本行为描述如下,首先会每隔一段时间会产生一个移动目标点(白色立方体),然后智能体(僵尸)会旋转并朝向目标点后,朝目标点移动,当到达目标点后,会开始攻击,整个攻击行为会持续5秒钟,最后倒地结束,一直到新的目标点产生,智能体循环执行上述行为。

整个demo可以在untiy5里面执行,打开Scenes目录下面的demo1,执行后,屏幕左上角有一个按钮,每点一次,就会加一个僵尸到场景中,还有一个滑动条,可以加速和减速整个游戏。

demo_debugui

整个demo工程可以点击这里下载

首先介绍一下这次新写的行为树的一些东西。

整个计算要做到结构上的共享,最重要的部分就是做到结构和数据的分离,也就是说整个树的计算是无态的,只要做到了这一点,就可以做到共享行为树的结构,所以在设计上,我把单个智能体在执行的状态数据都放到了传入的Workingdata的结构中,包括控制节点和行为节点上所有定义的状态数据。在这个结构中,我维护了一个字典,当新建行为树每一个节点的时候,都会附带生成一个当前行为树唯一的id值,把这个id做为这个节点的标示,也作为索引状态数据的key值。这样我就可以方便的从WorkingData里取出当前节点所对应的状态数据了

仔细看每一个控制节点的代码的时候,可以发现,所有的运行时数据都包在了一个context的数据结构里,当我要进行行为树计算的时候,就需要先从WorkingData中取出当前节点的context数据,然后根据context数据再进行下一步的计算操作。用户定义的行为节点也一样,例子的话,大家可以看AIBehavior.cs里的NOD_Attack类,看我是如何让僵尸攻击5秒钟的,唯一有一点小小的区别是,在用户的行为节点中所定义的运行时数据是放在TBTActionLeaf::TBTActionLeafContext里的userdata字段中的,我也提供了封装了方法,可以安全的访问。

在这样的封装和改造后,大家可以发现,现在对于行为树行为节点的写法和原本的行为树的写法几乎没有什么区别,但结构上已经变成了共享性的行为树结构。所以,不管我在场景中放了多少个僵尸,其实他们所使用的行为树实例只有一份,具体的可以参看AIBehavior.cs里面的AIEntityBehaviorTreeFactory类

行为树的部分就说这么多了,因为其实大部分的内容和以前分享的差不多,只是这次在结构上做了一些改进,具体的大家还是直接看代码更直观一点。接下来再可以聊聊这个demo所演示的另外一些东西

  • 分层的架构:在GameUpdater这个类里面,我演示了如何分离决策层和行为层,目前的决策层只有一个逻辑,就是产生新的移动目标点,如果当决策层比较复杂的时候,在决策层也可以使用行为树来产生决策,我在现在的项目里就是这样用的,这就是层次化的行为树结构,这样的分离带来的一个额外的好处就是可以方便的分割服务器和客户端的逻辑分离点,换句话说,我们可以很容易的决定,哪些逻辑应该写在服务器上,哪些逻辑应该写在客户端,关于这个话题可以以后继续展开。
  • 决策层和行为层的”粘合剂”–请求:在决策层和行为层的中间,我定义了请求层,在博客的两篇文章(12)中,我也提到过,关于请求层的作用,大家可以去翻一下,在这个demo里,我演示了如何在决策层中产生一个请求,以及如何在行为层中处理一个请求,当然,一般来说,我们会在WorkingData中把请求直接传递到行为层,而不是像我这里所写的,仅仅把一个值通过黑板传递过去
  • 使用黑板:在AI库中,我定义了一个简单的黑板的结构,在demo中,也演示了如何利用黑板传递一些数据,一般我们会将一些行为树需要用的额外数据结构存在黑板中,比如从世界中收集的信息,从其他模块传来的数据等等,黑板的结构非常适合作为这些数据的存储器来使用

关于这个AIToolkit,有时间,我会持续的更新,我的目标是把一些AI中常常会用到的东西做成一个个模块,彼此相对独立,并且尽量轻量化。欢迎多多讨论,也欢迎来信,或者在讨论区交流。

今天就先聊到这里,最后,感谢所有关注和支持我的朋友们,很开心写的东西能对大家有所帮助。

————————————————————————
作者:Finney
Blog:AI分享站(http://www.aisharing.com/)
Email:finneytang@gmail.com
本文欢迎转载和引用,请保留本说明并注明出处
————————————————————————


评论

  • 2015 年 12 月 3 日, zen_zhao 评论到: 为什么不考虑直接用U3D的可视化行为树编辑器?
  • 2015 年 12 月 3 日, Finney 评论到: 因为自己写一个也不复杂,够用就好
  • 2015 年 12 月 3 日, zen_zhao 评论到: 我现在基于你的文章实现了一套基于cocos2d-x的行为树。实际使用中发现如果有画图工具的话,跟策划交流起来更加直观(我都是在纸上画)。后来在网上找到个在线行为树编辑器,比较轻量级,导出json。看了下其作者的文章,实现行为树的思路基本跟你一样,唯一有区别的地方是在判断条件的处理上,你是把前置条件绑定在一个节点,他是把判断条件也作为一个节点,然后通过sequence将condition和action节点组合起来。http://behavior3.com/
  • 2015 年 12 月 3 日, Finney 评论到: 这种实现也有,我只是觉得,太多的sequence有点啰嗦,所以改成专门的绑定条件的方式。在线的编辑器不错,我去看看。其实unity里面还有一种做法,是用编辑器本身的树形结构来搭建行为树,然后绑定对应的脚本,也是可以的。
  • 2015 年 12 月 3 日, zen_zhao 评论到: 用绑定条件的方式对于程序来说会比较简洁,但是在跟策划交流的时候不太容易说明白,因为不好用图形表示我在这个节点上绑定了多少个条件,必须要在旁边用文字标注清楚。unity对于AI设计支持还是很好的,cocos2dx这一块基本是空白,只能自己搭建。
  • 2016 年 4 月 26 日, 风清扬笑 评论到: 你们的做法是程序来实现策划的AI需求么,有没有编辑器供策划自己使用的
  • 2016 年 4 月 27 日, Finney 评论到: 让策划来使用行为树编辑器有几个需要预先准备的事情,也可以说是前提,一个是行为树的编辑器,一个是对于行为树概念的理解,一个是行为树模块的合理性,逻辑的抽象和分隔比较合理,另一个是行为树比较复杂,或者行为树的种类比较多。有了这些准备,策划才能参与得比较好,要不反而会事倍功半。目前我们还没有具备这样的条件,不管是编辑器,还是策划层面,还是需求层面(行为树比较简单而且单一),所以现在还是由程序员来搭建的行为树的
  • 2016 年 5 月 1 日, 风清扬笑 评论到:
    Finney :让策划来使用行为树编辑器有几个需要预先准备的事情,也可以说是前提,一个是行为树的编辑器,一个是对于行为树概念的理解,一个是行为树模块的合理性,逻辑的抽象和分隔比较合理,另一个是行为树比较复杂,或者行为树的种类比较多。有了这些准备,策划才能参与得比较好,要不反而会事倍功半。目前我们还没有具备这样的条件,不管是编辑器,还是策划层面,还是需求层面(行为树比较简单而且单一),所以现在还是由程序员来搭建的行为树的
    我们这面也是c++的,想基于行为树来重构AI部分。和策划对接的部分不知道如何执行。如果是程序来搭建行为树,策划的需求该如何去实现。比如策划提出“在血量低于80%的时候释放一个技能A,其他时间里,每三秒释放一个技能B”。这里把这种需求做成一种机制,让策划去配置【80%,技能A,技能B】,这样的么?当有一个需求时,策划需要做什么?程序需要做什么?能具体些么
  • 2016 年 5 月 3 日, Finney 评论到: 如果用你说的这个例子,想要策划来参与的话,首先需要对于策划提的这些条件和行为做抽象,比如“血量低于80%”是一个需要抽象东西,“每三秒”也是一个需要抽象的东西,“释放技能”是一个行为等等,程序就是需要预备这些抽象后模块,再结合行为树的控制节点,才能让策划参与的比较舒服。要不策划还是只能填填数值,没法真正参与搭建行为树
  • 2016 年 5 月 3 日, 风清扬笑 评论到: 嗯嗯,策划只填数值的话,他也很难去理解这个AI,这些数值为什么存在。所以策划是参与到行为树的搭建的。就我的理解中,有一些需求是完全可以程序自己去做的,比如NPC的基础AI,这个不会涉及到数值,那么这种情况是完全可以程序自己做的,无论是用行为树还是用其他的都行。但如果AI涉及到数值,类似以上的各种数值的抽象,如果采用行为树的话,最后把这些数值整合到一起,还是得依赖编辑器。难道说必须撸个编辑器出来,囧
知识共享许可协议
本博客作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
捐赠本站 ]]>
http://www.aisharing.com/archives/750/feed 10
一大波分享即将来袭 http://www.aisharing.com/archives/739 http://www.aisharing.com/archives/739#comments Fri, 20 Nov 2015 14:06:55 +0000 http://www.aisharing.com/?p=739 http://ewo.ubi.com,由于服务器在国外,所以不管是加载,还是游戏,都相当相当慢。 EndWar-Online 然后呢,我就挪了个窝,到了一个国内的游戏公司,碰巧,在新地方,新项目里面,又有机会做到AI相关的东西,所以又可以有东西和大家分享了。针对项目的要求,不管是服务器和客户端,这次做了一些新的尝试,并且,我是希望通过这个项目,能作一些技术方面的积累,所以在架构和设计上考虑了很多方面因素,应该都能拿出一些东西来聊。我们整个游戏是个基于unity的手游项目,目前还在制作过程中,等发布了,也请大家捧个场,提提意见。 我想下次就先分享一下前段时间一直提到的c#版本的共享型行为树吧,很多人来信在问我什么时候放出来,其实这个东西没这么神秘,我只是换了一种做法,但还是行为树的那些概念,大家在问c#版本,主要还是能用在unity里面吧,可见现在手游竞争多激烈,:)。其实unity里面,腾讯有个行为树的版本,大家也可以用用,还带编辑器,很不错的。我做的实现都比较轻量级,没有编辑器什么的,很多时候只要能满足我的需求就可以了,大家多多包涵。 ———————————————————————— 作者:Finney Blog:AI分享站(http://www.aisharing.com/) Email:finneytang@gmail.com 本文欢迎转载和引用,请保留本说明并注明出处 ————————————————————————]]> 好久没更新文章了,一般都看到有朋友留言,或者发信给我,我会尽可能的及时回复一下,希望能帮到大家。

先聊聊近况,前面很长一段时间,博客都没有更新,主要的原因是,我没有做什么与AI相关的东西,也就没什么能分享,上个项目是一个Flash3D的项目,我主要负责Flash3D引擎和整套3D数据流工具的编写,所以我就潜心研究的两个东西,一个是Flash,一个是3D渲染,积累了不少经验,可以说,对于用Flash 3D来制作游戏,我有这个自信,应该算知道了不少了,但由于这个博客的主要内容是与AI相关的,在这里就不分享这些东西了,如果有朋友有这方面的问题,也可以来信问我,不过现在做页游的真心不多了,特别还是3D的页游。我们这个页游不在国内发布,主要针对欧美市场,大家有兴趣可以访问http://ewo.ubi.com,由于服务器在国外,所以不管是加载,还是游戏,都相当相当慢。

EndWar-Online

然后呢,我就挪了个窝,到了一个国内的游戏公司,碰巧,在新地方,新项目里面,又有机会做到AI相关的东西,所以又可以有东西和大家分享了。针对项目的要求,不管是服务器和客户端,这次做了一些新的尝试,并且,我是希望通过这个项目,能作一些技术方面的积累,所以在架构和设计上考虑了很多方面因素,应该都能拿出一些东西来聊。我们整个游戏是个基于unity的手游项目,目前还在制作过程中,等发布了,也请大家捧个场,提提意见。

我想下次就先分享一下前段时间一直提到的c#版本的共享型行为树吧,很多人来信在问我什么时候放出来,其实这个东西没这么神秘,我只是换了一种做法,但还是行为树的那些概念,大家在问c#版本,主要还是能用在unity里面吧,可见现在手游竞争多激烈,:)。其实unity里面,腾讯有个行为树的版本,大家也可以用用,还带编辑器,很不错的。我做的实现都比较轻量级,没有编辑器什么的,很多时候只要能满足我的需求就可以了,大家多多包涵。

————————————————————————
作者:Finney
Blog:AI分享站(http://www.aisharing.com/)
Email:finneytang@gmail.com
本文欢迎转载和引用,请保留本说明并注明出处
————————————————————————


评论

知识共享许可协议
本博客作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
捐赠本站 ]]>
http://www.aisharing.com/archives/739/feed 1
新开了一个专门的讨论区 http://www.aisharing.com/archives/721 http://www.aisharing.com/archives/721#comments Mon, 02 Mar 2015 16:10:08 +0000 http://www.aisharing.com/?p=721 讨论区入口点击这里]]> 试了几个论坛插件,都很麻烦,索性就新开一个页面,使用原本的评论功能来做讨论区了,如果大家以后有关于AI的问题,可以直接在讨论区留言讨论。

讨论区入口点击这里


评论

  • 2015 年 3 月 4 日, HelloWorld 评论到: 看了一系列的关于行为树的文章,学到了很多东西。在此感谢博主。 我将你写的行为树移植到了c#上,并做了几个小demo,感觉非常好用。 但c#中用工厂模式创造行为节点的那个函数实现不了,只能调用无参构造函数。。这点有些小遗憾,不过手动拼装也不是什么难事。 总之,感谢博主
  • 2015 年 3 月 5 日, Finney 评论到: 很高兴对你有帮助,我也写了一个c#版的,用的是共享节点型的行为树,在unity上也可以用,等我整理好了可以放出来,可以一起再交流一下
  • 2015 年 3 月 5 日, HelloWorld 评论到: 嗯,期待
  • 2015 年 3 月 6 日, 匿名 评论到: 说实话,博主的文章写得真的不错,对我帮助很大,谢谢了
  • 2015 年 11 月 10 日, guapis 评论到: 博主,看了您的关于行为树的文章,收获很多,最近在写一个类似RTS即时战争的寻路、避让战斗,想看看你的共享节点型的行为树C#版本的demo,能够发我一个domo么?万分感谢!
  • 2015 年 11 月 20 日, Finney 评论到: c#版本的,最近太忙,没时间整理,再等一下吧,我会在博客上共享出来的
  • 2016 年 10 月 8 日, firwg 评论到: 楼主有过GOAP的实践经验么
  • 2016 年 10 月 26 日, Finney 评论到: 没有,目前还没有机会用goap做一下
知识共享许可协议
本博客作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
捐赠本站 ]]>
http://www.aisharing.com/archives/721/feed 8
用行为树的方式思考(后续)- 任务系统设计 http://www.aisharing.com/archives/660 http://www.aisharing.com/archives/660#comments Fri, 13 Dec 2013 01:57:58 +0000 http://www.aisharing.com/?p=660 用行为树的方式思考》后,有同学想让我详细说一下任务系统,我这里就贴几个代码片段(伪代码)再详细说明一下 先是数据结构定义 1 由于Condition和Objective都定义成了树结构,所以我们就可以很方便的组织逻辑,下面是一个具体Condition的例子,Objective也可用同样的方式定义出来 2 这样接任务和更新任务就变的很简单 3 所以我们只需要定义出单个的逻辑片段,然后游戏设计去拼搭任务逻辑就可以了,和行为树的思路是很像很像的。当然任务系统还有很多细节需要处理,但这样的逻辑结构可以大大的帮助理清思路,实现出漂亮的代码 ———————————————————————— 作者:Finney Blog:AI分享站(http://www.aisharing.com/) Email:finneytang@gmail.com 本文欢迎转载和引用,请保留本说明并注明出处 ————————————————————————]]> 上次分享了《用行为树的方式思考》后,有同学想让我详细说一下任务系统,我这里就贴几个代码片段(伪代码)再详细说明一下

先是数据结构定义

1

由于Condition和Objective都定义成了树结构,所以我们就可以很方便的组织逻辑,下面是一个具体Condition的例子,Objective也可用同样的方式定义出来

2

这样接任务和更新任务就变的很简单

3

所以我们只需要定义出单个的逻辑片段,然后游戏设计去拼搭任务逻辑就可以了,和行为树的思路是很像很像的。当然任务系统还有很多细节需要处理,但这样的逻辑结构可以大大的帮助理清思路,实现出漂亮的代码

————————————————————————
作者:Finney
Blog:AI分享站(http://www.aisharing.com/)
Email:finneytang@gmail.com
本文欢迎转载和引用,请保留本说明并注明出处
————————————————————————


评论

  • 2014 年 2 月 12 日, lexus1989 评论到: 请大神指教,行为树如何做群体智能?比如:组队做任务
  • 2014 年 2 月 17 日, Finney 评论到: 群体智能的一种解决方案是,需要专门做一个群体决策的行为树,然后用一块黑板来作为共享知识库,然后个人的行为树再基于这个黑板来做个人决策
  • 2014 年 3 月 5 日, 土匪 评论到: 谢谢,之前是我提出来的要求,这么快就看到了伪代码实现。 ---------------------------- 我看到里面的inputParm,很显然,应该是属于一种万金油的结构(如 any),任务函数的调用方和内部互相协商传递的结构,可以解决沟通上的问题,但是,在序列化到数据库里面的时候,似乎也只能使用一个万金油结构了? 如 A 打怪任务(需要记录打怪数量) B 循环任务(需要记录第几环/第几次 + 当前环/第几次的 参数) 你们又是如何设计呢?
  • 2014 年 3 月 28 日, Finney 评论到: 在一个quest里每一个objective都对应一个唯一(quest里唯一)的id,每一个objective里面每一个要存的数据也对应一个唯一(objective里唯一)的id,这两个id会拼成一个key,数据库里面是存了一系列的key/value对,这样就可以序列化和反序列化了
  • 2014 年 3 月 31 日, Chucky 评论到: 博主你好,我是入行不久的游程一枚,博主的AI相关的博文写的很好,受益匪浅,想问下博主有没有AI相关的书籍推荐,想深入学习下AI,谢谢
  • 2014 年 4 月 1 日, Finney 评论到: 如果是学院派的书,我也不是很了解,因为我本身也不是科班研究人工智能的,如果是游戏相关的,大部分都是类似于论文集的书,像游戏编程精粹里有专门的AI部分可以看看,还有就是一些常用的游戏AI技术可以去了解一下,比如状态机,行为树,黑板,模糊逻辑,神经网络,贝叶斯等等,有个网站叫aigamedev,很不错,也可以经常上上。由于游戏AI比较特殊,更多的学习和经验需要在实践中积累。
  • 2014 年 4 月 1 日, Chucky 评论到: 好的,感谢博主这么快回复!
  • 2014 年 5 月 12 日, Lee 评论到: 在Behavior Tree里的 Loop节点 怎么用啊 请教一下 我这里 此节点只进一次 这是为什么啊
  • 2014 年 5 月 16 日, Finney 评论到: Loop就是循环这个节点n次,如果没有其他的条件,循环n次后结束
  • 2014 年 9 月 19 日, ShionRyuu 评论到: 土匪上面说的应该是数据的问题(objectiveA的属性只有一个数值代表打怪数量,objectiveB的属性有一个数值代表第几环,一个代表第几次),要怎么序列化和反序列化不同的objective类?
  • 2014 年 9 月 22 日, Finney 评论到: 首先我有quest id,我就能找到那个quest,就可以重建这个quest的类,这个是静态数据,然后就是通过这个quest里的objective的唯一id去重建每一个objective的类和数据
  • 2014 年 10 月 28 日, china 评论到: 你好,我想要在程序中模拟,游戏人物之间不能够相互穿越这个效果。目前的想法是,如果两个人物之间隔得太远就给他一个靠近的力。如果隔得很近就给他一个远离的力。但是我没有算好这个力和距离还有速度的关系?请问作者有没有相关的经验。
  • 2015 年 1 月 2 日, Finney 评论到: 你可以网上搜搜steering behavior
  • 2015 年 3 月 2 日, lufeng 评论到: Finney,您好,我想问下行为树AI如果用在游戏中的服务器端关于怪物的实现,要怎么做比较好呢,没明白关于根节点该怎么设计,大概能知道的思路应该是,根据怪物的不同表现,设计不同的行为节点(攻击,巡逻,逃跑,回巢,召唤怪物,汉喊话,释放技能等)和控制节点(血量降低百分之多少,周期间隔,出生时,死亡时,在战斗状态中,不在战斗状态中等)求指导。
  • 2015 年 3 月 2 日, Finney 评论到: 可以先列出所有的行为节点,就像你说的这样,然后再列出触发这些行为的前提条件,不是用控制节点,控制节点指的是行为间的关系,不是指前提。比如你有一个序列行为,控制节点就是这个序列,里面有几个子节点是行为(释放暴走技能,召唤怪物,喊话),这个序列行为的前提是血量降低到30%以下。类似于这样,然后把这些都挂到根节点上去,一般根节点是一个选择节点
  • 2015 年 3 月 3 日, lufeng 评论到: Finney,您好,我想问下行为树AI如果用在游戏中的服务器端关于怪物的实现,要怎么做比较好呢? 不太明白关于根节点该怎么设计,能想到的大概的思路应该是: 1、根据怪物的不同表现,设计不同的行为节点(攻击,巡逻,逃跑,回巢,召唤怪物,汉喊话,释放技能等) 2、设计不同的控制节点(血量降低百分之多少,周期间隔,出生时,死亡时,在战斗状态中,不在战斗状态中等) 具体的求指导。
  • 2015 年 3 月 4 日, lufeng 评论到: 恩,大致明白了,多谢Finney的解答,根节点其实是个控制节点吧(比如您说的选择节点?或者序列节点?),然后把这个根节点挂到怪物身上?然后根节点下有各种行为节点和他们的前提?(释放技能当血量降低80%,刚出生时喊话)类似这样的对吧?
  • 2015 年 3 月 4 日, Finney 评论到: 对的,就是这样,其实不只是根节点,根节点下面还可以连控制节点,这样逻辑就一层层关连起来了
  • 2015 年 3 月 4 日, lufeng 评论到: 多谢Finney,我好想明白了一些。
  • 2015 年 3 月 4 日, lufeng 评论到: 您好,我想再问一下,看了你的DEMO,发现tsiublogver_2这里的用到了Task,和之前的第一版的demo不一样,这里的Task主要是用来干啥的呢?
  • 2015 年 3 月 4 日, Finney 评论到: 这个是共享节点型的行为树,可以看http://www.aisharing.com/archives/563,不过这个实现比较繁琐,我后来又写了一种,感觉会比这个好一点
  • 2015 年 3 月 4 日, lufeng 评论到: 原来是共享节点型的树啊,你说的后来又写了一种,就是指的是共享节点型的树吧?
  • 2015 年 3 月 5 日, Finney 评论到: 对的,c#版的,在unity上可以用,等我整理好了可以放出来
  • 2015 年 3 月 5 日, lufeng 评论到: 太好了,感谢博主,赞美博主,期待。。这样就可以参考下用在服务器端的怪物AI之中了。。
  • 2015 年 3 月 5 日, lufeng 评论到: 博主你好,再问下,关于共享节点型的树,这里没有用到前提,而是直接写到了Update里面,是不是吧前提也提出来,这样耦合度不高,是不是会好一些?
  • 2015 年 10 月 15 日, ldc 评论到: Condition_LevelRequirement和Condition_FinishQuest所要用于条件检查的数据对象是不一样的(一个是等级大小,一个是任务完成否),应该不是inputParam。请问二者的条件检查要如何进行?
  • 2015 年 11 月 20 日, Finney 评论到: 我这里inputparam可以理解成一个any的数据结构,不同的语言可以有不同的表示方式,比如c++里面就可以用void*,当然你也可以用一个统一的数据结构,里面可以提供一些变量位,然后根据不同的类来解析,这就类似于协议的做法
  • 2016 年 5 月 16 日, cloudfreexiao 评论到: 楼主: 请问这个 UpdateQuest() 是每帧去同步的吗?
  • 2016 年 5 月 17 日, Finney 评论到: 不是每帧去调,是需要触发任务更新的点去调,比如升了一级,或者打了怪等等时间点上
  • 2016 年 6 月 22 日, Ihaiu 评论到: @Finney 请教一个策略类型的游戏AI。我就拿《蘑菇战争》作例子,这个游戏的战斗是这样的: 蘑菇战争网址 http://mgzz.163.com/ 1.假设有3个阵营。1个真人玩家阵营,1个电脑玩家阵营,1个中立阵营(中立不用AI,他不做任何决策) 2.每个阵营初始有a个城池。 3.游戏胜利规则:占领所有敌方的城池。 4.城池功能--兵力(相当血量),每个城池兵力有上限值(相当最大血量) 4.城池功能--兵力回复(相当血量回复) 4.城池功能--升级(有3个等级),可以提升兵力上限、兵力回复速度 4.城池功能--调兵,可以把兵派到任意城池,目标城池如果是自己的就是“调兵决策”给目标城池补充兵力,如果是敌人的就是”攻打“抵消目标城池兵力。调兵会消耗当前发兵城池的兵力。 AI需要做的就是 "城池升级"、”调兵“这两个行为。初步感觉AI对象有两种方式(或者叫两个层次) ”城池“和”玩家势力“。 #针对”城池“写的话可以用你之前的那些思想比较好实现。 #针对"玩家",我主要就是想知道这里你有什么建议?
  • 2017 年 9 月 12 日, qhg 评论到: Hi ,博主,用在任务系统上真的很爽,就是简单的继承和重写就能优美的解决一个复杂的系统,非常感谢!
  • 2018 年 3 月 1 日, andyscream 评论到: 感觉树模型是逻辑复用的利器,博主写得很棒,我也按照文章完成了一个复用的任务系统,所有逻辑的检查全变成了查表,就是实现繁琐。树模型和多态是OOP的一种绝妙的组合。
知识共享许可协议
本博客作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
捐赠本站 ]]>
http://www.aisharing.com/archives/660/feed 32
用行为树的方式思考 http://www.aisharing.com/archives/653 http://www.aisharing.com/archives/653#comments Fri, 06 Dec 2013 08:06:14 +0000 http://www.aisharing.com/?p=653 点击这里),一年多的工作收获满满,职位从AI Engineer变成了Engineer(“专科大夫”到“全科大夫”?)。虽然很多工作看似和AI没什么关系,但做AI时的那些经验也带给了我不少思考和借鉴。我的博客里分享的最多的就是行为树(Behavior Tree),被浏览的最多的是行为树,提问最多的还是行为树,行为树作为现在AI的流行技术确实得到了很多的关注,不少游戏和开发者都从中受益。我今天想聊聊的并不是行为树在AI中的某个具体应用,就像标题所说的,我想聊聊如何用行为树的方式去思考问题,这也是我这些时间做其他东西的时候一直在考虑的,并且我也成功的将行为树的思想用到了非AI的模块中,效果不错。如果你还不了解行为树,请先阅读本博客中关于介绍行为树的相关文章(点击这里)。 行为树从本质上来说,是一颗逻辑树,它把所有的行为逻辑用树形结构串联起来,仔细观察的话,可以发现行为树的核心思想有三个方面:
  • 逻辑分离
  • 逻辑关联
  • 逻辑抽象
听上去很玄乎,其实是很简单的东西,可以先想想我们平时要做一个功能是怎么做的,我们会先定义一个函数,定义好输入和输出,然后在这个函数里写代码来实现功能逻辑,这是第一步,是最直接和简单的方式。后来,当这个功能越来越复杂的时候,这个函数里的代码就会越来越长,变得难以阅读和维护,我们就会把一些逻辑拿出去,变成另一个函数,原先那个函数里就变成了一些简单逻辑和函数的组合,再然后,我们发现有些函数可以变成一些通用函数,我们就会把这些函数集合起来变成一个库,这样其他的函数也可以访问这个函数来获取他的逻辑功能。 这里的整个过程就包含了上面所说的三个方面,把逻辑移出去变成一个新的函数,就是“逻辑分离”,原本函数里的简单逻辑和函数组合就是“逻辑关联”,把函数变成通用库就是“逻辑抽象”。AI是游戏的逻辑大户,充斥着大量的游戏逻辑和算法,所以就特别需要好的架构来维护和管理“逻辑”,要不整个代码就是一团糟,不仅无法维护,而且也很难除错,现有的AI的架构基本都围绕这个展开。 让我用有限状态机(FSM)来举个例子,在FSM中就包含了逻辑的分离和抽象,它有“状态”这个概念,这就是一个逻辑块,它的逻辑块也可以重用,但它对于逻辑的关联做的相对比较薄弱,由状态自己来决定何时跳转,并且跳转比较随意,所以逻辑的关联性比较模糊,这就导致FSM在多状态的情况下很难维护。所以后来有了层次化的有限状态机(HFSM),部分解决了逻辑关联模糊的问题,但FSM的设计原理导致它并没有办法从根本上解决问题。但对于状态和跳转都不是很复杂的功能,FSM是个不错的选择。 让我们再回到行为树,行为树把逻辑分散在节点中,每个节点负责自己的逻辑部分,这些逻辑节点又可以被放在行为树的其他部分,也就是可以被重用。在这个基础上,行为树又抽象了三个逻辑概念,控制逻辑,前提逻辑,行为逻辑,其中行为逻辑用来描述功能,控制逻辑和前提逻辑用来描述逻辑间的关联,对于逻辑关联的抽象是行为树相较于FSM的一个重大突破,它使得逻辑的关联“可视化”了,用过行为树的人都会有这样的感觉,我只要看一下树的结构,我就能知道整个AI行为是如何协作的了,也正是这样的优势使得行为树现在被越来越多的用在了AI逻辑中。 但如果我们再往前思考一步,可以发现如果仅仅把行为树限制在AI部分,显得有点可惜,就像我前面一直强调,行为树就是逻辑树,是一种对于逻辑的维护和管理的架构。游戏中很多地方都是有逻辑的,有些甚至会非常复杂,这些地方为什么不能用行为树的方式来思考和实现呢?经过实践,我发现这是完完全全可行的,我甚至可以这样说,只要存在复杂的逻辑,就可以用行为树的方式去做,它可以很好的帮助你理清思路,实现漂亮的逻辑代码。由于行为树与AI有了“密切”的绑定,所以甚少接触AI的程序员对行为树基本不是很了解,这也导致行为树并没有得到广泛的应用,甚至都没有作为一个候选方案。 我有幸在现在项目里,做了很多其他的模块,所以我也把行为树的一些思路用到了其他的模块中,发现写起来非常的顺,也很爽快。我举一个我们在项目中碰到的例子,就是任务系统,做过游戏的人都知道,这个系统在逻辑层面是很复杂的,内容繁多,但如果用行为树的方式去思考的话,就会发现这个复杂的问题一下子就简化了。 仔细分析任务系统的话,可以把任务系统分成几个部分,一个是接这个任务条件,然后是任务的完成目标,然后是奖励(这个和下面的讨论关系不大,暂且略过)。我们先可以抽象两个概念,“单个条件”和“单个目标”。单个的条件包含“怎么算是达到条件”的一段逻辑,单个的目标也是一段逻辑,包含了“怎么算是完成”。所以这些都可以做成一个个逻辑单元,就像行为树的前提和行为节点一样。另外根据设计的需求,接任务的条件可能有多个,完成的目标也可以有多个,所以这些单个逻辑之间就存在逻辑关联,所以我们可以借鉴行为树中控制节点的概念,把这些逻辑关联也抽象出来,成为“关联”,比如一个最简单的逻辑关联就是“并且”,这样我们就可以描述这样一个逻辑,要完成目标1,“并且”完成目标2,这里我们就把两个“单个目标逻辑”用“关联”串起来了。 最后,我们就在每个任务中定义了两颗逻辑树(换个通用的名称,其本质和行为树是一样的),一个是接这个任务的条件树,一个是完成这个任务的目标树,这样每个任务都可以用配置文件来配,我们也做了一个工具来帮助设计师来生成和编辑任务,作为程序只要维护那些可以被用到的“条件”,“目标”,“关联”就可以了。 Mission Complete!非常简单! 这就是行为树的思维方式带来的好处,我们在游戏的教程系统,技能系统等相对逻辑复杂的系统中都或多或少有用到这样的架构方式,使得单个的逻辑被高度提炼和抽象,逻辑的关系非常清晰,分工也变得更为简单。 好,这次就聊到这里,推荐大家可以在碰到复杂逻辑,梳理不清的时候,尝试用行为树去思考一下,一定会有另一番天地~ ———————————————————————— 作者:Finney Blog:AI分享站(http://www.aisharing.com/) Email:finneytang@gmail.com 本文欢迎转载和引用,请保留本说明并注明出处 ————————————————————————]]>
这段时间做了很多和AI无关的事情,做了个Flash的3D引擎,用汇编写了些shader,做了很多引擎的工具,脚本,插件,游戏也发布了首个预告片(点击这里),一年多的工作收获满满,职位从AI Engineer变成了Engineer(“专科大夫”到“全科大夫”?)。虽然很多工作看似和AI没什么关系,但做AI时的那些经验也带给了我不少思考和借鉴。我的博客里分享的最多的就是行为树(Behavior Tree),被浏览的最多的是行为树,提问最多的还是行为树,行为树作为现在AI的流行技术确实得到了很多的关注,不少游戏和开发者都从中受益。我今天想聊聊的并不是行为树在AI中的某个具体应用,就像标题所说的,我想聊聊如何用行为树的方式去思考问题,这也是我这些时间做其他东西的时候一直在考虑的,并且我也成功的将行为树的思想用到了非AI的模块中,效果不错。如果你还不了解行为树,请先阅读本博客中关于介绍行为树的相关文章(点击这里)。

行为树从本质上来说,是一颗逻辑树,它把所有的行为逻辑用树形结构串联起来,仔细观察的话,可以发现行为树的核心思想有三个方面:

  • 逻辑分离
  • 逻辑关联
  • 逻辑抽象

听上去很玄乎,其实是很简单的东西,可以先想想我们平时要做一个功能是怎么做的,我们会先定义一个函数,定义好输入和输出,然后在这个函数里写代码来实现功能逻辑,这是第一步,是最直接和简单的方式。后来,当这个功能越来越复杂的时候,这个函数里的代码就会越来越长,变得难以阅读和维护,我们就会把一些逻辑拿出去,变成另一个函数,原先那个函数里就变成了一些简单逻辑和函数的组合,再然后,我们发现有些函数可以变成一些通用函数,我们就会把这些函数集合起来变成一个库,这样其他的函数也可以访问这个函数来获取他的逻辑功能。

这里的整个过程就包含了上面所说的三个方面,把逻辑移出去变成一个新的函数,就是“逻辑分离”,原本函数里的简单逻辑和函数组合就是“逻辑关联”,把函数变成通用库就是“逻辑抽象”。AI是游戏的逻辑大户,充斥着大量的游戏逻辑和算法,所以就特别需要好的架构来维护和管理“逻辑”,要不整个代码就是一团糟,不仅无法维护,而且也很难除错,现有的AI的架构基本都围绕这个展开。

让我用有限状态机(FSM)来举个例子,在FSM中就包含了逻辑的分离和抽象,它有“状态”这个概念,这就是一个逻辑块,它的逻辑块也可以重用,但它对于逻辑的关联做的相对比较薄弱,由状态自己来决定何时跳转,并且跳转比较随意,所以逻辑的关联性比较模糊,这就导致FSM在多状态的情况下很难维护。所以后来有了层次化的有限状态机(HFSM),部分解决了逻辑关联模糊的问题,但FSM的设计原理导致它并没有办法从根本上解决问题。但对于状态和跳转都不是很复杂的功能,FSM是个不错的选择。

让我们再回到行为树,行为树把逻辑分散在节点中,每个节点负责自己的逻辑部分,这些逻辑节点又可以被放在行为树的其他部分,也就是可以被重用。在这个基础上,行为树又抽象了三个逻辑概念,控制逻辑,前提逻辑,行为逻辑,其中行为逻辑用来描述功能,控制逻辑和前提逻辑用来描述逻辑间的关联,对于逻辑关联的抽象是行为树相较于FSM的一个重大突破,它使得逻辑的关联“可视化”了,用过行为树的人都会有这样的感觉,我只要看一下树的结构,我就能知道整个AI行为是如何协作的了,也正是这样的优势使得行为树现在被越来越多的用在了AI逻辑中。

但如果我们再往前思考一步,可以发现如果仅仅把行为树限制在AI部分,显得有点可惜,就像我前面一直强调,行为树就是逻辑树,是一种对于逻辑的维护和管理的架构。游戏中很多地方都是有逻辑的,有些甚至会非常复杂,这些地方为什么不能用行为树的方式来思考和实现呢?经过实践,我发现这是完完全全可行的,我甚至可以这样说,只要存在复杂的逻辑,就可以用行为树的方式去做,它可以很好的帮助你理清思路,实现漂亮的逻辑代码。由于行为树与AI有了“密切”的绑定,所以甚少接触AI的程序员对行为树基本不是很了解,这也导致行为树并没有得到广泛的应用,甚至都没有作为一个候选方案。

我有幸在现在项目里,做了很多其他的模块,所以我也把行为树的一些思路用到了其他的模块中,发现写起来非常的顺,也很爽快。我举一个我们在项目中碰到的例子,就是任务系统,做过游戏的人都知道,这个系统在逻辑层面是很复杂的,内容繁多,但如果用行为树的方式去思考的话,就会发现这个复杂的问题一下子就简化了。

仔细分析任务系统的话,可以把任务系统分成几个部分,一个是接这个任务条件,然后是任务的完成目标,然后是奖励(这个和下面的讨论关系不大,暂且略过)。我们先可以抽象两个概念,“单个条件”和“单个目标”。单个的条件包含“怎么算是达到条件”的一段逻辑,单个的目标也是一段逻辑,包含了“怎么算是完成”。所以这些都可以做成一个个逻辑单元,就像行为树的前提和行为节点一样。另外根据设计的需求,接任务的条件可能有多个,完成的目标也可以有多个,所以这些单个逻辑之间就存在逻辑关联,所以我们可以借鉴行为树中控制节点的概念,把这些逻辑关联也抽象出来,成为“关联”,比如一个最简单的逻辑关联就是“并且”,这样我们就可以描述这样一个逻辑,要完成目标1,“并且”完成目标2,这里我们就把两个“单个目标逻辑”用“关联”串起来了。

最后,我们就在每个任务中定义了两颗逻辑树(换个通用的名称,其本质和行为树是一样的),一个是接这个任务的条件树,一个是完成这个任务的目标树,这样每个任务都可以用配置文件来配,我们也做了一个工具来帮助设计师来生成和编辑任务,作为程序只要维护那些可以被用到的“条件”,“目标”,“关联”就可以了。

Mission Complete!非常简单!

这就是行为树的思维方式带来的好处,我们在游戏的教程系统,技能系统等相对逻辑复杂的系统中都或多或少有用到这样的架构方式,使得单个的逻辑被高度提炼和抽象,逻辑的关系非常清晰,分工也变得更为简单。

好,这次就聊到这里,推荐大家可以在碰到复杂逻辑,梳理不清的时候,尝试用行为树去思考一下,一定会有另一番天地~

————————————————————————
作者:Finney
Blog:AI分享站(http://www.aisharing.com/)
Email:finneytang@gmail.com
本文欢迎转载和引用,请保留本说明并注明出处
————————————————————————


评论

  • 2013 年 12 月 7 日, vincentLiu 评论到: 做了好多事啊~~~你们flash3D还用汇编写shader啊?
  • 2013 年 12 月 7 日, Finney 评论到: 是呀,flash3d用的AGAL是汇编的
  • 2013 年 12 月 9 日, 梧桐 评论到: 曾研究过一些业余的可视化游戏制作软件,得到了一些启发,和博主的思路类似:一切逻辑都是“状态”和“状态转移的条件”,把这二者抽象出来以至于可以可视化操作,对游戏的流程简化有巨大的帮助。不过过犹不及,粒度切分太细也会造成代码过于流程繁琐,感觉这也是可视化游戏编程最让人不爽的一点。 PS:博主视频看起来好霸气,羡慕啊,做万年泡菜游戏的伤不起
  • 2013 年 12 月 9 日, Finney 评论到: 如何切分粒度是一个很艺术的问题,需要调整和平衡,我借鉴过层次化状态机的思路,做成层次化的行为树,这样可以好一点
  • 2013 年 12 月 11 日, feelworld 评论到: 请问博士提供的那个svn 源码是一直有在更新是吗
  • 2013 年 12 月 12 日, 土匪 评论到: 我比较好奇你任务设计那一块,能够详细点么
  • 2013 年 12 月 13 日, Finney 评论到: 可以看,http://www.aisharing.com/archives/660
  • 2014 年 4 月 28 日, 求指教 评论到: 我向询问下博主,对于强客户端的游戏开发,是否可以考虑采用mvc的流程,把 C 用行为树实现,来黏合m和v?我自己感觉还可以,特地来询问下
  • 2014 年 5 月 16 日, Finney 评论到: 就像我说的,行为树只是对逻辑的一种组织,不限定于可以用在那里,就像状态机一样,只要有复杂逻辑的地方,都可以尝试
知识共享许可协议
本博客作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
捐赠本站 ]]>
http://www.aisharing.com/archives/653/feed 9
[转载]LISP 语言是怎么来的–LISP 和 AI 的青梅竹马 http://www.aisharing.com/archives/648 http://www.aisharing.com/archives/648#comments Fri, 16 Aug 2013 02:17:15 +0000 http://www.aisharing.com/?p=648 最近在看Paul Graham的《黑客与画家》,他极力推崇LISP语言,以前我虽有耳闻,但不曾了解,后来在网上搜索了一些LISP的文章来看,发现原来LISP和AI的渊源颇深,这两篇好文是徐宥发表在他博客上的,写得很棒,特地转载一下(合并成一篇),也作为我收藏之用 原文链接: http://blog.youxu.info/2009/08/31/lisp-and-ai-1/ http://blog.youxu.info/2010/02/10/lisp-and-ai-2/
第一部分
LISP 语言的历史和一些番外的八卦和有趣的逸事,其实值得花一本书讲。 我打算用三篇文章扼要的介绍一下 LISP 的早期历史。 讲 LISP, 躲不过要讲 AI (人工智能)的,所以干脆我就先八卦八卦他们的青梅竹马好了。 翻开任何一本介绍各种编程语言的书,都会毫无惊奇的发现,每每说到 LISP, 通常的话就是”LISP 是适合人工智能(AI)的语言”。 我不知道读者读到这句话的时候是怎么理解的,但是我刚上大学的时候,自以为懂了一点 LISP 和一点人工智能的时候, 猛然看到这句话, 打死我也不觉得”适合”。 即使后来我看了 SICP 很多遍, 也难以想象为什么它就 “适合” 了, 难道 LISP 真的能做 C 不能做的事情么? 难道仅仅是因为 John McCarthy 这样的神人既是 AI 之父, 又是 LISP 之父, 所以 AI 和 LISP 兄妹两个就一定是很般配? 计算机科学家又不是上帝,创造个亚当夏娃让他们没事很般配干啥? 既然本是同根生这样的说法是不能让人信服的, 那么到底这句话的依据在哪里呢? 我也是后来看 AI 文献, 看当年的人工智能的研究情况,再结合当年人工智能研究的指导思想, 当年的研究者可用的语言等历史背景,才完全理解“适合” 这两个自的。 所以,这篇既是八卦,也是我的心得笔记。我们一起穿越到 LISP 和 AI 的童年时代。 虽然他们现在看上去没什么紧密联系, 他们小时候真的是青梅竹马的亲密玩伴呢! 让机器拥有智能, 是人长久的梦想, 因为这样机器就可以聪明的替代人类完成一些任务。 二战中高速电子计算机的出现使得这个梦想更加近了一步。二战后,计算机也不被完全军用了,精英科学家也不要继续制造原子弹了,所以, 一下子既有资源也有大脑来研究 “智能机器”这种神奇的东西了。 我们可以随便举出当年研究繁盛的例子: 维纳在 1948 年发表了<控制论>, 副标题叫做 <动物和机器的控制和通信>,  其中讲了生物和机器的反馈,讲了脑的行为。 创立信息论的大师香农在 1949 年提出了可以下棋的机器,也就是面向特定领域的智能机器。同时,1949年, 加拿大著名的神经科学家 Donald Hebb 发表了“行为的组织”,开创了神经网络的研究;  图灵在 1950 年发表了著名的题为“计算的机器和智能”的文章,提出了著名的图灵测试。如此多的学科被创立,如此多的学科创始人在关心智能机器, 可见当时的确是这方面研究的黄金时期。 二战结束十年后, 也就是 1956 年, 研究智能机器的这些研究者, 都隐隐觉得自己研究的东西是一个新玩意,虽然和数学,生物,电子都有关系, 但和传统的数学,生物,电子或者脑科学都不一样, 因此,另立一个新招牌成了一个必然的趋势。John McCarthy 同学就趁着 1956 年的这个暑假, 在 Dortmouth 大学(当年也是美国计算机科学发展的圣地之一, 比如说, 它是 BASIC 语言发源地), 和香农,Minsky 等其他人(这帮人当年还都是年轻人),一起开了个会, 提出了一个很酷的词, 叫做 Artificial Intelligence, 算是人工智能这个学科正式成立。  因为 AI 是研究智能的机器, 学科一成立, 就必然有两个重要的问题要回答, 一是你怎么表示这个世界,二是计算机怎么能基于这个世界的知识得出智能。 第一点用行话说就是”知识表示”的模型, 第二点用行话说就是“智能的计算模型”。 别看这两个问题的不起眼, 就因为当时的研究者对两个看上去很细微的问题的回答, 直接造就了 LISP 和 AI 的一段情缘。 我们各表一支。 先说怎么表示知识的问题。 AI 研究和普通的编程不一样的地方在于, AI 的输入数据通常非常多样化,而且没有固定格式。 比如一道要求解的数学题,一段要翻译成中文的英文,一个待解的 sodoku 谜题,或者一个待识别的人脸图片。 所有的这些, 都需要先通过一个叫做“知识表示”的学科,表达成计算机能够处理的数据格式。自然,计算机科学家想用一种统一的数据格式表示需要处理多种多样的现实对象, 这样, 就自然的要求设计一个强大的,灵活的数据格式。 这个数据格式,就是链表。 这里我就不自量力的凭我有限的学识, 追寻一下为啥链表正好成为理想的数据结构的逻辑线。我想,读过 SICP 的读者应该对链表的灵活性深有感触。为了分析链表的长处,我们不妨把他和同时代的其他数据结构来做一比较。 如我在前面的一个系列所说,当时的数据结构很有限,所以我们不妨比较一下链表和同时代的另一个最广泛使用的数据结构-数组-的优劣。 我们都知道,数组和链表都是线性数据结构,两者各有千秋,而 FORTRAN 基本上是围绕数组建立的,LISP 则是围绕链表实现的。通过研究下棋,几何题等 AI 问题的表示,我们的读者不难发现, AI 研究关心于符号和逻辑计算远大于数值计算,比如下棋,就很难抽象成一个基于纯数字的计算问题。 这样,只能存数字的数组就显得不适合。 当然我们可以把数组扩展一下,让这些数组元素也可以存符号。不过即使这样,数组也不能做到存储不同结构的数据。 比方说棋类中,车马炮各有各自的规则,存储这些规则需要的结构和单元大小都不一样,所以我们需要一个存储异构数据单元的模块,而不是让每个单元格的结构一样。 加上在AI 中,一些数据需要随时增加和修改的。 比如国际象棋里,兵第一步能走两步,到底部又能变成皇后等等,这就需要兵的规则能够随时修改,增加,删除和改变。 其他问题也有类似的要求,所有的这些,都需要放开数组维度大小一样的约束,允许动态增加和减少某一维度的大小,或者动态高效的增加删除数组元素。 而一旦放开了单元格要同构和能随时增加和删除这样两个约束,数组其实就不再是数组了,因为随机访问的特性基本上就丢失了,数组就自然的变成了链表,要用链表的实现。 所以,用链表而不是数组来作为人工智能的统一的数据结构,固然有天才的灵机一动,也有现实需求的影响。当然,值得一提的是,在 Common LISP 这样一个更加面向实践而不是科学研究是 LISP 版本中,数组又作为链表的补充,成了基本的数据结构,而 Common LISP,也就能做图像处理等和矩阵打交道的事情。这个事实更加说明,用什么样的数据结构作为基本单元,都是由现实需求推动的。 当然,科学家光证明了列表能表示这些现实世界的问题还不够, 还要能证明或者验证额外的两点才行, 第一点是列表表示能够充分的表示所有的人工智能问题,即列表结构的充分性。 只有证明了这一点,我们才敢放心大胆的用链表,而不会担心突然跳出一个问题链表表达不了;第二是人工智能的确能够通过对列表的某种处理方法获得,而不会担心突然跳出一个人工智能问题,用现有的对链表的处理方法根本没法实现。只有这两个问题的回答是肯定的时候,列表处理才会成为人工智能的一部分。 对于这两个问题,其实都并没有一个确定的答案,而只是科学家的猜想,或者说是一种大家普遍接受的研究范式(paradigm)。 在 1976 年, 当年构想 IPL, 也就是 LISP 前身的两位神人 Alan Newell 和 Herbert Simon ,终于以回忆历史的方式写了一篇文章。 在这篇文章中,他们哲学般的把当时的这个范式概括为: 一个物理符号系统对于一般智能行为是既充分又必要的( A physical symbol system has the necessary and sufficient means for general intelligence action)。 用大白话说就是,“智能必须依赖于某种符号演算系统,且基于符号演算系统也能够衍生出智能”。 在实践中,如果你承认这个猜想,或者说这个范式,那你就承认了可以用符号演算来实现 AI。 于是,这个猜想就让当时几乎所有的研究者,把宝押在了实现一个通用的符号演算系统上,因为假如我们制造出一个通用的基于符号演算的系统,我们就能用这个系统实现智能。 上面我们说过, 链表的强大的表达能力对于这个符号演算系统来讲是绰绰有余的了,所以我们只要关心如何实现符号演算,因为假如上面的猜想是对的,且链表已经能够表示所有的符号, 那么我们的全部问题就变成了如何去构建这样的符号演算系统。后面我们可以看到, LISP 通过函数式编程来完成了这些演算规则的构建。 这里,需要提请读者注意的是, LISP 的全称是 LISt Processing, 即列表处理,但实际上 LISP 是由两种互相正交的哲学组合形成的, 一个是列表处理,另一个是函数式编程。 虽然在下面以后,我们会介绍 S-Expression 这样美妙的把两者无缝结合在一起的形式,但是为了清晰我们的概念,我要强调一下列表处理和函数式编程是两个正交的部分。实际上,我们完全可以用其他的不是函数的方式构建一个列表处理语言。在历史上,早在 FORTRAN 出现之前,Alan Newell 和 Herbert Simon 就用汇编实现了一个叫 IPL 的语言,而这个 IPL 语言就是面向过程的对列表处理的,而后,McCarthy 一开始也是用一系列的 FORTRAN 子程序来做列表处理的。比如 LISP 里面的 CAR 操作,其全成实际上是 Content of the Address portion of the Register, 顾名思义,寄存器的地址单元内容,也即列表的第一个元素(和C表达数组的方式类似,这里寄存器中存着指向列表第一个元素的指针)。 函数式的却不以列表为基本数据单元的语言也很多,比如 Scala ,就是以对象为基本数据单元。 因此,函数式和列表处理是不一定要互相耦合的。 那么,到底是什么原因使得 LISP 选择函数式,这样的选择又为啥更加适合当时 AI 的研究呢, 我们下节将继续介绍当时 AI 的研究范式,强弱 AI 之间的辩论和函数式编程在当年 AI 研究上的优点。
第二部分
上回我们说到 LISP 和 AI 很是青梅竹马的时候,浮光掠影地说因为 LISP 的基本数据单元–”链表”在知识表示上的比较优势。 我们说, AI 要处理的数据结构和要刻画的现实世界的模型很复杂,使得数组等其他简单数据结构不能胜任,所以“链表”成了最佳的选择。 如果我们顺着这样的逻辑线往下看,似乎选择 LISP 这个“列表处理的语言”似乎是理所当然的。 可是,这个原因并不充分。 因为 LISP 语言可不仅仅是列表处理,还包括函数式编程等等其他。 反过来说,如果仅仅是列表处理对于 AI 至关重要的话,那么在 FORTRAN 和 Algol 这些通用编程语言又非常普及的传统语言里面写一些关于列表处理的函数岂不是更加直观和方便? 归根结底,到底 LISP 还有什么其他奥妙呢? 当我们追寻函数式编程这条线索的时候,就会不可避免的触及到 AI 的早期历史。LISP 的特性,其实都是和当时 AI 的范式 (paradigm) 息息相关的。 AI 范式的演变 早在 McCarthy 这一代人提出 AI 之前,冯诺伊曼等人就开始研究什么是智能以及如何实现智能的问题了。 所不同的是,他们更加偏重于研究大脑的内部工作机理,并且试图通过在模拟大脑的工作机理,来实现智能。 这一学派的哲学很清晰: 人类大脑是一个标准的智能体,我们只需要让计算机模拟人的大脑的工作方式,计算机就有了和人类大脑一样的智能了。 对于这一派的研究者来说,他们相信大脑的结构和工作机理决定了智能,至于大脑是用脑细胞构成的,还是用电子电路模拟的,对于智能来说毫不重要。 这方面的著名工作就是冯诺伊曼的《计算机和大脑》这篇论文。 在这篇不算很学术的随笔里面,他分析了人的大脑有多少个神经元,计算机有多少个晶体管,通过这些定量的比较来解释计算机和人的大脑的差距。 当时和冯诺伊曼齐名的另一个神童是开创控制论的维纳。 他和冯诺伊曼一样,兼通很多学科。 和冯诺伊曼一样,他职业是数学家,但是也精通如神经科学和脑科学等学科。一个显然的例子就是在《控制论》这本书里面, 维纳对大脑和神经的分析比比皆是。这种对大脑和神经分析的传统,从 Cajal (对,就是写 Advice for a Young Investigator 的那个大神) 开始,一直延续到了后来 AI 中的联接主义(主要研究神经网络的一个人工智能学派)。 可是,从脑科学和认知科学的角度去分析智能在当时有一个非常大的局限: 脑神经解剖学本身不成熟。 比方说,现如今脑科学家在分析脑功能的时候一般会借助于 fMRI 和其他一些神经造影技术。这些技术可以做到实时观测到脑中血氧分布,直接确定大脑在执行特定任务时候的活跃部分。当年的科学家则只能使用有限的几种医学成像技术,或者从血管摄影的角度研究大脑。 受限于当时的研究条件,当年的研究者很难直接观测到脑神经的实时工作状态,分析大脑的实时工作机理。 因此,对脑的研究就很难做到非常深刻。 医学研究条件的限制,加上当时电子学的发展和集成度远远不够,用电子电路模拟大脑生成智能就显得非常遥远。 因此,虽然这一派的思想超前,但是大部分的工作都不在于真正的用电子电路模拟大脑,而是在探索脑科学和神经科学本身,或者仅仅用电子电路模拟一些简单的神经动力学行为和小规模神经网络。正是因为连接主义在实现人工智能本身方面进展并不大,所以在AI领域中一直不是潮流的研究方向。上个世纪 80 年代前成功实施的一些人工智能系统,极少是来自于连接主义学派的。直到80年代后随着 BP 算法的重新发现,联接主义才迎来了第二春。 这时候,LISP 已经过完 20 岁生日了。所以,联接主义 对 AI 领域使用的编程语言的选择的影响并不算大。 符号主义 虽然联接主义这一学派在当时不怎么流行,当年的 AI 研究可是进行的如火如荼。这如火如荼的学派,采用的是另外一套方法,我们称之为“符号主义学派”。 符号主义学派的渊源,可以直接追溯到图灵。图灵在人工智能方面做过很多的研究,其中最为大家所知的就是“图灵测试“了。 有句俗话叫做“在网上,没人知道你是一条狗”, 在这句话里,只要把“狗”换成“计算机”,就是简单版的图灵测试了。 用个比较“潮”的比方,图灵测试就是让一台计算机或者一个真实的人(又叫评委)在网上交流,然后让这个评委猜测和他交谈的究竟是人还是计算机。 如果这位评委也不能分辨谈话的对方到底是人还是计算机的话,我们就认为这个计算机已经足以“以假乱真”,拥有“和人类一样的智能”了,也就是通过“图灵测试了”。 在很长一段时间里,图灵测试一直是人工智能研究的圣杯(holy grail)。 也就是说,通过”图灵测试“ 成了人工智能研究的终极目标。 那么,自然的,最最直接的通过图灵测试的方法不是让计算机和人脑一样思考,而是只要能够让计算机处理对话中用到的的单词,句子和符号,并且在对话中能够和人一样的操纵这些单词和符号,计算机就有很大的希望通过图灵测试。 从最极端的情况来看,计算机甚至都不需要去“理解”这些句子的含义,都有可能通过图灵测试。 [具体细节可以阅读 Wikipedia 上的“Chinese Room (中文房间)”条目]。 有一个开源的聊天机器人,叫做 A.L.I.C.E., 就把上面我们说的“只要能够处理和操纵符号,就有可能通过图灵测试”发挥到了近乎极致。 这个聊天机器人在图灵测试比赛中已经多次骗过人类评委,到了非常“智能”几乎能以假乱真的地步。可是,就是这样一个离通过图灵测试很近的机器人,其基本结构却简单到了我们都不能想像的地步:A.L.I.C.E.  的数据库里面有一条一条的规则,这些规则规定了她看到你说什么的时候她说什么。唯一有点“智能”的地方,就是有些规则不光取决于你这句话,还取决于你的上一句话。 [比如日常对话中我们先问“你喜欢看电影么?”,接着再问“什么类型的?”,这时候就需要前一句话推出这个问题是“(喜欢)什么类型的(电影)”]。“中文房间”的例子,和 A.L.I.C.E. 机器人如此简单的结构,都出人意料的显示出,即使计算机拥有了对符号的操作能力,通过了图灵测试,它也未必是是“有智能”的。 可惜这句话只是我的马后炮而已,在 AI 发展的早期,因为图灵测试的拉动,联接主义的相对弱势和符号主义的繁盛,都把全世界的 AI 研究往一个方向拽,这个方向,很自然的,就是“符号处理”。 符号处理和 LISP 补充 其实上一篇我们已经提到了,Alan Newell 和 Herbert Simon 认为对符号演算系统就可以衍生出智能,所以上面的文字,算是对符号主义这个 paradigm 做一个历史的小注解。 当我们把 LISP 放到这段历史中,就会自然的想到, 什么语言适合人工智能的问题,就变成了“什么语言能做符号处理”。这个问题的答案,读者也都猜到了,就是 LISP。 符号处理在 LISP 里面的长处前文我已经介绍过一些了,这里我们可以再补充几点零碎的。LISP 里有一个大家都知道的统一表示程序和数据的方法,叫做 S-Expression。 这个 S,其实就是 Symbolic 的意思。 把程序和数据都统一的当成符号,用现代编程语言的话说,就是 LISP 支持 meta-programming。LISP 程序可以处理,生成和修改 LISP 程序。这个特性,加上函数是一阶对象的特性,使得 LISP 远远比同时代的任何语言灵活。我本人不是 LISP 的用户(初级用户都算不上),因此在这一点上所知有限。但单就我有限的对 LISP 的理解,我认为 LISP 的这种灵活,恰好满足了基于符号处理的 AI 领域对语言的“强大的表达能力”(可以对任何复杂系统建模)和“高层的抽象能力” 的需求。关于第一点,有一个著名的段子,说任何一门编程语言技巧和思想被提出的时候,总会有一个高人出来,说,这个玩意儿在 LISP 里面早就有了,具体的例子包括刚才说的 metaprogramming, object oriented language。这里面蕴含的,就是 LISP 的强大的表达能力,使得很多编程的范式,在 LISP 里面都能实现,或者找到影子。 关于第二点,SICP 中例子比比皆是,讲得都比我深刻许多,就无需废话了。 在上篇文章中我提到,翻开任何一本编程的书,都会讲到“LISP是适合 AI 的编程语言”。那么,如果您和我当年一样,有兴趣从事 AI 方面的研究和探索,就不免要疑惑:“为了学习 AI, 我要不要学习 LISP” 呢?现在距离我当年的这个疑惑已经差不多8年了,我并没有一个确定的答案,但是我知道了更多的一些事实。 如今的 AI 范式 如果你让任何一个 AI 方向的研究者推荐几本适合初学者的书的话,十有八九他会提到 “Artificial Intelligence: A Modern Approach”(人工智能,一种现代方法) 和 “Artificial Intelligence: A New Synthesis” (人工智能,一个新的综述)。 这两本书的作者分别是 Peter Norvig 和 Nils Nilsson,都是 AI 领域的元老级人物。 如果你是一个对书名很敏感的人,你肯定会想:奇怪了,这种书又不是畅销书,难道这两位大牛写了书怕卖不出去,非要在书名上加一个 “现代” 或者 “新” 来吸引眼球? 事实上,这个“现代”和这个“新”都大有来头。 实际上,这二十年来,AI 研究领域接连发生了好几个非常大的 paradigm shift. 传统的基于符号的 AI 方法不再是主流,取而代之的,是多种多样的基于统计的,基于自动推理的,基于机器学习的,基于群体智慧的,基于大规模数据集的等等各种各样研究方法的兴起。 这个 paradigm shift, 对于领域之外的人好像是静悄悄的,可实际上 AI 领域早已发生了翻天覆地的变化。所以才会有 “新” 和 “现代” 这样的词出现在书标题上。 不幸的是,大多写编程语言书的作者,未必全部知晓这个变化,因此还沿袭原来的框架,继续写下 “LISP是适合 AI 的编程语言” 这样一个早就不能完全反映现状的断言。 如果我们统计一个从事 AI 研究的研究者或者科学家用什么语言,答案可能是五花八门无所不有: 做 AI Search 的用 C/C++/Java, 做机器学习的如果模型和矩阵关系密切,可以用 Matlab, 如果统计计算较多,也可以用 R。 至于做数据挖掘等等,语言和库更加五花八门,根本无法宣称那一个语言占上风。LISP 是适合 AI 的语言的教科书神话,也早就被无数的这样的实例给打破了。 延伸阅读: http://stackoverflow.com/questions/130475/why-is-lisp-used-for-ai]]>

最近在看Paul Graham的《黑客与画家》,他极力推崇LISP语言,以前我虽有耳闻,但不曾了解,后来在网上搜索了一些LISP的文章来看,发现原来LISP和AI的渊源颇深,这两篇好文是徐宥发表在他博客上的,写得很棒,特地转载一下(合并成一篇),也作为我收藏之用

原文链接:

http://blog.youxu.info/2009/08/31/lisp-and-ai-1/

http://blog.youxu.info/2010/02/10/lisp-and-ai-2/

第一部分

LISP 语言的历史和一些番外的八卦和有趣的逸事,其实值得花一本书讲。 我打算用三篇文章扼要的介绍一下 LISP 的早期历史。 讲 LISP, 躲不过要讲 AI (人工智能)的,所以干脆我就先八卦八卦他们的青梅竹马好了。

翻开任何一本介绍各种编程语言的书,都会毫无惊奇的发现,每每说到 LISP, 通常的话就是”LISP 是适合人工智能(AI)的语言”。 我不知道读者读到这句话的时候是怎么理解的,但是我刚上大学的时候,自以为懂了一点 LISP 和一点人工智能的时候, 猛然看到这句话, 打死我也不觉得”适合”。 即使后来我看了 SICP 很多遍, 也难以想象为什么它就 “适合” 了, 难道 LISP 真的能做 C 不能做的事情么? 难道仅仅是因为 John McCarthy 这样的神人既是 AI 之父, 又是 LISP 之父, 所以 AI 和 LISP 兄妹两个就一定是很般配? 计算机科学家又不是上帝,创造个亚当夏娃让他们没事很般配干啥? 既然本是同根生这样的说法是不能让人信服的, 那么到底这句话的依据在哪里呢? 我也是后来看 AI 文献, 看当年的人工智能的研究情况,再结合当年人工智能研究的指导思想, 当年的研究者可用的语言等历史背景,才完全理解“适合” 这两个自的。 所以,这篇既是八卦,也是我的心得笔记。我们一起穿越到 LISP 和 AI 的童年时代。 虽然他们现在看上去没什么紧密联系, 他们小时候真的是青梅竹马的亲密玩伴呢!

让机器拥有智能, 是人长久的梦想, 因为这样机器就可以聪明的替代人类完成一些任务。 二战中高速电子计算机的出现使得这个梦想更加近了一步。二战后,计算机也不被完全军用了,精英科学家也不要继续制造原子弹了,所以, 一下子既有资源也有大脑来研究 “智能机器”这种神奇的东西了。 我们可以随便举出当年研究繁盛的例子: 维纳在 1948 年发表了<控制论>, 副标题叫做 <动物和机器的控制和通信>,  其中讲了生物和机器的反馈,讲了脑的行为。 创立信息论的大师香农在 1949 年提出了可以下棋的机器,也就是面向特定领域的智能机器。同时,1949年, 加拿大著名的神经科学家 Donald Hebb 发表了“行为的组织”,开创了神经网络的研究;  图灵在 1950 年发表了著名的题为“计算的机器和智能”的文章,提出了著名的图灵测试。如此多的学科被创立,如此多的学科创始人在关心智能机器, 可见当时的确是这方面研究的黄金时期。

二战结束十年后, 也就是 1956 年, 研究智能机器的这些研究者, 都隐隐觉得自己研究的东西是一个新玩意,虽然和数学,生物,电子都有关系, 但和传统的数学,生物,电子或者脑科学都不一样, 因此,另立一个新招牌成了一个必然的趋势。John McCarthy 同学就趁着 1956 年的这个暑假, 在 Dortmouth 大学(当年也是美国计算机科学发展的圣地之一, 比如说, 它是 BASIC 语言发源地), 和香农,Minsky 等其他人(这帮人当年还都是年轻人),一起开了个会, 提出了一个很酷的词, 叫做 Artificial Intelligence, 算是人工智能这个学科正式成立。  因为 AI 是研究智能的机器, 学科一成立, 就必然有两个重要的问题要回答, 一是你怎么表示这个世界,二是计算机怎么能基于这个世界的知识得出智能。 第一点用行话说就是”知识表示”的模型, 第二点用行话说就是“智能的计算模型”。 别看这两个问题的不起眼, 就因为当时的研究者对两个看上去很细微的问题的回答, 直接造就了 LISP 和 AI 的一段情缘。

我们各表一支。 先说怎么表示知识的问题。 AI 研究和普通的编程不一样的地方在于, AI 的输入数据通常非常多样化,而且没有固定格式。 比如一道要求解的数学题,一段要翻译成中文的英文,一个待解的 sodoku 谜题,或者一个待识别的人脸图片。 所有的这些, 都需要先通过一个叫做“知识表示”的学科,表达成计算机能够处理的数据格式。自然,计算机科学家想用一种统一的数据格式表示需要处理多种多样的现实对象, 这样, 就自然的要求设计一个强大的,灵活的数据格式。 这个数据格式,就是链表。

这里我就不自量力的凭我有限的学识, 追寻一下为啥链表正好成为理想的数据结构的逻辑线。我想,读过 SICP 的读者应该对链表的灵活性深有感触。为了分析链表的长处,我们不妨把他和同时代的其他数据结构来做一比较。 如我在前面的一个系列所说,当时的数据结构很有限,所以我们不妨比较一下链表和同时代的另一个最广泛使用的数据结构-数组-的优劣。 我们都知道,数组和链表都是线性数据结构,两者各有千秋,而 FORTRAN 基本上是围绕数组建立的,LISP 则是围绕链表实现的。通过研究下棋,几何题等 AI 问题的表示,我们的读者不难发现, AI 研究关心于符号和逻辑计算远大于数值计算,比如下棋,就很难抽象成一个基于纯数字的计算问题。 这样,只能存数字的数组就显得不适合。 当然我们可以把数组扩展一下,让这些数组元素也可以存符号。不过即使这样,数组也不能做到存储不同结构的数据。 比方说棋类中,车马炮各有各自的规则,存储这些规则需要的结构和单元大小都不一样,所以我们需要一个存储异构数据单元的模块,而不是让每个单元格的结构一样。 加上在AI 中,一些数据需要随时增加和修改的。 比如国际象棋里,兵第一步能走两步,到底部又能变成皇后等等,这就需要兵的规则能够随时修改,增加,删除和改变。 其他问题也有类似的要求,所有的这些,都需要放开数组维度大小一样的约束,允许动态增加和减少某一维度的大小,或者动态高效的增加删除数组元素。 而一旦放开了单元格要同构和能随时增加和删除这样两个约束,数组其实就不再是数组了,因为随机访问的特性基本上就丢失了,数组就自然的变成了链表,要用链表的实现。

所以,用链表而不是数组来作为人工智能的统一的数据结构,固然有天才的灵机一动,也有现实需求的影响。当然,值得一提的是,在 Common LISP 这样一个更加面向实践而不是科学研究是 LISP 版本中,数组又作为链表的补充,成了基本的数据结构,而 Common LISP,也就能做图像处理等和矩阵打交道的事情。这个事实更加说明,用什么样的数据结构作为基本单元,都是由现实需求推动的。

当然,科学家光证明了列表能表示这些现实世界的问题还不够, 还要能证明或者验证额外的两点才行, 第一点是列表表示能够充分的表示所有的人工智能问题,即列表结构的充分性。 只有证明了这一点,我们才敢放心大胆的用链表,而不会担心突然跳出一个问题链表表达不了;第二是人工智能的确能够通过对列表的某种处理方法获得,而不会担心突然跳出一个人工智能问题,用现有的对链表的处理方法根本没法实现。只有这两个问题的回答是肯定的时候,列表处理才会成为人工智能的一部分。

对于这两个问题,其实都并没有一个确定的答案,而只是科学家的猜想,或者说是一种大家普遍接受的研究范式(paradigm)。 在 1976 年, 当年构想 IPL, 也就是 LISP 前身的两位神人 Alan Newell 和 Herbert Simon ,终于以回忆历史的方式写了一篇文章。 在这篇文章中,他们哲学般的把当时的这个范式概括为: 一个物理符号系统对于一般智能行为是既充分又必要的( A physical symbol system has the necessary and sufficient means for general intelligence action)。 用大白话说就是,“智能必须依赖于某种符号演算系统,且基于符号演算系统也能够衍生出智能”。 在实践中,如果你承认这个猜想,或者说这个范式,那你就承认了可以用符号演算来实现 AI。 于是,这个猜想就让当时几乎所有的研究者,把宝押在了实现一个通用的符号演算系统上,因为假如我们制造出一个通用的基于符号演算的系统,我们就能用这个系统实现智能。

上面我们说过, 链表的强大的表达能力对于这个符号演算系统来讲是绰绰有余的了,所以我们只要关心如何实现符号演算,因为假如上面的猜想是对的,且链表已经能够表示所有的符号, 那么我们的全部问题就变成了如何去构建这样的符号演算系统。后面我们可以看到, LISP 通过函数式编程来完成了这些演算规则的构建。

这里,需要提请读者注意的是, LISP 的全称是 LISt Processing, 即列表处理,但实际上 LISP 是由两种互相正交的哲学组合形成的, 一个是列表处理,另一个是函数式编程。 虽然在下面以后,我们会介绍 S-Expression 这样美妙的把两者无缝结合在一起的形式,但是为了清晰我们的概念,我要强调一下列表处理和函数式编程是两个正交的部分。实际上,我们完全可以用其他的不是函数的方式构建一个列表处理语言。在历史上,早在 FORTRAN 出现之前,Alan Newell 和 Herbert Simon 就用汇编实现了一个叫 IPL 的语言,而这个 IPL 语言就是面向过程的对列表处理的,而后,McCarthy 一开始也是用一系列的 FORTRAN 子程序来做列表处理的。比如 LISP 里面的 CAR 操作,其全成实际上是 Content of the Address portion of the Register, 顾名思义,寄存器的地址单元内容,也即列表的第一个元素(和C表达数组的方式类似,这里寄存器中存着指向列表第一个元素的指针)。 函数式的却不以列表为基本数据单元的语言也很多,比如 Scala ,就是以对象为基本数据单元。 因此,函数式和列表处理是不一定要互相耦合的。 那么,到底是什么原因使得 LISP 选择函数式,这样的选择又为啥更加适合当时 AI 的研究呢, 我们下节将继续介绍当时 AI 的研究范式,强弱 AI 之间的辩论和函数式编程在当年 AI 研究上的优点。

第二部分

上回我们说到 LISP 和 AI 很是青梅竹马的时候,浮光掠影地说因为 LISP 的基本数据单元–”链表”在知识表示上的比较优势。 我们说, AI 要处理的数据结构和要刻画的现实世界的模型很复杂,使得数组等其他简单数据结构不能胜任,所以“链表”成了最佳的选择。 如果我们顺着这样的逻辑线往下看,似乎选择 LISP 这个“列表处理的语言”似乎是理所当然的。 可是,这个原因并不充分。 因为 LISP 语言可不仅仅是列表处理,还包括函数式编程等等其他。 反过来说,如果仅仅是列表处理对于 AI 至关重要的话,那么在 FORTRAN 和 Algol 这些通用编程语言又非常普及的传统语言里面写一些关于列表处理的函数岂不是更加直观和方便? 归根结底,到底 LISP 还有什么其他奥妙呢?

当我们追寻函数式编程这条线索的时候,就会不可避免的触及到 AI 的早期历史。LISP 的特性,其实都是和当时 AI 的范式 (paradigm) 息息相关的。

AI 范式的演变

早在 McCarthy 这一代人提出 AI 之前,冯诺伊曼等人就开始研究什么是智能以及如何实现智能的问题了。 所不同的是,他们更加偏重于研究大脑的内部工作机理,并且试图通过在模拟大脑的工作机理,来实现智能。 这一学派的哲学很清晰: 人类大脑是一个标准的智能体,我们只需要让计算机模拟人的大脑的工作方式,计算机就有了和人类大脑一样的智能了。 对于这一派的研究者来说,他们相信大脑的结构和工作机理决定了智能,至于大脑是用脑细胞构成的,还是用电子电路模拟的,对于智能来说毫不重要。 这方面的著名工作就是冯诺伊曼的《计算机和大脑》这篇论文。 在这篇不算很学术的随笔里面,他分析了人的大脑有多少个神经元,计算机有多少个晶体管,通过这些定量的比较来解释计算机和人的大脑的差距。 当时和冯诺伊曼齐名的另一个神童是开创控制论的维纳。 他和冯诺伊曼一样,兼通很多学科。 和冯诺伊曼一样,他职业是数学家,但是也精通如神经科学和脑科学等学科。一个显然的例子就是在《控制论》这本书里面, 维纳对大脑和神经的分析比比皆是。这种对大脑和神经分析的传统,从 Cajal (对,就是写 Advice for a Young Investigator 的那个大神) 开始,一直延续到了后来 AI 中的联接主义(主要研究神经网络的一个人工智能学派)。

可是,从脑科学和认知科学的角度去分析智能在当时有一个非常大的局限: 脑神经解剖学本身不成熟。 比方说,现如今脑科学家在分析脑功能的时候一般会借助于 fMRI 和其他一些神经造影技术。这些技术可以做到实时观测到脑中血氧分布,直接确定大脑在执行特定任务时候的活跃部分。当年的科学家则只能使用有限的几种医学成像技术,或者从血管摄影的角度研究大脑。 受限于当时的研究条件,当年的研究者很难直接观测到脑神经的实时工作状态,分析大脑的实时工作机理。 因此,对脑的研究就很难做到非常深刻。 医学研究条件的限制,加上当时电子学的发展和集成度远远不够,用电子电路模拟大脑生成智能就显得非常遥远。 因此,虽然这一派的思想超前,但是大部分的工作都不在于真正的用电子电路模拟大脑,而是在探索脑科学和神经科学本身,或者仅仅用电子电路模拟一些简单的神经动力学行为和小规模神经网络。正是因为连接主义在实现人工智能本身方面进展并不大,所以在AI领域中一直不是潮流的研究方向。上个世纪 80 年代前成功实施的一些人工智能系统,极少是来自于连接主义学派的。直到80年代后随着 BP 算法的重新发现,联接主义才迎来了第二春。 这时候,LISP 已经过完 20 岁生日了。所以,联接主义 对 AI 领域使用的编程语言的选择的影响并不算大。

符号主义

虽然联接主义这一学派在当时不怎么流行,当年的 AI 研究可是进行的如火如荼。这如火如荼的学派,采用的是另外一套方法,我们称之为“符号主义学派”。 符号主义学派的渊源,可以直接追溯到图灵。图灵在人工智能方面做过很多的研究,其中最为大家所知的就是“图灵测试“了。 有句俗话叫做“在网上,没人知道你是一条狗”, 在这句话里,只要把“狗”换成“计算机”,就是简单版的图灵测试了。 用个比较“潮”的比方,图灵测试就是让一台计算机或者一个真实的人(又叫评委)在网上交流,然后让这个评委猜测和他交谈的究竟是人还是计算机。 如果这位评委也不能分辨谈话的对方到底是人还是计算机的话,我们就认为这个计算机已经足以“以假乱真”,拥有“和人类一样的智能”了,也就是通过“图灵测试了”。

在很长一段时间里,图灵测试一直是人工智能研究的圣杯(holy grail)。 也就是说,通过”图灵测试“ 成了人工智能研究的终极目标。 那么,自然的,最最直接的通过图灵测试的方法不是让计算机和人脑一样思考,而是只要能够让计算机处理对话中用到的的单词,句子和符号,并且在对话中能够和人一样的操纵这些单词和符号,计算机就有很大的希望通过图灵测试。 从最极端的情况来看,计算机甚至都不需要去“理解”这些句子的含义,都有可能通过图灵测试。 [具体细节可以阅读 Wikipedia 上的“Chinese Room (中文房间)”条目]。 有一个开源的聊天机器人,叫做 A.L.I.C.E., 就把上面我们说的“只要能够处理和操纵符号,就有可能通过图灵测试”发挥到了近乎极致。 这个聊天机器人在图灵测试比赛中已经多次骗过人类评委,到了非常“智能”几乎能以假乱真的地步。可是,就是这样一个离通过图灵测试很近的机器人,其基本结构却简单到了我们都不能想像的地步:A.L.I.C.E.  的数据库里面有一条一条的规则,这些规则规定了她看到你说什么的时候她说什么。唯一有点“智能”的地方,就是有些规则不光取决于你这句话,还取决于你的上一句话。 [比如日常对话中我们先问“你喜欢看电影么?”,接着再问“什么类型的?”,这时候就需要前一句话推出这个问题是“(喜欢)什么类型的(电影)”]。“中文房间”的例子,和 A.L.I.C.E. 机器人如此简单的结构,都出人意料的显示出,即使计算机拥有了对符号的操作能力,通过了图灵测试,它也未必是是“有智能”的。 可惜这句话只是我的马后炮而已,在 AI 发展的早期,因为图灵测试的拉动,联接主义的相对弱势和符号主义的繁盛,都把全世界的 AI 研究往一个方向拽,这个方向,很自然的,就是“符号处理”。

符号处理和 LISP 补充

其实上一篇我们已经提到了,Alan Newell 和 Herbert Simon 认为对符号演算系统就可以衍生出智能,所以上面的文字,算是对符号主义这个 paradigm 做一个历史的小注解。 当我们把 LISP 放到这段历史中,就会自然的想到, 什么语言适合人工智能的问题,就变成了“什么语言能做符号处理”。这个问题的答案,读者也都猜到了,就是 LISP。

符号处理在 LISP 里面的长处前文我已经介绍过一些了,这里我们可以再补充几点零碎的。LISP 里有一个大家都知道的统一表示程序和数据的方法,叫做 S-Expression。 这个 S,其实就是 Symbolic 的意思。 把程序和数据都统一的当成符号,用现代编程语言的话说,就是 LISP 支持 meta-programming。LISP 程序可以处理,生成和修改 LISP 程序。这个特性,加上函数是一阶对象的特性,使得 LISP 远远比同时代的任何语言灵活。我本人不是 LISP 的用户(初级用户都算不上),因此在这一点上所知有限。但单就我有限的对 LISP 的理解,我认为 LISP 的这种灵活,恰好满足了基于符号处理的 AI 领域对语言的“强大的表达能力”(可以对任何复杂系统建模)和“高层的抽象能力” 的需求。关于第一点,有一个著名的段子,说任何一门编程语言技巧和思想被提出的时候,总会有一个高人出来,说,这个玩意儿在 LISP 里面早就有了,具体的例子包括刚才说的 metaprogramming, object oriented language。这里面蕴含的,就是 LISP 的强大的表达能力,使得很多编程的范式,在 LISP 里面都能实现,或者找到影子。 关于第二点,SICP 中例子比比皆是,讲得都比我深刻许多,就无需废话了。

在上篇文章中我提到,翻开任何一本编程的书,都会讲到“LISP是适合 AI 的编程语言”。那么,如果您和我当年一样,有兴趣从事 AI 方面的研究和探索,就不免要疑惑:“为了学习 AI, 我要不要学习 LISP” 呢?现在距离我当年的这个疑惑已经差不多8年了,我并没有一个确定的答案,但是我知道了更多的一些事实。

如今的 AI 范式

如果你让任何一个 AI 方向的研究者推荐几本适合初学者的书的话,十有八九他会提到 “Artificial Intelligence: A Modern Approach”(人工智能,一种现代方法) 和 “Artificial Intelligence: A New Synthesis” (人工智能,一个新的综述)。 这两本书的作者分别是 Peter Norvig 和 Nils Nilsson,都是 AI 领域的元老级人物。 如果你是一个对书名很敏感的人,你肯定会想:奇怪了,这种书又不是畅销书,难道这两位大牛写了书怕卖不出去,非要在书名上加一个 “现代” 或者 “新” 来吸引眼球? 事实上,这个“现代”和这个“新”都大有来头。 实际上,这二十年来,AI 研究领域接连发生了好几个非常大的 paradigm shift. 传统的基于符号的 AI 方法不再是主流,取而代之的,是多种多样的基于统计的,基于自动推理的,基于机器学习的,基于群体智慧的,基于大规模数据集的等等各种各样研究方法的兴起。 这个 paradigm shift, 对于领域之外的人好像是静悄悄的,可实际上 AI 领域早已发生了翻天覆地的变化。所以才会有 “新” 和 “现代” 这样的词出现在书标题上。 不幸的是,大多写编程语言书的作者,未必全部知晓这个变化,因此还沿袭原来的框架,继续写下 “LISP是适合 AI 的编程语言” 这样一个早就不能完全反映现状的断言。 如果我们统计一个从事 AI 研究的研究者或者科学家用什么语言,答案可能是五花八门无所不有: 做 AI Search 的用 C/C++/Java, 做机器学习的如果模型和矩阵关系密切,可以用 Matlab, 如果统计计算较多,也可以用 R。 至于做数据挖掘等等,语言和库更加五花八门,根本无法宣称那一个语言占上风。LISP 是适合 AI 的语言的教科书神话,也早就被无数的这样的实例给打破了。

延伸阅读:

http://stackoverflow.com/questions/130475/why-is-lisp-used-for-ai


评论

知识共享许可协议
本博客作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
捐赠本站 ]]>
http://www.aisharing.com/archives/648/feed 2