前言
原貼寫于饑荒游戲貼吧,為了使文章針對性更強(qiáng),將原文切割并精簡。此貼主要為編程0基礎(chǔ)的modder講解一些編程的基礎(chǔ)知識。至于說有關(guān)饑荒框架的介紹,則會放在另一篇文章里講解。
編程0基礎(chǔ)的人,要想學(xué)習(xí)制作MOD,難度是比較大的,因為缺乏一些基本的編程概念,只懂得復(fù)制別人的代碼或者在它們的基礎(chǔ)上稍加改變,遇到稍微復(fù)雜一點的代碼,就束手無策了。對于MOD崩潰或錯誤,也幾乎沒辦法自行處理。但我也不推薦先去學(xué)一門編程語言之后再來學(xué)習(xí)MOD代碼,這樣做花費的時間精力都過多,又缺乏足夠的正反饋,很容易半途而廢。事實上饑荒MOD里用到的基本編程知識都比較簡單,所使用的lua語言相比c之類的強(qiáng)類型語言,也已經(jīng)做了許多簡化。單純想要做MOD的話,只需要了解一些基本知識和概念就可以了。
以下內(nèi)容全部基于lua語言。
標(biāo)識符
一個名字
給常量,變量,函數(shù),類一個名字,這樣我們才能通過名字來使用它。一般使用英文字母、數(shù)字和下劃線的組合來命名。
推薦命名規(guī)則
- 常量:全大寫,單詞之間以下劃線隔開。
- 變量、函數(shù)、文件名:全小寫,單詞之間以下劃線隔開。
- 類:所有單詞首字母大寫。
這個只是個人推薦的命名規(guī)則,讀者可以根據(jù)自己的喜好決定命名規(guī)則。
變量
可以通過符號'='賦值改變的量。
典型代表:人物的饑餓值。這個值在游戲里幾乎每時每刻都在不停變化著,這樣一來,我們就可以根據(jù)不同的變化,設(shè)置不同的效果,比如沃爾夫?qū)煌酿囸I值會有不同的形態(tài),這個就是通過檢測饑餓值來實現(xiàn)的。
常量
程序運行時,不會被改變的量
實際上,lua語言里沒法自己定義常量。但是,對于某些量,我們不需要在游戲運行的過程中改變,又需要引用它。比如說長矛攻擊力,在游戲過程中不需要改變,但官方所做的所有的武器的攻擊力都是長矛攻擊力的某倍數(shù),這又需要引用它進(jìn)行計算。這時候,不妨就把長矛攻擊力看作是一個常量,用一個變量將其定義下來(SPEAR_DAMAGE,這個定義在tuning.lua里)
作用域
變量的生效區(qū)域。
在作用域以外的域內(nèi),如果你引用這個變量,又沒有域內(nèi)的同名變量,就會造成出錯。
lua中對一個變量的作用域只有兩個選項:local 和global。默認(rèn)不加local修飾的變量為global(全局)變量,加了local的為局部變量。全局變量的作用域為整個程序。局部變量的作用域則在所定義的域中。一個選擇控制結(jié)構(gòu)內(nèi),一個函數(shù)體內(nèi),或者一個文件內(nèi),都是一個域。
這個作用域的主要價值在于,使得系統(tǒng)免于混亂。比如說,在被攻擊的時候,需要計算所受到的傷害,為了方便進(jìn)行多次計算,我們把這個數(shù)值設(shè)置為一個變量。但是,面對的敵人的攻擊力會有變化,自身的防具減傷也會有變化,這時候我們就希望,計算結(jié)束,變量被引用到血量變化之后,這個變量能消失掉,不會影響我們下一次計算其他的傷害。這就是局部變量的重要作用。當(dāng)然,局部變量還有另一個好處就是讀取它的數(shù)據(jù),要比全局變量快一些,不過,提高M(jìn)OD性能不是本篇教程的重點,就不詳細(xì)展開了??偠灾肿兞?,不到必不得已的情況,應(yīng)當(dāng)盡可能少用。
定義和聲明##
定義,就是告訴系統(tǒng),我設(shè)置了的這個變量/常量是什么。
聲明,就是告訴系統(tǒng),我設(shè)置了一個變量/常量,你給我記好了。
定義和聲明是不一樣的,但常常會混在一起。如果你只是想寫寫mod的話,不了解他們的區(qū)別也沒關(guān)系,只認(rèn)為是定義就夠了。
引用##
引用就是告訴系統(tǒng),我要用這個變量/常量來做某某事。
比如說用于某個表達(dá)式的計算,系統(tǒng)就會幫你讀取儲存在其中的數(shù)據(jù)。
賦值##
賦值就是告訴系統(tǒng),往這個變量/常量里存入你給出的數(shù)據(jù)。
注意,此處的數(shù)據(jù),不僅僅指數(shù)字,可以是lua語言允許的任何類型,比如說一段文字(字符串),一個布爾值(真或假)等等。在某些語言比如C語言中,定義和賦值是可以分開的。但在饑荒MOD的腳本語言lua中,這兩者是連在一起的。對一個變量的第一次賦值,就是對它的定義了。
數(shù)據(jù)類型
對變量賦值,就是給它寫入數(shù)據(jù),那就涉及到了數(shù)據(jù)類型的問題。在初次賦值給一個變量時,所給予的值的數(shù)據(jù)類型,就是變量的數(shù)據(jù)類型。此后再給這個變量賦值,就必須賦予同一數(shù)據(jù)類型的值,如果值不同,就會導(dǎo)致系統(tǒng)崩潰(除了nil)。這里不展開細(xì)講,只針對lua語言,簡單地列出MOD中常用的幾種類型
- nil:表示無效值,可以給任何數(shù)據(jù)類型的變量賦這個值。實際效果相當(dāng)于刪除這個變量
- boolean:包含兩個值:false和true(假和真)
- string:字符串,用一對雙引號或單引號括起來
- function:函數(shù),這個會在下面講
- table:表,這個概念會在后面講
函數(shù)
這是編程里的一個非常重要的概念。
函數(shù)與變量的區(qū)別,可以做這樣的類比:一個變量,就好比是一個屬性,你可以給一個客體以某個屬性,讓它可以被描述,比如說,屬性:可以被燒毀。而函數(shù),則是一種操作方法,你讓一個客體擁有一個函數(shù),就是讓它有某種操作。比如說,操作方法:被燒毀的具體步驟和操作。
函數(shù)由函數(shù)名,參數(shù)表和函數(shù)體組成。函數(shù)也和變量一樣,能被引用,也有作用域。不過與變量不同的是,函數(shù)需要單獨定義,在不同的編程語言中,函數(shù)的定義格式不一樣,但都少不了上面所說的三個基本組成。在lua中,函數(shù)也可以看成是變量,可以被賦值。另外,函數(shù)可以有返回值,也就是把計算的結(jié)果返回,供另一個函數(shù)或者表達(dá)式使用。
Lua中,函數(shù)定義的基本格式如下為:
function 函數(shù)名(參數(shù)表)
函數(shù)體
end
如果希望函數(shù)的作用域是局部的,則在function 前面添加local。這樣,你將無法在其作用域之外調(diào)用該函數(shù)。
函數(shù)是怎樣工作的呢?首先,你需要明白,定義函數(shù)并不會讓函數(shù)工作。只有執(zhí)行了函數(shù)語句才會讓它工作。還是拿計算傷害來做例子。你定義了怎么計算傷害的函數(shù),參數(shù)為攻擊者的攻擊力和防御者的護(hù)甲。這個函數(shù)在定義之后,本身并不會立刻工作。只有你設(shè)定了一系列的流程,讓函數(shù)在出現(xiàn)攻擊狀態(tài)的時候觸發(fā),才能算是執(zhí)行了這個函數(shù)。函數(shù)執(zhí)行的時候,輸入了兩個參數(shù):攻擊者的攻擊力和防御者的護(hù)甲。在函數(shù)體中,經(jīng)過一系列的計算,得到了結(jié)果,利用return返回來,由變量接收或者加在各種表達(dá)式里使用。需要注意的是,即使是沒有參數(shù)的函數(shù),在執(zhí)行時,也必須寫成這樣的形式: 函數(shù)名(實際參數(shù)表)
代碼例子:
--定義了函數(shù)caldamage,但沒有執(zhí)行
function caldamage(attack,armor)
return attack-armor
end
local damage = caldamage(10,8) --這里執(zhí)行了函數(shù)caldamage,并把計算的結(jié)果返回,賦值給damage
函數(shù)的作用是什么呢?就是使得你的編程顯得更有邏輯,模塊化,還能減少代碼的使用量。定義好一個函數(shù)之后,就不再需要管這個函數(shù)里面詳細(xì)的執(zhí)行過程(也就是函數(shù)體寫了什么),我們只需要知道這個函數(shù)的名字,參數(shù)表和返回值,和這個函數(shù)有什么作用。因為在饑荒的MOD中,大量的函數(shù)是沒有返回值的(也就是返回值為nil),執(zhí)行這樣的函數(shù),目的在于使用它的功能。
函數(shù)是做饑荒MOD時最重要的東西。我們做MOD,主要的目標(biāo)就是修改或者向游戲添加函數(shù)。了解這些函數(shù)在什么時候會觸發(fā),需要哪些參數(shù),有什么功能,返回值是什么,是非常重要的。
我們是在原游戲的基礎(chǔ)上做MOD,也就是說,有很多已經(jīng)定義好了的函數(shù)可以供我們使用。打個比方,饑荒這個游戲,就好比一部車,函數(shù)就是這部車上面的零件,它讓這部車能夠擁有某些功能:啟動,剎車等等。我們現(xiàn)在覺得這部車不能滿足我們的需要了,那么,很顯然的,做適當(dāng)?shù)母难b,要比重新造一部車容易。做MOD,就好比是做一些改裝。既然是改裝,那你就有必要了解到,你所需要改裝的部分,需要哪些零件。有些核心零件是非要弄清楚不可的。
現(xiàn)在饑荒MOD本身的結(jié)構(gòu)是非常開放的,但官方?jīng)]有給出詳細(xì)的說明文檔,當(dāng)我們想要實現(xiàn)一項功能的時候,我們不知道官方有沒有給出來,怎么辦呢?我的建議是,思考一下游戲里的各種功能,以及他人已經(jīng)發(fā)布的MOD,有沒有和你的需求類似的,去參考一下相應(yīng)的代碼。易寧修改也是一個很好的參考,但易寧修改畢竟是直接修改游戲的核心文件,與MOD還是有一些區(qū)別的,所以要使用的話,前提是理解其含義。
表
表不是一門編程語言的必須概念,但這個概念是lua內(nèi)置的核心數(shù)據(jù)結(jié)構(gòu),在饑荒MOD里使用得非常頻繁。游戲的整個框架,也非常依賴于表。表的重要作用也和函數(shù)一樣,是為了讓你的編程顯得邏輯清晰。比方說,現(xiàn)在有4個個體,a,b,c,d,有多項屬性描述:health、sanity、hunger、damage、armor、attack_period、walkspeed、runspeed。這些屬性,對于4個個體來說,有的有,有的沒有,我們要怎么組織起來呢?用多張表連起來,就是一個好主意。首先,我們來給屬性分一下類,health、sanity、hunger是饑荒中的三大基本屬性,各自單獨成一類,damage、armor、attack_period是和戰(zhàn)斗有關(guān)的,分類為combat,至于walkspeed、runspeed則是和移動有關(guān)的,分類為locomotor。那么,我們就有多張表了:
一張總表:
個體屬性表
| 屬性 | a | b | c | d |
|---|---|---|---|---|
| health | a的血 | b的血 | c的血 | d的血 |
| sanity | a的精神 | b的精神 | c的精神 | d的精神 |
| hunger | a的饑餓 | b的饑餓 | c的饑餓 | d的饑餓 |
| combat | a的戰(zhàn)斗屬性 | b的戰(zhàn)斗屬性 | c的戰(zhàn)斗屬性 | d的戰(zhàn)斗屬性 |
| locomotor | a的移動屬性 | b的移動屬性 | c的移動屬性 | d的移動屬性 |
上表中的每一個元素,都是一張表,現(xiàn)在不妨取a的戰(zhàn)斗屬性表出來,是這樣的:
戰(zhàn)斗屬性表
| damage | armor | attack_period | |
|---|---|---|---|
| 數(shù)值 | 20 | 50 | 3 |
那么,我們想要引用a1的damage的時候,怎么辦呢?先在總表第一橫欄中找到a,然后在豎欄中找到combat,這樣我們就得到了提示:轉(zhuǎn)去找a1的戰(zhàn)斗屬性表。然后在戰(zhàn)斗屬性表中,我們在橫欄中找到了damage,這時候豎欄中只有一項,就不必再查找了。我們在查找過程中,尋找的a,combat,damage 就是所謂的索引。我們按先橫后豎的順序查找的,a為1級索引,combat為2級,damage為3級。按順序最后找到damage的具體的值(20),就是所謂的值。這個值不僅僅是數(shù)值,比如說在總表中找到的a的戰(zhàn)斗屬性表,也可以稱為值。再拓展一些,如果說戰(zhàn)斗屬性表中的豎表不只有一項,而是有兩項:max,min,此時我們想要查damage最大值,該如何呢?那就要增加一個四級索引max。當(dāng)你想要引用a的傷害最大值時,在編程里的調(diào)用語句,你就可以寫a.combat.damage.max
結(jié)合饑荒Mod編程,我們常常會看到類似這樣的一條語句
inst.components.sanity:DoDelta(-10)`
這句話的意思是當(dāng)前對象的精神減10。
具體是怎么操作的呢?首先,游戲里這么多個體,要在茫茫人海中找到你,必須要有個名字,這個名字就是inst,然后,inst下有很多屬性類,我們需要的精神值,歸類為components,也就是組件。組件這個概念,是饑荒為了編程上的邏輯清晰而創(chuàng)造出來的一個概念,會在介紹饑荒編程框架的文章里詳細(xì)說明。然后我們繼續(xù)在components表里找包含著精神值和操作精神值的函數(shù)的表,就是sanity。這時候你可以看到sanity后面是冒號:而不是之前的那些點號. 這是因為,我們是希望執(zhí)行這個函數(shù)。如果是想要引用這個函數(shù)做其他操作的話,還是要用點號. 的。學(xué)過C++的人都會了解類的概念,對這個肯定不陌生。沒學(xué)過的人呢,看我在下面關(guān)于類的解釋。
類和面向?qū)ο缶幊?/h2>
類這個概念,就是在面向?qū)ο缶幊痰乃枷肷习l(fā)展起來的。為什么要使用類呢?就是因為面向?qū)ο缶幊田@得邏輯結(jié)構(gòu)清晰,易于實現(xiàn)、互動和維護(hù)。那么,什么是一個類呢?在編程上,可以理解為一些變量和函數(shù)的集合。這個集合是封裝起來的,其中有一些變量和函數(shù)你可以訪問和引用,稱為公有變量和公有函數(shù),還有一些是你訪問不到,也無法使用的,就是私有變量和私有函數(shù)。需要注意的是,類本身只是一個邏輯結(jié)構(gòu),并不是實體。用現(xiàn)實的東西舉個例子,自行車就是一個類的概念,它有很多基本屬性:顏色,材質(zhì)等等,也有很多操作:騎、前進(jìn)、剎車等等。屬性就是變量,操作就是函數(shù)。顏色、材質(zhì),是你能夠看到的,就是公有變量,而內(nèi)部的轉(zhuǎn)盤的顏色你看不到,就是私有變量。而騎和剎車的操作,是你能夠決定的,就是公有函數(shù)。而前進(jìn)這個操作,你沒法直接進(jìn)行,你必須要反復(fù)踩踏板,才能讓自行車前進(jìn),所以前進(jìn)就是私有函數(shù)。在頭腦中想到自行車這個概念,就是類。而想到你的自行車,就是一個具體的實體。
結(jié)合到饑荒MOD里,sanity這就是一個類,這里面有很多屬性:當(dāng)前精神值,最大精神值等等,也有很多操作:精神增加/減少,設(shè)置當(dāng)前精神值,設(shè)置最大精神值等等。而具體到一個人物的sanity,那就是這個類的實體了。
實際上,在lua里,只有表,沒有類這個概念。但是饑荒的游戲制作者為了編程方便,還是用某種手段,在表的基礎(chǔ)上,類這個概念創(chuàng)造出來了。我們只需要認(rèn)識到,怎樣使用一個類就可以了。
引用類中的變量,操作方法就和引用表中元素一樣。如果想要調(diào)用函數(shù),則需要將點號.改成冒號:,并且在函數(shù)名后面添加(函數(shù)參數(shù)表)。
比如說人物的當(dāng)前精神值,人物的最大精神值等等,同時也有一些操作函數(shù),比如上面舉例的DoDelta。我之前說過了,在lua里,函數(shù)也可以看成變量。如果你想要引用這個函數(shù),比如說引用去給一個函數(shù)賦值,那么,上面的冒號:就要改成點號.,而且后面的"(-10)"也要去掉。如果你想要執(zhí)行這個函數(shù),那就要用冒號: 并且添加相應(yīng)的參數(shù)。