-☆-「jass技能入门教程~Part 2.5」「从 return bug 到 hashtable」-☆-

查看: 169|回复: 0
[复制链接]

WOW8 发表于 2018-7-18 14:53:14 | 显示全部楼层

欢迎访问本论坛,注册你的账号并登录,来与我们交流吧!

欢迎 登录 与我们交流!没有帐号?立即注册

x
夜の星 发布于 2014-3-4 21:51:40


☆ 前言
嘛...和s叔交流之后发现,也有些用惯了return bug+gamecache的人,也不是很能够理解新出来的hashtable

本篇教程的侧重点将会是帮助1.20的制图者理解1.24加入的元素的使用方法,你也可以认为,这是一篇讲解如何将地图从1.20更新到1.24版本的迁移教程~

*本教程的受众因此主要是1.20的return bug使用者,讲述的内容也将是新旧对比为主,如果是新人学习hashtable的用法,请参考这篇教程


☆ 为什么要选择hashtable?
在很久很久的以前,return bug这一技术被发明出来,这对于we来说是个革命性的喜讯:配合游戏缓存进行使用,数据绑定变得简单方便

"return bug" 这一命名其实是挺随意的,它的原理是利用到函数返回(return xxx)时jass语法检查的漏洞,来实现强制类型转换~你可以将单位、计时器、甚至实数转换成整数,或者逆向而行~

想必读者对于缓存和RB并不陌生,或许已经使用这一方法制作过很多的技能、系统......

在1.24的更新中,RB+GC的方法被禁止了~作为补偿,hashtable(哈希表) 系列函数被加入了,用于替代RB+GC;
可以说,hashtable是官方认可的数据绑定方法,它是游戏缓存的孪生兄弟,因此用法几乎一模一样~它的诞生其实正是受到了RB+GC体系的启发~
比起缓存,哈希表要更加专一、高效,能够省去return bug中的许多麻烦事~

以下是hashtable相对而言的优点:
  • 更加高效,更快的读写速度
  • 整数路径,避免字符串泄漏
  • 具有类型检查,避免不安全的转换
  • 支持1.24+,地图大小扩充至8M
  • 新的演示、技能大多都会从rb+gc转到hashtable,这是一个新技术取代旧事物的更迭过程

那么让我们开始吧~通过比较RB缓存和哈希表新旧两种写法,相信你能够很快掌握接下来的内容~



☆ H2I的重生
H2I可以说是return bug体系中最简单,但也是核心的组成部分:
function H2I takes handle h returns integer
    return h
    return 0
endfunction


这是一个典型的H2I函数,将一个handle转换成整数~1.24以上版本中,它将无法通过语法检查

幸运的是,一个新加入的库函数可以取代H2I的作用
native GetHandleId takes handle h returns integer

观察下可以发现,GetHandleId的参数和返回值和上面的H2I完全一致,事实上它们的功能也是完全一样,换句话说,GetHandleId就是一个官方版本的标准H2I...将你的所有H2I用GetHandleId替代吧~

function cftx_eating_cat takes nothing returns nothing
    local timer t=CreateTimer()
    local integer id=H2I(t)         //1.20
    local integer id=GetHandleId(t) //1.24+
endfunction


然而return bug的封杀同时也让这个过程变得不可逆向进行:
function I2U takes integer i returns unit
    return i
    return null
endfunction

function I2T takes integer i returns timer
    return i
    return null
endfunction


这些函数在1.24中不再可用,官方并未提供替代功能
事实上,你也并不需要这些了,哈希表对于每一种类型都提供了读取/存储函数


☆ 声明一个hashtable
在return bug系统中,我们通常会设置一个全局的gamecache用于数据的存储
一个全局hashtable也是按照同样的方法进行声明:
globals
    gamecache gc = InitGameCache("cirno")
    //创建并初始化一个游戏缓存
    hashtable ht = InitHashtable()
    //创建并初始化一个哈希表
endglobals


两者的用法十分相近,相比之下哈希表并不需要设置名字,这是它们仅有的区别呢...


☆ 使用hashtable存储/读取简单数据
对于游戏缓存(gamecache),各位应该很熟悉下面这些函数的用法:
local timer t=CreateTimer()
local string path=I2S(H2I(t))
call StoreBoolean(gc, path, "flag",   true)
call StoreInteger(gc, path, "count",  40)
call StoreReal   (gc, path, "speed",  10.0)
call StoreString (gc, path, "dialog", "bug")


哈希表版本几乎是和游戏缓存一一对应的..
local timer t=CreateTimer()
local integer id=GetHandleId(t)
call SaveBoolean(ht, id, 0, true)
call SaveInteger(ht, id, 1, 40)
call SaveReal   (ht, id, 2, 10.0)
call SaveStr    (ht, id, 3, "bug")


除了函数名不同以往,它们之间最初要的差别是:
  • gamecache使用string作为存储路径
  • hashtable使用integer作为存储路径

哈希表的存储函数都是以 "Save+类型名称" 来命名的,并不难记

二者读取数据的方式也非常相似:
GetStoredInteger(gc, path, "count")
GetStoredBoolean(gc, path, "flag")
// gamecache 读取数据

LoadInteger(ht, id, 1)
LoadBoolean(ht, id, 0)
// hashtable 读取数据


哈希表读取函数的命名方式一般是 "Load+类型名称"


☆ handle的存储与读取
以上几个都是最基本的类型,在存储单位、计时器、单位组这些handle类的时候,return bug+gamecache 和 hashtable 的用法则略有不同:

gamecache:
local timer t=CreateTimer()

call StoreInteger(gc, I2S(H2I(t)), "u", H2I(GetTriggerUnit()))
// unit(单位)被转换成整数存入gamecache

call KillUnit(I2U(GetStoredInteger(gc, I2S(H2I(t)), "u")))
// 读取的时候将整数再次转换为单位


hashtable:
local timer t=CreateTimer()

call SaveUnitHandle(ht, GetHandleId(t), 0, GetTriggerUnit())
// unit直接存入hashtable,不需要转换成整数存入

call KillUnit(LoadUnitHandle(ht, GetHandleId(t), 0))
// 读取也有自己的专用函数,不需要转换整数

对于各种Handle(单位、点、单位组、闪电、计时器、可破坏物......),哈希表为每一个类型都提供了专门的存取函数

它们的命名方式通常是: Save/Load  +  类型  +  Handle


☆ 判断路径下是否存储了数据
这一章比较简单,看实例自己理解下即可
HaveStoredInteger(gc, "5", "1")
// 检查gamecache中指定路径下是否存储了整数
HaveSavedInteger (ht,  5,   1)
// 检查hashtable中指定路径下是否存储了整数

HaveSavedHandle(ht, 5, 2)
// 在gc中,handle都被转换成整数存储,因此检查是否有整数即可
// 在哈希表中,handle有自己的检查函数
// 注意,在同一路径下面,你只能存储一个handle(例:1个点和1个计时器类型不同也会冲突,后写入的会占掉前者位置)


它们的命名方式通常是: HaveSaved  +  类型,不要和游戏缓存的HaveStored搞混了~


☆ 数据清理函数
缓存和哈希表都有相似的清理函数...可以移除指定数据
根据作用范围的不同,大概可以分成三类
清除单个数据:
call FlushStoredReal(gc, "1", "4")
// 清除缓存中单个的数据
call RemoveSavedReal(ht, 1, 4)
// 清除哈希表中单个的数据


命名方式: RemoveSaved  +  类型
目录清除(最常用):
call FlushStoredMission(gc, "2")
// 清除缓存中指定的目录
call FlushChildHashtable(ht, 2)
// 清除哈希表中指定的目录

完全毁灭:
call FlushGameCache(gc)
// 干掉整个缓存
call FlushParentHashtable(ht)
// 干掉整个哈希表


嘛...你得知道自己在做什么,这两个函数并不是清除数据
而是会删除缓存/哈希表本身
你得重新InitGameCache/InitHashtable才能再次使用


☆ 怀旧者的昔日之梦 -StringHash-
在缓存中,我们使用字符串作为存储路径~然而哈希表中则使用整数~
尽管整数的方式要更为高效,但字符方式也有它的优点,比如代码的可读性会增强...

总之,因为各种原因,如果你仍旧想使用字符串作为路径,这个函数可以帮你~
native StringHash takes string s returns integer
StringHash函数可以将字符串进行摘要,返回一个整数用作hash的路径~
call SaveReal(ht, StringHash("knock_back"), StringHash("speed_x"), 10.0)
call SaveReal(ht, StringHash("knock_back"), StringHash("speed_y"), 5.0)


当然,有些事情是你必须知道的:
  • StringHash的本质是无限集映射到有限集,理论上一定会出现两个不同字串返回相同的整数,当然你可以认为这种可能性极其极其小
  • 当这种情况发生时,我们称这种现象为「碰撞」,路径重复,数据会相互覆盖
  • StringHash和S2I函数有着本质的差别
  • 使用StringHash作为存储目录其实并不是个好的选择,整数目录的优点将丧失殆尽



☆ 非常科学的击退演示
嘛...讲了那么多东西,还是拿一点实际的例子来作演示吧~
击退演示.gif
这是一个非常简单的击退函数,用到了数据绑定~
有缓存+RB(1.20)和hash(1.24)两个版本,请下载两个版本对比观看的说~
knock_back_120.w3x (17.75 KB, 下载次数: 0) knock_back_124.w3x (17.44 KB, 下载次数: 1)


☆ 更加复杂的样例
下面这个演示更加复杂一些,涉及到循环存储单位来着的...
同样请对比观看,注意缓存和哈希之间微小的区别~
环刃.gif
Spell_120.w3x (18.4 KB, 下载次数: 0) Spell_124.w3x (18.1 KB, 下载次数: 1)

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

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