[Unity] Action在Unity中的用法

委托

C语言中的函数指针

#include <stdio.h>

int Add(int a, int b)
{
	return a + b;
}
int Multiply(int a, int b)
{
	return a * b;
}

int main()
{
	int a = 1;
	int b = 2;
	
	printf("Add: %d\n", Add(a, b));
	printf("Multiply: %d\n", Multiply(a, b));
    return 0;
}
复制代码

正常调用:

Add: 3
Multiply: 2
复制代码

定义一个 两个 int 参数,返回值为 int 的函数指针。存两个函数的地址,然后通过地址调用函数。

#include <stdio.h>

typedef int (*Fun_ptr)(int,int);

int Add(int a, int b)
{
	return a + b;
}
int Multiply(int a, int b)
{
	return a * b;
}
int main(void) { 
	Fun_ptr fun1 = &Add;
	Fun_ptr fun2 = &Multiply;
	
	int a = 1;
	int b = 2;
	
	printf("fun1 Add: %d\n", fun1(a, b));
	printf("fun2 Multiply: %d\n", fun2(a, b));

	return 0;
}
复制代码
fun1 Add: 3
fun2 Multiply: 2
复制代码

C语言中的回调函数

在main中用xAddRandomNum时,xAddRandomNum需要传入一个“不要参赛,且返回值为int”的函数指针。

xAddRandomNum需要调用另一个函数完成自己的任务,被调用的那个函数就是回调函数。

#include <stdlib.h>  
#include <stdio.h>

int getNextRandomValue(void)
{
    return rand() % 100;
}

int xAddRandomNum(int x, int (*getRandomFun)(void))
{
    return x + getRandomFun();
}

int main(void) { 

	printf("xAddRandomNum: %d\n", xAddRandomNum(1, getNextRandomValue));

	return 0;
}
复制代码

C#中的委托

   class HelloWorld
   {
	// 定义一个委托方法DeleFun() 相当于C里面定义方法指针
	public delegate void DeleFun();
	// 声明一个 如上定义 没有参赛没有返回值的 委托变量 
	static DeleFun deleFun;
        
        static void Main(string[] args)
        {
            deleFun = new DeleFun(Fun1); 
          //deleFun = Fun1; 简写,一样的效果 
            deleFun.Invoke();
            Console.ReadKey();
        }
	private static void Fun1()
	{ Console.Write("Invoke Fun1");}
   }
复制代码
Invoke Fun1
复制代码

可以一个委托变量绑定多个方法:

   class HelloWorld
   {
	// 定义一个委托方法DeleFun() 相当于C里面定义方法指针
	public delegate void DeleFun();
        // 声明一个 如上定义 没有参赛没有返回值的 委托变量 
	static DeleFun deleFun;
        
        static void Main(string[] args)
        {
            deleFun += Fun1; 
            deleFun += Fun2; 
            deleFun.Invoke();
            Console.ReadKey();
        }
	private static void Fun1()
	{ Console.WriteLine("Invoke Fun1");}
	private static void Fun2()
	{ Console.WriteLine("Invoke Fun2");}
   }
复制代码
Invoke Fun1
Invoke Fun2
复制代码

绑定多个方法,叫多播,绑定单个方法叫单播,但是单播实际上也是通过多播实现的,只不过把个数限制为1个。

        static void Main(string[] args)
        {
            deleFun += Fun1; 
            // 去掉+
            deleFun = Fun2; 
            deleFun.Invoke();
            Console.ReadKey();
        }
	private static void Fun1()
	{ Console.WriteLine("Invoke Fun1");}
	private static void Fun2()
	{ Console.WriteLine("Invoke Fun2");}
   }
复制代码

最后一个不用“+=”,而是“=”。

Invoke Fun2
复制代码

换一下,先“=”,再“+=”:


        static void Main(string[] args)
        {
            deleFun = Fun1; 
            deleFun += Fun2; 
            deleFun.Invoke();
            Console.ReadKey();
        }
	private static void Fun1()
	{ Console.WriteLine("Invoke Fun1");}
	private static void Fun2()
	{ Console.WriteLine("Invoke Fun2");}
   }
复制代码

正常多播:

Invoke Fun1
Invoke Fun2
复制代码

Action

Action就是C#官方给你提供一个拿来就直接用的委托。

   class HelloWorld
   {
     // 定义一个变量
      static Action<int> OneKeyThreeFuns;
      
      static void Main(string[] args)
      {
       // 直接绑方法
          OneKeyThreeFuns += Fun1;
          OneKeyThreeFuns += Fun2;
          OneKeyThreeFuns += Fun3;
		  
	  OneKeyThreeFuns.Invoke(2);
          Console.ReadKey();
      }
	  
	  static void Fun1(int a)
	  {
		  Console.WriteLine($"num:{a}, Fun1");
	  }
	   static void Fun2(int a)
	  {
		  Console.WriteLine($"num:{a}, Fun2");
	  }
	   static void Fun3(int a)
	  {
		  Console.WriteLine($"num:{a}, Fun3");
	  }
   }
复制代码

Action<int, bool, string ….> 绑最多十六个参数,无返回值的方法。

Func

前面是参数,最后面是返回类型,也是最多16个参数。

using System;
namespace HelloWorldApplication
{
   class HelloWorld
   {
	static void Main()
        {
            Func<int, string> method = Fun1;

            Console.WriteLine(method(3));
			
            Console.ReadLine();
        }


        static string Fun1(int x)
        {
            return (x * x).ToString();
        }

   }
}
复制代码

lambda表达式形式

    Func<int, int, string> method = (x, y) =>
    {
         int val = x + y;
         return val.ToString();
    };
复制代码

匿名委托形式

    Func<int, int, string> method = delegate (int x, int y) 
    {
        int val = x + y;
        return val.ToString();
    };

复制代码

使用方法其一

假如有这样一个玩家数据的类,存储了击杀数、死亡数、分数等数据。

public class PlayerStats
{
    public string playerName;
    
    // 击杀数、死亡数、得分数
    public int killNum;
    public int deathNum;
    public int scoreNum;
}
复制代码

playerStatsList在「GameManager」中,是存储了所有玩家的列表。

public class GameManager : Singleton<GameManager>
{
    public PlayerStats[] playerStatsList;
}
复制代码

如果想要获得,击杀数最高的玩家的名字,可以如下写一个遍历所有玩家,然后记录最大值对应的名字。

public class GameManager : Singleton<GameManager>
{
    public PlayerStats[] playerStatsList;
    void Awake()
    {
        print(GetTopKillPlayerName());
    }
    private string GetTopKillPlayerName()
    {
        string playerName = "";
        int topKillNum = 0;

        foreach (PlayerStats playerStats in playerStatsList)
        {
            int tempNum = playerStats.killNum;
            if (tempNum > topNum)
            {
                topNum = tempNum;
                playerName = playerStats.playerName;
            }
        }
        return playerName;
    }
}
复制代码

如果还想获得死亡数最高的玩家的名称,再写一个方法,只不过比较的不是击杀数而是死亡数。

public class GameManager : Singleton<GameManager>
{
    public PlayerStats[] playerStatsList;
    void Awake()
    {
        print(GetTopKillPlayerName());
        print(GetTopDeathPlayerName());
    }
    private string GetTopKillPlayerName()
    {
        string playerName = "";
        int topKillNum = 0;

        foreach (PlayerStats playerStats in playerStatsList)
        {
            int tempNum = playerStats.killNum;
            if (tempNum > topNum)
            {
                topNum = tempNum;
                playerName = playerStats.playerName;
            }
        }
        return playerName;
    }
    private string GetTopDeathPlayerName()
    {
        string playerName = "";
        int topDeathNum = 0;

        foreach (PlayerStats playerStats in playerStatsList)
        {
            int tempNum = playerStats.deathNum;
            if (tempNum > topNum)
            {
                topNum = tempNum;
                playerName = playerStats.playerName;
            }
        }
        return playerName;
    }
}
复制代码

问题出现了,很明显,大部分代码都是相同的,但是为了不同的部分,重新写了一个方法,如果玩家有成百上千个属性,总不能写几百个方法吧?

private int GetKillNum(PlayerStats playerStats)
{
    return playerStats.killNum;
}
private int GetDeathNum(PlayerStats playerStats)
{
    return playerStats.deathNum;
}
private int GetScoreNum(PlayerStats playerStats)
{
    return playerStats.scoreNum;
}
复制代码

所以可以写三个,参数为PlayerStats,返回值为int的方法。然后将委托作为GetTopItemPlayerName的参数。

string topKillPlayerName = GetTopItemPlayerName(GetKillNum);
string topDeathPlayerName = GetTopItemPlayerName(GetDeathNum);
string topScorePlayerName = GetTopItemPlayerName(GetScoreNum);
复制代码

那么tempNum获得的,就是对应的数据。

private string GetTopItemPlayerName(Func<PlayerStats, int> func)
    {
        string playerName = "";
        int topNum = 0;

        foreach (PlayerStats playerStats in playerStatsList)
        {
            int tempNum = func(playerStats);
            if (tempNum > topNum)
            {
                topNum = tempNum;
                playerName = playerStats.playerName;
            }
        }
        return playerName;
    }
复制代码

再简洁一点,用Lambda表达式,可以进一步省略声明方法的过程。

string topKillPlayerName = GetTopItemPlayerName(playerStats => playerStats.killNum);
string topDeathPlayerName = GetTopItemPlayerName(playerStats => playerStats.deathNum);
string topScorePlayerName = GetTopItemPlayerName(playerStats => playerStats.scoreNum);
复制代码

将方法中不确定的部分,利用委托作为参数,生成模板方法。这是委托的使用方法其一。

使用方法其二

假设现在有「GameManager」和「UIManager」:

public class GameManager : MonoBehaviour
{
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.A))
            GameOver();
    }
    void GameOver()
    {
        Debug.Log("GameOver");
        // 让UI Manager显示 “GameOver”字样
    }
}
复制代码
public class UIManager : MonoBehaviour
{
    public TextMeshProUGUI gameOverText;

    void Awake()
    {
        gameOverText.gameObject.SetActive(false);
    }

    private void ShowGameOverText()
    {
        gameOverText.gameObject.SetActive(true);
    }
}
复制代码

我想在「GameManager」执行GameOver方法时,调用UI管理器的“private”方法,显示GameOver字样。

在「GameManager」中声明一个 “static” 委托「OnGameOver」,那么委托绑定的方法,会在GameOver()中被调用。

using System;
public class GameManager : MonoBehaviour
{
    public static Action OnGameOver;

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.A))
            GameOver();
    }
    void GameOver()
    {
        // 委托不为空的时候,调用委托绑定的所有方法
        OnGameOver?.Invoke(); 
    }
}
复制代码

在UI Manager脚本中,脚本“自愿的”将自己的private方法,绑定在别人的委托上,这样其它脚本就相当于通过委托,获得了这个private方法的地址,自然就可以调用了。

public class UIManager : MonoBehaviour
{
    public TextMeshProUGUI gameOverText;

    private void OnEnable()
    {
        GameManager.OnGameOver += ShowGameOverText;
    }
    private void OnDisable()
    {
        GameManager.OnGameOver -= ShowGameOverText;
    }
    void Awake()
    {
        gameOverText.gameObject.SetActive(false);
    }

    private void ShowGameOverText()
    {
        gameOverText.gameObject.SetActive(true);
    }
}
复制代码

游戏开始:

image.png

按A后

image.png


再拓展一下,一个委托可以绑定,各个不同的类的各个不同的方法。比如音效管理器里,停止音乐的方法,玩家脚本里,玩家死亡后的各种方法。通过多播委托实现。

但是使用多播委托,会有各种各样的问题,比如在多人协作里,负责某一脚本的人,将“+=”写成了“=”,前面绑定的其它方法全都被这个方法顶掉了,造成各种问题。

所以event,事件,就应运而生,相当于将多播委托进行了规范,封装成了事件,详细介绍见下篇

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享