技术策划（Technical Designer）这个岗位真的是有必要的吗？
因为游戏策划本身就应该是掌握程序的，不然你如何提出需求？只有厚颜无耻的国游策会在那里说，啊呀，我们只要想好游戏好玩，提出需求就好了，可是你真的会提需求吗？我相信大多国游策和程序的交流中，都存在着善意的欺骗，所以你真要成为一个好的游戏策划，你就应该会程序。 如果你看不清下面文章的表或者图，可以点这里看我发的原贴：  在项目开发的时候，策划会发现一个有趣的问题——程序做的功能总是不那么顺应自己的设计。也由此推论出了一个道理——好的程序员是懂的策划心事的人——因为他们总是能做出看起来和策划案需求一样的东西。但是藏在这个问题背后的更深层次的问题是什么？是程序的阅读能力不行还是策划的表达能力不足呢？事实上问题的关键在于——策划写的文案总是在撒谎。并不是策划真心想玩弄谁，也不是因为策划想卖弄文笔而最终表达能力不足。唯一的原因就是——文字描述的内容是表面现象，而非真实的需求。下面我们就来举一段实际例子来深入分析这个合作中的问题，策划设计了一个玩法（此处我们不讨论以下举例中的玩法是否OK）：  这是一个横板的动作射击游戏，玩法类似于魂斗罗。游戏中玩家可以收集4种不同的武器，每一关开始前，玩家都可以选择携带2把武器进入关卡，一旦进入关卡，就只能在选择的2把武器之间做切换，而无法选择另2把武器。  武器  说明  弩箭  最基础的武器，直线发出弩箭，攻击路线上命中的第一个敌人，造成单体伤害。  投掷发射器  投出抛物线爆弹，爆弹落地后会弹跳2次，当2次弹跳结束后或者轨迹中碰到敌人时，爆弹就会爆炸，对范围内的所有敌人造成伤害。  自跟踪武器  可以发射出锁定目标的子弹，自动锁定最近的敌人，子弹一旦发出就会追踪目标而去，直到击中目标，对目标造成较大单体伤害。  火箭炮  重型武器，在较长的蓄力（表现为填装炮弹并瞄准）后，发射出火箭炮，火箭炮慢速向前飞行，击中路线上的第一个敌人或撞上障碍物后会爆炸，对大范围内的敌人造成伤害。  分析背后真正的需求  我们可以看到，这是一个标准的国游策写出来的策划案，最多可能产生的不同就是“文字戏法”，但是核心内容还是如此。在这里，我们就会简单的分析出游戏的玩法：  当我们列出这个脑图的时候，我们看似已经想的非常全面了，不仅仅是对已知的需求进行了分析，还追加了可能延伸出来的其他需求。这个时候我们就会直接开始动手进入下一环工作——设计游戏中的表格，我们的游戏中会有一个武器表，表结构为：  表项  作用  Id  武器的id，唯一的标识。  武器名称  武器名称。  icon  武器图标的资源文件。  说明  武器的说明文字。  子弹数量  武器可以装载的最大子弹数量，-1代表子弹无限。  子弹轨迹  子弹发射出去的轨迹：0=直线，1=抛物线，2=追踪。  子弹速度  子弹飞行的速度。  子弹参数  如果子弹是抛物线，那么这个参数就是顶点高度；如果子弹是追踪的，这个就是追踪规则（blabla大段）。  伤害范围  子弹打中敌人后的伤害范围，0代表单体目标，>0代表子弹的爆炸半径。  伤害系数  子弹的杀伤力，用于带到伤害计算公式中计算伤害。  聚气时间  开枪时候需要预先聚气的时间，此时角色会做出举枪聚气的动作，待聚气时间结束后，子弹发射出去，玩家在聚气时间是不能操作角色的。  冷却时间  子弹发射出去多久以后可以再次发射。  武器图形  武器拿在手里的图形。  子弹图形  子弹的图形。  命中图形  子弹击中目标时，播放的图形效果，主要用于爆炸。  发射音效  子弹发射出来的音效。  命中音效  子弹命中目标时候的音效。  被击表现  被子弹击中的敌人做出的动作名。  最大等级  武器的最大等级，强化武器最终不会超过这个等级。  价格  武器卖店的价格。  稀有程度  武器的稀有程度，0=白色，1=绿色，2=蓝色，3=紫色，4=橙色。  设计到这里，我们大概可以满意的说，武器系统已经设计完成了，程序实现了这个表以后，再把对应功能一做，游戏中的武器系统就算完成了。我们也看到了分析这个需求的人是具有一定经验的，能够分析出可能扩展的玩法非常棒！虽然在设计表的时候发生了Magic Number、二义性名词等错误，但是因为有了这样一个表，策划可以无限制的设计武器。  但是！上面的分析全都是错的！这正是一个典型的被策划善意欺骗的结果！  乍一看，我们要做的就是一个简单的武器系统，武器在战斗中可以发射出子弹，还能强化。但事实上，这个需求真正的概念是：   所以藏在这个设计背后的，并不是简简单单的一个武器数据就能满足的需求。我们深入思考几个问题：  1，  子弹是不是必须是武器发出的？会不会地图上某个点也发出类似的子弹？  2，  武器发出的子弹是不是一定的？我有没有可能改装某件武器之后，比如改装弩箭之后，他可以发出榴弹炮来了？  3，  子弹是不是可以改造？弩箭可不可以发射出火矢、毒矢、冰矢？亦或者连续发射出不同属性的箭矢？  4，  子弹的效果仅仅只是伤害吗？我能不能强化之后发生变化？比如跟踪弹在命中之后产生爆炸？或者更进一步跟踪弹击中目标之后散射出一排箭矢，其中一些是爆炸箭矢，命中敌人之后会产生爆炸伤害？  5，  ……  当我们进一步深入思考扩展性的时候，就会发现很多有趣的设定，这些设定不仅可以让游戏变得更丰富，也同时可以埋下收费点。但是如果我们按照之前的那种分析，是不是我们就做不成了？当然我相信也不是，因为总有笨办法的，比如你一种武器可以扩展出几十种子弹，就让策划填写几十条数据……但是这样的设计，并不是一个好的设计。  当我们通过进一步思考总结后，我们再来看上面这个脑图的分析，事实上，我们游戏中会有若干个数据结构：   在上面这个脑图里面，我们会看到好几处用到了Model和Object（或者结尾Obj的）。这其实是设计的一个陷阱——我们通常把静态数据（来自策划填写的）和动态数据混为一谈。在这里，Model就是静态的数据，称为“读表数据”，是策划从通过一些方式（如填表、编辑器）最后转化出来的数据，这些数据始终就是那样的，不会有任何变化。Object则是随着游戏进程变化会逐渐发生变化的数据。  Object和Model的关系是：Model是产生Object的依据，但是Model无法代表Object。   1，  武器Model（Weapon）：这是策划需要配置的武器数据。  数据  作用  Id  String，武器模板（或者说单种类武器）的唯一标示。  名称  String，武器名称。  说明  String，武器说明。  Icon  String，武器icon文件名。  造型  String，武器拿在手里的造型文件夹名。  冷却时间  Int，单位毫秒，武器使用的冷却时间。  Timeline  String，使用武器（即开火）之后，角色会做出的动作，在这个动作中，会负责决策发射出的子弹等信息。  OnFire  String，武器发射后的回调函数，在武器发射开始执行Timeline的同时，会给出一个回调函数，调用策划编写的逻辑脚本，程序提供的回调参数：  l  WeaponObj*：使用的武器。  l  Character*：使用武器的人。  2，  武器Object（WeaponObj）：这是在游戏运行中，结合当前情况产生出来的数据，玩家拥有的武器等都属于WeaponObj，而不是Weapon。  属性  说明  UniqueId  String，唯一id，在这个世界上，任何一把武器的实体都是唯一的，即使他们采用同一个模板生成。  Model  Weapon，非指针，而是克隆一个Model，即武器的模板信息，改装等玩法可能改变这个武器的信息。  子弹  Hash<String,
  AoE>和Hash<String, Bullet>，Timeline中的子弹发射点作为key，Value就是要发出的子弹对象的模板。  等级  Int，武器当前的等级，当然还会有很多关于武器强化的属性，这里就列几个说明一下问题。包括稀有程度（如果可以被提升），星级（如果可以被提升）都会放在这里，如果不能提升的，
  比如“AK47不论怎么升级都是蓝色的”，那“颜色”就应该放到Model中去。  价格  Int，当前武器的卖家，由于强化之后武器的价格会变高，因此他属于Obj而不是Model，Model中顶多可以有一个参数用于获取这个值。  3，  角色动作线（Timeline）：角色使用枪支、受到攻击等，在游戏中可以归纳一个角色任何的一个动作都是一个Timeline。Timeline有一个宿主，这个宿主就是播放这个Timeline的角色。Timeline上可能存在若干个事件点，每个点可能的事件包括：  事件类型  说明  播放动作  让Timeline的宿主开始做一个动作1次，需要的参数：  l  动作名：播放什么动作。  l  保持：保持某一段的动作（用于死亡动作等）。  场景特效  在场景中指定位置播放一个视觉特效，需要的参数：  l  特效名：播放什么特效。  l  位置：在什么位置播放。  l  循环：1次还是始终。  l  时间：如果不是1次的，那么播放多久结束。  角色特效  在宿主身上播放一个视觉特效，需要的参数。  l  特效名：播放什么特效。  l  绑点：角色身上某个播放这个特效的点。  l  循环：1次还是始终。  l  时间：如果不是1次的，那么播放多久结束。  产生AoE  产生出AoEObj，即非锁定类子弹，需要的参数：  l  AoEId：要产生的AoEObj，他的Model的Id。这个会由Timeline产生的时候决策，是从某个WeaponObj的子弹Hash表中获得，还是直接走读取的值，这是一个有点Magic的用法。  l  位置：AoEObj初始坐标。  l  生命周期：AoEObj存在时长。  l  Tween：AoEObj的运动轨迹和变化方式，不仅仅是坐标的变化，还可以设置如范围等的变化。  产生Bullet  产生出bulletObj，即锁定类子弹，需要的参数：  l  bulletId：创建这个bulletObj所用的Model的id，和AoEId一样有个Magic用法（我都不好意思再写了）。  l  位置：bulletObj产生时候的位置。  l  生命周期：bulletObj最多存在多久。  l  速度：这发bulletObj的速度。  l  目标：选择目标的规则，返回一个Character*。  开始僵直  从这个时间点开始，角色将进入僵直，所谓僵直就是不再接受操作指令，直到做其他动作（进入另一个Timeline了，比如受伤、死亡）。无参数。  结束僵直  从这个时间点开始，角色将离开僵直。无参数。  其他  以上只是一些举例，根据实际需要，可以在Timeline中加入更多的动作类型，比如给宿主添加buff等，这个看项目的需要决定，而针对本文中的上述范例，是没有这个需求的。  4，  非跟踪子弹Model（AoE）：非跟踪的子弹，实际上是通过AoE来实现的，包括子弹命中后产生的爆炸也属于AoE。我们可以在AoE的2个回调函数中写关于伤害等效果的逻辑。  属性  说明  Id  String，AoE模板的id，每一个模板都是唯一的。  视觉效果  String，位于AoEObj位置上要播放的特效，可以没有即不播放。  循环播放  Boolean，是否要反复循环的播放这个特效，如果没有特效这个就没意义。  范围  Int，AoE的初始有效范围，实际有效范围以AoEObj为准，由于这个游戏的设定，因此范围都是圆形的，这里的数据是圆形的半径。  碰角色结束  Enum，这个AoEObj在碰撞到任何角色时是否会结束？有几个风格：  1，  任何碰撞角色都不会结束。  2，  碰撞到Caster同阵营角色结束。  3，  碰撞到非Caster同阵营角色结束。  4，  碰撞任何角色结束。  碰障碍结束  Boolean，这个AoEObj在碰撞任何地形的时候是否会结束。  OnEnter  String，当有角色进入AoEObj的瞬间触发的回调，一个角色在一个AoEObj的生命周期内，只能触发一次这个回调。需要的参数：  l  AoEObj：AoEObj*，这个AoEObj。  l  EnterCharacter：Character*，进入AoE范围内的这个角色。  l  CharacterInRange：Array<Character*>，所有当前已经在AoEObj范围内的角色（不包含EnterCharacter）。  l  DesignParam：Array<Int>，策划配置的一些int值。  OnDestory  String，当AoE结束时执行的函数回调。需要的参数：  l  AoEObj：AoEObj*，这个AoEObj。  l  CharacterInRange：Array<Character*>，所有此时在AoEObj范围内的角色。  l  DesignParam：Array<Int>，策划配置的一些int值。  5，  非跟踪子弹Object（AoEObj）：在游戏中的实体，根据实际状况产生。  属性  说明  UniqueId  String，每一个存在于世界上的子弹都是独一无二的。  Model  AoE*，产生这个子弹的Model，由此获取基础信息。  释放者  Character*，这个子弹的创建者，可能是Null，比如来自地形。  位置  Position，当前这个时间点上，这个子弹的位置，对于一个射击游戏而言，大多子弹的位置应该是经常变动的。  范围  Int，子弹当前的逻辑有效范围。  生命周期  Int，还有多久AoEObj就将结束。  其他  根据需要还可以添加一些其他的信息，包括可以给策划一个Param来记录一些动态的数据等。  6，  跟踪子弹Model（Bullet）：会跟踪的子弹，这种子弹只会追击目标，直到命中、自身生命周期结束或者目标消失（或其他条件比如死亡）之后会结束。  属性  说明  Id  String，子弹模板的Id，唯一的标识。  造型  String，子弹的图形文件。  范围  Int，子弹的初始有效范围，同样是一个圆形，所以这代表了半径。  碰障碍结束  Boolean，子弹是否会因为碰撞任何障碍物而结束。  OnHit  String，当子弹命中了追踪的目标后执行的回调函数，如造成伤害，产生爆炸等逻辑都编写在这个脚本中。只有子弹真的命中了目标（或者碰障碍而结束的时候）才会进入这个回调，如果子弹提前消失就不会进入。需要的参数：  BulletObj：BulletObj*，这个子弹。  TargetCharacter：Character*，被击中的角色，可能是Null，因为存在碰撞障碍物结束的可能性。  DesignParams：Array<int>，策划填写的一些int值。  7，  跟踪子弹Object（BulletObj）：跟踪子弹在游戏中的实体。  属性  说明  UniqueId  String，世界上任何一个子弹都是唯一的。  Model  Bullet*，创建这个Obj的模板。  释放者  Character*，释放这个子弹的目标，可能是Null，比如GM指令创建的。  追踪目标  Character*，子弹追踪的目标，如果这个目标不存在了，子弹也就结束了  位置  Position，子弹在这一时刻所在的世界位置。  范围  Int，子弹当前的有效范围。  生命周期  Int，子弹剩余的时间，如果生命周期小于等于0，那么子弹也就销毁了，不会进入OnHit回调。   到这里，所有跟武器有关的基础玩法的数据都有了。可能会有一个疑惑——那么假如我游戏中有合成武器的玩法，上述数据结构中并没有任何相关的信息，是不是漏了什么？或者说我应该把它们加在哪儿？合成这个例子是典型的、常见的。  我们可能会有这样一个玩法需求——可以用多种材料或者武器合成一个武器。  我们直接跳过被欺骗的抽象，来看背后真实的需求：  在我们游戏中可能会同时存在这两种合成方式，虽然可能早期策划并不这么考虑，但是随着项目的推进，运营的介入，这些都可能会成为最终的需求。因此我们要考虑到的是一个武器，可能会有多个“配方”来合成他。所以，我们需要设计的结构是：  数据  说明  id  String，每一个配方需要独特的标识。  产出结果  Weapon，最后产出的结果的Model（但是产出的还是WeaponObj，是由这个Model为依据产生的）。  材料  Array<ItemObj>，需要的材料的。  资源  Array<Resource>，需要的资源，如金币，石油等等。  数据中，只有id是唯一的，意味着产出结果可以相同，因此“同一个武器会有多种合成方式”的需求是满足的。但是这里会延伸出一个问题——为什么这个要独立建一个数据？而不把它放在Weapon下，既然我是产生这个Weapon，那么当然产生他的“零件”，也属于他了。这里有一个非常核心的原因——逻辑依赖性！   我们在研发游戏的过程中，往往忽略了很多软件工程最重要的一步——建立单向依赖图（DAG，也称单环依赖图）。这往往是因为游戏策划或者程序员脑子里都是浆糊，所以在设计数据和结构的时候，并没有一个依赖关系的思路，最终导致做出来的东西是高耦合性的，至于高耦合有多危险，这个我就不多说了。  在上面这个问题中，我之所以把合成玩法的数据提出来，就是因为依赖性问题——在这个游戏中，武器合成玩法依赖于武器，而（或者说也应该“因此”）武器不依赖于武器合成。你可以简单的理解为：游戏在没有武器合成玩法的情况下并不影响有武器，但是游戏在没有游戏的情况下就不可能有武器合成玩法，因此武器合成玩法依赖于武器，而武器不依赖于武器合成玩法，假如我们强行让武器也依赖于武器合成玩法，他们互相依赖，就形成了一个耦合，这是在设计任何玩法的时候都必须避免的。  因此当我们设计玩法的时候，需要设计一个单向依赖图（而不要去想设计一个毫无合理性可言的UML，至于具体喷UML的，这就不该是本篇的内容了），在这个游戏系统中，单向依赖关系是：  每个玩法和元素之间的关系，都是单向依赖的，不会因为删除或者改动上层的内容，而使得下层的内容需要作出调整，比如改变了武器合成玩法的规则，产生一些武器合成的数据要改动，我们只需要改动武器合成的数据，而不需要（也本不应该需要）改动武器数据结构。   说到这里，你可能觉得这样的工作有一个问题——策划有太多的数据表要处理。事实上，这是另外一个问题，我是这么来归纳从策划填写的数据到游戏中的数据的：  在上图中有3个元素：  1，  Input：即策划填写的数据表。  2，  Data：即本文上述分析的一些数据结构。  3，  Logic：即游戏中使用的逻辑数据结构，其中一些恰好“长得和Data中对应部分一样”。  我们通常的工作中，会直接把策划填写的数据当做最后逻辑用的数据来处理，因此建立了一张几乎无法维护的数据表，当然这个无法维护不仅仅是因为直接使用，还因为上述分析的各种问题。这样的工作中，我们漏掉了最终要的一环——Data的分析，也正是本篇上述范例中最大笔墨所做的内容。  为什么我们需要一个这样的Data？因为Data才是被研发真正依赖的东西：  我们可以看到，在这个依赖关系下，
我们只需要在系统设计的时候定出这个Data，就可以分头开始工作了，程序根据这个Data来coding逻辑，而策划设计一种input方式来产出这个data，程序本不应该坐等策划先配完表（至少配一个测试数据）才能工作，程序的工作并不应该依赖于策划的工作。  而作为策划，我们最关注的还是input的环节，我们看到上面的实例中，有这么多要我们去填写的数据，我们真的需要配这么多表吗？回答可以是no，回到最初我们的需求，那个带有欺骗性的需求，我们可以用最好的语法来填写这个表，这个表只有一列：  当策划填写好这些数据后，通过工具转换，就能产生出我们上述需要的Data：  至于如何将这一句话提炼分析成4个文件，这是文件转化工具的工作，当然你别找我要这样的工具，因为我们尝试了5年让人可以用自然语言编程（至少是填表），但都没成功。也不要吐槽为什么图标是Delphi，这毫无意义……当然，这么做的最重要的因素，不仅仅是整理了研发的思路，更重要的是做到了工作内容的解耦。   有些需求，只有提出的人才会知道真正想要的是什么，所以首先提出的人想要。最简单的例子是，策划的需求是一个苹果，但是提出的是“圆圆的、酸酸的水果”，那么橘子是不是也没问题呢？所以无论你怎么沟通，都不如你真的抽象好结构和实现方式来的OK，如何做到这样，很简单——你得会用程序员的逻辑方式来思考问题。