08. Điểm số và chơi lại (Score and replay)
Trong bài học này, chúng ta sẽ xây dựng giao diện người dùng (UI) tối giản để hiển thị điểm số, thêm nhạc nền phát liên tục và cho phép người chơi nhấn phím để bắt đầu lại màn chơi sau khi thua cuộc.
Tạo giao diện hiển thị điểm số
Chúng ta sẽ sử dụng các node UI (màu xanh lá cây) của Godot để tạo giao diện 2D đè lên màn chơi 3D:
- Mở lại scene
main.tscn. Chuyển sang màn hình làm việc 2D trên thanh công cụ trên cùng. - Chọn node gốc
Main, thêm một node conControlvà đổi tên thànhUserInterface. - Chọn
UserInterface, thêm một node conLabelvà đổi tên thànhScoreLabel.

- Trong bảng Inspector của
ScoreLabel: - Nhập nội dung tạm thời vào thuộc tính Text:Score: 0.

- Chữ mặc định có màu trắng trùng với màu nền sáng, ta kéo xuống mục Theme Overrides -> Colors -> tích chọn ô Font Color và chọn màu đen để chữ tương phản rõ nét.

- Dùng chuột kéo
ScoreLabelra xa góc trên bên trái một khoảng nhỏ để không bị dính sát mép màn hình.

Thiết lập Theme và Font chữ dùng chung
Node UserInterface giúp gom nhóm toàn bộ các thành phần UI và truyền tài nguyên Theme xuống tất cả các node con:
- Chọn node
UserInterface. - Trong Inspector, tại mục Theme -> chọn New Theme.

- Nhấp vào Theme vừa tạo để mở bảng chỉnh sửa Theme ở phía dưới màn hình:
- Trong bảng FileSystem, mở thư mục
fonts/. - Kéo file phông chữMontserrat-Medium.ttfthả vào ô Default Font.

- Đặt Default Font Size thành
22px để chữ to rõ và sắc nét.

Lập trình cập nhật điểm số
Gắn Script vào ScoreLabel
- Nhấp chuột phải vào node
ScoreLabel, chọn Attach Script (score_label.gd). - Viết mã lưu trữ điểm và cập nhật chuỗi hiển thị:
extends Label
var score = 0
func _on_mob_squashed():
score += 1
text = "Score: %s" % score
Kết nối tín hiệu từ mã nguồn trong Main
Mỗi khi một con quái vật bị dẫm bẹp, nó sẽ phát tín hiệu squashed. Vì quái vật được sinh ra động từ code trong lúc chơi, chúng ta không thể kéo thả kết nối tín hiệu từ editor, mà phải kết nối bằng code trong hàm _on_mob_timer_timeout() của main.gd.
Mở script main.gd và thêm dòng kết nối tín hiệu vào cuối hàm _on_mob_timer_timeout():
func _on_mob_timer_timeout():
# ... (các dòng code sinh quái vật trước đó) ...
# Kết nối tín hiệu squashed của mob với ScoreLabel để cộng điểm
mob.squashed.connect($UserInterface/ScoreLabel._on_mob_squashed.bind())
Tạo màn hình chơi lại (Retry Overlay)
Khi người chơi hy sinh, chúng ta sẽ làm tối màn hình và hiển thị thông báo "Press Enter to retry":
- Trong
main.tscn, chọn nodeUserInterface, thêm một node conColorRectvà đổi tên thànhRetry. Node này vẽ một hình chữ nhật phẳng dùng làm màn che. - Để khung che phủ kín toàn bộ màn hình game:
- Chọn
UserInterface, trên thanh công cụ chọn Anchor Preset -> chọn Full Rect.



- Chọn
Retry, trên thanh công cụ chọn Anchor Preset -> chọn Full Rect. 3. Trong Inspector củaRetry, nhấp vào thuộc tính Color: - Chọn màu đen.
- Kéo thanh trượt A (Alpha - độ trong suốt) sang trái khoảng một nửa để tạo hiệu ứng màn che mờ ảo nhìn xuyên qua được sàn đấu.

- Chọn node
Retry, thêm một node conLabel. - Đặt thuộc tính Text:Press Enter to retry.- Trên thanh công cụ, chọn Anchor Preset -> Center để đưa dòng chữ vào chính giữa màn hình.

Lập trình cơ chế Chơi lại trong main.gd
Mở script main.gd:
1. Trong _ready(), ẩn màn hình Retry đi khi game mới mở.
2. Trong _on_player_hit(), hiển thị màn hình Retry lên khi người chơi chết.
3. Sử dụng hàm xử lý phím bấm toàn cục _unhandled_input() để nhận phím Enter:
func _ready():
$UserInterface/Retry.hide()
func _on_player_hit():
$MobTimer.stop()
$UserInterface/Retry.show()
func _unhandled_input(event):
if event.is_action_pressed("ui_accept") and $UserInterface/Retry.visible:
# Khởi động lại toàn bộ scene hiện tại
get_tree().reload_current_scene()
Hành động ui_accept là action có sẵn của Godot tương ứng với phím Enter và Space. Khi người chơi nhấn Enter trong lúc màn hình Retry đang hiện, hàm get_tree().reload_current_scene() sẽ tải lại màn chơi từ đầu.
Thêm nhạc nền phát liên tục với Autoload
Nếu bạn đặt nhạc trực tiếp trong scene main.tscn, mỗi khi màn chơi reload lại thì bản nhạc cũng sẽ bị cắt ngang và phát lại từ đầu. Tính năng Autoload (Singleton) của Godot cho phép tải một node độc lập nằm ngoài cây scene chính, giúp nhạc phát liên tục không bị gián đoạn.
1. Tạo scene MusicPlayer
- Tạo một scene mới (Scene -> New Scene).

- Nhấn nút Other Node, chọn node
AudioStreamPlayervà đổi tên thànhMusicPlayer.

- Trong bảng FileSystem, mở thư mục
art/, kéo file nhạcHouse In a Forest Loop.oggthả vào ô thuộc tính Stream trong Inspector. - Tích chọn ô Autoplay để nhạc tự động phát.

- Lưu scene lại với tên
music_player.tscn.
2. Đăng ký Autoload trong Project Settings
- Vào menu Project -> Project Settings...
- Chuyển sang tab Globals -> chọn mục con Autoload.
- Tại ô Path, bấm vào biểu tượng thư mục và chọn file
music_player.tscn. - Nhấn nút Add.

- Node
MusicPlayersẽ xuất hiện trong danh sách autoload và tự động chạy ngầm trong mọi màn chơi.


Toàn bộ mã nguồn hoàn chỉnh của Main (main.gd)
extends Node
@export var mob_scene: PackedScene
func _ready():
$UserInterface/Retry.hide()
func _on_mob_timer_timeout():
var mob = mob_scene.instantiate()
var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
mob_spawn_location.progress_ratio = randf()
var player_position = $Player.position
mob.initialize(mob_spawn_location.position, player_position)
add_child(mob)
# Kết nối tín hiệu đè bẹp quái vật với ScoreLabel
mob.squashed.connect($UserInterface/ScoreLabel._on_mob_squashed.bind())
func _on_player_hit():
$MobTimer.stop()
$UserInterface/Retry.show()
func _unhandled_input(event):
# Khởi động lại màn chơi khi nhấn Enter
if event.is_action_pressed("ui_accept") and $UserInterface/Retry.visible:
get_tree().reload_current_scene()
Kiểm thử
Nhấn F5 để trải nghiệm: - Nhạc nền sẽ vang lên du dương và không hề bị ngắt quãng. - Nhảy đè bẹp mỗi con quái vật sẽ làm số điểm trên góc màn hình tăng thêm 1. - Khi bị quái vật húc trúng, màn hình sẽ mờ đi và dòng chữ "Press Enter to retry" xuất hiện. - Nhấn Enter (hoặc Space) để lập tức làm lại ván mới!