查看原文
其他

写了多年的策划案,我竟然都不知道这种宝藏方法

猴与花果山 腾讯GWB游戏无界 2022-08-30


编者按 我们在游戏开发过程中,经常会遇到一些“含糊不清”的问题,在这些问题上,有些是策划本身可能想得非常清楚,但是苦于表达能力有限,到策划案上的文字就变得不那么清楚了;也有些是策划想得简单了,错过了一些“边界问题”产生了设计矛盾。如何避免这个一直以来都是策划和程序交流中存在的问题呢?这里推荐一种设计思路给游戏策划们,这个思路并不帮你完成好完整的策划案,但却可以帮你进一步有效的提高一份策划案的有效沟通功能——这就是“用设计断言的思路来写你的需求”。



作者:猴与花果山

(本文内容由公众号“千猴马的游戏设计之道”提供,转载请征得同意。文章仅为作者观点,不代表GWB立场)



断言(Predicate)在计算机编程领域的定义是一种判断式,他的返回值是一个bool,常被用来作为排序、查找等准则,通常会有1到2个参数作为操作参数,分为单参判断式(Unary Predicate)和双参判断式(Binary Predicate)。通常我们在编写代码的时候,比如要筛选某个事物,会用到单参判断式;要进行排序,会用到双参判断式。

当然,这里要说的“断言”,是在这个知识基础上扩展出来的,我们丢掉了“单参”和“双参”等概念,我们可以直接把断言表达为:


在这个表达式中:

SomeThing(对谁)


是我们要进行方法的对象,这个“对象”和计算机编程的“对象”不是一个概念,他就是“对象”这个词儿的字面意义,也就是我们要对什么进行处理。这里的“什么”通常是指一个组,一组某个东西,比如“一碗馄饨”,“一群人”,“一场比赛中的所有球员”,“背包中的所有道具”等等。

SomeMethod(做什么)


是我们要对于这一组东西做什么样的处理,比如最常见的Sort(排序),Find(查询)等等,抛开计算机程序的约定,他从自然语言的角度出发(也是我们设计的角度出发)可以是任何事情,比如“(一碗馄饨中)【符合条件】的盛出来”,“(一群人中)【符合条件】的发给他们一个喇叭”,“(一场比赛中所有球员)【符合条件】的给予掌声”,“(背包中)【符合条件】的卖出去”。这里的“盛出来”、“发给他们一个喇叭”等,就是这个SomeMethod,值得注意的是,他们都有一个【符合条件】的这个描述,这个所谓的“符合条件”,也就是后面括号中的条件,这是一个Lambda表达式,也就是“编程领域的断言”。

Reference(参照对象)


是一些参考变量,他服务于“符合条件”这些事情,比如我们在通过一个矮小的洞的时候,要对队员发出指令“身高高于洞窟高度的请蹲下”,那么此时我们就需要一个Reference,就是“洞窟高度”,因为洞窟高度是一个“变量”,他不是事先就能写好的,所以他不能是一个“常量”。事先能写好的(常量)我们称之为“规则”,比如五子棋连到5个就获胜,他一定是5个。

Things(操作对象)


也就是我们之前提到的“单参”还是“双参”,事实上这是这个断言需要做的事情决定的,正如上面举例的,如果我要做的是筛选,只需要遍历每一个对象,所以只需要一个参数;如果我要进行排序,那么需要两个对象对比决定当前对象是2个参数换位还是不换位,那就需要2个参数。值得注意的是,只有要被操作的对象,他才能成为Things。

Lambda表达式(断言本身)


最后,也是最重要的,就是这个[Reference](Things)=>bool的Lambda表达式了,这也就是断言本身,也就是我们真正需要费心思去设计的核心内容。通常我们策划案甚至是策划脑海中不够清晰的设计,都是因为没有设计好这个断言。


当我们了解了“断言”之后,进一步来看我们的游戏设计。在我们的游戏设计中,通常会出现类似这样的想法:

  • “雪山上的敌人都会掉落雪球”

  • “对比玩家弱的敌人造成5000点伤害”

  • “低等级的玩家可以领取一个礼包”


抛开玩法好不好,在看到这些需求的时候,我们直觉上可能不会有任何疑问,因为从人类的角度来看,这些说法都很简单,但是他们真的都没有问题吗?

概括是原罪


当我们为这些需求脑补并自洽了一些内容后,我们确实会觉得这些描述本身没有问题,可是你进一步批判这些问题:

  • 首先是“雪山上的敌人的都会掉落雪球”,“雪山”指的是什么?一个地方还是所有符合某个条件的地形?或者是某些被标注为“雪山”的地区?“敌人”是指什么?

  • 接着是“对比玩家弱的敌人造成5000点伤害”,“比玩家弱”是怎么认定的?等级更低还是HP更少?“敌人”是指什么?

  • 再来是“低等级的玩家可以领取一个礼包”,“低等级”是指什么?“一个礼包”又是什么?

这里的问题非常明显,因为我们可以脑补,但是计算机没法脑补,你需要给他更精确的信息。通常我们在生活中善于使用概括,概括是为了交流变得简单,因为人都会思考,结合经验的思考,所以生活和知识面越是接近的人之间,概括词用的越不会产生歧义(所谓的“圈子”正是如此)。但是计算机最大的问题是它不会思考,它也没有选择知识的能力,所以你必须对它表达清楚每一个细节,这就是我们要设计的断言。

缕清思路的过程就是一点点事无巨细的去思考

如何思考这个断言

这里我们可以分为4个步骤来做这个思考:

第一步,为你的“对象”选定一个范围


所谓“对象”通常是这个想法中的一些名词,比如“角色”等。而这些对象,通常在游戏中会有很多,并且他们会被储存在一些列表中,随时可以索引到并且使用。只是在每个游戏中,这些对象的储存列表可能会不同,我们根据自己的设计需求,可以先大致锁定到这究竟是一个什么列表。

就比如游戏中的角色列表:我们这个游戏可能是一个网游,在每个aoi或者每个场景都会有一个角色列表;也可能是一个单机游戏,他就只需要一个当前游戏进程相关的所有角色组成的列表。如果是后者,那对象的范围就是这个角色列表,如果是前者,我们就需要一个新的简单断言来找出这个列表,比如“雪山”的角色列表,那么如何得出“雪山”,这也需要一个断言,比如是概念里就是某个地图被我们认为是雪山,那么就是那张地图下的角色列表;如果是说我们有好几张地图,他们的类型是雪山,那么我们需要的就是所有类型为雪山的地图的角色列表。

我们再回到上述的例子中几个需要确定范围的“对象”:

  • 雪山的范围:是所有的地图。

  • 敌人的范围:是角色列表,或者说“当前角色列表”,如果是“当前”角色列表,我们则需要对于“当前”设计进一步的断言,从而从范围中选出“当前”。

  • 一个礼包的范围:可能是礼包列表,或者道具组列表等。


用计算机编程的理解来看,这就是找到了需要遍历的数组,这也是这类需求必然存在的一个数组。

第二步,精确地列出你的断言内容


逐条的列出一个条件组,对“范围”中每一个单位套用这个条件组,如果合适他就是要被操作的对象,这个“条件组”正是我们要设计的断言的内容。在做这个工作的时候,我们得逐词地去想清楚他们具体指代的是什么。这个“指代”并不是说“我想要的是什么”,而是在游戏的数据中,我可以如何获取到这些信息。

比如我们用上述例子中比较复杂的一条“比玩家弱的敌人”,我们现在进一步将它转为一个断言,因此首先我们需要定义的是:

  • 什么是玩家?在这个语境下,玩家应该特指的是某个角色,或者是某个指标。那么很显然,我们对每个角色,无论怎么判断其数据,都无法得出“他一定是唯一的”这个结论,因为尽管我们总觉得“我可以保证只有一个角色的PlayerControlled是true”或者“我可以保证只有一个角色的Side是0”,但是这种“保证”一文不值,从计算机角度来说,在列表中符合某个条件的元素应该是可以组成一个新的列表的。所以真正的“玩家”的意思,在这里说的,应该就是"Side==0"或者“PlayerControlled==true”,他是一个简单的判断条件的概括,而非是对一种角色的概括。

  • 什么是弱?这也是游戏设计的一环,假如我们游戏中有“战门力”这个属性,这个属性越高我们认为越强,反之则越弱,然后“战门力”是一个角色的属性,那这里的“弱”说的就是角色下的战门力属性小于一个值。而因为整句话是“比玩家弱”,所以他的比对的对象确实就是一个角色,或者由一群角色作为参数算出来的一个值:“参考战门力”=f(角色组)。想到这里,我们不得不回过头再去看“什么是玩家?”这时候我们就需要更进一步精确的去设计这个,最终得出一个可以必对的值来定义“弱”,比如“所有Side==0的角色中等级战门力最高的那个”来作为“玩家”。

  • 什么是敌人?这看似没什么问题,但是计算机依然不能明白什么叫“敌人”,你需要定义比如“我能打他,他不能打我,他算我的敌人吗?”以及“我不能打他,他能打我,算是我的敌人吗?”等等,你至少得定义出一个条件,比如对应于“什么是玩家”得出“Side != 0”就是“玩家的敌人”。



最后我们把这些条件一条条梳理清楚了,得出了“Side!=0的、战门力低于‘战门力最高的Side==0的角色’”这组条件,就是我们的断言,符合这个条件的角色都将被执行“造成5000点伤害”,因为这个设计意图就是“对比玩家弱的敌人造成5000点伤害”。

第三步,演算与验算


当我们设置完断言之后,应该尝试带入一些符合范围的角色进去,验证一下这个断言是否返回了我们预期的值。接着上面的例子,比如我们把角色当前心灵控制(或者奴役等)的角色代进去,因为我们游戏的实现方式是,当“玩家”心灵控制或者奴役一个角色的时候,这个的Side会被暂时设为0,这样一来,我们上面设计的断言,就会导致一个“比玩家弱的敌人”因为被控制了所以(暂时不是敌人了)而不受到这个5000点伤害(设定如此)——这是演算的结果,如果这个结果符合设计(确实被精神控制的角色就不该受到这个伤害)那么这个断言成立,否则,我们就需要进一步思考来改变或者添加条件,使得这个断言演算出来的结果符合我们的期望。

当然,到这里不得不提一句,如果接受了演算的结果,我们应该把“被精神控制的敌人除外”这句话加在“Side!=0的、战门力低于‘战门力最高的Side==0的角色”后面,作为一个补充说法,尽管他对于计算机程序来说是一句废话,但是它可以让我们将来有一天回头看代码的时候(重构)不至于怀疑这个实现是不是有漏洞。

第四步,分析变量和常量


最后就是分析出这个断言中的变量与常量,这将有助于实现的时候对于数据的处理,比如我们结合上述例子,得出的断言是——对所有“雪山地图的角色中:Side不等于0,战门力低于Side等于0的角色中战门力的最高值的角色(被“玩家”精神控制的角色不在此列)”造成5000点伤害。

我们先把这句话装上主语,也就是“我”,“我”指代的是“雪山地图中的某个角色”,这个角色是遍历时的每一个角色。得出——如果我的Side不等于0,且我的战门力小于Side等于0的角色中最高战门力。然后我们再把一些数据不是“我”的抽出来看,因为“我”的肯定是Things的。因此“战门力小于Side等于0的角色中最高战门力”才是我们要分析的,首先是分析我们需要的是这个角色还是战门力?这很好理解,因为我们的“算法”也就是断言中比对的是“战门力”,所以我需要的参数只是“战门力”不是角色,这里我们可以丢掉“这个战门力是怎么来的”这个概念,因为他想怎么来的不重要了,只要知道他是一个Reference就足够了。那如果我们换成“所有身高在1米8以上的同学”,这里的“1米8”就不是一个Reference,而是直接在断言中的一个常量。


到此,我们的整个断言公式所有的内容就填充完毕了:

  • SomeThing=所有雪山地图的角色

  • SomeMethod=造成5000点伤害

  • Reference=第1个参数(总共1个参数):所有角色中Side不等于0,且战门力最高的(这是另一个断言得出的结论)

  • Things=“我的Side不等于0”并且“我的战门力比Reference第一个参数小”


这时候,逻辑已经通了,但说的显然不是人话,于是我们需要进一步的加工包装成一句人话,写进策划案,只是相比原来,我们把很多概括词而替换为了更具有逻辑性的语句。比如我们可以这样描述这个需求:

我们先从所有的地图中找到所有Tag中带有"雪山”的地图,定义这些地图为“雪山地图”,如果没有一张地图符合条件,那么这件事情到此结束。然后我们对游戏中所有的角色做个判断,找出所有Side等于0的角色(通常来说,都是会有一个玩家列表的),根据战门力从大到小进行排序,得出战门力最高的角色,如果没有这个角色,这件事情到此结束,如果有,我们将列表第一个角色的战门力视为“玩家战门力”。比对每一张雪山地图中的每一个角色,如果这个角色的Side不等于0,并且他的战门力小于(不含等于)玩家战门力,就对这个角色造成5000点伤害。

到此一段设计完成,至少在逻辑上他已经是完整的了。


在游戏设计中(确切的说是任何设计中),使用概括是一件非常危险的事情,因为概括本身是为了交流方便而存在的一种“模棱两可”,而人间绝大多数的误会,也产生自概括,为了项目更好的推进,也为了大家目标明确,在设计时请避免使用概括词,而避免的方式,就是先转化成断言,然后再转化回策划案。




您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存