Delegate và Event trong Unity (phần 1)

Sử dụng Delegate và Event trong Unity (phần 1)

 

     Trong bài viết này mình sẽ mô tả cách sử dụng Delegate và Event trong Unity. Trước hết chúng ta xem qua định nghĩa xem chúng là gì đã.

     

     Delegate: theo như định nghĩa trên trang web www.tutorialspoint.com chuyên mục về C#, thì delegate trong C# có chức năng tương tự như các con trỏ và trỏ tới các hàm như trong C và C++. Một delegate là 1 biến có kiểu tham chiếu giữ tham chiếu đến 1 phương thức nào đó, và tham chiếu có thể được thay đổi trong khi thực hiện chương trình (tức runtime). Các delegate đặc biệt được sử dụng trong việc thực thi các sự kiện (Event) và các phương thức gọi lại (tức call-back), nếu ai chưa hiểu call-back là gì thì đại ý nó là sau khi chúng ta thực hiện xong 1 hàm nào đấy như hàm tính toán chẳng hạn thì 1 cái hàm nào đó khác sẽ được thực thi sau khi cái hàm đầu tiên thực thi xong, ví dụ đó có thể là hàm thực hiện đưa ra thông báo kết quả của hàm tính toán vừa rồi là thành công hay không thành công, nếu thành công thì nó làm sao…v…v…, thì cái hàm đưa ra thông báo đó được gọi là hàm call-back. Tất cả delegate hoàn toàn được dẫn xuất từ lớp System.Delegate.

 

     Event: cũng theo định nghĩa trên www.tutorialspoint.com chuyên mục C#, thì event là các hành động (sự kiện xảy ra) của người dùng như ấn một phím, 1 cú click hay di chuyển chuột trên màn hình hoặc 1 thông báo nào đó do hệ thống tạo ra, các sự kiện này được sử dụng để liên lạc giữa các quá trình chương trình hoạt động.

 

     => Tóm lại: cả delegate và event đều giữ vai trò quan trọng trong chương trình và được sử dụng trong mẫu lập trình “Observer pattern“. Sự khác nhau cơ bản giữa chúng đó là delegate giữ tham chiếu đến 1 phương thức còn event cung cấp cách truy cập đến phương thức đó sử dụng delegate.

 

     Lý thuyết dài dòng là vậy nhưng khi bắt tay vào làm việc cùng delegate và event có thể chúng ta sẽ thấy rằng 2 thằng này có chức năng hoạt động tương tự như nhau, chúng ta có thể định nghĩa 1 delegate, khởi tạo nó rồi gắn tham chiếu đến phương thức để nó thực thi, hoặc tạo ra 1 event từ delegate đó rồi gắn phương thức nào đó đến event đó để nó thực thi. Xem ví dụ:

/********************* Chỉ sử dụng delegate ********************/
...
     // Định nghĩa 1 delegate trả về kiểu void với 0 tham số
     public delegate void PressKeyDown();
     ...

     // Đâu đó trong chương trình ta sẽ gắn 1 phương thức đến delegate này
     // Có nhiều cách để gắn, ta sẽ đề cập đến sau, đây là 1 cách
     PressKeyDown pkd = IncreaseInt;
     ...

     // Kích hoạt phương thức bằng cách nhấn 1 phím
     if (Input.GetKeyDown(KeyCode.Space))
     {
          pkd();
     }
     ...

     // Đây là phương thức ta gắn delegate PressKeyDown đến
     private void IncreaseInt()
     {
          Debug.Log("Pressed Key - Space");
     }
...

/********************* Sử dụng event ********************/
...
     // Định nghĩa 1 delegate trả về kiểu void với 0 tham số 
     public delegate void PressKeyDown();
     // Định nghĩa 1 event với kiểu delegate là PressKeyDown
     public static event PressKeyDown onPressKeyDown;
     ...
     
     // Đâu đó trong chương trình ta gắn 1 phương thức - gọi là đăng ký sự kiện
     onPressKeyDown += Dosomething;
     ...

     // Kích hoạt phương thức bằng cách nhấn 1 phím 
     if (Input.GetKeyDown(KeyCode.Space)) 
     { 
          onPressKeyDown();
     }
     ...

     // Đây là phương thức ta gắn delegate PressKeyDown đến 
     private void Dosomething() 
     { 
          Debug.Log("Pressed Key - Space"); 
     }
...

 

     Cả 2 cách thức sử dụng delegate hay event đều sẽ cho ra kết quả là chuỗi “Pressed Key – Space“. Vậy câu hỏi là tại sao lại phải dùng event trong khi delegate có thể làm chức năng giống hệt thế? Chà, đúng là chức năng giống nhưng delegate lại gặp phải 1 vấn đề, đó là nó có thể dễ dàng bị ghi đè (override) lên các thuộc tính của nó, điều này có thể sẽ dẫn đến các lỗi nghiêm trọng và các vấn đề khác mà chúng ta không muốn phải tìm hiểu 🤔. Ví dụ sau khi gắn các phương thức chán chê vào delegate rồi, sau đó ở trong 1 script nào đó khác ta ngứa tay và làm thêm quả SomeDelegate = null, thế là nó xóa tất đống phương thức được thêm vào lúc trước.

 

     Do đó để tránh vấn đề vừa xong, C# đã sử dụng Event, như 1 lớp bao quanh các delegate. Không thể khởi tạo 1 thể hiện của event bằng cách SomeEvent = new SomeEvent(); và 1 event không thể được đặt bằng null như này SomeEvent = null vì event  chỉ cho phép sử dụng toán tử +=-= ở vế phải của nó, tức đăng ký hoặc hủy đăng ký 1 phương thức nào đó. Như vậy event đã giải quyết được vấn đề của delegate.

     

     Rồi, đã xong phần định nghĩa, giờ chúng ta trở lại chủ đề chính, sử dụng chúng thế nào trong Unity. Trong Unity chúng ta có thể làm việc bằng cách tham chiếu giữa các thành phần (component) với nhau, ví dụ ta có thể tạo ra 1 gameObject trên scene rồi gắn cho nó script là GameManager, trong GameManager có thể có hàm xử lý sự kiện người chơi đạt điểm, sau đó trong 1 gameObject tên Player ta viết đoạn code tham chiếu đến script GameManager kia, có thể là dùng code để lấy nó hoặc kéo thả nó trên trình duyệt, sau đó trong script của Player ta viết gì đó đại loại như …GameManager.Instance.Setscore(9999)… như vậy là ta đã tham chiếu đến script GameManager từ script của Player để thực hiện 1 sự kiện nào đó. Cách này có tốt hay không, ưu điểm nhược điểm thế nào thì trong bài viết này chúng ta không quan tâm 🤣.

 

     Chúng ta có thể sử dụng 1 cách khác để thực hiện các sự kiện xảy ra trong game, đó là sử dụng delegate cùng event. Ý tưởng cơ bản của phương pháp này (hay ít nhất thì đó là của mình 🤭) đó là chúng ta sẽ tạo ra 1 thằng chuyên phân phối các sự kiện, khi 1 sự kiện được tạo ra thằng đó sẽ hét ầm lên “ê, có 1 sự kiện như này này, ai đăng ký thực thi nó giơ tay”, thằng nào cảm thấy mình chơi được sự kiện đó sẽ gào lên “em em em anh ơi, để cho em”, (đó gọi là đăng ký sự kiện) sau đó khi đến thời điểm thích hợp kích hoạt sự kiện thì thằng phân phối chỉ việc thông báo cho thằng đăng ký biết để mà làm.

 

     Code thôi anh em. Mở Unity lên, tạo 1 cái project đơn giản, tạo 1 thằng gameObject empty ra đặt tên là Player với 1 script Player.cs gắn vào nó, tạo thêm 1 thằng gameObject khác tên là Manager (tên là không quan trọng, demo thôi), gắn cho nó cái script tên Manager.cs, cuối cùng tạo 1 cái script tên EventDispatcher.cs không cần gắn vào thằng nào cả (đây chính là cái bảng phân phối và thông báo sự kiện của chúng ta). 

 

     Mở EventDispatcher.cs lên, xóa cái kế thừa MonoBehaviour đi vì chúng ta không cần thiết nó phải nằm ở trên scene. Sau đó ta sẽ tạo ra 1 delegate kiểu trả về là void với 0 tham số và 1 event từ delegate này. Cuối cùng là 1 hàm để thông báo cho kẻ đã đăng ký sự kiện rằng hãy thực thi sự kiện đó.

public class EventDispatcher 
{
     public delegate void PressKeyDown();
     public static event PressKeyDown onPressKeyDown;  // Định nghĩa sự kiện nhấn phím
     
     // Hàm yêu cầu thực thi sự kiện nhấn phím
     public static void ExecutePressKeyDownEvent()
     {
          onPressKeyDown?.Invoke();
          // Bên trên là dạng viết tắt của if (onPressKeyDown != null) { onPressKeyDown(); }
     }
}

     

     Tiếp theo mở Manager.cs lên, viết code để đăng ký xử lý sự kiện nhấn phím.

public class Manager : MonoBehaviour
{
     private void OnEnable()
     {
          // Đăng ký sự kiện
          EventDispatcher.onPressKeyDown += DosomethingWithoutParams;
     }

     private void OnDisable()
     {
          // Hủy đăng ký sự kiện để giải phóng bộ nhớ
          EventDispatcher.onPressKeyDown -= DosomethingWithoutParams;
     }
     
     // Sự kiện khi ta nhấn phím
     private void DosomethingWithoutParams()
     {
          Debug.Log("Pressed Key: Space");
     }
}

     

     Cuối cùng, mở Player.cs lên, viết code để thực hiện hành động nhấn phím.

public class Player : MonoBehaviour
{
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            EventDispatcher.ExecutePressKeyDownEvent();
        }
    }
}

     

     Bấm Play trên editor và check thử, ta có thể thấy là Player chỉ cần thông báo xử lý việc nhấn phím Space, còn nó được xử lý thế nào thằng khác tự lo, Player không cần phải tham chiếu trực tiếp đến script Manager nữa, rất tiện và có vẻ khá là trực quan đúng không. Nhớ rằng chúng ta luôn có thể đăng ký sự kiện nhiều lần sử dụng toán tử += . Ví dụ trong trường hợp khi 1 enemy chết, ta có thể kích hoạt đồng thời hoạt động thông báo tăng điểm cho player lên và nhả coin ra từ xác của enemy. Trong trường hợp đó chỉ cần đăng ký liên tiếp sự kiện tại hàm OnEnable() Ví dụ như sau:

...
     private void OnEnable() 
     { 
          // Đăng ký 2 sự kiện hiển thị điểm và tạo coin khi 1 kẻ địch bị chết
          EventDispatcher.onEnemyDie += UpdateScore;
          EventDispatcher.onEnemyDie += CreateCoin;
          // Bonus :3
          EventDispatcher.onEnemyDie += MinhThichThiMinhThemThoi;
     } 
     
     private void OnDisable() 
     { 
          // Hủy tất cả sự kiện để giải phóng bộ nhớ 
          EventDispatcher.onEnemyDie -= UpdateScore; 
          EventDispatcher.onEnemyDie -= CreateCoin;
          EventDispatcher.onEnemyDie -= MinhThichThiMinhThemThoi;
     } 
     
     // Xử lý sự kiện 1 
     private void UpdateScore() { Debug.Log("Update Score!"); }

     // Xử lý sự kiện 2 
     private void CreateCoin() { Debug.Log("Create Coin!"); }

     // Xử lý sự kiện 3
     private void MinhThichThiMinhThemThoi() { Debug.Log("Mình thích thì mình thêm thôi!"); }
...

 

     Vừa rồi là chúng ta đã tự định nghĩa ra 1 delegate sau đó là dùng nó để tạo ra event với kiểu trả về là void và 0 tham số, các trường hợp delegate với các kiểu trả về và có chứa tham số cũng sẽ được tạo tương tự như cách trên. Cách thức sử dụng event để tạo mẫu lập trình Observer pattern trong Unity chỉ có như vậy, lần tới mình sẽ nói thêm 1 chút về 1 vài dạng delegate khác chúng ta có thể sử dụng trong khi lập trình với Unity, đó là những Action, Action<T1, T2…>, Func, Func<T1, T2…>, Lambda, và cuối cùng là 1 ví dụ đơn giản tạo hàm Call-back.

 

Bạn nghĩ sao về bài viết này?

[Total: 1    Average: 5/5]

 

 

 

 

One thought on “Delegate và Event trong Unity (phần 1)

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *