Work in progress

The content of this page was not yet updated for Godot 4.2 and may be outdated. If you know how to improve this page or you can confirm that it's up to date, feel free to open a pull request.

設計敵人場景

在這一部分中,我們要為怪物編寫程式碼,我們後續會稱之為“mob”(小怪)。在下一節課中,我們會在遊戲區域周圍隨機生成它們。

讓我們在一個新場景中設計這些怪物。節點結構和 player.tscn 場景類似。

還是用 CharacterBody3D 節點作為根節點來建立場景。命名為 Mob。新增一個 Node3D 節點作為其子項,將其命名為 Pivot。將 mob.glb 檔從*檔案系統*面板拖放到 Pivot 上,這樣就把怪物的 3D 模型新增到了場景之中。

../../_images/drag_drop_mob.webp

你可以將新建立的 mob 節點重命名成 Character

image0

我們的實體要新增碰撞形狀後才能正常工作。按右鍵場景的根節點 Mob,然後按一下*新增子節點*。

image1

CollisionShape2D

image2

在*屬性面板*中為 Shape*(形狀)屬性分配一個 *BoxShape3D

../../_images/08.create_box_shape3D.jpg

我們要調整一下它的大小,來更好地框住 3D 模型。可以按一下並拖動橙色的小點來進行。

碰撞盒應該接觸地面,並且比模型稍微瘦一點點。即便玩家的球體只接觸了這個碰撞盒的角落,物理引擎也會判定發生了碰撞。如果盒子比 3D 模型要大一點,你可能距離怪物還有一定的距離就死了,玩家就會覺得不公平。

image4

請注意,我的盒子要比怪物稍高。在這個遊戲裡是沒問題的,因為我們是從遊戲場景的上方用固定角度觀察的。碰撞形狀不必精確配對模型。決定碰撞形狀形式和大小的關鍵是你在試玩遊戲時的手感。

刪除舊的怪物

我們要在遊戲關卡中按照一定的時間間隔刷怪。如果你不小心,它們的數量可能就會無限地增長下去,我們可不想那樣。每個小怪實例都需要付出一定的記憶體和處理代價,我們不希望讓螢幕之外的小怪浪費資源。

怪物離開螢幕之後,我們就不再需要它了,所以我們可以把它刪除。Godot 有一個可以偵測物件離開螢幕的節點, VisibleOnScreenNotifier3D ,我們就要用它來銷毀我們的小怪。

備註

如果要在遊戲中不斷產生實體同一種物件,可以通過一種叫“池化”(pooling)的技術來避免持續地建立和銷毀實例。做法是預先建立一個該物件的陣列,然後去不斷地重用裡面的元素。

使用 GDScript 時,你不必擔心這個問題。用物件集區的主要目的是避免 C# 或 Lua 等帶垃圾回收的語言帶來的停滯。GDScript 管理記憶體的技術和它們是不同的,用的是引用計數,不會產生那種問題。你可以在這裡瞭解更多相關內容:記憶體管理

選中 Mob 節點,並為其新增一個 VisibleOnScreenNotifier3D 作為子項。這回出現的就是一個粉色的框。這個框完全離開螢幕後,該節點就會發出訊號。

image5

使用橙色的點來調整大小,讓它覆蓋住整個 3D 模型。

image6

為小怪的移動編寫程式碼

讓我們來實作怪物的運動。我們要分兩步來實作。首先,我們要為 Mob 編寫腳本,定義初始化怪物的函式。然後我們會在 main.tscn 場景中編寫隨機刷怪的機制並進行呼叫。

將腳本附加到節點。

image7

這是最初的移動程式碼。我們定義了兩個屬性 min_speedmax_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()

與玩家類似,在每一影格我們都會通過呼叫 CharacterBody3D.move_and_slide() 方法來移動小怪。這一回,我們不會再每影格更新 velocity 了:我們希望怪物勻速移動,然後離開螢幕,即便碰到障礙物也一樣。

我們需要再定義一個函式來計算初始的速度。這個函式會讓怪物面朝玩家,並將其運動角度和速度隨機化。

這個函式接受小怪的生成位置 start_position 以及玩家的位置 player_position 作為參數。

我們首先將小怪定位在 start_position 並用 look_at_from_position() 方法將它轉向玩家,並通過圍繞 Y 軸旋轉隨機量來隨機化角度。下面,rand_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 -45 and +45 degrees,
    # so that it doesn't move directly towards the player.
    rotate_y(randf_range(-PI / 4, PI / 4))

我們已經獲取到了一個隨機的位置,現在我們需要一個 random_speedrandi_range() 可以給我們需要的隨機整數,並且我們要使用 min_speedmax_speedrandom_speed 是一個整數,我們只是使用它與我們的 CharacterBody3D.velocity 相乘。在乘完 random_speed 之後,我們將 random_speed 旋轉至朝向玩家的方向。

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)

測試場景

我們還需要在小怪離開螢幕後將其銷毀。實作方法是將 VisibleOnScreenNotifier3D 節點的 screen_exited 訊號連接到 Mob 上。

按一下編輯器頂部的 3D 標籤回到 3D 視口。你也可以按 Ctrl + F2`(macOS 上則是 :kbd:`Alt + 2)。

image8

選中 VisibleOnScreenNotifier3D 節點,然後在介面右側打開*節點*面板。按兩下 screen_exited() 訊號。

image9

連接訊號至方法

image10

這樣你就會被帶回到腳本編輯器,並且幫你新增了一個新的函式 _on_visible_on_screen_notifier_3d_screen_exited()。請在裡面呼叫 queue_free() 方法。這樣 VisibleOnScreenNotifier3D 的框離開螢幕時就會將小怪的實例銷毀。

func _on_visible_on_screen_notifier_3d_screen_exited():
    queue_free()

我們的怪物已經準備好進入遊戲了!在下一部分,你將在遊戲關卡中生成怪物。

這是僅供參考的完整 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()