Tối ưu hiệu năng game Godot 4 trên Android: 5 mẹo thực tế
Godot 4 hỗ trợ Android khá tốt, nhưng nếu không tối ưu, game của bạn có thể chạy 30 FPS thay vì 60 FPS trên các thiết bị Android tầm trung. Bài viết này tổng hợp 5 kỹ thuật tối ưu thực tế mà tôi đã áp dụng cho các dự án indie cho thị trường Việt Nam.
1. Chọn renderer phù hợp với thiết bị mục tiêu
Godot 4 có ba renderer: Forward+, Mobile và Compatibility (GLES3). Chọn sai renderer là lỗi phổ biến nhất khi deploy lên Android.
| Renderer | Ưu điểm | Nhược điểm | Khuyến nghị |
|---|---|---|---|
| Forward+ | Đẹp nhất, ray tracing, GI | Yêu cầu Vulkan, RAM cao | Chỉ flagship Android (Snapdragon 8 Gen 2+) |
| Mobile | Cân bằng | Một số tính năng bị giới hạn | Thiết bị tầm trung (Snapdragon 7xx) |
| Compatibility (GLES3) | Chạy mọi thiết bị | Ít hiệu ứng | Thiết bị giá rẻ, máy cũ |
Để chuyển renderer: Project Settings > Rendering > Renderer > Rendering Method. Hoặc khi tạo project mới, chọn renderer ở dialog "Create New Project".
Nếu game của bạn nhắm đến thị trường Việt Nam phổ thông, dùng Compatibility sẽ đảm bảo chạy được trên đa số máy. Forward+ chỉ phù hợp khi target là gamer cao cấp.
2. Texture compression đúng cách
Texture không nén có thể chiếm 70% RAM của game mobile. Godot 4 hỗ trợ ETC2 và ASTC cho Android.
Cấu hình project:
Project Settings > Rendering > Textures > VRAM Compression > Import ETC2 ASTC. Bật cả hai.
Trong inspector từng texture:
Chọn texture trong FileSystem, nhìn tab Import:
Mode: VRAM CompressedLossy Quality: 0.7-0.8 cho most cases- Bấm "Reimport"
ASTC cho chất lượng tốt hơn ETC2 ở cùng kích thước file, nhưng chỉ Android 5.0+ hỗ trợ. ETC2 chạy trên Android 4.3+. Nếu game của bạn target Android 9+ (như đa số game Việt Nam hiện tại), dùng ASTC.
Một mẹo nữa: với pixel art, dùng Mode: Lossless thay vì compression — pixel art bị blur khi compress, mà file size cũng không lớn lắm.
3. Giảm draw calls bằng MultiMeshInstance
Mỗi sprite/mesh trong Godot tốn một draw call. Trên Android, mỗi draw call cost cao hơn desktop khoảng 5-10 lần. Game có 200 cây cối có thể tốn 200 draw calls mỗi frame.
Sử dụng MultiMeshInstance2D cho 2D, MultiMeshInstance3D cho 3D:
extends Node2D
@export var tree_texture: Texture2D
var multimesh: MultiMesh
func _ready() -> void:
multimesh = MultiMesh.new()
multimesh.transform_format = MultiMesh.TRANSFORM_2D
multimesh.use_colors = true
var mesh = QuadMesh.new()
mesh.size = Vector2(64, 64)
multimesh.mesh = mesh
multimesh.instance_count = 200
for i in 200:
var t = Transform2D()
t.origin = Vector2(randf_range(0, 1920), randf_range(0, 1080))
multimesh.set_instance_transform_2d(i, t)
var multimesh_instance = MultiMeshInstance2D.new()
multimesh_instance.multimesh = multimesh
multimesh_instance.texture = tree_texture
add_child(multimesh_instance)
200 sprites gộp thành 1 draw call. Tăng FPS từ 25 lên 60 trên Snapdragon 700-class chip.
4. Vật lý: tránh _physics_process không cần thiết
Mỗi node có _physics_process chạy 60 lần/giây kể cả khi không cần. Trên 100 enemy, đó là 6000 function calls/giây trên CPU mobile yếu.
Disable _physics_process khi không cần:
extends CharacterBody2D
func _ready() -> void:
set_physics_process(false) # tắt mặc định
func _on_player_entered_range() -> void:
set_physics_process(true) # bật khi player đến gần
func _on_player_left_range() -> void:
set_physics_process(false) # tắt khi player đi xa
Dùng VisibleOnScreenNotifier2D để tự động bật/tắt khi enemy vào/ra khỏi camera:
@onready var notifier: VisibleOnScreenNotifier2D = $VisibleOnScreenNotifier2D
func _ready() -> void:
notifier.screen_entered.connect(_on_screen_entered)
notifier.screen_exited.connect(_on_screen_exited)
set_physics_process(false)
func _on_screen_entered() -> void:
set_physics_process(true)
func _on_screen_exited() -> void:
set_physics_process(false)
Một mẹo nữa: _physics_process mặc định chạy 60Hz, nhưng game mobile có thể chạy ở 30Hz để tiết kiệm pin. Project Settings > Physics > Common > Physics Ticks Per Second = 30.
5. Audio: limit số lượng AudioStreamPlayer cùng lúc
Mỗi AudioStreamPlayer trên Android cost khoảng 0.2-0.5ms decode. 20 sound effects cùng lúc có thể đẩy CPU lên 50%.
Pattern pool đơn giản:
extends Node
const POOL_SIZE = 8
var _players: Array[AudioStreamPlayer] = []
var _next_index: int = 0
func _ready() -> void:
for i in POOL_SIZE:
var player = AudioStreamPlayer.new()
add_child(player)
_players.append(player)
func play(stream: AudioStream) -> void:
var player = _players[_next_index]
_next_index = (_next_index + 1) % POOL_SIZE
player.stream = stream
player.play()
Pool size 8-16 là đủ cho hầu hết game indie. Nếu cần nhiều âm thanh đồng thời (như game bắn súng), pool size 32 cũng được nhưng kiểm tra performance.
Mẹo iOS Safari (cho web build):
iOS không cho phép audio tự phát trước khi user tap. Nếu game của bạn export ra web (HTML5), hiển thị một button "Bắt đầu game" và chỉ start audio sau khi user tap.
Profiling: cách đo trước khi tối ưu
Đừng tối ưu mù. Dùng Godot Profiler trên Android device thật:
- Build game ở chế độ debug:
Project > Export > Android > Debug Build = ON. - Cắm Android vào USB, bật USB debugging.
- Trong editor, bấm
Remote Debug > Run on Device > [tên thiết bị]. - Mở
Debugger > Profilervà bấm Start. - Chơi game, xem các function nào tốn thời gian nhất.
Profiler sẽ chỉ ra chính xác bottleneck. Đa số bottlenecks là:
_processcủa UI nodes (vẽ HUD mỗi frame)- Pathfinding chạy quá thường xuyên
- Texture không compress
- Quá nhiều physics body
Kết luận
5 bước cơ bản:
- Chọn renderer phù hợp với thiết bị mục tiêu
- Bật texture compression (ETC2 hoặc ASTC)
- Dùng MultiMesh cho 200+ sprites cùng loại
- Disable
_physics_processkhi không cần - Pool AudioStreamPlayer thay vì instantiate
Game indie Việt Nam thường target Android tầm trung (Snapdragon 600-700 class). Nếu áp dụng đúng 5 mẹo này, game 2D có thể chạy 60 FPS ổn định trên những thiết bị phổ thông như Redmi Note 11 hay Galaxy A23.
Tài liệu chính thức về tối ưu Godot Android cũng có nhiều thông tin chi tiết. Nếu game của bạn vẫn chậm sau khi áp dụng những kỹ thuật này, có thể vấn đề ở rendering pipeline hoặc design game level chứ không phải code.
All rights reserved