用类来表示逻辑运算–关于行为树前提的一种实现方式

我们在学习程序的时候,都会提到一些逻辑计算方面的事情,像与(and),或(or),非(not)等,在编程语言层面也提供了相关的符号来表述逻辑的计算。我们都知道,对于AI来说,逻辑是非常重要的,一些简单的AI系统,就是由if-else搭起来的一个庞大的逻辑网,里面包含了各种预设好的可能性,最后得到一个决策结果,这样的系统还有一个名字,叫基于规则的AI系统(Rule based AI System),当然随着游戏逻辑越来越复杂,这样简单的靠分支的系统已经不能满足要求了,所以出现了各种AI架构,像有限状态机(FSM),行为树(Behavior Tree),计划器(Planner)等等。但不管架构如何变化,我们在AI中还是充斥着各种逻辑的计算。

那为什么我们要用类来表示逻辑计算呢?这就要牵扯到我们一直在聊的行为树的问题了,我们知道行为树在每个节点上都需要一个称之“前提”(Precondition)的部分,用来帮助我们来决定此节点是否能被执行,所以这个“前提”就会包含一个逻辑表达式,要么返回False,要么返回True,对于不同的节点来说,它的“前提”基本上都不会相同,那自然的,我们就会从程序设计的角度来思考,如何去实现它。当然,方法有很多,不过如果从以下两个方面来考量的话,可以考虑用类的方式来做。

  1. 模块化(目的复用逻辑项)
  2. 为行为树的编辑器做准备(目的可视化的逻辑表达)

说到类的实现方式之前,我们可以先来看一下,一般代码中的逻辑表示,以c/c++为例:

 1: bool a = IsAOk();
 2: bool b = IsBOk();
 3: bool c = IsCOk();
 4: if(a && (b || c))
 5:     //do sth
 6: else
 7:     //do other things

这样的方式很直接,很简单,作为“前提”的实现也是很好的,比如我们可以提供这样一个类,然后让用户继承这个类,自己在isTrue的方法里写需要的逻辑计算。

 1: class Precondition
 2: {
 3: public:
 4:     virutal bool isTrue() const = 0;
 5: };

但考虑到我上面的两点需求,这样的实现就显得不够模块化,因为我们并不能复用上面的任何一个逻辑项,而且更重要的是,我们也没有办法在编辑器中“看到”它的逻辑表达。基于这样的原因,我们再来看看用如何用类来表示逻辑计算,还是用上面这个纯虚类,我们定义几个称之为类逻辑操作符的新类

  • PreconditionAND
  • PreconditionOR
  • PreconditionNOT
  • PreconditionXOR

这些类继承自上面的Precondition,并重写了isTrue方法,我们用PreconditionAND举例:

 1: class PreconditionAND : public Precondition
 2: {
 3: public:
 4:     PreconditionAND(Precondition* lhs, Precondition* rhs)
 5:         : m_Lhs(lhs),m_Rhs(rhs)
 6:     {}
 7:     virtual bool isTrue() const
 8:     {
 9:         return m_Lhs->isTrue() && m_Rhs->isTrue();
 10:     }
 11: protected:
 12:     Precondition* m_Lhs;
 13:     Precondition* m_Rhs;
 14: };

可以看到,这个类很简单,它接受两个Precondition的实例,然后在isTrue里再调用这两个实例的isTrue方法,并返回它们的And结果。对于其余的逻辑符号类,我们也用同样的方式来定义,值得注意的是,对于PreconditionNOT来说,它只接受一个Precondition的实例,因为Not是一元的操作符。

有了逻辑操作符,我们就可以复用Precondition的逻辑项了,比如

 1: Precondition* precon = new PreconditionNOT(
 2:                             new PreconditionAND(
 3:                                 new PreconditionA(), new PreconditionB()
 4:                             )
 5:                         );
 6: //ret means !(A && B)
 7: bool ret = precon->isTrue();

我们用类逻辑操作符链接了逻辑项,使之达到了复用和模块化的效果。对于这种方式,我们需要不断的整理和提炼,并预先定义一些我经常用到的一些单个的逻辑项,就像上面所示的A,B一样,然后通过类逻辑操作符,组合成我们需要的逻辑计算结果。对于这些单个的逻辑项,因为需要被复用的关系,所以要尽量保持内部的逻辑是简单的。

就我们想要的行为树编辑器来说,我们可以将这些预先定义好的单个逻辑项导出到编辑器,然后在编辑器里,我们通过类逻辑操作符,来组成出我们需要“前提”,这样,我们就可以在编辑器里直观的“看到”逻辑的表达式,也方便我们检查逻辑的正确性。

当然,这种方式比起第一种直接在代码中书写逻辑的方式,相对复杂,在开发过程中,team对于这种理念的坚持程度也有待验证(因为需要不断的优化逻辑表达式),但对于一种用类实现逻辑的方式,我觉得还是有必要在这里介绍一下。我在实践中,两种方式都用到过,各有利弊,对于极度追求模块化的设计理念,并且想做行为树的可视化编辑器的朋友来说,用类的方式还是非常值得一试的。

好,今天就聊到这里,希望对大家有所帮助。

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

(已被阅读9,176次)

3 评论

  1. 感觉子逻辑的粒度很难控制。首先策划和程序人员对于逻辑的最优单位理解是不同的,策划同学的 base units 可能是有逻辑交集的;然后逻辑上的“可理解性”和“复用性”是天然相悖的,如果粒度太细,就必然造成理解难度的增加,对于策划同学来说可能是不小的负担。

    1. 程序可以提供若干个原子级的判断类,策划再来组装。逻辑式重用和优化都可以脚本批量处理。逻辑式的复杂度也好管理。

发表评论

邮箱地址不会被公开。

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

Copyright © 2011-2020 AI分享站    登录