Unity/C_Basic Review

Posted by lancey10 on Thu, 29 Aug 2019 11:59:46 +0200

Reference material

[1] Unity 3D scripting using C # language to develop cross-platform games
[2]@Zhang Ziyang [Delegation and Event in C # - Part.1] http://www.tracefact.net/tech/009.html
[3]@Zhang Ziyang [Delegation and Event in C # - Part.2], http://www.tracefact.net/tech/029.html
[4]@Mao Nebula [Effective C
[5] Robert Nystrom, Game Programming Mode

Basic knowledge

  1. C# uses the delegate keyword to easily complete a mechanism similar to a callback function.

Troubleshooting

  1. What is the observer model? What are the scenarios of its application in games?
  2. The application scenario of the mediator pattern?
  3. What did the delegate keyword do for us?
  4. What is the relationship between event and delegate?

Observer model

Summary

The observer pattern is a pattern that defines one-to-many relationships between objects. When the observee changes state, all observers receive notification and respond. Sometimes this relationship can also be understood as a publish/subscribe model, in which all the objects that subscribe to the message respond when the object we care about (that is, the observer) publishes the message, thus doing something.

There is a great example in Unity3D scripting to explain this pattern, which is described in this way [1].

  • The task of newspapers and periodicals is to publish newspapers.
  • Customers can subscribe to newspapers and periodicals.
  • When a newspaper is published, all subscribers will receive it.
  • When customers do not need it, they can also cancel their subscriptions. After cancelling, they will not receive newspapers published by newspapers.
  • Newspapers and customers are two different subjects. As long as newspapers exist, different subscribers can subscribe or cancel their subscriptions.

The newspaper is what we call the Subject, and the subscriber is the Observer.

When is the observer mode used?

Design patterns are not silver bullets. All design patterns have an application scenario that is most suitable for them. The abuse of design patterns will lead to serious code redundancy, and subsequent modifications will be even more difficult. Obviously, the observer pattern should also have its best application scenario.

It seems that the explanation of introducing the observer model in game development on the Internet is not so clear at present. Here bloggers try to talk about which occasions can get good results by using the observer model combined with their own experience. Of course, up is still a beginner of C# and design mode, there may be some mistakes or omissions, if you find it, please don't hesitate to give advice, I will be extremely grateful!!!

In my opinion, the observer model is a model that transforms the relationship of A dependence on B into that of B dependence on A. So the first point of using the observer pattern, I think, should be to determine the dependencies between AB s and to determine which object, he can not tolerate the situation of multiple objects in the code.

This may be a bit abstract. Let's take a look at some concrete examples.

Application scenario 1 -- Judging AB dependencies

When a unit HP in the game drops, the bar of life on the UI should also change its width (or height, etc.) proportionally. The change of unit life value is a very common thing in the game. For example, when a unit receives damage, or even when it releases skills (call DNF Red Devil).

So, if we don't use the observer pattern, we might write the following code.

import UI.HpUI;
// Game unit object
class Character{

    // Character objects depend on the UI of hp because they are actively notified to update the blood strip width
    HpUI hpView;

    int hp;
    // Method of execution upon receiving injury
    public void Damaged(int damage){
        this.hp -= damage;
        // Active notification of hpUI updates
        hpView.update();
    }

    // Method of execution in releasing skills
    public void SkillExecute(Skill skill){
        // Some special skills need to consume HP
        if(skill == xxx){
            Damaged(xxxx);
        }
    }
}

Can we accomplish this goal or not, but we can find that on Character, the game object relies on the UI of Hp, which is particularly abrupt. If there are MP UI, character attributes (attack defense, etc.) UI, then all Character objects should be quoted.

Imagine that when you happily create a new unit and want to let him play weird, you find that the report is wrong. The reason is that there is no UI dependency added to the new unit. At this time, it can be found that every new unit object needs to add all UI dependencies to it.

In fact, whether the game object must rely on UI, it does not feel that the unit is the unit, even if there is no UI, he will be injured will withhold blood will also die, will not say that without the UI will report errors, the unit will not die (funny).

So, can you tell if it's actually more reasonable to rely on game objects for UI? Without the game objects he binds to, the UI certainly can't show a specific effect.

Here is the code after using the observer pattern.

// NoHp indicates the amount of blood after injury and damage indicates the number of injuries.
public delegate OnCharacterDamageHandler(int nowHp,int damage);

// Game unit object
class Character{
    // Delegate to send messages to all subscribers who subscribe to this event when the unit is injured
    public OnCharacterDamageHandler onDamage;

    int hp;
    // Method of execution upon receiving injury
    public void Damaged(int damage){
        this.hp -= damage;
        if(onDamage!=null)
            // Publish a message - the object is injured
            onDamage(hp,damage);
    }

    // Method of execution in releasing skills
    public void SkillExecute(Skill skill){
        // Some special skills need to consume HP
        if(skill == xxx){
            Damaged(xxxx);
        }
    }
}

class HpUI{
    // hp bar, when unit hp changes, adjust width automatically
    Image hpbar;

    // UI depends on game objects
    Character character;

    // Method of changing blood strip UI according to current hp
    private void update(int hp,int damage){...}

    // Method of UI initialization
    public void Init(){
        // Injury Events in Bloodstrip UI Subscribers
        character.onDamage += update;
    }
}

In this way, the dependency relationship changes from Character dependency on HpUI to HPUI dependency on Character. HpUI monitors Character's injuries. Is that good? Feel different opinions, or that sentence, to judge who AB objects can not tolerate to rely on other objects.

Summary of Application Scenario 1

You can see that, after the above operation, the coupling between objects does not disappear, but changes from one form (A depends on B) to another form (B depends on A).

Application Scenario 2 - Is there a one-to-many relationship?

To determine whether to use the observer model, I think the second point is whether the publisher and subscriber of the message have a one-to-many relationship.

Still, if there is a need for us to build an achievement system at this time, and when the unit first dies, it shows an achievement effect (similar to the First Blood of various MOBA games), then we can still keep Character objects from relying on this achievement system.

Why? It still uses onDamage to publish a message, and the Achievement System subscribes to it. When the unit is injured, the Achievement System subscribes to the injured message to determine whether the unit's HP has been reduced to 0, if it is 0, then it judges the unit's death, then it judges whether the unit is the first death, and then uses the corresponding method.

The pseudocode below.

class AchievementSystem{
    // Achievement systems depend on game objects
    Character character;

    // Triggering the first blood achievement based on the corresponding event
    public void update(int nowhp,int damage){...}

    public void Init(){
        character.onDamage = update;
    }
}

You can see that we haven't even changed the Character class line, because it doesn't have much to do with this class.~~~

What if there's a similar need next? Just keep subscribing. For example, if planning requires a passive skill that reduces a unit's blood volume to less than 20% and increases defensive power, what can we do?

It's still a passive skill that subscribes to injuries, judges whether the health is below 20%, and if so, triggers the skill effect.

A brief summary

This is what I think is a one-to-many situation, that is, when a message is published, he will have multiple subscribers, and each subscriber will respond differently, so we can consider using the observer mode to eliminate some coupling (which can not be completely eliminated).

Intermediary model

Summary

If the observer pattern can only eliminate part of the coupling, then the mediator pattern can completely eliminate the dependency of two objects. In the book Game Programming Patterns, the definition of the mediator pattern (also often referred to as the service-oriented pattern) is as follows [5]:

Provide a global access entry for a service to avoid coupling between the user and the specific implementation class of the service.

In my opinion, the main function of the intermediary model is to change the spider-web-like reference relationship into N objects depending on the intermediary. The intermediary provides services for N objects, that is, to change the original many-to-many relationship into many-to-one, one-to-many relationship. This may be a bit abstract. Here's a concrete example to illustrate it.

In the game, we often need to make a prompt UI, which often does the following things:

  1. When a player purchases an item, judge whether the player's resources are adequate or not. If not, prompt the player.
  2. When player releases skill, judge whether player mp is enough, if not, prompt player
  3. When a player wants to perform a prohibited operation, prompt the player, such as attacking an invincible enemy, releasing skills to friendly forces, and so on.
    .....
    This kind of UI is quite common. He will be scattered in all corners of the game system. Maybe when he takes some action on another UI, he will jump out to remind you.

If the mediator pattern is not used, the following reference relationships may occur in the code:

If the mediator pattern is used, then dependencies can be transformed into the following.

After adding intermediaries, all objects that originally referred directly (or indirectly) to the UI prompt turned into direct reference intermediaries. When they had a message to publish (for example, a player did an operation that was not allowed), they would publish it directly to the intermediaries. The tipsUI, which subscribes to this message, automatically gets the message and processes it. Thus, each object is decoupled from the TipsUI.

When will the mediator model be used?

As mentioned earlier, the mediator pattern can minimize the coupling and reduce the dependencies between objects. So, is it possible to use the mediator model where there is dependency everywhere in the game? For example, replace all observer patterns with mediator patterns. The answer should be: No. As mentioned earlier, design patterns are not omnipotent. Intermediary patterns have their advantages and disadvantages.

The mediator pattern is essentially very similar to the singleton pattern. If we look at the previous cases carefully, we can also use static methods (classes) or singleton patterns to solve the problem. But for Unity's Mono Behavior class, this TipsUI object inherited from Mono Behavior can't be set to static, but we can also set TipsUI as a singleton mode to solve the problem of its scattered distribution in the game system.

One of the major drawbacks of the mediator model is that it makes coupling less intuitive. Reading the code using the mediator pattern is often difficult to understand who depends on him, which is also determined by his nature. The mediator does not know who will publish the message (that is, who will locate the service). We need full code to find who will publish the message to the mediator and who will process the message. This is harder to see than direct object references.

According to the book Game Programming Patterns, the first application scenario of the mediator pattern should be like this [5]:

When passing an object manually seems unreasonable or makes the code difficult to read, you can try to consider using this design pattern. After all, passing an environment attribute to a 10-level function to allow access to a low-level function adds meaningless complexity to the code.

In game development, there are several distinct examples to illustrate this point, such as audio management. Audio needs to be played wherever it is needed, but if there is no reason to transfer the object of audio manager between each code, then the mediator mode (or singleton mode) can be considered.

In addition, there are many examples, such as the logging system, the prompt UI mentioned earlier, and so on.

What is the difference between the mediator model and the singleton model?

The mediator pattern mentioned earlier is very similar to the singleton pattern, and they are all globally accessible objects. When we use one of them, we should consider which one is more suitable for our needs.

There is not much discussion on how to choose these two modes in Game Programming Mode. Based on the weak project experience and shallow cognition of vegetable bloggers over the years, I think the intermediary mode is more robust than the singleton mode.

This is because some of the mediator patterns in C# are designed using delegation. When subscribing to a message, it is actually a response method to that delegation+= and when publishing a message, it is actually a direct call to the delegation method.

In this way, if we use the intermediary mode to design the audio manager, then even if our audio manager makes a mistake and can't play the sound in the game, the game can run normally, or even if we delete all the code of the audio manager and write a more powerful audio system, as long as The new audio system responds to the same message (as the previous one), so the mediator mode is still working correctly.

How to Implement the Mediator Pattern in C#

As mentioned earlier, the broker model is mostly implemented by delegation, and the following is the basic code framework (refer to from the Internet).

public delegate void CallBack();
public delegate void CallBack<T>(T arg);
public delegate void CallBack<T, X>(T arg, X arg1);
public delegate void CallBack<T, X, Y>(T arg, X arg1, Y arg2);
public delegate void CallBack<T, X, Y, Z>(T arg, X arg1, Y arg2, Z arg3);
public delegate void CallBack<T, X, Y, Z, W>(T arg, X arg1, Y arg2, Z arg3, W arg4);

/// <summary>
/// Acting as a mediator for all UI view objects, the singleton class has two functions
/// 
/// 1. Subscription events:
/// To initiate a subscription to an event, the parameter is a delegate delegate delegate delegate delegate function.
/// Represents that when an event occurs, the function is called
/// 2. Publishing events:
/// Publishing events is equivalent to OnXXX delegation before we used intermediaries.
/// When an event occurs, the mediator's publishing event method is invoked.
/// Method to call all objects subscribing to the event
/// </summary>
public class MessageAggregator {

    // Singleton class loaded in hungry sweat mode to prevent errors in multi-threaded environment
    public static readonly MessageAggregator Instance = new MessageAggregator();
    private MessageAggregator() { }

    private Dictionary<string, Delegate> _messages;

    public Dictionary<string, Delegate> Messages {
        get {
            if (_messages == null) _messages = new Dictionary<string, Delegate>();
            return _messages;
        }
    }

    /// <summary>
    /// Called when subscribing to events
    /// </summary>
    /// <param name="string"></param>
    /// <param name="callback"></param>
    private void OnListenerAdding(string string,Delegate callback) {
        //Determine whether the dictionary contains the event code
        if (!Messages.ContainsKey(string)) {
            Messages.Add(string, null);
        }
        Delegate d = Messages[string];
        if (d != null && d.GetType() != callback.GetType()) {
            throw new Exception(string.Format("Trying to be an event{0}Add different types of delegates, and the corresponding delegates for the current event are{1},The delegation to be added is{2}", string, d.GetType(), callback.GetType()));
        }
    }


    /// <summary>
    /// Called when unsubscribed event
    /// </summary>
    /// <param name="string"></param>
    /// <param name="callBack"></param>
    private void OnListenerRemoving(string string,Delegate callBack) {
        if (Messages.ContainsKey(string)) {
            Delegate d = Messages[string];
            if (d == null) {
                throw new Exception(string.Format("Remove listening event errors: events{0}No corresponding delegation", string));
            } else if (d.GetType() != callBack.GetType()) {
                throw new Exception(string.Format("Remove listening event errors: try to be an event{0}Remove different types of delegates, and the corresponding delegation for the current event is{1},The delegation to be removed is{2}", string, d.GetType(), callBack.GetType()));
            }
        } else {
            throw new Exception(string.Format("Remove listening event errors: no event code{0}", string));
        }
    }

    /// <summary>
    /// A method of listening to events without reference (i.e. subscription events)
    /// </summary>
    /// <param name="string"></param>
    /// <param name="callBack"></param>
    public void AddListener(string string,CallBack callBack) {
        OnListenerAdding(string, callBack);
        Messages[string] = (CallBack)Messages[string] + callBack;
    }

    // 1 Reference to the method of listening for events (i.e. subscription events)
    public void AddListener<T>(string string, CallBack<T> callBack) {...}

    // 2. Reference to the method of listening for events (i.e. subscription events)
    public void AddListener<T,X>(string string, CallBack<T,X> callBack) {...}

    // 3. Reference to the method of listening for events (i.e. subscription events)
    public void AddListener<T,X,V>(string string, CallBack<T,X,V> callBack) {...}

    // 4 References to the method of listening for events (i.e. subscription events)
    public void AddListener<T,X,Y,Z>(string string, CallBack<T,X,Y,Z> callBack) {...}

    // 5 References to the method of listening for events (i.e. subscription events)
    public void AddListener<T, X, Y, Z,W>(string string, CallBack<T, X, Y, Z,W> callBack) {...}

    // Method of removing listening events without reference
    public void RemoveListener(string string,CallBack callBack) {
        OnListenerRemoving(string, callBack);
        Messages[string] = (CallBack)Messages[string] - callBack;
    }

    // 1. Method of removing listening events
    public void RemoveListener<T>(string string, CallBack<T> callBack) {...}


    // 2. Method of removing listening events
    public void RemoveListener<T, X>(string string, CallBack<T, X> callBack) {...}

    // 3-parameter method for removing listening events
    public void RemoveListener<T, X, V>(string string, CallBack<T, X, V> callBack) {...}

    // Method of removing listening events with 4 parameters
    public void RemoveListener<T, X, Y, Z>(string string, CallBack<T, X, Y, Z> callBack) {...}

    // 5-parameter method for removing listening events
    public void RemoveListener<T, X, Y, Z,W>(string string, CallBack<T, X, Y, Z,W> callBack) {...}

    // Unattended Radio Monitoring Events
    public void Broadcast(string string) {
        Delegate d;
        if (Messages.TryGetValue(string, out d)) {
            CallBack callBack = d as CallBack;
            if (callBack != null)
                callBack();
            else
                throw new Exception(string.Format("Broadcast Event Error: Event{0}There are different types of corresponding delegates", string));
        }
    }

    // 1. Participants'Broadcasting Monitoring Events
    public void Broadcast<T>(string string,T arg0) {...}

    // 2. Radio Monitoring Events
    public void Broadcast<T,V>(string string, T arg0,V arg1) {...}

    // 3. Radio Monitoring Events
    public void Broadcast<T,V,X>(string string, T arg0,V arg1,X arg2) {...}

    // 4. Radio Monitoring Events
    public void Broadcast<T, V, X,Z>(string string, T arg0, V arg1, X arg2,Z arg3) {...}

    // 5. Radio Monitoring Events
    public void Broadcast<T, V, X, Z,W>(string string, T arg0, V arg1, X arg2, Z arg3,W arg4) {...}
}

As an example of TipsUI, let's implement a prompt for players if mp is not enough when releasing skills.

// Unit object
class Character{
    int mp;
    // Release skills
    public void ExecuteSkill(Skill skill){
        if(this.mp < skill.mp)
            // Publish inadequate spell mp messages to intermediaries
            MessageAggregator.Instance.Broadcast< int,int >("ExecuteSkill",mp,skill.mp);
    }
}

// Prompt UI object
Class TipsUI{
    // String information to prompt
    private string tips;
    
    public void update(int nowmp,int skillmp){
        tips = string.format("current mp by:%d,Skills to be consumed mp:%d,mp Not enough to unleash skills",nowmp,skillmp);
    }

    // Subscribe to various messages
    public void Bind(){
        // Subscribe to spell-casting messages
        MessageAggregator.Instance.AddListener<int,int>("ExecuteSkill",update);
    }
}

At this point, when the player releases the skill, if the mp is not enough, the UI prompt will be triggered to indicate that the skill cannot be released.

What did delegate do?

In C #, delegate is a very convenient keyword. It is generally used for callback function mechanism. Using it, we can easily write low-coupling observer mode. It is similar to the function pointer in C++, but compared with the function pointer, the delegation in C# naturally supports multicast delegation (i.e. multiple callback methods, i.e. delegation chain), and type safety, which is quite convenient and comfortable to write.

So what did delegate do for us? Is the type or variable declared after the delegate keyword? To be honest, bloggers often write silly code like the one below when they first delegate.

// error code
class Main{

    delegate void Func();

    public void init(){
        // Adding callback methods to delegates
        Func+=Func1;
    }

    void Func1(){}
}

Everybody found out.~
sjm, a novice blogger, often sees delegation as something like a function pointer, and then assumes that the declaration is a pointer-like variable, so it points directly to the target method. So of course it's wrong.~~

This is the trouble of not knowing the work behind delegate. The ~C# compiler has done a lot of work for us. To understand the delegation, we have to look at the intermediate code IL generated by the C# code.

Here is a simple code example using delegates.

using System;
public delegate void Func(int a);

public class C {
    Func func;
    public void M() {
        func += aa;
        
        func(11);
    }
    void aa(int a){
        Console.WriteLine(a);
    }
}

We can go to https://sharplab.io to see what the intermediate code generated by C# is like.

The IL (intermediate) code generated from the above code is probably the following.

.class private auto ansi '<Module>'
{
} // end of class <Module>

.class public auto ansi sealed Func
    extends [mscorlib]System.MulticastDelegate
{
    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor (
            object 'object',
            native int 'method'
        ) runtime managed 
    {
    } // end of method Func::.ctor

    .method public hidebysig newslot virtual 
        instance void Invoke (
            int32 a
        ) runtime managed 
    {
    } // end of method Func::Invoke

    .method public hidebysig newslot virtual 
        instance class [mscorlib]System.IAsyncResult BeginInvoke (
            int32 a,
            class [mscorlib]System.AsyncCallback callback,
            object 'object'
        ) runtime managed 
    {
    } // end of method Func::BeginInvoke

    .method public hidebysig newslot virtual 
        instance void EndInvoke (
            class [mscorlib]System.IAsyncResult result
        ) runtime managed 
    {
    } // end of method Func::EndInvoke

} // end of class Func

.class public auto ansi beforefieldinit C
    extends [mscorlib]System.Object
{
    // Fields
    .field private class Func func

    // Methods
    .method public hidebysig 
        instance void M () cil managed 
    {
        .....
        IL_0014: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate)
        .....
        IL_002b: callvirt instance void Func::Invoke(int32)
        ....
    } // end of method C::M

    .method private hidebysig 
        instance void aa (
            int32 a
        ) cil managed 
    {...} // end of method C::aa
} // end of class C

As you can see, something amazing happened. When we declared Func with delegate, a class named Func was declared locally! Thus, we can also discover the secret of delegate, that is, where delegate is defined, the compiler will automatically help us declare a type inherited from Multicast Delegate. In fact, if we continue to trace back, we can also find that MulticastDelegate inherits from Delegate, which has a Combine method, which is mainly used to connect two delegates to form a delegate chain.

Therefore, when we see delegate XXXX later, we can automatically equate it to class xxx: Multicast Delegate. Because delegate is used to declare types, modifiers that can be used by types can also be used theoretically, such as public, private, internal, and so on.

For the type of this new declaration, two methods are noteworthy, namely, his construction method and Invoke method.

The signature of its construction method is as follows:

.method public hidebysig specialname rtspecialname 
        instance void .ctor (
            object 'object',
            native int 'method'
        )

You can see here that there are two parameters, one object object object and one integer. When a delegate adds an instance method, the object is the object that the instance method operates on, and if it is a static method, the parameter is null.
The integer method can be seen as a pointer to the function we are calling, that is, the address of the function is saved. In Unity3D scripting, this parameter is introduced as a runtime method to identify callbacks and a handle to refer to callback functions.~

The Invoke method, as its name implies, is a method used to call callback functions.

In addition, delegate has a lot of grammatical sugar. For example, when we initialize, we don't need to fill in the object parameter, and the compiler will automatically help us complete it. Also, we can call a delegate just like a normal method. Like the code above, we declare a Func-type delegate variable func, which can be called through func() just like a normal method. The compiler translates this code into func.invoke(xxx);

What is event? Is it related to delegate?

In addition to delegate, we often see code like the following.

public delegate OnCharacterDamageHandler(int nowHp,int damage);

// Game unit object
class Character{
    // Delegate to send messages to all subscribers who subscribe to this event when the unit is injured
    public event OnCharacterDamageHandler onDamage;

    int hp;
    // Method of execution upon receiving injury
    public void Damaged(int damage){
        this.hp -= damage;
        if(onDamage!=null)
            // Publish a message - the object is injured
            onDamage(hp,damage);
    }

    // Method of execution in releasing skills
    public void SkillExecute(Skill skill){
        // Some special skills need to consume HP
        if(skill == xxx){
            Damaged(xxxx);
        }
    }
}

class HpUI{
    // hp bar, when unit hp changes, adjust width automatically
    Image hpbar;

    // UI depends on game objects
    Character character;

    // Method of changing blood strip UI according to current hp
    private void update(int hp,int damage){...}

    // Method of UI initialization
    public void Init(){
        // Injury Events in Bloodstrip UI Subscribers
        character.onDamage += update;
    }
}

Reading the above code, you can see that event keyword seems to achieve the same effect as delegate. So what's the difference between these two keywords?

The Origin of event

This should start with the origin of event. We already know that the delegate keyword actually declares a type, while the inside of the Character class declares a variable of OnCharacterDamageHandler type to which callback methods can be added or eliminated.

So onDamage, as a field in a class, has to consider the problem of accessing modifiers. We know that attributes in a class should not be public, and some attributes should not be exposed. Because there may be some strange external changes to this property, such as assigning it to null.

For the delegate type, what we actually need outside is only + = and - =.

However, when you really want to change onDamage to private or internal, you will find that this variable can't be changed to access modifiers other than public at all. Why? Because after changing it to non-public, the external delegate type can not be added to the method ah! And we design the commission, not to be able to register with him outside the method. If you make him private, it's like he's totally ineffective.~

However, we do not want to make onDamge public, because "it can be arbitrarily assigned to the client and other operations, seriously undermining the encapsulation of objects" [2].

For this purpose, C # provides event to solve this problem. When we encapsulate a delegate type variable with event, the delegate variable accepts only Add and Remove operations externally, but can not be arbitrarily assigned, which enhances security.

Try to put the code above

character.onDamage += update;

Change to

character.onDamage = update;

You can find compilation errors~~

What did Evet do?

public event OnCharacterDamageHandler onDamage;

So what exactly happened in the above line that we don't know?

Still the old way, look at the generated intermediate code.

To prevent the generated intermediate code from being too long, here is a simple example of using event.

using System;
public delegate void Func(int a);

public class C {
    public event Func func;

    public void M() {
        func += aa;
        
        func(11);
    }
    void aa(int a){
        Console.WriteLine(a);
    }
}

The intermediate code generated by him is shown below.

.class private auto ansi '<Module>'
{
} // end of class <Module>

.class public auto ansi sealed Func
    extends [mscorlib]System.MulticastDelegate
{...} // end of class Func

.class public auto ansi beforefieldinit C
    extends [mscorlib]System.Object
{
    // Fields
    .field private class Func func
    ...

    // Methods
    .method public hidebysig specialname 
        instance void add_func (
            class Func 'value'
        ) cil managed 
    {...} // end of method C::add_func

    .method public hidebysig specialname 
        instance void remove_func (
            class Func 'value'
        ) cil managed 
    {...} // end of method C::remove_func

    .method public hidebysig 
        instance void M () cil managed 
    {
        ...
        IL_000e: call instance void C::add_func(class Func)
        ...
        IL_0015: ldfld class Func C::func
        IL_001a: ldc.i4.s 11
        IL_001c: callvirt instance void Func::Invoke(int32)
        ...
    } // end of method C::M
    ...
    // Events
    .event Func func
    {
        .addon instance void C::add_func(class Func)
        .removeon instance void C::remove_func(class Func)
    }
} // end of class C

I omitted some code that is not relevant to this example. As you can see, in the public event line, although we use public to declare delegate variables, the compiler finally treats them as private variables. At the same time, the compiler adds two methods to the class, namely add_func and remove_func, to add or delete methods to delegates.

In this way, it is equivalent to encapsulating the Func type in the class, only exposing the interface of adding and deleting methods to the outside, which enhances security.~~

Topics: C# Programming Unity less Attribute