Godot入門到棄坑:腳踏實地,騰空而起


3樓貓 發佈時間:2024-03-02 20:32:30 作者:cameLcAsE Language

在這篇文章中我們繼續完善之前還沒做完的角色。在上一篇文章中我們已經實現了水平移動和幀動畫的基礎設施。

碰撞

碰撞(collision)是遊戲中頗為常見的事件之一。即使遊戲玩法和物理沒有直接關係,很多時候也需要感知碰撞的發生。以平臺跳躍遊戲為例,我們很可能需要知道玩家角色和牆壁、地板、敵人發生了碰撞。
現在我們的場景中除了玩家角色什麼也沒有。我們先來構造一個會擋住玩家去路的柱子。知道怎麼和牆壁碰撞了,構造一個供玩家站立的平臺也就簡單了。
新建一個場景比如叫它Platform。簡單給它一個Sprite2D。在沒有合適的素材時,其實可以新建一個PlaceholderTexture佔位。默認是粉色的。當然你也可以在那個素材包中找一個喜歡的先放裡面。
我們可以點擊Texture中的PlaceholderTexture2D來編輯它的屬性,給它一個大一點的尺寸,比如30x50。要注意到此時Sprites2D的原點在這個紋理的中心位置。為了方便對齊,我們直接調整Offset(偏移)屬性,將y調整為-25。這樣原點就到了底部。由於我們調整的是它的偏移,因此Sprite2D在場景中的位置依然保持為0。
然後我們把它放到主場景中,位於角色的右邊。由於目前我們沒有一個真正的地面,所以看一下玩家角色的位置然後直接設置平臺的位置。
當然現在角色走過去會直接穿過這個柱子。要實現碰撞,你完全可以自己用數學方法去求解。但是這個需求太常見,遊戲引擎一般都會內置的。在Godot要讓節點“感知”碰撞,“至少”需要給它一個Area2D節點。按文檔所說,它定義了一個2D空間,可以用來檢查和其他CollisionObject2D(2D碰撞物體)發生的碰撞。Area2D本身也是派生自CollisionObject2D,也就是說我們只要有兩個有Area2D的場景,就可以讓它們進行互動了。
給柱子的場景添加Area2D之後,我們會看到一個警告標識。它說這個Area2D沒有形狀,也就沒有任何碰撞會被檢測到。Area2D本身並沒有一個具體的形狀,“形狀”需要一個專門的定義來表示。這個節點就叫CollisionShape2D(2D碰撞形狀)。
選中Area2D節點,然後給它添加一個CollisionShape2D作為子節點。添加之後,這個CollisionShape2D又有一個感嘆號,說還是沒有形狀。我們需要給它的Shape(形狀)屬性給值才行。
這裡有多種選擇。它們代表了不同形狀的Shape資源。當然你會說我們的狐狸是“狐狸形狀”的該選什麼。實際上定義碰撞形狀的節點除了CollisionShape還有個CollisionPolygon(碰撞多邊形),這個節點允許你通過編輯器自行創建碰撞形狀。但實際上碰撞檢測這個東西往往不需要完全精確的形狀——因為很多時候沒必要,精確的多邊形也會增加消耗。我們只需要一個大概的形狀把我們的Sprite圍住就可以了。因此選擇矩形(RectangleShape2D)。
這裡順便提一下,可以看到很多節點的某些屬性的輸入處都是這樣的,比如上面的CollisionShape2D和Sprite2D:
這樣的樣式表示這個屬性是“資源”類型的。資源是可以複用的。我們前面都是按右邊的箭頭然後選擇一種類型新建。如果需要複用,我們可以直接在文件系統中創建各種資源,然後直接拖到這種輸入框處。
比如在文件系統任意位置右鍵,Created New(新建)選項中除了文件夾、場景、腳本之外還有個Resource(資源)選項:
點擊它就可以看到一個對話框。這裡顯示的是所有繼承了Resource類的資源類型,比如我們可以查找到RectangleShape2D。這樣創建了資源之後它就會出現在文件系統中,可以到處拷貝到處放,也可以直接給需要資源的節點屬性。資源在加載後是大家共享的,多個需要同一資源的節點不會出現多份拷貝,所以這就是為什麼說它可以複用。
創建完形狀之後,場景中就會出現一個框,我們用框上的點調整大小讓它覆蓋住這個佔位用的紋理。也可以點擊Shape槽中的值直接調整大小。
現在玩家還是會穿過場景,因為我們玩家的角色還沒有碰撞形狀。所以按照同樣的辦法給玩家的場景添加碰撞。

感知碰撞

現在我們準備好了感知碰撞的基礎設施,但是我們還是不知道“何時”發生了碰撞。這種情況就是在暗示我們要檢查一下有沒有相應的的信號可以接收。
答案是肯定的。Area2D中有一對信號叫area_entered和area_exited。顧名思義是在其他Area2D進入和離開自己找個Area2D時發出。在玩家的腳本中連接entered信號(如果忘記了怎麼操作請看上一篇文章)。在響應這個信號的方法中隨便寫個print,運行遊戲,玩家進入這個平臺時就會在輸出面板中看到相應內容。
當然目前我們檢測到了碰撞發生但是玩家並沒有被柱子擋住。自然,我們可以編寫更多的代碼來讓玩家在發生碰撞時停止運動,比如在area_entered裡直接讓角色停下。
但實際上效果並不好。很有你可能你的角色會在進入柱子之後(想想entered的意思)才停下。

專業的角色節點

碰撞相關功能會和引擎的物理系統進行交互。但是我們之前移動的實現實際上是直接修改節點的位置而沒有和物理系統發生交互。因此在處理碰撞時顯得有點彆扭。
玩家角色在物理系統中有一定特殊性。根據遊戲類型和設計的不同,它可能需要受重力控制,可能需要和其他物體發生碰撞,但是很有可能不需要物理系統來控制它的移動——這往往由玩家控制,並且受到各種遊戲機制的影響。
在Godot中有一個叫做CharacterBody2D的節點(3D場景中有對應的3D版節點),圖標是一個貌似在跑的小人。它就是為了方便我們實現角色相關的功能,它能夠檢測碰撞,但是其運動可以不受物理系統影響。
這次我們可能真的需要對代表玩家的場景進行大改。你可以直接修改,也可以把原來的場景複製一份並改名。如果你選擇留一份原來的版本,那麼腳本也要複製並改名,然後把這個用作備份的場景的腳本改成複製後的腳本。
我們可以直接把根節點改成CharacterBody2D,因為我們的腳本在根節點上,很多代碼也是從根節點的角度來說的。
CharacterBody2D和Area2D一樣是CollisionObject的子類,它也需要一個CollisionShape。修改完根節點類型之後它也會提示需要一個形狀。這裡我們不再需要Area2D,所以直接把它的CollisionShape移到外面,讓它成為CharacterBody2D的直接子節點即可:
此外,雖然目前直接運行遊戲也不會報錯,但是首先要修改一個地方。之前的腳本根節點為Node2D,所以腳本最上方默認生成的是extends Node2D。由於CharacterBody2D毫無疑問也是Node2D的子類,所以代碼沒有問題。不過為了使用CharacterBody2D的各種功能,我們還是需要把它改成extends CharacterBody2D。
此外由於不再使用Area2D,相應的和信號連接的方法也要刪掉。
當然現在還是沒有和牆壁發生碰撞。

Area和Body的區別

Area和Body系列節點都可以檢測碰撞。但是Area是一個抽象的空間,而Body往往是一個有具體形狀的物體(儘管它的具體形狀也是由CollisionShape決定)。Body在碰到Body時不會相互重疊,如果設置得當運動的一方碰到靜止的一方會停止移動。Area的用法可以想象一下地板上機關,玩家可以踩到地板上(而不是碰到地板邊緣就停下來了),在代碼中可以連接到Area的信號來感知有玩家站了上來。
目前主場景中的這個柱子一樣的東西顯然是有實體的,我們應該考慮把它做成一個Body來和玩家角色碰撞。由於目前這個柱子不會動,所以應該考慮用StaticBody2D來表示它。StaticBody2D顧名思義,是不會受外力影響而移動的靜止物體。
可以刪去之前給它的Area2D重新添加StaticBody2D,也可以直接把Area2D的類型改成StaticBody2D。當然這樣做的話最好給這個節點改個名字。StaticBody和Area都需要一個CollisionShape,所以直接改類型就不需要重新添加CollisionShape了。

移動CharacterBody2D

現在我們還需要修改移動代碼讓物理系統明白髮生了什麼。使用了CharacterBody2D之後就不建議再繼續使用修改position來實現移動的方法了。
CharacterBody2D主要有兩個用來移動的方法,一個叫move_and_slide,一個叫move_and_collide(這個方法實際上來自它的基類PhysicsBody2D)。從名字上我們可以瞭解到,前者在碰到障礙物時(根據運動速度的情況)會滑走,而後者發生碰撞後會停止移動。
說白了,move_and_slide更加簡單粗暴。它依靠的是CharacterBody2D的velocity屬性來移動。如果發生了碰撞返回值就為true。發生碰撞和會根據與之碰撞的物體的情況來修改自身的速度。
而move_and_collide發生碰撞之後會停下,除非你讓它再次運動起來。它需要你給他一個motion(運動)參數來移動物體。它的返回值是一個更復雜的表示碰撞信息的KinematicsCollision2D對象。你可以根據碰撞信息和設計需要讓它以你想要的速度繼續移動,也可以就這樣讓它停下。
就目前來說,我們可以簡單地用move_and_slide。

稍微瞭解一下Vector2

我們接下來會不斷地使用Vectors2類型的值——實際上我們在前面的很多地方都已經用到了Vector2類型。position就是Vector2類型,它有x和y兩個最主要的屬性。前面提到的velocity屬性也是Vector2
Vector是向量的意思。2指的是二維向量,也就是說它有兩個分量(component)。向量在一些領域中表示的是一種既有方向也有長度的量。向量和矩陣(matrix)是線性代數中的兩個核心概念。
但是在實際的開發工作中,向量很多時候並不單單隻表示“有方向的量”——儘管它們可能有幾何含義,很多時候會被用來表示“各種有幾個分量的量”。例如position是位置,它本身表示的是一個點,本身沒有方向含義,但是仍然用Vector2表示。因為剛好我們常用的座標系是笛卡爾座標系,二維座標系中的向量和點都可以用二元組(兩個數構成的序列)表示,所以用Vector2來表示一個點也是可以的。此外Godot中的很多屬性比如Size(大小)都和”方向“沒有直接關係,但都用Vector2表示。在很多時候顏色也會用三/四維向量表示。所以當你看到向量類型用來表示一些看似和方向無關的數據時也不用驚訝。

“速度”(speed)和“速度”(velocity)

關於前面提到CharacterBody2D的velocity屬性,在日常會話中,speed和velocity我們都說“速度”。不過這兩個概念雖然相互關聯但是也有區別。回憶一下中學上物理課時老師說的“速度”和“速率”之間的區別。speed就是速率,它是一個標量(一個數字類型的值);velocity是速度,是一個向量(Vector2)。
此外瞭解一下單位向量的含義。單位向量表示的是”長度為1的向量“。注意單位向量不是“各分量均為1”的向量,如(1, 1)。回憶一下,用二元組(座標)表示的向量的長度實際上是各分量平方之和再開方。畫圖時向量一般是一條線加上箭頭來表示。向量(1, 1)的長度就是點(1, 1)和原點之間的距離。也就是說是求點(1, 1)向座標軸作垂線,這條垂線和座標軸的交點、點(1, 1)、原點構成一個直角三角形,其斜邊剛好是向量(1, 1)(對應的一條線段),這條線段的長度就是向量(1, 1)的長度。怎麼求它不言自明,就是用勾股定理求直角三角形的斜邊。
有些時候,我們會把單位向量認為是”只表示方向的向量“。向量可以和標量(單個數字)做乘法。在計算上只向量乘標量的結果也是向量,它的分量就是把之前的向量的各分量和這個標量乘起來。得到的新向量方向不變,長度變為這個標量倍。
在GDScript中我們直接用*運算符就可以把Vector2和數字相乘了:

移動

現在我們修改handle_input中的代碼。此前我們直接修改position,現在我們修改velocity的值。Godot的二維座標系原點在左上角,右側和下側為正方向。因此向右移動時velocity設為(1, 0),向左為其反方向(-1, 0):
Vector2是一個內置的類。我們直接用Vector2函數而不是Vector.new這種形式來構造向量(畢竟Vector2沒暴露new這個方法)。兩個參數的版本就是二維向量的兩個分量,如果不填參數就會構造一個0向量——和常量Vector2.ZERO等價。不過提醒一句由於內置類型的特殊性,向量類型的值在變量之間相互賦值是會發生向量本身的拷貝,所以不用擔心修改向量影響了其他引用同一個向量的變量。
此外如果怕上下左右分不清,可以用Vector2類型幾個常量。例如LEFT就等於(-1, 0),RIGHT、UP、DOWN同理。後面我可能會更多地用這些常量。
當然這裡的修改很簡單,就是把修改position的地方改成了修改velocity。為了表達”停下“,就把速度設為零向量。由於不再需要在這裡根據速度修改位置,所以delta參數也不需要了。
單單是這樣我們還無法移動。我們還需要調用move_and_slide方法才行。不過要注意在哪裡調用這個方法。由於我們現在要和物理系統互動,因此涉及物理的代碼應當放到另一個特殊方法_physics_process中來。從名字中可以看出它和_process有一定的相似之處。_process的調用頻率和幀率有關,而physics和物理系統的更新速度有關。物理系統會“儘量”以一個恆定頻率進行更新,這個頻率來自於項目設置中的General/Physics/Common/Physics Ticks Per Second。默認為60。你可以在_physics_process中直接print一下delta。它的值很可能在絕大部分時候都在0.01666666666667左右。
這樣一來核心代碼大概就變成了這樣:
現在水平移動就算完成了。也能夠和障礙物發生碰撞了!

重力

現在還不能跳躍。跳躍和水平移動有一個最大的不同,那就是我們需要在跳起來之後受重力影響又掉回來。
現實生活中人跳起來為什麼會掉下來?那是因為重力、地球上重力加速度的存在。我們跳起來之後,和我們速度方向大致相反的重力會把我們的速度拉回來。
遊戲裡呢,道理很簡單,我們只需要”時刻根據重力加速度修改CharacterBody的速度即可“。
我們定義一個重力加速g。當然這個g你可以根據需要設置成任何你需要的值,比如你想做一個在低重力環境下的遊戲。
Godot的文檔中提到可以選擇獲取設置中的重力加速度值,以便於和RigidBody2D的重力加速度保持一致。RigidBody(剛體)是另一種body,它和CharacterBody不一樣,一般情況下它們的運動完全受物理系統控制。像這樣獲得設置中的重力加速度:
這個路徑形式的字符串就是設置中的項目路徑,你可以試著找到對應的設置項目。這個默認值是980。
現在如何讓重力影響角色的速度呢?我們實際上只需要在_physics_process中修改速度的y值就好了,因為重力總是影響垂直方向上的速度:
當然這裡有兩個問題值得思考一下。首先,重力加速度也應該是向量,但是我們用的是一個float來表示。當然這就是為了方便沒啥說的,因為重力加速度我們默認它總是方向向下的。用它去修改速度也就只考慮y分量就好了。
另一個問題是,為什麼這裡要乘delta?為什麼計算速度的時候沒有乘delta?為什麼之前修改position的時候要乘delta?一方面來說,`_physics_process`也不一定100%以恆定頻率調用,乘上它可以做到和調用頻率無關。另一方面乘上delta更符合加速度的定義。
我們以一個獨特的角度來思考這個問題。position表示位置,假設它的單位是米(m)。速度的單位應該是米每秒(m/s)。delta就是一小段經過的時間,它的單位就是秒(s)。因此計算position時,速度(m/s)要乘上時間(s)才能得到m。加速度的單位是m/s²,乘上時間s後,單位是m/s,是一個速度,也就是delta這一小段時間內速度的變化量,這樣我們才能把它加到速度上。
當然這都是理論,實際上你修改速度時不乘delta可能也沒太大問題。
不過啟動遊戲你會發現還是沒有下落。這是因為handle_input
中還有一處代碼需要修改。我們之前在不進行操作時默認將速度設為了ZERO。不過這樣一來不操作時重力加速度的影響就被抹掉了。因此我們這裡只把水平方向上的速度設為0就好了。
啟動遊戲,角色應該會直接往下掉。

地板

接下來建一個代表地板的平臺。目前來說我們也不用重複新建一個和Platform差不多的場景了。我們先複製一個Platform在主場景中,調整它的位置。但是它的大小不太像地板。我們可以修改一下。當然由於它是Platform場景的實例,所以我們看不到它內部的各個節點也沒法編輯。這裡需要用到一個菜單項叫Make Local(令其為本地<節點樹>)。它可以直接展開場景,這樣我們就可以編輯它而不影響原本的場景本身了。
但是要注意的是目前沒法在檢視面板中直接修改佔位紋理和CollisionShape的尺寸。如果試圖在場景中直接調整這個複製出來的東西的CollisionShape,你會發現原本的柱子的CollisionShape也跟著變了!你不是說不會影響原本的場景嗎?
沒錯,但是我提到過CollisionShape和Texture一樣都是資源(Resource)它們是可以被共享的。此時你點擊複製出來的CollisionShape的Shape屬性中的值,在下面的Resource欄中的Path,你會看到它的路徑指向了Platform場景。
如果你也嘗試了Make Local,可以直接新建這些資源再進行調整。如果想避免這種問題(或者說偷下懶),複製了Platform後可以直接修改Scale來調整尺寸。由於目前這個用的是佔位的紋理,所以隨便縮放都無所謂。修改Scale屬性時,你可能會發現x和y會一起修改。此時只要點一下鎖鏈圖標(鎖定/解鎖分量比例)就可以解除。
當然我只是演示一下Make Local的作用,最終我還是偷懶直接複製然後縮放了。
地板放在角色下方有一點距離的地方,這樣在啟動遊戲時就可以看到它落到了地板上。

跳躍

增加操作映射並在處理輸入的方法中增加代碼。同時我們需要在State中增加一個JUMP狀態表明正在跳躍過程中。這裡不贅述。動畫暫時不管。
跳躍和水平移動類似,我們在要跳躍時給它一個向上的速度即可。不過跳躍的初速度大小應該更大,所以我們單獨export一個jump_speed變量。接下來就是在handle_input中修改velocity即可:
現在你會發現其實如果不斷按跳躍它會連著跳。我們需要限制這種行為。比如如果按下跳的時候發現正在跳那就不跳了:
但是這樣還是會跳!為什麼呢,這是因為跳了之後由於沒有操作,狀態又回到了IDLE。所以再次按下跳還是會跳。因此,我們需要考慮給IDLE狀態有一個更嚴格的定義。比如我們要在角色沒有在空中且沒有操作的時候才能被判定為IDLE狀態。要判斷一個CharacterBody2D是否在空中,可以用它的一個表示相反意思的方法is_on_floor(是否在地板上):
現在跳起來再按跳就不會繼續跳了!當然還是那句話,你的遊戲你可以給它加上不一樣的跳。
不過還有個問題,我們現在在移動中按下跳是不會跳的。因為我們處理了水平方向的輸入之後就直接return了。好吧,你說我把前兩個return刪掉——好吧,這下直接動不了了。因為這樣只要不跳躍就會直接進入是否在地板上的判斷,然後水平速度又被設為0。
好吧你說加上一個條件,那就是在地板上且水平速度為0時才IDLE:
現在一開始移動就停不下來了!因為我們剛鬆開按鍵時速度還沒為0,這個時候這個判斷的條件為假,因此永遠不可能停下來。
我們換個思路。現在的代碼是把可能會按下的按鍵挨個走一遍再覺得我們可能沒按鍵要停下來。何不一開始就判斷一下是不是有哪個鍵按了呢?如果沒按我們就停下來,否則再進一步處理。
Input中確實有個is_anything_pressed方法。如果啥都沒按(這個方法的字面意思是“是不是按了啥”),那就試著停下來:
現在可以跳,可以走,跳了還可以左右移動。但是!還有個bug,如果跳了之後按下方向鍵,再按跳,它還是會一直跳!
依然分析原因。跳了之後,按下左右,狀態為RUN。由於return被刪掉,還會繼續作JUMP的判斷,如果按下了左右並且同時按下了跳,那麼此時由於狀態不是JUMP,看起來還是會一直跳!這裡就不再用JUMP判斷了,依然用is_on_floor來判斷是否在空中。問題基本上就解決了!
當然這個方法有個問題就是,它失去了在下落過程中跳一次的機會。解決也很簡單,我們用速度的y分量來判斷當前是在起跳還是下落即可!
看看起來條件有點複雜。稍微分析一下。首先確定角色在空中,同時還要滿足右側的條件。要麼狀態是JUMP(按鍵起跳了),要麼垂直速度向上(我們沒準備二段跳)。此時我們就不能接著跳了。
現在,自由落體可以跳一次,但是我們又有bug了!按住方向鍵時又可以一直跳了!
這個問題好說,我們在水平移動的部分如果發現在空中我們就不設置狀態為RUN就好啦。這樣我們在後面配置動畫時也是必要的!

向量加法

最後補充一點數學知識。在前面的代碼中我們實際上簡化了很多運算。我們直接在velocity的分量上加減。實際上這些運算本質應該是向量之間的加減,只不過我們剛好需要控制水平和垂直方向上的速度,這些改變量的兩個分量上總是有一個為0。
兩個向量可以做加法。在計算上就是把各分量對應相加,得到一個新的向量。在幾何上,它滿足三角形定律。形象得說相加時,把一個向量整個移動起來,讓尾巴在另一個向量的尖尖上。然後讓一個向量的尾巴指向另一個向量的尖尖,就得到了兩個向量的和,比如(1, 1) + (0, 2):
在GDScript中我們有:
向量減法也存在,但是減法無非就是加上一個反向的向量。負號同樣可以應用到向量上,結果是一個各分量均為原來相反數的的向量,在幾何上就是一個方向和原來相反的向量,比如(1, 1) - (0, 2)即(1, 1) + (0, -2),黑色向量即為結果:
同樣,也可以視作是“減數(向量)的尖尖往被減數(向量)尖尖畫的向量”。
可以看到,同一個向量在笛卡爾座標系中畫出來實際上有無數種畫法。但是它們都是表示同一個向量,它們在幾何上都是平行的,長度也一樣——那就是同一個向量。
兩個點座標相減就可以得到一個從減數指向被減數的向量,試試看!
GDScript中不言自明,用減號就可以了。

動畫

最後完成跳躍的動畫。跳躍的動畫實際上有兩個不同的情況,一個是起跳,一個是下落。這個很簡單,依靠速度的y分量正負即可區分。給玩家的Sprite加上jump和fall兩個動畫,這兩個動畫要用到的素材在player/jump裡面,實際上也就是兩張圖片,拖進去即可。
play_animation方法的match最後加入:
我們還有最後一個bug。現在我們主動起跳和下落時動畫可以按照預期播放。但是,如果我們從平臺上落下去,或者從空中落到平臺上,下落的動畫是不會播放的。
當然解決方案也很簡單,只要在任何狀態下我們發現不在地面上,我們就根據垂直方向速度方向來播放起跳和下落的方向。現在我們就不把控制起跳和落下動畫的代碼放到match中了,我們把它挪到前面,這樣就不會受到其他狀態的影響。注意如果確實在空中那麼我們播放動畫之後就直接返回,因為我們目前在空中無論怎麼操作都只有可能播放這兩種動畫。
太棒了!現在看起來真的是一個像模像樣的玩家角色了!當然這個過程中有些行為可能並不是你想要的,你完全可以根據自己的想法來改進代碼!







© 2022 3樓貓 下載APP 站點地圖 廣告合作:asmrly666@gmail.com