偶久网

 找回密码
 注册会员

简单一步 , 微信登陆

QQ登录

只需一步,快速开始

12
返回列表 发新帖
楼主: 邪恶叔

JASS教程—0基础新手向教程

  [复制链接]
邪恶叔 title=
 楼主| 发表于 2016-8-20 16:51:27
第十七章:哈希表(Hashtable)——基础语句

PS:请结合十六章一起看。
其实呢~ 如果你被套了魔免盾后没有出现排斥现象的话~
那么恭喜这位同学,你已经会用哈希表在一个函数里绑定单位到计时器,并在另一个函数里等计时器到期后读取被绑定的单位并命令这个单位做动作了。当然了,还不止~ 你连哈希表的排泄都已经学会了~
好吧。。要是光看那个就能学会大家也不用那么辛苦了。。

现在让我们将第十六章中的故事翻译成J:
首先要理解下什么是哈希表:那个装本子的箱子就是哈希表,那些本子贴的标签叫哈希表的主目录(也有叫父目录之类的),本子的页码叫哈希表的子目录,最后每页记录的人名就是被记录在哈希表的主目录中的子目录中的值了。
至于管理员呢。。就是电脑

比如说我们在地图里放了个单位名字叫A(A就是这个单位的全局变量名,放到地图上WE会自动为其编个号)
然后试着用哈希表完成下面这三件事:
1.    将A存到一个哈希表的1号主目录1号子目录下
2.    从这个哈希表的1号主目录1号子目录中读取A
3.    将A从这个哈希表的1号主目录1号子目录中删除

第一步,我们需要初始化一个哈希表并为其取个名字:
  1. globals
  2. hashtable ht=InitHashtable()
  3. endglobals
复制代码


初始化哈希表一般就用全局变量初始化一个就够了,要存不同的东西完全可以存在一个哈希表的不同目录下。

然后就是储存这个单位:
  1. globals
  2. hashtable ht=InitHashtable()
  3. endglobals

  4. function Save takes unit A returns nothing
  5.     call SaveUnitHandle( ht, 1, 1, A )
  6. endfunction
复制代码


额。。SaveUnitHandle()这个函数不是翻翻字典就能理解的,所以我解释下,前面两个单词很好理解,Save(储存)和Unit(单位);关键是最后一个单词,Handle(手柄、把柄),在第十四章最后部分有提过,除了integer、real、boolean、string之外,都是指针型的数据,所谓指针型数据都是只有一个整数作为地址,然后这些数据就像个路标指针一样指向这个地址,所以叫指针型数据(比如一个农民,其实并没有“农民”这样的数据,而是以一个整数作为数据,然后这个农民就是指向这个整数的指针)

那么这里Handle的意思就是指这个单位的整数地址。
顺带一提,即使数据类型相同,比如都是单位,但是不同的单位整数地址也是不同的。

要举一反三就很简单了,比如储存特效X:
  1. call SaveEffectHandle( ht, 1, 1, X )
复制代码


如果不是指针型的呢,去掉Handle就行了,比如储存实数8.0
  1. call SaveReal( ht, 1, 1, 8.0 )
复制代码


第二步读取:
就是在函数中使用LoadUnitHandle()等这种函数,但是一般读取出来为了方便使用,就直接用变量赋值。
  1. function Load takes nothing returns nothing
  2.     local unit u=LoadUnitHandle( ht, 1, 1 )
  3. endfunction
复制代码


在这个函数中就讲ht中主目录1的子目录1的单位地址读取出来,但后赋值给局部变量u
虽然单位地址之前说过是整数,但是那只是个地址,实际读取出来的是这个单位。

最后一步就是删除哈希表里这个储存的单位了:
  1. call RemoveSavedHandle( ht, 1, 1 )
复制代码


既然单位是指针型数据,也就是有handle,那么管它是什么数据的handle,反正都是handle,直接删handle就行,其它的不是handle型的数据,就要区别开来。
  1. call RemoveSavedInteger( ht,1,1 )
  2. call RemoveSavedReal( ht, 1, 1 )
  3. call RemoveSavedString( ht, 1, 1 )
  4. call RemoveSavedBoolean( ht, 1, 1 )
复制代码


删除一个主目录下所有的子目录:
  1. call FlushChildHashtable( ht, 1 )
复制代码


也就是把主目录1的所有子目录删除了

如果要清理整个哈希表的话,也就是将所有的主目录删除:

  1. call FlushParentHashtable( ht )
复制代码

邪恶叔 title=
 楼主| 发表于 2016-8-20 16:53:38
第十八章:哈希表——基础应用

现在我们来看下十六章中的剧情弄成J到底是怎么样的。
先假设事件为任意单位使用“睡眠”技能后触发,函数就叫F吧,技能目标叫A吧:
  1. function F_Act takes nothing returns nothing
  2.     local unit A=GetSpellTargetUnit()
  3.     set A=null
  4. endfunction

  5. function F_Cond takes nothing returns Boolean
  6.     return GetSpellAbilityId() == “睡眠”
  7. endunction

  8. function Init_F takes nothing returns nothing
  9.     local trigger trig=CreateTrigger()
  10.         call TriggerRegisterAnyUnitEvent( trig, EVENT_PLAYER_UNIT_SPELL_EFFECT )
  11.         call TriggerAddCondition( trig, Condition(function F_Cond) )
  12.         call TriigerAddAction( trig, function F_Act )
  13.     set trig=null
  14. endfunction
复制代码


PS:还是再提醒下吧,之后我就把没改变的函数截掉了。

显然需要个计时器:
顺便简化下动作部分,就事件触发3s后叫醒A这个单位好了


同学们应该发现了,这个F_TimeAct是“动作”或者说“代码”,也就意为着不能有参数,那么用局部变量记录的单位A无法用传参的方式从F_Act传递到F_TimerAct(注意局部变量只能在一个函数内部使用,别的函数无法使用的,要用的话就必须使用某种手段传递过去),有的同学说用全局变量就好了啊,的确能解决问题,但是注意全局有可能会造成多人使用时变量冲突,尤其是在做技能的时候,一个技能若被多次使用,那么这个全局变量会被最后使用的技能覆盖,也就造成了变量冲突,使用局部变量就能简单有效地避免这样的问题。

虽然这个单位变量无法传递,但是这个计时器却可以传递:
  1. function F_TimeAct takes nothing returns nothing
  2.     local timer t=GetExpiredTimer()
  3.     set t=null
  4. endfunction
复制代码


那么有什么方法能使单位随着这个计时器一起传过去吗?
这就要用哈希表把单位A和这个计时器绑定了,换成哈希表的术语说,就是把这个单位A储存在计时器的整数地址主目录下。

估计某些同学忘了,于是复习一下,除了基础数据外的数据都是指针型数据,都有个整数地址,计时器也是个数据类型,自然也有整数地址。
也就是先在F_Act里记录,之后便可以在F_TimeAct里读取,相当于管理员在闹钟响后取出对应闹钟编号的本子查看人名一样。

但是问题又来了。。如何获得计时器(或者其他指针型数据)的整数地址。。
这就要用到GetHandleId()这个函数了,所有的指针型数据都可以用这个来获取整数地址。
记得初始化哈希表~
  1. globals
  2.     Hashtable ht=InitHashtable()
  3. endglobals

  4. function F_TimeAct takes nothing returns nothing
  5.     local timer t=GetExpiredTimer()
  6.     set t=null
  7. endfunction

  8. function F_Act takes nothing returns nothing
  9.     local timer t=CreateTimer()
  10.     local unit A= GetSpellTargetUnit()
  11.     local integer i=GetHandleId( t )
  12.         call TimerStart( t, 3, false, function F_TimeAct )
  13.         // 被储存的是单位不是计时器,计时器只是作为主目录编号
  14.         call SaveUnitHandle( ht, i, 1, A )
  15.     set t=null
  16.     set A=null
  17. endfunction
复制代码


之后在F_TimeAct中读取这个单位,当然了,还要再获取次这个计时器的地址:
  1. globals
  2.     Hashtable ht=InitHashtable()
  3. endglobals

  4. function F_TimeAct takes nothing returns nothing
  5.     local timer t=GetExpiredTimer()
  6.     local integer i=GetHandleId( t )
  7.     local unit u=LoadUnitHandle( ht, i, 1 )
  8.     set t=null
  9.     set u=null
  10. endfunction

  11. function F_Act takes nothing returns nothing
  12.     local timer t=CreateTimer()
  13.     local unit A= GetSpellTargetUnit()
  14.     local integer i=GetHandleId( t )
  15.         call TimerStart( t, 3, false, function F_TimeAct )
  16.         // 被储存的是单位不是计时器,计时器只是作为主目录编号
  17.         call SaveUnitHandle( ht, i, 1, A )
  18.     set t=null
  19.     set A=null
  20. endfunction
复制代码


这样单位A就从F_Act传到F_TimeAct了,最后就是在F_TimeAct中写让A做的动作,比如要叫醒A,那么就使用UnitWakeUp()这个函数。。额。。不过这个函数不会影响催眠魔法效果(我没有试过,所以不是很清楚,想知道的同学可以自己试试~)
记得计时器的排泄~(这里的计时器是一次性的,所以不用先暂停以避免BUG了)
  1. function F_TimeAct takes nothing returns nothing
  2.     local timer t=GetExpiredTimer()
  3.     local integer i=GetHandleId( t )
  4.     local unit u=LoadUnitHandle( ht, i, 1 )
  5.         call UnitWakeUp( u )
  6.         call DestroyTimer( t )
  7.     set t=null
  8.     set u=null
  9. endfunction
复制代码


最后的最后,就要对哈希表进行排泄了,记得那个管理员把第一批人叫醒后做了什么吗?
撕下“1”这个标签,把本子扔了……
也就是把计时器整数地址的子目录全部清除了,因为之后绑定到闹钟1上的人可能不同了,虽然也可能重复,或者说可以新数据覆盖旧数据,但是一直放在箱子里很占重量的……
有人想偷懒不排泄,于是不服:一本本子能有多重?
那如果有十万本呢?并且其中99999本没用呢?

所以排泄有益于减少内存的占用,提高运行速度~
PS:一般比较常用的是清除一个主目录下的所有子目录,子目录里的数据一般都是被新数据覆盖的,不用替换数据前特地清除一下
  1. function F_TimeAct takes nothing returns nothing
  2.     local timer t=GetExpiredTimer()
  3.     local integer i=GetHandleId( t )
  4.     local unit u=LoadUnitHandle( ht, i, 1 )
  5.         call UnitWakeUp( u )
  6.         call DestroyTimer( t )
  7.         call FlushChildHashtable( ht, i )
  8.     set t=null
  9.     set u=null
  10. endfunction
复制代码


之前忘说了于是补上:每个子目录只能记录一个数据。

举个实例的思路吧:
比如击退,施放技能后局部变量获取被击退目标,记作u,绑定u到一个计时器t,然后开启计时器t每X秒运行另一个函数F;
F中获得到期的计时器t,读取绑定的单位u,移动u,用一个整数i记录移动的次数,也就相当于t的循环次数,当达到一定值后(用条件判定式判断)停止t,删除t,清空哈希表中储存在主目录t下的所有子目录。

小误区
有的同学可能觉得把计时器的地址作为子目录也行,主目录用普通整数(1,2,3,……),是没问题,但是存多个单位时就要改变主目录或者让计时器的地址做算术运算,对于前者,清理的时候要清除主目录而不是子目录,也就会把其它储存的主目录也删掉,自然不行;对于后者。。不仅麻烦,也可能会造成主目录冲突,因为主目录是用数字,而触发做多了之后又记不得哪个用的数字1,哪个用的数字2,万一不小心弄了两个一样的主目录,就冲突了,而且这问题排查的时候还很麻烦,但是用数据的地址就不同了,因为每个数据即使类型相同,地址也是不同的。

补充:
      如果要以一个string作为主目录储存怎么办?因为目录必须都是整数,所以字符串明显不能作为目录嘛……这时就需要用到StringHash这个东西,这个函数能把一个string转成一个整数地址,当然,相同的string得到相同的整数,不同的自然不同~
       比如说StringHash("a")就把a这个字符串转成了一个整数地址,具体是多少我没研究过不太清楚。。想知道的同学可以用 I2S 这个函数把得到的整数再转成string后看一下。
      用法非常简单,之前把单位绑计时器的时候是GetHandleId( GetExpiredTimer() )作为主目录,如果要把这个单位绑到“a”这个string上呢就StringHash( "a" )作为主目录即可~

现在同学们可以去看下血戮魔动冰前辈的哈希表教程了,链接在顶楼,虽然可能还是会晕 ^_^

发表于 2019-7-27 20:24:25
牛 B~~~~~~~~~~~~~~
丫无象
发表于 2020-7-19 16:01:23
遇见神帖岂能不顶?
znoj
发表于 2020-8-26 14:30:39
确实是难得好帖,顶
元以内
发表于 2020-11-4 18:51:30
邪恶叔 发表于 2016-8-20 16:53
第十八章:哈希表——基础应用
现在我们来看下十六章中的剧情弄成J到底是怎么样的。
先假设事件为任意单 ...

好帖支持一下!!
发表于 2021-7-9 16:21:54
遇见神帖岂能不顶?感谢大佬讲解魔兽知识,请问大佬有jass 格式化缩进插件或者编辑器吗?有的j很多if,也没有格式化,找起来很难受
发表于 2023-5-31 12:31:58
牛的,但是16章没有了
zh_496799725
发表于 2024-11-19 06:42:55
感谢分享,66666
快速回复 返回顶部 返回列表