奇幻遊戲社群

標題: NWN指令碼教學 [打印本頁]

作者: None    時間: 2014-4-15 16:06:33     標題: NWN指令碼教學

本帖最後由 None 於 2014-4-17 20:56 編輯

本教學文是對岸網友Celowin所作,再經敝人翻譯為台灣繁體並作排版編輯
至於原始出處不得而知,既然作者已說可隨意轉貼,也就不管他了
章節安排除五、六兩章標題為筆者所加(因為作者似乎忘了)之外大致上按照原文格式
為求閱讀及查詢之方便,以下每欄貼一個章節並在本開版文作目錄

目次











作者: None    時間: 2014-4-15 16:12:55

本帖最後由 None 於 2014-4-17 20:57 編輯

第一章:基礎

前言:

這個系列的教學課程的目的是,幫助一個完全的nwn指令碼FC學會如何使用nwn指令碼來製作自己的模組。最開始的課程會非常基本,任何有nwn程式設計基礎的人都可以直接跳過這部分內容。之所以設定這堂課,是為了使本系列教學文章可以適應更多的人,不管他的程式設計水平如何。


本系列文章(中文版)可以隨便進行轉貼,列印或者更改,但是不可以用於任何商業目的。關於本系列各個章節的問題或者評論,請直接以re文的格式進行,我會儘快做出答覆。

現在,我假設,看到我這篇文章的人已經玩了toolset有一陣子了,所以,如果你連怎麼設定一個物體,修改他的屬性都不會的話,不要在這裡提問,自己嘗試一陣子再來吧。(此段與原文有出入,屬於我自己的要求)


說明:

開啟toolset(如果你非稱他為工具箱,隨便你),用模組精靈建立一個模組,我們給他起名為TestModule。建立一個區域命名為TestArea001。大小什麼的都隨便,因為這不影響這堂課,不過我推薦把他弄小點,比如2*2,這裡有個小竅門:當你把區域的大小設定為16*16甚至更大的時候,你就應該好好考慮下,因為,這會大大降低機器的速度和遊戲的流暢度,不過,如果是網路模組的話,另當別論。

然後,建立一個生物npc,只要不是敵對的就行,放置在地圖上,並做以下的事情:

·在你剛剛放置的npc身上點選滑鼠的右鍵。

·在彈出的面板上選擇屬性。

·更改生物的標籤(tag)為「SINGER」。

·選擇指令碼(scripts)面板。

·這個時候,每一行都已經存在有預設的指令碼名,清空他們!

·OnHeartBeat(心跳??那一行,點選右側的編輯按鈕。·在彈出的指令碼編輯器裡面,輸入如下的指令碼:


voidmain()

   {

     ClearAllActions();

     ActionSpeakString("Thisis the song that never ends.");

     ActionWait(1.5);

     ActionSpeakString("Yesit goes on and on, my friends.");

     ActionWait(1.5);

     ActionSpeakString("Somepeople started singing it not knowing what it was.");

     ActionWait(1.5);

     ActionSpeakString("Andnow they'll keep on singing it forever, just because...");

   }


我強烈推薦你自己一個字一個字的把這幾行指令碼敲進去,而不是簡簡單單的c&v,這樣做,你會學到更多有關於指令碼書寫格式的知識,如果你不小心打錯了一個字元,那是最好不過了,你會明白一個簡簡單單的字元會有多麼的重要。·點選「另存為按鈕」,並且把名字命名為「tm_singer_hb」。·完成這一系列工作後,你應該注意到窗口最下方的狀態列顯示「0錯誤」,如果沒有的話,再檢查一遍你的指令碼,看看疏漏了哪一個字元。·關閉指令碼編輯器。·npc屬性窗口點選「完成」,完成對npc屬性的修改。·儲存你的模組。·關閉toolset,進入遊戲,選擇自己的模組,進去看看自己的成果吧。·你的npc應該會不斷地唱起我們給他寫的小曲兒……


分析:

現在,咱們進入最難的一部分……我將要一步一步地分析並且解釋所有我剛才所作的。我將會以問答的形式來進行這一部分,盡量覆蓋到所有你們可能會問的問題。

為何我們叫這個模組為TestMoule?事實上,並沒有什麼特別的原因,只是為了更方便辨認。

為何我們把區域命名為TestArea 001?對於我們小小的測試模組,這並沒什麼影響,但是,為自己的區域重新命名永遠都是一個builder的好習慣,目的在於方便將來匯出自己的區域。如果你想匯出當前模組的一個區域到另一個模組中,他必須要有一個唯一的名字,否則,將會覆蓋標的模組的同名稱區域。如果每一個人的模組中,都有Area001,麻煩就可想而知了。

為何你把那個npc的標籤(tag)設定為SINGER?為何全部都是大寫字母?由於一些小問題,標籤需要都是大寫字母。你可能永遠不會碰到那些問題,但是,再一次的,把他全都弄成大寫的格式是一個builder的好習慣。同樣的,我推薦你為自己放置的任何一個物體(可放置物體或者生物,或者任何其他的東西)都設定一個簡單容易記憶的標籤。SINGER十分完美的表達了這個NPC的特性,這將會使我們在更複雜的指令碼編輯中,隨時可以方便並且正確的參照這個標籤。你最好不要把標籤設得太長,我推薦最多8個字母。

為何你要刪除所有的預設指令碼?既然你要刪除他們,那他們又為何會存在呢?預設的指令碼為生物設定了很多預設的行為方式,這些都是我們這趟可用不到的。比如說,如果你把他設定為敵對,當你進入模組的時候,他就會主動攻擊你。我們把那些預設指令碼刪除是為了使事情簡單明了:我們只是想讓他唱歌,除此之外,別無他圖。在後續課程中,我會教大家如何更好的使用預設指令碼。

為何我們把指令碼放到OnHeartBeat那一欄?為什麼我們不把指令碼放到其他地方,比如OnDeath?每一個指令碼欄裡面的指令碼,只有滿足一定的條件之後,裡面的指令碼才會被觸發並且執行。OnHeartBeat那一欄,每過6秒鐘,其中的指令碼就會被觸發並且執行一次。這就是為何那個npc會一直不停地唱著小曲兒……每過6秒鐘,他唱4句歌詞。根據你想實作的行為,每一個指令碼欄都各盡其職。事實上,「OnHeartBeat」指令碼欄應該是你最少用到的一個指令碼欄才對。如果每個6秒鐘,觸發的指令碼內容過多,這勢必會給你的電腦造成不小的麻煩。

voidmain是個什麼東西呢?這玩意兒看起來十分的簡單,而且,你會發現幾乎所有人們寫的nwn指令碼裡面都有這個東西。我將會在這裡儘可能詳細的把他解釋清楚,但我估計如果沒有學習後續的課程的話,你可能並不能完全理解下面的內容。nwn指令碼是使用函式來編寫的,這些函式告訴編譯器應該做什麼事。有很多函式已經事先由bio公司的開發人員寫好了,我們直接把他們寫到指令碼裡就可以了。不過,當我們在寫指令碼的時候,其實我們是在寫自己的函式。那一行事實上,是在建立我們自己的函式,有些時候,我們稱他為函式的結構。首先是void。他告訴編譯器,這個函式會回傳什麼型別的「答案」。我們的小歌唱家只是唱歌,並不會計算並且回傳任何型別的答案。所以,void告訴編譯器,這個函式事實上不會回傳任何形式的值。(void:空)main這個詞,宣告如下內容是這個指令碼的主要部分。我們可以在指令碼裡自己編寫其他名稱的函式,但main函式是當指令碼開始編譯執行的時候最開始的地方,voidmain函式應該只有一個。在兩個括弧"()"之間是我們這個函式的輸入內容。同樣的,我們的指令碼事實上,並不需要任何輸入內容,所以在他們之間沒有任何內容。即使沒有輸入,我們還是得寫上「()」,這是格式的問題,編譯器可沒有我們那麼觸類旁通。

{」和「}」是什麼意思?這兩個話括弧結合起來使用,來表示其間的內容是屬於voidmain()函式的。

為何每一行指令碼後面都有一個「;」?簡單得說,這是用來告訴編譯器,這裡是這行指令碼的結尾。有的時候,你需要寫一行非常非常長的指令碼命令,這並不美觀和方便,如果你把他放到一行裡,所以,你需要用「;」把它分為若干行。空行不會有任何問題,編譯器會把那些字元看作一行內容,直到他遇到一個「;」。你把他想像成句號就可以了。

ClearAllActions()是幹什麼用的?這是一個比較巧妙的東西,因為,事實上,他什麼都沒有作。他放在那裡只是為了保險,我們實際上是期望他不要做任何事情的。好吧好吧,我來解釋一下……指令碼中的每一行都是個Action(動作)命令。npc一次只會執行其中的一個動作,這樣是為了保證直到他確實完成了這個動作之後,才繼續執行下面的動作。不過,現在我們的指令碼是被放在了OnHeartBeat裡面了不是麼?它每過6秒鐘就會觸發一次。我們已經看到了我們希望npc做的7個動作,那麼,萬一他在6秒鐘裡面,只完成了4個呢?下個6秒,npc被告知要再作7個動作,這次,他有10個動作要做了,他又作了4個,然後,又來了7個新動作……13個要做的摟!他落後得越來越多,最終,問題就會產生了……為了防止這一切,我們有更好的辦法去做,不過,現在,最簡單的方法就是「讓npc忘記一切自己要做,但是還沒有做的動作」,這就是ClearAllActions()的作用了!如果你想試驗一下,更改指令碼,把所有在ActionWait()裡面的浮點數都從1.5改為3.0吧。你會發現npc還沒把歌曲唱完,就又開始唱新的歌曲了。現在,我們把之前的知識結合到一起:ClearAllActions()十一個函式,就好像voidmain(),所以,「()」裡面沒有東西表示他不需要任何的輸入。

main函式中,其他那些行是幹什麼的呢?那些指令碼告訴npc應該做什麼動作。我想這是很明顯的吧,用奇摩字典Dr.eye查查那些詞語,speakwait你就會明白了,不過我還是解釋一下。ActionSpeakString()npc說話,在編輯指令碼的時候,由於只能輸入英文,我們只好讓他唱鳥語,以後我會告訴你們解決的方法。再來看看著個函式,注意到沒有,這個函式事實上是有輸入的……那就是npc要說的話。那些話我們稱之為"string"(字串),上引號中間的內容就是字串的內容,你寫什麼都行,這裡沒有格式要求,我們寫的是歌詞。ActionWait()npc等一段時間,其間,什麼都不做。我們傳入函式的是一個數字,準確地說,是一個浮點數,就是小數的意思拉!記住要有小數點。因此,ActionWait(1.5)的意思就是讓npc等待1.5秒。

為何把指令碼命名為tm_singer_hb?為了方便記憶和匯出:·tm表示TestModule ·singer當然就是表示歌唱家SINGER·hb表示這個指令碼是npcOnHeartBeat指令碼。


作者: None    時間: 2014-4-15 16:17:57

本帖最後由 None 於 2014-4-17 20:59 編輯

第二章:本地變數(Local Variables

說明:

這堂課估計會比第一堂課要來的長一點,內容也要深一些。我不確定自己是否可以把他們說的通透明白,所以,一旦你有任何疑惑(關於這一堂課的),儘管提問,因為你的任何問題,可能同樣也是其他人遇到的問題。

現在,就讓我們繼續修改我們之前設計的那個模組(TestModule,還記得吧……),用toolset開啟他。

右擊那個中了邪的歌唱家(SINGER),進入指令碼(script)面板。

第一個指令碼框是OnSpawn,顧名思義也就是當生物第一次被建立的時候(在遊戲中被建立)觸發並且執行這裡面的指令碼。加入如下內容(裡面本應該是空的,因為上堂課我們已經清空了所有指令碼不是麼?)


voidmain()

  {

    SetLocalInt(OBJECT_SELF,"SINGER_COUNT", 0);

  }


另存為tm_singer_os('os' 代表OnSpawn)

現在,儘管這個指令碼只有一行,它也足以把那些從沒寫過指令碼的人弄得暈頭轉向了。所以,我會先把這裡解釋清楚,然後再進行「真正的指令碼」。

首先,我們應該明確,我們現在是在處理一個全新的指令碼事件。所有放在OnSpawn裡面的指令碼,事實上只會被觸發並執行一次……npc第一次被遊戲載入並且建立的時候。正因為如此,這是初始化一些東西最理想的地方,這也正是我們那行指令碼所作的事情。

那行指令碼設定了一個變數,回憶一下你所上過的數學課(咱中國人數學的底子可不淺啊),什麼是變數呢?一個代表數字的字母(比如x,y)。我們在這裡做了同樣的事情,唯一不同的是,我們用了一個單詞,而不是一個字母。

函式SetLocalInt是做這件事的命令列。他擁有三個輸入(不少喲),分別用逗號隔開。

?第一個輸入代表將要被「賦予這個變數」的東西,它可以是任何東西,簡單的說,就是object,以後,你們會經常看到這個單詞,所以現在趕快熟悉一下這個詞彙吧。?第二個表示變數的名稱。?最後一個輸入代表我們賦值給那個變數的值(初始值)。

這裡,我希望你們可以理解後兩個輸入代表的意思……我們把這個變數叫做SINGER_COUNT(一個字串(string),對吧,我們第一堂課介紹過的),賦值為0。只有第一個輸入需要解釋一下。

在一個模組中,幾乎所有的東西都是一個object(瞧,我前頭說什麼來著,你將永遠和各種各樣的object打交道)。NPCsobjects,可放置物品是objects,路標(Waypoints)是objects,甚至連PCs自己也都是objects。既然模組中有那麼多objects同時存在,關鍵的一點就是,如何準確的定位一個我們需要操作的標的物體(object)。

OBJECT_SELF可以說是最常用的一種定位物體的方式。估計你已經可以猜到,它代表了:當指令碼被觸發並且執行的時候,指令碼所在的物體。既然我們把指令碼放到了NPC身上,那麼OBJECT_SELF,就代表了npc本身這個物體。

結合以上所有的內容……那一行指令碼事實上是在定義一個名稱為SINGER_COUNT,初始值為0,儲存在npcSINGER身上的變數。

另外,在SetLocalInt當中的int代表了一個「整數」,它可以是0,3,-35等等,但是,絕對不可以是3.8之類的小數(浮點數),猜猜SetLocalFloatSetLocalString是什麼意思?你應該能猜到。

現在,我們再次進入OnHeartBeat指令碼欄,刪除之前我們的指令碼,然後輸入以下內容。


intnCount=GetLocalInt(OBJECT_SELF, "SINGER_COUNT");

   voidmain()

   {

     nCount= nCount+1;

     ActionSpeakString("Ihave spoken "+IntToString(nCount)+" times.");

     SetLocalInt(OBJECT_SELF,"SINGER_COUNT", nCount);

   }


編譯並且儲存它,名字還是tm_singer_hb

在我解釋他之前,我想你最好先看看他的效果。在npc屬性面板上點選ok,然後儲存模組進入遊戲,看看npc現在在幹什麼。

在這個指令碼中,我們第一次在voidmain()之外書寫指令碼內容,所以這很值得特別解釋一下。任何在voidmain()之前的指令碼內容被我們稱之為「指令碼的初始化」。簡單的說,它是用來定義並且準備好所有這個指令碼可以參照的內容。和我們之前定義的本地變數(localvariable)不同,nCount只有在這個指令碼執行的這段時間之內才有意義。一旦指令碼執行完畢,nCount就被刪除,它所佔用的記憶體空間隨即釋放。正因為如此,類似nCount在指令碼中定義的變數(注意他和localvariable的區別,一個是定義在物體身上,一個是定義在指令碼裡)被我們稱作「臨時變數」。

你應該已經注意到了,我們在main函式中多次參照了nCount這個臨時變數,想像一下吧,如果我們沒有把它提前定義為nCount,而是在每次使用的時候都參照一下GetLocalInt(OBJECT_SELF,SINGER_COUNT),那該多噁心啊……

名字nCount是另一種命名規範。當你定義一個臨時變數的時候,第一個字母用來表示你的變數是什麼型別的,n就代表了他是整數型別(Integer)。根據指令碼,你可以隨便起名字,但透過這樣的命名規範,每當你參照這個變數的時候,你可以立刻知道他是一個整數型別的變數。名字中」Count」那一部分告訴你這個變數是幹什麼用的(計數)。

int」告訴編譯器我們是在定義一個整數型別的變數,「=」表示「將他賦值為」。而函式GetLocalInt用來得到本地變數SINGER_COUNT。注意到兩個函式的相似性沒有——我們用SetLocalInt來設定一個本地變數,用GetLocalInt來得到一個本地變數。

因此,第一行指令碼事實上幹了不少事情:他建立了一個名為nCount的臨時變數供這個指令碼使用,並且立刻為他賦值為原來儲存在npc身上的本地變數SINGER_COUNT。因此,當這個指令碼第一次被觸發的時候(它每隔6秒鐘被觸發一次,希望你沒有忘記),nCount的值應該是0,因為SINGER_COUNT的初始值為0

我知道,用這麼多話來解釋一行語句的確有點過分,可能會有不少人無法一次將他完全消化掉,如果是這樣,就再仔細讀上他一遍,如果還是不很清楚,也沒必要太擔心。這類東西只有做了一些練習之後才會徹底明白。

現在,讓我們繼續下一行,也就是指令碼主體(main)中的第一行。


nCount= nCount +1


這句話的作用就是為我們的臨時變數nCount加一。為了便於理解,我們可以在讀這行指令碼的時候,加上「賦值」這兩個字。這樣一來,這句話就可以被翻譯為「將nCount復值為nCount1」。

如果nCount的初始值為9,那麼這句話會把它重新賦值為10

另外,這句話在nwn指令碼中還有一種簡單的表示方法:


nCount++;


符號「++」表示遞增1。這種表示方法很方便,但是對於第一次接觸指令碼的人來說,這可能比較容易令人疑惑。

第二行是我們的老朋友ActionSpeakString,只不過,它的輸入內容有點兒奇怪不是麼?在這裡我做了一些工作:首先,注意到我們事實上在輸入裡面有3個東西,分別用「+」相連線。當「+」被應用在string(字串)型別上的時候,它的作用是將一個字串「接到」另一個字串後面。舉一個簡單的例子:」Thisis a string.」和」Thisis +astring.」是完全一樣的。

儘管這和我們實際指令碼中的內容有點出入,但以上的解釋會方便我們理解中間的那個東西:IntToString(nCount)。和字面(如果你看得懂這個簡單的鳥語)意思一樣,這個函式把輸入的int(整數)型別變數轉化為一個string(字串)。

對於一個指令碼FC來說,這的確有一點困惑,我打個比方來加深你們的理解:當我說出「大象,真大!」的時候,你馬上可以理解我的意思,但當你要把它敲入電腦的時候,你就會開始這樣思考:大--逗號---感嘆號。

電腦同樣也是這樣思考的。他儲存變數的時候,並不會思考如何把他「寫出來」。IntToString告訴他把那行字元轉化為可以說出去的話並列印出來。

最後一行你應該可以明白。他是在把nCount的新值重新賦給本地變數SINGER_COUNT。還記得麼?nCount是臨時變數,當指令碼結束後,它就消失了,為了讓他有記憶性,以使我們可以切實的計數,我們需要把它的值儲存下來,而儲存在npc身上的本地變數就是這個作用。


作者: None    時間: 2014-4-15 16:20:44

本帖最後由 None 於 2014-4-17 21:00 編輯

第三章:條件語句
前瞻:

第二堂課本來要比現在長兩到三倍,但最終我決定把它分為幾個部分。

如果你足夠仔細,你應該已經注意到了一些毛病,英語語法上的毛病。當你測試模組的時候,你會聽見歌唱家這樣唱到:「Ihave spoken 1 times」,是的,這並不難理解,但和自然語法比起來,它還是顯得不太合適不是麼?

解決這個問題的方法是使用「條件語句」,在第三堂課之前,我不會在這裡對他進行解釋,不過下面就是解決這個問題的指令碼,用它替換OnHeartBeat裡面的內容就可以了。


intnCount=GetLocalInt(OBJECT_SELF, "SINGER_COUNT");

   voidmain()

   {

   nCount= nCount + 1;

   if(nCount == 1)

   {

      ActionSpeakString("Thisis the first time I have spoken.");

   }

   else

   {

      ActionSpeakString("Ihave spoken " + IntToString(nCount) + " times.");

   }

   SetLocalInt(OBJECT_SELF,"SINGER_COUNT", nCount);

   }


我希望這個指令碼不算太難理解。並且,事實上,一旦你理解了他,你可能離自己寫指令碼只差一步之遙了。


開始吧!

這次我們要講的可能是指令碼裡面最重要的部分喲...就是如何讓你的程式碼只在某些時候執行也就是說僅當滿足某些條件才執行。你可以看看bioware論壇,90%的問題都是透過分為這樣和那樣情況來解決的。第二課裡面偶已經提到過這個問題了,不過沒有解釋。這一次我們要看的例子程式碼比較複雜,所以說點理論的咚咚先。

條件語句(if-語句)的基本格式就是


NWScript:

if( condition )

{

   do_this;

   do_something_else;

   do_another_thing;

}


這不是真的指令碼,只不過是給你一個概念。

基本上,如果這個"condition"條件是滿足的,指令碼將會執行{ }之間的程式碼。如果不滿足,啥都不作。(注意格式:if(...)這一句後面沒有分號的唷)

這就是條件句型的全部了...最重要的是找出條件是什麼。有許許多多的條件你可以放在這裡,一些規則決定他們如何工作。這短短的文章不可能覆蓋到所有的規則。實際上我自己也不知道所有的函式只希望你能了解到一些基本的東西,讓你可以看懂別人的指令碼,然後從裡面學習新東西。

大部分條件是比較。例如,第二課最後部分裡面的條件句就是if(nCount == 1) 留意"=="2個等號。以前說過,一個等號是用來設定變數的值的。2個等號的意思是詢問「這2邊的貨色是否一樣呀?」看看這句運算式,這2邊看起來很不一樣啞~一邊是一堆文字,而另一邊則是個數字。這2便怎麼可能一樣呢?嗯嗯,要記得nCount是個變數呀~所以,是否一樣要決定於nCount裡面存放的數值。如果nCount裡面存的是1,這條件顯然就滿足了,下面的動作就可以執行了。而要是nCount放的是7,條件就不滿足,那麼什麼都不會發生。另外一個常用的判斷條件是否成立的方式是透過一些預定義的函式。在下一個例子中將會看到。

例子腳本:讓我們建立一個條件很簡單的指令碼。用到的一些新的函式以後會慢慢解釋的。那麼開始吧

-開啟我們一直用的那個測試模組。

-建立一個新area,地形為forest,大小4x2。起名叫TestArea 002

-在地圖的一側畫一個模組起始點startlocation

-在另一側畫一個npc。簡單起見,弄個平民commoner就好。

-把這個npctag改為GUARD- 開啟npc的指令碼欄,刪除所有指令碼(下一課裡我們就不必老這麼幹了...)

-來到"OnPerceived"那個槽,輸入如下腳本程式碼。


NWScript:

objectoSeen = GetLastPerceived();

voidmain()

{

   if(GetIsPC(oSeen))

   {

     ActionSpeakString("Greetings,friend.");

   }

}


-儲存,起名為tm_guard_op(op代表OnPerceived)

-那麼一路都Ok,儲存模組,測試它。

朝著這個npc跑過去....當你接近他的時候,他就會說"Greetings,friend."這句話,然後你跑開,再度接近他,他會再次說話。如果你一直和他保持比較緊的距離,別害怕,他什麼都不會做。

那麼再度使用Q&A模式吧~

Q:又是一個新的槽?OnPercieved是幹啥用的?

A:npc注意到什麼的時候,槽裡面的指令碼就會執行。當然如果你隱形或者躲藏著接近他,他不會發覺到你,對應的指令碼就不會執行。我們希望這個npc看到pc就打招呼,所以要用到這個槽。

Q:第一行裡的object又是啥玩藝?你幹嗎老把東西放在main前面?

A:呃,是的,和以前一樣的用法,這是一個新的資料型別"datatype"。以前,我們設定一個變數來存放整數,現在,我們還是設定一個變數,只不過存放的是物件"object"。以前說過了,以後也可能再重複:遊戲裡幾乎所有的東西都是objectNPCs,players, items, waypoints,placeables...這些都是object。很多很多函式僅用來指出某樣東西究竟是什麼型別的object,還有更多的函式用來操縱這些object。在這兒,我們建立了一個臨時變數,叫做oSeen(開頭的o表示這是個object),然後存了個值進去。

Q:這一行裡的GetLastPerceived()是什麼咚咚?

A:這是一個BioWare寫好的函式。所有Get開頭的函式都回傳某種資料。這些名字都很容易猜到意思...這個函式給出npc剛剛看到的那個物件。(友情提醒...OnPerceived槽之外用這個函式可能會出乎你的意料,最好不要。)綜合上一個問題,這第一行指令碼的意思就是得到一個可用的object變數,而這個物件是npc看到的咚咚。

Q:前面說了那麼多關於比較的事情,而現在這if裡面就一句...這是幹什麼的?

A:呃,這是個GetIsPC函式,這個函式把一個object作為輸入值,如果這個object是個PC的話,回傳真(TRUE),反之則回傳假(FALSE)。這就是我們需要判斷的!如果這物件是一個PC,就和他打招呼。如果不是pc,就不用理他,你不想你的npc看到狗跑過也打招呼吧?你也可以寫成 if(GetIsPC(oSeen)==TRUE) 這樣看來更像是個比較...然而,這實在沒必要,要養成好的編輯習慣喲~

Q:這個if到底是否必要呢?npc是不是只會注意PC?

A:在這個小小的testmodule裡面,除了pc之外啥都沒有,確實沒有其他東西值得留意的。但是在一個真實的模組裡,如果一個氣勢洶洶的地精走過呢?你不想他對著它打招呼吧。即使是其他的npc...周圍沒有pc在的話說了也沒p用。

Q:!說道點子上了!不是所有的PC都是友好的呀!

A:好主意。那麼在弄弄我們的模組吧。修改一下這個Guardnpc。現在我們要做的是讓這個guardpc身上的標誌。就用戒指吧~如果pc沒有這個戒指,npc就會攻擊;如果有的話,就會友好的打招呼,不錯吧~

-開啟toolset,載入模組,開啟TestArea 002

-首先,製作那個戒指。來到右側面板"PaintItems", "Miscellaneous", "Jewelry", "Rings","Copper Ring" 把它放在模組起始點附近。

-編輯copperring的屬性,把tag弄成PASSRING

-你可以自行加上名字和描述等,在這指令碼裡,只有tag起作用。

-再度開啟guardOnPerceived槽,改之:


NWScript:

//Friend or Foe Script:  tm_guard_op

//This should be placed in the OnPerceived handle of a guard.

//

//The guard will check to see if a PC has a passring, and if not,attack.

objectoSeen = GetLastPerceived();

objectoRing = GetItemPossessedBy(oSeen, "PASSRING");

voidmain()

{

   //If it isn't a PC that the guard sees, it won't do anything.

   if(GetIsPC(oSeen))

   {

     if(oRing == OBJECT_INVALID)

     {

        //If the PC doesn't have the ring, attack the PC.

       ActionSpeakString("Die,trespasser!");

       ActionAttack(oSeen);

     }

     else

     {

        //Otherwise the PC does have the ring.  Be friendly.

      ActionPlayAnimation(ANIMATION_FIREFORGET_GREETING);

       ActionSpeakString("Greetings,friend.");

     }

   }

}


你發覺到了吧,我寫的指令碼有越來越多的command加進去了。一開始可能有些迷惑人,但是隨著你對基本結構越來越熟悉,很快的就會對這些command了若執掌的。

存檔,進遊戲測試。首先,撿起地上的那個戒指,走向那個npc守衛。他會很友好的。接下來,跑遠一點,把戒指扔掉,再接近他,這次他應該會跑過來k你的。


分析:

Q:!你又作了一個新的初始化,我討厭這樣!

A:我要說:相信我(嘔...),沒有這些指令碼看起來會更糟。那麼來看看這新的一行吧。objectoRing = GetItemPossessedBy(oSeen, "PASSRING");再一次的,又是一個臨時變數,裡面存放著objectGetItemPossessedBy這個函式接受2個輸入值...第一個是你要檢查包包裡物品的那個生物物件,第二個則是你所期望看到的物品的tagoSeen就是定義好的那個觸發這段動作的人,也就是被npc注意到的那個傢伙。所以,oRing就是那個人身上帶的那個tagPASSRING的戒指。

Q:那麼如果那個人沒有這個戒指,會發生什麼呢?

A:唔,GetItemPossessedBy函式仍然執行,然而找不到那件東西,於是就回傳一個值OBJECT_INVALID。通俗地說就是指「沒有那種東西」。

Q://後面跟的是什麼?他們看起來像是e文,而不像指令碼?

A://後面的東西會被指令碼忽略,這些稱為注釋"comments"。好的指令碼編寫者會在注釋裡解釋這些程式碼的原理的喲~這是一個很好的習慣。當你寫的時候你自然很好的理解你所寫的東西...但是一個月後,當你要修改它的時候,你還能記得多少呢?並且,為了讓想看你的指令碼的人能更快更好的理解,這也是十分必要的。

Q:main之間的東西好多啊~4{}!我怎麼知道它們各自的範圍呢?

A:這的確很糟,指令碼越複雜,這些巢狀的"nested"運算式就越多。有一個小技巧可以幫你。一些人喜歡用空行來把整段指令碼分成好幾塊。我以前也這麼做,我不認為這有多大的幫助,但是也不費什麼力氣,所以我也這麼做。注釋同樣可以起到分割指令碼的作用。不管怎麼努力,他看起來還是很糟糕,慢慢得你會習慣的。

Q:"else"是什麼咚咚?A:"else"if-判斷句的附加裝置。格式如下:


if( condition )

{

do_this;

and_this;

}

else

{

do_this_instead;

and_also_this;

}


簡單地說,如果"if"那部分不滿足條件的話,就做"else"後面的那部分。我們整體的分析一下這個if-句。它檢查...那個叫oRing的臨時變數是否等於OBJECT_INVALID?(就是說,pc是否有那樣東西在身上?)如果等於,就攻擊。反之,如果不等,就是說PC有這個戒指,那麼做"else"那部分,友好。對不少人來說,這樣抽象的思考邏輯不太容易。如果你習慣於比較具體的東西,那麼畫流程圖吧。

Q:這些新的動作action又都是幹什麼的?

A:看名字就可以知道了吧~ActionAttack顯然就是攻擊你所輸入的那個物件。ActionPlayAnimationnpc表演一段動畫animation–在這裡,動畫名字叫做ANIMATION_FIREFORGET_GREETING.這個動畫就是讓npc揮手。

另一個修改之處我還要放一個新東西進這片教學(好吧,其實我在說謊...我想放進的東西實際上還有5,217個,但是,我想讓文章更容易被理解.)

如果我們想讓這個npc作出相反的舉動呢?我們想讓他看到pc有戒指就攻擊,沒戒指才是友好。比如說,這戒指是失竊品?

只要改變一個字元就可以辦到:if(oRing == OBJECT_INVALID) 改成if(oRing != OBJECT_INVALID)

"!="是另外一種比較。它檢查2邊是否不一樣。所以現在,如果PC有戒指,會被這個守衛追殺喲。


總結一下:

這裡出現了不少新東西,一旦你掌握了,NWScript的大部分威力就到手了。一般來說,混合條件語句和本地變數可以完成大部分的工作。


作者: None    時間: 2014-4-15 16:22:59

本帖最後由 None 於 2014-4-17 21:01 編輯

第四章:使用者定義事件
擊敗第3章中的指令碼

如果你足夠細心的看這個系列教學的話,你應當會注意到第3章中的guard指令碼不夠好,並不是完全符合我們的期望。每當這個守衛表現得很友好的時候,都會連續問候你2次。雖然要足夠仔細才會注意到,但是俺們還是要修復它。並且乘這個機會搞清楚另外一個概念。首先,我們要查明問什麼這個守衛會表現得如此大條...這就需要弄明白OnPerception這個指令碼處理處。有4件事情會引發這裡的指令碼:

-npc看到某物

-npc聽到某物

-npc注意到某些東西從視野裡消失

-npc注意到某些東西不再發出呻吟..不對,是聲音

可想而知之這裡發生了什麼,npc既看到了又聽到了pc,所以這指令碼執行了2次。這當然算不上世界末日,然而總是很不爽。既然不爽就要修復它:我們讓這個守衛只在看到咚咚的時候做反映,看到你以後他就用眼睛來找那個戒指,通常要聽到一個戒指不太符合邏輯的說。

我們可以再用一個巢狀的if-運算式搞掂之,不過這似乎有點複雜~簡單來說,我們想要做的是讓同時指令碼檢查2件事情:守衛發現的物件是個pc,而且是用眼睛看到的。這2樣都為真才做接下來的事情。只要修改判斷條件就可以做到,這裡用到了"&&"&&(讀作"""and")在判斷條件中表示同時考慮2個條件。只有2個條件都滿足,整個的總條件才算滿足了。那麼,來改吧:

if(GetIsPC(oSeen))

變成 if(GetIsPC(oSeen) && GetLastPerceptionSeen())

現在,只有當這個守衛用眼睛察覺到pc的存在時,這段指令碼才執行。GetLastPerceptionSeen這個小函式顧名思義就是當剛才是透過肉眼發現標的的時候回傳真TRUE,透過其他手段發現的則為FALSE。還有一種連線2個條件的手段是||,這裡就不做例子了。||讀做"""or"意思是只要滿足2個條件中的一個,整個總的條件就可以滿足了~這可以用於當這個守衛有多種借口可以砍你的時候(比如說pc沒有帶戒指,或者是pc帶著市長的腦袋,每一條都足夠這個守衛幹掉你了。)


好吧,我承認....

其實我以前一直在對你說謊。前面幾課我一直說我在這裡寫的指令碼堅持用比較規範的寫法是因為這些指令碼都可以滿足實際的要求。然而我現在必須指出目前為止教學裡指令碼無一可用...問題在於,我們的這個守衛在很多情況下都沒有應有的反應。最簡單的情況,你可以做個實驗:增加這個守衛的hp和等級,免得被你一刀就砍死了。進入模組,檢起戒指,守衛就會友好的對待你。現在上去給他一刀~...他怎麼沒反應...就像秀逗了,這明顯不是我們所期望的。我們造出這樣的一個智障+低能是因為我們丟棄了所有的BioWare寫好的官方指令碼。我們刪除的那些預設指令碼定義了一系列的標準行為。(實際上,雖然我們現在造的這個npc不要預設指令碼,然而大部分情況下,我們都需要這些指令碼。)那麼,問題就在於,如何在加入我們定義的行為的同時,保留那些很好用的預設行為呢?我們使用使用者定義"userdefined"指令碼處理點scripthandle。只要你夠聰明,完全可以利用所有的handle–現在我們在OnSpawn指令碼裡略作一些小改動,大部分指令碼都寫在UserDefined裡面。

讓我們繼續來完善這個守衛npc

-開啟TestModule -移除這個guard

-重新在原地造一個新的npc(這樣所有的預設指令碼又回來了)

-npctag改為GUARD-"Advanced"欄,確保guard屬於平民"commoner"一族faction

-來到scripts欄。編輯"OnSpawn"指令碼。那裡面有好多好多東西,無視其中的大部分。來到這段指令碼的底部,找到如下一行:


NWScript:

//SetSpawnInCondition(NW_FLAG_PERCIEVE_EVENT);

//OPTIONALBEHAVIOR - Fire User Defined Event 1002


把第一個//去掉。(保留第2)

-儲存指令碼為tm_guard_os-關閉script窗口,來到OnUserDefined那個handle

-選擇指令碼tm_guard_op-在指令碼編輯器裡開啟它。把它另存為tm_guard_ud

-更新注釋comment以和當前的位置,名字相符合。

-再度儲存。

-好了,收工,儲存你的模組。

現在進入遊戲,我們的守衛表現的正常多了。以前寫的友好或者敵對的判斷還在,並且對其他狀況也能有所反應了。如果你攻擊它,他會反擊。還有很多行為被寫好了,很多都在幕後進行,你看不到。那麼,我們剛才到底做了些什麼?嗯,當我們移除OnSpawn指令碼裡的//時,我們對一些東西"uncommented"取消注釋。還記得指令碼裡面所有//開頭的一行都會被忽略麼?BioWare所作的就是在OnSpawn指令碼裡面留下了很多可選的"optional"專案,把//放在開頭關閉這些專案。

但是現在,我們需要開啟一些選項,k掉前面的//,我們需要他們起作用。那麼,這行程式碼到底是幹什麼的?最基本的來說就是表明:"OnPerception指令碼執行的時候,記得把OnUserDefined裡面的指令碼也一起執行掉。"你可能已經開始想到"如果我同時要在OnPerceivedOnHeartbeat裡作一些特別的行為該怎麼辦?"

這還是可以辦到滴~就是要耗一些功夫。簡單起見,我們讓npc只作一些簡單的事情。這個npc每隔6s鐘說一遍"I'mbored",然後當看到pc時就鞠躬示意。

-開啟toolset,畫一個npctag叫做BORED

-開啟OnSpawn指令碼,去掉OnHeartBeatOnPerception2行前面的注釋//符號。

注意到2者都有一個數字聯繫在一起。OnPerception1002OnHeartBeat則是1001

-儲存為tm_bored_os

-把以下指令碼放進OnUserDefined,儲存為tm_bored_ud


NWScript:

//On User Defined Script: tm_bored_ud

//Will be called by the OnHeartbeat and OnPerception scripts

//

//The npc will complain about being bored every six seconds,

//and will bow if it sees a pc.

//

intnCalledBy=GetUserDefinedEventNumber();

voidmain()

{

switch(nCalledBy)

{

   case1001:  // Called by OnHeartbeat

     ActionSpeakString("I'mbored.");

     break;

    //

   case1002:  // Called by OnPerception

     objectoSeen=GetLastPerceived();

     if(GetIsPC(oSeen) && GetLastPerceptionSeen())

       ActionPlayAnimation(ANIMATION_FIREFORGET_BOW);

     break;

}

}


Question&Answer時間又到了....

Q:GetUserDefinedEventNumber是什麼?

A:記得上面提到的數字了麼?BioWare很聰明(noname:我不完全這麼認為)...不僅在每一個指令碼處理點handle裡面都可以呼叫使用者定義指令碼,並且呼叫的時候都傳給它一個不同的數字。所以,第一行所作的就是檢查到底是哪一個指令碼handle在呼叫使用者定義事件。是Heartbeat(1001)還是Perception(1002)?

Q:switch我以前沒見過呀?

A:我不打算詳細解釋,簡單來說,你給switch一個整數輸入值,指令碼就會跳到"jumps"case標記的同樣數值的地方。這樣的話,如果nCalledBy1001,指令碼就會往下尋找"case1001:"那一行。從那一行開始執行命令,直到遇到一個叫break的。

一些格式問題:switch命令知包括在{}之間。而且,和if一樣,結尾也沒有分號。當寫使用者定義事件的時候,我一般都會把它們寫在一個switch裡面,即使只有一種情況....這麼做是因為我可能以後會改注意。預先弄好,省得到時候手忙腳亂。

Q:喂喂,那個if語句裡你沒有用{}!

A:如果只有一句,就把大括弧省了吧~這樣看起來更舒服,也更整潔,當然這只是個人觀點。


作者: None    時間: 2014-4-15 16:25:34

本帖最後由 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-CommonerCommoner-Target都設為50

-Dartboardfaction改為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

-npctag改為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"dartblueprintref,最後那個3則是stacksize。除了這些,看看你自己能不能搞明白這段指令碼。這裡面有一些新的命令,但是那些名字已經很好的解釋了用途。有不明白的地方就提問吧,(問原作者或者問我都可以呀,Nonameat your service)最近,有不少人在罈子裡要實作各種詭異功能的指令碼。如果我有時間,我當然樂意解答,但你們也知道,我只是一個地精苦力,並不是總有富餘的實踐來回答各種問題(而且,小白的需求總是高的要命,甚至不切實際)。所以,我將花費一點時間來介紹一些地方,當你遇到指令碼的問題,你可以去那裡尋求幫助。事實上,你們問的超過90%的問題都可以在那裡找到答案。


相關資源

總的來說,有4個地方是你可以經常光顧的,排名不分先後:

*ScriptingFAQ(在bio官方論壇上,如果鳥文可以的話就去)

*官方toolset論壇(別裸身跪求,別一味索取,這裡是提問的地方,如果你只有需求沒有問題,最好別發帖)

*官方模組

*toolset本身!

讓我們來看看這四個東西到底怎麼用(當然,還有Lexicon呢,不過你已經知道了,呵呵)

///////////////////ScriptingFAQ(如果你鳥文不好,這個對你沒用)

它被稱為「常見問題」是有原因的。其中的指令碼都是經過編譯和校訂後,可以切實有效的解決很多常見的指令碼問題的。如果真的遇到了什麼問題,你最好先在這裡面查檢視。也許這裡的內容無法直接解決你的問題,不過也許未來就會有了,因為這個東西是不斷更新的。

而且,不必害怕去看那些當前對你的問題沒有任何幫助的內容。通常情況下,這裡的問題,你遲早會碰上的。你對nwn指令碼了解的越多,你就會越發覺得這裡的東西很有用。一般,每當我讀哪些我用不到的指令碼的時候,我總是可以看到一些有益的東西,它可能是一個我沒有用過的函式,或者是一個十分巧妙的使用變數的方法。

由於這裡的指令碼作者的多樣性,裡面總是會有一些內容是在你當前的理解範圍之外的。如果真的是那樣……沒辦法,多學點再回來看吧。即便是現在,你也可以看懂其中的很多東西了,很驚訝不是麼?你不妨立刻就去試試看。

//////////////////////星空官方論壇(或者TROWtoolset版(廣大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這個動作……所以我們為PCAssignCommand。這個方法在任何地方都適用,只要你可以傳入正確的物體(object

另外一個指令碼也並不像他看起來那樣難。關鍵點其實就是如何利用本地變數來記錄兩個控制桿的開啟和關閉狀態。不過,這裡仍然有些東西值得一提。我在指令碼中連用兩次nUsed1是不太好的。一次,我用它代表任何一個控制桿的狀態,下一次,我又用它來表示LEVER1的狀態。有些程式設計師可能會給我一板兒磚,警告我應該分別給他們定義兩個變數。不過,既然這只是一個簡單的指令碼,而且這兩種用法是如此相近,為了方便,我就這樣做了,當處理更大的指令碼時,我想我會給他們分別定義兩個變數的。

另一個值得一提的東西是CreateObject。透過這個函式,我希望大家能夠把標籤(tag)和藍圖示簽(blueprintResRef)區分開。在遊戲中的任何一個物體,都有自己的標籤。但是,那些還沒有被建立的物體就沒有標籤了。不妨把標籤想像為一個木頭牌子,如果這個物體還沒有被創造出來,這個牌子又怎麼能掛在他的頭上呢?

為此,要建立物體的時候,我們需要找到一個唯一的標籤,來告訴便一起,我們到底需要創造什麼物體。這就是」plc_portal」所作的事。他是傳送點這種可放置物體的藍圖資源參照標籤(blueprintresourcereference)。得到一個物體的ResRef,你可以直接到物體面板裡面,右擊物體,選擇屬性,進行察看(在高階面板裡面)。值得注意的是,標準物品的ResRef是不可能以001為結尾的。

除了以上這些,我希望其他東西還算比較好懂。我盡量把注釋寫得比較細緻,以幫助你們理解指令碼的內容。


課外提高練習

好吧,現在,我老師的本性又顯露出來了……留作業的時間到了!我不會強求你做這些東西,但他們很值得你做一做,不管你是為了鞏固本次課程的內容,還是從中觸類旁通到更多的東西。

那麼,試著做做這個吧:剛才,我們所作的:開啟兩個控制桿,傳送點出現,這很正常。開啟一個,然後關掉,再開啟令一個,再關掉它,傳送門不會出現,這也正常。但是,當我們開啟傳送點後,關掉其中一個控制桿,傳送門還在!這就不太正常了。現在,就請你解決這個bug,使得當一個控制桿被關閉後,傳送點隨即失效。當然,如果再開啟他,傳送點又會重新生效。

擴充功能內容:剛才,傳送點是單向的,請把它做成雙向的,使得你可以透過傳送點再傳送回來。


作者: None    時間: 2014-4-15 16:29:17

本帖最後由 None 於 2014-4-17 21:04 編輯

第六章:迴圈

我想,第一部分內容應該算是中等難度,而第二部分則是簡單,對大部分人來說之前五堂課,我們討論和學習的東西,都是在nwn指令碼編輯時,經常回使用到的技能,而從這堂課開始,我們將開始學習一些在nwn中比較少使用,甚至很少使用的東西。

但這並不是我會躊躇不決的原因……真正的原因是,這這將是我們第一次討論可能很「危險」的東西。當然,之前我們學過的東西也很容易犯錯誤,但代價只是指令碼不能實作我們預期的功能,但這堂課的要學習的技術,如果犯了錯,則可能真的使事情變得不可收拾。

幸好BioWare對這些錯誤做了一些工作以使他們不會滑落到失控的地步,但我們在使用下面即將討論的東西時,仍然需要十分小心。


什麼是迴圈?

簡單的說,迴圈就是讓你的指令碼連續執行多次。編譯器執行指令碼一次,然後檢查是否需要繼續執行,如果是,就回到標記好的指令碼開頭,再次執行,直到他被告知不需要繼續執行為止。

使用迴圈的關鍵是,知道在哪裡使用他,並且知道如何設定「檢查點」讓它能夠在預期的時候停下來。「檢查點」是非常重要的——這也正是我之前提到過的「危險」。如果你的「檢查點」設定的不正確,編譯器可能永遠都不會跳出迴圈,不斷地執行指令碼,好在,BioWare針對這點進行了限制,如果迴圈次數過多,引擎會自動終止迴圈,但即便如此,潛在的危險還是存在的。

迴圈有兩種:「for迴圈」和「while迴圈」。首先閃現在你們腦海裡的問題可能是:「我怎麼知道什麼時候用什麼樣的迴圈?」我希望我可以給你們一個經准而100%正確的答案。但事實上,什麼時候該用什麼迴圈,很大程度上是根據經驗來的。事實上,所有用while迴圈可以做到的,for迴圈也可以,反之亦然,那麼,為何還要分出兩種迴圈呢?

不過,還是有一個通用規則可以用的。這個規則不完美,但它基本上可以解決我們會遇到的所有情況。如果你要反覆做一件事的次數是已知而且固定的,用for迴圈。如果在指令碼執行前,你無法得知它可能執行的次數,那麼,用while迴圈。讓我們先單獨看看這兩種迴圈。


For迴圈

for迴圈的關鍵是你需要有一個「目錄(index)」變數來指導迴圈的進行。

它的基本形式是這樣滴:


intnIndex;

   for(nIndex = 1; nIndex <= 7; nIndex++)

   {

    作點什麼;

   }


這有點複雜,所以讓我們分而制之。

首先,我們要建立「index(目錄)」變數。(在C語言中,index辯論可以在迴圈內部建立,但nwn指令碼裡只能在迴圈前定義)我們不需要給它指定初始值,但我們必須提前定義它。

for後面的括弧裡,有三個東西。首先「nIndex1」是初始化index變數。外面已經定義了index變數,所以在這裡我們要做的是給它一個初始值。

第二步分「nIndex<=7」被稱作「條件」。這裡是決定迴圈什麼時候執行的地方。直到這個「條件」被滿足,這個迴圈都會繼續執行。迴圈每執行一次,都會來這裡檢查一下是否滿足條件了,知道當這個條件不再是真的了(不再被滿足了)。因此,在幾次迴圈後,nIndex如果變成8,這個迴圈就會停止並跳出了。

最後一部分「nIndex++」是在每次迴圈的結尾要執行的程式碼。我之前的課程已經提到過「++」了,所以我就不再贅述了。

把他們的結合到一起,什麼事情會發生了?nIndex最開始的時候是1,比7小,所以迴圈會執行「做點什麼」裡面的指令碼。第一次迴圈結束後,第三部分nIndex++執行,nIndex變為2,程式回到「條件」那裡進行檢查,還是比7小所以繼續執行「做點什麼」裡的指令碼,然後,nIndex++,變為3,還是比7………………如此,直到nIndex變為8,進行條件判斷,發現nIndex<= 7不成立了(FALSE),這時,迴圈就結束了,「做點什麼」不再執行,編譯器繼續執行迴圈之後的程式碼。

For迴圈的例子

這些東西比較複雜,讓我們來舉一個例子。我們要懲罰一個老是愛砸箱子的玩家。所以,我們做一個箱子,只要pc一旦打碎它,它就會招呼5個殭屍5個骷髏在玩家周圍。我們可以連續使用CreateObject10次,但是如果用迴圈的話會更加合適。

1.mod裡放置一個箱子。

2.在「上鎖」的屬性面板,設定為「鎖住的」,並把開鎖DC設定為100

3.在指令碼欄,編輯「OnDeath」一行,輸入如下指令碼:


   1.

//On Death Script: tm_chest_dt

//

//This script summons 5 zombies and 5 skeletons

//near the person that destroyed the chest.

//

//Written by Celowin

//Last Updated: 7/13/02

//

voidmain()

{

  //Initialization: Get the location of the PC that destroyed the chest,

  //Set up for the loop.

  objectoPC = GetLastKiller();

  intnUndeadIndex;

  locationlSpawn = GetLocation(oPC);


  //Loop 5 times

  for(nUndeadIndex = 1; nUndeadIndex <= 5; nUndeadIndex++)

  {

    //Each loop, create a zombie and a skeleton at the PC's location


    CreateObject(OBJECT_TYPE_CREATURE,"nw_zombie01", lSpawn, TRUE);

    CreateObject(OBJECT_TYPE_CREATURE,"nw_skeleton", lSpawn, TRUE);

  }

}


測試這個mod,打碎箱子,你的角色就被一群不死生物圍起來了,很簡單吧。

另外要補充的一點是,迴圈的三個部分都可以改變,比如第三部分改為nIndex= nIndex + 100,但是,一般情況下我們只需要改變第二部分,也就是「條件」部分,就可以實作我們的目的。

讓我們來調整一下剛才的指令碼,這次我們只召喚殭屍,但是我們將召喚和pc等級相同個數的殭屍。我不會寫出這個指令碼,但我會告訴你需要更改哪裡:

1.在「objectoPC」那一行下面,加入這行指令碼:innPCLevel = GetHitDice(oPC);

2.在迴圈開始的地方,在第二部分,修改為:nUndeadIndex<= nPCLevel

3.刪掉召喚骷髏的那一行。

用不同等級的人物測試一下吧!

for迴圈的總結

使用for迴圈的關鍵是要理解他的三個部分。初始化部分只在迴圈的最開始執行一次,條件部分在每次開始新的迴圈時進行判斷,如果是真的,就繼續迴圈,否則跳出迴圈。第三步分在每次迴圈結束後執行。

最重要的是,一定要讓你的迴圈最終停止。如果你把條件設定為nUndeadIndex>0,那麼,召喚就不會停止,也許看看他最終會召喚幾個殭屍很有趣,但在真正大mod設計時,這是要竭力避免的。


while迴圈

while迴圈和for迴圈很相似,但是,我認為它更難用,因為,它所包含的不確定因素更多。我之前曾經提到過,當你不知道一個迴圈將要進行多少次的時候,用whlie迴圈,這有點難懂,所以讓我來解釋一下……假設,有一堆地獄犬在一個區域裡遊蕩,那裡還有一個邪惡的祭壇,當你破壞祭壇之後,所有的地獄犬都會死掉。現在的問題是,你不知道到底會有多少地獄犬,因為pc可能已經k了幾隻了。因此,那個祭壇可以不斷地召喚地獄犬,直到它自己被破壞,這樣,周圍的地獄犬就不至於太少了。

現在的問題是,你不知道到底要執行多少次「殺死地獄犬」的命令。它可能是0,也可能是50。這是無法預知的,這時就應該使用while迴圈。從根本上說,我們是要實作:「一直殺死地獄犬,直到沒有為止」,換個說法:「當地獄犬還存在,殺死它」

while迴圈的基本格式是:

while(條件){ 做點什麼;}

這看起來很清楚不是麼?當指令碼執行到while迴圈,檢查條件,滿足條件,「做點什麼」,然後繼續檢查條件,如果還滿足,繼續做,知道條件不滿足了,跳出迴圈。夠簡單的吧,在很多地方,它和for迴圈很像。

我們待會再處理地獄犬的事情,首先,讓我們先哪點簡單的東西練手兒。我們經常需要在pc完成某個任務後,給pc經驗值。如果只是一個pc,那簡直是小菜一碟兒!但如果是多人遊戲呢?你如何能夠保證每個pc都得到了經驗呢?

記得我們之前那個PASSRING戒指麼?如果你沒有順序學下來,也沒關聯,你只要創造一個標籤為PASSRING的戒指就ok了,然後為一個npc編輯這個對話:


(開始)拿到戒指了麼?

   |-(PC反應1)沒有.

   ||- 結束對話.

   |

   |-(PC反應2)是的.

   |-(NPC 反應)謝謝.


對於pc反應2,直接在「對話出現」那裡使用指令碼精靈,創造一個「擁有某種物品」的指令碼,並把標籤PASSRING加入到擁有物品列表裡。對於npc反應,「謝謝」,在「執行動作」裡添加以下指令碼:


//Conversation Script: tm_guard_c1

  //Takes the ring from the speaking player,

  //And awards 50 XP to every PC.

   //

   voidmain()

   {

   objectoPC = GetFirstPC();

   DestroyObject(GetObjectByTag("PASSRING"));


   while(oPC != OBJECT_INVALID)

   {

      GiveXPToCreature(oPC,50);

      oPC= GetNextPC();

   }

   }


儲存指令碼,儲存對話,儲存mod,測試,你最好可以找個朋友和你一起測試多人的效果。

注意到,除了DestroyObject命令,我們所採用的還是for迴圈的那一套:三個部分,首先我們初始化(oPC= GetFirstPC()),我們設定條件(oPC!=OBJECT_INVALID),我們更新目錄變數(oPC= GetNextPC())。

所以,這個迴圈會一直執行並且給相應的pc50exp,知道pcOBJECT_INVALID,也就是沒有更多的pc了。

在寫while迴圈的時候,要注意給目錄變數更新,因為,它的第三部份在迴圈內部,往往容易被人忽略,少了oPC= GetNextPC(),那麼這個指令碼就會永遠迴圈給第一個pc50exp,直到崩潰。


作業

………………我原本打算把地獄犬的指令碼寫出來,但當我寫到這裡,我決定把它留給你們當作作業,因為,事實上,它所需的指令碼在本次和以往的課程中已經都以不同的形式出現了——到底用什麼事件觸髮腳本,用什麼函式,所有的東西。放一個祭壇,然後放一堆地獄犬,看看你能不能做到當你破壞了祭壇,所有的地獄犬都被殺死(難度=中等,如果你只是不斷的召喚新的地獄犬,難度=難,如果你可以控制地獄犬的數目在一個上限以下,不如不會多於10隻。)

還有個作業可以做做看:設定一個觸發器,當pc踩到就召喚2倍於pc數量的生物。(我把它作為附加題,但難度很一般不是麼?)


作者: None    時間: 2014-4-15 16:31:39

本帖最後由 None 於 2014-4-17 21:06 編輯

第七章:為物品編寫指令碼

我看過很多這樣的問題:「怎麼給一個物品編輯指令碼?」

答案很簡單:「你不能。」這個答案可能不怎麼令人滿意,但是,讓我來解釋一下。不管是出於什麼原因,BioWare並沒有允許直接為一個物品編輯指令碼。但是,我們仍然可以為物品編輯一些獨特的行為,只是做法比較間接一些。

當編輯一個物品的屬性的時候,在「施放魔法」的選項中,有兩個附加選項「物品獨特能力」(UniquePower)和「自身物品獨特能力」(UniquePower SelfOnly)。透過使用這兩個「魔法」,我們可以為物品加入特殊能力。「物品獨特能力」是需要指定一個施法物件,而「自身物品獨特能力」則不需要。

既然我們無法在一個物品的屬性裡加入指令碼,那麼我們怎麼讓它實作我們希望的功能呢?現在,進入編輯->模組屬性->事件面板,你會看到附加給整個mod的一些事件指令碼。其中有一個選項是"OnActivateItem"(使用物品),這就是我們要找的東西摟。但是,首先我們要看幾個在我們為物品編輯特殊能力的時候需要使用到的函式:

1.GetItemActivated-回傳被使用的物品。

2.GetItemActivatedTarget-回傳使用「物品獨特能力時」所瞄準的標的(object)。

3.GetItemActivatedTargetLocation-如果「物品獨特能力」瞄準的是地面,這個函式可以回傳一個位置(location),如果瞄準的是物體,則回傳那個物體的位置。

4.GetItemActivator-回傳使用物品的PC

這些較少很簡單,我們將在下面的例子中一一學習他們。


例子1:破損的治癒項鍊

現在,讓我們做一個可以治療PC的項鍊。為了能讓他更有趣一些,我們假設它不是一個完美的治癒效果-它只能治療PC一半的傷勢,為了讓它不會過於強大,我們設定PC一天只能使用一次。

首先,建立基礎物品:

1.開啟物品精靈。

2.選擇「項鍊」作為物品種類。

3.名字寫上「破損的項鍊」。

4.在「魔法的」選項框中選擇「15級」,低品質。

5.在下一個對話面板,選擇物品分類:雜物->珠寶->項鍊。

6.最後,開啟物品屬性面板。

7.在基本面板中,設定標籤為「HEALNECK」(這很關鍵)

8.在屬性欄,去掉所有的特殊能力。

9.在「施放魔法」中選擇「自身物品獨特能力」(UniquePower Self Only

10.在右下角選擇編輯能力屬性,選擇「一次/一天」。

11.為了測試方便,在「鑒定」一欄打勾。12.點選OK

現在,開始編寫指令碼。進入「編輯」->模組屬性->事件,在OnActivateItem事件中加入編輯如下指令碼:


//Module OnActivateItem Script: tm_activate

//

//This script handles all the unique item properties for the module.

//Currently scripted items:

//Cracked Amulet

//

//Written By: Celowin

//Last Updated: 7/15/02

//

voidmain()

{

  //General Initialization:  Find what item was used

  objectoUsed = GetItemActivated();


  //The following section is for the Cracked Amulet

  //The amulet heals half the PCs damage when used.

  if(GetTag(oUsed) ==  "HEALNECK")

  {

    //Get the PC, first.

    objectoPC = GetItemActivator();


    //Find out how much damage was taken, we're healing half of that.

    intnDamageTaken = GetMaxHitPoints(oPC)-GetCurrentHitPoints(oPC);

    intnHealing = FloatToInt(IntToFloat(nDamageTaken)/2);


    //Perform the healing, and a little visual effect to show it working.

    effecteHeal = EffectHeal(nHealing);

    ApplyEffectToObject(DURATION_TYPE_INSTANT,eHeal, oPC);

    effecteVisual = EffectVisualEffect(VFX_FNF_SMOKE_PUFF);

    ApplyEffectToObject(DURATION_TYPE_TEMPORARY,eVisual, oPC, 1.0);

  }

}


儲存指令碼然後去測試一下(別忘記在mod裡面放一個項鏈在地上……

在這個指令碼中,有兩件事值得解釋一下。首先就是這一行:intnHealing = FloatToInt ( IntToFloat( nDamageTaken ) / 2 );

想法其實很簡單:我們需要把PC所受到的傷害值減半以作為治療PC的治療值。那麼,InToFloatFloatToInt是幹啥的呢??好吧,首先我們要知道,int是個整數對吧,但是,當我們做除法的時候,很可能會產生一個小數部分,這樣,我們就需要做以上處理以使最終回傳的值為整數int

float」(浮點數)是擁有小數部分的數,因此,當我們做除法的時候,我們首先要把整nDamageTaken轉換為一個小數(只有小數可以進行除法)……所有我們用IntToFloat。然後,但除法結束後,會計算出一個浮點數(小數),但我們需要最終的結果是整數……因此我們又使用FloatToInt來進行轉換。也就是直接捨掉小數部分啦~

現在,讓我們舉個計算的例子:我們說PC受到的傷害是17。我們首先把它轉換為浮點數17.0。然後除以2,得到8.5。我們把它轉換為一個整數,去掉小數部分,得到8。所以我們需要恢復8HPPC

第二個值得一提的東西是我們在最後幾行中所使用的「效果」(Effect)。幾乎每當你要施加某種影響給物體的時候,你都會使用「效果」,比如治療(Heal),視覺(Visual),特殊能力(SpecialAbilities),擊倒(knockdowns……這些全部都是效果。我們在這裡使用了兩種效果-首先是治療效果(Heal),另一種是在PC身上產生煙霧的視覺效果。使用每一個效果都分為兩個部分,定義效果和施加效果(這裡是給PC)。

對於治療效果,首先要確定治療的HP點數:effecteHeal = EffectHeal(nHealing);然後,我們治療PCApplyEffectToObject( DURATION_TYPE_INSTANT , eHeal, oPC);當施加效果的時候,DURATION_TYPE_INSTANT表示效果是瞬間產生反應的,沒有持續時間。eHeal是我們定義的效果,oPCPC自己。

下一個效果類似。我們定義煙霧效果,然後施加給pc。這個效果需要持續一段時間,所以持續的型別為:_TEMPORARY,然後,我們傳入效果,再傳入施加效果的物件,最後的浮點數表示效果持續時間。

可以使用的效果實在是太多了,但它們的使用方法都是和上面的大同小異。


例子2Alfred的戒指

對於那些剛開始對指令碼產生興趣,並且躍躍欲試的人,跳過這一段,我要開始囉唆了因為我想起我一個老朋友,他曾經是一個dnd玩家和DM。他曾經參加過一個非常幽默的戰役,我從他那裡聽到了很多有趣的故事。他扮演的角色是Alfred-雞人(were-chicken)。我記不清這個角色的背景故事了,但他的確擁有變形術……在月圓之夜變身,對於非銀質武器免疫,當然,還可以控制一些動物。好吧,如果你手頭有本AD&D第一版規則書,你可以查閱到一隻雞窩裡的雞的價值:一枚銅幣……200枚銅幣兌換1枚金幣,所以,用2GP,我們就可以擁有400隻雞陪伴左右。當然,單獨的一隻雞不會有什麼用,但是,如果你同時被400隻雞圍攻……不管如何,這個物品是出於對Alfred-雞人的紀念。

這個戒指的工作原理是召喚一隻雞,如果你施放標的是地上,那麼它僅僅是召喚一隻雞,如果你施放在一個生物身上,這隻雞就會去攻擊那個生物(這個……雞的戰力……

首先創造物品,過程和前面的一樣,只是把名字改成Alfred的戒指,標籤設定為CHCKRING,然後每天可以使用無限次特殊能力(如果這是一種有用的生物,這會不平衡,但誰會在乎一隻雞呢?)

現在,讓這個東西工作起來,我們仍然需要繼續編輯剛才那個指令碼。如果我們希望破損的治療戒指可以繼續工作,我們必須保留原來的那些指令碼,然後加入如下的指令碼:


//The following section is for Alfred's Ring

//Summons a chicken, which will attack the target if a creature

if(GetTag(oUsed) == "CHCKRING")

  {

    //Get where the ring was used

    locationlTarget = GetItemActivatedTargetLocation();


    //Set up the "gate in" effect

    effecteChickIn = EffectVisualEffect(VFX_FNF_SUMMON_MONSTER_3);


    //Summon the chicken

    objectoNewChicken = CreateObject(OBJECT_TYPE_CREATURE, "nw_chicken",lTarget, TRUE);


    //If the ring was targeted on a creature, the chicken will be summonedoffset a bit.

    //Update the location, then apply the effect.

    lTarget= GetLocation(oNewChicken);

    ApplyEffectAtLocation(DURATION_TYPE_TEMPORARY,eChickIn, lTarget, 1.5);


    //Finally, if check to see if an object was targeted.  If so, attackit.

    objectoEnemy = GetItemActivatedTarget();

    if(GetIsObjectValid(oEnemy))

      AssignCommand(oNewChicken,ActionAttack(oEnemy));

  }


然後,更新你檔案頭的注釋。(我知道每次更新注釋很無聊,……但是這總是一個很好的訓練)

儲存,然後測試一下吧。(注意:這隻雞事實上不會攻擊任何友好的生物,所有也許你希望在faction(陣營)上做點文章讓它工作起來。一旦做好後,效果會很搞笑,小雞會衝過去攻擊,然後害怕地逃跑……


一些警告

儘管有些人可能已經開始盤算起如何給物品加入各種cool到斃的特殊能力了,但我還是建議你們在自己的mod中不要使用太多。一個經典的理由是:你加入的物品越多,你留給玩家的震撼和印象就越小。另一個原因是,由於所有的物品效果都放在一個指令碼裡,如果物品太多,指令碼就會變得龐大而緩慢。

另一個警告是,你要記住,你的物品不會在另一個mod中工作。你當然可以拷貝指令碼在貼上過去,但是這不是自動進行的。

由於這些原因,一般物品的特殊能力都被限定為劇情物品。這不是必須的,但是在使用他們之前你最好考慮一下。


練習題

我想大部分人在劇情中對於物品的特殊能力都有不同的需求,但如果你需要一些練習,這裡我為你準備了一些:

1.在兩個不同的地點設定兩個祭壇。在一個祭壇前面祈禱,這裡就會被設定為你的回城點,然後你使用一個物品的特殊能力後,你就會被傳送回來。(中等難度)

2.如果你覺得Alfred的戒指召喚了太多的雞,減緩了遊戲執行速度,那麼,讓這些雞在一段時間後自動消失吧!(簡單,如果你找到了正確的函式)

3.做一個充能杖,它可以治療標的(不只是PC喲),但是每使用一次會減少PC100xp(中等,如果你可以加入一個「射線」beam效果,難度就是困難)。


作者: None    時間: 2014-4-15 16:33:53

本帖最後由 None 於 2014-4-17 21:08 編輯

第八章:函式(自訂函式)
介紹

這堂課會比較難,而且我不會僅是對內容淺嘗輒止。從第一堂課開始,我們都是在使用BioWare的函式。這一次,我們要開始自己寫函式了。

如果你無法完全理解我下面講的內容,不必擔心,因為這絕非一日之功,只有透過大量的練習你才能熟練掌握。如果你被它徹底整暈了,那就先甭看它,去做點別的工作,再回來看說不定你就會豁然開朗了。

在開始之前先說明一下。雖然我們要開始寫函式了,但這些函式只能在「本地」使用。也就是說,想像一下,你在一個NPCOnPerception事件指令碼裡寫了一個函式,那麼這個函式只能在這個指令碼裡面使用,換在別的事件指令碼裡,或者別的生物的OnPerception事件裡,這個函式都是無法使用的。

沒錯,這的確限制了我們寫的函式的使用。但是,即便如此,它還是留下了很多可能性的。而且,在未來的課程中,我會解釋如何製作通用函式,但是,路要一步一步走,飯要一口一口吃,不是麼?


函式宣告

任何一個函式都應該從類似下面的格式開始(這只是一個例子,說明一下格式):


voidGateIn(string sBluePrint, location lGate)


這簡簡單單的一行,卻包含很多複雜的原理。

第一部分 void告訴我們這個函式的回傳值是什麼。因此,在這個例子中,函式的回傳是…………空。可以回傳的數值型別有很多,比如intfloat object event...最常用的回傳值是空,我們堂課的函式的回傳值都將是空。

下一部分,GateIn是函式的名字。一旦我們定義好它,我們就可以使用它的名字來呼叫函式,就像BioWare定義的函式一樣。(當然,我們要記住使用自訂函式的限制:只在當前的指令碼中……

在圓括弧裡面是最難理解的部分……這裡是設定函式輸入的地方。我們為函式定義了兩個輸入值,一個string型別的,一個location型別的。

到現在為止,一切都還好。現在是煩人的地方了。在我們真正開始寫函式之前,我們需要明白當這個函式被呼叫後,會出現什麼效果。顯然,我們會利用那些輸入值做什麼事,要不我們不會定義那兩個輸入的。

假設,我們在指令碼中的某個地方使用了這個函式,比如這樣:


GateIn("nw_fireelder",lSummonPoint);


現在,「nw_fireelder」是我們的第一個輸入值,一個string。這個字串的作用是將"sBluePrint"變數設定為「nw_fireelder」。這樣,在函式內部的任何地方,我們都可以用sBluePrint來代替"nw_fireelder"這個字串。同樣的,第二個輸入lSummonPoint將變數lGate設定為了lSummonPoint所表示的一個地點(location)。


定義函式

現在,讓我們看看函式的主體部分:


//This function summons the creature with blueprint ResRef

//given by sBluePrint at the location lGate, then it has the

//summoned creature attack the closest PC to the object that

//calls this function.

voidGateIn(string sBluePrint, location lGate)

{

  //Create the creature

  objectoNewCreature = CreateObject(OBJECT_TYPE_CREATURE, sBluePrint, lGate);


  //Find the closest PC

  objectoPC = GetNearestCreature(CREATURE_TYPE_PLAYER_CHAR,PLAYER_CHAR_IS_PC, OBJECT_SELF);


  //Cause the creature to attack the PC

  AssignCommand(oNewCreature,ActionAttack(oPC));

}


注意,如果你立刻把這個函式拷貝到編譯器裡進行編譯,會發現錯誤:「缺少MAIN()函式」。如果只是這個函式自己,這不算一個指令碼,你仍然需要一個voidmain(),不過,我們現在需要單獨來看這個函式。

最重要的一句應該是第一行,也就是我們創造生物的那一行。我們仍然是在使用熟悉的CreateObject函式,但不同的是,我們傳入的參數是我們的函式的輸入值。不論GateIn函式輸入的是什麼參數,它都會被傳入CreateObject函式。這看起來有點難懂,但這正是力量的來源——我們可以多次呼叫GateIn函式,每次傳給它不同的參數,它就會創造不同的生物。

下一行,以「objectoPC =」開始的那一行看起來很長,但這僅僅是因為我們需要傳入的參數比較多。這一行的作用是:「找到距離OBJECT_SELF,最近的PC」,但是,我們還不知道OBJECT_SELF是誰,因為我們還不知道到底是誰觸發了這個函式。

最後一行,我們讓這個生物去攻擊PC

試試看吧!

我將要利用這個函式寫一個非常複雜的指令碼。我們當然可以做一些簡單的工作,但我希望舉一個例子讓大家明白到底應該在什麼時候使用自訂函式。

1.開啟toolset

2.首先,用精靈做一個中型物品

3.命名為「女巫的頭骨」

4.在屬性面板->基本屬性裡設定為劇情物品

5.編輯物品屬性

6.設定標籤為ALTSKULL

7.改變外形為iit_midmisc_021

8.ok,推出。

9.畫一個路標,設定標籤為ALTSUMWP

10.在附近放一個祭壇,編輯屬性。

11.設定標籤為SUMMALTR

12.設定為「可用的」,「擁有物品」

13.開啟物品欄,放置一個我們剛剛製作的頭骨進去

14.點選ok,然後進入祭壇的指令碼面板

15.OnDisturbed事件裡,加入如下指令碼:


   1.

//OnDisturbedScript: tm_summaltr_ds

//

//This script gates in one of 10 random creatures

//when a skull ALTSKULL is removed from the altar.

//The creature is gated in at the waypoint

//ALTSUMWP

//

//Written by Celowin

//Last Updated: 7/16/02


//This function summons the creature with blueprint ResRef

//given by sBluePrint at the location lGate, then it has the

//summoned creature attack the closest PC to the object that

//calls this function.

voidGateIn(string sBluePrint, location lGate)

{

  //Creates the creature

  objectoNewCreature = CreateObject(OBJECT_TYPE_CREATURE, sBluePrint, lGate);

  //Find the closest PC

  objectoPC = GetNearestCreature(CREATURE_TYPE_PLAYER_CHAR,PLAYER_CHAR_IS_PC, OBJECT_SELF);

  //Cause the creature to attack the PC

  AssignCommand(oNewCreature,ActionAttack(oPC));

}// end function GateIn


//Here is our main function:

voidmain()

{

  //If the skull is in the altar, we don't care, nothing will happen.

  //Note that OBJECT_SELF refers to the altar

  if(GetItemPossessor(GetObjectByTag("ALTSKULL")) !=OBJECT_SELF)

  {

   //Find the summon spot, via the waypoint.

  locationlSummonPoint = GetLocation(GetWaypointByTag("ALTSUMWP"));


  //Create the visual effect for the gate.

  effecteGate = EffectVisualEffect(VFX_FNF_SUMMON_GATE);

  ApplyEffectAtLocation(DURATION_TYPE_TEMPORARY,eGate, lSummonPoint, 3.0);


  //Randomize what creature is being summoned.

  intnCreature = d10();

  switch(nCreature)

    {

    case1:  // Summon a polar bear.

      DelayCommand(3.0,GateIn("nw_bearpolar",lSummonPoint));

      break;

    case2:  // Summon a cow.

      DelayCommand(3.0,GateIn("nw_cow",lSummonPoint));

      break;

    case3:  // Summon a bone golem.

      DelayCommand(3.0,GateIn("nw_golbone",lSummonPoint));

      break;

    case4:  // Summon an elder fire elemental.

      DelayCommand(3.0,GateIn("nw_fireelder",lSummonPoint));

      break;

    case5:  // Summon an ogre high mage

      DelayCommand(3.0,GateIn("nw_ogremageboss",lSummonPoint));

      break;

    case6:  // Summon a yuan ti mage

      DelayCommand(3.0,GateIn("nw_yuan_ti002",lSummonPoint));

      break;

    case7:  // Summon a spitting fire beetle

      DelayCommand(3.0,GateIn("nw_btlfire02",lSummonPoint));

      break;

    case8:  // Summon a Kreshar

      DelayCommand(3.0,GateIn("nw_kreshar",lSummonPoint));

      break;

    case9:  // Summon a werecat, human form

      DelayCommand(3.0,GateIn("nw_werecat001",lSummonPoint));

      break;

    case10:  // Summon a high lich

      DelayCommand(3.0,GateIn("nw_lichboss",lSummonPoint));

      break;

    }// end switch

  }// end if

}// end main


存檔結束測試。當你把頭骨從祭壇裡面拿走,就會從10種怪物種隨機召喚一種來攻擊PC,(嗯,乳牛是不會攻擊的……但其他的會)

雖然它很長,但事實上,它並沒有看起來那樣複雜。首先,我們檢查頭骨的擁有者是誰,一旦它的擁有者不是祭壇,我們的指令碼就會繼續執行。

我們在路標標記的位置做了一個「召喚惡魔」的視覺效果。這和我們上節課將的效果的使用方法是一樣的。然後,我們產生了一個110的隨機數,然後呼叫我們的自訂函式GateIn來召喚生物。

現在,我們有了例子了,讓我們來討論一下為何我們要使用自訂函式。事實上,這裡有兩個原因……一個是很好理解的,另一個就比較複雜了……

讓我們先來討論簡單的原因:每次我們呼叫GateIn函式,我們就少寫了3行指令碼。透過GateIn函式,我們一次性完成了創造生物,尋找標的,攻擊標的的任務。既然我們有10種不同的情況,我們節約了20行的指令碼,更不用提附註了。(而我我也不會讓那個噁心的尋找最近PC的函式反覆出現在我的指令碼中的……

第二個原因有一點難理解……但不管如何,我們必須在這裡使用自訂函式。我們希望延遲一段時間來召喚生物,這樣視覺效果就能夠更好的體現召喚的效果(否則,召喚儀式還沒有結束,我們的生物就出現了……)為此,我們需要使用DelayCommand。但是,我們使用DelayCommand的時候,裡面的延遲操作的函式的回傳值必須是void,而CreateObject的回傳值是object。如果你直接對CreateObject使用DelayCommand,編譯是不會透過的。但是,如果把CreateObject放到一個獨立的回傳值為空的函式裡,就ok了。


除錯

我想,透過這一堂課就讓你們自己寫函式還是比較困難的,但我會先讓你們自己改善一下前面那個指令碼。現在,如果頭骨在祭壇裡,一切ok,你可以隨便向祭壇裡添加或者移除物品,只要頭骨還在。一旦你移除頭骨,一個生物被召喚,這都還好。

但是,,如果你繼續玩弄那個祭壇呢?如果你拿著頭骨,然後隨便往祭壇裡放東西,都會有生物被召喚出來!這也許正是你希望看到的,但我估計不是。因此,做一些改進,只有在頭骨被移除的時候才會有生物被召喚。事實上你根本不需要改變那個自訂函式,你只需要在指令碼的條件上做點文章就可以了。


例子2:移除劇情物品

有一個變態的DM(哦,等等,那是我)曾經為PC們設計了一個超現實的夢境世界。可以說話的企鵝,啞巴歌唱家,詭異的謎題,等等。最後,PC們會醒來。用鋼筆和紙來實作去除所有在夢境中獲得的物品很簡單,但我們在nwn裡面如何做呢?

好吧,這是可以做到的,但我們得先未雨綢繆一下。首先,我們將我們在夢境裡獲得的所有物品都用比較相似的方式來命名標籤。所有劇情物品都是DREAMITM,所有武器都是DREAMWPN,所有盔甲都是DREAMARM,所有鑰匙都是DREAMKEY

然後,我們在夢境的場景的OnExit事件中,加入如下指令碼:


//On Exit Area script: tm_area002_ex

//

//This removes all items with tag DREAMITM, DREAMWPN,

//DREAMARM, or DREAMKEY from the exitng PC.

//

//Written by Celowin

//Last updated: 7/16/02


//This function strips all items with tag

//sTag from the object oStrippee

voidStripItems(object oStrippee, string sTag)

{

  //Initialize: Get the first inventory item

  objectoCurrentItem = GetFirstItemInInventory(oStrippee);


  //Loop through all items in inventory

  while(oCurrentItem != OBJECT_INVALID)

  {

   if(GetTag(oCurrentItem) == sTag)

     DestroyObject(oCurrentItem);// Destroy items with correct tag

   oCurrentItem= GetNextItemInInventory(oStrippee);

  }// end while

}// end StripItems function


voidmain()

{

  //First, get the one exiting

  objectoPC = GetExitingObject();

  if(GetIsPC(oPC))  // If it is a PC, strip the items

  {

    StripItems(oPC,"DREAMITM"); // Generic dream items

    StripItems(oPC,"DREAMWPN"); // Dream weapons

    StripItems(oPC,"DREAMARM"); // Dream armor

    StripItems(oPC,"DREAMKEY"); // Dream key

  }// end if

}// end main


(事實上,要實作這個目的,可以透過處理字串更好地實作……這個版本確實很不方便,因為它整整迴圈了PC的物品欄4次。但是,它是有效的,如果你設計的是單人mod,這個開銷還是可以接受的)

你可以測試一下這個指令碼,做一些標籤為DREAM***的物品放在地上,在區域的OnExit事件裡加入這個指令碼,然後試試看。當然,作為一個殘酷的DM,稍加改動,你就可以移除PC身上所有的物品。


總結

我在這裡只是簡單地對自訂函式做了一些講解。我們可以製作函式來讓他替我們做計算,也可以製作自己的函式庫(library),我們可以透過自訂函式大大減少我們編輯指令碼的體積,提高易讀性。

我(指Celowin,我收到的回復都是正面的,可惜沒有什麼問題)收到的回復大部分都是正面的。只有一個小問題大家經常抱怨:「我跟得上你的進度,你解釋的很清楚,因此我知道如何做和為何做,但我經常不知道要合適使用他們。當我想做一些其他東西的時候,我不知道到底我應該在什麼情況下是使用你教給我的那些工具。」

我也在試圖解釋到底合適使用什麼工具,但這也是要*經驗的。如果你已經看了很多指令碼,你自然會漸漸明白該在何時幹什麼。但這並不是說這很簡單……就算我知道了應該做什麼,我還是要埋頭電腦前數個小時來讓我那個複雜的指令碼工作起來。

因此,對於自訂函式,到底有沒有一個「通用的規則」來覺得合適使用呢?我得說,視情況而定。如果你感覺自己在不停地重複寫同樣的東西,你可能就需要來定義一個自訂函式令你擺脫繁瑣的勞動了。即使你的函式只有幾行,這也會令你的程式碼更簡潔易懂。


對未來的展望(Celowin對未來的展望)

我真的有些想不出該教些什麼了。我已經覆蓋了所有「基本」的知識,事實上,我已經開始涉及一些很高階的程式技巧了。當然,這裡或那裡總有一些細節我忽略了,但是,我覺得,到現在為止,你應該可以讀懂大部分指令碼了,當你隨便開啟一個指令碼,你應該會說:「嘿,我知道它的作用。」也許你還無法寫出自己希望的指令碼,但你應該已經可以看理解部分指令碼。

我當然應該涉及一下「include」宣告,並且教會各位如何寫自己的library(函式庫),但我認為這不會是一堂很難的課。

因此,我想,除非我靈光乍現,我想這個系列教學將在第十章左右結束-至少是這種形式的教學。






歡迎光臨 奇幻遊戲社群 (http://fgc.tw/tuxbb/) Powered by Discuz! X2