04. Thiết kế scene quái vật (Designing the mob scene)


Trong phần này, chúng ta sẽ tạo các con quái vật (trong game gọi là Mob). Ở bài tiếp theo, chúng ta sẽ lập trình để chúng xuất hiện ngẫu nhiên xung quanh khu vực chơi.

Chúng ta sẽ thiết kế Mob trong một scene riêng biệt với cấu trúc node tương tự như player.tscn.

Thiết lập cây Node cho Mob

  1. Trên thanh menu, chọn Scene -> New Scene.
  2. Chọn Other Node và tìm chọn CharacterBody3D. Đổi tên node gốc thành Mob.
  3. Chọn Mob, thêm một node con Node3D và đổi tên thành Pivot.
  4. Trong bảng FileSystem, mở thư mục art/, kéo và thả file mob.glb vào node Pivot.

  1. Đổi tên node mô hình vừa thả vào thành Character.

Thêm vùng va chạm (CollisionShape3D)

Để quái vật tương tác vật lý với thế giới game: 1. Chọn node gốc Mob, nhấn Ctrl + A để thêm node CollisionShape3D.

  1. Trong bảng Inspector, tại thuộc tính Shape, chọn New BoxShape3D (Hình hộp va chạm).

  1. Dùng các chấm màu cam trong viewport 3D để chỉnh kích thước hộp va chạm sao cho vừa vặn với mô hình quái vật: - Hộp nên tiếp xúc với mặt sàn. - Chiều ngang của hộp nên thon gọn hơn mô hình một chút.
💡 Mẹo hay
Các động cơ vật lý sẽ tính là va chạm ngay khi hình cầu của Player chạm vào góc rìa của hộp va chạm. Nếu bạn để hộp va chạm của Mob quá to, người chơi sẽ bị chết ngay cả khi nhìn thấy mình vẫn đứng cách quái một khoảng, tạo cảm giác thiếu công bằng và khó chịu. Hãy để hộp va chạm hơi nhỏ hơn một chút so với hình ảnh hiển thị.


Tự động hủy quái vật khi rời màn hình

Trong quá trình chơi, quái vật sẽ liên tục được sinh ra theo chu kỳ. Nếu không dọn dẹp, số lượng node sẽ tăng lên vô tận, làm tiêu tốn RAM và làm chậm máy tính.

Khi một con quái vật đã bay ra khỏi màn hình, chúng ta không cần đến nó nữa và nên xóa nó đi. Godot cung cấp một node rất tiện lợi là VisibleOnScreenNotifier3D chuyên để phát hiện khi đối tượng rời khỏi tầm nhìn camera:

  1. Chọn node gốc Mob, thêm một node con VisibleOnScreenNotifier3D.

  1. Một khối hộp màu hồng sẽ xuất hiện trong viewport.
  2. Kéo các chấm màu cam để mở rộng hộp màu hồng này bao trùm lấy toàn bộ mô hình quái vật.

📌 Lưu ý
**Kỹ thuật Object Pooling:** Trong các ngôn ngữ có cơ chế gom rác (Garbage Collection) như C#, việc liên tục tạo mới và hủy đối tượng (instantiate / destroy) có thể gây giật lag (frame drop), vì vậy người ta hay dùng kỹ thuật *Object Pooling* (tạo sẵn một danh sách đối tượng và tái sử dụng). Nhưng với GDScript, Godot sử dụng cơ chế đếm tham chiếu (*reference counting*) siêu nhẹ, nên việc gọi `queue_free()` là hoàn toàn an toàn và mượt mà.

Lập trình chuyển động cho quái vật

Nhấp chuột phải vào node Mob, chọn Attach Script (để Template là Empty) và nhấn Create.

Quái vật sẽ di chuyển theo một đường thẳng với tốc độ ngẫu nhiên từ min_speed đến max_speed. Chúng chỉ cần tính toán vận tốc một lần lúc sinh ra và giữ nguyên hướng đó:

extends CharacterBody3D

# Tốc độ tối thiểu của quái vật (mét/giây).
@export var min_speed = 10
# Tốc độ tối đa của quái vật (mét/giây).
@export var max_speed = 18


func _physics_process(_delta):
    move_and_slide()

Hàm khởi tạo ngẫu nhiên initialize()

Chúng ta tạo một hàm initialize(start_position, player_position) để scene Main gọi tới mỗi khi sinh ra một con quái vật mới: - Đặt quái vật tại vị trí xuất phát start_position. - Hướng mặt quái vật về phía người chơi player_position. - Làm lệch góc ngẫu nhiên trong khoảng từ -45° đến +45° (tức $-\frac{\pi}{4}$ đến $+\frac{\pi}{4}$ radian) để quái không bay đâm thẳng trực diện vào người chơi. - Chọn ngẫu nhiên một tốc độ và tính vector vận tốc velocity.

# Hàm này sẽ được gọi từ scene Main khi sinh quái vật
func initialize(start_position, player_position):
    # Đặt vị trí quái vật tại start_position và xoay mặt hướng về phía player_position
    look_at_from_position(start_position, player_position, Vector3.UP)

    # Xoay lệch ngẫu nhiên góc từ -45 độ đến +45 độ quanh trục Y
    rotate_y(randf_range(-PI / 4, PI / 4))

    # Tính tốc độ ngẫu nhiên
    var random_speed = randi_range(min_speed, max_speed)

    # Vector vận tốc tiến về phía trước theo hướng quái vật nhìn
    velocity = Vector3.FORWARD * random_speed
    velocity = velocity.rotated(Vector3.UP, rotation.y)

Kết nối tín hiệu hủy quái vật

Khi hộp màu hồng của VisibleOnScreenNotifier3D hoàn toàn biến mất khỏi tầm nhìn camera, nó sẽ phát tín hiệu screen_exited:

  1. Chọn node VisibleOnScreenNotifier3D.
  2. Ở bảng góc phải màn hình, nhấp sang tab Node (bên cạnh tab Inspector) để xem danh sách Signals (Tín hiệu).
  3. Nhấp đúp vào tín hiệu screen_exited().
  4. Chọn node nhận là Mob và nhấn Connect.

Godot sẽ tự động tạo hàm _on_visible_on_screen_notifier_3d_screen_exited() trong script của bạn. Hãy gọi lệnh queue_free() bên trong hàm đó:

func _on_visible_on_screen_notifier_3d_screen_exited():
    queue_free()

Mã nguồn hoàn chỉnh của Mob

extends CharacterBody3D

# Tốc độ tối thiểu của mob tính bằng mét/giây.
@export var min_speed = 10
# Tốc độ tối đa của mob tính bằng mét/giây.
@export var max_speed = 18


func _physics_process(_delta):
    move_and_slide()


# Hàm này sẽ được gọi từ scene Main.
func initialize(start_position, player_position):
    # Đặt vị trí mob tại start_position và hướng mặt về player_position
    look_at_from_position(start_position, player_position, Vector3.UP)

    # Xoay ngẫu nhiên từ -45 đến +45 độ quanh trục Y
    rotate_y(randf_range(-PI / 4, PI / 4))

    # Lấy tốc độ ngẫu nhiên (số nguyên)
    var random_speed = randi_range(min_speed, max_speed)

    # Tính vận tốc tiến tới phía trước
    velocity = Vector3.FORWARD * random_speed
    # Xoay vector vận tốc theo góc quay hiện tại của mob
    velocity = velocity.rotated(Vector3.UP, rotation.y)


func _on_visible_on_screen_notifier_3d_screen_exited():
    queue_free()
using Godot;

public partial class Mob : CharacterBody3D
{
    [Export]
    public int MinSpeed { get; set; } = 10;

    [Export]
    public int MaxSpeed { get; set; } = 18;

    public override void _PhysicsProcess(double delta)
    {
        MoveAndSlide();
    }

    public void Initialize(Vector3 startPosition, Vector3 playerPosition)
    {
        LookAtFromPosition(startPosition, playerPosition, Vector3.Up);
        RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));

        int randomSpeed = GD.RandRange(MinSpeed, MaxSpeed);
        Velocity = Vector3.Forward * randomSpeed;
        Velocity = Velocity.Rotated(Vector3.Up, Rotation.Y);
    }

    private void OnVisibilityNotifierScreenExited()
    {
        QueueFree();
    }
}

Lưu scene lại với tên mob.tscn (Ctrl + S).