09. Hoạt ảnh nhân vật (Character animation)
Trong bài học này, chúng ta sẽ sử dụng công cụ diễn hoạt có sẵn của Godot là AnimationPlayer để tạo hiệu ứng nhún nhảy (floating/flapping) sinh động cho nhân vật người chơi và quái vật. Bạn sẽ học cách thiết kế animation trực quan trên giao diện và điều khiển tốc độ diễn hoạt bằng mã nguồn.
Làm quen với bảng Animation Editor
Godot tích hợp sẵn công cụ diễn hoạt rất mạnh mẽ ngay trong trình soạn thảo:
- Mở scene
player.tscn. - Chọn node gốc
Player, nhấnCtrl + Athêm một node conAnimationPlayer.

- Bảng điều khiển Animation sẽ tự động xuất hiện ở thanh panel phía dưới màn hình.
- Nhấp vào menu Animation trên thanh công cụ của bảng này -> chọn New.

- Đặt tên hoạt ảnh là
float(nhún nhảy).


Thiết lập lặp lại và tự động phát:
- Bật biểu tượng mũi tên lặp lại (Animation looping) để hoạt ảnh tự lặp vô tận.
- Bật biểu tượng chữ A có hình chữ nhật (Autoplay on Load) để hoạt ảnh tự động chạy ngay khi game mở.


- Đặt thời lượng hoạt ảnh (Animation length) ở góc trên bên phải thành
1.2giây.
![]()

Thiết kế hoạt ảnh "float"
Chúng ta sẽ tạo các điểm mốc thời gian (Keyframes) cho hai thuộc tính: Position (Vị trí) và Rotation (Góc xoay) của node Character (chứa mô hình 3D):
Các bước đặt Keyframe:
- Chọn node
Charactertrong cây scene. - Trong Inspector, mở rộng mục Transform. Bạn sẽ thấy các biểu tượng chiếc chìa khóa nhỏ bên cạnh Position và Rotation.


- Nhấp vào chìa khóa bên cạnh Position và Rotation để tạo keyframe đầu tiên tại
0giây (nếu có hộp thoại hỏi tạo RESET track, hãy bấm Create).


- Kéo biểu tượng hình thoi của Position đến mốc
0.3giây và của Rotation đến mốc0.1giây.

- Di chuyển con trỏ thời gian đến mốc
0.5giây:

- Đổi Position -> Y thành
0.65mét. - Đổi Rotation -> X thành
8độ. - Bấm vào biểu tượng chìa khóa cạnh cả hai thuộc tính để chèn keyframe.
- Kéo keyframe Position vừa tạo dời sang mốc
0.7giây trên dòng thời gian. 6. Di chuyển con trỏ thời gian đến cuối hoạt ảnh tại1.2giây: - Đổi Position -> Y thành
0.35mét. - Đổi Rotation -> X thành
-9độ. - Bấm chìa khóa để chèn keyframe cho cả hai.




Nhấn nút Play (hoặc phím tắt Shift + D) để xem trước chuyển động.

Tinh chỉnh đường cong chuyển động (Easing Curves)
Chuyển động mặc định là chuyển động thẳng đều (linear) nên trông khá cơ học và đơ cứng. Chúng ta sẽ chỉnh đường cong chuyển tiếp: 1. Dùng chuột quét chọn 2 keyframe đầu tiên trên dòng thời gian.

- Trong Inspector, tìm mục Easing:

kéo đường cong cong sang trái (Ease-out) để chuyển động bật nhanh lúc đầu và hãm chậm dần.

- Chỉnh Ease-out tương tự cho keyframe thứ hai của Rotation, và kéo Ease-in sang phải cho keyframe thứ hai của Position. Chuyển động nhún nhảy lúc này sẽ cực kỳ mềm mại và đàn hồi.



Điều khiển hoạt ảnh bằng mã nguồn trong player.gd
Chúng ta muốn khi người chơi đứng yên thì nhún nhảy thong thả, còn khi chạy thì đập cánh nhanh gấp 4 lần. Đồng thời, khi nhảy lên không trung, nhân vật sẽ hơi chúi người về phía trước theo quỹ đạo hình vòng cung.
Mở script player.gd và cập nhật hàm _physics_process():
![]()
# Tăng tốc độ animation khi di chuyển
if direction != Vector3.ZERO:
direction = direction.normalized()
$Pivot.basis = Basis.looking_at(direction)
$AnimationPlayer.speed_scale = 4
else:
$AnimationPlayer.speed_scale = 1
# ... (các đoạn code tính vận tốc và va chạm giữ nguyên) ...
# Nghiêng người theo góc nảy khi nhảy (đặt ở cuối hàm)
$Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse
Tái sử dụng hoạt ảnh cho quái vật (Mob)
Vì cả Player và Mob đều có cùng cấu trúc phân tầng (Pivot -> Character), chúng ta có thể sao chép nguyên vẹn animation float sang cho Mob:
- Trong scene
player.tscn, chọn nodeAnimationPlayer. - Trên thanh menu Animation, chọn Manage Animations... -> nhấp vào biểu tượng Copy animation to clipboard (hai hình vuông nhỏ) bên cạnh animation
float. Nhấn OK. - Mở scene
mob.tscn. Thêm một node conAnimationPlayervàoMob. - Trong Animation menu, chọn Manage Animations... -> chọn New Library (để trống tên thư viện và bấm OK) -> nhấn vào biểu tượng Paste (bảng kẹp giấy). Nhấn OK.
- Đảm bảo bật cả biểu tượng Autoplay và Loop cho animation trong
mob.tscn.
Đồng bộ tốc độ vỗ cánh với tốc độ bay của Quái vật
Mỗi con quái vật có một tốc độ ngẫu nhiên khác nhau. Những con bay nhanh sẽ vỗ cánh dồn dập hơn những con bay chậm.
Mở script mob.gd, thêm dòng sau vào cuối hàm initialize():
func initialize(start_position, player_position):
# ... (code trước đó giữ nguyên) ...
# Tốc độ animation tỷ lệ thuận với tốc độ di chuyển
$AnimationPlayer.speed_scale = random_speed / min_speed
Mã nguồn hoàn chỉnh của Player (player.gd)
extends CharacterBody3D
signal hit
@export var speed = 14
@export var fall_acceleration = 75
@export var jump_impulse = 20
@export var bounce_impulse = 16
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.basis = Basis.looking_at(direction)
$AnimationPlayer.speed_scale = 4
else:
$AnimationPlayer.speed_scale = 1
# Vận tốc mặt đất
target_velocity.x = direction.x * speed
target_velocity.z = direction.z * speed
# Trọng lực
if not is_on_floor():
target_velocity.y = target_velocity.y - (fall_acceleration * delta)
# Nhảy
if is_on_floor() and Input.is_action_just_pressed("jump"):
target_velocity.y = jump_impulse
# Dẫm bẹp quái vật
for index in range(get_slide_collision_count()):
var collision = get_slide_collision(index)
if collision.get_collider() == null:
continue
if collision.get_collider().is_in_group("mob"):
var mob = collision.get_collider()
if Vector3.UP.dot(collision.get_normal()) > 0.1:
mob.squash()
target_velocity.y = bounce_impulse
break
velocity = target_velocity
move_and_slide()
# Tạo độ nghiêng khi nhảy
$Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse
func die():
hit.emit()
queue_free()
func _on_mob_detector_body_entered(_body):
die()
Mã nguồn hoàn chỉnh của Mob (mob.gd)
extends CharacterBody3D
signal squashed
@export var min_speed = 10
@export var max_speed = 18
func _physics_process(_delta):
move_and_slide()
func initialize(start_position, player_position):
look_at_from_position(start_position, player_position, Vector3.UP)
rotate_y(randf_range(-PI / 4, PI / 4))
var random_speed = randi_range(min_speed, max_speed)
velocity = Vector3.FORWARD * random_speed
velocity = velocity.rotated(Vector3.UP, rotation.y)
# Đồng bộ tốc độ vỗ cánh
$AnimationPlayer.speed_scale = random_speed / min_speed
func squash():
squashed.emit()
queue_free()
func _on_visible_on_screen_notifier_3d_screen_exited():
queue_free()
Chúc mừng bạn đã hoàn thành game 3D đầu tiên!
Bấm F5 để thưởng thức thành quả: - Các nhân vật nhấp nhô sống động trên sân đấu 3D. - Nhân vật nghiêng người uyển chuyển mỗi khi bật nhảy và vỗ cánh nhanh thoăn thoắt khi chạy nước rút. - Toàn bộ cơ chế nhảy, đè bẹp kẻ thù, âm thanh nền, tính điểm và chơi lại đều vận hành trơn tru!