03. Lập trình di chuyển cho người chơi (Moving the player with code)


Đã đến lúc bắt tay vào lập trình! Chúng ta sẽ sử dụng các Input Action vừa tạo ở phần trước để điều khiển nhân vật di chuyển trên mặt đất 3D.

📌 Lưu ý
**Quy ước đặt tên trong Godot:** - **GDScript**: Tên lớp (Nodes) dùng `PascalCase`, biến và hàm dùng `snake_case`, hằng số dùng `ALL_CAPS`. - **C#**: Tên lớp, biến Export và phương thức dùng `PascalCase`, trường private dùng `_camelCase`, biến cục bộ và tham số dùng `camelCase`.

Tạo Script cho Player

  1. Nhấp chuột phải vào node gốc Player trong scene player.tscn và chọn Attach Script (Gắn mã lệnh).

  1. Trong cửa sổ hiện ra, đổi mục Template thành Empty (để tạo file trống tự viết mã từ đầu).
  2. Nhấn nút Create.

Khai báo các thuộc tính cơ bản

Chúng ta bắt đầu bằng việc định nghĩa tốc độ chạy (speed), gia tốc rơi tự do mô phỏng trọng lực (fall_acceleration) và vector vận tốc mục tiêu (target_velocity):

extends CharacterBody3D

# Tốc độ di chuyển của người chơi tính bằng mét/giây.
@export var speed = 14
# Gia tốc rơi tự do khi ở trên không tính bằng mét/giây bình phương.
@export var fall_acceleration = 75

var target_velocity = Vector3.ZERO

C

using Godot;

public partial class Player : CharacterBody3D
{
    // Tốc độ di chuyển của người chơi tính bằng mét/giây.
    [Export]
    public int Speed { get; set; } = 14;
    // Gia tốc rơi tự do khi ở trên không tính bằng mét/giây bình phương.
    [Export]
    public int FallAcceleration { get; set; } = 75;

    private Vector3 _targetVelocity = Vector3.Zero;
}
📌 Lưu ý
Các giá trị số trong 3D khác biệt rất lớn so với 2D vì đơn vị tính trong 3D là **mét**. Trong không gian 2D, 1000 pixel có thể chỉ bằng nửa màn hình, nhưng trong 3D, 1000 đơn vị tương đương cả 1 cây số (kilometer).

Xử lý chuyển động với _physics_process()

Thay vì dùng _process(), trong lập trình vật lý chúng ta luôn sử dụng hàm ảo _physics_process(). Hàm này được gọi đều đặn theo các khoảng thời gian cố định (fixed time intervals), rất lý tưởng để tính toán va chạm và vận tốc cho CharacterBody3D.

1. Tính toán vector hướng di chuyển

Trong không gian 3D của Godot: - Trục X: Đi ngang (Trái / Phải). - Trục Y: Đi lên / xuống (Độ cao). - Trục Z: Đi tới / lui (Mặt phẳng nằm ngang là mặt phẳng XZ).

func _physics_process(delta):
    # Biến cục bộ lưu trữ hướng di chuyển từ người chơi
    var direction = Vector3.ZERO

    # Kiểm tra các phím bấm và cập nhật hướng tương ứng
    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"):
        # Trong 3D, trục Z dương hướng về phía màn hình (lùi lại)
        direction.z += 1
    if Input.is_action_pressed("move_forward"):
        # Trục Z âm hướng vào sâu trong màn hình (tiến tới)
        direction.z -= 1

2. Chuẩn hóa vector (Normalize) và xoay hướng nhìn

Nếu người chơi bấm cùng lúc cả hai phím (ví dụ W và D để đi chéo), độ dài vector hướng sẽ bằng khoảng $\sqrt{1^2 + 1^2} \approx 1.414$, khiến nhân vật chạy chéo nhanh hơn đi thẳng. Ta dùng hàm normalized() để giữ độ dài luôn bằng 1.

Đồng thời, ta muốn nhân vật tự động quay mặt về hướng di chuyển bằng cách cập nhật thuộc tính basis của $Pivot:

    if direction != Vector3.ZERO:
        direction = direction.normalized()
        # Xoay Pivot nhìn về hướng di chuyển
        $Pivot.basis = Basis.looking_at(direction)

3. Cập nhật vận tốc và gọi move_and_slide()

Ta tính toán vận tốc mặt đất và vận tốc rơi theo phương thẳng đứng tách biệt nhau:

    # Vận tốc trên mặt đất (trục X và Z)
    target_velocity.x = direction.x * speed
    target_velocity.z = direction.z * speed

    # Vận tốc thẳng đứng (Trọng lực)
    if not is_on_floor(): # Nếu đang ở trên không, rơi xuống mặt sàn
        target_velocity.y = target_velocity.y - (fall_acceleration * delta)

    # Gán vào thuộc tính velocity có sẵn của CharacterBody3D và thực hiện di chuyển
    velocity = target_velocity
    move_and_slide()
  • Hàm is_on_floor() trả về true nếu nhân vật vừa tiếp đất trong khung hình này. Do đó trọng lực chỉ tích lũy khi nhân vật đang rơi tự do trên không.
  • Hàm move_and_slide() là phương thức cốt lõi của CharacterBody3D, tự động xử lý trượt mượt mà dọc theo các bề mặt va chạm khi chạm phải vật cản.

Toàn bộ mã nguồn hoàn chỉnh của Player

extends CharacterBody3D

# Tốc độ di chuyển của người chơi tính bằng mét/giây.
@export var speed = 14
# Gia tốc rơi tự do khi ở trên không tính bằng mét/giây bình phương.
@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()
        # Xoay Pivot hướng mặt về phía di chuyển
        $Pivot.basis = Basis.looking_at(direction)

    # Vận tốc trên mặt đất
    target_velocity.x = direction.x * speed
    target_velocity.z = direction.z * speed

    # Vận tốc trọng lực rơi tự do
    if not is_on_floor():
        target_velocity.y = target_velocity.y - (fall_acceleration * delta)

    # Thực hiện di chuyển nhân vật
    velocity = target_velocity
    move_and_slide()
using Godot;

public partial class Player : CharacterBody3D
{
    [Export]
    public int Speed { get; set; } = 14;

    [Export]
    public int FallAcceleration { get; set; } = 75;

    private Vector3 _targetVelocity = Vector3.Zero;

    public override void _PhysicsProcess(double delta)
    {
        var direction = Vector3.Zero;

        if (Input.IsActionPressed("move_right"))
        {
            direction.X += 1.0f;
        }
        if (Input.IsActionPressed("move_left"))
        {
            direction.X -= 1.0f;
        }
        if (Input.IsActionPressed("move_back"))
        {
            direction.Z += 1.0f;
        }
        if (Input.IsActionPressed("move_forward"))
        {
            direction.Z -= 1.0f;
        }

        if (direction != Vector3.Zero)
        {
            direction = direction.Normalized();
            GetNode<Node3D>("Pivot").Basis = Basis.LookingAt(direction);
        }

        // Vận tốc mặt đất
        _targetVelocity.X = direction.X * Speed;
        _targetVelocity.Z = direction.Z * Speed;

        // Trọng lực
        if (!IsOnFloor())
        {
            _targetVelocity.Y -= FallAcceleration * (float)delta;
        }

        // Di chuyển
        Velocity = _targetVelocity;
        MoveAndSlide();
    }
}

Kiểm thử di chuyển của người chơi

Để kiểm tra xem người chơi đã di chuyển được chưa, chúng ta cần đưa Player vào trong scene Main và thêm một camera 3D để quan sát.

1. Khởi tạo Player trong Main scene

  1. Mở lại scene main.tscn.

  1. Nhấp chuột phải vào node gốc Main và chọn Instantiate Child Scene (Khởi tạo scene con) hoặc phím tắt biểu tượng mắt xích.
  2. Chọn file player.tscn. Nhân vật người chơi sẽ xuất hiện ở ngay tâm sàn đấu.

2. Thiết lập Camera dạng cần cẩu (Camera Rig)

  1. Chọn node Main, thêm một node con Marker3D, đổi tên thành CameraPivot.
  2. Chọn CameraPivot, thêm một node con Camera3D.

Cấu trúc cây node lúc này:

Cấu trúc Node
Main
├── Ground
├── DirectionalLight3D
├── Player
└── CameraPivot (Marker3D)
    └── Camera3D

3. Căn chỉnh góc nhìn Camera

  1. Chia màn hình làm 2 khung nhìn để dễ quan sát: trên thanh công cụ góc trên khung nhìn 3D, chọn View -> 2 Viewports (hoặc nhấn Ctrl + 2).
  2. Ở khung nhìn bên dưới, chọn Camera3D và tích vào ô Preview ở góc trên bên trái khung nhìn đó để xem trước những gì camera thu lại.

  1. Ở khung nhìn bên trên, chọn Camera3D và kéo mũi tên màu xanh dương (trục Z) lùi lại khoảng 19 đơn vị (Z = 19).

  1. Chọn node cha CameraPivot, dùng công cụ xoay (phím E) xoay quanh trục X (vòng đỏ) một góc -45 độ (hoặc nhập -45 vào Rotation -> X trong Inspector). Bạn sẽ thấy camera chúc xuống sàn đấu như một chiếc cần cẩu máy quay chuyên nghiệp.

4. Đổi sang chế độ hiển thị trực giao (Orthogonal Projection)

Mặc định camera dùng phép chiếu phối cảnh (Perspective), làm cho các vật thể ở xa trông nhỏ lại. Trong thể loại game arcade nhìn từ trên xuống này, phép chiếu trực giao (Orthogonal) sẽ giúp khung cảnh phẳng đều, dễ ước lượng khoảng cách hơn:

  1. Chọn node Camera3D.
  2. Trong bảng Inspector, đổi thuộc tính Projection từ Perspective sang Orthogonal.
  3. Đặt thuộc tính Size thành 19. Lúc này sàn đấu sẽ lấp đầy màn hình nhìn cực kỳ cân đối.

💡 Mẹo hay
Nếu bạn thấy bóng đổ mặt trời bị mờ khi dùng camera Orthogonal, hãy giảm thuộc tính **Far** của `Camera3D` xuống khoảng `100` trong Inspector.

Chạy thử nghiệm!

Nhấn phím F6 (hoặc Cmd + R trên Mac) để chạy scene hiện tại (main.tscn): - Sử dụng các phím mũi tên hoặc W, A, S, D để di chuyển nhân vật. - Bạn sẽ thấy nhân vật xoay mặt mượt mà theo 8 hướng và di chuyển linh hoạt trên sàn đấu mà không bị rơi xuyên sàn!