記憶體漏失(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))就是一個點。可以想成,我要電腦在操場中間產生一個步兵,但是電腦不懂什麼叫做 「操場中間」。所以我到操場中間插一個旗子(也就是一個點),之後我就命令電腦在那個旗子的位置產生一個步兵。好了,現在步兵產生了,那麼旗子呢?因為我沒有刪掉它,所以它會一直留在記憶體中。一兩個不打緊,但是如果這一類的動作很多,記憶體裡堆了數十萬個沒有用的東西,跑起來效率自然大打折扣。

同樣的情形也發生在其它的物件中。對了,什麼是「物件」?籠統地說,只要是遊戲中一個具體的東東就是物件,除了布林(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 <gen>))

    建立了一個部隊群組及一個點

  • 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 <gen>
 Conditions
 Actions
  Unit - Order (Entering unit) to Attack-Move To (Center of DemonBase1 <gen>)
那麼,每當有一個部隊進入了HumanBase1,就會產生一個點(Center of DemonBase1 <gen>),而且產生的點都不會被刪除?

這些點既然都位於同樣的位置,為什麼不重複使用呢?所以為了環保起見,我們可以改寫成這樣:

Map Init
 Events
  Map initialization
 Conditions
 Actions
  Set DemonBase[1] = (Center of DemonBase1 <gen>)
  ……

Unit Attack 1
 Events
  Unit - A unit enters HumanBase1 <gen>
 Conditions
 Actions
  Unit - Order (Entering unit) to Attack-Move To DemonBase[1]

也就是預先把可以多次利用的物件用變數記錄下來,然後重複使用這個物件,減少新物件的產生。

刪除使用過且不再需要的新建物件

首先介紹刪除的方法(需JASS)

  • trigger (觸發器)--Custom script: call DestroyTrigger(xxx)

  • location (點)--Custom script: call RemoveLocation(xxx)

  • group (部隊群組)--Custom script: call DestroyGroup(xxx)
    ※注意Clear Group是清除部隊群組中的部隊資料,而不是把整個部隊群組刪除

  • force (玩者群組)--Custom script: call DestroyForce(xxx)
    ※注意ForceClear是清除玩者群組中的玩者資料,而不是把整個玩者群組刪除

  • 部隊--死亡的部隊於屍體消失之際,系統會自動移除。所以不用特別去管它。像隱藏施法部隊這種用後不理的,要刪的話可以用GUI的Unit - Remove、Unit - Kill、Unit - Add Expiration Timer,甚至把生命回復設成負值讓它自動死亡都可以

  • 物品--被用掉的物品,系統會自動移除。所以不用特別去管它。真的要刪的話用GUI的Item - Remove即可

  • lightning (閃電特效)--用GUI的Lightning - Destroy Lightning Effect

  • effect (特效)--用GUI的Special Effect - Destroy

  • texttag (浮動文字)--用GUI的Floating Text - Destroy、Floating Text - Change Lifespan皆可。前者是直接刪除;後者是設定它的壽命,時間一到會自動被刪除

  • 影像、貼圖、任務、……--同樣用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 <gen>
 Conditions
 Actions
  Set P1 = (Center of DemonBase1 <gen>)
  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
bj_wantDestroyGroup是Blizzard.j裏的一個全域變數,預設為false。在執行Blizzard.j中和Unit Group有關 的函數時,會先檢查bj_wantDestroyGroup,決定是否移除傳入的Unit Group,然後自動把bj_wantDestroyGroup 還原為 false。例如:

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設為假。

除了Pick Every Unit In group…以外,Blizzard.j中還有這些函數會檢查bj_wantDestroyGroup並決定是否刪除當作引數傳入的group:

  • Pick every unit in Unit Group and do Action(ForGroupBJ)

  • All units of Unit Group are dead(IsUnitGroupDeadBJ)

  • Unit Group is empty(IsUnitGroupEmptyBJ)

  • Number of units in Unit Group(CountUnitsInGroup)

  • Random unit from Unit Group(GroupPickRandomUnit)

  • Unit Group - Add Unit Group(GroupAddGroup)

  • Unit Group - Remove Unit Group(GroupRemoveGroup)

同理可以如下運用:

Custom script: set bj_wantDestroyGroup = true
If ((Number of units in (Units in (Playable map area))) Greater than 50) then do (Skip remaining actions) else do (Do nothing)
(執行到Number of units in Unit Group時,檢查bj_wantDestroyGroup為真,即刪除(Units in (Playable map area))

Custom script: set bj_wantDestroyGroup = true
If ((All units of (Units owned by Player 1 (Red)) are dead) Equal to True) then do ------------ else do (Do nothing)
(執行到All units of Unit Group are dead時,檢查bj_wantDestroyGroup為真,即刪除(Units owned by Player 1 (Red)))

如果希望更了解這個方法的運作原理,可以去看Blizzard.j中的函數(搜尋bj_wantDestroyGroup即可以找到許多相關資料)。

使用自訂的Group變數時,有兩大類常見用法:

  1. 新增-清空法(重複利用):

    1. 初始化時建立Group變數及Group

    2. 在此變數中加入部隊(例:Unit Group - Add (Triggering unit) to ToTGroup)

    3. 使用此變數

    4. 清空此變數

    5. 重複或不重複2~4

    此做法屬重複利用的方式,不要把Group刪除

  2. 設定-刪除法(非 重複利用):

    1. 初始化時建立Group變數及Group

    2. 設定此變數的值為某個部隊群組(例:Set ToTGroup = (Last created unit group))

    3. 使用此變數

    4. 清空此變數

    5. 重複或不重複2~4

    此做法非重複利用的方式,可以把Group刪除

此外由於B社的疏失,Units Of Type(GetUnitsOfTypeIdAll)使用此用法會有bug, 不但無法正規選到所要的部隊,也會造成其它地方的bj_wantDestroyGroup判斷出錯。請不要對它這樣用。

舉例來說,這樣的一段程式碼會出問題:

Custom script: set bj_wantDestroyGroup = true
Unit Group - Pick every unit in (Units of type Footman) and do (Actions)
 Loop - Actions
  ……

這裡筆者不想多著墨,因為解釋這個錯誤的詳細結果沒什麼意義,如有興趣請自行查詢Blizzard.j中的GetUnitsOfTypeIdAll函數,並且自己去做實驗。

如果你無法安心地使用這個指令,也可以參照刪除點的作法。同樣要記得加上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倍,換句話說,兩個event就比一個trigger大了,這也是為什麼筆者不推薦全地圖部隊受到傷害事件的原因。
順帶一提,在觸發中登錄事件是蠻耗資源的事,跑這一類的函數比跑大多數其它的函數慢得多。
而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(T, 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 

這個步驟稱為變數清空(nullify)。之所以連這個都要做,是由於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左右)。更何況使用區域變數的人並沒有那麼多,這個問題幾乎是小到可以忽略,事實上,B社在blizzard.j撰寫的所有函數,也沒有做到這個動作。關於是否有必要清空,可以看個人需要,有些人就是很龜毛,不能容忍一絲一毫的浪費;而有些人則嫌那幾行清空指令太礙眼,目前兩派的人都有,讀者可自己決定要怎麼做。

此外有人發現,在極少數的情況下,做變數清空以後會影響物件的編碼,造成使用return bug傳回的值發生一些問題,這個現象最常發生在Timer的清空上。解決的辦法當然就是--不要做變數清空。

以下提供一個簡單的模型解釋記憶體的運作原理與和物件的關係。這個模型只是為了方便說明,未必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 1048802 1048803 0 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
    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 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
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
1048810 0 101~120

顯然這時候1048809的位置早該被清空等著其它的物件放,可是卻由於這個bug,導致這個空間不能再被使用。
所以我們只好在區域變數被系統自動刪除前,手動把它的內容清空,使它對應的編號表的變數連結被扣掉。
如果沒有這樣做,久而久之,就有一大堆空間被佔據住不能使用, 造成記憶體漏失。

最後再重申一次,這個模型和裡面寫的數字純粹作為舉例用,一個location未必只佔用5個位址;物件也未必是從0開始往上編;編號表也不一定是從1048801開始。

還沒清完篇

看完上一篇大家感想如何?嗯……以後寫JASS,最後面記得加個幾行清空的指令吧(誰叫你就是愛用區域變數?)。

不過像這種函數怎麼辦呢?

//這個函數是實際可用的,它比blizzard.j中的GetUnitsOfTypeIdAll漂亮且有效率
function GetUnitsOfType takes integer id returns group
    local group g = CreateGroup()
    call GroupEnumUnitsOfType(g,UnitId2String(id),null)
    return g
endfunction

當然誰都知道要把區域變數g清空,問題是清空了怎麼回傳group?

這時候我們可以利用那個leak的特性,以神奇的「包裝法」解決這個問題。因為沒有定義區域變數g,所以自然可以不必清空:

//這個函數是實際可用的,它比blizzard.j中的GetUnitsOfTypeIdAll漂亮且有效率
function GetUnitsOfType_core takes group g, integer id returns group
    call GroupEnumUnitsOfType(g,UnitId2String(id),null)
    return g
endfunction

function GetUnitsOfType takes integer id returns group
    return GetUnitsOfType_core(CreateGroup(),id)
endfunction

當然,這個方法也可以用在前面的情形。例如前面的某段程式碼也可以改寫成這樣,不管要定義幾個物件型的全域變數,都可以套用此方法:

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_Core takes trigger T, triggeraction A returns nothing
    set T = CreateTrigger()
    set A = TriggerAddAction(T, function Ampify_Damage_child)
    call TriggerRegisterUnitEvent( T, GetSpellTargetUnit(), EVENT_UNIT_DAMAGED )
    call PolledWait(45.0)
    call TriggerRemoveAction(T,A)
    call DestroyTrigger(T)
endfunction

function Trig_Ampify_Damage_Actions takes nothing returns nothing
    call Trig_Ampify_Damage_Actions_Core(null, null)
endfunction

還有另一個方法,就是祭出我們的大神--return bug。像這樣:

//這個函數是實際可用的,它比blizzard.j中的GetUnitsOfTypeIdAll漂亮且有效率
function GetUnitsOfType takes integer id returns group
    local integer g = H2I(CreateGroup())
    call GroupEnumUnitsOfType(I2G(g),UnitId2String(id),null)
    return g
    return null
endfunction

(不懂什麼是H2I和I2G函數的話,請參見JASS教學 - 物件的身分證字號

筆者廢話篇

看完以後有沒有覺得世界末日到了?點、部隊群組、觸發要刪不打緊,連觸發的動作還要另外刪,定區域變數還要清空,如果要回傳甚至還得扯上return bug…… 不過大家大可不必那麼擔心,還記得我們解決記憶體漏失的目的嗎--減少遊戲lag和減少跳出的延遲(後者是我自己偷加的XD)。

不要看前面說得那麼可怕,那些高頻率的執行對電腦而言只是小事一樁,倒是魔獸各方面的運算,3D貼圖的計算等等反而更耗用資源。小問題累積很久才可能變成大問題,而我們通常在大問題還沒發生以前就結束遊戲了。以點的累積來說,大約10~30萬以上會感覺到明顯的跳出延遲,再更多才會造成遊戲中的lag。區域變數沒清空造成的記憶體漏失與此相比之下 ,更加微不足道(大約是觸發影響力的1/12左右)。而觸發動作(triggeraction)雖然嚴重程度和點差不多,可是它實在是非常非常非常非常不好刪,所以除非你太常用臨時觸發,問題嚴重,否則當做沒看到對身心較有幫助。

以B社的官方地圖為例,它們其實也只做到刪除點、部隊群組、特效與浮動文字,大致上用到的是本文「實用篇」的方式。其它像觸發、觸發動作、變數清空等,B社根本沒有在管。B社提供的blizzard.j函數中用的區域變數(雖然用到的不多)也沒有做到變數清空。然而大家玩B社出的地圖會很不順 ,或者狂lag嗎?

記住,我們左刪右刪的最終目的是增加遊戲的順暢度。一條觸發或函數,除非解決記憶體漏失不會太麻煩,或者它的使用頻率實在太高,漏洞很嚴重,才有必要去處理;否則都可以不理它。很多老外天天在寫JASS,常把memory leak掛在嘴邊,主要是為了嚴謹性的考量,不希望有人使用了他們的函數或系統後出現lag等症狀,所以對此方面的要求較高。 筆者建議像點、部隊群組、特效最好都刪掉;而像triggeraction和需要用到return bug來清空變數的麻煩事就免了;其它的……自行判斷。總之我們製作地圖,只要跑起來順就好了,記憶體漏失把主要的做好即可,次要的、不重要的就可以馬虎一點,不必把時間浪費在不重要的事情上。(謎:那你寫這一串廢話不是也在浪費時間嗎? 我:……………………)

最後,總結一下本篇的重點:

  • 要搞清楚什麼可以刪,什麼不能刪。千萬別把重要的公用物件變數刪除了。

  • 除了trigger以外,event和triggeraction也會造成leak。此外,登錄event比起大多數的函數更耗資源,執行更慢。

  • 處理區域變數造成的微量記憶體漏失:

    1. 做奱數清空(nullify)的動作

    2. 使用「包裝法」

    3. 使用return bug

    4. 改用全域變數

綜合教學/記憶體漏失.txt · 上一次變更: 2007年11月11日 4:32 pm 來自 wasabi
www.chimeric.de Creative Commons License Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0