如果要說這一路走來還有什麼我們視作理所當然的遊戲元素還沒出現過,那開始遊戲時的主菜單肯定要算一個。
主菜單一般會視作是UI的一部分。我們常說的UI(User Interface,用戶界面)一般指的是GUI(Graphical User Interface,圖形用戶界面)。也就是說用一些視覺元素呈現的用戶界面。
與之相對的,像CLI(Conmmand-Line Interface,命令行界面)就沒有我們熟悉的這些按鈕、窗口等視覺元素。但是它也算界面。
命令行界面一般用文字展示
Interface由inter和face組成,在中文中除了叫“界面”,也稱“接口”。這個術語絕非計算機科學專用的,實際上計算機領域中可能接口的說法用得更多。但是無論如何,interface的往往都是作為溝通兩個系統的橋樑。
這個也叫interface
下面我們來試著製作一個簡單的開始菜單。
這次新建場景時,我們可以先別急著選擇Node2D了。這次我們選擇User Interface(用戶界面)選項。在新場景中,我們的場景根節點是一個Control。Control是Godot中所有UI節點的基類。Control一般翻譯為“控件”。Godot中內置了多種常見的控件來幫助我們構建UI,我們也可以自己構建自己的控件(也就是從Control派生新的控件出來)。我們把這個場景的根節點改成MainMenu或者其它你喜歡的名字,然後保存。
簡單設計一下,我們可能希望畫面上方是我們遊戲的標題或者logo。然後下方是堆疊起來幾個按鈕,它們分別負責常見的開始遊戲、繼續遊戲、設置、退出等工作。
Label
Label控件用於展示簡單的文字。其本意是標籤。我們先創建一個Label用於展示遊戲標題。添加Label節點後可以在檢視面板中看到Text屬性,它展示了一個文本框用於輸入文本內容。你想給遊戲取啥名字就輸入什麼。
Text屬性下方有一些常見的文本相關屬性。例如可能是最常用的水平/垂直對齊選項。我們調成居中就好。默認情況下,新加入的Label節點應該在左上方。和其它節點一樣,我們可以直接按下w或者選擇移動工具來調整它在畫面中的位置。移動過程中,可以啟用網格吸附工具來輔助定位。
你會發現可能並不是那麼好居中,但是現在姑且調整個大概位置。我們稍後會講到如何正確地調整UI元素的位置。
調整字體大小
作為標題的文字我們可能希望它大一點。不過不得不說Godot中要想調整字體大小並不那麼直接。Godot中的UI元素都會引用一種叫主題的資源,它們都有一種默認的資源。這種資源中定義了各種UI元素的默認外觀。
在檢視面板,Control部分的Theme下可以找到主題屬性。雖然默認值為空,但是實際上也引用了默認主題。你可以試著新建一個主題,實際上就是默認主題:
當然這裡我們暫時不介紹自定義主題,所以它的主題屬性留空就好。順帶一提Godot編輯器也是用Godot開發的,你也可以在編輯器設置中找到編輯器用的主題設置。
如果只是想調整某一個控件的字體,我們需要覆蓋主題原本的字體大小設置。我們需要在檢視面板找到Theme Override,然後勾選Font Size表示我們要無視主題的設置自行調整字體大小:
現在標題看起來應該大一點了。
Button
我可以這麼說,但凡是個用來開發GUI的東西,99%的可能性都有叫Button的東西(不過用來展示一般文本的東西在不同的框架裡其實有很多不同的名字和變體)。
我們在下面放上三個按鈕,分別是開始遊戲、繼續遊戲和退出——就是添加三個Button節點。Button節點也有Text屬性,就是按鈕上面的文字,把它們修改成想要的文字然後調整位置:
響應按鈕的信號
按下按鈕之後通常都會發生些什麼。我們先來實現最簡單的退出遊戲。毫無疑問,我們通過連接到按鈕的信號來相應相關事件。為主菜單場景添加一個腳本,然後選中退出按鈕(有好好給按鈕節點命名嗎),連接pressed信號,隨後編寫相關代碼。退出遊戲需要調用場景樹的quit方法。沒錯就這麼簡單:
現在可以把主菜單設置為主場景(這裡指的是Godot中的設為主場景選項,不是我們那個主場景),啟動遊戲,然後點擊退出按鈕我們就可以退出遊戲了。
GUI最頭疼的問題——畫面尺寸變化
現在主流的帶GUI的桌面操作系統都支持多窗口、多任務。我們還可以隨時調節窗口的大小。如何讓GUI中的元素根據環境變化來保持或者調整佈局是一個非常複雜的課題。即使你計劃開發一個強制全屏運行的遊戲,仍然可能需要考慮不同的屏幕比例。
但是,啟動遊戲並調整窗口大小,我們的主菜單並沒有發生離譜的走樣。這主要是由於我們在開始學習時調整了遊戲的拉伸模式以適應我們對小尺寸像素遊戲素材的需求。當時我寫道拉伸模式調整為canvas_item,但實際上文檔中更推薦使用了像素美術的遊戲選擇viewport模式。不過如果不進行其他操作的話,viewport拉伸模式下UI的默認字體看起來會很“像素”,所以至少在這一系列文章中我們姑且保持canvas_item模式。啟用了拉伸後,我們在開發過程中很多時候可以認為我們的畫面會保持我們設定的窗口大小。
但如果你想要進一步瞭解GUI方面的知識,請先禁用拉伸。在項目設置的Display/Window/Stretch/Mode這裡,調整為disabled。
另外不得不吐槽,Godot官方文檔這部分很久沒更新過。最新版本文檔的GUI部分還是以前的老內容。而且API參考中相關部分也缺少很多內容。
現在啟動遊戲,調整窗口,我們就會發現我們的界面就是一坨大便了。
控件如何決定自己在畫面中的位置呢?隨便選中一個控件節點,然後在檢視面板中找到Control部分下的Layout欄:
Layout是佈局的意思,可以說控件的位置和大小主要就是受這些屬性控制。
目前,Layout Mode(佈局模式)設置為Position(位置)。簡單來說在這種模式下,控件的位置就是你給它設置的位置——無論畫面怎麼變化,它們都不會移動。
選中任意一個控件,在選擇模式下(按下快捷鍵Q或者點擊左上方的鼠標圖標),控件除了有個調整大小的框,還可以看到一個綠色的由四個針尖模樣的東西組成的準星一樣的東西:
這個東西就是控件的錨點(錨點是常用說法,但是實際上並不是“一個點”)。在剛才看到的Layout Mode選項中,除了Position,就是Anchors模式。其實在Position模式下,錨點也在發揮作用,只不過作用沒那麼大。因為Position模式下錨點始終在左上角,也就是(0, 0)處,我們的位置始終以此為基準計算。那麼Anchors模式怎麼用呢?
在右上方的這裡可以找到Godot提供的錨點預設:
使用過Unity和Unreal的GUI系統、以及使用過各種GUI開發/設計工具的讀者一定不會對它感到陌生。菜單被兩條線分割為四個部分。左上部分是將控件錨定到四角和四邊中心。右下部分是讓控件延展開來鋪滿整個畫面。右上部分是讓控件在垂直方向上的上中下橫向展開。左下部分是在水平方向上三個位置展開。
如果選中標題Label節點,然後選擇第一行第二列處的預設,我們的標題就會錨定到畫面上方中心:
設置錨點預設之後,標題自己就跑過去了。此時在檢視面板中也可以看到,佈局模式也發生了變化:
此時還多出了一個錨點預設屬性,且已經設置為Center Top。
現在啟動遊戲,調整畫面,可以發現我們的標題始終錨定到了畫面上方的中間。
現在你依然可以直接調整控件的位置(Position)屬性,畫面發生變化、需要調整控件位置時,即使有錨點,Godot也會把它納入考慮範圍。
在編輯器中,錨定預設看似只有這麼幾種(雖然很多時候這些預設就已經夠用了),但實際上錨點取值是無限多的。Layout Mode為Anchors時,在Anchor Preset的菜單中選擇Custom我們就可以自行編輯錨點和偏移量。同時,如果前面已經設置了Anchor Preset,此時切換到Custom之後就能看到對應的錨點預設到底是怎麼定義的。
可見,受錨點控制的控件的位置和尺寸如何變化會由錨點、偏移、增長方向共同決定。我們一個個來看。
Anchor Points
把anchor翻譯成錨點的尷尬了。這個才是真正的錨“點”。
錨定點有四個,對應上下左右。在編輯器中可以看出,它們的取值範圍均為0到1的小數。
能在代碼裡操作的東西不一定能在編輯器中操作,但是能在編輯器中操作的東西一定可以在代碼中操作。這四個東西分別對應著Control節點的anchor_xx屬性:
對於這四個好朋友,文檔如是說:
將節點的某某邊錨定到父控件的起點、中點、終點。當節點移動或尺寸發生變化時,它會控制某某邊的偏移量如何更新。使用Anchor常量(枚舉)可以更便捷地設置它們。
某某指的是上下左右,後同。首先我們瞭解到錨點是相對的,相對於父控件的。在我們這個場景中,所有小東西的父級控件都是根節點那個Control(你可能已經改成MainMenu了)。新建場景作為根節點的控件,錨點預設默認為Full Rect(佔滿整個矩形區域),也就是說它始終會佔滿整個畫面。因此我們整個場景中的各個小控件的錨點在目前來說可以認為是針對整個畫面來說的。
錨點的取值是歸一化的(0到1),對應的某一邊的起點、中點、終點對應的就是0、0.5、1。文檔中提到的Anchor常量(枚舉),實際上只定義了兩個值:
起點就是0,終點就是1。如果你像我一樣是把標題節點的Anchor Preset直接從Top Center改成Custom的的話,你可以在錨點屬性中看到Top和Bottom均為0,即垂直方向上錨定到畫面上邊,Left和Right均為0.5,即水平方向上錨定到中間。造成的結果就是我們可以在2D視口中看到那四個尖尖在畫面參考邊界的上邊的中點位置。
這四個尖尖實際上是可以分別移動的,在2D視口中直接移動控件的綠色尖尖也等於是在編輯錨點:
這裡要勞煩你把它調整回去,我們換一個控件來展示它的作用。
無論怎樣,四個錨點會確定畫面中的一個矩形區域。由於它們是歸一化的值,所以畫面尺寸變化時這個區域的相對位置是不變的。接下來我們來介紹Anchors Preset下的Anchor Offset(錨點偏移)。
Anchor Offset
我們前面介紹了錨點可以控制控件的位置,但是沒有介紹如何影響控件的大小。接下來用開始按鈕做實驗。如果我們把它的錨點設置為預設的Center,它會始終在畫面中心,但是其大小不會變化。
假設,我們希望按鈕的寬度始終佔據畫面寬度的40%,但是垂直方向上依然維持在畫面中間。根據前面所學,在設置完錨點預設為Center之後,把預設改為Custom,我們可以把錨點按左、上、右、下依次設置為0.3、0.5、0.7、0.5。現在開始按鈕變成了這樣:
看起來有點怪。調整錨點的時候怎麼大小也自動變了——但是不是我們希望的那樣。現在先試著把Anchor Offsets全部改成0試一下。怎樣?是不是按鈕的高度不變,而寬度按照預期匹配上了錨點指定的畫面佔比?啟動遊戲調整窗口大小,可以看到,這個按鈕會按照期望始終保持正確的寬度和垂直方向上的位置:
來看錨點偏移在文檔中是如何描述的:
在某某邊錨點的基礎上,節點的某某邊和其父級控件(某某邊)的距離
也就是說控件在錨定之後,這個偏移量會以錨點為基礎再加上去。如果偏移不為零,我們就可以期待它會貼到錨點所決定的邊界上。如果我們把左邊的偏移設為10,右邊的偏移設為-10(控件右邊在錨點右邊的左邊的話這個偏移就是負值),那麼就可以達到一種邊距的效果:
由於是應用到了錨點的基礎之上,所以毫無疑問這個偏移也是動態的。
但此時你還有一個疑問。你說四個錨點決定一個矩形區域,但是如果兩條對邊的值如果相同的話,那麼這個矩形的寬或高不就為0——上面按鈕的錨點設置也是如此,那為什麼偏移量全部為0時這個按鈕還是有高度,並且它的高度自動放到了錨點區域中心(垂直方向上的0.5處)對齊呢?
Grow Direction
答案是Anchor Preset中的第三個選項Grow Direction。增長方向的描述是說如果控件的當前尺寸小於它的最小尺寸時,應當讓它沿哪個方向展開。
在GDScript層面控件的默認最小尺寸是實現細節沒有暴露出來,我們只能通過custom_minimum_size來自定義最小尺寸。如果不自定義最小尺寸,其最小尺寸還是交給控件自己決定。例如按鈕的最小尺寸會根據上面的文字大小來決定。
我們設置錨點上下邊的值為同一個值,且把偏移全部設為0時,上下錨點確定的寬度自然為0——但是我們按鈕的最小高度要更大,所以此時需要展開來。Grow Direction會決定朝哪個方向展開。如果你前面和我一樣是先將按鈕的錨點預設設為Center然後直接變成Custom的話,這兩個方向應該都被調整為了Both。如果你的情況和我不一樣,可以看一下方向的值。
Both就是朝兩個方向展開。因此這個按鈕就會從畫面中心朝上下展開,看起來就是按鈕垂直方向的中點和畫面垂直方向的中點對齊了。
設置為Bottom的話,它就會向下展開。此時按鈕的上邊就會和錨點區域對齊。其它方向道理類似。
現在動手實踐一下。假設我們改變了UI設計,現在希望把按鈕放到畫面左側,但是不完全靠到畫面左邊,畫面變大時,按鈕改變寬度,向右延展(畢竟向左可能會超出屏幕左邊)。
那麼就可以這樣設置:
錨定區域的右邊位於在橫向的0.3處,高度始終在縱向中點。左偏移為20,保證按鈕左側離畫面左邊有一定距離。水平增長方向設置為右,這樣寬度改變時按鈕會向右變變寬而左側不變。
錨點預設菜單中還有一個按鈕叫設為當前比例:
點一下之後,錨點區域就變成了控件當前所確定的矩形區域:
有了前面的知識,這個按鈕做了什麼不言而喻。
這邊還有個圖標就是個錨的按鈕,啟用它再移動控件的話,就會帶著錨點一起移動。
呼!相信這一段下來你對Godot的UI系統有了一個不錯的瞭解。有了這些基礎知識,使用各種控件、調整它們的佈局也會更加得心應手。
當然,肯定還是直接設置畫面的拉伸模式,然後直接用anchor方便!我們還是調回來繼續吧!(至少把拉伸模式調回來)。
Container——控制一系列控件的佈局
前面放的三個按鈕我們都是隨便放的,但是我們希望它們和開始按鈕有同樣的大小,只是在垂直方向上的位置不同,但是按鈕之間又有相同的間距。當然,你可以手動用剛才學到的知識挨個設置錨點。但是我們可以用更方便的控件來管理它們的佈局。
Control的繼承樹上有一系列叫做Container(容器)的控件,它們主要的作用就是調整其子節點的佈局。
為了實現垂直方向上等間距放置多個按鈕,我們可以使用VBoxContainer(V就是vertical的意思)。我們添加一個VBoxContainer,然後手動調整它的錨點:
縱向增長方向設為Bottom可以使得空間不夠時讓它向下增長。這裡你不必和我一樣,你也可以把Bottom設為和Top一樣,這樣不管怎樣都讓Container中的內容向下溢出好了。
然後把按鈕交給這個Container。把一個控件放到一個Container中之後,它的錨點就無法調整了,它會受Container控制。
太棒了,按鈕全部規規矩矩地放到了容器的區域內,並且有一定的間距:
VBoxContainer有一個叫Alignment(對齊)的屬性:
說白了,它的意思其實是“從哪裡開始排布子節點”。Begin就是從最上面開始放,Center和End不言而喻。類似的容器HBoxContainer是水平方向排列子節點,類似地,它的對齊方式是左、中、右。
Godot內置了很多不同的容器,你可以自行探索。
實現按鈕功能
最後來實現按鈕的功能。退出按鈕已經完成了。我們要來實現開始遊戲功能。我們希望按下開始遊戲按鈕後,進入我們之前做的主場景。
我們首先定義一個export出來的、類型為PackedScene的變量,便於我們在編輯器中設置要加載的主場景。前面的文章中介紹過,這裡不單獨重複。
具體切換場景的代碼要怎麼實現呢。Godot的場景樹系統很簡單直白,我們加入主場景,然後把菜單刪掉就行:
Godot甚至還有個專門為這種情況使用的方法,只需要一句話:
有啥區別呢?就我們這裡的需要來說,肯定是越簡單越好,直接調用change_scene。使用add_child是常規的場景樹操作方法,添加新場景之後,舊場景依舊存在,你要銷燬它或者隱藏它都是你自己的事情。如果你的這個UI會經常用到,那麼還是不要銷燬,在必要的時候讓它再次顯示就好。但是主菜單這種東西,一場遊戲中顯示多於兩三次的情況可能都不會太常見,所以我們直接調用change_scene,它會自動刪除舊場景。
change_scene的另外一個版本是加載場景文件而非PackedScene變量。它的參數是場景資源文件的路徑(res開頭)。調用時Godot編輯器會自動提示文件路徑:
現在我們有了一個主菜單,簡直越來越像一個遊戲了!