本文為Google Translate英譯中結(jié)果,DrGraph在此基礎(chǔ)上加了一些校正。英文原版頁面:
Your first 3D game — Godot Engine (stable) documentation in English
你的第一個 3D 游戲?
在這個循序漸進的教程系列中,您將使用 Godot 創(chuàng)建您的第一個完整的 3D 游戲。到本系列結(jié)束時,您將擁有自己的一個簡單但已完成的項目,就像下面的動畫 gif 一樣。
我們將在此處編寫的游戲類似于您的第一個 2D 游戲,但有一點不同:您現(xiàn)在可以跳躍,您的目標是壓扁小兵。這樣,您既可以識別在上一教程中學到的模式,又可以使用新代碼和功能在這些模式的基礎(chǔ)上進行構(gòu)建。
您將學習:
-
使用跳躍機制處理 3D 坐標。
-
使用運動體移動 3D 角色并檢測它們何時以及如何發(fā)生碰撞。
-
使用物理層和一個組來檢測與特定實體的交互。
-
通過定期實例化怪物來編寫基本的程序游戲。
-
設(shè)計一個運動動畫并在運行時改變它的速度。
-
在 3D 游戲上繪制用戶界面。
以及更多。
本教程適用于學習了完整入門系列的初學者?!鹃_始時我們會慢一些,以進行詳細說明】,并在后續(xù)執(zhí)行類似步驟時簡要一些。如果您是一位經(jīng)驗豐富的程序員,您可以在此處瀏覽完整演示的源代碼:Squash the Creep 源代碼。
注:您可以在不完成 2D 系列的情況下關(guān)注本系列。但是,如果您不熟悉游戲開發(fā),我們建議您從 2D 開始。3D 游戲代碼總是更復雜,而 2D 系列將為您提供更舒適的基礎(chǔ)。
我們準備了一些游戲資源,以便我們可以直接跳轉(zhuǎn)到代碼。您可以在這里下載它們:Squash the Creeps assets。
我們將首先為玩家的動作制作一個基本原型。然后我們將添加我們將在屏幕周圍隨機生成的怪物。之后,我們將在用一些漂亮的動畫改進游戲之前實現(xiàn)跳躍和擠壓機制。我們將以得分和重試屏幕結(jié)束。
設(shè)置游戲區(qū)?
在第一部分中,我們將設(shè)置游戲區(qū)域。讓我們從導入開始資源和設(shè)置游戲場景開始吧。
我們已經(jīng)準備了一個 Godot 項目,其中包含我們將在本教程中使用的 3D 模型和聲音,鏈接在索引頁面中。如果您還沒有這樣做,您可以在此處下載存檔:Squash the Creeps assets。
下載后,將 .zip 存檔解壓縮到您的計算機上。打開 Godot 項目管理器并單擊導入按鈕。
在導入彈出窗口中,輸入新創(chuàng)建目錄的完整路徑?squash_the_creeps_start/
。您可以單擊右側(cè)的瀏覽按鈕打開文件瀏覽器并導航到文件夾包含的project.godot
文件。
單擊導入和編輯【Import & Edit】以在編輯器中打開項目。
啟動項目包含一個圖標和兩個文件夾:art/
和fonts/
。在那里,您會找到我們將在游戲中使用的美術(shù)資源和音樂。
有兩個 3D 模型,player.glb
和mob.glb
,屬于這些模型的一些材料,以及一個音樂曲目。
設(shè)置可玩區(qū)域?
我們將以普通節(jié)點作為其根來創(chuàng)建我們的主場景。在?Scene?dock 中,單擊左上角“+”圖標表示的Add Child Node按鈕,然后雙擊Node。命名節(jié)點Main
。重命名節(jié)點的另一種方法是右鍵單擊節(jié)點并選擇重命名(或F2)?;蛘?,要將節(jié)點添加到場景,您可以按Ctrl?+?a(或在 macOS 上Cmd?+?a)。
按Ctrl?+?s(在 macOS 上Cmd?+?s)將場景另存為main.tscn
。
我們將從添加防止角色掉落的地板開始。要創(chuàng)建地板、墻壁或天花板等靜態(tài)碰撞體,您可以使用StaticBody3D節(jié)點。它們需要CollisionShape3D子節(jié)點來定義碰撞區(qū)域。選擇Main
節(jié)點后,添加一個StaticBody3D?節(jié)點,然后添加一個CollisionShape3D。將StaticBody3D重命名為Ground
.
你的場景樹應該是這樣的
CollisionShape3D旁邊會出現(xiàn)一個警告標志,因為我們還沒有定義它的形狀。如果單擊該圖標,將出現(xiàn)一個彈出窗口,為您提供更多信息。
要創(chuàng)建形狀,請選擇CollisionShape3D節(jié)點,前往Inspector?并單擊Shape屬性旁邊的<empty>字段。創(chuàng)建一個新的BoxShape3D。
盒子形狀非常適合平坦的地面和墻壁。它的厚度使其能夠可靠地阻擋快速移動的物體。
一個盒子的線框出現(xiàn)在視口中,帶有三個橙色點。您可以單擊并拖動它們以交互方式編輯形狀的范圍。我們還可以在檢查器中精確設(shè)置尺寸。單擊BoxShape3D以展開資源。將其大小設(shè)置為X 軸60
、Y 軸2
和Z 軸60
。
碰撞形狀是不可見的。我們需要添加一個與之配套的視覺地板。選擇該Ground
節(jié)點并添加一個MeshInstance3D作為其子節(jié)點。
在Inspector中,單擊Mesh旁邊的字段并創(chuàng)建一個BoxMesh?資源以創(chuàng)建一個可見的框。
再一次,默認情況下它太小了。單擊方框圖標展開資源并將其大小設(shè)置為60
、2
和60
。
您應該會在視口中看到一個覆蓋網(wǎng)格以及藍色和紅色軸的寬灰色平板。
我們要把地面向下移動,以便我們可以看到地板網(wǎng)格。選擇?Ground
節(jié)點,按住Ctrl鍵打開網(wǎng)格捕捉,然后單擊并向下拖動 Y 軸。它是移動小工具中的綠色箭頭。
注:如果您看不到上圖所示的 3D 對象操縱器,請確保視圖上方工具欄中的選擇模式處于活動狀態(tài)。
向下移動地面1
米,以便有一個可見的編輯器網(wǎng)格。視口左下角的標簽告訴您節(jié)點的平移程度。
注:向下移動Ground節(jié)點會同時移動兩個子節(jié)點。確保移動Ground節(jié)點,而不是MeshInstance3D或?CollisionShape3D。
最終,Ground
的 transform.position.y 應該是 -1
讓我們添加一個定向光,這樣我們的場景就不會全是灰色的。選擇Main
?節(jié)點并添加子節(jié)點DirectionalLight3D。
我們需要移動和旋轉(zhuǎn)DirectionalLight3D節(jié)點。通過單擊并拖動操縱器的綠色箭頭將其向上移動,然后單擊并拖動紅色圓弧使其繞 X 軸旋轉(zhuǎn),直到地面被點亮。
在Inspector中,通過單擊復選框打開Shadow -> Enabled 。
此時,您的項目應該如下所示。
這是我們的【新起點】。在下一部分中,我們將處理玩家場景和基地運動。
玩家場景和輸入動作?
在接下來的兩節(jié)課中,我們將設(shè)計玩家場景、注冊自定義輸入操作以及編寫玩家移動代碼。到最后,您將擁有一個可以向八個方向移動的可玩角色。
通過轉(zhuǎn)到左上角的 Scene 菜單并單擊New Scene創(chuàng)建一個新場景。
創(chuàng)建一個CharacterBody3D節(jié)點作為根節(jié)點
將CharacterBody3D命名為Player
。角色身體是對 2D 游戲教程中使用的區(qū)域和剛體的補充。像剛體一樣,它們可以移動并與環(huán)境發(fā)生碰撞,但不是由物理引擎控制,而是您決定它們的運動。在編寫跳躍和擠壓機制代碼時,您將看到我們?nèi)绾问褂霉?jié)點的獨特功能。
參考:要了解有關(guān)不同物理節(jié)點類型的更多信息,請參閱?物理介紹。
現(xiàn)在,我們將為角色的 3D 模型創(chuàng)建一個基本裝備。這將允許我們稍后在播放動畫時通過代碼旋轉(zhuǎn)模型。
添加一個Node3D節(jié)點作為Player的
子節(jié)點并命名為Pivot
然后,在文件系統(tǒng)??繖谥?,通過雙擊展開文件夾art/
并將其下player.glb文件
拖放到Pivot
.
這應該將模型實例化為Pivot
.?您可以將其重命名為Character
.
注:這些.glb
文件包含基于開源 GLTF 2.0 規(guī)范的 3D 場景數(shù)據(jù)。它們是專有格式(如 Godot 也支持的 FBX)的現(xiàn)代且強大的替代品。為了生成這些文件,我們在Blender 3D中設(shè)計了模型并將其導出到 GLTF。
與各種物理節(jié)點一樣,我們需要一個碰撞形狀讓我們的角色與環(huán)境發(fā)生碰撞。再次選擇該Player
節(jié)點并添加一個子節(jié)點?CollisionShape3D。在Inspector的Shape屬性上,添加一個新的SphereShape3D。
球體的線框出現(xiàn)在角色下方。
這將是物理引擎用來與環(huán)境碰撞的形狀,所以我們希望它能更好地適應 3D 模型。通過在視口中拖動橙色點將其縮小一點。我的球體的半徑約為0.8
米。
然后,向上移動形狀,使其底部與網(wǎng)格平面大致對齊。
您可以通過單擊Pivot
或Character
節(jié)點旁邊的眼睛圖標來切換模型的可見性?。
將場景另存為player.tscn
準備好節(jié)點后,我們幾乎可以開始編碼了。但首先,我們還需要定義一些輸入動作。
創(chuàng)建輸入操作?
要移動角色,我們將監(jiān)聽玩家的輸入,例如按箭頭鍵。在 Godot 中,雖然我們可以在代碼中編寫所有鍵綁定,但有一個強大的系統(tǒng)允許您為一組鍵和按鈕分配標簽。這簡化了我們的腳本并使它們更具可讀性。
該系統(tǒng)是輸入映射。要訪問其編輯器,請前往項目菜單并選擇項目設(shè)置。
在頂部,有多個選項卡。單擊輸入地圖【Input Map】。此窗口允許您在頂部添加新操作;他們是你的標簽。在底部,您可以將鍵綁定到這些操作。
Godot 項目帶有一些為用戶界面設(shè)計設(shè)計的預定義操作,我們可以在這里使用它們。但我們正在定義自己的游戲手柄。
我們將命名我們的動作move_left
,?move_right
,?move_forward
,?move_back
?和jump
。
要添加一個動作,請在頂部的欄中寫下它的名稱,然后按 Enter 鍵。
創(chuàng)建以下五個操作:
要將鍵或按鈕綁定到操作,請單擊其右側(cè)的“+”按鈕。這樣做是為了move_left
。按向左箭頭鍵并單擊確定。
也將A鍵綁定到 move_left動作
上。
現(xiàn)在讓我們添加對游戲手柄左操縱桿的支持。再次單擊“+”按鈕,但這次選擇Manual Selection -> Joypad Axes。
選擇左操縱桿的負 X 軸。
將其他值保留為默認值,然后按OK
注:如果您希望控制器具有不同的輸入操作,您應該使用附加選項中的設(shè)備選項。設(shè)備 0 對應第一個插入的游戲手柄,設(shè)備 1 對應第二個插入的游戲手柄,依此類推。
對其他輸入操作執(zhí)行相同的操作。例如,將右箭頭 D 和左搖桿的正軸綁定到move_right
。綁定所有鍵后,您的界面應如下所示。
最后要設(shè)置的動作是jump
動作。綁定Space鍵和游戲手柄的A鍵。
您的跳轉(zhuǎn)輸入操作應如下所示。
這就是我們在這個游戲中需要的所有動作。您可以使用此菜單來標記項目中的任何按鍵和按鈕組。
在下一部分中,我們將編寫代碼并測試玩家的移動。
使用代碼移動Player?
是時候編碼了!我們將使用在上一部分中創(chuàng)建的輸入動作來移動角色。
右鍵單擊該Player
節(jié)點并選擇附加腳本以向其添加新腳本。在彈出窗口中,在按下創(chuàng)建按鈕之前將模板設(shè)置為空?。
讓我們從類的屬性開始。我們將定義一個移動速度,一個代表重力的下落加速度,以及一個我們將用來移動角色的速度。
extends CharacterBody3D
# How fast the player moves in meters per second.
@export var speed = 14
# The downward acceleration when in the air, in meters per second squared.
@export var fall_acceleration = 75
var target_velocity = Vector3.ZERO
這些是移動物體的共同屬性。target_velocity
是3D 矢量,它結(jié)合了速度和方向。在這里,我們將它定義為一個屬性,因為我們想要跨幀更新和重用它的值。
注:這些值與二維碼有很大不同,因為距離以米為單位。在 2D 中,一千個單位(像素)可能只對應于屏幕寬度的一半,而在 3D 中,它是一公里。
讓我們對運動進行編碼。我們首先在_physics_process()中
使用全局對象計算輸入方向向量Input
。
func _physics_process(delta):
# We create a local variable to store the input direction.
var direction = Vector3.ZERO
# We check for each move input and update the direction accordingly.
if Input.is_action_pressed("move_right"):
direction.x += 1
if Input.is_action_pressed("move_left"):
direction.x -= 1
if Input.is_action_pressed("move_back"):
# Notice how we are working with the vector's x and z axes.
# In 3D, the XZ plane is the ground plane.
direction.z += 1
if Input.is_action_pressed("move_forward"):
direction.z -= 1
在這里,我們將使用_physics_process()
?虛函數(shù)進行所有計算。就像 一樣_process()
,它允許您每幀更新節(jié)點,但它是專門為物理相關(guān)代碼設(shè)計的,例如移動運動學或剛體。
參考:_process()
要了解有關(guān)和?之間區(qū)別的更多信息_physics_process()
,請參閱空閑和物理處理。
我們首先將一個變量direction
初始化為Vector3.ZERO
.?然后,我們檢查玩家是否按下了一個或多個輸入move_*
并相應地更新向量x
和z
組件。這些對應于地平面的軸。
這四個條件給了我們八種可能,即八個可能的方向。
如果玩家同時按下 W 和 D,向量的長度約為1.4
.?但是如果他們按下一個鍵,它的長度將是1
.?我們希望向量的長度是一致的,而不是沿對角線移動得更快。為此,我們可以調(diào)用它的normalize()
方法。
#func _physics_process(delta):
#...
if direction != Vector3.ZERO:
direction = direction.normalized()
$Pivot.look_at(position + direction, Vector3.UP)
在這里,我們僅在方向的長度大于零時才對向量進行歸一化,這意味著玩家正在按下方向鍵。
在這種情況下,我們還獲取Pivot
節(jié)點并調(diào)用其look_at()
方法。此方法在空間中獲取一個位置,以在全局坐標和向上方向中查看。在這種情況下,我們可以使用Vector3.UP
常量。
注:節(jié)點的局部坐標,如position
,是相對于其父節(jié)點的。全局坐標,例如global_position
,相對于您可以在視口中看到的世界主軸。
在 3D 中,包含節(jié)點位置的屬性是position
。通過向其中添加direction
,我們得到一個距離 Player
1 米位置來查看。
然后,我們更新速度。我們必須分別計算地面速度和下落速度。【一定要注意縮進】以便這些行在_physics_process()
函數(shù)內(nèi)部,但在我們上面剛剛寫的條件之外。
func _physics_process(delta):
#...
if direction != Vector3.ZERO:
#...
# Ground Velocity
target_velocity.x = direction.x * speed
target_velocity.z = direction.z * speed
# Vertical Velocity
if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity
target_velocity.y = target_velocity.y - (fall_acceleration * delta)
# Moving the Character
velocity = target_velocity
move_and_slide()
如果身體在此幀中與地板發(fā)生碰撞,則該CharacterBody3D.is_on_floor()
函數(shù)返回true。這就是為什么我們只在Player
在空中時才對其施加重力。
對于垂直速度,我們減去下降加速度乘以每幀的增量時間。這行代碼將導致我們的角色在每一幀中掉落,只要它沒有落在地板上或與地板發(fā)生碰撞。
如果發(fā)生移動和碰撞,物理引擎只能在給定幀期間檢測與墻壁、地板或其他物體的交互。稍后我們將使用此屬性來編寫跳轉(zhuǎn)代碼。
在最后一行,我們調(diào)用CharacterBody3D.move_and_slide(),這
是CharacterBody3D
類的一個強大方法,它允許您平滑地移動角色。如果它在運動中途撞到墻壁,引擎會嘗試為您平滑它。它使用CharacterBody3D固有的速度值
這就是在地板上移動角色所需的全部代碼。
這里是完整的Player.gd
代碼供參考。
extends CharacterBody3D
# How fast the player moves in meters per second.
@export var speed = 14
# The downward acceleration when in the air, in meters per second squared.
@export var fall_acceleration = 75
var target_velocity = Vector3.ZERO
func _physics_process(delta):
var direction = Vector3.ZERO
if Input.is_action_pressed("move_right"):
direction.x += 1
if Input.is_action_pressed("move_left"):
direction.x -= 1
if Input.is_action_pressed("move_back"):
direction.z += 1
if Input.is_action_pressed("move_forward"):
direction.z -= 1
if direction != Vector3.ZERO:
direction = direction.normalized()
$Pivot.look_at(position + direction, Vector3.UP)
# Ground Velocity
target_velocity.x = direction.x * speed
target_velocity.z = direction.z * speed
# Vertical Velocity
if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity
target_velocity.y = target_velocity.y - (fall_acceleration * delta)
# Moving the Character
velocity = target_velocity
move_and_slide()
測試我們玩家的動作?
我們將把我們的Player放在Main
場景中進行測試。為此,我們需要實例化播放器,然后添加攝像機。與 2D 不同,在 3D 中,如果您的視口沒有相機指向某物,您將看不到任何東西。
保存Player
場景并打開Main
場景。您可以單擊?編輯器頂部的Main選項卡來執(zhí)行此操作。
如果您之前關(guān)閉了場景,請前往文件系統(tǒng)??繖诓㈦p擊?main.tscn
以重新打開它。
要實例化Player
,請右鍵單擊Main
節(jié)點并選擇Instance Child Scene。
在彈出窗口中,雙擊player.tscn
。角色應該出現(xiàn)在視口的中心。
添加相機?
接下來讓我們添加相機。就像我們對Player的Pivot所做的那樣,我們將創(chuàng)建一個基本的裝備。再次右鍵單擊該Main
節(jié)點并選擇?添加子節(jié)點。創(chuàng)建一個新的Marker3D并命名CameraPivot
。選擇并向其CameraPivot
添加一個子節(jié)點Camera3D 。您的場景樹應如下所示。
當您選擇了相機時,請注意左上角出現(xiàn)的預覽復選框。您可以單擊它來預覽游戲中的相機投影。
我們將使用Pivot來旋轉(zhuǎn)相機,就像它在起重機上一樣。讓我們首先拆分 3D 視圖,以便能夠自由瀏覽場景并查看相機所見。
在視口正上方的工具欄中,點擊View,然后點擊2 Viewports。您也可以按Ctrl?+?2(在 macOS 上Cmd?+?2)。
在底部視圖中,選擇您的Camera3D并通過單擊復選框打開相機預覽。
在頂視圖中,沿Z 軸(藍色軸)向上移動相機19
單位。
這就是魔法發(fā)生的地方。選擇CameraPivot并 繞 X 軸旋轉(zhuǎn)-45
度(使用紅色圓圈)。您會看到相機像掛在起重機上一樣移動。
您可以通過按F6并按箭頭鍵移動角色來運行場景。
由于透視投影,我們可以在角色周圍看到一些空白空間。在此游戲中,我們將改用正交投影來更好地框定游戲區(qū)域并讓玩家更容易讀取距離。
再次選擇Camera并在Inspector中,將Projection設(shè)置為?Orthogonal并將Size設(shè)置為19
。角色現(xiàn)在應該看起來更平坦,地面應該填滿背景。
注:在 Godot 4 中使用正交相機時,定向陰影質(zhì)量取決于相機的Far值。Far值越高,相機能夠看到的距離就越遠。但是,較高的Far值也會降低陰影質(zhì)量,因為陰影渲染必須覆蓋更大的距離。
如果在切換到正交相機后定向陰影看起來太模糊,請將相機的Far屬性減小到較低的值,例如?100
.?不要將此Far屬性減小太多,否則遠處的對象將開始消失。
測試您的場景,您應該能夠在所有 8 個方向上移動,并且不會在地板上出現(xiàn)故障!
最終,我們同時擁有玩家移動和視野。接下來,我們將處理怪物。
設(shè)計MOB場景?
在這一部分中,您將為怪物編寫代碼,我們稱之為mob。在下一課中,我們將在可玩區(qū)域周圍隨機生成它們。
讓我們在新場景中設(shè)計怪物本身。節(jié)點結(jié)構(gòu)將與player.tscn
場景相似。
再次創(chuàng)建一個以CharacterBody3D節(jié)點作為其根的場景。命名它?Mob
。添加一個子節(jié)點Node3D,命名為Pivot
。并將文件mob.glb
從FileSystem??繖谕戏诺?以Pivot
將怪物的 3D 模型添加到場景中。
您可以將新創(chuàng)建??的mob
節(jié)點重命名為Character
.
我們需要一個碰撞形狀來讓我們的身體工作。右鍵單擊Mob
場景的根節(jié)點,然后單擊添加子節(jié)點。
添加CollisionShape3D。
在Inspector中,將BoxShape3D分配給Shape屬性。
我們應該改變它的大小以更好地適應 3D 模型。您可以通過單擊并拖動橙色點以交互方式執(zhí)行此操作。
盒子應該接觸地板并且比模型薄一點。物理引擎的工作方式是,如果玩家的球體接觸到盒子的角,就會發(fā)生碰撞。如果盒子比 3D 模型大了一點,你可能會死在離怪物很遠的地方,游戲會給玩家一種不公平的感覺。
請注意,我的盒子比怪物高。在這個游戲中沒問題,因為我們是從上方觀察場景并使用固定視角。碰撞形狀不必與模型完全匹配。當你測試它時,游戲的感覺應該決定它們的形式和大小。
移除屏幕外的怪物?
我們將在游戲關(guān)卡中定期生成怪物。如果我們不小心,它們的數(shù)量可能會增加到無窮大,而我們不希望這樣。每個生物實例都有內(nèi)存和處理成本,當生物在屏幕外時我們不想為此付費。
一旦一個怪物離開了屏幕,我們就不再需要它了,所以我們應該刪除它。Godot 有一個節(jié)點VisibleOnScreenNotifier3D可以檢測物體何時離開屏幕?,我們將使用它來摧毀我們的生物。
注:當您不斷實例化一個對象時,可以使用一種技術(shù)來避免一直創(chuàng)建和銷毀實例的成本,稱為池化。它包括預先創(chuàng)建一個對象數(shù)組并一遍又一遍地重復使用它們。
使用 GDScript 時,您無需擔心這一點。使用池的主要原因是避免使用垃圾收集語言(如 C# 或 Lua)凍結(jié)。GDScript 使用不同的技術(shù)來管理內(nèi)存,即引用計數(shù),它沒有那個警告。您可以在此處了解更多相關(guān)信息:內(nèi)存管理。
選擇該Mob
節(jié)點并添加一個子節(jié)點VisibleOnScreenNotifier3D。出現(xiàn)另一個盒子,這次是粉紅色的。當這個盒子完全離開屏幕時,節(jié)點會發(fā)出一個信號。
使用橙色點調(diào)整它的大小,直到它覆蓋整個 3D 模型。
編碼MOB的運動?
讓我們來實現(xiàn)怪物的動作。我們將分兩步進行。首先,我們將在Mob
上編寫一個腳本,定義一個初始化怪物的函數(shù)。然后我們將在main.tscn
場景中編寫隨機生成機制的代碼并從那里調(diào)用函數(shù)。
將腳本附加到Mob
.
這是開始的移動代碼。我們定義了兩個屬性min_speed
?和max_speed
來定義一個隨機速度范圍,稍后我們將使用它來定義CharacterBody3D.velocity
。
extends CharacterBody3D # Minimum speed of the mob in meters per second. @export var min_speed = 10 # Maximum speed of the mob in meters per second. @export var max_speed = 18 func _physics_process(_delta): move_and_slide()
與玩家類似,我們通過調(diào)用函數(shù)CharacterBody3D.move_and_slide()
在每一幀移動mob。這次,我們在每一幀不更新velocity
;我們希望怪物以恒定的速度移動并離開屏幕,即使它會撞到障礙物。
我們需要定義另一個函數(shù)來計算CharacterBody3D.velocity
.?此函數(shù)會將怪物轉(zhuǎn)向玩家并隨機化其運動角度和速度。
該函數(shù)將把怪物的生成位置start_position
和?player_position
作為它的參數(shù)。
我們將怪物定位在start_position
并使用方法look_at_from_position()
將其轉(zhuǎn)向玩家,并通過圍繞 Y 軸隨機旋轉(zhuǎn)一個角度來隨機化角度。下面代碼,randf_range()
輸出一個介于弧度-PI/4
和弧度PI/4
之間的隨機值。
# This function will be called from the Main scene. func initialize(start_position, player_position): # We position the mob by placing it at start_position # and rotate it towards player_position, so it looks at the player. look_at_from_position(start_position, player_position, Vector3.UP) # Rotate this mob randomly within range of -90 and +90 degrees, # so that it doesn't move directly towards the player. rotate_y(randf_range(-PI / 4, PI / 4))
我們得到了一個隨機位置,現(xiàn)在我們需要一個random_speed
.?函數(shù)randi_range()
將很有用,因為它提供隨機 int 值,我們將使用min_speed與
max_speed
。?random_speed
只是一個整數(shù),我們只是用它來乘以我們的CharacterBody3D.velocity
.?應用random_speed
后,我們將Vector3速度向量CharacterBody3D.velocity向
玩家旋轉(zhuǎn)。
func initialize(start_position, player_position): # ... # We calculate a random speed (integer) var random_speed = randi_range(min_speed, max_speed) # We calculate a forward velocity that represents the speed. velocity = Vector3.FORWARD * random_speed # We then rotate the velocity vector based on the mob's Y rotation # in order to move in the direction the mob is looking. velocity = velocity.rotated(Vector3.UP, rotation.y)
離開屏幕?
我們?nèi)匀恍枰谏镫x開屏幕時消滅它們。為此,我們將VisibleOnScreenNotifier3D節(jié)點的screen_exited
信號連接到Mob
.
單擊編輯器頂部的3D標簽返回 3D 視口。您也可以按Ctrl?+?F2(在 macOS 上Alt?+?2)。
選擇VisibleOnScreenNotifier3D節(jié)點,然后在界面右側(cè)導航到節(jié)點停靠欄。雙擊screen_exited()
信號。
將信號連接到Mob
這會將您帶回腳本編輯器并為您添加一個新函數(shù)?_on_visible_on_screen_notifier_3d_screen_exited()
.?在該函數(shù)中調(diào)用queue_free()
?方法。這個函數(shù)銷毀它被調(diào)用的實例。
func _on_visible_on_screen_notifier_3d_screen_exited(): queue_free()
我們的怪物已經(jīng)準備好進入游戲了!在下一部分中,您將在游戲關(guān)卡中生成怪物。
這是完整的Mob.gd
腳本以供參考。
extends CharacterBody3D # Minimum speed of the mob in meters per second. @export var min_speed = 10 # Maximum speed of the mob in meters per second. @export var max_speed = 18 func _physics_process(_delta): move_and_slide() # This function will be called from the Main scene. func initialize(start_position, player_position): # We position the mob by placing it at start_position # and rotate it towards player_position, so it looks at the player. look_at_from_position(start_position, player_position, Vector3.UP) # Rotate this mob randomly within range of -90 and +90 degrees, # so that it doesn't move directly towards the player. rotate_y(randf_range(-PI / 4, PI / 4)) # We calculate a random speed (integer) var random_speed = randi_range(min_speed, max_speed) # We calculate a forward velocity that represents the speed. velocity = Vector3.FORWARD * random_speed # We then rotate the velocity vector based on the mob's Y rotation # in order to move in the direction the mob is looking. velocity = velocity.rotated(Vector3.UP, rotation.y) func _on_visible_on_screen_notifier_3d_screen_exited(): queue_free()
生成怪物?
在這一部分中,我們將沿著一條路徑隨機生成怪物。到最后,您將看到怪物在游戲板上漫游。
在文件系統(tǒng)中雙擊main.tscn
打開Main
場景。
在繪制路徑之前,我們要更改游戲分辨率。我們的游戲有一個默認的窗口大小1152x648
。我們要把它設(shè)置為720x540,
一個漂亮的小盒子。
轉(zhuǎn)到項目 -> 項目設(shè)置。
在左側(cè)菜單中,向下導航至Display -> Window。在右側(cè),將?寬度設(shè)置為720
,將高度設(shè)置為540
。
創(chuàng)建生成路徑?
就像您在 2D 游戲教程中所做的那樣,您將設(shè)計一條路徑并使用?PathFollow3D節(jié)點對其上的隨機位置進行采樣。
但是在 3D 中,繪制路徑有點復雜。我們希望它圍繞游戲視圖,這樣怪物就出現(xiàn)在屏幕外面。但是如果我們繪制一條路徑,我們將不會從相機預覽中看到它。
為了找到視圖的限制,我們可以使用一些占位符網(wǎng)格。您的視口仍應分為兩部分,相機預覽位于底部。如果不是這種情況,請按Ctrl?+?2(在 macOS 上Cmd?+?2) 將視圖一分為二。選擇Camera3D節(jié)點并單擊底部視口中的預覽復選框。
添加占位圓柱體?
讓我們添加占位符網(wǎng)格。添加一個新的Node3D作為?Main
節(jié)點的子節(jié)點并將其命名為Cylinders
。我們將使用它來對圓柱體進行分組。選擇Cylinders
并添加子節(jié)點MeshInstance3D
在Inspector中,將CylinderMesh分配給Mesh屬性。
使用視口左上角的菜單將頂部視口設(shè)置為頂部正交視圖?;蛘?,您可以按鍵盤上的 7 鍵。
網(wǎng)格可能會分散注意力。您可以通過轉(zhuǎn)到?工具欄中的“查看”菜單并單擊“查看網(wǎng)格”來切換它。
您現(xiàn)在想要沿著地平面移動圓柱體,在底部視口中查看相機預覽。我建議使用網(wǎng)格捕捉來這樣做。您可以通過單擊工具欄中的磁鐵圖標或按 Y 來切換它。
移動圓柱體,使其位于左上角相機視野之外。
我們將創(chuàng)建網(wǎng)格的副本并將它們放置在游戲區(qū)域周圍。按Ctrl?+?D(在 macOS 上Cmd?+?D) 復制節(jié)點。您還可以右鍵單擊場景停靠欄中的節(jié)點,然后選擇復制。沿藍色 Z 軸向下移動副本,直到它正好位于相機預覽之外。
通過按下Shift鍵并單擊未選擇的圓柱體并復制它們來選擇兩個圓柱體。
通過拖動紅色 X 軸將它們向右移動。
白色有點難看,不是嗎?讓我們通過給他們一種新材料讓他們脫穎而出。
在 3D 中,材質(zhì)定義了表面的視覺屬性,例如顏色、反射光的方式等。我們可以使用它們來改變網(wǎng)格的顏色。
我們可以一次更新所有四個圓柱體。選擇場景??繖谥械乃芯W(wǎng)格實例?。為此,您可以單擊第一個,然后按住 Shift 單擊最后一個。
在Inspector中,展開Material部分并將StandardMaterial3D分配給 slot?0。
單擊球體圖標以打開材料資源。您可以預覽材料和一長串填充有屬性的部分。您可以使用它們來創(chuàng)建各種表面,從金屬到巖石或水。
展開反照率部分。
將顏色設(shè)置為與背景形成對比的顏色,例如亮橙色。
我們現(xiàn)在可以使用圓柱體作為指南。單擊它們旁邊的灰色箭頭,將它們折疊到場景停靠欄中。展望未來,您還可以通過單擊Cylinders旁邊的眼睛圖標來切換它們的可見性。
添加子節(jié)點Path3D到Main
節(jié)點。在工具欄中,出現(xiàn)四個圖標。單擊添加點工具,即帶有綠色“+”符號的圖標。
您可以將鼠標懸停在任何圖標上以查看描述該工具的工具提示。
單擊每個圓柱體的中心以創(chuàng)建一個點。然后,單擊工具欄中的關(guān)閉曲線圖標以關(guān)閉路徑。如果任何點有點偏離,您可以單擊并拖動它以重新定位。
你的路徑應該是這樣的。
要對其上的隨機位置進行采樣,我們需要一個PathFollow3D節(jié)點。添加一個?PathFollow3D作為Path3D
.?分別將這兩個節(jié)點重命名為SpawnPath
和?SpawnLocation
。它更能描述我們將使用它們做什么。
這樣,我們就可以編寫生成機制的代碼了。
隨機生成怪物?
右鍵單擊該Main
節(jié)點并將新腳本附加到它。
我們首先將一個變量導出到Inspector,以便我們可以mob.tscn
?為其分配或任何其他怪物。
extends Node @export var mob_scene: PackedScene
我們希望以固定的時間間隔生成生物。為此,我們需要回到場景并添加一個計時器。不過,在此之前,我們需要將?mob.tscn
文件分配給mob_scene
上面的屬性(否則它為空?。?/p>
返回 3D 屏幕并選擇Main
節(jié)點。從FileSystem停靠欄拖動mob.tscn
到Inspector中的Mob Scene插槽。
添加一個新的Timer節(jié)點作為 的子節(jié)點Main
。命名它MobTimer
。
在Inspector中,將其Wait Time設(shè)置為0.5
秒并打開?Autostart以便它在我們運行游戲時自動啟動。
定時器timeout
每次到達等待時間結(jié)束時都會發(fā)出一個信號。默認情況下,它們會自動重啟,并循環(huán)發(fā)出信號。我們可以從主節(jié)點連接到這個信號,每0.5
秒生成一次怪物。
在MobTimer仍處于選中狀態(tài)的情況下,前往右側(cè)的節(jié)點??繖?,然后雙擊信號timeout
。
將其連接到主節(jié)點。
這會將您帶回腳本,并帶有一個新的空?_on_mob_timer_timeout()
函數(shù)。
讓我們編寫MOB生成邏輯。我們要:
-
實例化MOB場景。
-
在生成路徑上的隨機位置采樣。
-
獲取玩家的位置。
-
調(diào)用生物的
initialize()
方法,將隨機位置和玩家的位置傳遞給它。 -
將生物添加為主節(jié)點的子節(jié)點。
func _on_mob_timer_timeout(): # Create a new instance of the Mob scene. var mob = mob_scene.instantiate() # Choose a random location on the SpawnPath. # We store the reference to the SpawnLocation node. var mob_spawn_location = get_node("SpawnPath/SpawnLocation") # And give it a random offset. mob_spawn_location.progress_ratio = randf() var player_position = $Player.position mob.initialize(mob_spawn_location.position, player_position) # Spawn the mob by adding it to the Main scene. add_child(mob)
上面,randf()
在0和1之間產(chǎn)生一個隨機值,這是PathFollow節(jié)點所期望的:0 是路徑的起點,1 是路徑的終點。我們設(shè)置的路徑圍繞相機的視口,因此 0 到 1 之間的任何隨機值progress_ratio
都是視口邊緣的隨機位置!
main.gd
這是到目前為止的完整腳本,供參考。
extends Node @export var mob_scene: PackedScene func _on_mob_timer_timeout(): # Create a new instance of the Mob scene. var mob = mob_scene.instantiate() # Choose a random location on the SpawnPath. # We store the reference to the SpawnLocation node. var mob_spawn_location = get_node("SpawnPath/SpawnLocation") # And give it a random offset. mob_spawn_location.progress_ratio = randf() var player_position = $Player.position mob.initialize(mob_spawn_location.position, player_position) # Spawn the mob by adding it to the Main scene. add_child(mob)
您可以按F6測試場景。你應該看到怪物生成并直線移動。
現(xiàn)在,當它們的路徑交叉時,它們會相互碰撞和滑動。我們將在下一部分解決這個問題。
跳躍和擠壓怪物?
在這一部分中,我們將添加跳躍和擠壓怪物的能力。在下一課中,我們會讓玩家在怪物擊中地面時死亡。
首先,我們必須更改一些與物理交互相關(guān)的設(shè)置。進入物理層的世界。
控制物理相互作用?
物理實體可以訪問兩個互補屬性:圖層【layers】和遮罩【mask】。圖層定義對象位于哪個物理層上。
遮罩控制身體將聆聽和檢測的層次。這會影響碰撞檢測。當你想讓兩個物體相互作用時,你至少需要一個物體有一個與另一個物體相對應的掩碼。
如果這讓您感到困惑,請不要擔心,我們馬上會看到三個示例。
重要的一點是您可以使用圖層和遮罩來過濾物理交互、控制性能并消除代碼中對額外條件的需求。
默認情況下,所有物理體和區(qū)域 layer 和 mask都設(shè)置為1
。這意味著它們都相互碰撞。
物理層由數(shù)字表示,但我們可以給它們命名以跟蹤什么是什么。
設(shè)置圖層名稱?
讓我們給物理層起個名字。轉(zhuǎn)到項目 -> 項目設(shè)置。
在左側(cè)菜單中,向下導航至Layer Names -> 3D Physics。您可以在右側(cè)看到一個圖層列表,每個圖層旁邊都有一個字段。你可以在那里設(shè)置他們的名字。分別命名前三層player、enemy和world。
現(xiàn)在,我們可以將它們分配給我們的物理節(jié)點。
分配圖層和蒙版?
在主場景中,選擇Ground
節(jié)點。在Inspector中,展開?Collision部分。在那里,您可以將節(jié)點的圖層和蒙版視為按鈕網(wǎng)格。
地面是世界的一部分,所以我們希望它成為第三層的一部分。單擊亮起的按鈕關(guān)閉第一層并打開第三層。然后,通過單擊關(guān)閉蒙版。
如前所述,Mask屬性允許節(jié)點監(jiān)聽與其他物理對象的交互,但我們不需要它發(fā)生碰撞。Ground
不需要聽任何東西;它只是為了防止生物掉落。
請注意,您可以單擊屬性右側(cè)的“...”按鈕來查看已命名復選框的列表。
接下來是Player
和Mob
。通過雙擊文件系統(tǒng)??繖谥械奈募?code>player.tscn打開。
選擇Player節(jié)點并將其Collision -> Mask設(shè)置為“enemies”和“world”。您可以保留默認Layer屬性不變,因為第一層是“播放器”層。
然后,雙擊打開Mobmob.tscn
場景并選擇?Mob
節(jié)點。
將其Collision -> Layer設(shè)置為“enemies”并取消其Collision -> Mask 的設(shè)置,使遮罩為空。
這些設(shè)置意味著怪物將相互移動。如果你想讓怪物相互碰撞和滑動,打開“敵人”面具。
注:生物不需要屏蔽“世界”層,因為它們只在 XZ 平面上移動。我們不會通過設(shè)計對它們施加任何重力。
跳躍?
跳躍機制本身只需要兩行代碼。打開播放器?腳本。我們需要一個值來控制跳躍的強度并更新?_physics_process()
以對跳躍進行編碼。
在定義fall_acceleration
的行之后,在腳本的頂部,添加jump_impulse
.
#... # Vertical impulse applied to the character upon jumping in meters per second. @export var jump_impulse = 20
在內(nèi)部_physics_process()
,在代碼塊之前添加以下代碼move_and_slide()
。
func _physics_process(delta): #... # Jumping. if is_on_floor() and Input.is_action_just_pressed("jump"): target_velocity.y = jump_impulse #...
這就是你需要跳的全部!
該is_on_floor()
方法是來自該類的工具CharacterBody3D
。true
如果身體在此幀中與地板碰撞,它會返回。這就是為什么我們對Player施加重力:所以我們與地板碰撞而不是像怪物那樣漂浮在地板上。
如果角色在地板上并且玩家按下“跳躍”,我們會立即給他們很大的垂直速度。在游戲中,您真的希望控件能夠響應并提供像這樣的即時速度提升,雖然不切實際,但感覺很棒。
請注意,Y 軸向上為正。這與 2D 不同,其中 Y 軸向下為正。
壓扁怪物?
接下來讓我們添加壁球機制。我們要讓角色在怪物身上彈跳并同時殺死它們。
我們需要檢測與怪物的碰撞并將它們與與地板的碰撞區(qū)分開來。為此,我們可以使用 Godot 的組標記功能。
再次打開場景mob.tscn
并選擇Mob節(jié)點。轉(zhuǎn)到?右側(cè)的節(jié)點??繖谝圆榭葱盘柫斜?em>。Node?dock 有兩個選項卡:?您已經(jīng)使用過的Signals和允許您將標簽分配給節(jié)點的Groups?。
單擊它以顯示一個字段,您可以在其中寫入標簽名稱。在字段中輸入“mob”,然后單擊“添加”按鈕。
場景??繖谥袝霈F(xiàn)一個圖標,指示該節(jié)點至少屬于一個組。
我們現(xiàn)在可以使用代碼中的組來區(qū)分與怪物的碰撞和與地板的碰撞。
編碼擠壓機制?
回到Player腳本來編寫擠壓和彈跳的代碼。
在腳本的頂部,我們需要另一個屬性bounce_impulse
.?壓扁敵人時,我們不一定希望角色像跳躍時那樣飛得那么高。
# Vertical impulse applied to the character upon bouncing over a mob in # meters per second. @export var bounce_impulse = 16
然后,在我們在上面_physics_process()
添加的Jumping代碼塊之后,添加以下循環(huán)。使用 時?move_and_slide()
,Godot 有時會連續(xù)多次移動身體以平滑角色的運動。所以我們必須遍歷所有可能發(fā)生的碰撞。
在循環(huán)的每次迭代中,我們檢查我們是否降落在暴徒身上。如果是這樣,我們殺死它并反彈。
使用此代碼,如果給定幀上沒有發(fā)生碰撞,則循環(huán)不會運行。
func _physics_process(delta): #... # Iterate through all collisions that occurred this frame for index in range(get_slide_collision_count()): # We get one of the collisions with the player var collision = get_slide_collision(index) # If the collision is with ground if (collision.get_collider() == null): continue # If the collider is with a mob if collision.get_collider().is_in_group("mob"): var mob = collision.get_collider() # we check that we are hitting it from above. if Vector3.UP.dot(collision.get_normal()) > 0.1: # If so, we squash it and bounce. mob.squash() target_velocity.y = bounce_impulse
這是很多新功能。這里有一些關(guān)于它們的更多信息。
函數(shù)get_slide_collision_count()
和get_slide_collision()
都來自CharacterBody3D類,并且與?move_and_slide()相關(guān)
.
get_slide_collision()
返回一個?KinematicCollision3D對象,該對象包含有關(guān)碰撞發(fā)生的位置和方式的信息。例如,我們使用它的get_collider
屬性,通過調(diào)用is_in_group()
來檢查我們是否與“暴徒”發(fā)生碰撞 :?collision.get_collider().is_in_group("mob")
注:該is_in_group()
方法在每個Node上都可用。
為了檢查我們是否降落在怪物身上,我們使用向量點積:Vector3.UP.dot(collision.get_normal())?>?0.1
。碰撞法線是垂直于發(fā)生碰撞的平面的 3D 矢量。點積允許我們將其與向上方向進行比較。
對于點積,當結(jié)果大于 時0
,兩個向量的夾角小于 90 度。高于的值0.1
告訴我們,我們大致在怪物之上。
我們正在調(diào)用一個未定義的函數(shù)mob.squash()
,因此我們必須將它添加到 Mob 類中。
通過在文件系統(tǒng)??繖谥须p擊Mob.gd
腳本來打開它。在腳本的頂部,我們要定義一個名為squashed
的新信號。在底部,您可以添加 squash 函數(shù),我們可以在其中發(fā)出信號并摧毀生物。
# Emitted when the player jumped on the mob. signal squashed # ... func squash(): squashed.emit() queue_free()
我們將在下一課中使用該信號為得分加分。
有了它,你應該能夠通過跳上它們來殺死怪物。您可以按?F5嘗試游戲并設(shè)置main.tscn
為項目的主場景。
但是,玩家還不會死。我們將在下一部分進行處理。
殺死玩家?
我們可以通過跳到敵人身上殺死他們,但玩家仍然不能死。讓我們解決這個問題。
我們想要檢測被敵人擊中與壓扁他們的方式不同。我們希望玩家在地板上移動時死亡,但在空中則不會。我們可以使用矢量數(shù)學來區(qū)分這兩種碰撞。不過,我們將使用Area3D節(jié)點,它適用于碰撞盒。
帶有 Area 節(jié)點的 Hitbox?
回到player.tscn
場景并添加一個新的子節(jié)點Area3D。將其命名?MobDetector
?為添加一個CollisionShape3D節(jié)點作為它的子節(jié)點。
在Inspector中,為其指定一個圓柱體形狀。
這是一個技巧,您可以使用它來使碰撞僅在玩家在地面上或靠近地面時發(fā)生。您可以降低圓柱體的高度并將其向上移動到角色的頂部。這樣,當玩家跳躍時,形狀會高到敵人無法與其發(fā)生碰撞。
您還希望圓柱體比球體寬。這樣,玩家在碰撞并被推到怪物的碰撞箱頂部之前就被擊中了。
圓柱體越寬,玩家就越容易被殺死。
接下來,再次選擇該MobDetector
節(jié)點,并在Inspector中關(guān)閉其Monitorable屬性。這使得其他物理節(jié)點無法檢測到該區(qū)域。互補的Monitoring屬性允許它檢測碰撞。然后,移除Collision -> Layer并將蒙版設(shè)置為“enemies”層。
當區(qū)域檢測到碰撞時,它們會發(fā)出信號。我們將把一個連接到Player
節(jié)點。選擇MobDetector
并轉(zhuǎn)到Inspector的節(jié)點選項卡,雙擊?body_entered
信號并將其連接到Player
MobDetector將在CharacterBody3D或?RigidBody3D節(jié)點進入時發(fā)射body_entered
。因為它只屏蔽“敵人”物理層,所以它只會檢測Mob
節(jié)點。???????
在代碼方面,我們要做兩件事:發(fā)出一個信號,稍后我們將使用它來結(jié)束游戲并摧毀玩家。我們可以將這些操作包裝在一個die()
函數(shù)中,幫助我們在代碼上貼上描述性標簽。
# Emitted when the player was hit by a mob. # Put this at the top of the script. signal hit # And this function at the bottom. func die(): hit.emit() queue_free() func _on_mob_detector_body_entered(body): die()
按 再次嘗試游戲F5。如果一切設(shè)置正確,角色應該在敵人撞上碰撞器時死亡。請注意,沒有Player
,以下行
var player_position = $Player.position
給出錯誤,因為沒有 $Player!
另請注意,與玩家碰撞并死亡的敵人取決于?Player
和 的Mob
碰撞形狀的大小和位置。您可能需要移動它們并調(diào)整它們的大小以獲得緊湊的游戲感覺。
結(jié)束游戲?
我們可以使用Player
的hit
信號來結(jié)束游戲。我們需要做的就是將它連接到Main
節(jié)點并停止MobTimer
反應。
打開main.tscn
,選擇Player
節(jié)點,然后在Node?dock 中,將其hit
信號連接到Main
節(jié)點。
在函數(shù)中獲取計時器并停止它_on_player_hit()
。
func _on_player_hit(): $MobTimer.stop()
如果你現(xiàn)在嘗試游戲,怪物會在你死后停止生成,剩下的會離開屏幕。
您可以表揚一下自己:您制作了一個完整的 3D 游戲原型,即使它還有些粗糙。
從那里,我們將添加一個分數(shù),重試游戲的選項,您將看到如何使用簡約的動畫讓游戲感覺更加生動。
代碼檢查點?
Main
以下是、Mob
和Player
節(jié)點的完整腳本,供參考。您可以使用它們來比較和檢查您的代碼。
從main.gd
開始
extends Node @export var mob_scene: PackedScene func _on_mob_timer_timeout(): # Create a new instance of the Mob scene. var mob = mob_scene.instantiate() # Choose a random location on the SpawnPath. # We store the reference to the SpawnLocation node. var mob_spawn_location = get_node("SpawnPath/SpawnLocation") # And give it a random offset. mob_spawn_location.progress_ratio = randf() var player_position = $Player.position mob.initialize(mob_spawn_location.position, player_position) # Spawn the mob by adding it to the Main scene. add_child(mob) func _on_player_hit(): $MobTimer.stop()
接下來是Mob.gd
。
extends CharacterBody3D # Minimum speed of the mob in meters per second. @export var min_speed = 10 # Maximum speed of the mob in meters per second. @export var max_speed = 18 # Emitted when the player jumped on the mob signal squashed func _physics_process(_delta): move_and_slide() # This function will be called from the Main scene. func initialize(start_position, player_position): # We position the mob by placing it at start_position # and rotate it towards player_position, so it looks at the player. look_at_from_position(start_position, player_position, Vector3.UP) # Rotate this mob randomly within range of -90 and +90 degrees, # so that it doesn't move directly towards the player. rotate_y(randf_range(-PI / 4, PI / 4)) # We calculate a random speed (integer) var random_speed = randi_range(min_speed, max_speed) # We calculate a forward velocity that represents the speed. velocity = Vector3.FORWARD * random_speed # We then rotate the velocity vector based on the mob's Y rotation # in order to move in the direction the mob is looking. velocity = velocity.rotated(Vector3.UP, rotation.y) func _on_visible_on_screen_notifier_3d_screen_exited(): queue_free() func squash(): squashed.emit() queue_free() # Destroy this node
最后,最長的腳本Player.gd
:
extends CharacterBody3D signal hit # How fast the player moves in meters per second @export var speed = 14 # The downward acceleration while in the air, in meters per second squared. @export var fall_acceleration = 75 # Vertical impulse applied to the character upon jumping in meters per second. @export var jump_impulse = 20 # Vertical impulse applied to the character upon bouncing over a mob # in meters per second. @export var bounce_impulse = 16 var target_velocity = Vector3.ZERO func _physics_process(delta): # We create a local variable to store the input direction var direction = Vector3.ZERO # We check for each move input and update the direction accordingly if Input.is_action_pressed("move_right"): direction.x = direction.x + 1 if Input.is_action_pressed("move_left"): direction.x = direction.x - 1 if Input.is_action_pressed("move_back"): # Notice how we are working with the vector's x and z axes. # In 3D, the XZ plane is the ground plane. direction.z = direction.z + 1 if Input.is_action_pressed("move_forward"): direction.z = direction.z - 1 # Prevent diagonal moving fast af if direction != Vector3.ZERO: direction = direction.normalized() $Pivot.look_at(position + direction, Vector3.UP) # Ground Velocity target_velocity.x = direction.x * speed target_velocity.z = direction.z * speed # Vertical Velocity if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity target_velocity.y = target_velocity.y - (fall_acceleration * delta) # Jumping. if is_on_floor() and Input.is_action_just_pressed("jump"): target_velocity.y = jump_impulse # Iterate through all collisions that occurred this frame # in C this would be for(int i = 0; i < collisions.Count; i++) for index in range(get_slide_collision_count()): # We get one of the collisions with the player var collision = get_slide_collision(index) # If the collision is with ground if (collision.get_collider() == null): continue # If the collider is with a mob if collision.get_collider().is_in_group("mob"): var mob = collision.get_collider() # we check that we are hitting it from above. if Vector3.UP.dot(collision.get_normal()) > 0.1: # If so, we squash it and bounce. mob.squash() target_velocity.y = bounce_impulse # Moving the Character velocity = target_velocity move_and_slide() # And this function at the bottom. func die(): hit.emit() queue_free() func _on_mob_detector_body_entered(body): die()
下一節(jié)課見,添加分數(shù)和重玩選項。
得分和重玩?
在這一部分,我們將添加得分、音樂播放和重新啟動游戲的功能。
我們必須在變量中跟蹤當前分數(shù),并使用最小界面將其顯示在屏幕上。我們將使用文本標簽來做到這一點。
在主場景中,添加Main
一個新的子節(jié)點Control并命名為UserInterface
。您將自動進入 2D 屏幕,您可以在其中編輯用戶界面 (UI)。
添加Label節(jié)點并命名ScoreLabel
在Inspector中,將Label的文本設(shè)置為占位符,如“Score: 0”。
此外,文本默認為白色,就像我們游戲的背景一樣。我們需要改變它的顏色才能在運行時看到它。
向下滾動到Theme Overrides,展開Colors?并啟用Font Color以便將文本著色為黑色(與白色 3D 場景形成鮮明對比)
最后,單擊并拖動視口中的文本,將其從左上角移開。
該UserInterface
節(jié)點允許我們將 UI 分組到場景樹的一個分支中,并使用將傳播到其所有子節(jié)點的主題資源。我們將使用它來設(shè)置我們游戲的字體。
創(chuàng)建 UI 主題?
再次選擇UserInterface
節(jié)點。在Inspector中,在Theme -> Theme中創(chuàng)建一個新的主題資源。
單擊它以在底部面板中打開主題編輯器。它使您可以預覽所有內(nèi)置 UI 小部件與您的主題資源的外觀。
默認情況下,一個主題只有一個屬性,即Default Font。
參閱:您可以向主題資源添加更多屬性以設(shè)計復雜的用戶界面,但這超出了本系列的范圍。要了解有關(guān)創(chuàng)建和編輯主題的更多信息,請參閱GUI 皮膚簡介。
這需要一個字體文件,就像您計算機上的字體文件一樣。兩種常見的字體文件格式是 TrueType 字體 (TTF) 和 OpenType 字體 (OTF)。
在FileSystem?dock 中,展開fonts
目錄并單擊我們包含在項目中的文件Montserrat-Medium.ttf
并將其拖到?Default Font上。文本將重新出現(xiàn)在主題預覽中。
文字有點小。將默認字體大小設(shè)置為22
像素以增加文本的大小。
跟蹤分數(shù)?
接下來讓我們研究分數(shù)。將新腳本附加到ScoreLabel
并定義score
變量。
extends Label var score = 0
每次我們壓扁一個怪物時,分數(shù)應該增加1。我們可以使用他們的squashed
信號來知道什么時候發(fā)生。但是,因為我們從代碼中實例化了怪物,所以我們通過
編輯器無法將 mob 信號連接到ScoreLabel
。
相反,我們必須在每次生成怪物時從代碼中建立連接。
打開腳本main.gd
。如果它仍然打開,您可以在腳本編輯器的左欄中單擊它的名稱。
或者,您可以雙擊文件系統(tǒng)main.gd
??繖谥械奈募?。
在函數(shù)的底部_on_mob_timer_timeout()
,添加以下行:
func _on_mob_timer_timeout(): #... # We connect the mob to the score label to update the score upon squashing one. mob.squashed.connect($UserInterface/ScoreLabel._on_mob_squashed.bind())
這一行的意思是,當生物發(fā)出信號時squashed
,?ScoreLabel
節(jié)點將接收信號并調(diào)用函數(shù)_on_mob_squashed()
。
返回ScoreLabel.gd
腳本以定義_on_mob_squashed()
?回調(diào)函數(shù)。
在那里,我們增加分數(shù)并更新顯示的文本。
func _on_mob_squashed(): score += 1 text = "Score: %s" % score
第二行使用變量的值score
來代替占位符%s
。使用該特性時,Godot 會自動將值轉(zhuǎn)換為字符串文本,方便在標簽中輸出文本或使用函數(shù)時使用print()
。
參閱:您可以在此處了解有關(guān)字符串格式化的更多信息:GDScript 格式化字符串。在 C# 中,考慮使用帶有 "$" 的字符串插值。
您現(xiàn)在可以玩游戲并壓扁一些敵人以查看得分增加。
注:在復雜的游戲中,您可能希望將用戶界面與游戲世界完全分開。在那種情況下,您不會跟蹤標簽上的分數(shù)。相反,您可能希望將其存儲在一個單獨的專用對象中。但是當制作原型或當你的項目很簡單時,讓你的代碼保持簡單是很好的。編程始終是一種平衡行為。
重玩游戲?
我們現(xiàn)在將添加死后再次播放的功能。當玩家死亡時,我們將在屏幕上顯示一條消息并等待輸入。
回到main.tscn
場景,選擇UserInterface
節(jié)點,添加一個子節(jié)點ColorRect,并將其命名為Retry
。該節(jié)點用統(tǒng)一的顏色填充一個矩形,并將用作使屏幕變暗的覆蓋層。
要使其跨越整個視口,您可以使用工具欄中的“錨點預設(shè)”菜單。
打開它并應用Full Rect命令。
什么都沒發(fā)生。好吧,幾乎沒有;只有四個綠色圖釘移動到選擇框的角落。
這是因為 UI 節(jié)點(所有帶有綠色圖標的節(jié)點)使用相對于其父邊界框的錨點和邊距。在這里,UserInterface
節(jié)點的尺寸很小,并且Retry
受其限制。
選擇UserInterface
并應用Anchor Preset -> Full Rect。該?Retry
節(jié)點現(xiàn)在應該跨越整個視口。
讓我們改變它的顏色,使游戲區(qū)域變暗。選擇Retry
并在?Inspector中,將其Color設(shè)置為深色和透明的顏色。為此,在顏色選擇器中,將A滑塊拖動到左側(cè)。它控制顏色的 Alpha 通道,也就是說,它的不透明度/透明度。
接下來,添加一個Label作為子項Retry
,并為其提供文本?“Press Enter to retry”。要移動它并將其錨定在屏幕中央,請?對其應用錨定預設(shè) -> 中心。
編碼重玩選項?
我們現(xiàn)在可以使用代碼來在Retry
玩家死亡并再次玩游戲時顯示和隱藏節(jié)點。
打開腳本main.gd
。首先,我們想在游戲開始時隱藏疊加層。將此行添加到_ready()
函數(shù)中。
func _ready(): $UserInterface/Retry.hide()
然后,當玩家被擊中時,我們會顯示疊加層。
func _on_player_hit(): #... $UserInterface/Retry.show()
最后,當Retry
節(jié)點可見時,我們需要監(jiān)聽玩家的輸入,如果他們按下 enter 則重新啟動游戲。為此,我們使用內(nèi)置?_unhandled_input()
回調(diào),它在任何輸入時都會觸發(fā)。
如果玩家按下預定義的ui_accept
輸入操作并且Retry
可見,我們將重新加載當前場景。
func _unhandled_input(event): if event.is_action_pressed("ui_accept") and $UserInterface/Retry.visible: # This restarts the current scene. get_tree().reload_current_scene()
該函數(shù)get_tree()
使我們能夠訪問全局SceneTree對象,這使我們能夠重新加載和重新啟動當前場景。
添加音樂?
要添加在后臺連續(xù)播放的音樂,我們將使用 Godot 中的另一個功能:自動加載。
要播放音頻,您需要做的就是將一個AudioStreamPlayer節(jié)點添加到您的場景并向其附加一個音頻文件。當您啟動場景時,它可以自動播放。但是,當您重新加載場景時,就像我們再次播放一樣,音頻節(jié)點也會重置,音樂會從頭開始播放。
您可以使用自動加載功能讓 Godot 在游戲開始時自動加載當前場景之外的節(jié)點或場景。您還可以使用它來創(chuàng)建全局可訪問的對象。
通過轉(zhuǎn)到“場景”菜單并單擊“新建場景”?或使用當前打開的場景旁邊的+圖標來創(chuàng)建新場景。
單擊Other Node按鈕創(chuàng)建一個AudioStreamPlayer并將其重命名為?MusicPlayer
.
art/
我們在目錄中包含了音樂配樂House In a Forest Loop.ogg,
單擊并將其拖到Inspector中的Stream屬性上。此外,打開自動播放,以便在游戲開始時自動播放音樂。
將場景另存為MusicPlayer.tscn
.
我們必須將其注冊為自動加載。前往Project -> Project Settings…菜單并單擊Autoload選項卡。
在路徑字段中,您要輸入場景的路徑。單擊文件夾圖標打開文件瀏覽器并雙擊MusicPlayer.tscn
.?然后,點擊右側(cè)的添加按鈕注冊節(jié)點。
MusicPlayer.tscn
現(xiàn)在加載到您打開或播放的任何場景中。所以如果你現(xiàn)在運行游戲,音樂會在任何場景中自動播放。
在我們結(jié)束本課之前,讓我們快速了解一下它是如何工作的。當您運行游戲時,您的Scene?dock 會更改為您提供兩個選項卡:?Remote和Local。
遠程選項卡允許您可視化正在運行的游戲的節(jié)點樹。在那里,您將看到主節(jié)點和場景包含的所有內(nèi)容以及底部的實例化生物。
頂部是自動加載節(jié)點MusicPlayer
和根節(jié)點,這是您游戲的視口。
這就是本課的內(nèi)容。在下一部分中,我們將添加一個動畫,使游戲的外觀和感覺都更好。
這是完整的main.gd
腳本以供參考。
extends Node @export var mob_scene: PackedScene func _ready(): $UserInterface/Retry.hide() func _on_mob_timer_timeout(): # Create a new instance of the Mob scene. var mob = mob_scene.instantiate() # Choose a random location on the SpawnPath. # We store the reference to the SpawnLocation node. var mob_spawn_location = get_node("SpawnPath/SpawnLocation") # And give it a random offset. mob_spawn_location.progress_ratio = randf() var player_position = $Player.position mob.initialize(mob_spawn_location.position, player_position) # Spawn the mob by adding it to the Main scene. add_child(mob) # We connect the mob to the score label to update the score upon squashing one. mob.squashed.connect($UserInterface/ScoreLabel._on_mob_squashed.bind()) func _on_player_hit(): $MobTimer.stop() $UserInterface/Retry.show() func _unhandled_input(event): if event.is_action_pressed("ui_accept") and $UserInterface/Retry.visible: # This restarts the current scene. get_tree().reload_current_scene()
角色動畫?
在最后一課中,我們將使用 Godot 的內(nèi)置動畫工具讓我們的角色漂浮和拍打。您將學習在編輯器中設(shè)計動畫并使用代碼讓您的游戲充滿活力。
我們將從介紹使用動畫編輯器開始。
使用動畫編輯器?
該引擎帶有在編輯器中創(chuàng)作動畫的工具。然后您可以使用代碼在運行時播放和控制它們。
打開播放器場景,選擇Player
節(jié)點,并添加一個AnimationPlayer節(jié)點。
動畫???/em>欄出現(xiàn)在底部面板中。
它的頂部有一個工具欄和動畫下拉菜單,中間有一個當前為空的軌道編輯器,底部有過濾、捕捉和縮放選項。
讓我們創(chuàng)建一個動畫。單擊動畫 -> 新建。
將動畫命名為“浮動”。
一旦您創(chuàng)建了動畫,時間線就會出現(xiàn),其中的數(shù)字代表時間(以秒為單位)。
我們希望動畫在游戲開始時自動開始播放。此外,它應該循環(huán)。
為此,您可以分別單擊動畫工具欄中帶有“A+”圖標的按鈕和循環(huán)箭頭。
您還可以通過單擊右上角的固定圖標來固定動畫編輯器。這可以防止它在您單擊視口并取消選擇節(jié)點時折疊。
在??繖诘挠疑辖菍赢嫵掷m(xù)時間設(shè)置為1.2
秒。
你應該看到灰色絲帶變寬了一點。它向您顯示動畫的開始和結(jié)束,垂直的藍線是您的時間光標。
您可以單擊并拖動右下角的滑塊來放大和縮小時間線。
浮動動畫?
使用動畫播放器節(jié)點,您可以根據(jù)需要為任意數(shù)量的節(jié)點上的大多數(shù)屬性設(shè)置動畫。請注意Inspector中屬性旁邊的鑰匙圖標。您可以單擊其中任何一個來為相應的屬性創(chuàng)建關(guān)鍵幀、時間和值對。關(guān)鍵幀將插入時間軸中時間光標所在的位置。
讓我們插入我們的第一把鑰匙。在這里,我們將為節(jié)點的位置和旋轉(zhuǎn)設(shè)置動畫Character
。
選擇Character
并在Inspector中展開Transform部分。單擊Position和Rotation旁邊的鑰匙圖標。
對于本教程,只需創(chuàng)建默認選擇的 RESET Track(s)
兩個軌道出現(xiàn)在編輯器中,每個關(guān)鍵幀都有一個菱形圖標。
您可以單擊并拖動菱形以及時移動它們。將位置鍵移至0.2
秒,將旋轉(zhuǎn)鍵移至0.1
秒。
通過在灰色時間線上單擊并拖動,將時間光標移動到0.5
秒。
在Inspector中,將Position的Y軸設(shè)置為0.65
米,將Rotation的X軸設(shè)置為8
.
為兩個屬性創(chuàng)建一個關(guān)鍵幀
0.7
?現(xiàn)在,通過在時間軸上拖動將位置關(guān)鍵幀移動到秒。
注:關(guān)于動畫原理的講座超出了本教程的范圍。請注意,您不想均勻地安排時間和空間。相反,動畫師使用時間和間隔這兩個核心動畫原則。您想抵消和對比角色的動作,讓他們感覺自己還活著。
將時間光標移動到動畫的末尾,以1.2
秒為單位。將 Y 位置設(shè)置為大約0.35
,將 X 旋轉(zhuǎn)設(shè)置為-9
度數(shù)。再次為這兩個屬性創(chuàng)建一個鍵。
您可以通過單擊播放按鈕或按Shift?+?D來預覽結(jié)果。單擊停止按鈕S或按停止播放。
您可以看到引擎在關(guān)鍵幀之間進行插值以生成連續(xù)的動畫。不過目前,這個動作感覺非常機械化。這是因為默認插值是線性的,導致不斷的過渡,這與現(xiàn)實世界中生物的移動方式不同。
我們可以使用緩動曲線來控制關(guān)鍵幀之間的過渡。
單擊并拖動時間軸中的前兩個鍵以框選它們。
您可以在Inspector中同時編輯兩個鍵的屬性,您可以在其中看到一個Easing屬性。
單擊并拖動曲線,將其向左拉。這將使它緩出,也就是說,最初過渡很快,隨著時間光標到達下一個關(guān)鍵幀而減慢。
再次播放動畫以查看不同之處。上半場應該已經(jīng)感覺有點活潑了。
對旋轉(zhuǎn)軌道中的第二個關(guān)鍵幀應用緩出。
對第二個位置關(guān)鍵幀執(zhí)行相反的操作,將其拖到右側(cè)。
你的動畫應該看起來像這樣。
注:動畫每幀更新動畫節(jié)點的屬性,覆蓋初始值。如果我們直接為Player節(jié)點設(shè)置動畫,它會阻止我們在代碼中移動它。這就是Pivot節(jié)點派上用場的地方:即使我們?yōu)?em>Character設(shè)置了動畫,我們?nèi)匀豢梢栽谀_本中移動和旋轉(zhuǎn)Pivot以及在動畫頂部更改圖層。
如果您玩游戲,玩家的生物現(xiàn)在會漂?。?/p>
如果該生物離地面有點太近,您可以Pivot
向上移動以抵消它。
在代碼中控制動畫?
我們可以使用代碼根據(jù)玩家的輸入來控制動畫播放。讓我們改變角色移動時的動畫速度。
通過單擊旁邊的腳本圖標打開Player
的腳本。
在 中_physics_process()
,在我們檢查向量的行之后direction
?,添加以下代碼。
func _physics_process(delta): #... if direction != Vector3.ZERO: #... $AnimationPlayer.speed_scale = 4 else: $AnimationPlayer.speed_scale = 1
這段代碼使得當玩家移動時,我們將播放速度乘以?4
。當他們停止時,我們將其重置為正常。
我們提到Pivot
could 層在動畫之上進行變換。我們可以使用以下代碼行在跳躍時制作角色弧線。在末尾添加它_physics_process()
。
func _physics_process(delta): #... $Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse
動畫生物?
這是 Godot 中動畫的另一個不錯的技巧:只要使用類似的節(jié)點結(jié)構(gòu),就可以將它們復制到不同的場景中。
例如,場景Mob
和Player
場景都有Pivot
一個?Character
節(jié)點,所以我們可以在它們之間重用動畫。
打開Player場景,選擇 AnimationPlayer 節(jié)點并打開“浮動”動畫。接下來,單擊“動畫”>“復制”。然后打開mob.tscn
,創(chuàng)建一個AnimationPlayer子節(jié)點并選中它。單擊“動畫”>“粘貼”?并確保在底部面板的動畫編輯器中也打開帶有“A+”圖標的按鈕(加載時自動播放)和循環(huán)箭頭(動畫循環(huán))。就是這樣;?所有怪物現(xiàn)在都會播放浮動動畫。
我們可以根據(jù)生物的 改變播放速度random_speed
。打開Mob的腳本并在函數(shù)末尾initialize()
添加以下行。
func initialize(start_position, player_position): #... $AnimationPlayer.speed_scale = random_speed / min_speed
這樣,您就完成了第一個完整的 3D 游戲的編碼。
恭喜!
在下一部分中,我們將快速回顧您學到的內(nèi)容,并為您提供一些鏈接以繼續(xù)學習更多內(nèi)容。但現(xiàn)在,這里是完整的Player.gd
,?Mob.gd
因此您可以對照它們檢查您的代碼。
這是播放器腳本。
extends CharacterBody3D signal hit # How fast the player moves in meters per second. @export var speed = 14 # The downward acceleration while in the air, in meters per second squared. @export var fall_acceleration = 75 # Vertical impulse applied to the character upon jumping in meters per second. @export var jump_impulse = 20 # Vertical impulse applied to the character upon bouncing over a mob # in meters per second. @export var bounce_impulse = 16 var target_velocity = Vector3.ZERO func _physics_process(delta): # We create a local variable to store the input direction var direction = Vector3.ZERO # We check for each move input and update the direction accordingly if Input.is_action_pressed("move_right"): direction.x = direction.x + 1 if Input.is_action_pressed("move_left"): direction.x = direction.x - 1 if Input.is_action_pressed("move_back"): # Notice how we are working with the vector's x and z axes. # In 3D, the XZ plane is the ground plane. direction.z = direction.z + 1 if Input.is_action_pressed("move_forward"): direction.z = direction.z - 1 # Prevent diagonal movement being very fast if direction != Vector3.ZERO: direction = direction.normalized() $Pivot.look_at(position + direction,Vector3.UP) $AnimationPlayer.speed_scale = 4 else: $AnimationPlayer.speed_scale = 1 # Ground Velocity target_velocity.x = direction.x * speed target_velocity.z = direction.z * speed # Vertical Velocity if not is_on_floor(): # If in the air, fall towards the floor target_velocity.y = target_velocity.y - (fall_acceleration * delta) # Jumping. if is_on_floor() and Input.is_action_just_pressed("jump"): target_velocity.y = jump_impulse # Iterate through all collisions that occurred this frame # in C this would be for(int i = 0; i < collisions.Count; i++) for index in range(get_slide_collision_count()): # We get one of the collisions with the player var collision = get_slide_collision(index) # If the collision is with ground if (collision.get_collider() == null): continue # If the collider is with a mob if collision.get_collider().is_in_group("mob"): var mob = collision.get_collider() # we check that we are hitting it from above. if Vector3.UP.dot(collision.get_normal()) > 0.1: # If so, we squash it and bounce. mob.squash() target_velocity.y = bounce_impulse # Moving the Character velocity = target_velocity move_and_slide() $Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse # And this function at the bottom. func die(): hit.emit() queue_free() func _on_mob_detector_body_entered(body): die()
還有 The?Mob的腳本。
extends CharacterBody3D # Minimum speed of the mob in meters per second. @export var min_speed = 10 # Maximum speed of the mob in meters per second. @export var max_speed = 18 # Emitted when the player jumped on the mob signal squashed func _physics_process(_delta): move_and_slide() # This function will be called from the Main scene. func initialize(start_position, player_position): # We position the mob by placing it at start_position # and rotate it towards player_position, so it looks at the player. look_at_from_position(start_position, player_position, Vector3.UP) # Rotate this mob randomly within range of -90 and +90 degrees, # so that it doesn't move directly towards the player. rotate_y(randf_range(-PI / 4, PI / 4)) # We calculate a random speed (integer) var random_speed = randi_range(min_speed, max_speed) # We calculate a forward velocity that represents the speed. velocity = Vector3.FORWARD * random_speed # We then rotate the velocity vector based on the mob's Y rotation # in order to move in the direction the mob is looking. velocity = velocity.rotated(Vector3.UP, rotation.y) $AnimationPlayer.speed_scale = random_speed / min_speed func _on_visible_on_screen_notifier_3d_screen_exited(): queue_free() func squash(): squashed.emit() queue_free() # Destroy this node???????
更進一步?
您可以為自己完成了第一個使用 Godot 的 3D 游戲而感到欣慰。
在本系列中,我們介紹了廣泛的技術(shù)和編輯器功能。希望您已經(jīng)見證了 Godot 的場景系統(tǒng)是多么直觀,并學到了一些可以在您的項目中應用的技巧。
但我們只是觸及了表面:Godot 為您節(jié)省創(chuàng)建游戲的時間提供了更多。您可以通過瀏覽文檔了解所有這些。
你應該從哪里開始?在下面,您將找到幾頁以開始探索和構(gòu)建您目前所學的內(nèi)容。
但在此之前,這里有一個下載項目完整版本的鏈接:?https://github.com/godotengine/godot-3d-dodge-the-creeps。
瀏覽手冊?
每當您有疑問或?qū)δ稠椆δ芨械胶闷鏁r,手冊就是您的盟友。它不包含有關(guān)特定游戲類型或機制的教程。相反,它解釋了 Godot 的一般工作原理。在其中,您會找到有關(guān) 2D、3D、物理、渲染和性能等方面的信息。
以下是我們建議您接下來探索的部分:
-
閱讀腳本部分,了解您將在每個項目中使用的基本編程功能。
-
3D和物理部分將教您更多有關(guān)在引擎中創(chuàng)建 3D 游戲的信息。
-
輸入是任何游戲項目的另一個重要輸入。
您可以從這些開始,或者,如果您愿意,可以查看左側(cè)的側(cè)邊欄菜單并選擇您的選項。文章來源:http://www.zghlxwxcb.cn/news/detail-703038.html
我們希望您喜歡這個教程系列,我們期待看到您使用 Godot 取得的成就。文章來源地址http://www.zghlxwxcb.cn/news/detail-703038.html
到了這里,關(guān)于Godot引擎 4.0 文檔 - 第一個 3D 游戲的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!