本帖最后由 hatsutoli 于 2013-2-10 19:53 编辑
The Elder Scrolls:Oblivion Construction Set腳本優化進階教程
(字多,圖少,初心者&玩家可略過)
作者:Asaba Hayato淺羽隼人(Heiji Hatsutoli,過去3DM的hatsutoli) 前言
很久沒在版上發文章了,沒錯,至少三年…有了吧,不知道還有多少人認得我XD暫時脫離上古卷軸好一陣子,去尋找生命的意義(?),但最近因緣際會又接觸了上古,想說野人獻曝一點東西給曾經給過我很大支持,也陪我成長的3DM論壇Oblivion版,希望對一些有心想寫腳本但卻覺得力不從心的Mod製作者有些許幫助。
我們都知道當Oblivion模組裝多了會容易CTD,但其中最主要的原因是因為大部分的模組製作者都不知道怎麼優化他們自己寫的腳本(包括三年前的我在內),會造成系統超載往往是因為每個有使用到Script腳本語言在遊戲中執行的模組,都在同一個frame底下(約每1/30秒)跑太多串無效的Script,以程式語言的專用術語來說就是「時間複雜度」太大,演算法不夠有效率,所以以下介紹如何在最常被使用的Gamemode模式下如何透過簡單的return與stopquest來進行遊戲腳本的優化。
Gamemode Scripts
Frame的概念簡單來說,是指呈現連續動畫所需的每秒靜止畫面張數,通常為每秒29.97個Frame,而常聽見的Frame-per-second就是每秒幾個靜止畫面的專有名詞縮寫,當一台電腦的FPS值太低時,你看到的遊戲動畫就會出現延遲或停滯的現象。而Oblivion的Gamemode Block是指在遊戲模式底下,也就是玩家不是正開啟物品欄的狀態下,以一個frame為單位跑一次的ScriptBlock,也就是每秒至少會跑29次以上,假設你Gamemode底下有十行Script,那遊戲每秒就會消耗額外的系統資源去執行290行你的Script,以此類推。
因此,如果你要我說實話,置放在隱藏Quest底下執行的Gamemode腳本是最「髒」也最偷懶的Script寫法,這種在背景以frame為單位大量執行的Script遊戲模組,不僅吃掉大量的記憶體空間,也使得遊戲模組本身非常不穩定,故障機率相當高。因此,當你能使用其他Begin模式如OnLoad、OnEquip或ScriptEffectStart的時候,請盡量別使用Gamemode掛載在Quest底下來執行你想要的功能。但理論雖是如此,有很多很酷的MOD都在使用Gamemode,那要怎麼優化Script讓Quest腳本中的Gamemode對玩家電腦的殺傷力減到最低呢?以下我用範例來逐一說明。
如果你說:「我的遊戲模組無論如何都要使用Gamemode block才能實現許多功能。」,那我會建議你用IF-ELSE來讓你的Script只在條件符合時執行。舉例來說,如果你需要在玩家於遊戲中碰觸一個按鈕(通常為Activator)時執行一個物品腳本,那你必須在該按鈕(Activate)下掛上這樣的腳本(我以The Elder Scrolls Construction SetWiki的範例先做說明):
scn YourSwitchScript
short Working
begin onActivate
set Working to 1
end
然後在你想要執行腳本的物品或任務底下這樣寫:
scn YourItemScript
begin GameMode
if YourSwitchScript.Working
... ;放上所有你想要在玩家按下按鈕時執行的酷炫腳本
endif
end
然而,The ElderScrolls系列的遊戲模組編輯器自從三代(Morrowind)以來就沒有多大改變,根據Morrowind的遊戲模組社群提供的開發經驗,即使我們在Gamemode底下使用if-else去防止程式碼在不滿足條件的情況下被執行,遊戲引擎本身還是會跑完所有if-else裏頭的程式碼,直到它找到一個出口(endif或return)為止。
有聽沒有懂?沒關係,我們先來看看優化過的腳本應該長怎樣。
當If-Else blocks碰上 Return函式-重要Script優化撰寫原則
也就是說,if-else的理想條件判定就跟定義求職條件一樣,舉例來說,我開女僕咖啡廳,定義我要招募的員工的先決條件是「女性」,那只要不滿足「女性」的這個先決條件,我就以下甚麼履歷都不用看了,我不需要去關心他的「學歷」,不需要關心他的「服務熱誠」,不需要關心他「懂多少種外語」…等等,因為這些都是在滿足「女性」的前提下才需要去關心的次要條件;又,我們不太可能在問一個人「有沒有結婚」之前,就問他「你太太在哪裡上班」,當然要先結婚才有老婆啊!
;; 未經優化的腳本
begin GameMode
if (某些條件) != 0
(一些沒效率的/複雜的演算法)
endif
end
乍看下沒有問題?但我們之前也說過了,上古卷軸系列(或該說Bethesda這家公司)愛用的遊戲引擎對腳本的運算原則是:即使碰上if-else了,在沒有碰到endif或者return做為出口前,仍必須一行一行把程式碼跑完,因此就造成在不符合條件前提時運算資源的大量浪費。那種無效的浪費,就像是明明知道很遠的地方的一家餐廳沒開了,卻還是特地以一兩個小時的車程跑到該店門口,看到「休業」,然後才回家(我一時只想到這種生活的舉例XD)。
那麼,優化過的程式碼差在哪裡呢?請看:
;; optimized優化後的腳本
begin GameMode
if (某些條件) == 0
;; logical negation前提條件不成立,邏輯否定
RETURN
;; 因為前提條件不成立所以呼叫了RETURN,以下都將不會被運算,會直接略過
endif
(一些沒效率的/複雜的演算法)
end
最主要的邏輯差別,就在於加上「如果餐廳歇業,我們就不去」的這個前提,告訴遊戲引擎如果「前提不符」,就「RETURN(中文意思的回去,就是要系統略過以下腳本)」,夠簡單明瞭吧?這樣的好處在於,如果前提不符,在同樣的效果下,有優化的腳本將比沒優化的腳本,節省大量的運算資源(一行return與好幾行根本不會執行的複雜腳本相比)。
因此,經常在你所設計的複雜腳本中,提早呼叫條件不符時的RETURN,會有助於動態的節省遊戲引擎運算資源。
進階範例:Hatsutoli Tiefling Races 1.9.1腳本優化-我做了甚麼修改?
以下範例的複雜度可能會讓人頭痛所以我放到最後來舉例XD,不想看的人可以跳過XD。
以下,用我先前寫過的TieflingRaces中判斷該給玩家甚麼惡魔化能力的腳本來進行「未經優化的程式碼」的恐怖錯誤範例說明:
;; 未經優化的腳本範例
Begin Gamemode
if player.getisrace tiefling == 1 || player.getisrace tieflingCharacteristic == 1 || player.getisrace tieflingTail == 1 || player.getisrace tieflingTailCharacteristic == 1 || player.getisrace TieflingTailCharacteristicWings == 1 || player.getisrace TieflingTailwithWings == 1
if player.getitemcount 000DemonCompleteBody == 0
player.additemNS 000DemonCompleteBody 1
endif
if player.getequipped 000DemonCompleteBody == 1 && player.hasspell 001DemonicHound == 0
player.addspellNS 001DemonicHound
elseif player.getequipped 000DemonCompleteBody == 0 && player.hasspell 001DemonicHound == 1
player.removespellNS 001DemonicHound
endif
endif
if TransformBlock == 0
if player.getisrace tiefling == 1 || player.getisrace tieflingCharacteristic == 1 || player.getisrace tieflingTail == 1 || player.getisrace tieflingTailCharacteristic == 1 || player.getisrace TieflingTailCharacteristicWings == 1 || player.getisrace TieflingTailwithWings == 1
if player.getisrace TieflingTailCharacteristicWings == 1 || player.getisrace TieflingTailwithWings == 1
if player.hasspell 001DemonicFlight == 0
player.addspellNS 001DemonicFlight
endif
else
if player.hasspell 001DemonicFlight == 0 && player.hasspell 001FlyingTired == 1
player.removespellNS 001FlyingTired
endif
endif
if player.hasspell 001TieflingAbility == 0
player.addspellNS 001TieflingAbility
endif
if player.hasspell 001TieflingEyes == 0
player.addspellNS 001TieflingEyes
endif
if DemonicHorseCheck == 2 && player.hasspell 001TieflingStallionSummon == 0
player.addspellNS 001TieflingStallionSummon
endif
if player.hasspell 001DemonicTransform == 0 && getstage 001TieflingTraining >= 60 && getglobalvalue transformlevel == 0
player.addspellNS 001DemonicTransform
elseif player.hasspell 001DemonicTransform2 == 0 && getstage 001TieflingTraining >= 60 && getglobalvalue transformlevel == 2
player.addspellNS 001DemonicTransform2
elseif player.hasspell 001DemonicTransform3 == 0 && getstage 001TieflingTraining >= 60 && getglobalvalue transformlevel == 3
player.addspellNS 001DemonicTransform3
elseif player.hasspell 001DemonicTransform4 == 0 && getstage 001TieflingTraining >= 60 && getglobalvalue transformlevel == 4
player.addspellNS 001DemonicTransform4
elseif player.hasspell 001DemonicTransform5 == 0 && getstage 001TieflingTraining >= 60 && getglobalvalue transformlevel == 5
player.addspellNS 001DemonicTransform5
elseif player.hasspell 001DemonicTransform6 == 0 && getstage 001TieflingTraining >= 60 && getglobalvalue transformlevel == 6
player.addspellNS 001DemonicTransform6
endif
endif
endif
…知道多恐怖了嗎?算了一下大概有45行,而以每秒29.97次的速度在背景執行,也就是哪個不幸的玩家要是裝了我在三年前寫的這種爛模組,他的上古卷軸每秒鐘就必須多承受至少1348.65行的Script運算資源。條列來說,上面的腳本犯了以下幾個主要錯誤:
1. 沒有將if-else主要條件和次要條件分開,大量重複使用已經檢驗過的主要條件。
2. If-else階層沒有畫清楚,容易造成階層混亂。
3. 只使用endif作為Script的出口,使遊戲引擎即使運算條件不符,每次仍必須跑到腳本最後一行才能結束運算。
4. 將應該同時判斷的狀況按照前後順序寫在一起,造成即使前面的if-else條件不符,依然不能呼叫return,必須要把所有想檢驗的條件都檢驗完才能結束。
首先,我在上面這個腳本裏頭的主要目標是想要做兩件事,第一件是檢查玩家作為「所有種類」的可變身魔族,是否「在沒喝下抑制變身的藥水的前提下」,擁有所有魔族該有的基本能力;以及玩家「在通過變身任務之後」現在惡魔化能力的修練層級,來決定該給玩家哪個等級的變身power(由弱到強分成六個等級,即001DemonicTransform至001DemonicTransform6)。第二件目標則是檢查玩家作為「天生就有翅膀」的可變身魔族,是否「在沒喝下抑制惡魔能力的藥水的前提下」,擁有飛行能力?
然而,根據以上的優化演算法邏輯,我後來對新版(1.9.1版本)HatsutoliTiefling Races腳本所重新整理歸納出來的條件階層優先權由主要到次要排序如下:
1. 玩家必須是我的六種可變身魔族的其中一種:
player.getisrace tiefling == 1 || player.getisracetieflingCharacteristic == 1 || player.getisrace tieflingTail == 1 || player.getisracetieflingTailCharacteristic == 1 || player.getisraceTieflingTailCharacteristicWings == 1 || player.getisrace TieflingTailwithWings == 1
2. 玩家必須沒有喝下變身抑制藥水:TransformBlock == 0
3. 通過以上兩個主要條件以後開始檢查玩家的基本能力是否符合預設,但這裡出現了3-1和3-2兩個同等級(應該同時判斷)的次要條件:
3-1. 檢查玩家作為可變身魔族的基本能力是否符合預設,意即只要是魔族都應該具備的能力:if player.hasspell 001TieflingAbility == 0等等
4-1. 在基本能力符合預設後,再檢查玩家是不是可變身魔族中「天生就具備翅膀」的類型?是的話,才需要檢查玩家有沒有飛行能力:
if player.getisraceTieflingTailCharacteristicWings == 1 || player.getisrace TieflingTailwithWings== 1
3-2. 檢查玩家是否已經通過「控制變身任務」:getstage 001TieflingTraining >= 60
4-1. 滿足已經通過任務的條件後,才判斷玩家是否通過「召喚惡魔化坐騎」的任務,是的話就給予召喚惡魔化坐騎的能力:DemonicHorseCheck == 2
4-2. 滿足已經通過任務的條件後,才判斷玩家的「惡魔化能力的修練層級」,來決定該給玩家哪個等級的變身power:getglobalvalue transformlevel == ?
於是現在我們很清楚了,必須先滿足條件1,才需要檢查條件2;滿足了條件2,才需要檢查條件3-1和條件3-2,以此類推。其中條件3-1和條件3-2必須要同時被檢查,所以我們應該將3-2改寫在另一個非預設執行的Gamemode的Script裏頭,當玩家滿足3-2所需的三項條件前提時,再以startquest呼叫它,然後檢查4-1和4-2。
根據這個邏輯,本來一個龐大的、占空間的、而且缺乏效率的腳本,被依照條件階級拆散成四個小腳本,第一個腳本如下,將運算目標簡化為純粹檢查抑制惡魔能力藥水的效果,是不是清楚很多了呢?
;; 優化過的腳本範例1
scn 001TieflingAbilityBugFix
Begin Gamemode
;;優先權1
;;判斷玩家是不是六種可變身魔族的其中一種,如果不是就永遠停止此任務(阻止其繼續在背景運算),並呼叫return,略過以下所有Script。
if player.getisrace tiefling == 0 && player.getisrace tieflingCharacteristic == 0 && player.getisrace tieflingTail == 0 && player.getisrace tieflingTailCharacteristic == 0 && player.getisrace TieflingTailCharacteristicWings == 0 && player.getisrace TieflingTailwithWings == 0
stopquest 001TieflingRestoreFix
return
endif
;;優先權2
;;玩家是六種可變身魔族的其中一種,所以繼續判斷下一個式子。
if player.getisrace tiefling == 1 || player.getisrace tieflingCharacteristic == 1 || player.getisrace tieflingTail == 1 || player.getisrace tieflingTailCharacteristic == 1 || player.getisrace TieflingTailCharacteristicWings == 1 || player.getisrace TieflingTailwithWings == 1
;;優先權3
;;玩家有沒有喝下抑制惡魔化的藥水(TransformBlock是否為0),要是有,就略過以下所有Script。
if TransformBlock != 0
return
endif
if TransformBlock == 0
;;玩家沒有喝下抑制惡魔化的藥水,所以應該判斷玩家是否具備可變身魔族應具備的基本能力(player.hasspell)。
if player.hasspell 001TieflingAbility == 0
player.addspellNS 001TieflingAbility
endif
if player.hasspell 001TieflingEyes == 0
player.addspellNS 001TieflingEyes
endif
;;檢查玩家是否在沒有飛行能力的前提下也有飛行中消耗體力的負面狀態加成
;;會在玩家變身飛行到一半結束變身時,或者玩家飛行到一半喝下抑制變身藥水時發生(player.hasspell 001FlyingTired)
if player.hasspell 001DemonicFlight == 0 && player.hasspell 001FlyingTired == 1
player.removespellNS 001FlyingTired
endif
;;玩家身上有沒有準備好變身時會用到的惡魔化盔甲(隱藏於物品欄,player.getitemcount 000DemonCompleteBody)
;;要是沒有,就給玩家一套
if player.getitemcount 000DemonCompleteBody == 0
player.additemNS 000DemonCompleteBody 1
endif
endif
endif
end
第二個腳本,將檢查玩家是不是可變身魔族中「天生就具備翅膀」的類型的部分從第一部分中獨立出來,如果玩家不是選擇天生就有翅膀的可變身魔族,則這部分腳本就會被關閉,每秒鐘將節省三百行以上的腳本運算需求。
;; 優化過的腳本範例2
scn 001TieflingAbilityBugFixWings
Begin Gamemode
if player.getisrace TieflingTailCharacteristicWings == 0 && player.getisrace TieflingTailwithWings == 0
stopquest 001TieflingRestoreFixWings
return
endif
if player.getisrace TieflingTailCharacteristicWings == 1 || player.getisrace TieflingTailwithWings == 1
if TransformBlock != 0
return
endif
if TransformBlock == 0
if player.hasspell 001DemonicFlight == 0
player.addspellNS 001DemonicFlight
endif
endif
endif
end
第三個腳本如下,一樣是把運算目標單純化,將本來包含在第一個腳本中運算「控制變身任務」的部分獨立出來,並設定成非初始執行(因為當變身任務進行到一定階段的時候,這個部分才會有意義),然後在主要任務的對應stage底下呼叫它,這樣玩家在遊戲開始時就不會讓電腦受苦於在背景運算那麼多還沒用到的腳本:
;; 優化過的腳本範例3
SCN 001TieflingTrainingAbilityCheckSCN
Begin Gamemode
;;優先權1
;;判斷玩家是不是六種可變身魔族的其中一種,如果不是就永遠停止此任務(阻止其繼續在背景運算),並呼叫return,略過以下所有Script。
if player.getisrace tiefling == 0 && player.getisrace tieflingCharacteristic == 0 && player.getisrace tieflingTail == 0 && player.getisrace tieflingTailCharacteristic == 0 && player.getisrace TieflingTailCharacteristicWings == 0 && player.getisrace TieflingTailwithWings == 0
stopquest 001TieflingTrainingAbilityCheck
return
endif
;;優先權2
;;玩家是六種可變身魔族的其中一種,所以繼續判斷下一個式子。
if player.getisrace tiefling == 1 || player.getisrace tieflingCharacteristic == 1 || player.getisrace tieflingTail == 1 || player.getisrace tieflingTailCharacteristic == 1 || player.getisrace TieflingTailCharacteristicWings == 1 || player.getisrace TieflingTailwithWings == 1
;;玩家有沒有喝下抑制惡魔化的藥水(TransformBlock是否為0),要是有,就略過以下所有Script。
if TransformBlock != 0
return
endif
if TransformBlock == 0
;;優先權3
;;除非玩家中途更改種族,否則玩家不可能在不是魔族的前提下進行這個腳本。
;;有沒有通過惡魔化控制任務(getstage 001TieflingTraining),如果沒有,以下略過。
if getstage 001TieflingTraining < 60
stopquest 001TieflingTrainingAbilityCheck
return
endif
if getstage 001TieflingTraining >= 60
;;優先權4-1,檢查玩家的控制惡魔化修練等級並給予對應該等級的變身能力。
if player.hasspell 001DemonicTransform == 0 && getglobalvalue transformlevel == 0
player.addspellNS 001DemonicTransform
elseif player.hasspell 001DemonicTransform2 == 0 && getglobalvalue transformlevel == 2
player.addspellNS 001DemonicTransform2
elseif player.hasspell 001DemonicTransform3 == 0 && getglobalvalue transformlevel == 3
player.addspellNS 001DemonicTransform3
elseif player.hasspell 001DemonicTransform4 == 0 && getglobalvalue transformlevel == 4
player.addspellNS 001DemonicTransform4
elseif player.hasspell 001DemonicTransform5 == 0 && getglobalvalue transformlevel == 5
player.addspellNS 001DemonicTransform5
elseif player.hasspell 001DemonicTransform6 == 0 && getglobalvalue transformlevel == 6
player.addspellNS 001DemonicTransform6
endif
;;優先權4-2,檢查玩家是否已經通過控制惡魔坐騎的任務。
if DemonicHorseCheck == 2 && player.hasspell 001TieflingStallionSummon == 0
player.addspellNS 001TieflingStallionSummon
endif
endif
endif
endif
end
第四個腳本,將本來包含在第一個腳本中,檢查玩家魔族角色是否已變身,來設定角色是否得到惡魔之吼的能力,獨立成物品腳本,並掛在惡魔變身使用的裝甲底下,這樣就只有在玩家變身的一瞬間才需要運算,至少每秒又幫遊戲引擎節省了300行腳本的運算空間。
;; 優化過的腳本範例4
scn aaaDemonicHoundAbilityAddOn
Begin OnEquip player
player.addspellNS 001DemonicHound
end
Begin OnUnEquip player
player.removespellNS 001DemonicHound
end
結語&感言
其實我也不知道我打這麼長一篇是要給誰看,又是不是能有甚麼啟發性XD 我相信應該很少版友是認得我的,轉眼間Oblivion陪伴我也已經五年以上了,這段過程中我的人生發生了不少轉變,因此個性慢慢從懵懂、自大又衝動,得罪了不少不該得罪的人,也傷害了很多不想傷害的人(包括自己),然後慢慢被磨得圓滑、內斂且自制(好吧,跟以前比起來應該是有進步啦XD),總之就當成新年禮物送給陪伴我長大的上古卷軸和曾經鼓舞過我的3DM論壇。
不管以後我還會不會繼續製作電玩遊戲,我都很感激這一切帶給我的回憶與成長,嘛,就這樣了XD。
|