51、C 编程与 Unity 3D:委托、事件与扩展函数的应用

C# 编程与 Unity 3D:委托、事件与扩展函数的应用

1. 委托与事件基础

在 Unity 3D 中使用 C# 编程时,将大量代码作为整个游戏的中央开关板会变得非常繁琐。委托可以实现很多很酷的技巧,带有泛型类型的委托能实现更多有趣的功能,尤其是事件。事件就像一个容器,可用于存放任意数量的函数。当事件被调用时,所有分配给它的函数都会被调用,这是一种很好的通知机制,能告知另一个函数你所等待的事情已经发生。

1.1 委托与事件的声明

我们通常使用 public void functionName() {//code statements} 这样的声明来定义函数。而委托的声明则根据具体需求,例如 delegate void delegateName(); 。下面通过一个基本示例来详细说明。

1.2 基本示例

在 Unity 中启动 Events_Scene ,定位到同名游戏对象上的 Events 脚本。通常, EventHandler 的命名会基于它要处理的事件类型。在了解其用途后,我们再确定更合适的名称。

声明 public delegate void Handler(); 是一种类型声明,创建该类型后,会创建变量 HandleDelegate 。为了等待委托被处理,我们创建一个新的 Listener 类,其中有一个函数会被分配给 HandleDelegate 变量。事件的声明与委托类似,但会添加 event 关键字。

在很多情况下,我们认为只能给变量赋值,但委托可以分配函数。当调用委托时,分配的函数也会被调用,不过分配有一个条件:签名必须匹配,即委托的声明和分配给它的函数的签名必须一致。

创建 Listener 类后,将 OnDelegateHandled() 事件分配给 Dispatcher HandleDelegate HandleEvent 变量。创建调度器和监听器后,有两种方式分配委托:
1. 使用 += 运算符。
2. 使用新的 Dispatcher.Handler() 函数初始化器。

但使用第二种方式分配时会出现有趣的情况。当调度器调用 CallHandlers() 函数时,只有 Frasier 的委托被处理,Sigmond 的 OnDelegateHandled 函数未被调用。调用 new Dispatcher.Handler() 时, HandleDelegate 会丢失之前的所有分配,清除委托是被允许的。

我们还可以将 null 分配给 HandleDelegate 处理程序,调用 CallHandlers() 时将不会发生任何事情,因为 HandleDelegate 已清除所有要调用的函数。通过使用 event 关键字标记 HandleEvent 处理程序,可以防止对该变量的误用。错误提示表明,在给事件赋值时只能使用 += -= 运算符,所以 event 关键字更像是委托的特殊访问器关键字。尝试将 HandleEvent 设置为 null 时也会出现相同的错误。

1.3 事件的保护机制

事件保护委托只能使用 += -= 来分配函数,这可以保护变量不被误操作或意外清除已分配给事件处理程序的内容。要从处理程序中移除一个函数,可以使用 -= 运算符。例如,使用 -= HandleEvent 中移除 Frasier.OnDelegateHandled 后,只有 Sigmund 的委托处理程序会被调用。委托和事件的操作方式基本相同,主要区别在于它们的保护级别。

1.4 符合规范的事件

.NET 框架有许多规范程序,代码需要遵循这些规范。Microsoft 描述了许多最佳实践,涵盖从变量名到事件的各个方面。为了遵循正确的代码程序,我们需要了解如何编写事件。

public delegate void ProperEventHandler(object sender, EventArgs args);

将上述委托添加到 EventDispatcher 类中,紧接在旧的 EventHandler 之后。这里添加了 object sender EventArgs args ,稍后会详细解释它们的用途。

在类作用域外部声明委托后,在类作用域内添加该委托的处理程序:

public event ProperEventHandler ProperEvent;

为了使事件更有用,我们可以添加 EventArgs (事件参数),以便新事件具有更多功能。 EventArgs 用于向监听事件的对象传递参数。在创建自定义 EventArg 之前,需要添加新的指令。

1.5 EventArgs 的使用

系统指令让我们可以访问一个名为 EventArg 的新类。 EventArg 类型来自 System ,因此要创建基于它的类,需要在类作用域中添加 using System; 。完成此操作后,在 EventDispatcher 的全局作用域中创建一个新类,例如 MessageEventArgs

graph TD;
    A[开始] --> B[添加 using System;]
    B --> C[创建 MessageEventArgs 类]
    C --> D[在构造函数中分配 string message]
    D --> E[更改委托签名以发出 MessageEventArgs 类型]
    E --> F[确保 OnProperEvent 函数签名匹配]
    F --> G[在 EventDispatcher 中添加 ProperEvent 到调用语句]
    G --> H[实例化 MessageEventArgs 对象并传递参数]
    H --> I[结束]

创建或实例化 MessageEventArgs 类后,应在构造函数中分配 string message 。需要注意的是,自定义事件参数允许将特定的事件信息传递给监听事件的对象。要使用新的 MessageEventArgs 对象调度事件,需要更改委托签名以发出 MessageEventArgs 类型。在 ProperEventArgListener 类中,事件调用的 OnProperEvent 函数的签名必须与 ProperEventHandler 匹配。

现在,调度器的 MyEvent 调用 EventListeners CallMeMaybe 函数, ProperEvent 调用 CallMePlease 函数。最后,在 EventDispatcher 中,需要将 ProperEvent 添加到进行调用的 if 语句中。 MessageEventArgs() 对象是一个类,使用前需要使用 new 进行实例化。在 ProperEvent 参数列表中,使用 this 作为发送者, new MessageEventArgs() 作为第二个参数,这两个值会传递给监听器。

采用事件驱动的游戏设计是最佳选择,这样可以避免在条件满足时立即执行任务,而是等待事件发生后再处理,从而避免过多脚本同时更新。在 PC 上这可能不是问题,但在移动设备上,处理器性能会受到很大限制。

1.6 泛型 EventArgs

我们可以让多个事件从单个委托类型派生,为此需要使用泛型类型来声明委托。当创建带有 <TEventArgs> EventHandler 委托时,在参数列表中使用泛型类型代替强类型。这意味着 MessageEvent NumberEvent 都可以从 EventHandler 委托派生,每个事件用自己的特定 EventArg 类型替换 TEventArgs

使用 GenericEventArgs 中的泛型值,我们可以通过 GenericEventArg 与委托 EventHandler 进行交互。创建事件时需要指定类型,创建参数时也必须包含匹配的类型。同样,泛型类型的监听器也必须包含具有匹配签名的事件监听器。

使用事件时,通过少量类型定义就能获得很大的灵活性。例如, sigmund.OnGenericEvent 可以分配给 IntEvent StringEvent ,并为每个事件提供适当的重载函数。 GenericEventArgs<int> GenericEventArgs<string> 能确保在调用不同事件时找到要调用的函数。

综上所述,直观地使用事件很重要,泛型在基于灵活委托声明创建事件时增加了灵活性。使用事件而非委托主要有两个原因:一是事件增加了保护级别,二是允许从单个委托类型派生多个事件。

2. 扩展函数

2.1 传统编程方式的问题

在编写新类时,传统程序员可能会编写一个庞大的单一类,用复杂的算法处理多个不同的任务。但这种方法存在一些问题,例如调试困难、缺乏灵活性以及代码难以解释等。在处理复杂问题时,更好的、更适合 Unity 的方法是从通用解决方案入手。

2.2 扩展方法的引入

当使用 Unity 3D 的内置类型(如 GameObject )时,我们无法直接扩展这些类。如果尝试使用 MyGameObject 继承 GameObject ,会得到错误信息 'MyGameObject': cannot derive from sealed type 'GameObject' ,这表明 GameObject 是一个密封类,不能被扩展或修改。但这并不意味着我们不能为 GameObject 添加新功能,通过扩展方法可以实现这一点。

2.3 基本示例

在 Unity 中启动 ExtensionFunctions_Scene ,打开同名对象上的 ExtensionFunctions.cs 脚本。在脚本顶部添加 using Tricks; ,以便 ExtensionFunctions: MonoBehaviour 类可以使用命名空间中的内容。通常,命名空间应放在另一个文件中,但为了清晰起见,我们将它们合并在一个文件中。

在命名空间中,创建一个新类,例如 GameObjectExtensions ,这是一个静态类,其中包含一个新的静态函数,用于扩展密封的 GameObject 类。

public static void NewTrick(this GameObject gameObject)
{
    Console.WriteLine(gameObject.name + " has a new trick!");
}

上述代码中的 NewTrick() 就是扩展函数,使用 this GameObject gameObject 作为参数, this 关键字告知 C# 这是一个扩展方法。在函数中,使用 gameObject.name 打印游戏对象的名称和提示信息。

ExtensionFunctions_Scene 中,附加到 ExtensionFunctions 游戏对象上的 MonoBehaviour 会显示 (Extension) void NewTrick() 作为 gameObject 类型的可用函数。选择并运行 NewTrick() 函数,控制台将输出 Penn has a new trick!

任何额外的参数都必须放在使用 this 关键字的参数之后。根据第一个参数中 this 后面的类型,可以更改函数以扩展不同的密封类。例如,添加 Vector3 position 作为第二个参数,并使用 gameObject.transform.position = position; 可以更新游戏对象的位置。

public static void Move(this GameObject gameObject, Vector3 position)
{
    gameObject.transform.position = position;
}

使用 Move() 扩展函数时,虽然第一个参数是游戏对象本身,但在使用时不会显示。通过使用第一个参数引用对象的语法,将函数转换为扩展函数,扩展函数可以应用于任何密封类。在使用第三方库时,为没有源代码的类添加自己的函数非常有用。

static 关键字对于使函数在任何上下文中都可见是必要的,包含扩展函数的类也需要是静态的。扩展函数理论上可以写在任何静态类中,但如果出现在不相关的类中会造成混淆,因此清晰的命名规范非常重要。

由于扩展方法的存在,为密封类添加功能变得很容易。但如果可能的话,最好直接将函数添加到类本身,这样可以避免编写大量扩展函数,并且这种方法不仅适用于 gameObject

2.4 扩展函数的重载

除了通过扩展为 Transform gameObject 添加新函数外,还可以为现有函数创建新的重载。例如,新的 Reset() 扩展函数被调用时,游戏对象会移动到场景的原点,即使它的父对象已经移动。

Transform 对象的 SetParent() 函数有几种重载方式。第一种方式设置父对象时会保留子对象的局部位置、旋转和缩放;第二种方式在设置父对象后保留世界位置;而我们自定义的扩展函数在设置父对象后将子对象设置到场景的原点。

graph TD;
    A[创建 Child 游戏对象并定位到 1.0 1.0 1.0] --> B[创建 Parent 对象并移动到 0,0,1]
    B --> C[使用重载的 SetParent 函数设置父对象]
    C --> D[检查 Child 的局部位置]
    D --> E[输出结果]

通过以下代码示例可以更清楚地看到重载的效果:

// 创建 Child 游戏对象
GameObject child = new GameObject("Child");
child.transform.position = new Vector3(1.0f, 1.0f, 1.0f);

// 创建 Parent 游戏对象
GameObject parent = new GameObject("Parent");
parent.transform.position = new Vector3(0.0f, 0.0f, 1.0f);

// 使用重载的 SetParent 函数
child.transform.SetParent(parent.transform, resetToWorldPosition: true);

// 检查 Child 的局部位置
Vector3 localPosition = child.transform.localPosition;
Debug.Log("Child local position: " + localPosition);

有两种使用 (Transform, bool) 签名模式的重载方式。内置版本使用 bool 参数指示子对象是否保留其相对世界位置,使用该选项后,子对象的 localPosition 会报告为 1.0, 1.0, 0.0 。通过明确写出 bool 参数名 resetToWorldPosition: true ,我们告诉 C# 使用自定义的扩展函数而不是内置版本。

2.5 魔法镜像扩展函数

程序员经常编写代码来减少代码量,虽然这听起来有些矛盾,但大部分编程时间都花在编写被短函数调用的大函数上。在追求降低复杂度的过程中,复杂度是不可避免的。

这里我们为 GameObject 创建一个名为 Mirror 的扩展函数,它接受一个 Vector3 值作为镜像轴,例如反射 x 轴。

public static void Mirror(this GameObject original, Vector3 axis)
{
    // 创建克隆对象
    GameObject clone = Instantiate(original);
    clone.transform.position = original.transform.position;
    clone.transform.localScale = new Vector3(-axis.x * original.transform.localScale.x, axis.y * original.transform.localScale.y, axis.z * original.transform.localScale.z);

    // 创建更新系统
    UpdateReflection updateReflection = original.AddComponent<UpdateReflection>();
    updateReflection.Setup(original, clone, axis);
}

Mirror 函数会创建原始对象的克隆,并设置其大小和位置。计算镜像缩放并应用到克隆对象上,然后创建一个更新系统来跟踪原始对象、镜像对象和反射值。

UpdateReflection 类会跟踪镜像对象和反射向量。创建时,它会为原始对象添加一个名为 UpdatesReflected 的组件。 UpdatesReflected 对象有一个更新事件,会调用 OnReflectionUpdated 函数,该函数会根据反射轴值设置镜像对象的位置和旋转。

graph TD;
    A[调用 Mirror 函数] --> B[创建克隆对象]
    B --> C[设置克隆对象的大小和位置]
    C --> D[计算并应用镜像缩放]
    D --> E[创建 UpdateReflection 类实例]
    E --> F[添加 UpdatesReflected 组件]
    F --> G[设置更新事件]
    G --> H[根据反射轴更新镜像对象位置和旋转]
    H --> I[结束]

原始对象会附加 MonoBehaviour UpdatesReflected ,其中的 ReflectionUpdateEvent 会调用 UpdateReflection 类中的 OnReflectionUpdated 函数。事件会传递原始对象的变换信息,以便镜像对象了解原始对象的变换更新情况。

通过合理使用委托、事件和扩展函数,我们可以提高代码的可维护性和灵活性,使游戏开发更加高效。在实际应用中,需要根据具体需求选择合适的方法,并遵循良好的编程规范。

3. 总结与应用建议

3.1 核心要点回顾

技术概念 主要特点 应用场景
委托与事件 - 委托可存放函数,事件基于委托,增加保护机制
- 泛型委托增加事件灵活性,允许派生多个事件
- 游戏中事件通知机制,如角色状态变化通知
- 多模块间的消息传递
扩展函数 - 为密封类添加新功能,可重载现有函数
- 提高代码可维护性,避免庞大单一类
- 为 Unity 内置类或第三方库类添加自定义功能
- 简化复杂类的功能扩展

3.2 应用建议

  • 委托与事件的使用
    • 在设计游戏系统时,优先考虑使用事件驱动的架构。例如,当角色的生命值发生变化时,可以触发一个 HealthChanged 事件,所有监听该事件的模块(如 UI 显示、技能触发等)都会收到通知并做出相应处理。
    • 合理使用泛型委托和事件,根据不同的事件类型传递特定的参数。比如,在处理不同类型的伤害事件时,可以使用泛型 EventArgs 传递伤害类型、伤害值等信息。
  • 扩展函数的使用
    • 对于经常使用的功能,考虑为相关类创建扩展函数。例如,为 GameObject 类添加一个 IsInRange 扩展函数,用于判断该游戏对象是否在某个范围内。
    • 在使用扩展函数重载时,要明确每个重载的功能和适用场景,避免造成混淆。同时,使用有意义的参数名,确保代码的可读性。

3.3 注意事项

  • 委托与事件
    • 注意事件的订阅和取消订阅,避免内存泄漏。在对象销毁时,确保取消所有订阅的事件。
    • 合理使用 event 关键字,避免对事件变量的误用。
  • 扩展函数
    • 扩展函数应保持功能的单一性和独立性,避免在扩展函数中添加过多复杂逻辑。
    • 清晰命名扩展函数和包含它们的类,遵循一致的命名规范。

4. 案例分析:游戏中的具体应用

4.1 委托与事件在角色系统中的应用

在一个角色扮演游戏中,角色的状态变化(如生命值、魔法值、等级等)可以通过事件来通知相关模块。以下是一个简单的示例:

// 定义事件参数类
public class CharacterStatusChangedEventArgs : EventArgs
{
    public int Health { get; set; }
    public int Mana { get; set; }
    public int Level { get; set; }

    public CharacterStatusChangedEventArgs(int health, int mana, int level)
    {
        Health = health;
        Mana = mana;
        Level = level;
    }
}

// 定义委托和事件
public delegate void CharacterStatusChangedEventHandler(object sender, CharacterStatusChangedEventArgs args);

public class Character
{
    public event CharacterStatusChangedEventHandler StatusChanged;

    private int health;
    private int mana;
    private int level;

    public int Health
    {
        get { return health; }
        set
        {
            health = value;
            OnStatusChanged();
        }
    }

    public int Mana
    {
        get { return mana; }
        set
        {
            mana = value;
            OnStatusChanged();
        }
    }

    public int Level
    {
        get { return level; }
        set
        {
            level = value;
            OnStatusChanged();
        }
    }

    protected virtual void OnStatusChanged()
    {
        StatusChanged?.Invoke(this, new CharacterStatusChangedEventArgs(Health, Mana, Level));
    }
}

// 监听事件的类
public class CharacterStatusListener
{
    public void Subscribe(Character character)
    {
        character.StatusChanged += OnCharacterStatusChanged;
    }

    public void Unsubscribe(Character character)
    {
        character.StatusChanged -= OnCharacterStatusChanged;
    }

    private void OnCharacterStatusChanged(object sender, CharacterStatusChangedEventArgs args)
    {
        Debug.Log($"Character status changed: Health = {args.Health}, Mana = {args.Mana}, Level = {args.Level}");
    }
}
graph TD;
    A[角色状态改变] --> B[触发 StatusChanged 事件]
    B --> C[调用 OnCharacterStatusChanged 函数]
    C --> D[输出状态信息]

4.2 扩展函数在游戏对象操作中的应用

在一个策略游戏中,需要对游戏对象进行一些额外的操作,如移动到指定位置、旋转等。可以为 GameObject 类创建扩展函数来实现这些功能。

using UnityEngine;

public static class GameObjectExtensions
{
    public static void MoveTo(this GameObject gameObject, Vector3 position, float speed)
    {
        gameObject.transform.position = Vector3.MoveTowards(gameObject.transform.position, position, speed * Time.deltaTime);
    }

    public static void RotateAroundAxis(this GameObject gameObject, Vector3 axis, float angle)
    {
        gameObject.transform.Rotate(axis, angle);
    }
}

在游戏脚本中,可以直接使用这些扩展函数:

using UnityEngine;

public class GameController : MonoBehaviour
{
    public GameObject targetObject;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            targetObject.MoveTo(new Vector3(10, 0, 0), 5f);
            targetObject.RotateAroundAxis(Vector3.up, 90f);
        }
    }
}

5. 未来展望

随着游戏开发的不断发展,委托、事件和扩展函数在 Unity 3D 中的应用将会更加广泛和深入。未来可能会出现以下趋势:
- 更智能的事件管理系统 :自动处理事件的订阅和取消订阅,减少开发者的手动操作,进一步提高开发效率。
- 跨平台的扩展函数库 :为不同平台的 Unity 开发提供统一的扩展函数接口,方便开发者在不同平台上复用代码。
- 与其他技术的融合 :与人工智能、机器学习等技术结合,实现更加复杂和智能的游戏逻辑。

通过不断学习和实践,开发者可以更好地掌握这些技术,为游戏开发带来更多的创新和可能性。

【无人机】基于改进粒子群算法的无人机路径规划研究[和遗传算法、粒子群算法进行比较](Matlab代码实现)内容概要:本文围绕基于改进粒子群算法的无人机路径规划展开研究,重点探讨了在复杂环境中利用改进粒子群算法(PSO)实现无人机三维路径规划的方法,并将其遗传算法(GA)、标准粒子群算法等传统优化算法进行对比分析。研究内容涵盖路径规划的多目标优化、避障策略、航路点约束以及算法收敛性和寻优能力的评估,所有实验均通过Matlab代码实现,提供了完整的仿真验证流程。文章还提到了多种智能优化算法在无人机路径规划中的应用比较,突出了改进PSO在收敛速度和全局寻优方面的优势。; 适合人群:具备一定Matlab编程基础和优化算法知识的研究生、科研人员及从事无人机路径规划、智能优化算法研究的相关技术人员。; 使用场景及目标:①用于无人机在复杂地形或动态环境下的三维路径规划仿真研究;②比较不同智能优化算法(如PSO、GA、蚁群算法、RRT等)在路径规划中的性能差异;③为多目标优化问题提供算法选型和改进思路。; 阅读建议:建议读者结合文中提供的Matlab代码进行实践操作,重点关注算法的参数设置、适应度函数设计及路径约束处理方式,同时可参考文中提到的多种算法对比思路,拓展到其他智能优化算法的研究改进中。
标题中的"EthernetIP-master.zip"压缩文档涉及工业自动化领域的以太网通信协议EtherNet/IP。该协议由罗克韦尔自动化公司基于TCP/IP技术架构开发,已广泛应用于ControlLogix系列控制设备。该压缩包内可能封装了协议实现代码、技术文档或测试工具等核心组件。 根据描述信息判断,该资源主要用于验证EtherNet/IP通信功能,可能包含测试用例、参数配置模板及故障诊断方案。标签系统通过多种拼写形式强化了协议主题标识,其中"swimo6q"字段需结合具体应用场景才能准确定义其技术含义。 从文件结构分析,该压缩包采用主分支命名规范,符合开源项目管理的基本特征。解压后预期可获取以下技术资料: 1. 项目说明文档:阐述开发目标、环境配置要求及授权条款 2. 核心算法源码:采用工业级编程语言实现的通信协议栈 3. 参数配置文件:预设网络地址、通信端口等连接参数 4. 自动化测试套件:包含协议一致性验证和性能基准测试 5. 技术参考手册:详细说明API接口规范集成方法 6. 应用示范程序:展示设备数据交换的标准流程 7. 工程构建脚本:支持跨平台编译和部署流程 8. 法律声明文件:明确知识产权归属及使用限制 该测试平台可用于构建协议仿真环境,验证工业控制器现场设备间的数据交互可靠性。在正式部署前开展此类测试,能够有效识别系统兼容性问题,提升工程实施质量。建议用户在解压文件后优先查阅许可协议,严格遵循技术文档的操作指引,同时需具备EtherNet/IP协议栈的基础知识以深入理解通信机制。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值