为什么数据结构设计是游戏策划必备技能?

作者:猴与花果山 千猴马的游戏设计之道 2020-09-21
在游戏的实际开发过程中,有一个不可忽略的必经过程是数据结构设计,其实任何it产品中,都会有这样一个过程,只是通常做这件事情的人是程序员。实际上至少在游戏设计中,这样的分工是不对的,因为游戏设计的核心之一就是数据结构设计,而游戏的创意等的可能性、设计师对于游戏的理解能力等,也依赖于数据设计这件事。

01、数据结构设计是游戏设计的70%工作内容

这道理非常简单,因为游戏本身的运行原理,就是一系列逻辑系统在不断地改变游戏中运行的数据,最终当数据达到了一定情况的时候,一局游戏就结束了,游戏的结果(包括胜负、得分等等元素)也都在数据中了。这也正是Entity Component System(简称ECS)这个逻辑框架之所以能走红的原因。ECS本身形式,就表现为System可以理解为游戏系统的逻辑,Component可以理解为单纯的数据结构,而Entity则是数据结构的集合,通过System对于带有特定Component的Entity进行处理从而完成整个游戏系的逻辑结构。

(2017GDC上暴雪大佬发表了守望先锋的架构设计之后,ECS才进入了国人的视野,大多团队才开始逐渐重视起数据设计这个工作)

为什么说“数据结构设计好了,游戏的设计就完成了70%了”?因为设计数据结构这件事情,不但是对于游戏玩法的一次精确地抽象设计,还是游戏所有扩展性创意灵感的根源。当然,我们并不是说所有数据结构的设计应该遵循ECS的思路,甚至绝大多数时候设计游戏的数据结构都和ECS无关,游戏的数据结构设计,本质上是对游戏元素的逻辑化归纳。接下来我们就用一个实际的例子来看这件问题——中国象棋游戏的数据结构:

(截图来自《qq游戏 中国象棋》)

在中国象棋的运行过程中,存在2个核心的数据结构,也就是游戏的核心元素,他们分别是“棋子”和“棋局”:

首先是棋子(ChessObj),棋子的数据结构中包括的属性有:

  • 图形(icon|string):这个棋子的贴图是什么,其实就是我们肉眼能看到的,比如“将”。
  • 阵营(side|number/byte):是红方还是黑方,这里之所以不用boolean,是因为也许扩展为3方甚至4方可能更好玩,我们并不在这一层去“约束创意”,留多一些可能性,这也正是“创意发展点”的根本所在——当策划去思考数据结构的时候,自然而然会多想一些玩法扩展,尽管这些扩展大多未必合适(玩法设计),但是他们一定是合理的(从逻辑上来说是对的,只是设计上是否pick这个idea的问题,至少绝不会是“天马行空的创意”)。
  • 行走规则(moveOffset|array<array<point>>):以自身position为中心,取所有偏移单元格,作为这个棋子允许移动的单元格,即offset[落点信息][路径]。当策划设计到这里的时候,应该思考一个问题——中国象棋里的车,移动范围是左右各8格、上下各9格(因为一些格子不在棋盘内,因此依然不能移动到,这就是规则,也就是系统的约束)?这个问题其实很有意思,因为我们作为玩家的时候,一直根深蒂固的理解就是“车可以横竖走满”,但是从没有对这个定义进行过批判,而假如游戏被扩展为棋盘是18x20的时候,车的移动范围如何呢?还应该“走满”吗?这是一个非常有意思的思考,也是一个游戏策划设计游戏时候必须要做出的思考,因为这是一个游戏玩法平衡性的规则。而细心的朋友到这里可能发现了一个问题,为什么这采用的是二维数组?可以移动的单元格不应该一维point就足够了吗?只要列出所有能走的offset就行了?这里有一个细节,就是第一层array实际是指这个棋子有哪些可以走的路径,而第二维则是这个路径上的点,第二维最后一个元素,则有着这个棋子“落点”的性质,因为中国象棋里面有个“卡脚”的设计,我们看下图(图解1):

(图解1)

在“图解1”中,红色圈的象,原本的移动范围应该是3个红圈+1个紫色的圈,紫色的圈我们知道有了盟军的象所以就不能走了,但是其实左上角那个红色的圈依然无法走到,是因为象走路的规则中检查青色和蓝色的圈是否有子,有子(比如蓝色圈中有个将,不论敌我)他都不能走到,因此我们在数据结构中应该定义一个“路径”,路径中的任何一格有其他棋子都不能移动。而从这里,我们可以扩展出一个概念——如果我们把路径做了修改,他就能成为另一个棋子,比如我把所有青色或者蓝色的圈去掉,也就是最后结果是一个只有一单元(落点)的数组,那么象就变成了“飞象”。

  • 特殊规则(special|enum):行动的特殊规则,用于炮吃子和飞将等“不符合逻辑”的规则。炮吃子,即炮的所有行动规则中,如果落点有非本阵营棋子,那么路径上必须有且只有一个任意棋子;而飞将则是根据运行时的地图,动态判断,如果双方将之间没有任何棋子,那么行动方的将移动规则中将多出一条{对方将坐标}的数据。

接下来就是棋局的设计,棋局肯定是一个存在的数据结构,因为一局游戏玩的就是这个(即游戏中核心的数据就是这个):

  • 棋子(chess|array<array<ChessObj>>):实际上按照中国象棋标准棋盘来说,他是一个9x10的二维数组,每一个单元可能是null(没有棋子)或者1个棋子(ChessObj),和我们肉眼能看到的棋盘是对应的。但是我们在这里其实偷了一个懒,事实上即使双方对战的中国象棋,他的棋盘也应该是三维数组:Map[side][x][y],即各方阵内的5格高度才是真正的一个子棋盘,由规则(而非数据)建立起棋盘之间的“交通”,如“图解2”:
(图解2)

在图解2中,我们可以看到,每一家阵营是一个“子棋盘”他是一个9x5的数组,而对方阵营在双人象棋中是一个180度旋转后的“子棋盘”,这意味着规则上我们必须建立在2个子棋盘之间的交通,比如位于map[0][2][4]的棋子,前进一格达到的是map[1][6][4],即map[toSide][width - this.x][this.y]的单元格;除此之外,由于棋子移动有方向性,比如“兵”只能向前,因此在对方棋盘,即第一维下标不等于棋子的“阵营”时,他的“行走规则”中的y向是乘以-1的。

到这里可能有人要问了,为什么要自讨麻烦?明明二维数组就可以做好了的,其实这里不光有一个非双人象棋的问题:

(在我小时候,玩过这么一个“三国象棋”)

其实“三国象棋”本身是好玩的,因为热闹,而玩游戏,本质也并不是为了追求输赢,毕竟不是体育比赛象棋,玩,最重要的是开心,输赢,是带来开心的“标尺”。当然也不光是因为多方阵营的可能性,还有比如我们为中国象棋增加一个“技能玩法”,某个技能可以在2轮内将敌人的小兵变为自己的,那么这时候,这个小兵应该如何移动呢?因此真正的数据结构抽象,对于这种具有“双反阵营”概念的棋类游戏而言,他应该是3维数组,而非2维数组,同理《四国大战》等也是如此。

  • 行动方(side|number):现在是谁行动,正如我们说的,不光是三国象棋可能会有超过双方行动的时刻,为了好玩,我们可以让4个人走象棋,玩家1和3轮流控制一方,2和4轮流控制一方,行动顺序1黑2红3黑4红,2人同时控制一局也可以带出一些乐趣来,比如情侣象棋(当然现在的情侣多半不会土到去玩象棋就对了),这就是一个很好的拓展。而之所以没有回合这个概念,也正是“回合”是一个虚的概念,他是f(总的行动次数)可以算出的东西,所以不需要储存在数据结构里。

到此,一个中国象棋的核心数据结构被设计出来了,而基于这些数据结构,也可以轻易就实现出象棋的玩法。同时在这个设计过程中,我们看到了许多“存疑点”和“可扩展点”。比如“车的移动真的是全屏吗?”,就像俄罗斯方块“旋转方块,真的是旋转90度,而不是切换成另一种组合了吗?L旋转一下一定不能变成正方形吗?”,这些批判性的存疑点,并不是用来批判老的规则设计的,而是给我们一些启迪,告诉我们还有什么新的扩展契机,和“可扩展点”比如“能不能设计一个buff,让我方2回合内所有的棋子都飞起来不受路径上棋子影响?”一样,任何扩展契机,都是一个逻辑十分完美的“创意”,但是这个“创意”本身是否是“好的创意”(合适现在的游戏)就要回到设计层深入思考一下。

而看到这里的小伙伴也许还会问“中国象棋哪儿有这么麻烦的?我棋盘是一个array<array<int>>,负的代表红方,正的代表黑方,1代表将,2代表士……这样做不好吗?为什么要这么麻烦呢?”,其实这个想法也是对的,只是这就是“需求实现者(coder)”和玩家的理解,不是一个“游戏设计师(Game Designer)”应有的理解,Game Designer应该不放过游戏可能出现的创意。

02、游戏策划如何去从做数据结构设计游戏玩法

其实设计一个游戏本身是设计多个玩法,每一个玩法的设计,都是30%的UI和系统规则+70%的数据结构(事实上在设计数据结构的时候,会对规则有一次巩固和反思),所以一个玩法的设计过程,通常可以分为这样几个步骤,来做到用从数据结构出发设计玩法:

01、用一句简练的话说清一个玩法

任何玩法,其实都是可以用一句、最多几句话就能说清楚的。只要首先去掉那些感动自己,或者感动自己心中用户的那些话术,比如“这样设计玩家就能产生心流”,“这个设计的目的是为了让玩家不会疲劳”,这些话正是因为听起来非常有意义,所以会让你陶醉其中,感觉这个设计完美无缺。但是这些话,在真正的游戏设计这件“冰冷”的事情面前,就是纯粹的浪费时间,毫无意义。你应该去掉一个“设计”中所有类似的语句,只留下清晰的逻辑性语句,比如:

我们要做的是一个小镇经营的游戏,小镇上有几个建筑是玩家可以买下来的,买下后就会产钱给玩家。随着小镇等级的提高,每个建筑物会产生的产品变多,每个产品都会带来不同的产出。

(经营小镇的游戏产品其实挺多的,但是每个产品的细节设计都不一样,因此这一句话要说清楚自身的特点,而不是简单地“抄谁”)

这就是一句非常清晰的话,直观的就表达出了一个玩法,但是他并不是完整的设计,因为缺乏数据结构的设计,因此会缺失很多细节设计,由此会带来许多逻辑问题,因此我们接下来要:

02、分清每一件事情,找到玩法和数据结构

从语文上来说,这句话说的是一件事情,但是从逻辑设计上来说,这句话里藏着几件事情,我们需要进一步的分析一下,这些事情分别是:

  • 我们经营的【小镇】上有一些【建筑物】。
  • 【建筑物】本来就在那里,玩家要把它买下来变成自己的。
  • 【建筑物】本身不会产钱,产钱的是建筑物中的【产品】。
  • 【产品】的解锁和【小镇等级】挂钩。

到此,我们从这4件事情里面,就能分析出一个初步的数据结构:


接着,我们就要针对这个数据结构和对应的玩法,来进一步深入思考,因为很显然这个结构反馈出的玩法里还漏了不少细节:

先从产品出发:

  • 产出是单位时间产出吗?应该是的,那么单位时间是多久?整个游戏所有建筑物的产出都是一样的时间间隔吗?

(在《芭芭农场》中,不同“建筑物”中不同“产品”的产出速度是不同的)

如果是一个挂机游戏,是不是应该鼓励玩家经常上线,所以产出应该有一个时间上限的限制?

然后再看建筑:

  • 建筑物的外观总是一尘不变的吗?
  • 建筑物的外观应该如何变化?除了本身变化规则会不会还有节日等影响?
  • 建筑物的名称会不会随着外观变化?

(建筑外观随等级变化是经营类游戏常有的)

  • 如果产品有个产出上限,这个上限是否应该由建筑物承担?
  • 这个产出上限和什么属性有关吗?
  • 这个产出属性是否也可以升级?

最后我们来看小镇:

  • 等级是如何提升的?

当我们有了这些思考之后,会发现原来的一句话描述里,有太多还没说清楚的具体细节,然后这时候我们基于这些问题,又可以进一步修改数据结构,把它扩展为:


经过一轮对这些事情的思考和批判,我们得出了初步的数据结构,但这还远远不够,接着我们要深入到做游戏的细节和游戏的一些可能会扩展的设计去进一步加工:

03、批判每一个细节找到“隐藏属性”和解决“隐藏问题”

现在我们再来对于每一个元素的玩法进行一次更深入的思考和批判,这些思考与批判跟上一步的“补充”不太一样,更多需要从玩法设计的角度去分析和判断,而不再是从“常理”的角度看问题,我们依然针对每一个环节去分析:

产品:

产品是否存在季节性的产品?比如我们有个水果店,里面的一些水果是季节性的,比如草莓只有春末夏初才有。如果需要,那么产品属性下就应该有一个live属性,来表达当前产品是否激活中,而对于“大楼产出”这件事的运算又会有不同的算法,因为会关注这个live属性,只有live==true的产品才会产钱。

产品是否存在限制性解锁?比如有些产品必须在情人节活动的时候解锁,然后就一直拥有了,否则就要等来年的情人节。如果是的,那么至少在服务器发来的数据中,需要有一个visible的信息来表达产品是否需要显示。

如果这个项目是一个和某资源合作的游戏化项目,那么这个产品是否会因为游戏化内容而得到增幅的,比如我们做一个wegame小镇,里面有个拳头大楼,当我们在wegame玩lol的时候,“英雄联盟”这个产品产出的金币会在这期间翻倍。或者在wegame平台购买lol的某款英雄皮肤,可以让“英雄联盟”产出间隔缩短50%,持续2小时。如果是的话,那就会扩展出来一个增幅(boost)的数据,boost中有开始时间、结束时间、增幅内容。而boost本身并不是一件小事,我们可以展开看一下——问题的关键在于服务器与客户端的同步,尤其是不使用长连接的h5游戏,客户端在产出金币的时候,每一次产出都是最小时间单位,因此产出的瞬间,根据所有的boost计算一下,就能得出一个精确地数字,但是服务器不能这么做,因为这样客户端服务器交互频次会非常多,只能采取客户端每一定时间通告服务器一次,服务器根据时间戳算出2个时间点的产出,这时候问题就来了——2个时间点内的boost是会发生变化的,我们把2个时间点看成一个timeline:


我们从上图不难看出,多个起始、结束时间不同的boost,把两个产出时间中间的产出,按照产出量分出了5个区间,每个区间的产出量都不同。因此当我们设计这个boost的时候,就应该同时去设计一个服务器产出金币的算法来支持同步。这也是在数据结构发生扩展时候需要考虑到的设计问题之一。

建筑:

  • 是否会有影响整个建筑物产出收益的设计呢?比如玩家违规导致建筑物停产?这些其实都是对于建筑物下产品的boost,因此在建筑层完全不需要这个属性。
  • 建筑物是否允许增设?比如原来是卖手抓饼的店铺,可以增设奶茶店,这样产品列表里就有了奶茶。而这个思考,就会带来更深层次的思考:
  • 增设的店铺是否是随着建筑物就定下的,如果是那么可以利用产品的visible和live属性;如果不是,那本质上就是原本在地图上的建筑物,实际上是一个建筑物array,而显示依赖的是第一个建筑物(默认必定有1个建筑物)。
  • 增设是否可以提供效果?比如增设了芝士加工间,会让所有汉堡类产品产出提高?那这里就有一个增设是否可以拆除的问题(可以替换=可以拆除),不能拆除的,只需要增设的时候直接改变产品列表里对应的属性就行;能拆除的,则得走boost(永久存在的,在拆除时移除)。

小镇:

  • 是否会有随着时节换外观的设定?如果有,则当前小镇的外观将会是一个属性。比如圣诞节有一套特殊的小镇美术等。

到此,我们做出了一些假设,这些假设本身都可能是合理的设计,并且除了这些情况,可能还会有很多扩展内容,因此在游戏设计的过程中,基于数据结构,进一步思考设计细节,从而产生出新的idea;或者利用现有的数据结构略微改变一些规则产生出新的玩法,都是游戏设计中的核心设计部分。当我们列好这些数据结构之后,工作还没完,因为这些结构,只是运行时的,是在游戏运作时候用到的,最终会交给程序员去实现,而这些数据中有一些数据是“非常主观”的,他需要策划填表来实现,我们称之为“model”,也就是产生这个数据时用到的一些基础信息,也正是策划填表的数据。

04、找到Model

Model一定被真包含于数据结构之中,也就是说Model拥有的属性,在运行时的数据结构中也一定会有。从运行时的数据结构中找出Model,通常依赖于几个“现象”:

这个属性是主观的——计算机程序算不出来,或者即使能算出来,也是策划觉得是什么就是什么的,是model的属性。比如名称、图标、外观等,都必定是model的属性。

(卡牌游戏中角色造型、名称、属性等,都是主观的,属于Model的属性)

  • 这个属性可以是静态的——反例是比如清版射击游戏中的子弹坐标,清版射击游戏的子弹坐标每一帧都在变化,所以肯定不是model的属性;而一些游戏中建筑物、技能等元素可以升级,如果升级只是某些属性按规则变化了,那等级也不会是model的属性。

(清版射击游戏中的子弹坐标,一定不是Model的属性)

当我们把每个数据结构里的model都列出来的时候,那么对应的数据表也就完成了。到此,游戏设计基本也就落地了,剩下的就是一些UI和玩法设计。

03、设计游戏数据结构的一些技巧

在设计游戏数据结构的时候,其实还有一些非常重要的技巧,这些技巧其实和一个设计师玩游戏有关,确切的说是一个游戏设计师作为“游戏批评师”的能力,这些技巧包括且不限于:


(宝可梦之父田尻智在制作宝可梦之前是一个“游戏批评师”,“游戏批评师”并不是无脑说游戏怎么不好,而是从专业的角度去分析游戏设计中的一些缺陷和错误,作为游戏分析师,首先得能精确地区分出游戏的类型)

01、同类游戏数据结构相似,不同类型则必然不同

精确地区分出游戏的类型,不同的游戏的数据结构必然不同,而类似的游戏的数据结构则基本相似。所谓的相似,是指数据结构的类大同小异,区别仅仅是部分属性。

举个例子,动作游戏和回合制游戏(包含ARPG等)。动作游戏和回合制游戏的区分对于玩家来说是困难的,因为即使是realtime的游戏,玩家都可能理解为是动作游戏。但是从一个专业的游戏设计师、游戏批评师的角度,你应该非常轻易地可以看出两者的差异,比如我们拿两者的“技能”举例。

在动作游戏中,没有技能这个概念,所有的都是“动作(action)”,而每一个动作由多个“帧(frame)”组成:


而回合制游戏,比如diablo、lol等,其技能特点都是以一个技能为一个回合的,只是时间长短不同,我们可以把它看做一个Timeline的数据结构,在Timeline上有若干种点:


很显然,从我们说的、写的文档中的、交流中常用的“技能”这个词出发,在不同的游戏类型中,就有完全不同的数据结构。所以首先设计师必须能分清游戏类型,如果对于游戏类型都不够清晰,那的确没法建立好数据结构。而对于游戏类型的掌握,只能说“多玩游戏,多多感受”了。

02、一事一议,破除关联

这不光是一个游戏设计技术,也是一个人的理解能力。要求设计师能够把一件事情分到最细,去除掉所有不存在的因果关系、去除掉话术、去除掉固有的预设前提,能把一个想法(一句话)还原出清楚成原原本本的事情数组。这是需要抽象能力的,比如“麦当劳的安格斯牛排汉堡好吃,所以套餐卖50不贵”——在这句话里有2件事情,并且没有关联,但是通常我们会错误的建立起因果关系,因为这句话里有个“所以”,实际上这2件事情很显然:

“我”觉得麦当劳的安格斯牛排汉堡好吃(前半句)

50块钱的套餐“我”觉得不贵(后半句)

好不好吃和定价本身就是完全没有任何逻辑关联的事情,但是我们却出于情感,会把他们关联起来,认为“好吃所以值这个价”,这在做数据结构设计的时候是万万使不得的。过多的不必要关联,会把逻辑带向一个无法厘清的尴尬境界。

总结

只有在深入设计游戏数据结构的时候,我们通常才能得出一些设计问题,以及发现许多设计的可扩展点、可维护点,通过利用这些点,我们可以进一步设计出更多合理的玩法。当我们完成设计数据结构之后,游戏的核心逻辑基本也就清晰了。


来源:千猴马的游戏设计之道
原文:https://mp.weixin.qq.com/s/mdHzpkrHBS777g1wK0NU5w
最新评论
暂无评论
参与评论