|
发表于 2011-6-23 02:43:45
|
显示全部楼层
call ForGroupBJ( udg_qiuhongshizhen, function Trig_shizhen_______uFunc001A )
这个地方泄露了单位组...
我在想给你发txt不如直接贴出来...我已经转成了简体- 记忆体漏失(Memory Leak)
- By Danny
- 一、什麽是记忆体漏失
- 很多国外的WEer常常把谈Memory Leak(记忆体漏失)挂在嘴边,这是指有些创造出来的物件,已经不会再被用到了,却没有被删掉,因而佔用记忆体空间的情形。
- 例如:Unit - Create 1 Footman for Player 1 (Red) at (Center of (Playable map area)) facing Default building facing degrees
- 其中(Center of (Playable map area))就是一个点。可以想成,我要电脑在操场中间产生一个武士,但是电脑不懂什麽叫做地图中间。所以我到操场中间插一个旗子(也就是一个Point),之后我就命令电脑在那个旗子的位置产生一个武士。好了,现在武士产生了,那麽旗子呢?因为我没有删掉它,所以它会一直留在记忆体中。一两个不打紧,但是如果这一类的动作很多,记忆体裡堆了数十万个没有用的东西,跑起来效率自然大打折扣。
- 同样的情形也发生在其它的物件中。对了,什麽是「物件」?笼统地说,只要是游戏中一个具体的东东就是物件,除了布林(boolean)、整数(integer)、字串(string)、实数(real)、程式码(code)不是以外,其它类型的变数皆属之 。有些物件像单位、物品是可以看得到的;而有些像计时器、点、单位群组,却是看不见的。有些物件通常并不会大量产生,而且很多都是可重覆利用的,因此并不需要太注意它 们的删除。
- 当你执行了一些函数,就会产生物件,例如点或单位群组的函数,只要呼叫一次就创造一个点(单位群组)。
- 最常见堆积记忆体的动作如:
- Unit - Create 1 Footman for Player 1 (Red) at (Center of (Playable map area)) facing Default building facing degrees
- 建立了一个点
- Unit Group - Pick every unit in (Units owned by Player 7 (Green) of type Barracks) and do (Unit - Create 1 Footman for Player 7 (Green) at (Position of (Picked unit)) facing Default building facing degrees)
- 建立了一个单位群组及一个点
- Unit Group - Pick every unit in (Last created unit group) and do (Unit - Order (Picked unit) to Attack-Move To (Center of Region 000 ))
- 建立了一个单位群组及一个点
- Special Effect - Create a special effect at (Target point of issued order) using Abilities\\Spells\\Human\\ThunderClap\\ThunderClapCaster.mdl
- 建立了一个点和一个特效
- 其中红色和粗字的部分就是建立而没有被删除的物件。
- 二、记忆体漏失的解决方法(实用篇)
- 记忆体漏失不是太複杂的一件事,但是概念并不是很容易理解。而且此问题从小到大都有,如果你真的要解决所有的记忆体漏失,那麽你会寸步难行,一行简单的触发都必须被写得複杂百倍。因此这裡先提供最实用的几个技巧,基本上能掌握住这个部分,你的地图已经没什麽大问题了。
- 重覆利用既有的物件
- 假设你要製作一张类似DotA或三国无双的地图,像这一类的攻击触发应该是免不了的:
- Unit Attack 1
- Events
- Unit - A unit enters HumanBase1
- Conditions
- Actions
- Unit - Order (Entering unit) to Attack-Move To (Center of DemonBase1 )
- 但是,每当有一个单位进入了HumanBase1,就要产生一个点(Center of DemonBase1 ),是不是太多馀了?反正点都是同样的位置,何不重覆利用呢?
- 所以为了环保起见,我们可以改写成这样:
- Map Init
- Events
- Map initialization
- Conditions
- Actions
- Set DemonBase[1] = (Center of DemonBase1 )
- Set DemonBase[2] = (Center of DemonBase2 )
- Set DemonBase[3] = (Center of DemonBase3 )
- Unit Attack 1
- Events
- Unit - A unit enters HumanBase1
- Conditions
- Actions
- Unit - Order (Entering unit) to Attack-Move To DemonBase[1]
- 也就是预先把可以多次利用的物件用变数记录下来,然后直接套用以减少新物件的产生。
- 删除使用过且不再需要的新建物件
- 首先介绍删除的方法,有一些要用到JASS:
- trigger (Trigger)--Custom script: call DestroyTrigger(xxx)
- location (Point)--Custom script: call RemoveLocation(xxx)
- group (Unit Group)--Custom script: call DestroyGroup(xxx) ※注意Clear Group是清除单位群组中的单位资料,而不是把整个单位群组删除
- force (Player Group)--Custom script: call DestroyForce(xxx) ※注意ForceClear是清除 玩者群组中的玩者资料,而不是把整个玩者群组删除
- Unit--死亡的单位于尸体消失之际,系统会自动移除。所以不用特别去管它。像隐藏施法单位这种用后不理的,要删的话可以用GUI的Unit - Remove、Unit - Kill、Unit - Add Expiration Timer,甚至把生命回复设成负值让它自动死亡都可以
- Item--被用掉的物品,系统会自动移除。所以不用特别去管它。真的要删的话用GUI的Item - Remove即可
- lightning (Lightning)--用GUI的Lightning - Destroy Lightning Effect
- effect (Special Effect)--用GUI的Special Effect - Destroy
- texttag (Floating Text)--用GUI的Floating Text - Destroy、Floating Text - Change Lifespan皆可。前者是直接删除;后者是设定它的寿命,时间一到会自动被删除
- image、ubersplat、quest、……--同样用GUI就能删掉,废话不多话了
- 删除特效--
- 创造特效后要移除,只要你没删掉,即使它只出来一下就会消失,看起来好像没了,但是实际上它还是存在,会佔用记忆体的空间。
- 而建立在单位身上的特效,在单位死亡尸体腐烂后,特效就看不到了,但是和前面一样,只要你没用触发删掉它,它还是会佔用记忆体。
- 删除的方法就是用Special Effect - Destroy (Last created special effect)。
- 创造出特效后立刻移除它,特效还是会播放至少一次,所以对于只播一次的特效,顺手删掉它吧。
- 例如:
- Special Effect - Create a special effect attached to the overhead of (Triggering unit) using Abilities\\Spells\\Other\\TalkToMe\\TalkToMe.mdl
- Special Effect - Destroy (Last created special effect)
- 如果你要让它持续播放一段时间,可以先利用变数存起来,时间到再删掉。
- 删除触发--把执行一次就用不到的触发删除(关掉(Turn Off)可让它不再执行,但是仍会佔用空间。删除比较一劳永逸)。
- 方法是在动作的最前面加上Custom script: call DestroyTrigger(GetTriggeringTrigger())。例如:
- Melee Initialization
- Events
- Map initialization
- Conditions
- Actions
- Custom script: call DestroyTrigger(GetTriggeringTrigger())
- Melee Game - Use melee time of day (for all players)
- Player - Set Player 1 (Red) Current gold to 100000000
- Player - Set Player 1 (Red) Current lumber to 100000000
- 删除点--
- 点是最容易产生的垃圾物件,所以特别要注意怎麽删除临时建立的点。除了如之前所述,可以先用变数记录常用的点以外,还有一个简单的方法可以清除这些垃圾。之前的范例我们也可以改用这个方法:
- Unit Attack 1
- Events
- Unit - A unit enters HumanBase1
- Conditions
- Actions
- Set P1 = (Center of DemonBase1 )
- Unit - Order (Entering unit) to Attack-Move To P1
- Custom script: call RemoveLocation(udg_P1)
- 简单的说,就是先用变数记录打算要用到的点,对它进行操作,然后删掉它。
- 特别注意第三行,由于b社会把所有触发编辑器裡定义的变数加上字首udg_,所以这裡要填入udg_P1,而不是P1。
- 另外像这样的写法常常有人会疏忽:
- Unit - Move (Triggering unit) instantly to ((Position of (Triggering unit)) offset by 500.00 towards (Facing of (Triggering unit)) degrees)
- 实际上这一行接连产生了两个点,而不是一个。
- 首先Position of (Triggering unit)就像是在单位的位置插一支旗子,回传之。
- 后一步的((Position of (Triggering unit)) offset by 500.00 towards (Facing of (Triggering unit)) degrees)就是在那个旗子位移后的位置再插一支旗子,回传之。
- 所以想把点删乾淨就得像这样写:
- Set P1 = (Position of (Triggering unit))
- Set P2 = (P1 offset by 500.00 towards (Facing of (Triggering unit)) degrees)
- Unit - Move (Triggering unit) instantly to P2
- Custom script: call RemoveLocation(udg_P1)
- Custom script: call RemoveLocation(udg_P2)
- 使用这个方法,一个地图裡只需要一、两个像P1、P2这样的临时变数就够了,不必动用大量的变数及阵列。
- 删除单位群组--
- 对于单位群组的移除,最简单的方法是用Custom script: set bj_wantDestroyGroup = true。
- 当Blizzard.j里的Unit Group相关函数执行时会检查bj_wantDestroyGroup, 再决定是否移除传入的Unit Group。
- 例如:
- Custom script: set bj_wantDestroyGroup = true
- Unit Group - Pick every unit in (Units owned by Player 1 (Red)) and do (Unit - Kill (Picked unit))
- 执行到Pick Every Unit In group...动作时,电脑会检查bj_wantDestroyGroup这个变数是否为真,如果为真,就会在进行完动作后,把传入的group(此例中就是(Units owned by Player 1 (Red)))删除掉,并且自动把变数bj_wantDestroyGroup设为假。
- 但请注意,使用自订变数的Group千万不要乱删,除非你晓得自己在做什麽。例如: Unit Group - Pick every unit in ToTGroup and do (Actions)
- Loop - Actions
- Unit - Remove (Picked unit) from the game
- 前面千万不要加set bj_wantDestroyGroup = true ,否则那个变数会被砍掉,以后再也不能用来储存单位。
- 还有Units Of Type使用此用法会有bug,所以也不要对它这样用。
- 如果你不能放心地使用这个指令,也可以参照删除点的作法。不过同样要记得加上udg_: Set TempGroup = (Units owned by Player 1 (Red))
- Unit Group - Pick every unit in TempGroup and do (Unit - Kill (Picked unit))
- Custom script: call DestroyGroup(udg_TempGroup)
- 不要产生物件
- 要建立单位于一特定地点,在触发中是Create Units Facing Angle、Create Units Facing Point,对应的JASS函数则是CreateNUnitsAtLoc、CreateNUnitsAtLocFacingLocBJ。但是在JASS中还有像这样的函数可用:
- native CreateUnit takes player id, integer unitid, real x, real y, real face returns unit
- 也就是说,你可以给座标,而不用创造点。
- 例如:Unit - Create 1 Footman for Player 1 (Red) at (Position Of Triggering Unit) facing Default building facing degrees
- 用JASS可以改写成:call CreateUnit( Player(0), 'hfoo', GetUnitX(GetTriggerUnit()), GetUnitY(GetTriggerUnit), bj_UNIT_FACING )
- 如果你要写一整段的JASS,可以考虑用座标来传送,就可以避免创造不必要的点。
- 三、记忆体漏失的解决方法(进阶篇)
- 首先先声明,以下是写给功力高深,动辄落JASS、耍Custom Script、玩区域变数、搞return bug、……的屌人看的。如果你都没用到,甚至连前面写的那几个名词都不懂,那麽这不是你该来的地方,乖乖回去玩你的GUI Trigger吧!
- 明辨是非篇
- 有些强者看了这篇教学以后,回去就开始厉行清扫工作,见point砍point、见group杀group……。像这种: Player Group - Pick every player in (All allies of Player 1 (Red)) and do (Player - Add 1000 to (Picked player) Current gold)
- 他们自然知道要优化成: Set TempForce = (All allies of Player 1 (Red))
- Player Group - Pick every player in TempForce and do (Player - Add 1000 to (Picked player) Current gold)
- Custom script: call DestroyForce(udg_TempForce)
- 啥?你不知道?那你显然是只懂第二篇的GUI Trigger player,早说过你不该来这裡了,赶快回去玩GUI吧-o-"
- 结果你还是看了……好吧,既然要看就把它看完,不准临阵脱逃,嘿嘿!
- 依此类推: Set TempForce = (All players)
- Player Group - Pick every player in TempForce and do (Player - Add 500 to (Picked player) Current gold)
- Custom script: call DestroyForce(udg_TempForce)
- 写完以后就会发现--一切都变得不对劲了!!
- 为什麽勒?拜託,见鬼杀鬼、见妖斩妖、见魔屠魔,可不要见神也砍神、见佛照灭佛啊XD
- 传回物件的函数,大致上可分成两种。一种是先建立一个物件再传回;另一种是传回已知物件的记忆体位址。
- 像All allies of Player 1 (Red)函数是GetPlayersAllies,我们可在blizzard.j中找到它: function GetPlayersAllies takes player whichPlayer returns force
- local force f = CreateForce()
- call ForceEnumAllies(f, whichPlayer, null)
- return f
- endfunction
- 由此可知它先建立了一个force,把传入的玩者的同盟加入,再传回。所以这个函数实际上产生了一个玩者群组,所以该不该杀? 当然该杀!
- 而All players呢?它的函数是GetPlayersAll,我们也可以在blizzard.j中找到它: function GetPlayersAll takes nothing returns force
- return bj_FORCE_ALL_PLAYERS
- endfunction
- 嘎?怎麽是变数?我们再继续找,费尽千辛万苦,终于在InitBlizzardGlobals下找到这两行: function InitBlizzardGlobals takes nothing returns nothing
- set bj_FORCE_ALL_PLAYERS = CreateForce()
- call ForceEnumPlayers(bj_FORCE_ALL_PLAYERS, null)
- endfunction
- 这样了解了吗?其实B社在地图初始化之时,就先创造了一个叫bj_FORCE_ALL_PLAYERS的玩者群组,并且把所有的玩者加入。所以我们在地图中无论呼叫GetPlayersAll几遍,它都只是传回同样一个玩者群组,而不是先创一个玩者群组再传回。所以当然不能乱杀,不然以后就抓不到了。
- 啥?你问我要怎麽写?啊就不杀啊,事实上这样写就没错了,很轻鬆愉快,是不是? Player Group - Pick every player in (All players) and do (Player - Add 500 to (Picked player) Current gold)
- 同样的,像Playable Map Area、Entire Map也是在初始化就建好等着被人随便乱叫的变数函数,以后看到可别乱砍。
- 而像Triggering Unit、Sold Item、……这种一看就知道是传回一个已存在的单位(物品),而不是先创一个再传回。该怎麽处理不用多说了吧?
- 所以以后杀人前要先睁大眼睛,亲朋好友不杀、朝廷命官不杀、皇帝宠妃不杀、……咳,扯远了,总之,不确定的话就先查一下blizzard.j和common.j吧。
- 本手册的触发器中英对照表中已将会造成记忆体问题的函数以红底标示,以区域为例,Initial Camera Bounds没标示,表示它只是传回一个已存在的物件;Region With Offset有标红底,表示它会先建立一个区域再传回,这种最好在用完后就把它删掉。祝屠杀愉快!
- 要杀乾淨篇
- 大家长大后大概多少会写到像这样的东西,以下是一个技能触发的片段:
- Code1: function Ampify_Damage_child takes nothing returns nothing
- call SetUnitLifeBJ( GetTriggerUnit(), RMaxBJ(( GetUnitStateSwap(UNIT_STATE_LIFE, GetTriggerUnit()) - GetEventDamage() ), 0.50) )
- endfunction
- function Trig_Ampify_Damage_Actions takes nothing returns nothing
- local trigger trg = CreateTrigger()
- call TriggerRegisterUnitEvent( trg, GetSpellTargetUnit(), EVENT_UNIT_DAMAGED )
- call TriggerAddAction(trg, function Ampify_Damage_child)
- call PolledWait(45.0)
- call DestroyTrigger(trg)
- endfunction
- 好啦,我承认范例很烂,JASS的范例很难找咩,不要强人所难了XD
- 回归正题,以上的程式码是否有记忆体漏失的问题?
- 嗯,乍看之下没有,实际上是有……。基本上它还漏了一个triggeraction。这样写才不会有这个问题:
- Code2: function Ampify_Damage_child takes nothing returns nothing
- call SetUnitLifeBJ( GetTriggerUnit(), RMaxBJ(( GetUnitStateSwap(UNIT_STATE_LIFE, GetTriggerUnit()) - GetEventDamage() ), 0.50) )
- endfunction
- function Trig_Ampify_Damage_Actions takes nothing returns nothing
- local trigger T = CreateTrigger()
- local triggeraction A = TriggerAddAction(trg, function Ampify_Damage_child)
- call TriggerRegisterUnitEvent( T, GetSpellTargetUnit(), EVENT_UNIT_DAMAGED )
- call PolledWait(45.0)
- call TriggerRemoveAction(T,A)
- call DestroyTrigger(T)
- endfunction
- 所以如果你常常写那种创临时触发来用的JASS,要记得连Action也一起删除喔。
- common.j中还有一个TriggerClearActions函数,它是把触发中的动作清空,但是不会真的把动作删掉。
- 我们可以想成,CreateTrigger建立一个触发传回;TriggerAddAction建立一个触发动作,把它连结到该触发,再传回触发动作。
- TriggerRemoveAction把触发动作和它与触发的连结关係删除;TriggerClearActions是把触发中与所有触发动作的连结切断,但是没有把那些触发动作删除。
- 等等,……action会造成记忆体漏失,那麽类似的event和condition呢?
- 每一个event都会佔不小的空间并造成leak。不过它就像是附着在trigger中的一部分,所以只要删trigger,event就会一併消失。 然而,如果一个触发摆了上百个event...那也是很佔资源的(trigger佔用量大约为event的1.5倍)。
- 顺带一提,在触发中注册事件是蛮耗资源的事,跑这一类的函数比跑大多数其它的函数慢得多。
- 而condition嘛……一般我们在创临时触发的时候都不写condition,把条件直接加在action裡,所以这个问题几乎不用考虑。
- 事实上和triggercondition相较之下,boolexpr反而比较有可能造成问题。我们通常在建立condition时都是这种格式:
- Code3: function Trig_Test_Conditions takes nothing returns boolean
- if ( not ( IsUnitType(GetTriggerUnit(), UNIT_TYPE_HERO) == true ) ) then
- return false
- endif
- return true
- endfunction
- function Trig_Test_Actions takes nothing returns nothing
- endfunction
- //===========================================================================
- function InitTrig_Test takes nothing returns nothing
- set gg_trg_Test = CreateTrigger( )
- call TriggerAddCondition( gg_trg_Test, Condition( function Trig_Test_Conditions ) )
- call TriggerAddAction( gg_trg_Test, function Trig_Test_Actions )
- endfunction
- Condition( function Trig_Test_Conditions )本身就先製造出一个conditionfunc,而TriggerAddCondition又製造一个triggercondition。那麽真要删的话,一定删到头昏眼花。所幸笔者测试conditionfunc和triggercondition的记忆体漏失情形,结果是无法观测(可能没有,可能有但是太小)。所以这部分可以放心。
- 变数清空篇
- 我们再来问:上面连triggeraction都删的龟毛函数(code2)还有没有记忆体问题。答桉是:有。
- 我勒!@#$()*&@$%&^!@%#!%#@!&^#@,到底要怎麽改才对?要这样:
- function Ampify_Damage_child takes nothing returns nothing
- call SetUnitLifeBJ( GetTriggerUnit(), RMaxBJ(( GetUnitStateSwap(UNIT_STATE_LIFE, GetTriggerUnit()) - GetEventDamage() ), 0.50) )
- endfunction
- function Trig_Ampify_Damage_Actions takes nothing returns nothing
- local trigger T = CreateTrigger()
- local triggeraction A = TriggerAddAction(trg, function Ampify_Damage_child)
- call TriggerRegisterUnitEvent( T, GetSpellTargetUnit(), EVENT_UNIT_DAMAGED )
- call PolledWait(45.0)
- call TriggerRemoveAction(T,A)
- call DestroyTrigger(T)
- set T = null
- set A = null
- endfunction
- 这个步骤称为变数清空(nullifying)。之所以连这个都要做,是由于B社的程式师偷懒,留下区域变数的bug。
- 详细原因后面会说明。只有区域变数会造成这个问题,全域变数不会。例如:
- function MyFunc takes nothing returns nothing
- local trigger T = CreateTrigger()
- call DestroyTrigger(T)
- set T = null
- endfunction
- 和
- function MyFunc takes nothing returns nothing
- set udg_T = CreateTrigger()
- call DestroyTrigger(udg_T)
- endfunction
- 两者同样都不会造成记忆体问题。
- 此外,这个问题只发生在物件变数,也就是像整数、实数、字串等都不会有问题。譬如以下这个无聊函数,你给电脑跑几十万遍也不会有记忆体问题: function MyFunc takes nothing returns nothing
- local string s = "实在太感谢Danny了!"
- local integer i = 520
- local real r = 5438.49
- local boolean IShouldStudyHarder = true
- endfunction
- 事实上和物件没删相比,物件区域变数没清空所造成的影响小非常多(笔者测试大约是1/12左右),更何况使用区域变数的人并没有那麽多。这个问题几乎是小到可以不用考虑,只是既然连物件都很龟毛地删了,最后加个几行清空变数的指令,好人做到底,应该也没什麽大不了吧?
- 以下提供一个简单的模型解释记忆体的运作原理与和物件的关係。这个模型只是为了方便说明,未必100%正确(想知道正确理论的请去找B社的程式师);而裡面提供的数据仅作为举例用,未必是确切的数值。电脑方面并非笔者的专业,如果有哪位大大对这方面有更进一步的了解,敬请不吝指教。 变数 编号表
- 名称 类型 内容(物件编号) 编号 变数连结 物件位址
- udg_Archmage unit 1048802 1048801 0 21~25
- udg_Paladin unit 1048807 1048802 2 1~20
- udg_MyHero unit 1048802 1048803 1 31~40
- udg_GameTimer timer 1048803 1048804 0 26~30
- (local) target unit 1048809 1048805 1
- udg_P1 location 1048808 1048806 0
- udg_P2 location 1048805 1048807 1 41~60
- udg_FootmanGuard unit 1048805 1048808 1 121~125
- 1048809 1 81~100
- 1048810 0 101~120
- 这是记忆体内部大致的配置情形,它大约是这样运作的:
- 删除物件:假设我们要删除一个点: call RemoveLocation(udg_P1)。电脑会去读udg_P1对到的编号表1048808号,把对应的物件121~125区域清空,并且更新编号表。这是删除后的情形: 变数 编号表
- 名称 类型 内容(物件编号) 编号 变数连结 物件位址
- udg_Archmage unit 1048802 1048801 0 21~25
- udg_Paladin unit 1048807 1048802 2 1~20
- udg_MyHero unit 1048802 1048803 1 31~40
- udg_GameTimer timer 1048803 1048804 0 26~30
- (local) target unit 1048809 1048805 1
- udg_P1 location 1048808 1048806 0
- udg_P2 location 1048805 1048807 1 41~60
- udg_FootmanGuard unit 1048805 1048808 1
- 1048809 1 81~100
- 1048810 0 101~120
- 建立物件:假设我们要建立一个山王。电脑会从记忆体中找一大块可用的位址画给它用,假设是61~80号。然后搜寻变数连结=0且物件位址为空的空间建立物件。这是建立后的情形: 变数 编号表
- 名称 类型 内容(物件编号) 编号 变数连结 物件位址
- udg_Archmage unit 1048802 1048801 0 21~25
- udg_Paladin unit 1048807 1048802 2 1~20
- udg_MyHero unit 1048802 1048803 1 31~40
- udg_GameTimer timer 1048803 1048804 0 26~30
- (local) target unit 1048809 1048805 1
- udg_P1 location 1048808 1048806 0 61~80
- udg_P2 location 1048805 1048807 1 41~60
- udg_FootmanGuard unit 1048805 1048808 1 121~125
- 1048809 1 81~100
- 1048810 0 101~120
- 那个61~80八成就是记录山王的生命啦、魔力啦、技能啦、经验值啦、……等等有的没有的资料。
- 或许有人会问了:1048805不是没有物件吗?为什麽不建立在这裡? 当然这是为了防止bug,想想为什麽1048805有被变数连结却没有值?也许它之前是连到一个武士,后来那个武士在一场战斗中壮烈牺牲了,系统就很聪明地把它从记忆体中挪走。然后变成一开始那样。但是它仍旧被一个变数udg_FootmanGuard连结,假设我们把山王创造在这个地方,那麽我们会发现,udg_FootmanGuard本来一直是指一个武士,后来武士死了,有一天udg_FootmanGuard突然变成一个山王……这当然不合理,武士死后udg_FootmanGuard理所当然要一直指向空的物件才行。所以只要有被变数连结,那个位置就不能被使用,即使它是空的。
- 修改变数:假设我们修改变数: set udg_MyHero = udg_Paladin。电脑会把udg_MyHero重新连到1048007号,并且修改编号表中的变数连结个数: 变数 编号表
- 名称 类型 内容(物件编号) 编号 变数连结 物件位址
- udg_Archmage unit 1048802 1048801 0 21~25
- udg_Paladin unit 1048807 1048802 1 1~20
- udg_MyHero unit 1048807 1048803 1 31~40
- udg_GameTimer timer 1048803 1048804 0 26~30
- (local) target unit 1048809 1048805 1
- udg_P1 location 1048808 1048806 0
- udg_P2 location 1048805 1048807 2 41~60
- udg_FootmanGuard unit 1048805 1048808 1 121~125
- 1048809 1 81~100
- 1048810 0 101~120
- 清空变数:假设我们清空变数: set udg_MyHero = null。电脑会把udg_MyHero连到空号(可能是0),并且修改编号表中的变数连结个数: 变数 编号表
- 名称 类型 内容(物件编号) 编号 变数连结 物件位址
- udg_Archmage unit 1048802 1048801 0 21~25
- udg_Paladin unit 1048807 1048802 1 1~20
- udg_MyHero unit 0 1048803 1 31~40
- udg_GameTimer timer 1048803 1048804 0 26~30
- (local) target unit 1048809 1048805 1
- udg_P1 location 1048808 1048806 0
- udg_P2 location 1048805 1048807 1 41~60
- udg_FootmanGuard unit 1048805 1048808 1 121~125
- 1048809 1 81~100
- 1048810 0 101~120
- 删除区域变数:最后是我们的重点,假设我们的某个函数(其中有一个区域变数target)执行到endfunction,此时target会被清除。电脑会把target这个变数清掉, 此时1048809的变数连结理当被改成0,但是它并不会(应该是一个bug): 变数 编号表
- 名称 类型 内容(物件编号) 编号 变数连结 物件位址
- udg_Archmage unit 1048802 1048801 0 21~25
- udg_Paladin unit 1048807 1048802 2 1~20
- udg_MyHero unit 1048802 1048803 1 31~40
- udg_GameTimer timer 1048803 1048804 0 26~30
- 1048805 1
- udg_P1 location 1048808 1048806 0
- udg_P2 location 1048805 1048807 1 41~60
- udg_FootmanGuard unit 1048805 1048808 1 121~125
- 1048809 1 81~100
- 1048810 0 101~120
- 现在,假设我们执行call RemoveUnit(target)以后离开函数,结果变成: 变数 编号表
- 名称 类型 内容(物件编号) 编号 变数连结 物件位址
- udg_Archmage unit 1048802 1048801 0 21~25
- udg_Paladin unit 1048807 1048802 2 1~20
- udg_MyHero unit 1048802 1048803 1 31~40
- udg_GameTimer timer 1048803 1048804 0 26~30
- 1048805 1
- udg_P1 location 1048808 1048806 0
- udg_P2 location 1048805 1048807 1 41~60
- udg_FootmanGuard unit 1048805 1048808 1 121~125
- 1048809 1
- 1048810 0 101~120
- 显然这时候1048809的位置早该被清空等着其它的物件放,可是却由于这个bug,导致这个空间不能再被使用。 所以我们只好在区域变数被系统自动删除前,手动把它的内容清空,使它对应的编号表的变数连结被扣掉。如果没有这样做,久而久之,就有一大堆空间被佔据住不能使用,就成了 记忆体漏失。
- 最后再重申一次,这个模型和裡面写的数字纯粹作为举例用,一个location未必只佔用5个位址;物件也未必是从0开始往上编;编号表也不一定是从1048801开始。
- 还没清完篇
- 看完上一篇大家感想如何?嗯……以后写JASS,最后面记得加个几行清空的指令吧(谁叫你就是爱用区域变数?)。
- 不过这个才是真的麻烦: function bird takes unit whichUnit returns location
- local location loc = GetUnitLoc(whichUnit)
- //这裡有一段不足为外人道,惨不忍赌,血泪纵横的运算...
- return loc
- endfunction
- 当然我们知道要把区域变数loc清空,问题是清空了怎麽回传loc?
- 这时候只好祭出我们的大神--return bug来了,请看: function H2I takes handle h returns integer
- return h
- return 0
- endfunction
- function bird takes unit whichUnit returns location
- local location loc = GetUnitLoc(whichUnit)
- local integer li
- //这裡有一段不足为外人道,惨不忍赌,血泪纵横的运算...
- set li = H2I(loc)
- set loc = null
- return li
- return null
- endfunction
- 这时候一定有高手会问啦:
- ㄟ,你根本在乱写嘛!那个H2I函数不是要传入handle吗?怎麽传入location?那个H2I函数不是要传回整数吗?怎麽传回点?那个鸟(bird)函数明明要传回点,你怎麽传回整数?
- 关于第一个问题,如果你会问建议你重新阅读一遍JASS教学。因为location是handle的子类型,所以当然也算是handle,当然可以传入。
- 至于后面两个问题……没错,正常来说你的世界编辑会一如往常地冒出一段废话要你赶快修正,不过这次没有,所以它叫做回传错误(return bug)。
- 这个错误是由于世界编辑器在检查函数的回传时,会从最下面(endfunction那行)往上查,只要查到一个正确的return就停了,不会再管上面有没有错误的return。
- 譬如H2I函数,WE只检查return 0是正确的就通过,根本不管上面有个错误的return h
- 那麽我使用这个回传错误干麻呢?它的用途可广了,你可以用这个函数随便地把单位变成物品、把物品变成触发、把触发变成整数、把整数转成点、……。简单地说,可以做到物件间以及物件和整数间的自由转换。物件间的转换没有问题,可是物件转成整数怎麽转呢?它会把物件在记忆体内部的编号传出来,换句话说,每一个物件会传回一个唯一的整数,就像身分证字号一样。同样地,只要把记忆体内码(整数)输入,就可以回传出对应的物件。这个bug主要用在物件和整数间的自由转换,想也知道,如果你把单位当成物品交给电脑去设它的物品数,显然十之八九会出问题,轻则没效,重则当掉。把物件和字串互转会当机,至于布林和实数笔者没试过, 有兴趣的读者可自行测试,不过那似乎没有什麽实际的用途。
- 我们先解决这个问题,首先我们定义区域变数loc,然后进行一段操作,接着把loc的内部编码存到整数变数li,接着把loc变数清空,然后回传li,li当然会自动变成对应在记忆体内的点,最后,li是整数变数,所以不会有变数未清空的问题。所以,OK!圆满解决!收工!
- 附带一提,这个return bug转换配合游戏暂存(Game Cache)可以允许你製作无限多的Custom Value。
- 我们只要先定义一些转换用函数: function H2I takes handle h returns integer
- return h
- return 0
- endfunction
- function I2U takes integer i returns unit
- return i
- return 0
- endfunction
- function I2T takes integer i returns trigger
- return i
- return 0
- endfunction
- function I2Ti takes integer i returns timer
- return i
- return 0
- endfunction
- ...
- (为什麽不用一个I2H代替全部?……ㄟ,儿子可以代替老子,老子可不能代替儿子啊!如果一个函数takes handle,你可以传unit进去;可是一个函数如果takes unit,你就不能放handle进去,谁知道你的handle不会是trigger或者其它类型的东西)
- 我们可以把一个单位转成整数再转成字串当做资料夹名,随便取个档名如"UnitLifeTrigger",然后把单位对应的触发转成整数存进去。再用类似的方法读取之。
- 换句话说,我们可以随便地在物件之间做连结,有一个就能找出其它个,这对于製作多重施法的触发技能是非常好用的功能 。不过当然,游戏暂存的存取速度,相较于一般正统的变数或者内建的Custom Value为慢。至于return bug怎麽应用,笔者也没办法教太多,请自行上国外论坛深造。
- 既然这是个BUG,以后要是有一天B社改版把它修正了怎麽办?别担心,大概是这个bug太有用了,B社之前公开说明:「魔兽争霸III的世界编辑器日后永远不会修正return bug」。因此,放手去做吧!
- 笔者废话篇
- 看完以后有没有觉得世界末日到了?点、单位群组、触发要删不打紧,连触发的动作还要另外删,定区域变数还要清空,如果要回传甚至还得扯上return bug……
- 不过大家大可不必那麽担心,还记得我们解决记忆体漏失的目的吗--减少游戏lag和减少跳出的延迟(后者是我自己偷加的XD)。
- 不要看前面说得那麽可怕,那些高频率的执行对电脑而言只是小事一桩,倒是魔兽各方面的运算,3D贴图的计算等等反而更耗用资源。小问题累积很久才可能变成大问题,而我们通常在发生大问题还没发生以前就结束游戏了。以点的累积来说,大约10~30万以上会感觉到明显的跳出延迟,再更多才会造成游戏中的lag。区域变数没清空造成的记忆体漏失与此相比之下更加微不足道(大约是触发影响力的1/12左右)。而触发动作(triggeraction)虽然严重程度和点差不多,可是它实在是非常非常非常非常不好删,所以除非你太常用临时触发,问题严重,否则还是可以当做没看到。
- 以B社的官方地图为例,它们其实也只做到删除点、单位群组、特效与漂浮文字,大致上用到的是本文「实用篇」的方式。其它像触发、触发动作、变数清空等,B社根本没有在管。B社提供的blizzard.j函数中用的区域变数(虽然用到的不多)也没有做到变数清空。然而大家玩B社出的地图会很不顺吗?
- 记住,我们左删右删的最终目的是增加游戏的顺畅度。一条触发或函数,除非解决记忆体漏失不会太麻烦,或者它的使用频率实在太高,漏洞很严重,才有必要去处理;否则都可以不理它。很多老外天天在写JASS,常把memory leak挂在嘴边,不过它们主要是为了严谨性的考量,不希望有人使用了他们的函数或系统后出现lag等症状,所以对此方面的要求较高。 笔者建议像点、单位群组、特效最好都删掉;而像triggeraction和需要用到return bug来清空变数的麻烦事就免了;其它的……自行判断。
- 总之我们製作地图,只要跑起来顺就好了,记忆体漏失把主要的做好即可,次要的、不重要的就可以马虎一点,不 必把时间浪费在不重要的事情上。(谜:那你写这一串废话不是也在浪费时间吗? 我:……………………)
- 最后,总结一下重点:
- 要搞清楚什麽可以删,什麽不能删。千万别把重要的公用物件变数删除了。
- 除了trigger以外,event和triggeraction也会造成leak。此外,注册event比起大多数的函数更耗资源,执行更慢
- 如果有使用handle类的区域变数,还要再做变数清空(nullify)的动作才能完全清除。如果有困难可利用return bug解决。区域变数未清空造成的leak不太严重,如果嫌麻烦可以忽略。
- return是一个被JASS高手广泛研究的工具,有兴趣可多爬文研究
复制代码 |
|