Trước khi đi vào các khái niệm về delegate và event. Ta cần phải hiểu một loại design pattern ứng dụng mạnh mẽ delegate và event. Đó là Observer Design Pattern.
Mình sẽ nói chi tiết về Observer Design Pattern về một bài riêng, trong phạm vị bài này mình sẽ nói sơ lược qua nó và cách ứng dụng delegate và event để tạo ra các Observer như thế nào.
Vấn đề đặt ra
Tiếp tục, Khi player dùng 1 vật phẩm nào đó, sẽ gọi đến UI chứa vật phẩm đó để giảm số lượng nó đi 1 đơn vị
Tượng tượng khi dùng item thì mình sẽ play một audio nào đó => lại tiếp tục Getcomponent đén audio và play.
Sau một hồi đặt ra ví dụ, chúng ta thấy càng scale thì code của chúng ta sẽ phát sinh một số thứ sau đây:
- Spaghetti code (code rối rắm)
- Tạo ra các depedency cứng
- Highly coupled code
- Mất 1 phần tử nào đó => break cả game
- 1 thay đổi nhỏ => break cả game
Observer Design Pattern sinh ra để giải quyết vấn đề này
Observer Pattern là gì?
Một object (người thông báo) sẽ gửi broadcast messenger đến một hoặc nhiều object khác được lựa chọn (subscribe) và phải "do something" dựa trên những thứ nó đã đăng kí.
Giải thích ý tưởng trên là thế này: chúng ta sẽ tạo 1 biến static là người thông báo và người thông báo này có trách nhiệm sẽ gửi messenger cho toàn bộ những người lắng nghe nó, và những người lắng nghe nó sẽ phải thực hiện 1 hành động nào đó khi có messenger gửi tới. Chúng ta có delegate để thực hiện điều này.
Lợi ích của Observer Pattern
- Dễ dàng giao tiếp giữa các object
- Dễ dàng chia sẽ thông tin
- Phân tách object và code dễ dàng, tránh bị couple
- Loại bỏ các dependency không mong muốn
Delegates
Nếu bạn đã học qua c++ thì chả lạ gì khái niệm con trỏ hàm, delegates chính là khái niệm tương tự trong c#. Các biến tạo ra từ delegate có thể tham chiếu đến các phương thức cùng kiểu với delegate, ngoài ra có thể truyền vào function như một callback
//Khai báo định nghĩa các delegate public delegate void VoidDelegate(); public delegate int IntDelegate(); public delegate void DelegateParameter(int a, int b); static void PrintInt() => Console.WriteLine("Int"); static int ReturnInt() { return 5; } static void PrintDoubleInt(int a, int b) => Console.WriteLine("{0} - {1}", a, b); static void Main(string[] args) { //Tạo ra instance của delegate VoidDelegate myDelegate = PrintInt; DelegateParameter delegateParameter = PrintDoubleInt; IntDelegate intDelegate = ReturnInt; intDelegate = PrintInt; //Error }
Xem đoạn code trên, để gán bằng một delegate thì method phải cùng kiểu với delegate đó
Và để thực thi delegate ta có method Invoke.
Invoke sẽ thực thi tất cả Subscriber (định nghĩa ở bên dưới) có trong delegate đó
Console.WriteLine(intDelegate?.Invoke()); // Output: 5 myDelegate?.Invoke(); // Ouput: Int
Dấu ? trước delegate là kiểm tra xem nó có phải null không, nếu nó là null thì không invoke. Nếu bạn bỏ qua nó thì nó sẽ throw exception.
Nhiêu đây vẫn chưa thể hiện sức mạnh sủa delegates, sức mạnh của delegate nằm ở chỗ khả năng multiple functions assigned, có thể += nhiều function và invoke cùng lúc.
+= : tăng thêm 1 subscribe cho delegate đó
-= : giảm đi 1 subscribe cho delegate đó
các đối tượng subscribe phải cùng kiểu định nghĩa với delegate
Ta có ví dụ sau:
//Khai báo định nghĩa các delegate public delegate void VoidDelegate(); static void PrintInt() => Console.WriteLine("Int"); static void PrintString() => Console.WriteLine("String"); static void PrintTanDepTrai() => Console.WriteLine("Tan dep trai vai l*n"); static void Main(string[] args) { //Tạo ra instance của delegate VoidDelegate myDelegate = null; myDelegate += PrintInt; // Add subscriber myDelegate += PrintString; myDelegate += PrintTanDepTrai; myDelegate?.Invoke(); //Output là cả 3 function trên Console.WriteLine("_________"); myDelegate -= PrintInt; myDelegate?.Invoke(); // Output mất đi PrintInt }
Event
- Về bản chất Event là một delegate
- Khi khởi tạo 1 event, thực chất chỉ là khởi tạo một instance delegate
- Mọi thao tác đều giống delegate
- Ngăn chặn overwriten (gán bằng) một function
- Ngăn chặn public Invoking, tức là bên ngoài class không thể gọi ?.Invoke
public class Testing { public delegate void DelegateTemplate(); public static event DelegateTemplate example; void Test() { example?.Invoke(); } }
class Program { static void PrintInt() => Console.WriteLine("Int"); static void PrintString() => Console.WriteLine("String"); static void PrintTanDepTrai() => Console.WriteLine("Tan dep trai vai l*n"); static void Main(string[] args) { Testing.example += PrintInt; Testing.example += PrintString; Testing.example += PrintTanDepTrai; Testing.example?.Invoke(); // Error - không thể gọi invoke ở đây } }
Vậy thì ứng dụng trong lập trình game như thế nào?
Trong lập trình game, cụ thể là Unity - C#, mình sẽ ít dụng delegate và event lý do là nó khai báo nhiều :)), thay vào đó mình sẽ dùng Action và Func
Đừng sợ, nó chỉ là đơn giản hóa của delegate mà thôi.
Thay vì bạn phải khai báo một Delegate và tạo ra một instance của nó với 2 dòng code, thì Action làm việc đó chỉ với 1 dòng và tương tự với Func
public delegate void VoidDelegate(int a); public static VoidDelegate voidDelegate; //Same public static Action<int> voidDelegate;
or
public delegate string MyDelegate(int); public static MyDelegate myDelegate; //same Func<int, string> myDelegate;
Action: là một delegate void với các parameter
Func : là một delegate với kiểu dữ liệu giống với parameter cuối cùng
Lưu ý: Do Func có giá trị trả về => giá trị của nó sẽ là method cuối cùng được invoke
Oke, xong cơ bản về Func và Action. Như những gì mình đã nêu ở mục đặt vấn đề ta thường dùng để giao tiếp giữa gameplay và UI ví dụ như tăng điểm số, trừ máu, mana point, ...
Mình sẽ demo nhỏ một ví dụ ứng dụng vào game như sau.
Trong class GameManager ta có:
public static Action<int> e_SetScore; [Header("Game state")] public bool isPause; public int gameScore { get => _gameScore; set { _gameScore = value; e_SetScore?.Invoke(_gameScore); } }
Ta có một Action với đầu vào là một số nguyên dùng để Set Score cho UI khi player ăn điểm
Khi ăn điểm thì property gameScore++ , lúc này thực hiện e_SetScore?.Invoke()
Và nơi ta đăng kí observer cho event này sẽ là UIManager
Tại UIManager ta có:
public void SetScore(int score) { text_Score.text = score.ToString(); }
private void OnEnable() { GameManager.e_SetScore += SetScore; SetScore(0); } private void OnDisable() { GameManager.e_SetScore -= SetScore; }
Oke, thay vì cứ đặt trong update liên tục ta sẽ dùng cách này giúp ta tối ưu game performance hơn, đặc biệt khi UnActive UIManager cũng sẽ chẳng làm ảnh hướng đến Gameplay
Đây chỉ là một phần của một ứng dụng rất rất nhỏ của event và delegate trong c#, còn rất nhiều, mình thì còn hay dùng khi truyền callback cho một function nữa nhưng mà thêm vào thì bài này lại dài quá, các bạn từ tìm hiểu nhé :v
Tổng kết
Vậy ứng dụng cực kì mạnh mẽ của delegate và event giúp chúng ta giải quyết được rất nhiều vấn đề, khiến cho mã nguồn trông sạch sẽ hơn, giúp bạn lên level mới trong lập trình game.
Okay, nếu thấy hay thì nhớ share :)) có gì thắc mắc thì nhớ comment bên dưới đề mình giải đáp nhé, cảm ơn mọi người đã đọc.
0 nhận xét:
Đăng nhận xét