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.
Tạo Script cho Player
- Nhấp chuột phải vào node gốc
Playertrong sceneplayer.tscnvà chọn Attach Script (Gắn mã lệnh).

- 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).
- 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;
}
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ềtruenế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ủaCharacterBody3D, 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
- Mở lại scene
main.tscn.

- Nhấp chuột phải vào node gốc
Mainvà 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. - 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)
- Chọn node
Main, thêm một node conMarker3D, đổi tên thànhCameraPivot. - Chọn
CameraPivot, thêm một node conCamera3D.

Cấu trúc cây node lúc này:
Main
├── Ground
├── DirectionalLight3D
├── Player
└── CameraPivot (Marker3D)
└── Camera3D
3. Căn chỉnh góc nhìn Camera
- 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). - Ở khung nhìn bên dưới, chọn
Camera3Dvà 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.


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

- Chọn node cha
CameraPivot, dùng công cụ xoay (phímE) xoay quanh trục X (vòng đỏ) một góc-45độ (hoặc nhập-45và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:

- Chọn node
Camera3D. - Trong bảng Inspector, đổi thuộc tính Projection từ
PerspectivesangOrthogonal. - Đặ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.

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!