三、記憶體漏失的解決方法(進階篇)
首先先聲明,以下是寫給功力高深,動輒落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
這個步驟稱為變數清空(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左右)。更何況使用區域變數的人並沒有那麼多,這個問題幾乎是小到可以忽略,事實上,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 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,最後面記得加個幾行清空的指令吧(誰叫你就是愛用區域變數?)。
不過像這種函數怎麼辦呢?//這個函數是實際可用的,它比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.改用全域變數
[ 本帖最后由 jiwalv 于 2007-12-21 14:05 编辑 ]