Thứ Hai, 15 tháng 2, 2021

Delegate, Event, là gì? Ứng dụng trong lập trình game như thế nào?

 

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

Trong 1 game gồm player và các enemy. 
Khi player Kill 1 enemy => phía UI sẽ tăng lên 1 đơn vị, mình thấy đại đa số các bạn sẽ GetComponent<X> (với X là component sẽ render ra UI) trực tiếp trong method Kill():

   void Kill()
    {
       ScoreDisplay score = GameObject.Find("ScoreText").GetComponent<ScoreDisplay>();
       score.Update();
    }


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ị


    void UseItem()
    {
       Item item = GameObject.Find("ScoreText").GetComponent<Item>();
       item.Update();
    }

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
Như vậy sơ lược của Observer Pattern là như thế, vậy các thực hiện nó như thế nào, ta phải sẽ bắt đầu tìm hiểu về Delegate và Event.

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

Sau khi đã biết delegate, event là một khái niệm khá dễ hiểu, mình sẽ tóm lược như sau:
  • 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();  
    }
 
}

Ta có class Testing chứa event example (là 1 instance của delegate) và có thể gọi invoke trong nội bộ của class đó

Ta có đoạn code mẫu duối đây
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
 
    }
 
}
Tại class Program, gọi đến testing và subscribe cho example các method ở ví dụ trước, tuy nhiên ta không thể gọi ?.Invoke tại class nà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 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<intstring> 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.

Share:

0 nhận xét:

Đăng nhận xét