本帖最後由 None 於 2014-4-17 21:02 編輯
第五章:起始點&觸發器
再來一個例子
目前為止,你已經懂了不少了,這教學已經可以說是稍有深入了,你也不再是一個初學者了。然而,還有很多函式和技巧你需要學的。但是,就我們已經學的東西來說,已經可以做出一些很cool的玩意來了。現在再來一個例子。這指令碼有點複雜了,需要不少的設定,使用了一些新的命令。希望這是值得的。 -開啟TestModule -來到我們以前建立的第一塊地方,這次不用守衛了。 -刪除那個SINGERnpc -畫一個模組起始點。 -在房間的角落裡畫一個commonernpc(是否是commoner也不是特別的重要,我們要改很多東西) -在"Basic"欄:把firstname改成Dartboard,tag改成DARTBRD,種族race改成"Construct",外觀appearance改成箭靶子ArcheryTarget,性別改成none,頭像為po_PLC_F01_(在"PlaceableObjects and Doors"裡) -在"Advanced"欄,選中"Plot"。 -還是在Advanced欄,來到factioneditor。建立一個新的faction叫"Target",parentfaction為"Hostile"。把Target-Commoner和Commoner-Target都設為50。 -把Dartboard的faction改為Target。 -在"Scripts"欄,刪掉所有的指令碼(在這裡我們不希望一個箭靶子還會發飈砍人) -編輯"OnDamaged"handle.放入下面的指令碼。
NWScript: //OnDamaged Script: tm_dartbrd_dm //Dartboard script //Goes "thunk" when hit with a ranged weapon. // voidmain() { if(GetWeaponRanged(GetLastWeaponUsed(GetLastAttacker()))) { SpeakString("**Thunk**"); } }
你現在就可以試試看這個dartboard,但是好戲還在後頭呢~ -畫一個路徑點waypoint,離開dartboard一格左右。 -把這個路徑點tag命名為DARTWP001 -在這個waypoint附近畫一個commonernpc。 -把npc的tag改為DARTPLAY-為了好玩,點一下"randomname"。 -在npc的"Feats"欄,給予他專長"WeaponProficiency (simple)" -點選"Inventory",按鈕在npc全身照的下方。 -在右邊的欄目裡,來到"Weapons","Throwing","Dart",拖進"Contents"。 -右擊剛拖進來的dart,編輯屬性,把stacksize改為3 -點OK,把dart裝備上npc手裏。 -點OK,離開inventory。-接下來來到scripts欄。 -編輯OnSpawn指令碼,把HeartBeat行前的注釋符號去掉。 -還有一行:SetSpawnInCondition(NW_FLAG_SET_WARNINGS);前面的//也要去掉 -再加入下面這一行:SetLocalInt(OBJECT_SELF, "DARTSTATE", 1); -儲存指令碼名為tm_dartplay_os-來到UserDefinedhandle, 加入下面的指令碼:
NWScript: //On User Defined Script //tm_dartplay_ud //Used to have someone play darts. Called by the OnHeartBeat script. // // The Dart Player will throw all darts in inventory (should start with3), // walk to the dartboard, get 3 darts, walk back, and repeat. // voidmain() { intnCalledBy = GetUserDefinedEventNumber(); objectoTarget = GetNearestObjectByTag("DARTBRD"); intnDartsReady = GetLocalInt(OBJECT_SELF, "DARTSTATE"); // switch(nCalledBy) { case1001: // Called by OnHeartbeat // //nDartsReady will be 1 if ready to throw, 2 if not. // if((GetIsObjectValid(GetItemInSlot(INVENTORY_SLOT_RIGHTHAND))) &&(nDartsReady == 1)) { //If we have darts in the right hand, and we're ready to throw, go forit. ClearAllActions(); ActionAttack(oTarget,TRUE); } else { //Otherwise, there are two cases. We've either just run out, or we arein //the process of getting darts. We don't want to interrupt the cycleif we're //already working on it. if(nDartsReady == 1) { SetLocalInt(OBJECT_SELF,"DARTSTATE", 2); ActionMoveToObject(oTarget); ActionWait(0.5); ActionPlayAnimation(ANIMATION_LOOPING_GET_MID,1.0, 1.0); ActionWait(0.5); ActionPlayAnimation(ANIMATION_LOOPING_GET_MID,1.0, 1.0); ActionWait(0.5); ActionPlayAnimation(ANIMATION_LOOPING_GET_MID,1.0, 1.0); objectoDestination=GetNearestObjectByTag("DARTWP001"); ActionMoveToObject(oDestination); CreateItemOnObject("nw_wthdt001",OBJECT_SELF, 3); ActionEquipMostDamagingRanged(); ActionDoCommand(SetLocalInt(OBJECT_SELF,"DARTSTATE", 1)); } } break; } }
嗯嗯,這個指令碼看起來很複雜,實際上它也很複雜~那麼解釋一些關鍵的東西吧。我使用了一個本地變數DARTSTATE來保證不在準備好之前就出手,或者是多次執行"getdarts"那一系列動作。ActionDoCommand是一個很妙的玩意。他把一個不排隊的可立即執行的命令,變成需要排隊執行。普通情況下,當指令碼執行到SetLocalInt指令的時候,它立刻設定這個本地變數。放進ActionDoCommand之後,這強制當npc完成了前序的所有動作之後,再執行之。CreateItemOnObject用來給npc一些新的darts。"nw_wthdt001"是dart的blueprintref,最後那個3則是stacksize。除了這些,看看你自己能不能搞明白這段指令碼。這裡面有一些新的命令,但是那些名字已經很好的解釋了用途。有不明白的地方就提問吧,(問原作者或者問我都可以呀,Nonameat your service)最近,有不少人在罈子裡要實作各種詭異功能的指令碼。如果我有時間,我當然樂意解答,但你們也知道,我只是一個地精苦力,並不是總有富餘的實踐來回答各種問題(而且,小白的需求總是高的要命,甚至不切實際)。所以,我將花費一點時間來介紹一些地方,當你遇到指令碼的問題,你可以去那裡尋求幫助。事實上,你們問的超過90%的問題都可以在那裡找到答案。
相關資源
總的來說,有4個地方是你可以經常光顧的,排名不分先後: *ScriptingFAQ(在bio官方論壇上,如果鳥文可以的話就去) *官方toolset論壇(別裸身跪求,別一味索取,這裡是提問的地方,如果你只有需求沒有問題,最好別發帖) *官方模組 *toolset本身! 讓我們來看看這四個東西到底怎麼用(當然,還有Lexicon呢,不過你已經知道了,呵呵) ///////////////////ScriptingFAQ(如果你鳥文不好,這個對你沒用) 它被稱為「常見問題」是有原因的。其中的指令碼都是經過編譯和校訂後,可以切實有效的解決很多常見的指令碼問題的。如果真的遇到了什麼問題,你最好先在這裡面查檢視。也許這裡的內容無法直接解決你的問題,不過也許未來就會有了,因為這個東西是不斷更新的。 而且,不必害怕去看那些當前對你的問題沒有任何幫助的內容。通常情況下,這裡的問題,你遲早會碰上的。你對nwn指令碼了解的越多,你就會越發覺得這裡的東西很有用。一般,每當我讀哪些我用不到的指令碼的時候,我總是可以看到一些有益的東西,它可能是一個我沒有用過的函式,或者是一個十分巧妙的使用變數的方法。 由於這裡的指令碼作者的多樣性,裡面總是會有一些內容是在你當前的理解範圍之外的。如果真的是那樣……沒辦法,多學點再回來看吧。即便是現在,你也可以看懂其中的很多東西了,很驚訝不是麼?你不妨立刻就去試試看。 //////////////////////星空官方論壇(或者TROW)toolset版(廣大builder最好仔細閱讀以下條款)這裡總是有一些人在問問題,還有一些人在回答問題。正如我前面所說,很多你要問的問題已經有前輩問過了。 那麼,如何找到那些問題的答案呢?「搜尋」可能是個好幫手,雖然它並不總是可*,但至少它值得一試。 如果透過搜尋沒能找到我需要的答案,我會瀏覽一下前幾頁貼文。既然有些問題總是不斷地被人問到,因此你有很大的機會在前3頁找到你需要的東西。 只有當以上兩種方法都不行的時候,你才應該發帖詢問。一個小的竅門:你在自己解決問題的時候付出的努力越多,你將得到的別人的幫助也就越多,我舉個例子(如有雷同,絕無惡意,純屬巧合。) *一個人這樣發了一個貼文:「怎樣製作一個控制桿來控制一個門的開關?」,內容是「如題」。 *另一個人則詳細的在貼文中註明了自己為了解決這個問題所作的努力。並且描述了他給門和控制桿的標籤。他描述了他為控制桿的OnActivate寫的指令碼並且解釋了指令碼中出現的本地變數的含義和作用,並且描述了自己解決問題的思路。 我不能代表所有人,但如果是我的話,我肯定會更樂於幫助第二個人。從第一個貼文,我看不出任何誠意和鑽研精神,就算了那個人已經在這個問題上花費了15個小時……從他的貼文,我無法得知他到底為解決這個問題作了什麼工作。他好像在說:「我不想在這上面花費任何功夫,你來給我做。」而第二個人,我從他的貼文裡可以看出他的確做了很多嘗試。透過他詳細的解釋,我可以很清楚的知道他的問題到底在哪裡,因此我甚至會樂意親自為他寫出程式碼。 關於在論壇提問的最後一點建議:禮貌。儘管這不是必須的,但你最好在自己的指令碼上註明那些給你幫助的人的名字,比如這樣: //On user defined script: tm_guard_ud //Has a guard growl a warning when it sees a PC wielding a weapon. // //Last updated: 7/11/02 //Written by The Great Gat**y //Some debugging help from Celowin of the NWN Scripting Forum 儘管那些玩家不會看到這些東西,但那些細心的builder會看到。如果你不知道一個人的真實姓名,就直接用ID。那些幫助過你的人也會樂於看到這些的。 ////////////////////////////////官方模組 不知道多少次了,我看到這樣的問題:「我怎麼做出……的效果,像在官方模組的第……章裡面那樣?」我希望這樣的問題的提出是因為不知道如何在toolset中開啟官方模組,而不是僅僅因為懶惰。 在你的toolset開啟的時候,會進入開啟模組的選項板,這裡面有一個按鈕的功能就是開啟官方模組。 透過察看官方模組,你可以從bio工程師那裡學到非常非常多的東西。像知道如何讓一個npc主動開始談話?看看序章一開始那裡就知道了。想知道如何召喚一堆怪物來攻擊PC?看看序章A姐兒畢業典禮大廳那裡就知道了。想知道如何讓一個npc跟隨主角直到他看到另一個npc?看看序章最後那裡就知道了……類似的還有很多,在官方模組裡面有很多你可以學到的或者希望知道的東西,這完全取決於你是否願意去做。 就我個人而言,當我遇到問題,我通常會去檢視bio的序章……原因很簡單,因為它在toolset裡面載入的速度最快。當然,有很多東西在序章裡面都沒有出現,但你仍然可以從中受益頗多。 事實上,當人們開始編寫模組的時候,首先浮現在人們腦海裡的是模組裡面最cool的那一部分,不過,不幸的是,那往往也意味著最麻煩的指令碼。在跑之前,你首先要學會走路。你的模組中,不是所有的指令碼都必須很複雜,所以,從那些簡單的指令碼入手,儘管有的時候那顯得有點無聊。你需要一步一步的,去克服那些最困難的指令碼。我還記得有個人設計(只是設計,不是編寫)的第一個指令碼:其中要包含6個控制桿,12顆寶石,一個可以傳送到各個地方的傳送門,還有一大堆視覺效果。他所要做的東西當然不是不可能,甚至不算太難……一旦你真的弄明白了自己要做的是什麼。不過,作為自己寫的第一個指令碼,這無疑是一個惡夢。 ////////////////////////Tooslet本身 把toolset作為學習指令碼的一個方法看似有點奇怪,不過,說實話,我在這裡面學到的東西是最多的。一旦你學會了一些基本的指令碼知識,你就可以透過toolset觸類旁通地學到很多相關的其他指令碼知識。 首先,當你編輯指令碼的時候,是否注意過在你的右側,有一長列函式名。如果你點選其中任何一個函式,你就會在指令碼編輯器的下方得到關於這個函式的訊息。由於大部分函式的命名都是很容易理解的,你可以直接瀏覽這些函式,直到你找到和自己的需求看起來比較相吻合的函式,再透過說明看看他是否真的可以實作你的需求。 現在咱們以一個即將在下面的課程中用到的函式為例子。我希望把PC傳送到另一個地方。我不知道什麼函式可以做到這個功能,所以我開始瀏覽那一長列函式名。 我們想把PC弄到一個地方,所以ActionMoveToLocation看起來不錯,而且我們之前也用過,但他只是讓PC走到目的地,而不是傳送。不過,就在這個函式的旁邊,還有一個叫做ActionJumpToLocation的函式,這是啥意思?我怎麼沒看到過PC在遊戲裡面跳過?讓我們來看看。我點選了那個函式,並且看到了如下訊息: //The subject will jump to lLocation instantly (even between areas). //If lLocation is invalid, nothing will happen. voidActionJumpToLocation(location lLocation); 前兩行是注釋,它告訴我們一些有關這個函式的訊息。第一行告訴我們這就是我們要找的函式,而第二行則解釋了一些有關這個函式的特殊情況。那,第三行是幹啥的呢? 好吧,第三行實際上是告訴我們如何使用這個函式的重要訊息。關於這一行,我們可以追溯到第一堂課……void告訴我們,這個函式沒有回傳值——它完成了某種行為,但他不會計算出任何形式的答案並且回傳。然後就是函式的名字。最後,括弧裡面的內容告訴我們這個函式的輸入——我們得知我們需要傳入一個location型別的變數。 然後,我們就可以繼續瀏覽函式列表,尋找一些有幫助的東西。首先,這是一個action(動作),所以他將被加入到動作序列中。如果我們希望傳送立刻發生,則我們需要一個替代品……JumpToLocation怎麼樣?一查,果然有,而且他不是一個動作,而是一個命令(立刻執行) 下一個問題是,我們如何得到一個location型別的變數並且傳入到函式裡面?大部分由回傳值的函式,都是以Get作為開頭的,因此,讓我們來看看那一類。耶,這裡我們發現了一個叫做GetLocation的函式。再一次的,我們點選他,察看他的使用方法。 呃……這個JumpToLocation似乎沒有一個輸入變數來表示他要傳送的是什麼東西。這個問題比較棘手,不過我想讓你再自己多找一會兒,如果實在不行,你看看最後面的指令碼就會明白了。 現在你看到了吧,透過一些猜測和經驗(指令碼寫的多了,經驗也就多了),我們可以從toolset裡面學到多少東西!我推薦,當你無聊的時候,在那個函式列表裡面隨便點個看起來有趣函式看看,說不定你會因此找到一些對你未來的工作大有益處的東西呢。 //////////////////////////////////NWNLexicon 這個我介紹的不少了吧
繼續我們的課程
好了,關於學習指令碼的途徑我已經說了不少了,現在讓我們動手來寫一個指令碼。在之前的課程中,我們所寫的指令碼全都是npc身上的,下面,我們就來寫幾個有關可放置物品(placeable)和觸發器(trigger)的指令碼。 總的來說,不管你在給什麼東西寫指令碼,他們之間都是有共同之處的。你得注意到底是什麼時間觸發了指令碼,並且還要弄明白OBJECT_SELF,代表了什麼東西,不過除此之外,指令碼的基本結構都是一樣的。 在我們的TestModule裡面,如果你之前確實跟著我的教學來學,現在他裡面應該有兩個完全不相關聯的區域。我們之前只是根據需要來改變PC開始地點的位置,這的確是個很方便的測試工具,不過同樣很無聊。我們當然可以給他們之間作一個區域轉換,但還是很無聊。因此,讓我們來著手做一些真正有趣的東西吧。 這個如何:在其中一個區域裡面,我們做兩個控制桿(Lever),最開始,他們是出於「關閉狀態」,當兩個控制桿都被扳到「啟用狀態」的時候,我們會召喚一個傳送點,當PC走入其中的時候,它就會把PC傳送到另一個區域。好的,然我們一步一步來實作他: 1.開啟toolset,載入模組,開啟你希望放置傳送點的地圖。 2.放置兩個控制桿在地圖上(可放置物品->容器和控制桿) 3.其中一個控制桿的標籤設定為LEVER1,另一個為LEVER2。 4.在地圖上放一個路標(Waypoint),設定標籤為TM_INWP,用來標記傳送點出現的地方。 5.在路標旁邊畫一個圈觸發器(trigger),並且把它的標籤設定為PORTTRIG。 6.為每一個控制桿的OnUsed腳本欄加入如下指令碼,並且編譯儲存為tm_lever_ou。
//OnUsed script: tm_lever_ou // //This script sets up levers named LEVER1 and LEVER2. //They both start deactivated. If both get turned on simultaneously, //a portal is summoned at the waypoint TM_INWP and the trigger //PORTTRIG is turned on. // //Written by Celowin //Last Modified: 7/12/02 // voidmain() { intnUsed1 = GetLocalInt(OBJECT_SELF, "LEVER_STATE"); intnUsed2; // //nUsed1 and nUsed2 are used to temporarily hold the states of the twolevers. //0 is off, 1 is on //LEVER_STATE is the permanent holding spot for the variables. //Each lever stores its own LEVER_STATE // if(nUsed1== 0) { ActionPlayAnimation(ANIMATION_PLACEABLE_ACTIVATE); SetLocalInt(OBJECT_SELF,"LEVER_STATE", 1); nUsed1= 1; } else { PlayAnimation(ANIMATION_PLACEABLE_DEACTIVATE); SetLocalInt(OBJECT_SELF,"LEVER_STATE", 0); nUsed1= 0; } nUsed1= GetLocalInt(GetObjectByTag("LEVER1"), "LEVER_STATE"); nUsed2= GetLocalInt(GetObjectByTag("LEVER2"), "LEVER_STATE"); if((nUsed1 == 1) && (nUsed2 == 1)) // Are both levers on? { // If so, create the portal, and tell the trigger to get ready toport. objectoPortalSpot = GetWaypointByTag("TM_INWP"); CreateObject(OBJECT_TYPE_PLACEABLE,"plc_portal",GetLocation(oPortalSpot),TRUE); SetLocalInt(GetObjectByTag("PORTTRIG"),"READY", 1); } }
這裡的確有些東西需要進行說明,不過大部分我都已經注釋得很清楚了。我會把解釋放到所有準備工作都做好以後。 7.現在來到我們之前畫得那個觸發器(trigger)那裡。 8.開啟屬性面板,竟如指令碼面板。 9.在觸發器的OnEnter事件裡面添加如下指令碼,並且編譯並且儲存為tm_portal_en
1. //OnEnter script: tm_portal_en // //If the portal has been turned on, and a PC enters, warp that PC to //the waypoint TM_OUTWP // //Written by Celowin //Last Updated: 7/12/02 // voidmain() { //Set up the temporary variables that we need. objectoPC = GetEnteringObject(); objectoDest = GetWaypointByTag("TM_OUTWP"); intnReady = GetLocalInt(OBJECT_SELF, "READY");
//Check: Is it a PC and is the portal turned on? //If so, cause the PC to jump to the exit. if((nReady == 1) && (GetIsPC(oPC))) AssignCommand(oPC,JumpToLocation(GetLocation(oDest))); }
10.好的,現在幾乎全部完成了,點選確定按鈕。 11.現在,進入你希望被傳送到的那個區域。 12.放置一個路標,標籤設定為TM_OUTWP 13.儲存模組 14.進入遊戲,載入模組,看看效果吧! 傳送門的指令碼很容易看懂(就是觸發器trigger的指令碼),唯一值得提及的就是「AssignCommand」。這是一個讓你豁然開朗的東西。總的說來,它的作用是讓一個東西做一個動作。在這裡,我們希望PC這個東西作JumpToLocation這個動作……所以我們為PC阿AssignCommand。這個方法在任何地方都適用,只要你可以傳入正確的物體(object) 另外一個指令碼也並不像他看起來那樣難。關鍵點其實就是如何利用本地變數來記錄兩個控制桿的開啟和關閉狀態。不過,這裡仍然有些東西值得一提。我在指令碼中連用兩次nUsed1是不太好的。一次,我用它代表任何一個控制桿的狀態,下一次,我又用它來表示LEVER1的狀態。有些程式設計師可能會給我一板兒磚,警告我應該分別給他們定義兩個變數。不過,既然這只是一個簡單的指令碼,而且這兩種用法是如此相近,為了方便,我就這樣做了,當處理更大的指令碼時,我想我會給他們分別定義兩個變數的。 另一個值得一提的東西是CreateObject。透過這個函式,我希望大家能夠把標籤(tag)和藍圖示簽(blueprintResRef)區分開。在遊戲中的任何一個物體,都有自己的標籤。但是,那些還沒有被建立的物體就沒有標籤了。不妨把標籤想像為一個木頭牌子,如果這個物體還沒有被創造出來,這個牌子又怎麼能掛在他的頭上呢? 為此,要建立物體的時候,我們需要找到一個唯一的標籤,來告訴便一起,我們到底需要創造什麼物體。這就是」plc_portal」所作的事。他是傳送點這種可放置物體的藍圖資源參照標籤(blueprintresourcereference)。得到一個物體的ResRef,你可以直接到物體面板裡面,右擊物體,選擇屬性,進行察看(在高階面板裡面)。值得注意的是,標準物品的ResRef是不可能以001為結尾的。 除了以上這些,我希望其他東西還算比較好懂。我盡量把注釋寫得比較細緻,以幫助你們理解指令碼的內容。
課外提高練習
好吧,現在,我老師的本性又顯露出來了……留作業的時間到了!我不會強求你做這些東西,但他們很值得你做一做,不管你是為了鞏固本次課程的內容,還是從中觸類旁通到更多的東西。 那麼,試著做做這個吧:剛才,我們所作的:開啟兩個控制桿,傳送點出現,這很正常。開啟一個,然後關掉,再開啟令一個,再關掉它,傳送門不會出現,這也正常。但是,當我們開啟傳送點後,關掉其中一個控制桿,傳送門還在!這就不太正常了。現在,就請你解決這個bug,使得當一個控制桿被關閉後,傳送點隨即失效。當然,如果再開啟他,傳送點又會重新生效。 擴充功能內容:剛才,傳送點是單向的,請把它做成雙向的,使得你可以透過傳送點再傳送回來。 |