请选择 进入手机版 | 继续访问电脑版

GA地精研究院

 找回密码
 立即注册
查看: 1611|回复: 17

[教程] War3纯T进阶方法与技巧

[复制链接]
发表于 2016-9-7 20:09:11 | 显示全部楼层 |阅读模式
本帖最后由 边境拾遗2.0 于 2016-9-7 20:27 编辑

前言:由于这个教程在百度贴吧发不出来,于是我就决定将其发布在GA和WOW9,希望会有人需要它。

这个教程是基于我之前所写的《零基础魔兽地图制作教程系列》(贴吧文字部分:http://tieba.baidu.com/p/4235859387?pn=1,B站视频部分:http://space.bilibili.com/1970861/#!/index)的进阶教程,所以如果你没有看过我之前的教程的话,这个教程的意义就不大了。

由于我不是很明白论坛的排版,所以……稍微有些难看。故放出WORD文件的下载:

War3纯T进阶方法与技巧-1.rar (435.54 KB, 下载次数: 103)
 楼主| 发表于 2016-9-7 20:11:31 | 显示全部楼层
序章
       如果各位对我还有印象的话,一定会记得我在半年前发布过一套完整的War3基础教程。半年过去了,这篇教程对于原本看着它的人来说,已经开始不够了。所以说我决定抽出几日时间来写一篇新的,更为进阶的教程,详细介绍纯T编程的各种方法和技巧。当然,和之前一样,这篇教程也只是写给少数几个人看的,只是本着互联网精神,给几个人看也是看,给所有人看也是看,为什么不发出来呢?所以这一篇教程,如果不能让所有人都明白的话,还请原谅或者私下问我。

       当然,因为这篇文章介绍的都是纯T进阶的内容,所以对于还不熟悉纯T(认不全动作,不会用哈希表之类的)的人,以及使用Jass的人来讲,并没有什么帮助。但是如果你已经基本上掌握了触发器的各种功能,但是却苦于庞大的地图系统和脚本不知道该从何处下手,又或者是不知道该如何合理的组织触发器和动作,那么就希望这篇文章能带给你一些启示和帮助了。

       本人才疏学浅,如有疏漏之处,请包容并指正。

回复 支持 反对

使用道具 举报

 楼主| 发表于 2016-9-7 20:11:50 | 显示全部楼层
第一章:触发器
       先让我们回到一切的开始,来看看war3的触发器吧。触发器可以算得上是War3纯T编程的一个基本单位,比如说群体风暴之锤,只需要一个触发器就可以实现这一项功能——任意单位发动效果,如果释放的是群体风暴之锤,就选取施法者面前一定范围内的单位,每个单位创建一个相应的马甲对其释放风暴之锤(记得要把马甲删掉)。

       从这一个简单的例子里我们就可以窥见war3编程的一种思维模式——捕捉原本游戏中的某个事件,添加我们自 己的效果将其修改成我们自己的游戏所需要的效果。因为War3已经给我们提供了一个完整的RTS游戏了,所以我们并不需要纠结怎么做到单位选取,怎么做到创建单位之类的事情,我们要做的仅仅是通过一些触发器,把一个RTS游戏改成DOTA类的游戏或者是任何其他类型的游戏而已。就这个方面来讲,触发器确实是非常合理,非常适合的。

       姑且,我们将这种设计模式称为“触发器设计模式”吧。它所要解决的问题是作为RTS游戏的War3对某一件事情的处理不 符合我的要求,想要对其处理方式进行修改。而解决问题的方法则是创建一个触发器,设置事件以捕捉所想要处理的事件,设置条件来过滤掉那些不想要处理的情形,设置动作来添加我们自己想要的针对事件的处理。

       那么这种设计模式产生的效果,相信大部分人都已经烂熟于心了。其优点非常显而易见,那就是假如你的游戏相对war3改动不大的话,你的绝大部分需求都可以通过设置一个与之对应的触发器来解决。但是正是因为其简单粗暴的特性,很多缺点也产生了:
        1. 无法指定触发器的处理顺序:当你的游戏里有两个或两个以上的触发器,它们处理的都是同一件事情,你该如何区分它们的顺序呢?比如说你的一个单位受到两个触发器的作用,这两个触发器的作用都是捕捉这个单位受到的伤害,触发器A的作用是设置单位的生命值+10,触发器B的作用是设置单位的生命值-10。假设这个单位目前还剩下10点生命值,受到了5点伤害,那么假如触发器A先执行动作的话,单位的生命值就会变成20,然后再被触发器B减去10点,受到5点伤害仍然不会死亡。但是B先执行动作的化,结局就完全不同了,单位在生命值-10之后就会直接死亡,在这之后的触发器A和伤害都会徒劳的作用于这个早已死去的单位。这只是一个很简单的例子,很多时候这样的冲突并不止两个触发器,而是会更多。在这种情况下用单一触发器来解决单一的问题,而罔顾它们之间的相互作用的做法会变得非常致命并且容易发生BUG。
       2. 依赖于BLZ提供的API:你没有办法编辑事件(当然实际上是有的),能够添加在触发器的事件项目中的只有BLZ开放给你的事件。所以当你想要捕捉某一件BLZ没有给你提供的事件的时候,就很麻烦了。这个问题会在第二章详细的讨论。
       3. 单个触发器动作过于臃肿:当你只是做做群体风暴之锤之类的小技能的时候,这个问题还不明显。但是当你做了很多触发器,或者做了一个特别大的触发器的时候,就开始觉得麻烦了。因为一个触发器有一个事件,所以相应的针对这个事件的所有处理都必须写在动作中。万一这件事情特别复杂,事情就大发了——一个小小的问题产生,都会要你去检查一长串的动作,去找其中的错误。再比如你可能在游戏里做了很多触发器动作,但是它们都具有某种共通性,于是你就会觉得自己的时间就被浪费在了做这些重复而毫无意义的劳动上了。
       4. 动态注册是一件很麻烦的事情:很多时候,一个效果并不是永久性的,并且也不会只作用到特定的对象身上。比如给某个单位上一个Buff,这个单位在对敌人造成伤害的时候就会让敌人短暂的眩晕这种效果,就需要用到动态注册事件或者触发器这种技巧。这种技巧如果是用Jass来实现的话相对还比较轻松,用触发实现则是一件非常痛苦的事情——因为你既不能删除为触发器添加的多余事件,也不能删除触发器本身,在不采用其他设计方案的前提条件下,就必须借助更为复杂的机制来识别那些注册了事件但是效果早已到期的单位,以及触发器还有可能被添加了重复的事件的问题。当然YDWE很大程度上已经解决了这个问题(详见:tieba.baidu.com/p/4235859387?pn=2,56楼)。

第二,第三,第四个缺点并不是非常的明显,但是第一个缺点是一旦碰上了就会真正让人头痛的了的。所以为了克服这个缺点,很多人都采取了大致相同的做法。这个做法就留到下一个章节再讲了。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2016-9-7 20:16:26 | 显示全部楼层
第二章:监听与处理
   上回说到了大部分情况下触发器都可以很完美的工作,但是仍然有很多缺点。其中最明显的一个缺点就是,多个相同事件的触发器无法指定谁先执行,谁后执行,导致当触发器的效果发生相互冲突的时候,无法解决这种冲突。
   实 际上这个问题并不难解决,很多有经验的WEer在使用触发器的时候都察觉到了这个问题,并且大多都使用了同样的方式来解决这个问题——我们姑且将这种设计模式称为监听-处理模式。这种模式的思路是:既然多个拥有相同事件的触发器无法按照用户所期望的顺序调用触发器,那么不妨只让一个触发器来为多个需要捕捉相同事件的触发器捕捉事件,再由这个触发器来按照用户所需的顺序调用触发器,问题就被完美解决了。为了方便,我们将这个用来捕捉事件,本身不对事件进行处理,只负责按照顺序调用其他触发器来处理事件的触发器称为监听器(要不然不管是叫做事件触发器还是什么鬼都太别扭并且太长了)。
   这个设计模式相对于原本的依赖于单个触发器的设计来说,可以说是有了飞跃性的进步:
       1.      
首先,无法指定触发器被触发顺序的问题被完美解决了了
       2.      
具有超越触发器的可扩展性:之前说过,触发器很难去捕捉那些BLZ没有提供相应事件项目的事件,同样的也不会支持你去自定义一些事件。监听-处理模式就能很好的处理这个问题,因为它的事件捕捉依赖于监听器而不是BLZ提供给你的事件。我们可以假设你制作了一个弹幕系统,每当弹幕撞上敌人的时候就会对敌人造成伤害。那么假如在你的游戏设计中,有关于削减弹幕伤害或者反弹弹幕的设定,怎么办呢?很简单,你可以先创建一个没有任何事件的监听器,然后在弹幕撞上敌人,造成伤害之前调用这个监听器,这样这个监听器每次都会在弹幕撞上敌人,即将造成伤害的时候被触发,即拥有了任意弹幕即将造成伤害事件了。这个时候,再想要削减弹幕对敌人的伤害,或者是干脆让弹幕转个头,改变所属玩家,都不会是什么难事。(弹幕反射的演示地图放群里了,贴吧的请到云盘下载:1i5IxPRZ。)
   可扩展性,对于编程而言是非常重要的。就如上面所言,你可以想象一下,当你可以自定义游戏中的事件,加入你自己的事件的时候,游戏的可能性会增加多少呢?多少之前觉得头痛的问题会变得简单呢?而这一切根本就不需要什么高深的技巧,也不需要学习Jass,仅仅只需要使用这种监听处理的模式来设计你的触发器就行了。

  但是同样的,监听处理模式也只是解决了触发器模式的一些缺点而已。不仅如此,监听处理模式还有它自己的缺点:
1.      参数传递问题:不将用于处理事件的动作和相应的事件放在一起虽然有好处,但是也带来了另外一个问题——这样的话,处理事件的动作要怎么获得来自事件的信息?实际上答案并不复杂,设置几个公用的变量就可以达到参数传递的效果了,但是这让我们多了一个要担心的问题,并且远不止如此。
2.      事件嵌套触发问题:第二个问题实际上是第一个问题的衍生。你有没有想过,假如在一个事件被触发的过程中,这个事件又被触发了,会怎么样?实际上这样的问题很多人都遇到过,就是在受到伤害事件中又调用了造成伤害函数,于是导致死循环。与这个类似,监听器也会有相同的问题。并且更多的是,如果你选择用公共变量来传递事件参数,会有一个事件参数被覆盖的问题。举个例子,假如我有一个监听器,实现了任意单位被伤害的事件,这个时候发生了一次伤害事件造成了5点伤害,将要运行A和B两个触发器。其中,触发器A又会对某个单位造成10点伤害,这个时候监听器又被运行了,公共变量中存储的伤害值就被从5变成了10。这下就糟糕了,虽然这一次10点伤害的事件被完全处理了,但是不要忘了我们还需要回到5点伤害的那次事件,但是这次事件的信息——被保存在变量里的伤害值5 就被修改为了10,并且由于这5点的伤害值并没有被保存,这一次5点伤害的事件就永远的失去了它正确的信息了。解决这个问题的方法也不是没有,但是这个答案就比第一个问题的答案要复杂多了——我们可以把事件信息保存在栈里面,并且在每一次触发事件之后,都先将事件信息保存在栈中,在事件结束之后再取出来,这样就可以避免信息丢失了。
3.      动态注册变得更加麻烦:因为监听处理模式对事件的捕捉依赖于监听器而不是BLZ提供的事件,所以如果你依然希望在动态注册问题中使用监听处理模式,就必须自己再实现一个动态注册功能。好在这件事情并不是很难,只要给每个监听器都设置一个触发器数组,然后让动态注册的触发器将自身都加入到这个数组中,再让监听器在每次被触发的时候都额外调用数组中的所有触发器就行了。
4.      还是没有解决触发器动作臃肿问题:但是监听处理模式中,将触发器的功能分化的趋势已经开始显现了。“分而治之”是一种非常重要的思维,我们将会在第三章解决这个问题。

  总的来讲,虽然说监听处理模式也有它的缺点,但是相比起单纯的一个触发器来说,很多好处都已经开始体现出来了。这大概也是很多人所止步的地方。下一个章节我们会更为实际的去讨论结构化程序设计的问题。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2016-9-7 20:17:06 | 显示全部楼层
第三章:结构化程序设计
   看到这个标题你可能觉得有点眼熟,因为结构化程序设计在之前的教程里就已经提过了。但是我觉得这种设计思想还是需要再讲一遍。
   先提一个简单的 问题——你是否觉得自己在做触发的时候,效率低下,BUG丛生却又很难找到在哪里,进度与计划严重不符?当然,介于我们是在做War3的触发器编程,所以还要加上一条你是不是觉得一些复杂的效果自己根本无从下手(尽管你可能知道实现这个效果需要用到什么功能,但是却想不出该如何把它们组合起来)?如果你回答是的话,那么恭喜你不是一个人。上个世纪60年代这种事情经常发生,被称为软件危机,甚至还有(IBM事例)。这种现象困扰了人们很久,也激发了许多人的智慧。为了解决这个危机,产生了两个解决方案,其中之一就是结构化程序设计(另一个方案是软件工程),也就是这一章我们要谈论的内容。
   把视野转回WE,你会发现同样的事情也发生在WE中。WE也不乏许多有经验的人与大家分享解决这些问题的经验,尽管每次不同的人谈论的经验都不一样,但是在有一些方向上是惊人的一致的,那就是有很多人都赞同做地图应该先做系统这个看法。很多人都认为先做系统可以让许多游戏效果变得易于实现,可以节省时间,并且避免你在游戏制作的后期面对越来越多麻烦的问题。
   这 是一个正确的方向,先做系统的思想在某些方面与结构化程序设计不谋而合,比如系统符合结构化程序设计中模块化的思想,而先做系统则是着眼问题的整体,而不是技能效果这样的小零件上(很多人的坏习惯就是,做地图往往先做了很多角色或者是技能,最后却没能做出一个完整的游戏),符合自顶向下的指导思想。但是相比起结构化程序设计来说,先做系统显然还是不够深入,这也是WE编程十分缺乏的地方——WE的触发器与主流编程相差甚远,而很多人在接触WE之前并没有接触过编程,对于这些在编程中早已产生的先进思想自然是无从了解了。

   关于结构化程序设计更为详细的介绍,可以自行使用搜索引擎进行查找。概括的讲,结构化程序设计的核心思想可以用自顶向下,逐步细化模块化来概括。所谓自顶向下,逐步细化指的就是先从问题的整体着手,先构造整体的结构,然后再慢慢的细分,直到再也不能或者不需要细分为止——这可以避免你一开始就陷入诸如技能怎么做这样复杂的细节里,让原本复杂的事情变得更简单。模块化指的则是将程序视为不同功能模块的结合体,尽可能的减少模块之间的相互联系,增强它们的独立性,这样它们就可以像零件那样被用来构造更为复杂的东西,而相互独立又使它们更加的可靠。遵循这些指导思想,可以让你的程序变得更加结构清晰,富有条理,具有更高的可读性
   当然 这么说是非常抽象的,尤其是对于之前没有接触过编程的人来讲,要把这些抽象的思维关联到WE的触发器上面是非常困难的(而且确实,在有些时候你不能简单的将这些原则套用到WE的纯T上)。所以如果你还是没明白结构化程序设计对于WET编程究竟有什么用的话,不妨就让我们回到之前那个最朴素的结论好了 ——“先做系统
   这 里用弹幕系统举个例子。我一直觉得弹幕系统是最适合用于考验新手的试题,也可以很好的区分新手和已经入门者,因为弹幕系统高度的综合了War3纯T编程中 的各种要素。比如弹幕的飞行需要用到计时器,弹幕的碰撞需要用到选取单位,多个弹幕共同运作则不可避免的需要用到数组或是哈希表这样的数据结构,更进一步还可以考验系统的可维护性和可扩展性。先不考虑其他更为复杂的因素,我们先假设只需要做一个能设置飞行速度,射程和伤害值的弹幕系统就行了(更高级的弹幕 系统会在以后介绍给大家,实际上这个问题用面向对象可以更好的解决)。下面将向大家展示该如何设计和构造一个弹幕系统。
   首先来想想弹幕究竟是什么样的东西。弹幕是一种有模型,看得见的东西,它可以由一定的速度飞行,在碰到单位之后,自己会消亡,而被碰到的单位则会受到伤害。放在WE的触发器里,弹幕大概是这样一种东西:
1.      弹幕有模型,看得见:可以用单位作为弹幕。
2.      弹幕可以飞行:单位可以设置坐标,只需要用计时器周期性的设置弹幕的坐标,就可以使其飞行
3.      弹幕会碰上单位:可以周期性的选取弹幕周围的单位,假如选取单位的数量大于0,也就意味着有单位在弹幕的碰撞范围内,即弹幕与这个单位发生了碰撞。
4.      弹幕会造成伤害,自己会消失:伤害可以简单的使用伤害函数来实现,弹幕自己消失则是杀死单位
5.      会有一堆弹幕同时存在:可以把同时存在于场上的多个弹幕装进数组里面,这样只要把数组里的所有弹幕都周期性的过一遍飞行,碰撞检测,就可以让多个弹幕都能飞行和撞上敌人了。
6.      每个弹幕的速度,射程,伤害不同:速度,射程,伤害都是可以变动的值,在WE中给某个游戏物体绑定属于它自己的可变动的值的方法有很多种,自定义值,哈希表,数组,YDWE的逆天功能都能做到这一点。由于之前我们用到了数组,因此弹幕的不同属性我们也可以用数组装起来。
分析一下这个系统工作的流程的话,大概就是这样的:
1.      计时器周期性的运行,执行步骤2
2.      循环装有弹幕的数组,针对每个弹幕执行步骤3
3.      这是对单个弹幕所做的动作。首先设置这个弹幕的坐标,使其飞行,并将其移动的距离加入弹幕的计程属性中,如果弹幕累计跑过的路程超过了射程,也将其销毁,否则继续动作。其次,选取弹幕周围的单位,并判断单位的数量。如果单位的数量不为0,则执行步骤4,否则什么也不做。
4.      弹幕碰到了单位,则对敌人造成伤害,并销毁当前的弹幕。
这个过程看上去并不复杂,总共不过4个步骤而已。它看上去大概是这样的:
SysBltMainLoop
事件
时间 - 每当游戏逝去 0.01 秒
条件
动作
        -------- 步骤1:计时器周期性的运行 --------
循环动作[GobInt3]从 1 到 SysBltIntCount, 运行 (Loop - 动作)
            Loop - 动作
               -------- 步骤2:循环装有弹幕的数组 --------
设置 GobUnitActor = SysBltUnitArrayModel[GobInt3]
               -------- 步骤3-a:设置单位的坐标以实现弹幕飞行,并累加其已经飞行的路程 --------
单位 - 设置 GobUnitActor 的X坐标为 ((GobUnitActor 所在X轴坐标) + (SysBltRealArraySpeed[GobInt3] x((Cos((GobUnitActor 的面向角度))) x 0.01)))
单位 - 设置 GobUnitActor 的Y坐标为 ((GobUnitActor 所在Y轴坐标) + (SysBltRealArraySpeed[GobInt3] x((Sin((GobUnitActor 的面向角度))) x 0.01)))
设置 SysBltRealArrayCount[GobInt3] =(SysBltRealArrayCount[GobInt3] + (SysBltRealArraySpeed[GobInt3] x 0.01))
               -------- 步骤3-b:如果单位已经飞出了射程,就杀死单位并回收被它占用的数组空间 --------
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                   If - 条件
                       SysBltRealArrayCount[GobInt3] 大于或等于 SysBltRealArrayRange[GobInt3]
                   Then - 动作
                       -------- 步骤3-b-i:将数组顶端的弹幕数据移动到被空出的这个位置 --------
单位 - 杀死 GobUnitActor
设置 SysBltUnitArrayModel[GobInt3] =SysBltUnitArrayModel[SysBltIntCount]
设置 SysBltRealArraySpeed[GobInt3] =SysBltRealArraySpeed[SysBltIntCount]
设置 SysBltRealArrayRange[GobInt3] =SysBltRealArrayRange[SysBltIntCount]
设置 SysBltRealArrayCount[GobInt3] =SysBltRealArrayCount[SysBltIntCount]
                       -------- 步骤3-b-ii:将数组顶端的弹幕数据清空,否则就相当于同一个弹幕占用了两个位置 --------
设置 SysBltUnitArrayModel[SysBltIntCount] = 没有单位
设置 SysBltRealArraySpeed[SysBltIntCount] = 0.00
设置 SysBltRealArrayRange[SysBltIntCount] = 0.00
设置 SysBltRealArrayCount[SysBltIntCount] = 0.00
                       -------- 步骤3-b-iii:将弹幕的总数量减一 --------
设置 SysBltIntCount = (SysBltIntCount - 1)
                   Else - 动作
                       -------- 步骤3-c:选取弹幕周围的单位,并判断单位的数量 --------
设置 GobPointX = (GobUnitActor 的位置)
设置 GobUnitGroupMain = (半径为 64.00 圆心为 GobPointX 且满足 ((((匹配单位) 是存活的) 等于 TRUE) 且 ((((匹配单位) 是 (GobUnitActor 的所有者) 的敌对单位) 等于 TRUE) 且 (TRUE 等于 TRUE))) 的所有单位)
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                            If - 条件
                                (GobUnitGroupMain中的单位数量) 大于 0
                            Then - 动作
                                -------- 步骤3-d:如果单位的数量不为0,则执行步骤4 --------
设置 GobUnitTarget = (GobUnitGroupMain 中第一个单位)
                                -------- 步骤4-a:对敌人造成伤害 --------
单位 - 命令 GobUnitActor 对 GobUnitTarget 造成 SysBltRealArrayDamage[GobInt3] 点伤害(不是攻击伤害, 不是远程攻击) 攻击类型: 法术伤害类型: 火焰武器类型: 无
                                -------- 步骤4-b-i:将数组顶端的弹幕数据移动到被空出的这个位置 --------
单位 - 杀死 GobUnitActor
设置 SysBltUnitArrayModel[GobInt3] = SysBltUnitArrayModel[SysBltIntCount]
设置 SysBltRealArraySpeed[GobInt3] =SysBltRealArraySpeed[SysBltIntCount]
设置 SysBltRealArrayRange[GobInt3] =SysBltRealArrayRange[SysBltIntCount]
设置 SysBltRealArrayCount[GobInt3] =SysBltRealArrayCount[SysBltIntCount]
                                -------- 步骤4-b-ii:将数组顶端的弹幕数据清空,否则就相当于同一个弹幕占用了两个位置 --------
设置 SysBltUnitArrayModel[SysBltIntCount] = 没有单位
设置 SysBltRealArraySpeed[SysBltIntCount] = 0.00
设置 SysBltRealArrayRange[SysBltIntCount] = 0.00
设置 SysBltRealArrayCount[SysBltIntCount] = 0.00
                                -------- 步骤4-b-iii:将弹幕的总数量减一 --------
设置 SysBltIntCount = (SysBltIntCount - 1)
                            Else - 动作
单位组 - 删除 GobUnitGroupMain
点 - 清除 GobPointX

   这里有一些小细节可以补充,比如这种通过数组索引将马甲单位,速度,射程之类的属性关联起来的方式,在之前的教程中是提到过的。再者这个触发器使用了事件,而不是像第二章里面一样创建了一个监听器,然后由这个监听器来调用这个触发器。监听处理模式并不是任何时候都适用的,在处理这个弹幕系统的情况下,事件和动作密不可分,是一个实现功能的整体,并且我们目前也不关心游戏中其他与时间事件关联的系统和弹幕系统的先后次序关系,所以并不需要用到监听处理模式。
   但是从这里我们已经可以看出来一些问题了:
1.      步骤3-b和步骤4-b是重复的
2.      我们要怎么创建一个弹幕?
   当然问题1还不是非常明显。可是当我们会需要从弹幕系统以外的地方来删除弹幕的话,情况就会变得稍微复杂了,我们会需要复制12行动作来实现这么一个效果,并且如果一个触发器中要多次用到销毁弹幕这个功能的话,情况还会变得更复杂。
   问题2和问题1大致相同,因为我们在创建弹幕的时候也要经过如下几个步骤:
1.      将弹幕的总量计数加一,空出来的数组顶端的空间就是新的弹幕的数据存储空间
2.      为数组赋值
也就是:
设置 SysBltIntCount = (SysBltIntCount + 1)
设置 SysBltUnitArrayModel[SysBltIntCount] = (新建玩家1(红色) 的【大魔法师】冰球在((1575.00 + (随机实数,最小值: -100.00 最大值: 100.00)),(-1725.00 + (随机实数,最小值: -100.00 最大值: 100.00))),面向角度:90.00 度)
设置 SysBltRealArraySpeed[SysBltIntCount] = 500.00
设置 SysBltRealArrayRange[SysBltIntCount] = 1000.00
设置 SysBltRealArrayCount[SysBltIntCount] = 0.00
设置 SysBltRealArrayDamage[SysBltIntCount] = 100.00
   你可能会觉得哦,这没什么,只是复制粘贴的问题而已,然后就不管它了。然而在你复制粘贴了几处之后,想要给弹幕添加个碰撞大小之类的属性的时候,灾难就开始降临了!——你在创建和销毁弹幕的时候,都需要额外设置这一项弹幕的新属性,如果你复制了10次,你就要新添加10个动作,更别提这些动作还很有可能分布在你地图里的天涯海角之中!如果你复制了50次,那你很有可能都记不清自己在哪里使用了这些功能了,即使是物体管理器也很难帮你找到它们。
   这 就是在第二章提到的问题——我们不能只是简单的让所有动作挤在一个触发器里,这样会让触发器变得臃肿,一旦发生问题在这样又臭又长的触发器里寻找BUG一件很让人痛苦的事情;而且就像上面看到的一样,很多可以形成独立功能的重复动作如果只是采用复制粘贴来处理的话,不仅浪费时间,还容易出错;更要命的是,这些功能作为一个整体,如果不提取出来,修改起来会是一件非常痛苦甚至不可能的事情。
   这个问题不难解决,早在讲触发器的基础的时候我就提到过函数触发器的用法,如果你现在不记得或者不知道该怎样将一组具有特定功能的动作放在一个触发器里,通过调用触发器来像调用函数一样调用这组动作的话,可以查看群文件(贴吧可以搜索我的教程贴)。这样的话我们就会拥有三个触发器:一个触发器拥有时间事件,负责实现弹幕的行为;一个触发器能够被调用来创建弹幕;一个触发器能够被调用来销毁弹幕。而且之前要12个动作才能完成的销毁弹幕的动作,现在只要2个动作就能完成了(设置参数变量为要销毁的弹幕索引,运行销毁弹幕的触发器)。
   现在我们可以进一步改进和扩展这个系统了。按照自顶向下,逐步细化的设计原则,我们可以把弹幕的飞行,碰撞,以及伤害(即撞击产生的效果)都提取出来,分别做成三个独立的函数触发器。就像创建弹幕和销毁弹幕的触发器一样,这三个触发器也拥有独立的功能,只不过它们不大可能被本系统以外的触发器调用,基本只被用于实现弹幕行为的触发器调用而已:
SysBltMainLoop
    事件
        时间 - 每当游戏逝去 0.01 秒
    条件
    动作
        触发器 - 运行 GobStackPushReg <预设> (检查条件)
        触发器 - 运行 GobStackPushUnit <预设> (检查条件)
        -------- 步骤1:计时器周期性的运行 --------
        循环动作[GobInt3]从 1 到 SysBltIntCount, 运行 (Loop - 动作)
            Loop- 动作
               -------- 步骤2:循环装有弹幕的数组 --------
                设置 GobUnitActor =SysBltUnitArrayModel[GobInt3]
                设置 GobInt2 = GobInt3
               -------- 步骤3-a:设置单位的坐标以实现弹幕飞行,并累加其已经飞行的路程 --------
                触发器 - 运行 SysBltFly <预设> (检查条件)
               -------- 步骤3-c:选取弹幕周围的单位,并判断单位的数量 --------
                设置 GobUnitTarget = 没有单位
                触发器 - 运行 SysBltCollide <预设> (检查条件)
               -------- 步骤3-d:如果单位的数量不为0,则执行步骤4 --------
                如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                   If - 条件
                       GobUnitTarget 不等于 没有单位
                   Then - 动作
                       -------- 步骤4-a:对敌人造成伤害 --------
                       触发器 - 运行 SysBltEffect <预设> (检查条件)
                       触发器 - 运行 SysBltDestroy <预设> (检查条件)
                   Else - 动作
        触发器 - 运行 GobStackPopUnit <预设> (检查条件)
        触发器 - 运行 GobStackPopReg <预设> (检查条件)
   这样做触发器就会变得足够简短,更重要的是它会变得更具有可读性,你可以轻松的通过阅读调用的触发器的名字来了解这里究竟做了什么,而不是要对着好几个动作猜想它们之间的关联,推导它们的作用。当然,因为目前的这个系统还太小,所以并不能很好的体现出自顶向下,逐步细化的设计思想。比如弹幕的效果就只有一个伤害目标的动作而已,无法再往下细分了。
   我们可以想一想,假如我们希望把这个弹幕系统做得更复杂一些,会变成什么样?比如说弹幕的飞行不仅有匀速直线一种,还有加速运动或者是曲线运动;碰撞也不只有撞到单位就死,而是分一次性的弹幕和贯穿型的弹幕,又或者是在撞击了一定量的单位之后才会消失的弹幕;而效果更是可以五花八门,光是伤害就有各种物理伤害和法术伤害的区别,更别提技能效果之类的了。又或者我们还可以让这个弹幕系统开放更多的函数触发器,让我们可以修改弹幕在击中敌人的时候的伤害,配合第二章提到的自定义事件,不就可以很轻松的做出来属性伤害的弹幕了么?再把脑洞开大一点,我们还可以让弹幕的效果变成创建马甲释放技能,然后动态修改弹幕击中敌人的技能效果,比如如果是敌军就释放死亡缠绕,是友军就释放神圣之光……很多诸如此类的技能时常困扰着新手,但是真的是这些技能复杂吗?并不是,只是他们一开始就去纠结这些细枝末节的问题而已。
   “模块化则是主要针对系统之间 的。就如同各个部门分工合作,而各自不相互干涉其他部门的内部工作一样,系统作为组成游戏模块也是如此,不同的模块之间应当尽量的减少相互的联系,这样才能让各个模块的职责变得清晰,增强可读性,一旦发生问题,也更容易追踪其来源。实际上模块的概念不仅仅是针对游戏系统之间的,即使是单个系统内部,也可以分为更加小的各个子模块。比如之前我发过的一个选人系统的演示,将整个系统划分成为了负责逻辑的模块,负责输入和输出的模块,负责记录数据的模块以及负责控制流程的模块,这些组成庞大选人系统的子系统之间同样也可以适用模块化的原则。尽量将庞大的系统按照自顶向下的思想细分,形成模块之后再保证其功能相互独立,这样可以非常有效的避免你把一个系统做成一锅浆糊,过了几天可能连自己都不记得哪些部分是用来干什么的。
   这一章我们讨论该怎样让系统变得结构清晰,具有良好的可读性。一定程度上来讲,这让设计工作变得更加简单,同时维护起来也更加的方便。但是改进的空间依然非常大。下一章则将对可维护性和扩展性进行讨论。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2016-9-7 20:18:47 | 显示全部楼层
第四章:局限性
   原本按照我的计划,这一章应该是初步向大家介绍面向对象的内容的。但是因为有人没有认识到我为什么要极力推崇面向对象的方法,所以在这一章我打算谈一谈我们之前所掌握的方法的局限性。
   先回顾一下我们上一节的内容吧:
1.          把大量重复的动作整合到一个触发器里,将其中会变动的部分用变量替换,就是所谓的函数触发器啦。这样做既可以节省重复动作所占据的空间,又可以节省你复制它们,以及日后理解它们的时间。可以说是一个非常实用的技巧。
2.          坚持模块化的思想,让你的触发器分工明确,减少不同系统之间的相互联系,这样可以让BUG的发生限制在相对更加狭小的范围内,让你在这方面花更少的精力。
3.          自顶向下的思想可以很好的帮助你解决庞大的系统难以制作的问题。先前你可能会对于要如何制作一个系统实现一项功能感到很困惑,那是因为你还习惯于只用一个触发器和一堆动作来实现你所需要的功能,而当你把这个庞大的功能逐渐拆解成更小的功能,并用更多功能更单一更简单的触发器去实现它们的时候,事情就变得更简单多了。

   综上所言,这些技巧已经可以帮我们节省很多过去的麻烦事了。所以,到了这个时候很多人就失去了进一步改进编程方法的动力,因为他们觉得这样就已经足够了。但是实际上,上一章所提到的方法是有局限性的,尽管它们并不会直接的体现出来并让你感到困扰,却是会让你在不知不觉之中就会陷入泥沼的。
   首先的问题就是——不管是先做系统也好,还是“自顶向下和“模块化”也好,它们都是建立在同样的一个条件下的——那就是你已经知道了你的地图需要怎样的系统,又或者是你已经知道了你现在需要做的系统的功能。然而在实际的制作地图的过程中,很多时候都不是这样的。一张地图会需要的系统,以及系统会需要实现怎么样的功能,都要取决于这张地图的游戏里所包含的元素,比如弹幕地图肯定会需要弹幕,要做护甲穿透肯定会需要分析伤害云云……那么你现在能枚举出你的地图里包含的所有元素,以及所有会需要的系统么?大概是不能的,因为在我碰到的10WE用户中,有个都没有写游戏设计文档的习惯,而剩下来的那一个,即使是写了,也很有可能只是开了个帖子贴自己的英雄设定而已。
   所以说这个问题实际上是有其他的解决方法的,那就是事先做好设计。这是我认为比先做系统还要更重要的经验,那就是在做地图之前一定要想好我究竟要做一个什么样的游戏,会需要一些什么东西,并把这个设计方案记录下来。然后在你制作地图的触发部分的时候,就可以根据设计文档中枚举出的元素来设计自己的系统所需要提供的功能,以及系统的结构和触发器的内容。这样就能避免你在游戏做到一半的时候,突然发现你需要一个新的系统才能够处理一些原本没有想过的问题,否则的话一个新的系统不仅可能打乱你的制作进度,还有可能和你已有的系统产生冲突——这就是后悔都来不及的事情了,碰到这种局面的地图,不是大规模重做,就是直接死了。
   这确实是行之有效的方法,但是对于面向过程来讲,依旧没有从根本上解决问题——实际上你怎么可能在一个游戏设计的时候就预料到游戏在今后所有的变化呢?如果可以的话,这个世界上就不会有游戏补丁这种东西了。所以说随着地图的更新,你毫无疑问的会需要新的功能,而在这个时候,你就会需要修改已有的游戏系统,又或者是添加新的游戏系统。然后这个问题就回到了它的原点了——修改已有的系统就意味着修改它之前针对某个功能设计的结构,非常麻烦而且容易出问题(尽管模块化能够一定程度上避免这个问题,因为添加新的功能模块相较修改整个系统要更加简单,而又不需要修改其他的模块)。这是面向过程的一大局限性,当新的功能出现的时候,要添加已经成型的系统组成的游戏系统里会变得很麻烦。
   另外的一个问题就是动作的重复利用。War3触发器编程是面向过程的,传统方法下触发器动作的复用率非常低。大部分情况下,你都在做重复的动作。
   举个简单的例子——我要实现三个效果,第一个是每隔0.01秒飞行和检测周围单位的弹幕,第二个是每隔0.03秒进行位移和选取单位造成伤害的冲锋,第三个是每隔1秒对某个单位造成伤害的Buff。我们可以看得出来,这三个东西其实非常非常的相似,都是每隔xx秒,实现要么是位移,要么是伤害,要么是用马甲放个技能之类的效果。但是偏偏它们都分属于不同的功能,所以你不得不把相同的事情做个三遍,而其中,只有位移和伤害是不同的,其他的都是在事倍功半。在不使用YDWE的功能的情况下,你需要三个计时器,三个触发器,做的却是基本上相同的动作(而且还不支持多个技能同时释放)。
   针对这种问题,YDWE的一些功能可以帮你省去做这些重复的事情的时间。比如说逆天计时器,可以省去你开计时器和注册事件的功夫,还可以支持多人释放技能。再比如中心计时器也可以实现这个功能,但是并不如逆天计时器那样好用。所以说这些功能可以缓解一下这个问题。
   但是假如我在这个时候再提出了新的要求——我又想要做一个每隔2.00秒,提升单位1点攻击力的Buff,以及一个每隔0.5秒回复1%生命值的Buff。这两个功能和第三个效果就只有一个动作和时间间隔不同的区别而已,但是你是选择再把同样的设置逆天计时器的操作,包括实现检测BUFF,控制BUFF时间的操作再做两遍,还是又设计一个新的系统来实现一个Buff功能呢?
   所以说这就是另外一个局限性了,动作的重复利用率很低,导致我们会一直把时间花在做同样,重复的事情上面。
   还有一个问题,那就是技能的多重释放的问题。面向过程在解决这个方面具有天生的劣势,因为我们在做技能的时候都只会考虑单个技能释放的过程,当多个技能同时释放的时候,这个过程就会变得非常复杂了。传统的做法通常都是通过哈希表,将一个流程的信息与控制这个流程的计时器或者是触发器绑定。某种程度上来说这就已经体现出一些面向对象的思想了,因为触发器和计时器本身就是对象,这些操作实际上是在给对象动态添加成员。但是传统方法并不会考虑将包括流程信息和用于控制流程的计时器和触发器本身都抽象成一个对象,所以传统方法还是不得不把这些重复的事情一遍又做一遍。而面向对象可以将每一个对象与其独立的过程绑定,并不需要担心一场游戏中究竟有多少个相同的过程在同时进行。
   最后一个问题就是,面向过程本质上还是属于计算机的思考方式,和真正的人类的思维有很大的差距。当然平时在做系统的时候我们是察觉不出来这件事情的,因为在做系统的时候考虑的都是系统运行的过程,我们和电脑的想法是一样的。但是一旦做起弹幕或者Buff这样的东西的时候,你就会觉得“Buff每过xx秒,回复xx点生命值”应该是很正常的一个过程,但是为什么同时有好几个这样的过程工作的时候,就出问题了呢?在使用了面向对象的思路之后,这个问题就相对更好解决了。
   下一节我们就将讨论关于面向对象的内容了。当然,关于面向对象的基本内容我是不会讲的,所以如果你还不知道,请去查看我过去的教程。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2016-9-7 20:19:34 | 显示全部楼层
第五章:面向对象
   这一节就是被吹了很久的面向对象,因为吹得太多搞得我都觉得没什么新意了。所以说这一节我并不会讲关于面向对象的基础知识,包括生成对象ID,实现属性,方法,继承和多态等等。这一节的主要内容是关于过了差不多半年的实践之后,我对于纯T面向对象所做的改良以及面向对象是怎么解决上一节提出的那些内容的。
   首先是接口已经完全没必要存在了。在老的教程中,我把接口和多态放在同一章节中,是为了让大家能够更好的理解面向对象中继承和多态的概念。但是实际上在War3触发器的编程中,能实现面向对象的特性是十分有限的,所谓的面向对象特性,实际上很多都是依赖于触发器数组变量实现的——通过触发器变量来修改不同对象对于某个方法的实现。我们可以通过给触发器数组变量赋值来改变一个对象的某个方法的实现方法,比如从弹幕类派生出直线飞行弹幕和追踪弹幕只需要修改飞行触发器数组变量中对应索引所储存的触发器就ok了,甚至还能做到类似于在弹幕飞了一半就转换为追踪模式之类的事情(这里的触发器实际上充当的就不是方法的角色了,而是类似于闭包之类的东西)。但是这种方法无法用于实现接口的特性,因为一个类是能继承多个接口的,我们使用War3T编程却很难实现这种东西。所以我们在之前介绍的接口,其概念实际上更接近于抽象类。嗯,吧啦吧啦了一大段我猜没多少人能看得懂。简而言之就是,以后我们不需要使用接口了。如果我们需要一个类来派生其他的子类,并希望它们对于某个方法具有不同的实现的话,只需要一个普通的类,然后给他添加虚方法就行了。
   第二个是所有类的Destroy方法最好都设定为虚方法。因为大部分时候你都没有办法断言你会不会需要一个类派生出新的类,而当你的游戏中有多个子类都继承自同一父类,在某个方法中你将它们都统一视为它们的父类,并且希望调用它们的Destroy方法的时候,问题就来了——你怎么知道谁具体是哪个类型的子类呢?这个时候将Destroy设定为虚方法,并在子类被创建的时候就为其赋值,则可以避免这个问题。并且更进一步的,你可以通过判断一个触发器的Destroy触发器是否等于某个类的Destroy方法来判断一个对象的类型,这也是颇为方便的一点。
   第三个是除了没有继承任何类的类会需要分配,回收,构造,析构四个触发器以外,其他继承了某个类的类,都不会再需要分配和回收触发器。分配和回收触发器的作用是分配和回收对象的ID,而构造和析构触发器的作用则是生成一个对象(包括产生对象的ID和为对象成员赋初始值和一些初始化设置)和销毁一个对象(与构造作用相反)。原本我为每一个类都保留分配和回收触发器是为了让分配和回收的功能与构造和析构的功能分开,但是事实证明这毫无意义,只会徒增两个完全没必要的方法触发器而已——除了没有继承任何类的类有必要保留分配和回收触发器以外,继承了某个类的类完全可以把分配和回收的功能合并到了构造和析构触发器里面,而实现分配和回收只需要简单的调用其父类的分配和回收触发器即可。
   其他的改良主要体现在设计上,所以暂时不在这里讲。如果你无法看懂上面的内容,说明你需要找回我之前做的面向对象基础教程,详细查看其中的内容。
   那么接下来就让我们一边总结现有的面向对象方法,一边看它是如何解决我们上一期提出的问题的吧。现在我们仍然假设我们需要制作一个弹幕系统,但是这一次不同之处在于,我并不知道我会需要做一个什么样的弹幕,我不知道弹幕是怎么实现飞行的,我也不知道这个弹幕是否会需要撞上敌人,同样也不知道弹幕的具体效果。当然这种一问三不知的情况,即使是对于面向对象而言也是非常危险的,但是相比起介绍面向对象之前我们会完全无从下手(在不知道一个系统会有怎样的功能的情况下,你要怎么动手呢?),面向对象至少可以开始动手了。
   把上面的内容按照面向对象的方式翻译之后,就是:我们会需要一种对象叫做“弹幕”,并且我们知道“弹幕”具有“飞行”、“检测碰撞”和“产生效果”三种“行为”,但是至于弹幕具体是怎么做到这些事情的,我们并不知道
   首先,我们会需要新建一个弹幕类。构造一个类的全过程如下:
1.       创建一个触发器注释,在里面写上这个类的详细信息,再按照所写的内容去创建变量和触发器。这些注释的作用在于防止你把类弄得一团乱,同时当你记不清这个类究竟有些什么样的东西的时候,可以提供提示。相信我,花一点时间写这样一个注释绝对值得:
class AbstractBullet//Abstract的意思是抽象的,AbstractBullet即抽象的弹幕,因为我们还不知道弹幕究竟要怎么飞行和碰撞,故起此名。
{
属性:
    //暂时不知道有什么属性
方法:
   static Create()->this
   virtual Destroy()

   virtual Fly()
   virtual Collide()
   virtual Effect()
触发器:
    //传统的面向对象里没有触发器,但是war3纯T编程离不开触发器。会放在这里的触发器,只有一种功能——提供事件。其他的不提供事件的触发器都算是方法。
}
2.      按照注释里面列举的属性和方法,创建变量和触发器:
变量:属性和标记了virtual的方法,要创建变量。
属性的通常命名格式为:类名_属性名。方法的通常命名格式为:类名_m_方法名,m的意思是method,也就是方法的意思。
属性和virtual方法,如果没有标记static,则都要声明为数组(因为不被声明为数组的话,对象就无法根据自己的ID访问自己的属性和方法了)。
Static的字面意思是静态的,实际含义是指这个方法或者属性从属于所有这种类型的对象,而不是某一个具体的对象。比如人类的总数就不是属于某一个人的属性,而是全人类共有的属性。在纯T触发器编程中,Create方法就无论如何都是一个static方法,因为其构造对象的功能不被任何一个对象所拥有(当然,像是女人拥有生孩子的能力这种例外要额外分析),所以是从属于类本身而不是某个对象的。

如果你创建的类是一个没有继承任何类的类,那么你还应该声明这样的两个变量:Obj_类名(整数数组)和Obj_类名_Num(整数)。这是因为这个类没有继承任何类,所以它无法借用别人的分配和回收方法,只能自己实现这个功能。这两个变量就是用于实现类的分配和回收ID的功能的,名字的前缀Obj的意思是Object(对象),意思即为这个变量的功能是用于实现面向对象的基础功能,加上这个前缀可以使其与其他实现对象具体功能的变量区分开来,因为这种用于实现基础功能的变量在我们实现了基础功能之后几乎不会去管,放着它们混在一起会很碍事。

触发器:

任何一个类,都必须要有Create(构造)和Destroy(析构)这两个触发器。如果你的类没有继承任何类,那么它还需要有Obj_类名_Allocate(分配)和Obj_类名_Deallocate(回收)这两个触发器,它们的作用是实现对象ID的分配和回收:


Obj_AbstractBullet_Allocate
事件
条件
动作
设置 this = Obj_AbstractBullet[0]
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
           If - 条件
                this 不等于 0
           Then - 动作
设置 Obj_AbstractBullet[0] =Obj_AbstractBullet[this]
           Else - 动作
设置 Obj_AbstractBullet_Num =(Obj_AbstractBullet_Num + 1)
设置 this = Obj_AbstractBullet_Num
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
           If - 条件
                this 大于 8190
           Then - 动作
设置 this = 0
           Else - 动作
设置 Obj_AbstractBullet[this] = -1

Obj_AbstractBullet_Deallocate
事件
条件
动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
           If - 条件
                this 不等于 0
           Then - 动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                    If - 条件
                       Obj_AbstractBullet[this] 小于 0
                    Then - 动作
设置 Obj_AbstractBullet[this] =Obj_AbstractBullet[0]
设置 Obj_AbstractBullet[0] = this
                    Else - 动作
            Else - 动作

至于这两个触发器的动作究竟为何是这样的,你可以选择不用深究,只需要知道调用Allocate触发器可以将this赋值为一个独一无二的,介于1~8190之间的整数,而调用Deallocate可以回收一个已经被分配的数字,防止8190个数字全部都被分配出去以至于没有数字可以分配,就OK了。如果你创建了一个新的没有继承任何类的类,也需要这俩个触发器,就可以直接把这里的动作复制过去,只替换其中的Obj_类名和Obj_类名_Num变量为新的类的对应变量就ok了。

然后是Create和Destroy。对于一个没有继承任何类的类来讲,Create和Destroy里面至少都要包含着两个动作:


触发器 - 运行 Obj_AbstractBullet_Allocate <预设> (检查条件)
触发器 - 运行 Obj_AbstractBullet_Deallocate <预设> (检查条件)
至少要这样,这两个触发器才能实现构造和析构的功能。然后你可以添加一些为变量赋初始值的动作:


AbstractBullet_s_Create
事件
条件
动作
触发器 - 运行 Obj_AbstractBullet_Allocate <预设> (检查条件)
设置 AbstractBullet_m_Fly[this] = DoNothing <预设>
设置 AbstractBullet_m_Collide[this] = DoNothing<预设>
设置 AbstractBullet_m_Effect[this] = DoNothing<预设>
设置 AbstractBullet_m_Destroy[this] =AbstractBullet_o_Destroy <预设>
AbstractBullet_o_Destroy
事件
条件
动作
设置 AbstractBullet_m_Fly[this] = DoNothing <预设>
设置 AbstractBullet_m_Collide[this] = DoNothing<预设>
设置 AbstractBullet_m_Effect[this] = DoNothing<预设>
设置 AbstractBullet_m_Destroy[this] = DoNothing<预设>
触发器 - 运行 Obj_AbstractBullet_Deallocate <预设> (检查条件)
为什么Destroy方法的名字要叫做AbstractBullet_o_Destroy?o的意思是Override(重写),即这个方法是某个virtual方法的实现或者重写,比如AbstractBullet有一个virtual方法Destroy,这个触发器AbstractBullet_o_Destroy就是对于这个virtual方法的实现,你可以把这个触发器赋值给Destroy方法的触发器数组,然后调用它。
3.      创建一个触发器叫做类名_API,并禁用它。这个触发器的作用是用来存放一些成组的动作,其作用为使用某个这个类提供的功能:

当你需要使用这个类的某些功能的时候,就可以直接从这里面复制粘贴,而不需要从动作里再慢慢点调用触发器了。当然,从截图上来看没啥必要,因为截图上的函数触发器没啥参数,所以不能帮你节约多少时间。但是碰上那种有5、6个参数的,或者调用的动作已经形成套路了的,这种东西就比较有用了。总而言之,做这样一个触发器不会浪费你多少时间,而且相信它一定会有用的。
         于是乎我们就新建了一个类了,这就是面向对象和面向过程的差别——面向过程和结构化程序设计解决问题的思路都是去分析解决问题的过程,然后把它变成程序,再用各种方法实现。比如在第三章中,我们就先分析了弹幕飞行,检测碰撞,触发效果,循环所有弹幕的过程,然后将它们用多个触发器实现了。但是在事先不知道弹幕的具体过程的情况下,面向过程和结构化程序设计就很难进行下去了。面向对象则不同,面向对象解决问题的思路是去分析参与问题的对象,而不是固定的过程。因为在问题中,对象往往是不变的,会变动的是对象的属性和行为。比如在弹幕问题中,弹幕是不变的,会变化的是弹幕的飞行速度,和弹幕的飞行方式。
         这就是我在第四节的结尾所提到的。毫无疑问,面向对象解决问题的思维方式要更贴近人类的思维一些,尤其是对于游戏这种需要对真实世界进行模拟的程序而言,面向对象可以说是天生就有着巨大的优势。这也是为什么我要一而再再而三的推广面向对象——虽然并没有多少人意识到它的好处罢了。
         话说回来,虽然说我们现在创造了一个抽象弹幕类,但是它是完全抽象的,方法没有得到具体的实现,也就没有任何作用了。为了让它变得有用,我们需要创建一个新的子类,继承这个具有虚方法的类,并实现它的方法,这样它就能变得有用了。创建一个新的子类,让它继承自某一个类的过程如下:
1.      仍然是写一个触发器注释。但是与创建一个类不同的是,继承某个类的子类要在“class 子类名”的后面加上“: 父类名”,表明这个类继承了自某个类,以便于当你忘记类之间的继承关系的时候进行查看。
继承某个父类的子类,可以获得所有父类拥有的属性和方法。但是这些从父类获得的属性和方法,就不需要特地的写在这个类的说明里了,这个类只关心它自己独有的属性和方法,如果你想要查看它继承了其他类的哪些属性,只需要去查看它继承的类的注释就好了。
class UnitBullet : AbstractBullet
{
属性:
   Unit Object(马甲单位)
   Real Speed(速度)
   Real Range(范围)
   Real Radius(碰撞半径)
   Real Damage(伤害)

   Location Point(避免大量创建点)
   Real Count(飞行计程器)
   Bool HitFlag(是否击中了单位)
   Bool DestroyFlag(是否需要销毁)
   Unit Target(被击中的目标)
方法:
   static Create(GobUnitActor(马甲单位),GobReal1(速度),GobReal2(射程),GobReal3(碰撞大小),GobReal4(伤害))->this
   override Destroy()

   override Fly()
   override Collide()
   override Effect()
触发器:
   MainLoop(每过0.01秒)
}
触发器的格式为:触发器名(事件类型)。

2.      按照注释所说明的去创建变量和触发器。变量的话没有太大的区别,触发器稍微与创建不继承任何类的新类有些不同:



由于UnitBullet类是继承自AbstractBullet的类,因此它没有自己的Allocate和Deallocate触发器,必须要调用父类的Create和Destroy触发器才能实现这一项功能。这一点差别可以从它自己的Create和Destroy触发器的动作中看到,也是纯T面向对象必须遵守的定律。
同时在UnitBullet重写了AbstractBullet的虚方法时,也必须将自己用于实现这些功能的触发器赋值给对应的触发器变量,以覆盖掉AbstractBullet对其的实现(虽然说实际上AbstractBullet并没有实现)。如果UnitBullet再派生出了其他类型的变量,也必须做同样的事情,让子类再覆盖掉自己对于这些方法的重写(当然子类也可以选择不重写,不覆盖)。
3.      写个API触发器然后禁用:

UnitBullet_API
事件
条件
动作
       -------- 创建一个弹幕 --------
触发器 - 运行 GobStackPushThis <预设> (检查条件)
设置 GobUnitActor = 没有单位
设置 GobReal1 = 0.00
设置 GobReal2 = 0.00
设置 GobReal3 = 0.00
设置 GobReal4 = 0.00
触发器 - 运行 UnitBullet_s_Create <预设> (检查条件)
触发器 - 运行 GobStackPopThis <预设> (检查条件)
       -------- 销毁一个弹幕 --------
触发器 - 运行 UnitBullet_o_Destroy <预设> (检查条件)
4.      具体UnitBullet类是怎么实现Fly,Collide和Effect方法的,实际上没啥好演示的,就参考咱们以前做的弹幕就行了:




UnitBullet_o_Fly
事件
条件
动作
单位 - 设置 UnitBullet_Object[this] 的X坐标为 ((UnitBullet_Object[this] 所在X轴坐标) + (UnitBullet_Speed[this] x ((Cos((UnitBullet_Object[this] 的面向角度))) x 0.01)))
单位 - 设置 UnitBullet_Object[this] 的Y坐标为 ((UnitBullet_Object[this] 所在Y轴坐标) + (UnitBullet_Speed[this] x ((Sin((UnitBullet_Object[this] 的面向角度))) x 0.01)))
设置 UnitBullet_Count[this] =(UnitBullet_Count[this] + (UnitBullet_Speed[this] x 0.01))
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
           If - 条件
                UnitBullet_Count[this] 大于或等于 UnitBullet_Range[this]
           Then - 动作
设置 UnitBullet_DestroyFlag[this] = TRUE
            Else - 动作

UnitBullet_o_Collide
事件
条件
动作
点 - 移动 UnitBullet_Point[this] 到((UnitBullet_Object[this] 所在X轴坐标),(UnitBullet_Object[this] 所在Y轴坐标))
设置 GobUnitGroupMain = (半径为 UnitBullet_Radius[this] 圆心为 UnitBullet_Point[this] 且满足 ((((匹配单位) 是存活的) 等于 TRUE) 且 ((((匹配单位) 是 (UnitBullet_Object[this] 的所有者) 的敌对单位) 等于 TRUE) 且 (TRUE 等于 TRUE))) 的所有单位)
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
           If - 条件
                (GobUnitGroupMain 中的单位数量) 大于 0
           Then - 动作
设置 UnitBullet_Target[this] = (GobUnitGroupMain中第一个单位)
设置 UnitBullet_HitFlag[this] = TRUE
           Else - 动作
设置 UnitBullet_Target[this] = 没有单位
设置 UnitBullet_HitFlag[this] = FALSE
单位组 - 删除 GobUnitGroupMain

UnitBullet_o_Effect
事件
条件
动作
单位 - 命令 UnitBullet_Object[this] 对 UnitBullet_Target[this] 造成 UnitBullet_Damage[this] 点伤害(不是攻击伤害, 不是远程攻击) 攻击类型: 法术伤害类型: 火焰武器类型: 无
设置 UnitBullet_DestroyFlag[this] = TRUE

UnitBullet_t_MainLoop
事件
时间 - 每当游戏逝去 0.01 秒
条件
动作
触发器 - 运行 GobStackPushThis <预设> (检查条件)
触发器 - 运行 GobStackPushCX <预设> (检查条件)
循环动作[GobInt3]从 1 到 Obj_AbstractBullet_Num, 运行 (Loop - 动作)
           Loop - 动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                   If - 条件
                       Obj_AbstractBullet[GobInt3] 小于 0
                       AbstractBullet_m_Destroy[GobInt3] 等于 UnitBullet_o_Destroy <预设>
                    Then - 动作
设置 this = GobInt3
触发器 - 运行 AbstractBullet_m_Fly[this] (检查条件)
触发器 - 运行 AbstractBullet_m_Collide[this] (检查条件)
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                            If - 条件
                               UnitBullet_HitFlag[this] 等于 TRUE
                            Then - 动作
触发器 - 运行 AbstractBullet_m_Effect[this] (检查条件)
                            Else - 动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                            If - 条件
                               UnitBullet_DestroyFlag[this] 等于 TRUE
                            Then - 动作
触发器 - 运行 AbstractBullet_m_Destroy[this] (检查条件)
                            Else - 动作
                    Else - 动作
触发器 - 运行 GobStackPopCX <预设> (检查条件)
触发器 - 运行 GobStackPopThis <预设> (检查条件)

   这还只是一个非常简单的,直线飞行的弹幕而已,我们之前已经用面向过程写过一个同样的东西了。但是,我现在提出一个新的要求——游戏里不能只有直线飞行的弹幕,我们还需要能够变速飞行的弹幕。对于面向过程而言,这个问题就开始变得有点头疼了,因为我之前明明已经写好了那么棒的一个弹幕系统,但是你现在又要求我去修改一个原本浑然一体的东西,就必然会伴随着风险。好在这个问题并不是非常的复杂,所以即使是面向过程,也无非就是在负责实现飞行的模块里加几个动作而已。
   面向对象则有更好的处理方式——我们根本就不需要去考虑弹幕原本是如何飞行的,我们只需要考虑在弹幕的基础上,实现弹幕速度的动态变化就ok了。让我们再派生一个子类出来:
class AccelBullet : UnitBullet
{
属性:
    Real Accel(加速度)
    Real Max(最大速度)
    Real Min(最小速度)
方法:
    staticCreate(GobRealArray[1](速度),GobRealArray[2](射程),GobRealArray[3](碰撞半径),GobRealArray[4](伤害),GobRealArray[5](加速度),GobRealArray[6](最大速度),GobRealArray[7](最小速度),)->this
    overrideDestroy()

    overrideFly()
触发器:
}

可以看到,AccelBullet相比UnitBullet主要有两处修改:增加了用于实现速度变化的属性,以及对于飞行方法的重写。
增加属性比较简单,不做说明。重写Fly方法是比较有意思的地方,我们来看看AccelBullet是怎么实现不干涉UnitBullet对于Fly的实现却又能改变它的飞行速度的:

AccelBullet_o_Fly
事件
条件
动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
            If - 条件
               UnitBullet_Speed[this] 小于 AccelBullet_Max[this]
               UnitBullet_Speed[this] 大于 AccelBullet_Min[this]
            Then- 动作
设置 UnitBullet_Speed[this] = (UnitBullet_Speed[this] + (AccelBullet_Accel[this]x 0.01))
            Else- 动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                   If - 条件
                       UnitBullet_Speed[this] 大于 AccelBullet_Max[this]
                   Then - 动作
设置 UnitBullet_Speed[this] = AccelBullet_Max[this]
                   Else - 动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                   If - 条件
                       UnitBullet_Speed[this] 小于 AccelBullet_Min[this]
                   Then - 动作
设置 UnitBullet_Speed[this] = AccelBullet_Min[this]
                   Else - 动作
触发器 - 运行 UnitBullet_o_Fly <预设> (检查条件)

   答案非常简单,AccelBullet继承了UnitBullet类,自然也就获得了UnitBullet的所有方法触发器的使用权。我们只需要先按照AccelBullet自己的规则去修改UnitBullet的速度,然后再调用UnitBullet已经实现好了的Fly方法,就可以实现变速飞行了。
   这么做的高明之处就在于我们不需要去修改已经被证实是完全没问题的东西,就可以派生出新的功能。只要我们保证这层层派生的每一层功能都没有问题,整个程序就不可能出差错。这和提出任何修改都要牵一发而动全身的面向过程有着天壤之别。而且即使今后我想要修改UnitBullet的飞行方法,无论我如何修改,只要Speed这个属性的含义和用途没有改变,AccelBullet实现的修改Speed的功能就仍然会是可用可靠的。
   这也就是我在上一节中提出来的问题的解答:面向对象的继承和多态(AccelBullet继承UnitBullet为继承,重写Fly方法为多态)特性可以极大的提高程序的可扩展性,在不断提出新需求的游戏编程中,我们可以几乎不需要涉及修改已有的良好功能,就能派生出新的功能。
另外Create方法稍微有一些细节要向大家展示一下:

AccelBullet_s_Create
事件
条件
动作
触发器 - 运行 GobStackPushReal <预设> (检查条件)
设置 GobReal1 = GobRealArray[1]
设置 GobReal2 = GobRealArray[2]
设置 GobReal3 = GobRealArray[3]
设置 GobReal4 = GobRealArray[4]
触发器 - 运行 UnitBullet_s_Create <预设> (检查条件)
触发器 - 运行 GobStackPopReal <预设> (检查条件)
设置 AccelBullet_Accel[this] = GobRealArray[5]
设置 AccelBullet_Max[this] = GobRealArray[6]
设置 AccelBullet_Min[this] = GobRealArray[7]
设置 AbstractBullet_m_Fly[this] = AccelBullet_o_Fly <预设>
设置 AbstractBullet_m_Destroy[this] = AccelBullet_o_Destroy<预设>

   想必你之前也注意到了这一点,那就是带加速度的弹幕,等于是总共有7个要设置的属性了。而我们只有GobReal1,2,3,4,共计4个实数变量,要怎么实现把参数装在GobReal1,2,3,4里传给UnitBullet_s_Create,还能给额外的AccelBullet的属性赋值呢?答案就是如图。AccelBullet的参数不再使用GobReal1,2,3,4,而是拥有巨大容量的GobRealArray
   参数过多实际上也是一个问题,过去曾经有人跟我说过有弹幕系统的创建弹幕函数的参数数量高达20个之多。天,真不知道什么样的人能记住这20个参数都是干什么用的,以及每一次都填这么多参数,真的不累么……后面我就会讲到怎样去克服这样的一个问题——并不是每一次,所有的参数都是需要用到的,我们可以想办法将其省去。
   这一节只是介绍了一下目前改良的面向对象编程套路,让大家复习了一下相关的内容而已。从下一节开始,我们就将更加的深入面向对象,去探讨一些之前没有讨论过的更深刻的内容——以面向对象的视角,着眼于游戏编程的全局来审视War3T编程。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2016-9-7 20:21:25 | 显示全部楼层
第六章:对象与行为
         在开始讲这一章的内容之前,先让我问你一个问题——你是否觉得,自己在游戏里做了很多的技能或者是其他的效果,但是大部分实际上都差不多?
         好吧实际上这个问题我在第四章里面也问过一次了。当时我举的例子是弹幕,位移(冲锋)和Buff。三者之间具有共同点,比如都是与时间有关,弹幕和位移都具有让单位以一定的轨迹移动的功能等等。
         Buff有点复杂,暂时不做演示。我们把位移做出来,和弹幕对比一下吧。让我们回忆一下面向对象解决一个问题的步骤:分析问题中有什么对象,对象的属性与行为,然后实现它们。

         我们可以把“位移”这个概念抽取出来,形成一个对象——对象的属性包括进行位移的单位,以及描述位移轨迹的属性,比如速度和距离等等。写成触发器注释,大概是这么个样子:
         class Movement
{
属性:
Unit Object(进行位移的单位)

   Real Speed(速度)
   Real Facing(方向)
   Real Distance(距离)
   Real Count(用来统计已经位移了多少距离)
方法:
   static Create(GobUnitActor(进行位移的单位),GobReal1(位移的距离),GobReal2(方向),GobReal3(位移的速度))->this
override Destroy()

Move()
触发器:
MainLoop(每过0.01秒)
}

第二步是创建变量和触发器,过程和弹幕没太大的区别,只是位移不需要检测碰撞和造成效果而已。我只放出两个最关键的触发器的动作,相信大家看了就明白了:
file:///C:\Users\ADMINI~1\AppData\Local\Temp\msohtmlclip1\01\clip_image001.png
file:///C:\Users\ADMINI~1\AppData\Local\Temp\msohtmlclip1\01\clip_image003.jpg
Movement_t_MainLoop
事件
时间 - 每当游戏逝去 0.01 秒
条件
动作
触发器 - 运行GobStackPushThis<预设> (检查条件)
触发器 - 运行GobStackPushCX<预设> (检查条件)
循环动作[GobInt3]从 1 到Obj_Movement_Num, 运行 (Loop - 动作)
            Loop- 动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                   If - 条件
Obj_Movement[GobInt3] 小于 0
                   Then - 动作
设置 this = GobInt3
触发器 - 运行Movement_Move<预设> (检查条件)
                   Else - 动作
触发器 - 运行GobStackPopCX<预设> (检查条件)
触发器 - 运行GobStackPopThis<预设> (检查条件)

Movement_Move
事件
条件
动作
单位 - 设置Movement_Object[this] 的X坐标为 ((Movement_Object[this] 所在X轴坐标) + (Movement_Speed[this] x((Cos((Movement_Object[this] 的面向角度))) x 0.01)))
单位 - 设置Movement_Object[this] 的Y坐标为 ((Movement_Object[this] 所在Y轴坐标) + (Movement_Speed[this] x((Sin((Movement_Object[this] 的面向角度))) x 0.01)))
设置Movement_Count[this] = (Movement_Count[this] +(Movement_Speed[this] x 0.01))
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
            If - 条件
Movement_Count[this] 大于或等于Movement_Distance[this]
            Then- 动作
触发器 - 运行Movement_m_Destroy[this] (检查条件)
            Else- 动作

   相信你一定发现了——位移的触发器的动作跟弹幕的基本上没啥差别嘛!实际上,MainLoopMove的动作我是直接复制UnitBulletMainLoopFly的动作,只是稍微做了一些修改而已。
   为什么,为什么会这样呢?明明已经学会了面向对象,明明可以继承,为什么同样的事情还是要再做一遍呢?
   当然,你可能会想为什么不从Movement派生出UnitBullet呢?”,因为Movement实现了移动单位的功能,UnitBullet只需要在这个基础上再加上碰撞检测和触发效果的功能就行了,这样就不用Movement做过一遍的事情,Bullet再做一遍了。确实有道理,但是也有问题——首先UnitBullet继承自AbstractBullet,而一个类是不能同时继承两个类的,因为两个类的ID分配方法会有区别。就算UnitBullet不继承AbstractBullet了,从功能上来说,UnitBullet也不能继承Movement,因为“弹幕”和“位移”在游戏里是两种不同的功能,如果UnitBullet继承Movement,就会徒增这两种毫无瓜葛的功能的联系,导致修改Movement或是修改Bullet的时候,会相互影响。退一万步来讲,UnitBullet继承Movement从逻辑上来讲也说不通,为何“位移”会派生出“弹幕”?……所以说比较合理的解决方法之一,就是让Movement专心处理单位的位移,并派生出各种位移方式,然后UnitBullet将Movement对象作为其成员之一,这个对象专门负责弹幕单位的位移。
   除了这个方法以外呢?嗯,我们还可以试试把MainLoop的功能整合在一起。不管是弹幕也好,位移也好,Buff也好,它们跟时间相关的功能其实都可以概括成每隔xxx秒,执行一定的动作。我们可以考虑创建一个类,这个类具有一个虚方法和一个MainLoop触发器,MainLoop触发器的事件是每过0.01秒,动作是调用虚方法。然后让这些对象继承这个类,把它们每隔xxx秒要运行一次的动作都整合到一个触发器,作为对父类虚方法的实现。这样的话就可以避免我们每一次需要用到每隔xxx秒运行动作的情况下,又要新建一个MainLoop触发器——如果我们要用到这样的功能,只需要让这个类继承我们创建的具有MainLoop的类就行了。
   大致上来讲,这个问题算是解决了。但是这个问题只是无数相同问题中的其中一个。如果说“弹幕”和“位移”是在移动单位和时间事件上有相同,那么我再举一个其他例子:技能A是个可开关技能,开启之后单位的攻击会附带减速效果,技能B是个被动技能,效果是单位的攻击会附带闪电链。这两个技能也有雷同之处,那就是它们都需要监测“单位进行攻击”的事件,并造成额外效果。其解决方法也是相似的——我们可以统一建立一个具有监控单位进行攻击的类,然后设置一个虚方法并在监控到事件的时候执行,再让所有需要这个监控功能的类继承这个监控类,并用自己希望执行的动作重写监控类的虚方法。
   这种解决问题的思路类似于我们之前提到过的“监听-处理模式”,可以避免我们为每一个需要监测相同事件的类都创建一个用于检测事件的触发器(就像MainLoop那样,弹幕和位移明明是可以共用一个MainLoop触发器的)。但是这种方法也有它的局限性,那就是一个子类只能够继承一个父类,这就意味着一个类只能够继承某一个监控类,也就只能监控某一个事件了。
   如果你的思维稍微跳跃一点的话,你可能已经想到这个问题的答案了。但是现在,让我们从游戏的整体上来审视我们所需要实现的这些效果吧。这个时候我们就会发现一个非常惊人的事实——
   ——所有我们做的技能,BUFF,持续效果,实际上都是在做同样的事情——监控某种事件,进行相应的处理,并且这种对象具有一定的生命周期。
   如果你不相信,我们可以分析一下。
   首先是技能——技能要么是主动要么是被动,主动技能就是检测单位技能的释放,然后加上你自己的效果;被动技能则是检测一些其他的事件,比如攻击,每隔一段时间之类的,然后加上你自己的效果;技能可能并不是单位一开始就拥有的,所以技能的生命周期始于单位获得某一项技能,并终止于单位不再拥有这一项技能(如果你的单位一开始就拥有这项技能,又没有洗技能设定,那么这个技能的生命周期就会一直到你的单位被删除才会结束)。
   然后是BUFF——BUFF的生命周期更为明显,因为BUFF通常都是有持续时间的,其生命始于BUFF被套上某个单位,终于时间耗尽,亦或者是被驱散魔法驱散。单位死亡同样也可以导致BUFF消失。而BUFF的效果,则一般都是从BUFF的生命周期开始起效,到BUFF的周期结束消失,比如单位被套上BUFF时攻击力+5,到了BUFF消失的时候,自然就是取消这个效果了。有些时候BUFF还有点别的效果,比如给单位的攻击或者移动附加其他特效之类的,不一一列举。
   最后是持续效果——所谓持续效果就是技能或者是其他东西引发的效果,比如说烈焰风暴这种技能,放完之后施法者就跑了,但是地上的火还在燃烧。持续效果的生命周期实际上与BUFF类似,都是从自身的产生开始起效,终止于自身的消亡。
   我们完全可以把这种在游戏中“具有一定的生命周期,能监控游戏中的事件,并对其进行处理”的事物抽象成一种类,而其他的任何技能,BUFF,持续效果,都是这种类的子类。不仅对单位如此,对玩家,对物品,也是如此。这种类不产生任何实际效果,它只负责使用触发器对游戏中的各种现象进行监控,然后调用对应的虚方法。实现虚方法,让它具有实际效果则是子类的职责,包括弹幕,位移,BUFF都可以从这种类派生出来。
   如果说游戏中的“玩家”,“单位”,“物品”是实打实的游戏对象的话,这种附着在玩家,单位,物品身上的,监控它们的事件并触发效果的对象,我们就将其称之为“行为”吧。也就是说,“位移”是单位的一种行为,“弹幕”也是单位的一种行为,“BUFF”亦是附加于单位身上的一种行为。从现在开始我们就将游戏中的事物分为了两种——“游戏对象”与“游戏行为”。根据所附着的游戏对象的类型不同,我们还可以进一步把“游戏行为”分化成“单位行为”“玩家行为”“物品行为”等等……
   我们可以先创建一个类叫做“GameBehavior”,也就是“游戏行为”的意思。游戏行为自身不附加在任何游戏对象身上,因此它能够检测的事件非常有限,生命周期也不复杂:
class GameBehavior
{
属性:
    String Tag(用于区别不同行为的标签)

    Real Duration(这个行为持续的时间,小于等于0则代表这个行为持续时间无限大)
    Real Time(这个行为从创建到现在已经过了多久了,如果超过Duration,则销毁这个行为自身)
方法:
static Create(GobReal1(行为持续时间))->this
virtual Destroy()

    virtual onStart()//这个虚方法只会被调用一次,会在行为开始计时的时候被调用
    virtual onUpdate()//这个虚方法每过0.01秒就会被调用一次
    virtual onEnd()//这个虚方法只会被调用一次,会在行为的Time超过Duration的时候被调用
触发器:
TimePeriodic(每过0.01秒)
}

GameBehavior_s_Create
    事件
    条件
    动作
        触发器 - 运行Obj_GameBehavior_Allocate<预设> (检查条件)
        设置 GameBehavior_Duration[this] = GobReal1
        设置 GameBehavior_Time[this] = 0.00
        设置GameBehavior_Tag[this] = GameBehavior
        设置 GameBehavior_m_onStart[this] = DoNothing<预设>
        设置 GameBehavior_m_onUpdate[this] =DoNothing<预设>
        设置 GameBehavior_m_onEnd[this] = DoNothing<预设>
        设置 GameBehavior_m_Destroy[this] =GameBehavior_o_Destroy<预设>

GameBehavior_t_TimePeriodic
    事件
        时间 - 每当游戏逝去 0.01 秒
    条件
    动作
        触发器 - 运行GobStackPushThis<预设> (检查条件)
        触发器 - 运行GobStackPushCX<预设> (检查条件)
        循环动作[GobInt3]从 1 到 Obj_GameBehavior_Num, 运行 (Loop - 动作)
            Loop - 动作
                如果(所有的条件成立) 则运行 (Then - 动作) 否则运行(Else - 动作)
                    If - 条件
Obj_GameBehavior[this] 小于 0
                    Then -动作
                        设置 this = GobInt3
                        如果(所有的条件成立) 则运行 (Then - 动作) 否则运行(Else - 动作)
                           If - 条件
GameBehavior_Time[this] 小于或等于 0.00
                           Then - 动作
                               触发器 - 运行 GameBehavior_m_onStart[this] (检查条件)
                           Else - 动作
                               触发器 - 运行 GameBehavior_m_onUpdate[this] (检查条件)
                        设置 GameBehavior_Time[this] =(GameBehavior_Time[this] + 0.01)
                        如果(所有的条件成立) 则运行 (Then - 动作) 否则运行(Else - 动作)
                           If - 条件
GameBehavior_Duration[this] 大于 0.00
GameBehavior_Time[this] 大于或等于 GameBehavior_Duration[this]
                           Then - 动作
                               触发器 - 运行 GameBehavior_m_onEnd[this] (检查条件)
                               触发器 - 运行 GameBehavior_m_Destroy[this] (检查条件)
                            Else - 动作
                    Else -动作
        触发器 - 运行GobStackPopCX<预设> (检查条件)
        触发器 - 运行GobStackPopThis<预设> (检查条件)

从上面的触发器可以看出,GameBehavior可以具有一定的持续时间,也可以永远的持续下去,并且可以在自己的生命周期开始,结束,和每过0.01秒的时候执行对应的虚方法。
我们先不管玩家,物品之类的东西,派生一个“单位行为”吧:

classUnitBehavior : GameBehavior
{
属性:
    Unit Object(行为所附加的单位)

Int Index(用于快速查找行为在单位行为列表中的位置)
    staticHashTableBehaviorList(用于记录单位所附加的行为的哈希表)
方法:
    staticCreate(GobUnitActor(行为所附加的单位),GobReal1(行为持续时间))->this
override Destroy()

virtualonDeath()
virtualonKill()

    staticGetBehavior(GobUnitActor(要获取行为的单位),GobStr1(要获取的行为的Tag))->GobInt1(获得的单位行为)
    staticGetBehaviors(GobUnitActor(要获取行为的单位),GobStr1(要获取的行为的Tag))->GobIntArray(获得的单位行为列表)
触发器:
onInit(地图初始化)
UnitDeath(任意单位死亡)
}

从上可以看得出UnitBehavior与GameBehavior的显著差别。首先UnitBehavior具有一个属性Object,指的是这个行为所附加的单位。另外就是UnitBehavior有一个哈希表用于记录每一个单位身上有哪些行为,并且拥有两个静态方法用来查询一个单位身上有哪些行为:
UnitBehavior_s_Create
    事件
    条件
    动作
        触发器 - 运行GameBehavior_s_Create<预设> (检查条件)
        设置 UnitBehavior_Object[this] = GobUnitActor
        设置 GameBehavior_m_Destroy[this] =UnitBehavior_o_Destroy<预设>
        -------- 将新建的行为添加到单位的行为列表 --------
        -------- 将行为列表的长度加一 --------
        哈希表 - 在UnitBehavior_s_BehaviorList 的主索引 (获取 GobUnitActor 的整数地址) 子索引 0 中保存整数 ((在 UnitBehavior_s_BehaviorList的主索引 (获取 GobUnitActor 的整数地址) 子索引 0 内提取整数) + 1)
        -------- 将新建的单位行为存储到列表中 --------
        哈希表 - 在UnitBehavior_s_BehaviorList 的主索引 (获取 GobUnitActor 的整数地址) 子索引 (在 UnitBehavior_s_BehaviorList 的主索引 (获取 GobUnitActor 的整数地址) 子索引 0 内提取整数) 中保存整数 this
        -------- 记录新建的单位行为在列表中的位置,便于快速查找 --------
        设置 UnitBehavior_Index[this] = (在 UnitBehavior_s_BehaviorList 的主索引 (获取 GobUnitActor 的整数地址) 子索引 0 内提取整数)

UnitBehavior_o_Destroy
    事件
    条件
    动作
        -------- 将单位行为列表顶部的行为挪动到要销毁的行为的位置 --------
        哈希表 - 在UnitBehavior_s_BehaviorList 的主索引 (获取 UnitBehavior_Object[this] 的整数地址) 子索引 UnitBehavior_Index[this] 中保存整数 (在 UnitBehavior_s_BehaviorList 的主索引 (获取 UnitBehavior_Object[this] 的整数地址) 子索引 (在 UnitBehavior_s_BehaviorList 的主索引 (获取 UnitBehavior_Object[this] 的整数地址) 子索引 0 内提取整数) 内提取整数)
        -------- 清空被挪动的行为列表顶部的空间,避免列表中出现两个相同行为 --------
        哈希表 - 在UnitBehavior_s_BehaviorList 的主索引 (获取 UnitBehavior_Object[this] 的整数地址) 子索引 (在 UnitBehavior_s_BehaviorList 的主索引 (获取 UnitBehavior_Object[this] 的整数地址) 子索引 0 内提取整数) 中保存整数 0
        -------- 将行为列表的长度减一 --------
        哈希表 - 在UnitBehavior_s_BehaviorList 的主索引 (获取 UnitBehavior_Object[this] 的整数地址) 子索引 0 中保存整数 ((在UnitBehavior_s_BehaviorList 的主索引 (获取 UnitBehavior_Object[this] 的整数地址) 子索引 0 内提取整数) - 1)
        -------- ↑将销毁的行为从单位的行为列表中删除 --------
        设置 UnitBehavior_Object[this] = 没有单位
        触发器 - 运行GameBehavior_o_Destroy<预设> (检查条件)

UnitBehavior_s_GetBehavior
    事件
    条件
    动作
        设置 GobInt1 = 0
        如果(所有的条件成立) 则运行 (Then - 动作) 否则运行(Else - 动作)
            If - 条件
                GobStr1 不等于 <空字符串>
GobUnitActor 不等于 没有单位
            Then - 动作
                触发器 - 运行GobStackPushCX<预设> (检查条件)
                循环动作[GobInt3]从 1 到 (在 UnitBehavior_s_BehaviorList 的主索引 (获取 GobUnitActor 的整数地址) 子索引 0 内提取整数), 运行 (Loop - 动作)
                    Loop -动作
                        设置 GobInt1 = (在UnitBehavior_s_BehaviorList 的主索引 (获取 GobUnitActor 的整数地址) 子索引 GobInt3 内提取整数)
                        如果(所有的条件成立) 则运行 (Then - 动作) 否则运行(Else - 动作)
                           If - 条件
                               (截取GameBehavior_Tag[GobInt1] 的 1 - (GobStr1的长度) 字节部分) 等于 GobStr1
                           Then - 动作
                               退出循环
                           Else - 动作
                触发器 - 运行GobStackPopCX<预设> (检查条件)
            Else - 动作

UnitBehavior_s_GetBehaviors
    事件
    条件
    动作
        设置 GobIntArray[0] = 0
        如果(所有的条件成立) 则运行 (Then - 动作) 否则运行(Else - 动作)
            If - 条件
GobUnitActor 不等于 没有单位
            Then - 动作
                触发器 - 运行GobStackPushCX<预设> (检查条件)
                循环动作[GobInt3]从 1 到 (在 UnitBehavior_s_BehaviorList 的主索引 (获取 GobUnitActor 的整数地址) 子索引 0 内提取整数), 运行 (Loop - 动作)
                    Loop - 动作
                        设置 GobIntArray[(GobIntArray[0] + 1)] = (在 UnitBehavior_s_BehaviorList 的主索引 (获取 GobUnitActor 的整数地址) 子索引 GobInt3 内提取整数)
                        如果(所有的条件成立) 则运行 (Then - 动作) 否则运行(Else - 动作)
                           If - 条件
                               Or - 任意条件成立
                                    条件
                                        (截取 GameBehavior_Tag[GobIntArray[(GobIntArray[0]+ 1)]] 的 1 - (GobStr1的长度) 字节部分) 等于 GobStr1
                                        GobStr1 等于 <空字符串>
                           Then - 动作
                               设置 GobIntArray[0] =(GobIntArray[0] + 1)
                           Else - 动作
                               设置GobIntArray[(GobIntArray[0] + 1)] = 0
                触发器 - 运行 GobStackPopCX<预设> (检查条件)
            Else - 动作

   这两个方法可以方便你进行行为之间的互动,比如说你可以设置行为B,在检测到单位身上有行为A的时候具有某种额外效果。至于搜索行为的条件是行为的Tag前端与参数字符串相同就OK,则是为了应付这种情况——假如说我有一个加BUFF的技能A,每个等级加的BUFF都不同,但是我又希望它们能被统一识别为技能A加的行为,就可以分别将不同等级的BUFF行为的Tag设置为A1,A2,A3……而在使用GetBehaviors搜索的时候,只需要搜索A,就会把A1,A2,A3……全部搜出来了。如果你是想要搜索某个特定等级的行为,也只需要搜索特定的A1,A2之类即可。
   除了给你用以外,这两个方法还用于实现监听单位事件并调用对应行为:
UnitBehavior_t_UnitDeath
    事件
        单位 - 任意单位 死亡
    条件
    动作
        触发器 - 运行GobStackPushUnit<预设> (检查条件)
        -------- 设置传递参数变量 --------
        设置 GobUnitActor = (凶手单位)
        设置 GobUnitTarget = (死亡单位)
        -------- 获取单位的某些行为 --------
        触发器 - 运行GobStackPushThis<预设> (检查条件)
        触发器 - 运行GobStackPushCX<预设> (检查条件)
        触发器 - 运行 GobStackPushArray<预设> (检查条件)
        触发器 - 运行GobStackPushStr<预设> (检查条件)
        -------- 获取凶手单位的行为 --------
        设置 GobStr1 = <空字符串>
        触发器 - 运行UnitBehavior_s_GetBehaviors<预设> (检查条件)
        循环动作[GobInt3]从 1 到 GobIntArray[0], 运行 (Loop - 动作)
            Loop - 动作
                设置 this = GobIntArray[GobInt3]
                触发器 - 运行UnitBehavior_m_onKill[this] (检查条件)
        -------- 获取死亡单位的行为 --------
        设置 GobUnitActor = (死亡单位)
        设置 GobStr1 = <空字符串>
        触发器 - 运行UnitBehavior_s_GetBehaviors<预设> (检查条件)
        设置 GobUnitActor = (凶手单位)
        循环动作[GobInt3]从 1 到 GobIntArray[0], 运行 (Loop - 动作)
            Loop - 动作
                设置 this = GobIntArray[GobInt3]
                触发器 - 运行UnitBehavior_m_onDeath[this] (检查条件)
        触发器 - 运行 GobStackPopStr<预设> (检查条件)
        触发器 - 运行GobStackPopArray<预设> (检查条件)
        触发器 - 运行GobStackPopCX<预设> (检查条件)
        触发器 - 运行GobStackPopThis<预设> (检查条件)
        触发器 - 运行GobStackPopUnit<预设> (检查条件)
     
    这个触发器的内容并不复杂,撇开那一堆GobStack的调用以外,剩下的就只有分别获取凶手单位和死亡单位身上所有的单位行为,然后调用这些行为的onDeath和onKill方法。如果你没有设置行为的这些方法,那么什么事情都没有发生。如果你设置了,就会运行你所重写的动作。
    我这里只添加了单位死亡的事件而已。你也可以依葫芦画瓢为单位行为加上针对更多事件的监听和虚方法,比如攻击,施法等等。当然,如果你一股脑把这些事件全加上去了,最后你就会发现UnitBehavior变得过于庞大了,游戏里随便发生一件事情,都要运行一下行为的虚方法,而这些虚方法你可能大部分都没有进行任何重写。在后面的章节中我会介绍更为灵活的方法来处理这个问题。
    哈!于是乎,现在我们就可以用UnitBehavior来实现我们之前所做的弹幕和位移了!方法非常简单,只要把原来每隔0.01秒运行的动作都整合到重写onUpdate方法的触发器里,然后赋值给GameBehavior_m_onUpdate[this]里就ok了:
    Movement_s_Create
    事件
    条件
    动作
        -------- 创建单位行为 --------
        触发器 - 运行 GobStackPushReal<预设> (检查条件)
        设置 GobReal1 = 0.00
        触发器 - 运行 UnitBehavior_s_Create<预设> (检查条件)
        设置 GameBehavior_Tag[this] = Movement
        设置 UnitBehavior_DestroyOnDeath[this] = TRUE
        触发器 - 运行 GobStackPopReal<预设> (检查条件)
        设置 Movement_Distance[this] = GobReal1
        设置 Movement_Facing[this] = GobReal2
        设置 Movement_Speed[this] = GobReal3
        设置 Movement_Count[this] = 0.00
        设置 GameBehavior_m_onUpdate[this] = Movement_o_onUpdate<预设>
        设置 GameBehavior_m_Destroy[this] = Movement_o_Destroy<预设>
     Movement_o_onUpdate
    事件
    条件
    动作
        单位 - 设置 UnitBehavior_Object[this] 的X坐标为 ((UnitBehavior_Object[this] 所在X轴坐标) + (UnitBullet_Speed[this] x((Cos((UnitBehavior_Object[this] 的面向角度))) x 0.01)))
        单位 - 设置 UnitBehavior_Object[this] 的Y坐标为 ((UnitBehavior_Object[this] 所在Y轴坐标) + (UnitBullet_Speed[this] x((Sin((UnitBehavior_Object[this] 的面向角度))) x 0.01)))
        设置 Movement_Count[this] = (Movement_Count[this] +(Movement_Speed[this] x 0.01))
        如果(所有的条件成立) 则运行 (Then - 动作) 否则运行(Else - 动作)
            If - 条件
Movement_Count[this] 大于或等于 Movement_Distance[this]
            Then - 动作
                触发器 - 运行 GameBehavior_m_Destroy[this] (检查条件)
            Else - 动作
     UnitBullet_s_Create
    事件
    条件
    动作
        -------- 创建单位行为 --------
        触发器 - 运行 GobStackPushReal<预设> (检查条件)
        设置 GobReal1 = 0.00
        触发器 - 运行 UnitBehavior_s_Create<预设> (检查条件)
        设置 GameBehavior_Tag[this] = Bullet
        设置 UnitBehavior_DestroyOnDeath[this] = TRUE
        触发器 - 运行 GobStackPopReal<预设> (检查条件)
        设置 UnitBullet_Speed[this] = GobReal1
        设置 UnitBullet_Range[this] = GobReal2
        设置 UnitBullet_Radius[this] = GobReal3
        设置 UnitBullet_Damage[this] = GobReal4
        设置 UnitBullet_Count[this] = 0.00
        设置 UnitBullet_Point[this] = (UnitBehavior_Object[this] 的位置)
        设置 UnitBullet_Target[this] = 没有单位
        设置 UnitBullet_HitFlag[this] = FALSE
        设置 UnitBullet_DestroyFlag[this] = FALSE
        设置 AbstractBullet_m_Fly[this] = UnitBullet_o_Fly<预设>
        设置 AbstractBullet_m_Collide[this] = UnitBullet_o_Collide<预设>
        设置 AbstractBullet_m_Effect[this] = UnitBullet_o_Effect<预设>
        设置 GameBehavior_m_onUpdate[this] = UnitBullet_o_onUpdate<预设>
        设置 GameBehavior_m_Destroy[this] =UnitBullet_o_Destroy<预设>
     UnitBullet_o_onUpdate
    事件
    条件
    动作
        触发器 - 运行 AbstractBullet_m_Fly[this] (检查条件)
        触发器 - 运行 AbstractBullet_m_Collide[this] (检查条件)
        如果(所有的条件成立) 则运行 (Then- 动作) 否则运行 (Else - 动作)
            If - 条件
UnitBullet_HitFlag[this] 等于 TRUE
            Then - 动作
                触发器 - 运行 AbstractBullet_m_Effect[this] (检查条件)
            Else - 动作
        如果(所有的条件成立) 则运行 (Then- 动作) 否则运行 (Else - 动作)
            If - 条件
UnitBullet_DestroyFlag[this] 等于 TRUE
            Then - 动作
                触发器 - 运行 AbstractBullet_m_Destroy[this] (检查条件)
            Else - 动作
     更多详细的触发请在演示地图中查看。
   
    现在让我们来总结一下GameBehavior的好处都有啥(谁说对了就给他)。
    GameBehavior的第一个好处在于它让我们可以专心的去编写某个游戏行为的逻辑和效果,也就是各种虚方法,而不需要把注意力再放在实现诸如事件监听,在单位死亡的时候摧毁行为这种底层功能上面。当你今后心血来潮,想要快速的创建一个新的游戏行为的时候,你就不必为这么做还要再额外新建一个类,新建一堆计时器或者触发器之类的东西犯愁,只需要想想当各种事件发生的时候,这个行为要如何应对就ok了。
    第二个好处就是GameBehavior让我们有能力快速的把游戏中的各种过程抽象成对象。举个例子,我曾经见过很多人头疼该如何做出像暴风雪或者龙卷风那样的持续性技能。他们一直在头疼该如何检测单位持续施法结束的问题,并提出了诸如使用计时器,所有持续技能以龙卷风为模板(通过检测龙卷风召唤物来判断技能是否结束)等等的方法。但是,实际上BLZ提供了让你完整的检测一个持续技能的生命周期的事件,包括技能的准备,开始,释放,结束,停止。只不过当时大部分人(好像现在也差不多)都还没有接触过我们之前提到过的思路,他们既不能想到检测五个技能事件来完整的监视一个技能的生命周期,也不能想到该如何在五个触发器之间建立联系来确定它们监测的是同一个单位所释放的技能的生命周期。但是我们有了UnitBehavior,我们只需要让UnitBehavior能够检测技能释放的五个事件,就能够做到用一个UnitBehavior检测一个完整的技能释放生命周期。这样我们就可以用一个空白的持续性技能,再加上一个重写了技能各个阶段效果的行为,做出一个自定义的持续释放技能了。
    还有一个好处就是用GameBehavior派生出游戏中的所有行为,可以让我们更为方便的统一管理游戏中的所有行为,以及它们之间的互动。GetBehavior就是一个很好的例子,假如说我们没有将游戏中所有的行为统一为GameBehavior,我们要如何判断一个单位是否是弹幕或者是正在位移的单位呢?我们只能再为Bullet和Movement类增加静态方法来判断一个单位是否被作为参数创建了Bullet或者Movement对象。但是有了GetBehavior方法之后我们只需要统一所有弹幕和位移行为的Tag格式,就可以非常轻松的通过GetBehavior来检测一个单位是否具有某种类型的弹幕或者位移行为了。
   
    现在看来GameBehavior非常的强大,可以说是为我们敞开了一扇“面向行为”的新大门。不过它依然有众多可以改良的地方。毫不夸张的说,我们现在才刚刚发挥出GameBehavior的半分实力而已,所以下一章即将介绍的新设计模式,就将让GameBehavior发挥出它全部的实力。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2016-9-7 20:22:52 | 显示全部楼层
第七章:装饰者模式
         现在回忆一下我们在第五章提出来的问题,这个问题和我们在上一章提到过的太多的事件集中在UnitBehavior上的问题有相似之处——虽然我们可以通过让UnitBullet派生出子类的方法来扩展弹幕的类型,比如我可以重写UnitBullet的飞行方式,派生出HomingBullet(追踪弹幕),又或者是我可以重写UnitBullet的效果,派生出AbilityBullet(击中敌人后创建马甲放技能的弹幕),但是一旦我需要一种既能追踪,又能击中敌人产生技能效果的弹幕的时候,还是没办法,只能再派生一个HomingAbilityBullet(追踪+技能效果弹幕)——因为HomingBulletAbilityBullet是两个不同的类,它们的构造方法只能创建出具有各自重写方法的对象,无法组合在一起。而如果我们想利用HomingBullet或者AbilityBullet已经实现的效果的话,一个类也只能继承自一个父类,也就是说我们要么继承HomingBullet,要么继承AbilityBullet,无法两者同时使用。
         哦!想一想,假设我们目前已经有3个重写了飞行方式的弹幕类,3个重写了碰撞检测方式的弹幕类,和3个重写了弹幕效果的弹幕类,要把它们组合起来就会需要新建3x3x3 = 27个类……画面太美不敢看。

         有没有什么办法能够让我们把重写的方法组合起来?答案就是装饰者模式。当然,这里提到的装饰者模式和真正的设计模式中所提到的设计模式有一定的差别,而且这是一个固定的套路,所以我就先不解释原理,让我们来看看装饰者模式应该如何应用:
1.      首先,新建一个装饰者类。我这里以AccelBullet为例,来看看它的触发器注释吧:
class AccelBullet : UnitBullet
{
属性:
    Real Accel(加速度)
   Real Max(最大速度)
   Real Min(最小速度)

   Trigger LastDestroy(所装饰的UnitBullet的旧的析构方法)
   Trigger LastFly(所装饰的UnitBullet的旧的飞行方法)
方法:
   static Create(this(修饰的UnitBullet对象),GobReal1(加速度),GobReal2(最大速度),GobReal3(最小速度))->this
   override Destroy()

   override Fly()
触发器:
}

嗯……可以看出来一些差别。首先我们多了两个触发器属性,分别是LastDestroy和LastFly。然后Create方法的参数莫名的减少了,但是增加了一个参数是this,同时这个构造方法本身返回的也是this……迷。
2.      让我们来看看方法究竟发生了什么变化:
AccelBullet_s_Create
事件
条件
动作
设置 AccelBullet_Accel[this] = GobReal1
设置 AccelBullet_Max[this] = GobReal2
设置 AccelBullet_Min[this] = GobReal3
设置 AccelBullet_LastFly[this] =UnitBullet_m_Fly[this]
设置 UnitBullet_m_Fly[this] = AccelBullet_o_Fly<预设>
设置 AccelBullet_LastDestroy[this] =GameBehavior_m_Destroy[this]
设置 GameBehavior_m_Destroy[this] =AccelBullet_o_Destroy <预设>

AccelBullet_o_Fly
事件
条件
动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
           If - 条件
                UnitBullet_Speed[this] 小于 AccelBullet_Max[this]
                UnitBullet_Speed[this] 大于 AccelBullet_Min[this]
           Then - 动作
设置 UnitBullet_Speed[this] =(UnitBullet_Speed[this] + (AccelBullet_Accel[this] x 0.01))
           Else - 动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                    If - 条件
                        UnitBullet_Speed[this] 大于 AccelBullet_Max[this]
                    Then - 动作
设置 UnitBullet_Speed[this] =AccelBullet_Max[this]
                    Else - 动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                    If - 条件
                        UnitBullet_Speed[this] 小于 AccelBullet_Min[this]
                    Then - 动作
设置 UnitBullet_Speed[this] =AccelBullet_Min[this]
                    Else - 动作
触发器 - 运行 AccelBullet_LastFly[this] (检查条件)

AccelBullet_o_Destroy
事件
条件
动作
设置 AccelBullet_Accel[this] = 0.00
设置 AccelBullet_Max[this] = 0.00
设置 AccelBullet_Min[this] = 0.00
触发器 - 运行 AccelBullet_LastDestroy[this] (检查条件)

嗯……好像稍微能理解一点了。首先是Create方法,Create方法以this作为参数,设置了this引用的对象的属性,将this的旧的虚方法重写保存起来了,然后将虚方法的重写换成了自己的重写。
也就是说Create方法其实并没有创建一个新的对象,而是对作为参数的this引用的对象进行了“装饰”,给它添加了一些额外的新属性和重写了它的方法。

那么Fly和Destroy方法呢?实际上我们可以把这两个方法的动作看成是两个部分——一部分是AccelBullet自己的部分,另外一部分是“之前所加工的对象”的。AccelBullet的Fly是先运行了自己的部分,再运行了老对象的Fly。Destroy同理。这种效果等于就是将老对象的虚方法实现扩写”了,添加了自己的部分,同时不影响老对象对这个方法的重写部分。

3.      让我们来整理一下在一个AccelBullet的生命周期中究竟发生了一些什么:
a)        运行UnitBullet_s_Create
                                      i.             UnitBullet_s_Create调用了其父类的构造方法UnitBehavior_s_Create,创建了一个单位行为对象
                                    ii.             UnitBullet_s_Create对这个被构造的父类对象进行自身的构造,为弹幕属性赋值,并重写Destroy,onUpdate,Fly等方法。在这之后这个单位行为才会变成一个弹幕行为,可以被视为UnitBullet。
b)        运行AccelBullet_s_Create对UnitBullet_s_Create所返回的弹幕对象this进行装饰”,为它添加新的加速度属性以及对于Fly等方法的重写。这个时候this对象就可以被视为AccelBullet类型的对象了。
c)        GameBehavior_t_TimePeriodic检测到游戏时间过去0.01秒事件,运行UnitBullet所重写的onUpdate方法。
                                      i.             UnitBullet_o_onUpdate调用了Fly方法
                                    ii.             现在this对象的Fly方法被AccelBullet_o_Fly重写了,这个方法先运行了自身的部分,
                                   iii.             然后这个方法又运行了保存在LastFly中的老对象对于UnitBullet_m_Fly的重写,这样就实现了“累加弹幕的飞行速度→计算弹幕的飞行轨迹”的复合过程。
                                   iv.             UnitBullet_o_onUpdate调用了剩下的Collide和Effect方法,它们都与AccelBullet无关。
d)        当弹幕飞行到尽头或者撞上单位的时候,DestroyFlag 等于 true,onUpdate方法调用了GameBehavior_m_Destroy
                                      i.             此时重写GameBehavior_m_Destroy的是AccelBullet_o_Destroy,这个方法先将this对象作为AccelBullet的属性给清空了,然后调用了LastDestroy
                                    ii.             LastDestroy中装载的触发器是UnitBullet_o_Destroy,这个方法清空了this对象作为UnitBullet的属性,杀死了单位Object,然后调用了UnitBehavior_o_Destroy
                                   iii.             UnitBehavior_o_Destroy清空了this对象作为UnitBehavior的属性,并将自身从单位的行为列表中删除了,然后调用了GameBehavior_o_Destroy
                                   iv.             GameBehavior_o_Destroy清空了this对象作为GameBehavior的属性,并最终调用了Obj_GameBehavior_Deallocate,回收了this对象的ID,正式把这个对象给销毁了。

这个过程……看上去好像有点复杂,这就需要你自己慢慢体会了。总而言之,这就是装饰者模式的套路,让我们总结一下吧:
1.  新建一个继承自所需要修饰的类的子类
2.  添加你希望给所修饰类添加的属性和方法(没错,方法也可以加),并为你希望扩写的方法新建用于存储它们的旧的实现方法触发器的触发器数组变量。
3.  新建Create方法的时候要注意,装饰类不需要调用父类的Create方法,因为它并不是要新建一个对象,而是要对已有的对象进行修饰。然后为修饰对象的新属性赋值,用之前添加的触发器属性保存修饰对象对于所需要扩写的方法的旧的实现,并将对应虚方法重新赋值为自身对其的实现。
4.  在扩写的方法的新实现中,添加自己所要运行的动作和调用所保存的老对象对虚方法的实现。

这就是这个设计模式的套路。它的好处就在于当我们希望给一个对象添加很多的功能,但是这些功能又不一定每一次都能用上的时候,就可以将这些功能转移给派生出的各种装饰类。然后在我们需要这些装饰功能的时候,对一个白板对象进行修饰就可以让它获得这些额外的功能了。
之前某人跟我提过,他把弹幕的功能都整合在一个类里面,结果搞得弹幕的参数很多,想合并参数的功能然后删减掉一些参数。但是现在看来的话,这不是完全没有必要嘛,装饰者模式可以非常完美的解决这个问题!

    向你们展示一下在我的地图中,已经有了多少对于弹幕和单位行为的装饰:
    (当然, 贴吧的读者估计是看不到的,因为我的相册莫名抽风了,死活打不开。不过在文档里这三张图片足足占了三页纸,很长)
如此之多的功能,都可以随意的自由组合!也就是说如果我心血来潮想要做一个自带追踪,越飞越快,碰到障碍物会被销毁,效果为对范围内的满足特定条件的单位创建马甲释放技能的弹幕,根本就花不了几分钟——我甚至连一个新的动作都不用新建,只需要从各个API触发器中把早就写好了的动作复制粘贴过来,然后改改参数就齐活了!这种要什么来什么的感觉,是不是棒极了?

    同样的模式也可以使用在UnitBehavior上面。之前我们不是嫌太多的事件都被集中在了UnitBehavior上,但是却不一定每一个UnitBehavior都需要用到这些功能吗?用装饰模式给UnitBehavior动态的添加这些功能就OK了!当然,死亡是每一个单位都要面临的,所以这个事件可以留给UnitBehavior,但是有些单位可能无法攻击,有些单位可能没有技能,有些单位可能不是英雄不能升级和复活,所以这些攻击事件,施法事件,英雄事件……都可以分配给装饰类来实现。
以单位施法为例,我来做一个演示:

class UBSpell : UnitBehavior
{
属性:
    Trigger LastDestroy

    static HashTableBehaviorList
方法:
    staticCreate(this)->this
    override Destroy()

    virtualonSpellPrepare()
    virtual onSpellStart()
    virtualonSpellEffect()
    virtual onSpellEnd()
    virtual onSpellStop()

    staticGetBehavior(GobUnitActor(要获取行为的单位),GobStr1(要获取的行为Tag))->GobInt1(搜索到的行为)
    staticGetBehaviors(GobUnitActor(要获取行为的单位),GobStr1(要获取的行为Tag))->GobIntArray(搜索到的行为列表)
触发器:
    onInit(地图初始化)
    UnitSpellPrepare(任意单位准备施法)
    UnitSpellStart(任意单位开始施法)
    UnitSpellEffect(任意单位发动效果)
    UnitSpellEnd(任意单位结束施法)
    UnitSpellStop(任意单位停止施法)
}
     
     UBSpell_s_Create
    事件
    条件
    动作
        设置 UBSpell_m_onSpellPrepare[this] = DoNothing <预设>
        设置 UBSpell_m_onSpellStart[this] = DoNothing <预设>
        设置 UBSpell_m_onSpellEffect[this] = DoNothing <预设>
        设置 UBSpell_m_onSpellEnd[this] = DoNothing <预设>
        设置 UBSpell_m_onSpellStop[this] = DoNothing <预设>
        设置 UBSpell_LastDestroy[this] = GameBehavior_m_Destroy[this]
        设置 GameBehavior_m_Destroy[this] = UBSpell_o_Destroy <预设>
        -------- 将新建的行为添加到单位的行为列表 --------
        设置 GobHashTable = UBSpell_s_BehaviorList
        触发器 - 运行 UnitBehavior_AddToList <预设> (检查条件)
   
     UBSpell_o_Destroy
    事件
    条件
    动作
        设置 GobHashTable = UBSpell_s_BehaviorList
        触发器 - 运行 UnitBehavior_RemoveFromList <预设> (检查条件)
        -------- ↑将销毁的行为从单位的行为列表中删除 --------
        设置 UBSpell_m_onSpellPrepare[this] = DoNothing <预设>
        设置 UBSpell_m_onSpellStart[this] = DoNothing <预设>
        设置 UBSpell_m_onSpellEffect[this] = DoNothing <预设>
        设置 UBSpell_m_onSpellEnd[this] = DoNothing <预设>
        设置 UBSpell_m_onSpellStop[this] = DoNothing <预设>
        触发器 - 运行 UBSpell_LastDestroy[this] (检查条件)
     
     UBSpell_s_GetBehavior
    事件
    条件
    动作
        设置 GobHashTable = UBSpell_s_BehaviorList
        触发器 - 运行 UnitBehavior_s_GetBehaviorFromList<预设> (检查条件)
     
     UBSpell_s_GetBehaviors
    事件
    条件
    动作
        设置 GobHashTable = UBSpell_s_BehaviorList
        触发器 - 运行 UnitBehavior_s_GetBehaviorsFromList<预设> (检查条件)
     
     UBSpell_t_UnitSpellPrepare
    事件
        单位 - 任意单位 准备施放技能
    条件
    动作
        触发器 - 运行 GobStackPushUnit <预设> (检查条件)
        -------- 设置传递参数变量 --------
        设置 GobUnitActor = (施法单位)
        设置 GobUnitTarget = (技能施放目标)
        设置 GobAbility = (施放技能)
        设置 GobPointX = (GobUnitActor 的位置)
        设置 GobPointY = (技能施放点)
        -------- 获取单位的某些行为 --------
        触发器 - 运行 GobStackPushThis <预设> (检查条件)
        触发器 - 运行 GobStackPushCX <预设> (检查条件)
        触发器 - 运行 GobStackPushArray <预设> (检查条件)
        触发器 - 运行 GobStackPushStr <预设> (检查条件)
        -------- 获取施法单位的行为 --------
        设置 GobStr1 = <空字符串>
        触发器 - 运行 UBSpell_s_GetBehaviors <预设> (检查条件)
        循环动作[GobInt3]从 1 到GobIntArray[0], 运行 (Loop - 动作)
            Loop - 动作
                设置 this = GobIntArray[GobInt3]
                如果(所有的条件成立) 则运行 (Then- 动作) 否则运行 (Else - 动作)
                    If - 条件
                        UBSpell_m_onSpellPrepare[this]不等于 DoNothing <预设>
                    Then - 动作
                        触发器 - 运行UBSpell_m_onSpellPrepare[this] (检查条件)
                    Else - 动作
        触发器 - 运行 GobStackPopStr <预设> (检查条件)
        触发器 - 运行 GobStackPopArray <预设> (检查条件)
        触发器 - 运行 GobStackPopCX<预设> (检查条件)
        触发器 - 运行 GobStackPopThis <预设> (检查条件)
        点 - 清除 GobPointX
        点 - 清除 GobPointY
        触发器 - 运行 GobStackPopUnit <预设> (检查条件)
     哦,剩下还有4个触发器做的是跟UBSpell_t_UnitSpellPrepare差不多的事情,只不过它们的事件分别是技能开始,发动效果,技能结束,和停止施法,调用的方法也不同而已。
    如你所见,这就是我创建的一个UnitBehavior的装饰类,UnitBehavior在被UBSpell装饰之后,就会获得会在单位释放技能的时候被调用的虚方法。我们还可以依葫芦画瓢做出更多的事件装饰类,比如我就做了这么多:
     这种事件监控类也可以和我们之前提到过的自定义事件相结合,比如我自定义了一个UBBullet类,这个类可以检测到发射弹幕,弹幕击中敌人。把这个类和检测单位收到伤害的类结合在一起,就可以做到模拟远程攻击(检测到伤害→抵消原伤害→发射弹幕→弹幕击中敌人,对敌人造成抵消的攻击伤害)。对于很多人来说这应该是求之不得的东西,然而实际上做到这个东西,只花了我大约半个小时而已。
    另外演示一下怎样用UnitBehavior实现一个技能,并且用这种方法实现的技能能够完美的解决我们在第四章里面提到过的“技能无法多人释放”的问题:
    classSkillAMBlizzard : UnitBehavior
{
属性:
    Location TargetPoint(技能释放点)
    Bool IsSpelling(是否在释放对应的技能)
方法:
    staticCreate(GobUnitActor(拥有技能的单位))->this
    override Destroy()

    override onUpdate()
    overrideonSpellEffect()
    override onSpellStop()
触发器:
}

SkillAMBlizzard_s_Create
事件
条件
动作
       -------- 创建单位行为 --------
触发器 - 运行 GobStackPushReal <预设> (检查条件)
设置 GobReal1 = 0.00
触发器 - 运行 UnitBehavior_s_Create <预设> (检查条件)
设置 GameBehavior_Tag[this] = SkillBlizzard
触发器 - 运行 GobStackPopReal <预设> (检查条件)
       -------- 为单位行为附加施法事件 --------
触发器 - 运行 UBSpell_s_Create <预设> (检查条件)
设置 SkillAMBlizzard_TargetPoint[this] =(UnitBehavior_Object[this] 的位置)
设置 SkillAMBlizzard_IsSpelling[this] = FALSE
设置 GameBehavior_m_onUpdate[this] =SkillAMBlizzard_o_onUpdate <预设>
设置 UBSpell_m_onSpellEffect[this] =SkillAMBlizzard_o_onSpellEffect <预设>
设置 UBSpell_m_onSpellStop[this] =SkillAMBlizzard_o_onSpellStop <预设>
设置 GameBehavior_m_Destroy[this] =SkillAMBlizzard_o_Destroy <预设>

SkillAMBlizzard_o_Destroy
事件
条件
动作
设置 SkillAMBlizzard_IsSpelling[this] = FALSE
点 - 清除 SkillAMBlizzard_TargetPoint[this]
触发器 - 运行 UBSpell_o_Destroy <预设> (检查条件)

SkillAMBlizzard_o_onSpellEffect
事件
条件
       GobAbility 等于暴风雪
动作
设置 SkillAMBlizzard_IsSpelling[this] = TRUE
点 - 移动 SkillAMBlizzard_TargetPoint[this] 到((GobPointY 的X轴坐标),(GobPointY 的Y轴坐标))

SkillAMBlizzard_o_onSpellStop
事件
条件
       GobAbility 等于暴风雪
动作
设置 SkillAMBlizzard_IsSpelling[this] = FALSE

SkillAMBlizzard_o_onUpdate
事件
条件
动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
           If - 条件
                SkillAMBlizzard_IsSpelling[this]等于 TRUE
                Or - 任意条件成立
条件
                       (GameBehavior_Time[this] mod 1.00) 小于 (1.00 / 1000.00)
                       (GameBehavior_Time[this] mod 1.00) 大于 (1.00 - 0.01)
           Then - 动作
特殊效果 - 删除 (新建特效Objects\Spawnmodels\Other\NeutralBuildingExplosion\NeutralBuildingExplosion.mdl在 SkillAMBlizzard_TargetPoint[this] 处)
           Else - 动作

这个技能的效果非常简单,就是单纯的给大魔法师的暴风雪技能加上每秒触发一次的爆炸特效而已(爆炸即艺术)。这个技能支持多人释放,并且在暴风雪技能被打断或者结束之后,爆炸特效也会停止产生,真正的做到了完整的检测一个技能的生命周期。当然这个制作技能的模式还可以进行优化——目前UBSpell会捕捉所有的技能释放,你可以选择派生一个Skill类来特化UBSpell的功能,使其能够捕捉你所指定的特定技能,并省去判断单位目前是否在施法,onUpdate多久真正起效一次的问题。

但是这种模式也有一个问题,那就是要让技能行为起效的话,就必须把它附加在你想要让它起效的单位身上。所以你必须捕捉所有进入地图的具有某种类型技能的单位来为它们添加对应的行为,或者手动设置已经放置在地图上的单位。这种事情还算是比较轻松的,假如在你的游戏中单位可能动态的获得技能的话,要捕捉单位动态获得技能事件还会变的更加麻烦。这个问题目前我也还没想出来答案,可能会需要用到纯T以外的技术来处理。

         装饰者模式只是面向对象的诸多设计模式的其中一种而已,如果你有兴趣可以搜索一下进行了解(嗯……尽管我觉得,没有基础的话不一定能看懂)。由于War3本身就帮你写好了整个游戏,所以WE编程,大部分时间都只是在给游戏添加额外效果而已,对于程序设计的要求并不会真正跟从头写一个游戏一样那么严格,所以我也只是介绍了我认为最为适用于WET编程的一种模式而已。
         原本我是打算写到这一节就结束了,但是考虑到一些额外的内容会对其他人有所帮助,所以我打算再写个两节。下一节我会开始介绍如何优化GameBehavioronUpdate调用算法,免得你的游戏里同时有几百个onUpdate在跑变得太卡。最后一节将会以介绍面向对象的一些原则和思路结尾。

回复 支持 反对

使用道具 举报

 楼主| 发表于 2016-9-7 20:23:29 | 显示全部楼层
第八章:行为改良
         大部分人都不会遇上这个问题——当你的地图里同时有几百个行为存在,每0.01秒就调用一次它们的onUpdate方法,1秒钟运行数万个触发器,你就会开始体会到什么叫做卡。这就是我们在这一节的主题——怎样在尽可能小的影响游戏演出效果的前提条件下,让游戏中存在尽可能多的onUpdate

         目前我们的onUpdate执行机制非常简单粗暴,就是单纯的每隔0.01秒就执行一次所有GameBehavioronUpdate方法而已。所以对应的,也有一个很简单粗暴的方法来优化这个问题——不再每隔0.01秒就执行一次所有GameBehavioronUpdate,改成0.02秒不就好了?假如你的地图里原本可以容纳100GameBehavior0.01秒执行一次onUpdate,那么改成0.02秒就可以容纳200个,效果拔群。
         当然,你可能会觉得200个还不够,又把0.02改成了0.03,这样就可以容纳300多个GameBehavior了。
如果你观察得仔细,就会发现我在每次计算位移和弹幕的移动的时候,都在速度的后面乘了一个0.01,这就表示我为位移和弹幕设置的速度都是以每秒为单位进行计算的,在onUpdate每隔0.01秒执行一次的情况下,要给速度乘以0.01才能正确让弹幕每秒飞行我所设置的速度。所以当你把onUpdate的执行间隔从0.01改成0.02的时候,如果你不把弹幕和位移onUpdate里的0.01换成0.02,弹幕和位移的速度就都会变慢2倍。诶,真是一件麻烦事。
假如我们要频繁的更改onUpdate的执行间隔的话,这就更麻烦了。所以我们不妨把onUpdate的执行间隔设置成一个变量,将所有的0.01都替换成这个变量,这样的话我们就可以随意调整所有onUpdate的执行间隔了:

在变量管理器中声明一个叫做GameBehavior_s_Period的变量。
然后把GameBehavior_t_TimePeriodic中的事件删除,因为在这里设置的事件并不能很方便的调整其时间间隔。
取而代之的是,我们新建了一个触发器叫做GameBehavior_t_onInit,其事件为地图初始化,动作为为GameBehavior_s_Period赋值,以及为GameBehavior_t_TimePeriodic添加每当游戏经过GameBehavior_s_Period秒的事件:

GameBehavior_t_onInit
事件
地图初始化
条件
动作
设置 GameBehavior_s_Period = 0.02
触发器 - 为 GameBehavior_t_TimePeriodic <预设>添加事件: (时间 - 每当游戏逝去 GameBehavior_s_Period 秒)

然后就是把所有之前用到过0.01秒的地方都修改成GameBehavior_s_Period……因为这个属性只在onUpdate方法中有用,所以我们只要搜索onUpdate就能找到所有可能用到过0.01的触发器了,不算很麻烦。
Ok,这样当你觉得地图因为onUpdate刷新得太频繁而变卡的时候,就可以在GameBehavior_t_onInit中修改GameBehavior_s_Period的初始值,来改变其刷新周期了。

然而这个方法,很蠢。
虽然你的游戏中可能会有300个GameBehavior,将GameBehavior_s_Period设置为0.03刚好可以让你的游戏变得不卡,但是你的游戏会每时每刻都充满了300个行为吗?这应该是不一定的。当然0.03秒和0.01秒差别不大,人的肉眼都很难发现差别,但是如果是0.05或者是0.10呢?差别就体现出来了——你可能会为了游戏中仅仅可能存在几秒钟的性能优化而牺牲整场游戏的视觉效果,这就很不值得了。
所以说让我们来另外想一个更加灵活的方法吧——一个可以根据游戏中的GameBehavior数量来自动调节刷新间隔的方法。

当然,首先你要明白,当你为GameBehavior_t_TimePeriodic添加完了每当游戏过去GameBehavior_s_Period秒的事件之后,无论你怎么修改,GameBehavior_t_TimePeriodic的刷新间隔都不会改变——这个跟War3事件的原理有关,不做详述。所以我们会需要一个新的方法来周期性的运行GameBehavior_t_TimePeriodic——用计时器:

GameBehavior_t_onInit
事件
地图初始化
条件
动作
设置 GameBehavior_s_Period = 0.01
触发器 - 为 GameBehavior_t_TimePeriodic <预设>添加事件: (时间 - GameBehavior_s_CentralTimer 到期)
计时器 - 启动 GameBehavior_s_CentralTimer,应用计时方式: 一次性,计时周期为 GameBehavior_s_Period 秒

GameBehavior_t_TimePeriodic
事件
条件
动作
触发器 - 运行 GobStackPushThis <预设> (检查条件)
触发器 - 运行 GobStackPushCX <预设> (检查条件)
循环动作[GobInt3]从 1 到 Obj_GameBehavior_Num, 运行 (Loop - 动作)
            Loop- 动作
设置 this = GobInt3
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                   If - 条件
                       Obj_GameBehavior[this] 小于 0
                   Then - 动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                            If - 条件
                               GameBehavior_Time[this] 小于或等于 0.00
                            Then - 动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                                    If - 条件
                                        GameBehavior_m_onStart[this]不等于 DoNothing <预设>
                                    Then - 动作
触发器 - 运行 GameBehavior_m_onStart[this] (检查条件)
                                    Else - 动作
                            Else - 动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                                    If - 条件
                                       GameBehavior_m_onUpdate[this] 不等于 DoNothing <预设>
                                    Then - 动作
触发器 - 运行 GameBehavior_m_onUpdate[this] (检查条件)
                                    Else - 动作
设置 GameBehavior_Time[this] = (GameBehavior_Time[this] +0.01)
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                            If - 条件
                                GameBehavior_Duration[this] 大于 0.00
                               GameBehavior_Time[this] 大于或等于 GameBehavior_Duration[this]
                            Then - 动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                                    If - 条件
                                       GameBehavior_m_onEnd[this] 不等于 DoNothing <预设>
                                    Then - 动作
触发器 - 运行 GameBehavior_m_onEnd[this] (检查条件)
                                    Else - 动作
触发器 - 运行 GameBehavior_m_Destroy[this] (检查条件)
                            Else - 动作
                   Else - 动作
触发器 - 运行 GobStackPopCX <预设> (检查条件)
触发器 - 运行 GobStackPopThis <预设> (检查条件)
计时器 - 启动 GameBehavior_s_CentralTimer,应用计时方式: 一次性,计时周期为 GameBehavior_s_Period 秒

这样就可以做到你随时设定GameBehavior_s_Period,GameBehavior_t_TimePeriodic的刷新间隔就会随时改变,并且你的弹幕也不会因为Period的修改而变快或者变慢。
接下来的问题就是Period要怎么修改才好?
有一个很简单的方法——在每次TimePeriodic运行的时候,统计这一次究竟运行了多少次onUpdate,然后乘以一个合适的系数,就能够根据当前有的onUpdate数量来调整TimePeriodic的运行间隔了:

GameBehavior_t_TimePeriodic
事件
条件
动作
设置 GameBehavior_s_Count = 0.00
触发器 - 运行 GobStackPushThis <预设> (检查条件)
触发器 - 运行 GobStackPushCX <预设> (检查条件)
循环动作[GobInt3]从 1 到 Obj_GameBehavior_Num, 运行 (Loop - 动作)
            Loop- 动作
设置 this = GobInt3
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                   If - 条件
                       Obj_GameBehavior[this] 小于 0
                   Then - 动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                            If - 条件
                               GameBehavior_Time[this] 小于或等于 0.00
                            Then - 动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                                    If - 条件
                                       GameBehavior_m_onStart[this] 不等于 DoNothing <预设>
                                    Then - 动作
触发器 - 运行 GameBehavior_m_onStart[this] (检查条件)
                                    Else - 动作
                            Else - 动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                                    If - 条件
                                        GameBehavior_m_onUpdate[this] 不等于 DoNothing <预设>
                                    Then - 动作
触发器 - 运行 GameBehavior_m_onUpdate[this] (检查条件)
设置 GameBehavior_s_Count = (GameBehavior_s_Count +GameBehavior_Complexity[this])
                                   Else - 动作
设置 GameBehavior_s_Count = (GameBehavior_s_Count + 0.01)
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                            If - 条件
                               GameBehavior_Duration[this] 大于 0.00
                                GameBehavior_Time[this] 大于或等于 GameBehavior_Duration[this]
                            Then - 动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                                    If - 条件
                                       GameBehavior_m_onEnd[this] 不等于 DoNothing <预设>
                                    Then - 动作
触发器 - 运行 GameBehavior_m_onEnd[this] (检查条件)
                                    Else - 动作
触发器 - 运行 GameBehavior_m_Destroy[this] (检查条件)
                            Else - 动作
                   Else - 动作
触发器 - 运行 GobStackPopCX <预设> (检查条件)
触发器 - 运行 GobStackPopThis <预设> (检查条件)
设置 GameBehavior_s_Period = (0.01 + ((GameBehavior_s_Count/ 50.00) x 0.01))
计时器 - 启动 GameBehavior_s_CentralTimer,应用计时方式: 一次性,计时周期为 GameBehavior_s_Period 秒

嗯,你可能会对这个GameBehavior_Complexity是什么东西感到有些疑惑……这个东西的默认值为1,你不妨先把它看成是1,再来看看这个触发器。
啊,并不复杂嘛。在循环所有GameBehavior之前,先将Count重置为0,然后只要有一个GameBehavior调用了onUpdate,就将Count+1,否则的话就只加0.01,最后Period = 0.01+(Count/50)*0.01,意思就是说Period的初始值等于0.01,在这个基础上每增加50个有onUpdate的GameBehavior(100个没有onUpdate的GameBehavior等于1个有onUpdate方法的GameBehavior)就将刷新间隔+1。
那么这样Compelxity就好理解了——Complexity的字面意思是复杂度,代表的是这个GameBehavior的onUpdate方法执行一次相当于执行多少个标准的onUpdate方法。比如说位移的onUpdate方法很简单,就是移动单位而已。而弹幕的onUpdate方法要更复杂一些,包含了位移和选取周围的单位,后者的复杂度要比位移高得多。所以如果我们假设位移的onUpdate方法的复杂度是1的话,弹幕的onUpdate方法的复杂度就可能是2~2.5。更有可能的是,弹幕的onUpdate方法还有可能被一些对象上附加的修饰给扩写了,在修饰类的Create方法里,我们还要增加Complexity的值。比如给弹幕附加了一个AccelBullet,就扩写了onUpdate方法,可能就需要给这个弹幕行为的Complexity+0.5。

这个方法提供的优化效果非常灵活。我在演示地图中以弹幕作为演示,弹幕数量较少时刷新间隔维持在0.01左右,完全感觉不到任何卡顿,而在弹幕数量多的时候,刷新间隔会根据弹幕的数量自动调整,虽然弹幕的移动看上去有一些不连贯,但是不会影响到玩家的操作,并且此时屏幕上的弹幕足有200发,纵使一发弹幕看上去会稍微有些不连贯,在几百发弹幕面前这样的瑕疵也不会显得那么明显了。

通常来讲,使用这样的优化机制就已经足够了。但是在有些情况下你可能会有一些特殊的需求,所以说在下面我再提出一些可以更进一步进行优化的方法。

分批刷新:
对于其他类型的游戏来讲这个现象可能并不明显,但是对于同屏幕几百个弹幕交替出现的游戏来讲,这就很明显了——之前我们延长onUpdate刷新间隔的策略虽然避免了随着GameBehavior增多,在一秒内调用onUpdate方法的次数飙升,但是却没有解决大量的onUpdate方法被集中在同一次TimePeriodic运行中的问题。这将会导致当弹幕增多,TimePeriodic的运行间隔增长到0.x秒的时候,突然所有onUpdate被全部运行产生间歇性卡顿。另外对于同样在这0.x秒钟内被刷出的弹幕而言,尽管它们可能不是在同一时间被刷出,是应该以不同的次序飞行的,但是由于onUpdate每隔0.x秒才全部运行一次,不管是离0.x秒相差0.01秒刷出的弹幕还是相差0.x-0.01秒刷出的弹幕,都会等到这一次onUpdate方法调用的时候才飞出去:


如图所演示,弹幕出现了严重的分层现象——而实际上这是一个随机发射的弹幕,原本不应该有任何层次感。这些弹幕实际上是每隔0.01秒刷3发的速度被刷出来的,但是因为自身的onUpdate方法得不到及时的调用,导致所有在onUpdate刷新间隔中被刷出的弹幕实际上都在同一时间点飞了出去,而不是在不同的时间点上。

解决这个问题的方法就是我们仍然以0.01秒作为TimePeriodic的运行间隔,但是对TimePeriodic在0.01秒内能运行的onUpdate方法数量进行控制,这样既能避免在一秒内TimePeriodic运行太多的onUpdate导致游戏不流畅,也能避免所有onUpdate被集中在一次运行中,导致瞬间顿卡。
但是这个分批的算法会比较复杂……如果你看不懂的话,也不建议花时间去深究,直接把我的演示地图里的触发器复制到你的地图里就行了。具体的做法如下:
class GameBehavior
{
属性:
    String Tag(用于区别不同行为的标签)

    RealDuration(这个行为持续的时间,小于等于0则代表这个行为持续时间无限大)
    Real Time(这个行为从创建到现在已经过了多久了,如果超过Duration,则销毁这个行为自身)
    RealComplexity(复杂度,默认为1,用于优化onUpdate的调用)

    static RealPeriod(TimePeriodic的刷新间隔)
    static RealCount(用于统计一次TimePeriodic运行,运行了多少个onUpdate)
    static RealLimit(一次TimePeriodic最多能运行多少个onUpdate)
    static TimerCentralTimer(中央计时器,用于调用TimePeriodic)

    staticHashTable BatchList(用于存储每个批次等待的行为列表的哈希表)、
    static IntCurrentBatch(当前批次)
    Int Batch(所在的批次)
    IntBatchIndex(所在批次列表中的位置)
    static IntBatchSize(批次最大大小)
    static IntLastBatch(最后一次循环到的批次)
方法:
    staticCreate(GobReal1(行为持续时间))->this
virtual Destroy()

    virtualonStart()//这个虚方法只会被调用一次,会在行为开始计时的时候被调用
    virtualonUpdate()//这个虚方法每过0.01秒就会被调用一次
    virtualonEnd()//这个虚方法只会被调用一次,会在行为的Time超过Duration的时候被调用

    staticCreateBatch()//在等待批次中新建一个批次
    staticDestroyBatch(GobInt1(要销毁的等待批次))//使当前等待批次进入备用状态
    staticGetNextBatch()->GobInt1(下一个刷新批次)
   AddToBatch()//将当前GameBehavior加入到当前的等待批次中
   RemoveFromBatch()//将当前GameBehavior从其所属Batch中移除
触发器:
    TimePeriodic(每过Period秒)
    onInit(地图初始化)
}


GameBehavior_s_CreateBatch
事件
条件
动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
            If - 条件
                (在 GameBehavior_s_BatchList 的主索引 GameBehavior_s_CurrentBatch 子索引 0 内提取整数) 小于或等于 0
                (在 GameBehavior_s_BatchList 的主索引 0 子索引 GameBehavior_s_CurrentBatch 内提取整数) 小于 0
            Then- 动作
               -------- 如果目前的批次是激活的,又是空的,就不必新建批次了 --------
跳过剩余动作
            Else- 动作
设置 GameBehavior_s_CurrentBatch = (在 GameBehavior_s_BatchList 的主索引 0 子索引 0 内提取整数)
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
            If - 条件
               GameBehavior_s_CurrentBatch 大于 0
            Then- 动作
哈希表 - 在 GameBehavior_s_BatchList 的主索引 0 子索引 0 中保存整数 (在 GameBehavior_s_BatchList 的主索引 0 子索引 GameBehavior_s_CurrentBatch 内提取整数)
            Else- 动作
哈希表 - 在 GameBehavior_s_BatchList 的主索引 0 子索引 -1 中保存整数 ((在 GameBehavior_s_BatchList 的主索引 0 子索引 -1 内提取整数) + 1)
设置 GameBehavior_s_CurrentBatch = (在 GameBehavior_s_BatchList 的主索引 0 子索引 -1 内提取整数)
哈希表 - 在 GameBehavior_s_BatchList 的主索引 0 子索引 GameBehavior_s_CurrentBatch 中保存整数 -1
游戏 - 显示Debug信息: (|cff00ff00启动批次|r  + (转换 GameBehavior_s_CurrentBatch 为字符串))



GameBehavior_s_DestroyBatch
事件
条件
动作
游戏 - 显示Debug信息: (|cffff0000回收批次|r  + (转换 GameBehavior_s_CurrentBatch 为字符串))
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
            If - 条件
               GobInt1 大于 0
            Then- 动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                   If - 条件
                        (在 GameBehavior_s_BatchList 的主索引 0 子索引 GobInt1 内提取整数) 小于 0
                   Then - 动作
哈希表 - 在 GameBehavior_s_BatchList 的主索引 0 子索引 GobInt1 中保存整数 (在 GameBehavior_s_BatchList 的主索引 0 子索引 0 内提取整数)
哈希表 - 在 GameBehavior_s_BatchList 的主索引 0 子索引 0 中保存整数 GobInt1
哈希表 - 清空 GameBehavior_s_BatchList 中位于主索引 GobInt1  之内的所有数据
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                            If - 条件
                                GobInt1 等于 GameBehavior_s_CurrentBatch
                            Then - 动作
触发器 - 运行 GameBehavior_s_CreateBatch <预设> (检查条件)
                            Else - 动作
                   Else - 动作
            Else- 动作


GameBehavior_s_GetNextBatch
事件
条件
动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
            If - 条件
                (在 GameBehavior_s_BatchList 的主索引 0 子索引 -1 内提取整数) 大于 0
            Then- 动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                   If - 条件
                       GameBehavior_s_LastBatch 大于或等于 (在 GameBehavior_s_BatchList 的主索引 0 子索引 -1 内提取整数)
                   Then - 动作
设置 GameBehavior_s_LastBatch = 0
                   Else - 动作
设置 GobInt1 = (GameBehavior_s_LastBatch + 1)
设置 GameBehavior_s_LastBatch = GobInt1
            Else- 动作
设置 GameBehavior_s_LastBatch = 0
设置 GobInt1 = 0



GameBehavior_AddToBatch
事件
条件
动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
            If - 条件
               GameBehavior_s_CurrentBatch 小于或等于 0
            Then- 动作
触发器 - 运行 GameBehavior_s_CreateBatch <预设> (检查条件)
            Else- 动作
哈希表 - 在 GameBehavior_s_BatchList 的主索引 GameBehavior_s_CurrentBatch 子索引 0 中保存整数 ((在 GameBehavior_s_BatchList 的主索引 GameBehavior_s_CurrentBatch 子索引 0 内提取整数) + 1)
哈希表 - 在 GameBehavior_s_BatchList 的主索引 GameBehavior_s_CurrentBatch 子索引 (在 GameBehavior_s_BatchList 的主索引 GameBehavior_s_CurrentBatch 子索引 0 内提取整数) 中保存整数 this
设置 GameBehavior_Batch[this] = GameBehavior_s_CurrentBatch
设置 GameBehavior_BatchIndex[this] = (在 GameBehavior_s_BatchList 的主索引GameBehavior_s_CurrentBatch 子索引 0 内提取整数)
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
            If - 条件
                (在 GameBehavior_s_BatchList 的主索引 GameBehavior_s_CurrentBatch 子索引 0 内提取整数) 大于或等于 GameBehavior_s_BatchSize
            Then- 动作
触发器 - 运行 GameBehavior_s_CreateBatch <预设> (检查条件)
            Else- 动作
游戏 - 显示Debug信息: ((|cffffff00批次|r  + (转换 GameBehavior_s_CurrentBatch 为字符串)) + ( |cffffff00添加行为|r  +(转换 this 为字符串)))



GameBehavior_RemoveFromBatch
事件
条件
动作
游戏 - 显示Debug信息: ((|cffff00ff批次|r  + (转换 GameBehavior_Batch[this] 为字符串)) + ( |cffff00ff移除行为|r  +(转换 this 为字符串)))
哈希表 - 在 GameBehavior_s_BatchList 的主索引 GameBehavior_Batch[this] 子索引 GameBehavior_BatchIndex[this] 中保存整数 (在 GameBehavior_s_BatchList 的主索引 GameBehavior_Batch[this] 子索引 (在 GameBehavior_s_BatchList 的主索引 GameBehavior_Batch[this] 子索引 0 内提取整数) 内提取整数)
哈希表 - 在 GameBehavior_s_BatchList 的主索引 GameBehavior_Batch[this] 子索引 (在 GameBehavior_s_BatchList 的主索引 GameBehavior_Batch[this] 子索引 0 内提取整数) 中保存整数 0
哈希表 - 在 GameBehavior_s_BatchList 的主索引 GameBehavior_Batch[this] 子索引 0 中保存整数 ((在 GameBehavior_s_BatchList 的主索引 GameBehavior_Batch[this] 子索引 0 内提取整数) - 1)
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
            If - 条件
                (在 GameBehavior_s_BatchList 的主索引 GameBehavior_Batch[this] 子索引 0 内提取整数) 小于或等于 0
                (在 GameBehavior_s_BatchList 的主索引 0 子索引 GameBehavior_Batch[this] 内提取整数) 小于 0
            Then- 动作
触发器 - 运行 GobStackPushAX <预设> (检查条件)
设置 GobInt1 = GameBehavior_Batch[this]
触发器 - 运行 GameBehavior_s_DestroyBatch <预设> (检查条件)
触发器 - 运行 GobStackPopAX <预设> (检查条件)
            Else- 动作
设置 GameBehavior_Batch[this] = 0
设置 GameBehavior_BatchIndex[this] = 0


GameBehavior_s_Create
事件
条件
动作
触发器 - 运行 Obj_GameBehavior_Allocate <预设> (检查条件)
设置 GameBehavior_Duration[this] = GobReal1
设置 GameBehavior_Time[this] = 0.00
设置 GameBehavior_Complexity[this] = 1.00
设置 GameBehavior_Tag[this] = GameBehavior
设置 GameBehavior_m_onStart[this] = DoNothing <预设>
设置 GameBehavior_m_onUpdate[this] = DoNothing <预设>
设置 GameBehavior_m_onEnd[this] = DoNothing <预设>
设置 GameBehavior_m_Destroy[this] = GameBehavior_o_Destroy<预设>
触发器 - 运行 GameBehavior_AddToBatch <预设> (检查条件)


GameBehavior_o_Destroy
事件
条件
动作
触发器 - 运行 GameBehavior_RemoveFromBatch <预设> (检查条件)
设置 GameBehavior_Duration[this] = 0.00
设置 GameBehavior_Time[this] = 0.00
设置 GameBehavior_Complexity[this] = 0.00
设置 GameBehavior_Tag[this] = <空字符串>
设置 GameBehavior_m_onStart[this] = DoNothing <预设>
设置 GameBehavior_m_onUpdate[this] = DoNothing <预设>
设置 GameBehavior_m_onEnd[this] = DoNothing <预设>
设置 GameBehavior_m_Destroy[this] = DoNothing <预设>
触发器 - 运行 Obj_GameBehavior_Deallocate <预设> (检查条件)



GameBehavior_t_onInit
事件
地图初始化
条件
动作
设置 GameBehavior_s_BatchList = (新建哈希表)
设置 GameBehavior_s_Period = 0.01
设置 GameBehavior_s_BatchSize = 50
设置 GameBehavior_s_Limit = 25.00
设置 GameBehavior_s_LastBatch = 1
触发器 - 为 GameBehavior_t_TimePeriodic <预设>添加事件: (时间 - GameBehavior_s_CentralTimer 到期)
计时器 - 启动 GameBehavior_s_CentralTimer,应用计时方式: 一次性,计时周期为 0.01 秒





GameBehavior_t_TimePeriodic
事件
条件
动作
        -------- 重置计数器 --------
设置 GameBehavior_s_Count = 0.00
触发器 - 运行 GobStackPushThis <预设> (检查条件)
触发器 - 运行 GobStackPushReg <预设> (检查条件)
        -------- 开始循环批次-一次循环的批次数量不超过总批次 --------
循环动作[GobInt3]从 1 到 (在 GameBehavior_s_BatchList 的主索引 0 子索引 -1 内提取整数), 运行 (Loop - 动作)
            Loop- 动作
               -------- 获取下一个批次到GobInt1 --------
触发器 - 运行 GameBehavior_s_GetNextBatch <预设> (检查条件)
               -------- 批次处于激活状态 --------
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                   If - 条件
                       (在 GameBehavior_s_BatchList 的主索引 0 子索引 GobInt1 内提取整数) 小于 0
                    Then - 动作
                       -------- 获取当前批次的等待时间并重置 --------
设置 GameBehavior_s_Period = (在 GameBehavior_s_BatchList 的主索引 GobInt1 子索引 0 内提取实数)
哈希表 - 在 GameBehavior_s_BatchList 的主索引 GobInt1 子索引 0 中保存实数 0.00
                       -------- 循环同批次创建的GameBehavior --------
触发器 - 运行 GobStackPushCX <预设> (检查条件)
循环动作[GobInt3]从 1 到 (在 GameBehavior_s_BatchList 的主索引 GobInt1 子索引 0 内提取整数), 运行 (Loop - 动作)
                            Loop - 动作
设置 this = (在 GameBehavior_s_BatchList 的主索引 GobInt1 子索引 GobInt3 内提取整数)
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                                    If - 条件
                                       Obj_GameBehavior[this] 小于 0
                                    Then - 动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                                            If- 条件
                                               GameBehavior_Time[this] 小于或等于 0.00
                                           Then - 动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                                                   If -条件
                                                       GameBehavior_m_onStart[this] 不等于 DoNothing <预设>
                                                   Then - 动作
触发器 - 运行 GameBehavior_m_onStart[this] (检查条件)
                                                   Else - 动作
                                           Else - 动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                                                   If - 条件
                                                        GameBehavior_m_onUpdate[this]不等于 DoNothing <预设>
                                                   Then - 动作
触发器 - 运行 GameBehavior_m_onUpdate[this] (检查条件)
设置 GameBehavior_s_Count = (GameBehavior_s_Count +GameBehavior_Complexity[this])
                                                   Else - 动作
设置 GameBehavior_s_Count = (GameBehavior_s_Count + 0.01)
设置 GameBehavior_Time[this] = (GameBehavior_Time[this] +GameBehavior_s_Period)
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                                            If - 条件
                                               GameBehavior_Duration[this] 大于 0.00
                                               GameBehavior_Time[this] 大于或等于 GameBehavior_Duration[this]
                                           Then - 动作
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                                                   If - 条件
                                                       GameBehavior_m_onEnd[this] 不等于 DoNothing <预设>
                                                   Then - 动作
触发器 - 运行 GameBehavior_m_onEnd[this] (检查条件)
                                                   Else - 动作
触发器 - 运行 GameBehavior_m_Destroy[this] (检查条件)
                                           Else - 动作
                                    Else - 动作
触发器 - 运行 GobStackPopCX <预设> (检查条件)
                   Else - 动作
               -------- 每次至少循环一个批次。判断是否达到运行onUpdate方法的次数限制 --------
如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                   If - 条件
                       GameBehavior_s_Count 大于或等于 GameBehavior_s_Limit
                   Then - 动作
退出循环
                   Else - 动作
        -------- 累加所有批次的等待时间 --------
循环动作[GobInt3]从 1 到 (在 GameBehavior_s_BatchList 的主索引 0 子索引 -1 内提取整数), 运行 (Loop - 动作)
            Loop- 动作
哈希表 - 在 GameBehavior_s_BatchList 的主索引 GobInt3 子索引 0 中保存实数 ((在 GameBehavior_s_BatchList 的主索引 GobInt3 子索引 0 内提取实数) + 0.01)
触发器 - 运行 GobStackPopReg <预设> (检查条件)
触发器 - 运行 GobStackPopThis <预设> (检查条件)
        -------- 新建一个批次,下一次onUpdate方法调用前创建的行为都将被添加到其中 --------
触发器 - 运行 GameBehavior_s_CreateBatch <预设> (检查条件)
计时器 - 启动 GameBehavior_s_CentralTimer,应用计时方式: 一次性,计时周期为 0.01 秒

还是稍微说说思路吧:每次运行onUpdate都新建一个批次,在下一次onUpdate之间,所有新建的GameBehavior就都会被添加到这个批次中。如果当前批次已满,就新建新的批次。然后TimePeriodic每次循环的对象就会从GameBehavior换成批次,当然Count计数的规则还是相同的,但是对应的处理措施就不是增加Period了,而是当Count大于等于Limit的时候,循环就会被终止,本次TimePeriodic就结束运行了。结束的时候会记录本次循环终止于哪一个批次,保存到LastBatch,下一次接着LastBatch继续循环。如果循环到了BatchList的尽头,就重头开始继续循环。
实际上这种优化方法是经过两次改良的。上一次改良的思路是为了控制每0.01秒中运行的onUpdate数量,而对GameBehavior进行分批。这种思路相对更为简单,并且也可以避免弹幕分层的数量,但是这种方法会导致一些原本应该在同一批中被刷新的弹幕被分到不同的批次中刷新。比如我一次性创建100个弹幕以360度发射,如果限制每0.01秒只能运行25次onUpdate方法,就会导致这100个弹幕并不能形成完整的环形,而是会参差不齐的飞出去,这就破坏游戏的视觉效果了。所以这一次的改良中每隔0.01秒会新建一个批次,将所有在同一个瞬间被创建的行为添加进去,刷新的时候也是每次至少刷新一个批次,这样就不会因为分批的问题而导致原本应该被同时刷新的行为不再同时被刷新。

呃,但是搞了这么久,实际效果呢……好像并不能看出来很大的差别的样子(
因为一秒能能够流畅的运行的onUpdate的总量是不变的,所以这个方法在一定程度上还是能够起效的,但是当弹幕的数量增大的时候,该发生分层的还是会发生分层……所以弹幕或者行为的优化,最根本的关键还是在于让一秒能够流畅运行的onUpdate数量增多,这又是另外一回事了。

如果你选择直接复制我的触发去用的话,那你大可不必理解我在上面讲得这么一段话,只需要记住这几个要点就行了:
1.      Q:原本在同一时间点创建的行为,其onUpdate却没有在同一时间点运行,怎么办?
A:检查一下你在同一时间创建的行为数量是不是大于BatchSize,如果是的话就增大GameBehavior_s_BatchSize直到比你在游戏中最多同一时间创建的行为数量还要更大。
2.      Q:游戏玩起来还是有点不流畅怎么办?
A:你要么选择行为的刷新不流畅,但是游戏和操作本身流畅,要么就是选择两者一起不流畅。如果你选择前者的话,只需要不断的增大GameBehavior_s_Limit的值就ok了。
3.      Q:如果分组本身太多了,导致TimePeriodic花费在调度算法上的时间太长了,怎么办?
A:尽可能把GameBehavior的创建集中在一个时间点上,比如用每0.1秒创建10个行为来替换每0.01秒创建1一个行为,可以减少批的数量。不过要注意同一时间创建的行为数量最好不要大于GameBehavior_s_BatchSize。

         优先树刷新:
         有些时候你可能会想要控制行为之间onUpdate的顺序,比如你可能会希望有加血效果的onUpdate优先于有伤害效果的onUpate之前运行,这样单位就会先加血再受到伤害,而不是先受到伤害死亡,之后的血就加不上去了。
         那么我们就要解决两个问题:1.怎么给行为加上优先值的设定2.怎么对行为按照优先值进行排序,然后依次运行onUpdate方法。当然这两个问题都不是你解决,而是我已经帮你解决好了,你可以选择把我解决问题的方法搞懂了再拿去用,也可以选择不用搞懂直接拿去用。
         class GameBehavior
{
属性:
   String Tag(用于区别不同行为的标签)

   Real Duration(这个行为持续的时间,小于等于0则代表这个行为持续时间无限大)
   Real Time(这个行为从创建到现在已经过了多久了,如果超过Duration,则销毁这个行为自身)
   Real Complexity(复杂度,默认为1,用于优化onUpdate的调用)

   static Real Period(TimePeriodic的刷新间隔)
   static Real Count(用于统计一次TimePeriodic运行,运行了多少个onUpdate)
   static Timer CentralTimer(中央计时器,用于调用TimePeriodic)

   Real Priority(这个行为的优先级。只读属性,请不要修改它,因为这没有任何作用)
   GameBehavior Left(左边的子节点,优先级比这个节点更大)
   GameBehavior Right(右边的子节点,优先级比这个节点更小)
   GameBehavior Parent(父节点)
   static GameBehavior Root(根节点)
   static GameBehavior[] Stack(栈)
   statuc GameBehavior Current(当前遍历节点)
方法:
   static Create(GobReal1(行为持续时间),GobReal2(行为的优先级))->this
   virtual Destroy()

   virtual onStart()//这个虚方法只会被调用一次,会在行为开始计时的时候被调用
   virtual onUpdate()//这个虚方法每过0.01秒就会被调用一次
   virtual onEnd()//这个虚方法只会被调用一次,会在行为的Time超过Duration的时候被调用

   static AddChild(GobInt1(父节点),GobInt2(子节点))
   static RemoveChild(GobInt1(要删除的节点))
   static PrevOrder()->GobIntArray(前序遍历数组)
触发器:
   TimePeriodic(每过Period秒)
   onInit(地图初始化)
}



GameBehavior_s_AddChild
    事件
    条件
    动作
       如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
           If - 条件
                GobInt1 大于 0
                Obj_GameBehavior[GobInt1] 小于 0
                GobInt2 大于 0
                Obj_GameBehavior[GobInt2] 小于 0
           Then - 动作
           Else - 动作
               游戏 - 显示Debug信息: (|cffcccccc添加的父节点或子节点不存在?父节点: + ((转换 GobInt1 为字符串) + (,子节点: + ((转换 GobInt2 为字符串) + |r))))
                跳过剩余动作
       -------- 判断左右 --------
       如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
           If - 条件
               GameBehavior_Priority[GobInt2] 大于或等于 GameBehavior_Priority[GobInt1]
           Then - 动作
                -------- 左 --------
                如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                    If - 条件
                        GameBehavior_Left[GobInt1]大于 0
                    Then - 动作
                        -------- 左边已经有子节点了 --------
                        触发器 - 运行 GobStackPushAX <预设> (检查条件)
                        设置 GobInt1 = GameBehavior_Left[GobInt1]
                        触发器 - 运行GameBehavior_s_AddChild <预设> (检查条件)
                        触发器 - 运行 GobStackPopAX <预设> (检查条件)
                    Else - 动作
                        设置 GameBehavior_Left[GobInt1] = GobInt2
                        设置 GameBehavior_Parent[GobInt2] = GobInt1
           Else - 动作
                -------- 右 --------
                如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                    If - 条件
                       GameBehavior_Right[GobInt1] 大于 0
                    Then - 动作
                        -------- 右边已经有子节点了 --------
                        触发器 - 运行 GobStackPushAX <预设> (检查条件)
                        设置 GobInt1 = GameBehavior_Right[GobInt1]
                        触发器 - 运行 GameBehavior_s_AddChild <预设> (检查条件)
                        触发器 - 运行 GobStackPopAX <预设> (检查条件)
                    Else - 动作
                        设置 GameBehavior_Right[GobInt1] = GobInt2
                        设置 GameBehavior_Parent[GobInt2] = GobInt1





GameBehavior_s_RemoveChild
    事件
    条件
    动作
       如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
           If - 条件
                GobInt1 大于 0
                Obj_GameBehavior[GobInt1] 小于 0
           Then - 动作
           Else - 动作
                跳过剩余动作
       -------- 是否有父节点 --------
       如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
           If - 条件
                GameBehavior_Parent[GobInt1] 大于 0
           Then - 动作
                -------- 有父节点 --------
                如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                    If - 条件
                       GameBehavior_Left[GameBehavior_Parent[GobInt1]] 等于 GobInt1
                    Then - 动作
                        -------- 在父节点的左边 --------
                        设置GameBehavior_Left[GameBehavior_Parent[GobInt1]] = 0
                    Else - 动作
                        如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                            If - 条件
                               GameBehavior_Right[GameBehavior_Parent[GobInt1]] 等于 GobInt1
                            Then - 动作
                                -------- 在父节点的右边 --------
                                设置GameBehavior_Right[GameBehavior_Parent[GobInt1]] = 0
                            Else - 动作
                                游戏 - 显示Debug信息: (Error:GameBehavior_s_RemoveChild:节点  + ((转换 GobInt1 为字符串) + ( 不在其父节点的任何一侧|r + <空字符串>)))
                设置 GameBehavior_Parent[GameBehavior_Left[GobInt1]] = 0
                设置 GameBehavior_Parent[GameBehavior_Right[GobInt1]] = 0
                -------- 将左右子节点添加给父节点 --------
                如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                    If - 条件
                       GameBehavior_Left[GobInt1] 大于 0
                    Then - 动作
                        触发器 - 运行 GobStackPushReg <预设> (检查条件)
                       设置 GobInt2 = GameBehavior_Left[GobInt1]
                        设置 GobInt1 = GameBehavior_Parent[GobInt1]
                        触发器 - 运行 GameBehavior_s_AddChild <预设> (检查条件)
                        触发器 - 运行 GobStackPopReg <预设> (检查条件)
                   Else - 动作
                如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                    If - 条件
                       GameBehavior_Right[GobInt1] 大于 0
                    Then - 动作
                        触发器 - 运行 GobStackPushReg <预设> (检查条件)
                        设置 GobInt2 = GameBehavior_Right[GobInt1]
                        设置 GobInt1 = GameBehavior_Parent[GobInt1]
                        触发器 - 运行 GameBehavior_s_AddChild <预设> (检查条件)
                        触发器 - 运行 GobStackPopReg <预设> (检查条件)
                    Else - 动作
           Else - 动作
                -------- 没有父节点 --------
                如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                    If - 条件
                        GameBehavior_s_Root 等于 GobInt1
                   Then - 动作
                        -------- 是根节点 --------
                        如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                            If - 条件
                               GameBehavior_Left[GobInt1] 大于 0
                            Then - 动作
                                -------- 将左节点作为新的根节点 --------
                                设置 GameBehavior_s_Root =GameBehavior_Left[GobInt1]
                                设置GameBehavior_Parent[GameBehavior_Left[GobInt1]] = 0
                                -------- 将右节点作为左节点的子节点 --------
                                如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                                    If - 条件
                                        GameBehavior_Right[GobInt1]大于 0
                                    Then - 动作
                                        设置GameBehavior_Parent[GameBehavior_Right[GobInt1]] = 0
                                        触发器 - 运行 GobStackPushReg <预设> (检查条件)
                                        设置 GobInt2 = GameBehavior_Right[GobInt1]
                                        设置 GobInt1 = GameBehavior_Left[GobInt1]
                                        触发器 - 运行 GameBehavior_s_AddChild <预设> (检查条件)
                                        触发器 - 运行 GobStackPopReg <预设> (检查条件)
                                    Else - 动作
                            Else - 动作
                                如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                                   If - 条件
                                       GameBehavior_Right[GobInt1] 大于 0
                                    Then - 动作
                                       -------- 将右节点作为新节点 --------
                                        设置 GameBehavior_s_Root = GameBehavior_Right[GobInt1]
                                        设置GameBehavior_Parent[GameBehavior_Right[GobInt1]] = 0
                                       -------- 左边没有节点,什么也不做 --------
                                    Else - 动作
                                       -------- 左右节点都没有,这就是一个光棍啊 --------
                                        设置 GameBehavior_s_Root = 0
                    Else - 动作
                        -------- 不是根节点 --------
                        游戏 - 显示Debug信息: (Error:GameBehavior_s_RemoveChild:节点  + ((转换 GobInt1 为字符串) +  不是根节点却没有父节点))
       设置 GameBehavior_Left[GobInt1] = 0
       设置 GameBehavior_Right[GobInt1] = 0
       设置 GameBehavior_Parent[GobInt1] = 0



GameBehavior_s_PrevOrder
    事件
    条件
    动作
       如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
           If - 条件
                GameBehavior_s_Root 大于 0
               Obj_GameBehavior[GameBehavior_s_Root] 小于 0
           Then - 动作
                设置 GameBehavior_s_Current = GameBehavior_s_Root
                设置 GameBehavior_s_Stack[0] = 0
                设置 GobIntArray[0] = 0
           Else - 动作
                跳过剩余动作
       触发器 - 运行 GobStackPushCX <预设> (检查条件)
       循环动作[GobInt3]从 1 到 8192, 运行 (Loop - 动作)
           Loop - 动作
                如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                    If - 条件
                        Or - 任意条件成立
                            条件
                                GameBehavior_s_Current大于 0
                               GameBehavior_s_Stack[0] 大于 0
                    Then - 动作
                        循环动作[GobInt3]从 1 到 8192, 运行 (Loop - 动作)
                            Loop - 动作
                                如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                                    If - 条件
                                       GameBehavior_s_Current 大于 0
                                    Then - 动作
                                       -------- 将当前节点入栈 --------
                                        设置 GameBehavior_s_Stack[0] =(GameBehavior_s_Stack[0] + 1)
                                        设置GameBehavior_s_Stack[GameBehavior_s_Stack[0]] = GameBehavior_s_Current
                                        --------设置左节点为当前节点 --------
                                        设置 GameBehavior_s_Current =GameBehavior_Left[GameBehavior_s_Current]
                                    Else - 动作
                                        退出循环
                        如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                            If - 条件
                               GameBehavior_s_Stack[0] 大于 0
                            Then - 动作
                                -------- 设置当前节点为栈顶节点 --------
                                设置 GameBehavior_s_Current =GameBehavior_s_Stack[GameBehavior_s_Stack[0]]
                                -------- 将当前节点加入数组 --------
                                设置 GobIntArray[0] = (GobIntArray[0] + 1)
                                设置 GobIntArray[GobIntArray[0]] =GameBehavior_s_Current
                                -------- 出栈节点 --------
                                设置GameBehavior_s_Stack[GameBehavior_s_Stack[0]] = 0
                                设置 GameBehavior_s_Stack[0] = (GameBehavior_s_Stack[0]- 1)
                                -------- 设置右节点为当前节点 --------
                                设置 GameBehavior_s_Current =GameBehavior_Right[GameBehavior_s_Current]
                            Else - 动作
                    Else - 动作
                        退出循环
       触发器 - 运行 GobStackPopCX <预设> (检查条件)


GameBehavior_s_Create
    事件
    条件
    动作
       触发器 - 运行 Obj_GameBehavior_Allocate <预设> (检查条件)
       设置 GameBehavior_Duration[this] = GobReal1
        设置 GameBehavior_Time[this] = 0.00
       设置 GameBehavior_Complexity[this] = 1.00
       设置 GameBehavior_Tag[this] = GameBehavior
       设置 GameBehavior_m_onStart[this] = DoNothing<预设>
       设置 GameBehavior_m_onUpdate[this] = DoNothing<预设>
        设置 GameBehavior_m_onEnd[this] = DoNothing <预设>
       设置 GameBehavior_m_Destroy[this] =GameBehavior_o_Destroy <预设>
       -------- 添加节点到优先树中 --------
       设置 GameBehavior_Left[this] = 0
       设置 GameBehavior_Right[this] = 0
       设置 GameBehavior_Parent[this] = 0
       设置 GameBehavior_Priority[this] = GobReal2
       如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
           If - 条件
                GameBehavior_s_Root 大于 0
               Obj_GameBehavior[GameBehavior_s_Root] 小于 0
            Then - 动作
                -------- 存在根节点,添加为子节点 --------
                触发器 - 运行 GobStackPushReg <预设> (检查条件)
                设置 GobInt1 = GameBehavior_s_Root
                设置 GobInt2 = this
                触发器 - 运行 GameBehavior_s_AddChild <预设> (检查条件)
                触发器 - 运行 GobStackPopReg <预设> (检查条件)
           Else - 动作
                -------- 不存在根节点,将当前节点作为根节点 --------
                设置 GameBehavior_s_Root = this


GameBehavior_o_Destroy
    事件
    条件
    动作
       -------- 将节点从优先树中移除 --------
       触发器 - 运行 GobStackPushAX <预设> (检查条件)
       设置 GobInt1 = this
       触发器 - 运行 GameBehavior_s_RemoveChild <预设> (检查条件)
       触发器 - 运行 GobStackPopAX <预设> (检查条件)
       设置 GameBehavior_Priority[this] = 0.00
       设置 GameBehavior_Duration[this] = 0.00
       设置 GameBehavior_Time[this] = 0.00
       设置 GameBehavior_Complexity[this] = 0.00
       设置 GameBehavior_Tag[this] = <空字符串>
       设置 GameBehavior_m_onStart[this] = DoNothing<预设>
       设置 GameBehavior_m_onUpdate[this] = DoNothing<预设>
       设置 GameBehavior_m_onEnd[this] = DoNothing <预设>
       设置 GameBehavior_m_Destroy[this] = DoNothing<预设>
       触发器 - 运行 Obj_GameBehavior_Deallocate <预设> (检查条件)



GameBehavior_t_TimePeriodic
    事件
    条件
    动作
        设置 GameBehavior_s_Count = 0.00
       触发器 - 运行 GobStackPushThis <预设> (检查条件)
       触发器 - 运行 GobStackPushCX <预设> (检查条件)
       -------- 获得按照优先级排序好的GameBehavior --------
       触发器 - 运行 GobStackPushArray <预设> (检查条件)
       触发器 - 运行 GameBehavior_s_PrevOrder <预设> (检查条件)
       循环动作[GobInt3]从 1 到 GobIntArray[0], 运行 (Loop - 动作)
           Loop - 动作
                设置 this = GobIntArray[GobInt3]
                如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                    If - 条件
                        Obj_GameBehavior[this] 小于 0
                    Then - 动作
                        游戏 - 显示Debug信息: ((运行GameBehavior + (转换 this 为字符串)) + ( ,优先级为  + (转换 GameBehavior_Priority[this] 为字符串)))
                        如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                            If - 条件
                               GameBehavior_Time[this] 小于或等于 0.00
                            Then - 动作
                                如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                                    If - 条件
                                       GameBehavior_m_onStart[this] 不等于 DoNothing <预设>
                                    Then - 动作
                                        触发器 - 运行 GameBehavior_m_onStart[this] (检查条件)
                                    Else - 动作
                            Else - 动作
                                如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                                    If - 条件
                                        GameBehavior_m_onUpdate[this]不等于 DoNothing <预设>
                                    Then - 动作
                                        触发器 - 运行 GameBehavior_m_onUpdate[this] (检查条件)
                                        设置 GameBehavior_s_Count = (GameBehavior_s_Count+ GameBehavior_Complexity[this])
                                    Else - 动作
                                        设置 GameBehavior_s_Count =(GameBehavior_s_Count + 0.01)
                        设置 GameBehavior_Time[this] =(GameBehavior_Time[this] + GameBehavior_s_Period)
                        如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                            If - 条件
                               GameBehavior_Duration[this] 大于 0.00
                               GameBehavior_Time[this] 大于或等于 GameBehavior_Duration[this]
                            Then - 动作
                                如果(所有的条件成立) 则运行 (Then - 动作) 否则运行 (Else - 动作)
                                    If - 条件
                                       GameBehavior_m_onEnd[this] 不等于 DoNothing <预设>
                                    Then - 动作
                                        触发器 - 运行 GameBehavior_m_onEnd[this] (检查条件)
                                    Else - 动作
                                触发器 - 运行 GameBehavior_m_Destroy[this] (检查条件)
                            Else - 动作
                    Else - 动作
       触发器 - 运行 GobStackPopArray <预设> (检查条件)
       触发器 - 运行 GobStackPopCX <预设> (检查条件)
       触发器 - 运行GobStackPopThis <预设> (检查条件)
       设置 GameBehavior_s_Period = (0.01 +((GameBehavior_s_Count / 50.00) x 0.01))
       计时器 - 启动 GameBehavior_s_CentralTimer,应用计时方式: 一次性,计时周期为 GameBehavior_s_Period 秒

呃……但是……运行地图感觉反而变卡了呀!?
恩,这是很正常的。因为这个优化方法提供的是一种优先机制,而不是限制了一秒钟运行的onUpdate方法次数,并不能改善性能。
所以说这个优化方法最好能与之前的分批方法结合,才能同时获得优先机制和性能上的提高。又或是说,这个方法实际上更适合用于处理那些不需要这么频繁的运行的东西。

以上就是两种进一步改良GameBehavior的方法,并且两者侧重于不同点。当然这里实际上还有第三种方法,那就是——有一些行为相比起其他的行为更注重实时性,比如弹幕,这是玩家要看在眼里的东西,所以一旦它们变得迟钝了,就很容易给玩家造成不好的体验。但是有一些东西是玩家看不见的,比如AI,AI的反应时间即使从0.01秒变成0.1秒,对于玩家来说其速度仍然十分迅速。所以按照这种思路我们可以给不同的行为设置权重,让TimePeriodic优先,更多的刷新这些权重更大的行为,而削减那些并不会给我们的游戏造成太大影响的行为的onUpdate运行次数。实际上类似的问题也出现在计算机操作系统中,因此在这方面可以有所参考。不过因为我实在是没时间再做一个第三个方案的演示了(而且相对来说效果也不如前两个那么明显),所以这个问题就交给给位自己探索了。

下一个章节就将是所有教程系列的最终章,以介绍面向对象的各种原则作为结束。实际上对于面向对象的世界来说,这也只是一个开始而已,更多的需要各位自己的探索。

回复 支持 反对

使用道具 举报

 楼主| 发表于 2016-9-7 20:24:00 | 显示全部楼层
第九章:面向对象原则
         天啊!这就是所有教程的最后一章!在这最后一章,我会“尽可能”浅显的介绍面向对象的种种原则。这是前人所总结的经验,所以我承担的职责,也只是尽量让大家能够了解这些经验的意义所在而已。也正是因为如此,所以实际上我自己能说的事情很少,大部分都是能在网上查到的。如果你想要更加深入的去了解相关的信息,可以去搜索更多的内容,我只会介绍其中的一小部分。编程的世界十分广阔!希望在你看完这最后一章之后,能够进入这个世界!

         内聚与耦合:
         内聚与耦合是程序设计中的两个基本概念。有很多人都听说过高内聚,低耦合的设计原则,但是它们究竟有什么意义?

         内聚内聚指一个模块内部元素彼此结合的紧密程度。从字面上来讲不难理解。模块和元素都是非常宽泛的概念,比如如果函数触发器模块,组成函数触发器的动作就是元素;如果系统或者类是模块,那么从属于系统和类的函数和变量就是元素;如果触发器分类是模块,那么放在触发器分类中的系统,类,触发器就都是元素……
    而模块内部的元素是否紧密结合,判断的根据就在于“它们是否都专注于同一个目的”。比如说一个类,如果它的变量和方法都是用于完成同一件事情的,那么它的内聚性就越高。反之,如果你给一个类添加太多的变量和方法,希望它去完成越来越多的事情,那么这个类就是低内聚了——一旦这个类负责的众多职责的其中一项发生了改变,你都会需要来修改这个类,这样这个类的修改就会非常频繁——这是我们不希望看到的。所以说不管是对于函数触发器,类,系统而言,尽量让它的功能集中在一个点上,可以减少我们需要修改它的次数。
    耦合:“耦合(或者称依赖)是程序模块相互之间的依赖程度。实际上我觉得“依赖”这个词比“耦合”更容易让人理解——所谓低耦合,指的就是模块之间应该尽量少的相互依赖。模块之间的相互依赖有很多种,比如一个类引用了另一个类的成员属性,另一个类调用了一个类的方法之类的。
   所以判断模块之间的耦合程度高低的根据就在于“模块之间依赖彼此功能的程度”。如果一个类自身不实现任何功能,全靠调用其他类的方法来实现自己的功能,耦合度就很高了;反之,如果一个类的功能过于强大,以至于其他所有类都来调用它的功能,就意味着有一大堆的类都依赖于这个类,耦合度同样很高。耦合度高的情况下,一旦你改动一个类,就有可能影响到其他依赖他的类,也就是所谓的“牵一发而动全身”。这就会让你在改动你的程序的时候变得摇摇晃晃,随便改一个东西都有可能在不知道什么地方引发BUG,当然是我们不希望看到的。
    所以说“高内聚,低耦合”的作用是指导我们如何设计程序中的模块,根本目的在于让程序的结构尽量合理和简单,不要那么复杂,减少我们维护它的成本。
    但是讲到这里你可能发现问题了——“高内聚”和“低耦合”其实是互相矛盾的!这就好比是我们说做事情要做得“又快又好”一样——然而实际上哪里有方法能够让事情又快又好啊!“快”就势必要牺牲工作的质量,“好”就势必要花费大量的时间,“又想马儿跑得快,又想马儿不吃草”完全是不现实的。
    如果你选择了“高内聚”,就意味着你的类的功能会尽量的单一,但是这样的话你就会需要大量的类来帮你完成程序中的各种事情,这种时候类就势必要相互依赖,变成了“高耦合”。反之如果你选择了“低耦合”,就意味着你要尽量减少类之间的相互依赖,每个类都需要把一大堆的功能集合到自身内部,变成了“低内聚”。前者一旦发生改变,你就会需要频繁的修改整个系统中相互依赖的类,后者一旦发生改变,你就会需要频繁的修改一个复杂而庞大的模块。

所以说“高内聚,低耦合”实际上指的并不是要尽量的去追求“高内聚”或者“低耦合”的极端,而是要追求两者的“平衡”——什么啊,这讲到最后不是还是等于什么都没讲嘛,根本就不存在同时满足“高内聚,低耦合”的设计方法嘛。
但是这个原则并不是毫无作用,它告诉了我们“内聚”和“耦合”之间的制衡关系,并且为我们提供了从“内聚”和“耦合”方面对设计进行评价的标准。所以虽然我们暂时可能还找不到最符合这个原则的设计方法,但是却至少有了一个前进的方向。

比如我们现在可以来审视一下我们之前“GameBehavior”的改良设计方案。在使用GameBehavior之前,我们将弹幕和位移都独立做成了两个类,两者之间相互毫无联系,满足“低耦合”,但是每个类不得不花很多的精力在实现自己并不关心的功能,比如计时器,周期性调用onUpdate方法等等,所以是“低内聚”的。
为了使弹幕和位移类都能专注于实现自身的功能,获得“高内聚”,我们提出了“GameBehavior”来实现所有行为的底层功能。这提高了弹幕和位移类的内聚性,但是同时也增加了GameBehavior和弹幕,位移类之间的耦合。不过这种耦合属于“外部耦合”,体现为弹幕和位移都依赖GameBehavior这个父类。这种依赖的危害不大,所以是可以接受的。
但是随着游戏的进一步开发,我们的UnitBehavior中集中了大量底层功能的实现,UnitBehavior的内聚性变得很低了,所以我们提出了“事件监视装饰类”的概念,应用装饰者模式来解决这个问题。这增加了UnitBehavior和各种装饰类之间的耦合,但是大大的提升了UnitBehavior的内聚性,再一次使整个程序在“高内聚,低耦合”上达到了一个比较平衡的状态。
所以如果你对内聚和耦合的原则还没有什么新的见解的话,最好就继续沿用我的GameBehavior设计方案吧。但是如果你有一天觉得我的设计方案不合理,打算提出更好的方案,我也丝毫不会感到奇怪。

类的设计原则:
面向对象,类自然是最常见的模块,让我们来看看类有哪些设计原则。

1.  单一职责原则:
顾名思义,就是“一个类最好只负责一个职责!”。
字面意思上的话这个原则蛮好理解的,就是一个类只负责干一件事情嘛。这样的话一个类的职责就不会太过复杂,否则的话当一个类糅合了太多的不同职责,就会降低这个类的“内聚性”(嗯,这么快就用到了新学的概念)。这个原则告诉我们应该在逻辑上尽量保证一个类的职责的单一,比如你不能用弹幕的飞行去实现位移,如果你需要位移功能,应该直接新建一个位移类来实现它。

2.  开闭原则:
这个原则从字面意思上来看就比较迷了,我们还是直接看详细解释吧——开闭原则的含义为“对扩展开放,对修改关闭”。
呃……这个有点迷啊,什么叫做“对扩展开放,对修改关闭”呢?

用大白话来讲,就是“当你需要一个新的功能的时候,应该尽可能的用扩展来实现新的功能,而不是修改已有的模块来实现新的功能”。比如说当你已经有了一个可以直线飞行的弹幕,但是需要一个可以变速飞行的弹幕的时候,不应该直接修改这个可以直接飞行的弹幕来实现变速飞行,而是应该扩展出一个变速飞行的弹幕类来实现这个新功能。
放在War3纯T编程里,这个原则实际上是在鼓励你多使用虚方法,尤其是针对那些可能会频繁的产生新功能的类。咱们的GameBehavior就很好的体现了这个原则,GameBehavior囊括了游戏中几乎所有行为类型的变化,具有onStart,onUpdate,onEnd这些虚方法,再加上其派生出的子类,虚方法的数量该有个十几二十个,这样我们需要任何新的行为就都可以通过派生GameBehavior类型的子类来实现,而不需要改动任何已有的方法。

3.  里氏替换原则:
这个……这个原则就更没法从字面猜是什么意思了-_-|||
这个原则也有很多版本的解释,最通俗易懂的一个版本就是“子类可以扩展父类的功能,但是不能改变父类原有的功能”。
WTF……这跟“替换”有毛关系?好吧,里氏替换原则的另外一个解释是“子类必须能替换成它们的父类”。所以说这一条原则的含义其实是——“不要去破坏继承体系”。

这在我们的装饰类里其实也很常见。比如说AccelBullet装饰了UnitBullet,但是没有修改其直线飞行的虚方法实现,而是先运行了自身实现加速飞行的动作,再运行了父类的直线飞行的动作——这就符合里氏替换原则呀!

4.  接口隔离原则:
这个原则在War3纯T编程中颇为尴尬——因为War3纯T编程实际上无法实现接口。所以我们跳过这个

5.  依赖倒置原则:
依赖倒置原则指的是“高层模块不应该直接依赖底层模块,两者都应该依赖抽象层。抽象不能依赖细节,细节必须依赖抽象”。这句话解释得够详细的,不过还是有一些问题没说明白。

首先,模块是一个很宽泛的概念,模块可以是系统,也可以是类。那么模块之间的依赖呢?所谓的高层模块依赖底层模块指的是高层模块调用底层模块提供的方法,依赖倒置原则告诉我们不应该直接这么做,而是应该构造一层“抽象层”,让高级模块使用调用抽象层提供的虚方法,而低层模块则继承抽象层并实现虚方法。所谓细节依赖抽象,指的也是低层模块实现抽象层的虚方法。

这个原则通过抽象层把高层模块和低层模块给隔离开来了,降低了高层模块和低层模块之间的耦合程度。高层模块不必知道低层模块究竟是如何实现抽象层的虚方法的,就可以调用虚方法。所以你大概可以猜出,这个原则和开闭原则是有联系的——当你构造了抽象层之后,低层模块想要产生新的功能就可以通过继承抽象层并重写虚方法来实现,也就是所谓的对扩展开放了。

6.  最少知识原则:
最少知识原则指的是“一个类应该对自己需要依赖的类知道得最少”。这用我们之前刚刚提到的“低耦合”可以很好的解释,降低一个类对于自己所耦合的类的了解就可以达成低耦合。

最少知识原则将出现在成员变量,以及方法的参数中的类称为“朋友类”,而出现在方法的动作中的类不属于朋友类。最少知识原则主张一个类最好只跟自己的朋友类交流,不与陌生类交流,所以应该尽量减少在方法中对于陌生类的交流。当然,一些基础类比如负责向量代数之类的工具类是要除外的。

这条原则在War3中的存在感实际上不高,因为当我们应用了GameBehavior之后,很少会碰上这种问题。
    以上就是面向对象的六个非常重要的原则。
    但是,重要归重要,却不一定是必要的。
    很多人都会犯这种类似的错误,那就是在刚学会面向对象的原则之后,就立刻套用到自己的程序中去。然后他们会绞尽脑汁的想,怎样才能让自己的程序更加符合这些原则,然后反而让自己的程序变得越来越复杂……最后他就遭重了。
    对于我们War3纯T编程来说,更是如此。因为War3纯T编程,从本质上来说并不是“从0开始写一个新的游戏”,而是“将一个RTS游戏修改成我们需要的游戏”。所以War3纯T编程的大部分精力,其实并不是花在游戏框架,和系统的构造上的,而是花在修改游戏中各式各样千奇百怪的行为上的。所以当我们掌握了GameBehavior之后,大部分的工作就都可以通过GameBehavior来完成了。既然War3纯T编程对于我们该如何构造一个完整的程序的要求不高,这些面向对象的原则的用武之地也会相对的更小。
    所以说,不要过度的迷信面向对象原则,它们是重要而非必要的,你不必时刻遵守它,只需要在关键的时刻想起它,把握好一个度就行了。
    甚至更进一步的——虽然我吹了很长时间的面向对象,也告诉了大家面向对象究竟有多重要——但是,没有面向对象,你就做不了War3纯T编程了吗?
    不,相反,面向对象并不是必要的,War3纯T编程甚至不支持面向对象,我们讲了这么多个章节,实际上都是在强行用原本是面向过程的War3纯T来实现面向对象。
    所以面向对象仅仅只是一件更为快捷的工具,而不是一件开天辟地的神器。当你使用面向对象能够更快更好的达成你的需要的效果的时候(当然,面向对象大部分时候都可以做到),你就可以使用面向对象。但是当面向对象受制于War3纯T的面向过程机制,或者用面向过程可以达成和面向对象相同效果的时候呢?——我仍然推荐你使用面向过程,如果面向对象无法提供比面向过程更好的效果的话,就还是面向过程吧。
    具体的实例在我的地图中也可以见到,比如在我的地图中尽管有GameBehavior和Bullet这样的对象,但是对于伤害分析系统,游戏流程控制之类的东西,我仍然选择了使用面向过程来实现——没啥特别的原因,单纯是因为面向过程在做这些单纯只是一个过程的系统比面向对象要更快而已。
    以上就是我最后要说的全部内容了。作为教程的最后一篇,我也只是把你领进了门而已,在这之后,你还能探索得多深,都取决于你自己。不要迷信,保持批判和好奇,你可以看见更为广阔的天地。
                        全教程完,作者:边境拾遗,写于2016年8月9日星期二

回复 支持 反对

使用道具 举报

发表于 2017-2-9 15:20:32 | 显示全部楼层
很棒的教程,谢谢边境拾遗
回复 支持 反对

使用道具 举报

发表于 2017-3-12 02:20:49 | 显示全部楼层
先标记,一下子消化不玩,慢慢看
回复 支持 反对

使用道具 举报

发表于 2017-3-25 22:29:12 | 显示全部楼层
66666666666
回复 支持 反对

使用道具 举报

发表于 2017-5-11 16:22:06 | 显示全部楼层
顶~~~~~~~~~~~~~~~~
回复 支持 反对

使用道具 举报

发表于 2017-7-12 07:00:40 | 显示全部楼层
好多字,辛苦了
回复 支持 反对

使用道具 举报

发表于 2017-12-20 23:05:10 | 显示全部楼层
不错的教程,辛苦了。希望多出这种优秀教程帮助我们这种新人。也希望有地图例子。只看字的话还是不会写
回复 支持 反对

使用道具 举报

发表于 2018-5-15 21:32:16 | 显示全部楼层
好帖好帖,多谢楼主分享!
回复 支持 反对

使用道具 举报

发表于 2018-5-25 12:15:02 | 显示全部楼层
楼主辛苦了  真正的技术贴啊  
回复 支持 反对

使用道具 举报

*滑动验证:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|移动端|小黑屋|地精研究院

GMT+8, 2018-10-20 14:53 , Processed in 0.082355 second(s), 11 queries , Gzip On, Redis On.

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表