Design Pattern P1: Observer Pattern
Keyword cần lưu ý
Coupling | Là hiện tượng phụ thuộc lẫn nhau giữa các module, class, hoặc thành phần trong một hệ thống phần mềm. Coupling cao nghĩa là các thành phần phụ thuộc chặt chẽ vào nhau, và ngược lại |
Event-driven programming | Lập trình hướng sự kiện, là một kiểu kiến trúc code xây dựng xung quanh các sự kiện |
Parameter | Tham số |
Argument | Đối số |
Đặt vấn đề
Trong game, sự trao đổi thông tin giữa các thành phần của game là rất quan trọng. Hãy tưởng tượng bạn đang xây dựng 1 game bắn súng góc nhìn thứ nhất. Khi nhân vật bị trúng đạn, ta sẽ muốn thực hiện rất nhiều hành động khác nhau:
Cập nhật thanh máu của người chơi (
HealthBar.UpdateHealth(int amount)
)Hiển thị hiệu ứng hình ảnh (
VisualEffect.PlayerHurtEffect()
)Phát âm thanh báo hiệu nhân vật bị thương (
GamePlayAudio.PlaySFX()
)Ghi lại log để phục vụ việc debug (
LogSystem.WriteLog(string message)
)
Cách tiếp cận đơn giản nhất là gọi trực tiếp các hàm cập nhật, hiển thị và phát âm thanh từ class PlayerController.
using UnityEngine;
public class PlayerController: MonoBehaviour
{
public HealthBar healthBar;
public VisualEffect visualEffect;
public GameAudio gameAudio;
public LogSystem logSystem;
public void GetShot(int damage)
{
HealthBar.UpdateHealth(damage);
VisualEffect.PlayerHurtEffect();
GameAudio.PlaySFX();
LogSystem.WriteLog($"Player get shot with {damage} damage");
}
}
Tuy nhiên, ta có thể thấy PlayerController đang bị phụ thuộc vào rất nhiều class khác (HealthBar, VisualEffect, GamePlayAudio, LogSystem), nếu ta vô tình sửa lại code của những class này có thể dẫn tới việc chương trình báo lỗi cú pháp hoặc tệ hơn là lỗi logic → rất khó để debug.
Ví dụ, nếu ta thay đổi cơ chế của hàm HealthBar.UpdateHealth(int amount)
từ “Tính toán lượng máu còn lại dựa trên sát thương nhận vào“ thành “Hiển thị lượng máu hiện tại với một lượng bằng đối số được truyền vào“ → Ta sẽ gặp ngay 1 lỗi logic mà cần lượng thời gian nhất định để fix
HealthBar.UpdateHealth(int amount){}; // hàm cũ: hiển thị lượng máu bằng: máu hiện tại - amount
HealthBar.UpdateHealth(int amount){}; // hàm mới: hiển thị lượng máu bằng: amount
Hơn nữa, điều gì sẽ xảy ra nếu ta muốn thêm một hiệu ứng mới, ví dụ như rung màn hình? ta sẽ cần sửa lại code của PlayerController.cs
→ dẫn tới tăng sự phức tạp và khó bảo trì của code
using UnityEngine;
public class PlayerController: MonoBehaviour
{
//....
public CameraController cameraController;
public void GetShot(int damage)
{
//....
CameraController.ScreenShake(1f);
}
}
Mặc dù PlayerController đang bị phụ thuộc vào các class: HealthBar, VisualEffect, GameAudio, LogSystem, nhưng bản thân PlayerController thực ra không cần quan tâm hành vi của các class này là gì, điều PlayerController quan tâm là những việc xảy ra với nhân vật ở trong game. Vậy làm thế nào để ta có thể giảm sự phụ thuộc của các class với nhau, làm dự án dễ mở rộng hơn?
Có nhiều câu trả lời cho vấn đề này, và một trong số đó là triển khai Observer Pattern - một design pattern xây dựng theo kiến trúc hướng sự kiện (event-driven
)
Observer Pattern là gì? Một số khái niệm liên quan
Observer Pattern là 1 design pattern hướng sự kiện, định nghĩa mối phụ thuộc một - nhiều (one-to-many dependency
) giữa các object để khi một object có sự thay đổi trạng thái, tất các object phụ thuộc của nó sẽ được thông báo về sự thay đổi này
Khi triển khai Observer Pattern
, ta sẽ có các khái niệm sau:
Observer (hay event listener): 1 object có mong muốn nhận thông tin về một hoặc nhiều
event
cụ thể xảy ra tại 1 object khácEvent: Một sự việc xảy ra trong game - bất cứ việc gì cũng có thể được coi là 1 sự kiện (nhân vật bị thương, người chơi nhất nút
thoát game
, boss xuất hiện dạng thứ hai sau khi bị đánh bại, người chơi đi tới cuối map,...)Subject: Là object được các
Observers
“quan sát”. Khi 1event cụ thể
xảy ra trên subject, nó sẽ thông báo tới cácObservers
đang quan sát để cácObserver
đó đưa ra phản ứng phù hợp vớievent
này
Giải quyết vấn đề ở đầu bài viết với Observer Pattern
Để triển khai Observer Pattern cho vấn đề ở đầu, ta sẽ gán vai trò cho chúng để dễ nhận biết:
Observer: Các class mong muốn nhận thông tin về 1 hoặc nhiều event cụ thể: HealthBar, VisualEffect, GameAudio, LogSystem
Event: Sự việc nhân vật bị trúng đạn
Subject: Bản thân class PlayerController
B1: Ta định nghĩa sẵn 1 số event bằng kiểu enum
public enum EventType
{
OnPlayerGetShot
//,.. Một số event khác
}
B2: Xây dựng interface IEventListener đại diện cho các Observer
- OnPlayerEvent() là 1 hàm abstract (trừu tượng) đại diện cho cách các Observer sẽ phản hồi lại với event được nhận về
// Define an interface for event listeners (observers)
public interface IEventListener
{
void OnNotifyEvent(EventType eventType, object data = null);
}
B3: Xây dựng PlayerController
public class PlayerController : MonoBehaviour
{
//Danh sách các subscriber đang "lắng nghe" PlayerController
private List<IEventListener> listeners = new List<IPlayerEventListener>();
private int currentHealth = 100;
//AddListener, RemoveListener: thêm/bớt các subscriber vào/khỏi danh sách
public void AddListener(IEventListener listener)
{
listeners.Add(listener);
}
public void RemoveListener(IEventListener listener)
{
listeners.Remove(listener);
}
//Khi Player bị bắn, sẽ gửi đi event "OnPlayerGetShot" cho các subscriber biết
public void GetShot(int damage)
{
currentHealth -= damage;
NotifyListeners(EventType.OnPlayerGetShot, damage);
}
//Dùng để gửi đi một event và dữ liệu cụ thể cho các subscriber
private void NotifyListeners(EventType eventType, object data = null)
{
foreach (var listener in listeners)
{
listener.OnNotifyEvent(eventType, data);
}
}
}
B4: Xây dựng các subscriber
public class HealthBar : MonoBehaviour, IEventListener
{
private PlayerController _playerController;
private void Start()
{
_playerController = GameObject.FindWithTag("Player").GetComponent<PlayerController>();
if (_playerController != null)
{
_playerController.AddListener(this);
}
}
public void OnNotifyEvent(EventType eventType, object data)
{
if (eventType == EventType.OnPlayerGetShot)
{
int damage = (int)data;
UpdateHealth(damage); // Cập nhật thông tin hiển thị về lượng máu của nhân vật
}
}
public void UpdateHealth(int damage) { /*....*/ }
}
public class VisualEffect : MonoBehaviour, IEventListener
{
private PlayerController _playerController;
private void Start()
{
_playerController = GameObject.FindWithTag("Player").GetComponent<PlayerController>();
if (_playerController != null)
{
_playerController.AddListener(this);
}
}
public void OnNotifyEvent(EventType eventType, object data)
{
if (eventType == EventType.OnPlayerGetShot)
{
PlayerHurtEffect(); // Hiển thị hiệu ứng hình ảnh thể hiện nhân vật bị bắn
}
}
public void PlayerHurtEffect() { /*Implementation*/ }
}
Ta sẽ triển khai tương tự với các class GameAudio, LogSystem
Với cách triển khai như vậy, PlayerController sẽ không cần quan tâm các class HealthBar, VisualEffect, GameAudio, LogSystem sẽ phản hồi lại các event như thế nào, thậm chí cũng không cần quan tâm các class này có tồn tại hay không
Tài liệu tham khảo
Observer pattern — How to use this crucial design pattern in game development with Unity - Medium
Observer Design Pattern - Geeksforgeeks
Observer - RefactoringGuru