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

 找回密码
 点一下
查看: 5886|回复: 31

一个写得比较混乱的系统教程……

[复制链接]
发表于 2009-10-18 15:47:58 | 显示全部楼层 |阅读模式
第一部分是教程
第二部分是关于系统的封装和移植性
第三部分是关于绑定数据的存储系统
写得都不是太长,也基本没什么内容……大家也都差不多可能看不懂…………哎………
就是这样吧。


喵呜……
来写系统教程了…………说实话真不好写啊……系统真正相同的制作步骤没几个的说………………

暂时分一下阶段教程吧……反正十一8天呢…………虽然作业很多喵……

PS:想把我这个教程看懂的话,先去学Jass吧。
最好还是吧VJ也学了。

系统制作的准备工作:
一般来说……做一个比较简单的系统其实是可以略过这一阶段的。不过在做系统之前,还是需要现在脑子里想一下,这个系统到底怎么实现它的功能。比如想到用哪些事件和Timer来实现某一个效果。需要哪些参数。怎么保存这些参数等等。一些很细节的东西就先不要想了。还有一个就是这个系统的【基本原理】,最根本的东西,你需要先把这个最基本的东西测试一下,看看到底效果能不能实现,才可以动手编写系统。

比如我的位面消隐系统,物品的显/消影,就是利用影子权杖(好像是精灵之火?),能够将隐形单位单独显形的(如果使用触发使单位打开对玩家的视野,原本的隐形单位是不会“完全”显形的)特性。
实际上,如果某些人直接认为【使用触发使单位打开对玩家的视野】就能让隐身单位显形的话,那么可能到最后系统编出来了,却发现基础的基础是有Bug的…………
(PS:对默认阴影使用打开视野,敌对单位可以攻击他,但是事实上,并没有阴影的模型,用触发可以直接强制敌方玩家选择之,但是虽然面板是阴影的,模型还是没有的)
这是一个技术型系统的例子。

再比如说,做一个弹射的技能。首先要考虑,怎么捕捉单位释放技能事件,投射物是怎么弹射的,是用Timer+马甲投射物模拟呢,还是用马甲单位攻击投射物+捕捉伤害事件呢?Timer+马甲似乎很简单。但是效率上却比马甲单位攻击投射物+捕捉伤害要慢多了。
那么到底要用哪个好呢?
看个人啦~(不过我是比较喜欢第一种的说……因为不用动态注册事件~)

当然,最重要的还是学识渊博~在这里我强烈建议大家去教学区把所有引导贴和其中链接的帖子全都看一遍~因为我都没有看完~所以看完的人可以超过我了~
问一下别人也是很好的选择~

系统制作的前期工作:
首先嘛~明确自己系统的实现过程。
比如从哪个触发器被触发作为整个系统的开始,哪里是结束。
中间用了什么触发、计时器、物品、马甲……都要弄清楚。
把要做的系统的构架大概想一下。
然后嘛,就是给触发、计时器这样的东西写绑定数据的代码了。
一般来说新手直接gamecache。
如果给单位、物品绑定数据的话,自定义值+VJ的struct最好了。
Timer这样可重复使用的东西用TimerDataSystem也很好。
自己写的数组HashTable也不错。
最推荐的是1.24的hashtable+VJstruct(虽然这两个不能同时存在,但是我们可以把VJstruct写成VJ源码)
把这些基本的存储系统代码都准备好~(这种代码一定要保证没有Bug)
(一般来说这种东西可以事先写好,把关键词写成$XXXXXX$,然后copy一个副本直接替换$XXXXXX$为你需要的关键字就可以了)

系统制作的中期工作:
这里就是制作系统的核心工作阶段啦~要把【基本原理】转换为合格的核心代码(比如把等待换成Timer),并且和接口函数连接起来~
在核心代码和接口函数之间的一些比较繁琐的条件和小动作也要写好。
嗯…………应该说是就是所谓的“写系统”的阶段吧。在这个阶段,就要把整个系统的代码全部写出来了。
如果可能的话,可以分模块地把这些系统的功能一一测试一下(只是普通情况的测试,不包括极端情况),如果发现有代码不能正常工作,就需要不断地Debug,至少能让模块在普通情况下正常工作才可以。
最后就是把整个系统拿来测试了。做系统的新手强烈建议进行模块化测试,然后再进行这个系统的测试。这样真的发生Bug了,找起来也会方便很多。而且在模块化测试中,有相当一部分Bug被排除掉了。

等你觉得这个系统在普通情况下可以正常工作的时候,这一阶段就结束了~

在这一阶段编写代码的时候,一定要写的很明确自己的目的,比如在一个模块里的,就在前后加上注释分割线,写上这一模块的用途、参数。
对于某些概念极端混淆的代码段,不要吝啬自己的注释,一定要把所有的if分支的含义写清楚、把loop的含义写清楚。尤其是一些递推、模拟递归的loop…………这种东西还是不太推荐大家使用,毕竟太复杂了(对于新手来说确实是这样),而且对于标准的递推和递归用loop的格式写是非常难受的。

系统制作的后期工作:
后期工作大约就是把一些极端的会发生Bug的情况找出来,然后对于这些情况Debug。
举个例子,全屏装备栏系统,如果大家只想到你构造的装备栏里的格子的物品交换,那很可能就会出现Bug。
这个Bug是什么呢?就是如果玩家开着装备栏,又直接把单位物品栏(不是你构造的物品栏)的物品进行交换或者直接扔地下。此时你构造的物品栏和单位默认的物品栏的同步就会出现差错。嗯……至少我在编这个系统的时候,是没有想到这一点的(而且貌似《遗失的记忆》枫霜写她的全屏装备栏的时候也是这样)。
为了解决这个Bug,你就要添加上单位丢弃物品,卖掉物品什么的trigger,然后再添加一些代码Debug,清除或改变对应格子的Data,才能没有Bug。
这个例子更极端的情况是:
当玩家用鼠标选择了你构造的物品栏中的一个物品(之后就不再进行操作),又把这个对应的物品从单位物品栏扔了出去。再点击另一个你构造的物品栏的格子(或者仓库栏的格子)。那么会出现什么情况呢?一般来说用鼠标点击一个格子,你会先读取这个格子的Data,然后等玩家点击下一个格子的时候,再把Data和后点击的格子的Data交换。如果在读取Data的时候,就直接读取了格子的物品,那么此时就会发生Bug,但是如果只是用一个指针指向这个格子,在交换的时候,读取两个格子的Data,这样就不会有Bug。
这样的Bug只能不断的测试或者冥思苦想才想得出来的,不过做系统的经验多了,这类Bug就好找了。
嗯…………后期工作除了Debug也就没有别的了。
除非你只是先完成了整个系统的一大部分,然后一点一点地给这个系统增加功能(比如全屏装备栏加个强化装备、或者锻造、或者镶嵌什么的……),又给这些单独的功能测试什么的(循环完成中期和后期工作)。

不过像骗分的系统就可以直接省略后期工作了(尤其是全屏装备栏这样老到不行的系统……),大家基本都不会去Down你的系统的…………
哦呵~哦呵~哦呵~哦呵呵呵~

剩下的是我一些在编系统中觉得应该注意的一些方面。
比如封装性、还有善用VJ的struct和存储系统的结合。
 楼主| 发表于 2009-10-18 15:48:45 | 显示全部楼层

关于系统的封装和移植性

嗯…………前几天cccty1和我讨论过在实现复活的效果(具体参见活宝的【转生领域】……)下,到底是用重新创建单位好呢,还是用魔兽自带的复活技能?
重新创建单位自然很好,非常简单。但是我认为使用魔兽自带的复活技能更好。
虽然复活相对于直接创建新单位更加麻烦,但是事实上魔兽自带的复活技能对于单位的数据保存得更好。
应该说是做到了极限。除了Buff丢失以外(实际上这是死亡的效果),单位几乎所有一切包括其余各种技能、系统绑定在单位身上的数据都不会丢失。
我从一个系统制作者出发,出于保护单位数据的考虑,我觉得复活单位是最好的。
第一,单位的Handle值不会发生改变(这点对于使用GameCache、两种HashTable来绑定单位数据的方法来说是绝对重要的)
第二,单位的自定义值(UserData)不会发生改变。
第三,用触发为单位添加的技能不会丢失(这个…………太重要了)
嗯,实际上活宝做这个娱乐技能……基本上只要演示图里不出Bug就可以的。
但是如果是做一个给别人用的系统,就必须要考虑这些。

封装性和移植性,先说简单的移植性吧。
这点基本就受两个限制:
1.VJ
2.1.24b的hashtable
脑残级别的限制:
有没有在变量管理器里声明变量…………
有没有用T(用了T的系统我是绝对不会移植的,除非就几句T)
没有这几个限制,什么系统都可以移植。
用了VJ,别人没有识别VJ的WE就不能移植。
用了1.24b,别人没有1.24b的原版WE(1.24b除了原版WE以外目前貌似没有哪个WE能兼容)也不能移植。
至于VJ和1.24b一起用的…………在目前阶段我很想了解这个系统的演示地图是怎么做出来的…………
脑残级别的限制就不说了,相信大家都知道这种系统的下场。

封装性,这个说起来比较复杂。
其实差不多就是如果别人把你的系统(在你的演示地图里是没有任何Bug的)移植到自己的地图里,会不会引发奇异的Bug,或者导致了一些废弃资源的产生。
这些都集中体现在unit,单位身上。
1.比如你的系统使用了UnitUserData(单位自定义值)来绑定数据,那么如果有别的系统也使用了自定义值的话,这两个系统都有可能出现因为数据不对应而产生的灵异Bug。虽然我以前做系统都是直接绑定UserData上的,但是我现在认为这个UserData是绝对不能碰的,因为总共UserData就只是一个integer数据,非常珍贵。
2.你的系统使用了马甲,那么CreateUnit会导致这个马甲响应单位进入完整区域事件,这个事件对于很多系统来说是非常重要的,比如伤害显示系统,就需要这个T来给新进入地图的单位注册伤害事件。而如果你使用完后直接RemoveUnit,那么单位却无法响应UnitDeath的trigger,很多系统都用这个t来删除它的动态注册的事件,所以删除马甲虽然看起来比较环保,但事实上用KillUnit或者UnitApplyTimedLife却更有封装性。
我在开头举的例子就是这样。
如果马甲除了这两个事件以外还有动作的话,就要小心了。
1.比如马甲用来释放技能,分成两个部分来说:
第一种就是因为马甲释放的技能本身而引发的问题。
这种问题比较好解决。
比如马甲释放一个伤害技能,这个伤害技能造成了伤害,但是此时这个马甲单位却因为极短的生命周期+DeathTrigger立即Remove单位瞬间消失了。
那么此时不管是伤害来源、这个马甲杀死的单位的凶手单位都为null空值以外,被杀死的单位的经验和金钱都是无法正确得到的。这个一般来说只要设置比较长的生命周期就可以了……
第二种就是因为是马甲释放的这个技能而引发的问题。
这种问题就比较极端了,除了做系统的以外一般都不会碰到了。
就是马甲和被替代的原单位之间的数据同步。
比如你的系统用马甲去释放一个原本应该是某个正常单位来施放的正常技能(这个我也在我的任意施法前摇/后摇的系统贴中和westwood讨论了一些,为什么会出现这种情况也详见那个帖子)。那么这就绝对是不一样的了。虽然已经技能可能被工程升级的替换了技能ID,但是GetSpellAbilityId()是获得的替换后的ID,所以无大碍,但是数据却是不能捕捉的。比如有一些技能会根据所谓的一种单位点数(绑定在原单位上,这里只是举例而已),造成不同的效果(可以想成不同值的伤害)。我们马甲释放这些技能时,就不会产生实际的效果。
所以我是觉得能不用马甲就不用马甲……
当然,如果真的必须要用马甲,那也是可以的。只不过需要一些必要数据同步(一般来说,只要响应产生和死亡的马甲就不需要了,比如用来做模型/阴影或者尸体什么的……)。
一般来说,英雄的马甲需要同步等级,物品……嗯……貌似能简单的同步的就是这么几个了。如果物品还绑定有数据那就更……
其他的嘛……用单位学习技能记录英雄技能和等级,所以同步还是可以的。
至于其他的用UnitAddAbility加上去的…………很难做到(但还是可以做到的)。
Buff基本不用想,也是很难做到。
对于别人绑定在单位身上的数据同步。
这个也是比较难办到的。分两种情况:
1.别人绑定的是UnitUserData,这个直接把UserData复制过去即可。
2.别人把数据绑定在了单位的handle值上了。
这个同步基本不可能,毕竟两个同时存在的unit的Handle值是绝对不可能一样的。
3.别人把数据绑定在除了单位以外的东西上了,比如timer。
这个同步也是属于近似不可能的事情。因为获得别人绑定的数据就很困难了,像这样unit没有指向的timer的数据就更难改了。

当然,如果别人和你都使用了特别制作的数据系统,那么在理论上都是完全可以实现除了handle值以外全部同步的效果。

对于单位的封装性还有一点,就是单位默认的数据,能自己找方法自动获得数据,就自己获得数据,最好不要让使用者注册太多的数据。那样使用者都会觉得很麻烦。

单位的说完了……好像就没有可说的了呢~
对了,只要用了VJ的就要多把VJ的功能发挥出来,绑定数据用struct最好了~
回复

使用道具 举报

 楼主| 发表于 2009-10-18 15:49:21 | 显示全部楼层

关于绑定数据的存储系统

存储数据的系统是做系统的基本。我是绝对赞成vj的struct的。就算1.24b还没有能用的vj,我也非常喜欢直接写伪vjstruct源码。
首先,存储数据有这样几个结构:
gamecache
hashtable
array
其中,cache的效率<hashtable的效率<array的效率。
这个应该是公认的……?
cache因为效率又慢,还可能因为Sync而产生互通图所以是早就应该被抛弃的鬼东西。
悲剧的是这个东西居然是jass教程中的“高级教程”…………
在下面我们不考虑cache。

1.24b的hashtable因为可以嵌套存储hashtable,所以可以很方便的实现任意长度的N维数组,在功能上比array和gamecache(那个东西其实也可以多维数组,但是因为会产生大量基本无实际作用的String所以废弃)要好得多。

array虽然效率最高,但是因为根本没有直接存储的方法,所以实际情况还要讨论。
1.vj的struct
vj的struct的原理就是两个函数:
allocate()
这个用来分配一个1-8190的不会与当前存储的struct实例重复的struct实例(在这里你可以把struct实例当成integer)
destroy()
这个用来回收struct实例,以备下次allocate()使用。
struct的原理其实就是用这个integer(Struct实例)作为索引(index),在全局变量数组中找到对应struct实例的数据。
需要给绑定对象绑定一个integer。
至于其他的我们先不管,因为如果需要用到hashtable(1.24b)的话,那么就只要把这两个函数翻译成jass基本上就没问题了。
2.直接求差法。
典型例子就是TimerDataSystem。直接把当前handle的Id减去最小handle的id就得到了index。
不过这个handle是不能被删除的。
这个是所有绑定无限事先声明的数据方法中,应该是最快的方法了。

我绑定数据的方法是,能用struct就用struct(其实求差法也是可以写成struct的,只不过这个struct不需要allocate()和destroy()而已)。
把所有数据全部写在struct里。
然后再使用hashtable(我现在是写数组hashtable,不过下一个系统考虑到N维数组可能就直接1.24b的hashtable了)把这个struct绑定在被绑定目标上。
比如:
[jass]
globals
    hashtable HT = null
endglobals

function Init takes nothing returns nothing
    call FlushParentHashtable( HT )
    set HT = InitHashtable()
endfunction

struct Buff
    timer t
    buff b
    unit u
   
    static method create takes unit u, buff b returns thistype
        local thistype this = thistype.allocate()
        set .b = b
        set .u = u
        call SaveInteger( HT, GetHandleId( b ), StringHash( "TheStruct" ), this )
        return this
    endmethod
   
    static method get takes buff b returns thistype
        return LoadInteger( HT, GetHandleId( b ), StringHash( "TheStruct" ) )
    endmethod
   
endstruct
[/jass]
(以上代码在我写这个帖子的时候,实际上是连我的WE是编译都编译不出来的,只不过是示意而已)
至于我为什么很喜欢用struct,因为看起来绑定的数据的类型、名称都一目了然。而如果全部使用hashtable或者gamecache,那么在编很大的系统时,就会比较麻烦,不知道自己绑定的数据在哪里,有没有绑定什么的。
最关键在于它可以和任何其他的存储系统结合在一起,而且因为每次它只需要在操作最开始get一下,然后就可以直接用index从数组里读数据,效率也是非常高的~
当然对于没学过vj的人来说应该是没用的吧…………
只绑定1个integer,移动数据的时候也是比较方便的。
理解面向对象这个概念的人应该会觉得struct非常好用。
回复

使用道具 举报

 楼主| 发表于 2009-10-18 15:51:44 | 显示全部楼层
说实话…………真要用我这个教程写系统那基本没有前途…………
不过嘛,struct很好,能不用马甲就不用马甲,这两句话记住就可以了……
当然不是一定正确,只是我的个人观点而已……哦呵呵呵~
回复

使用道具 举报

发表于 2009-10-18 17:44:57 | 显示全部楼层
某血你这篇东西就属于做过的人没学到啥没做过的人啥也没学到的尴尬境界……

马甲什么的主要还是要通过其他系统的纠错才比较有效吧,我通常是通过判断单位类型或是使用单位附加值弄的。
回复

使用道具 举报

发表于 2009-10-18 17:49:25 | 显示全部楼层
。。。你还用vj那么土的东西啊。
回复

使用道具 举报

 楼主| 发表于 2009-10-18 17:51:04 | 显示全部楼层
引用第4楼louter于2009-10-18 17:44发表的  :
某血你这篇东西就属于做过的人没学到啥没做过的人啥也没学到的尴尬境界……

马甲什么的主要还是要通过其他系统的纠错才比较有效吧,我通常是通过判断单位类型或是使用单位附加值弄的。

不过那样就增加了别人的负担。
回复

使用道具 举报

 楼主| 发表于 2009-10-18 17:52:27 | 显示全部楼层
引用第5楼eff于2009-10-18 17:49发表的  :
。。。你还用vj那么土的东西啊。

因为我喜欢面向对象。
jass对这方面太令人受不了了。
回复

使用道具 举报

发表于 2009-10-18 17:56:38 | 显示全部楼层
果然是很混乱的系统啊……

总之呢,一个系统就两个方面,运算和操作。
运算方面:主要是数据运算和存储,具体的内容很多,最后的结果得到后直接if判断或者作为参数。
操作方面:移动单位,创建单位之类的操作游戏中的实体对象。
回复

使用道具 举报

 楼主| 发表于 2009-10-18 18:05:56 | 显示全部楼层
LS的第二个我觉得有必要细分到使用系统里去…………
毕竟还有TimerDataSystem这样的纯存储系统存在的。
回复

使用道具 举报

发表于 2009-10-18 18:15:14 | 显示全部楼层
一个系统应该是这两个部分:
系统部分
数据部分

如果这两块揉在一起,那就不能称之为一个系统。

系统部分:你只要按照他的规则调用即可,不需要修改。
数据部分:按照你自己的意思,按照他给你的规则填写,来组成你想要的效果。
回复

使用道具 举报

发表于 2009-10-18 18:17:22 | 显示全部楼层
另外,编程不管怎么样都会套到面向过程上去的。。。面向对象你站在宏观一点的角度看也是面向过程
回复

使用道具 举报

 楼主| 发表于 2009-10-18 18:20:13 | 显示全部楼层
引用第11楼eff于2009-10-18 18:17发表的  :
另外,编程不管怎么样都会套到面向过程上去的。。。面向对象你站在宏观一点的角度看也是面向过程

只是习惯这种面向对象的思维了。
回复

使用道具 举报

发表于 2009-10-18 18:39:08 | 显示全部楼层
那个。。你真的知道啥叫面向对象么。。
回复

使用道具 举报

 楼主| 发表于 2009-10-18 18:43:07 | 显示全部楼层
……………………vj的struct至少比纯jass在面向对象上更明显吧。
回复

使用道具 举报

发表于 2009-10-18 19:07:27 | 显示全部楼层
没有吃完我就写去了……
……
说反了……

这样区分的是为了模块化方便
一般来说系统的核心都是数据处理
那么模块的最初区分就从这里开始
我们在系统时,最先写的也就是这个部分(当然过于简单的系统没有这个趋向,只是根据需要区分和写)
而对于J来说最麻烦的就是数据传递
这个问题主要发生在需要局域化的系统中的延时传递参数
也就是使用timer的时候,因为TimerStart不能带参数,所以我们才使用存储系统
其实hashtable这东西确实简化了存储系统的使用难度
毕竟懂hash数组的人还是很少的,
很多比较有名的图都是用GC做的
基本上没有hash存储系统的成品图吧……
至于我和小血做的基于Location的系统
那东西大约没有什么实用价值,
虽然功能很强
但效率太低

关于数据的传递,
大约是做一个系统中最困难也是最容易出错的地方
虽然你使用的是某个成熟的存储系统
但是在使用中很容易出项这样那样的问题
……

才发现自己说了好多废话……
我本来是想说数据记录的方法呢……
对于一个系统,我们传递一个参数
到底它能有多少信息
这个是要在做系统中考虑的问题
首先我们要知道我们需要在函数之间传递哪些内容
单位、整数、玩家、点、实数……
我们通常不会一个个的都之间传递过去
而是通过某些少量的参数来间接的获取
比如GC系统以及hashtable存储系统我们更多的是传递过去一个integer(或者说是使用的timer的handle值)
通过这一个传递的数据来获得所有传递的信息。

当然这种方式传递的实际内容只是一个
但是有些时候我们传递的内容无法用一个数据来获得
那么就有得考虑了

实质上,我们传递的每个数据的信息量都是不同的
比如我们用StartTimer函数
传递的是一个timer
它的信息是个integer
然后我们来获得对应key的存储内容(GC或hashtable)
或者hash计算后作为数组的索引

那么一个timer的数据内涵就不仅仅是一个integer了
如果分类考虑
实际上作为timer的handle值,这个传递的integer是一个固定死的值
相比一个可以随意定值的integer
它的可存储的信息量是少的(这个不是指通过它做索引的获得的数据,而是它自己的存储内容)
或者说一个可随意定值的integer是-2147483648~2147483647一共4294967296值
那么作为handle的integer只有1个值。
当然用于存储内容的integer的实际内容含量也不会是4294967296这么恐怖
如果我们存储有两种状态的数据,那么我们用一个integer实际只能存储32个数据信息(2进制),实际上我们正常最多用到31个……
你也许会问这有什么用

我来举个例子
比如你做个迷宫系统,那么每一个格子四个方向是否可以通过的记录就是2进制的(通、不通)
我们不需要用四个布尔值来存储,而用一个integer就行了,通过每一位的二进制运算,我们只使用integer的0~15一共16个值即可
你也许会问,我用4个布尔值也不麻烦啊
当然,但是模块化的处理来说,使用更多的变量时一件麻烦事
有些时候你不得不一堆if来判断使用哪个变量
而不能使用loop来批量处理
毕竟一个loop比一堆嵌套的if更容易读懂,也不容易出错
Debug也越简单
某些时候你可以专门写个function来解析integer
这样出错的几率会更小写

我原以为Real会有更大的信息量
结果失望了……
它比integer少的多,而且8位以上就不可靠了……
所以我说Location的系统时个没有实用价值的东西……

总之……
如上……
回复

使用道具 举报

发表于 2009-10-18 20:20:09 | 显示全部楼层
疯子,我需要一个随机奖励的开宝箱系统,有兴趣写一个否?
回复

使用道具 举报

发表于 2009-10-18 20:59:17 | 显示全部楼层
我现在欠一屁股帐呢……
某个区域副本系统Debug不下去了……
然后还有个1.24升级修改办法的教程……
这个还在等
自己还在考研复习中……
反正你愿意丢我也成……
不过什么时候做出来就没准了……
回复

使用道具 举报

发表于 2009-10-18 21:21:14 | 显示全部楼层
那看来还是我自己写好了。

另外:考研不是什么好事情啊
回复

使用道具 举报

发表于 2009-10-18 21:40:14 | 显示全部楼层
刚刚有个想法……
恩今晚之前能写出来就写
失败就算了……
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 点一下

本版积分规则

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

GMT+8, 2024-3-29 05:05 , Processed in 0.219986 second(s), 18 queries .

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

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