Unity 面向对象实战:掌握组件化设计与脚本通信,构建玩家敌人交互

news2025/4/1 17:07:20

Langchain系列文章目录

01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘

PyTorch系列文章目录

Python系列文章目录

C#系列文章目录

01-C#与游戏开发的初次见面:从零开始的Unity之旅
02-C#入门:从变量与数据类型开始你的游戏开发之旅
03-C#运算符与表达式:从入门到游戏伤害计算实践
04-从零开始学C#:用if-else和switch打造智能游戏逻辑
05-掌握C#循环:for、while、break与continue详解及游戏案例
06-玩转C#函数:参数、返回值与游戏中的攻击逻辑封装
07-Unity游戏开发入门:用C#控制游戏对象移动
08-C#面向对象编程基础:类的定义、属性与字段详解
09-C#封装与访问修饰符:保护数据安全的利器
10-如何用C#继承提升游戏开发效率?Enemy与Boss案例解析
11-C#多态性入门:从零到游戏开发实战
12-C#接口王者之路:从入门到Unity游戏开发实战 (IAttackable案例详解)
13-C#静态成员揭秘:共享数据与方法的利器
14-Unity 面向对象实战:掌握组件化设计与脚本通信,构建玩家敌人交互


文章目录

  • Langchain系列文章目录
  • PyTorch系列文章目录
  • Python系列文章目录
  • C#系列文章目录
  • 前言
  • 一、Unity 与面向对象:为何如此契合?
    • 1.1 Unity 的核心理念:组件化设计 (Component-Based Design)
    • 1.2 面向对象如何赋能组件化
  • 二、核心知识点:组件化设计
    • 2.1 什么是组件?
    • 2.2 组件化思维的优势
    • 2.3 如何设计良好的组件
  • 三、核心知识点:脚本间的通信
    • 3.1 为何需要通信?
    • 3.2 常见的通信方式
      • 3.2.1 直接引用 (Direct Reference)
        • (1)通过 `GetComponent<T>()` 获取
      • 3.2.2 `SendMessage` / `BroadcastMessage` / `SendMessageUpwards`
      • 3.2.3 事件/委托 (Events/Delegates) 与 C# 事件
      • 3.2.4 静态变量/单例模式 (Static Variables/Singleton Pattern)
    • 3.3 选择合适的通信方式
  • 四、实践:玩家与敌人交互
    • 4.1 场景设定
    • 4.2 创建 Player 脚本
    • 4.3 创建 Enemy 脚本
    • 4.4 实现交互逻辑
    • 4.5 可能遇到的问题与优化
  • 五、总结

前言

大家好!欢迎来到我们 C# 学习之旅的第 14 天。在过去的一周里,我们系统学习了 C# 面向对象编程(OOP)的核心概念,包括类、封装、继承、多态、接口以及静态成员。这些知识为我们构建结构清晰、可维护性强的代码打下了坚实的基础。然而,理论最终要服务于实践。今天,我们将聚焦于如何在强大的游戏引擎 Unity 中巧妙地应用这些 OOP 原则,特别是在游戏开发中最常见的场景:组件化设计脚本间的通信,并通过一个玩家与敌人交互的实例来巩固所学。理解如何在 Unity 的框架内运用 OOP,是 C# 游戏开发进阶的关键一步。


一、Unity 与面向对象:为何如此契合?

在我们深入具体技术点之前,首先需要理解 Unity 的核心设计哲学,以及为什么面向对象的思想能够与之完美融合,并极大地赋能游戏开发。

1.1 Unity 的核心理念:组件化设计 (Component-Based Design)

Unity 的架构并非严格意义上的传统面向对象,它更多地采用了实体-组件系统 (Entity-Component System, ECS) 的思想(尽管早期版本更偏向纯粹的组件化)。其核心理念是:

  • 游戏对象 (GameObject): 场景中的一切皆为游戏对象,但它们本身只是一个空的容器。
  • 组件 (Component): 负责赋予游戏对象特定的行为数据。例如,Transform 组件负责位置、旋转和缩放;Rigidbody 组件负责物理行为;我们编写的 C# 脚本(继承自 MonoBehaviour)也是一种组件,负责自定义逻辑。

这种设计的核心思想是组合优于继承。一个游戏对象可以通过挂载不同的组件组合,来实现复杂多样的功能,而不是通过复杂的继承树来定义。

1.2 面向对象如何赋能组件化

虽然 Unity 强调组件化,但面向对象的原则在设计和实现这些组件时至关重要:

  • 封装 (Encapsulation): 每个组件应该封装好自己的数据和逻辑,只暴露必要的接口(公共方法和属性)。这使得组件更加独立和内聚。例如,一个 PlayerHealth 组件应该封装玩家的生命值数据,并提供如 TakeDamage() 这样的公共方法来修改它,而不是让外部代码随意直接访问内部的 health 变量。
  • 单一职责原则 (Single Responsibility Principle - SRP): 这是 OOP 的重要设计原则,在组件设计中同样适用。一个组件应该只负责一项明确的功能。例如,将玩家的移动逻辑放在 PlayerMovement 组件中,将生命值管理放在 PlayerHealth 组件中,而不是将所有逻辑都塞进一个庞大的 Player 脚本。这提高了代码的可读性、可维护性和复用性。
  • 继承与多态 (Inheritance & Polymorphism): 虽然 Unity 鼓励组合,但在某些场景下,继承和多态依然有用。例如,你可以创建一个基础的 Enemy 类(组件),然后派生出 MeleeEnemyRangedEnemy,它们继承通用属性(如生命值)并重写(Override)攻击方法 (Attack()) 来实现不同的攻击行为。
  • 接口 (Interfaces): 接口在定义组件间的“契约”时非常有用,尤其是在需要解耦通信的场景。例如,任何可以被攻击的对象(玩家、敌人、可破坏的障碍物)都可以实现一个 IAttackable 接口,该接口定义了一个 ReceiveDamage(int amount) 方法。这样,攻击方代码只需要知道目标是否是 IAttackable,而不需要关心其具体类型。

二、核心知识点:组件化设计

理解了 Unity 的哲学后,我们来深入探讨组件化设计的具体实践。

2.1 什么是组件?

在 Unity 中,组件可以看作是附加到游戏对象上的功能模块或行为单元。

  • 内置组件: Unity 提供了大量内置组件,如 Transform, Mesh Renderer, Collider, Rigidbody, Animator 等,它们提供了游戏开发所需的基础功能。
  • 脚本组件: 我们通过 C# 编写的、继承自 MonoBehaviour 的类,就是自定义的脚本组件。它们是我们实现游戏特定逻辑的主要方式。

把游戏对象想象成一个空的乐高底板,而组件就是各种形状和功能的乐高积木。通过将不同的积木(组件)拼装到底板(游戏对象)上,我们就能创造出丰富多彩的游戏实体。

附加
附加
附加
附加
附加
GameObject
Transform 组件
Mesh Renderer 组件
Rigidbody 组件
PlayerMovement 脚本组件
PlayerHealth 脚本组件

2.2 组件化思维的优势

采用组件化思维进行开发,能带来诸多好处:

  • 模块化 (Modularity): 功能被拆分成独立的模块(组件),易于理解和管理。
  • 可重用性 (Reusability): 设计良好的组件可以在不同的游戏对象甚至不同的项目中使用。例如,一个通用的 Health 组件可以用于玩家、敌人、甚至可破坏的箱子。
  • 灵活性与可扩展性 (Flexibility & Scalability): 可以通过添加、移除或替换组件来轻松改变游戏对象的行为,而无需修改大量代码。需要新功能?添加一个新组件即可。
  • 解耦 (Decoupling): 组件之间应尽量减少直接依赖,使得修改一个组件不会轻易影响到其他组件。
  • 并行开发 (Parallel Development): 不同的开发者可以专注于开发不同的组件,提高团队协作效率。

2.3 如何设计良好的组件

遵循单一职责原则是关键。问问自己:这个组件的核心职责是什么?它是否做了太多事情?

  • 反例: 一个 Player 脚本处理移动、攻击、生命值、动画、物品栏、任务等所有逻辑。
  • 正例:
    • PlayerMovement: 处理输入和物理移动。
    • PlayerAttack: 处理攻击逻辑(输入、动画触发、伤害计算)。
    • PlayerHealth: 管理生命值、受伤和死亡逻辑。
    • PlayerAnimation: 控制动画状态机。
    • InventoryManager: 管理物品。
    • QuestLog: 管理任务。

将这些组件挂载到同一个 Player GameObject 上,它们协同工作,共同构成完整的玩家功能。


三、核心知识点:脚本间的通信

当我们将功能拆分到不同组件后,这些组件之间不可避免地需要进行交互和信息传递。这就是脚本间通信要解决的问题。

3.1 为何需要通信?

游戏逻辑往往涉及多个组件的协作:

  • 玩家的攻击脚本 (PlayerAttack) 需要通知敌人的生命值脚本 (EnemyHealth) 敌人受到了伤害。
  • 敌人的 AI 脚本 (EnemyAI) 可能需要获取玩家的位置信息(来自玩家的 Transform 组件或 PlayerMovement 脚本)。
  • 当玩家生命值降为零时,PlayerHealth 脚本可能需要通知游戏管理器脚本 (GameManager) 游戏结束。
  • UI 脚本 (UIHealthBar) 需要获取 PlayerHealth 脚本中的当前生命值来更新血条显示。

3.2 常见的通信方式

Unity 中实现脚本间通信有多种方法,各有优缺点:

3.2.1 直接引用 (Direct Reference)

这是最常用也最直观的方式。一个脚本持有对另一个脚本实例的引用。

(1)通过 GetComponent<T>() 获取

在一个脚本中,可以通过 GetComponent<T>() 方法获取同一个游戏对象上的其他组件。

// 假设此脚本和 PlayerHealth 脚本挂在同一个 GameObject 上
using UnityEngine;

public class PlayerEffects : MonoBehaviour
{
    private PlayerHealth playerHealth; // 存储对 PlayerHealth 组件的引用

    void Start()
    {
        // 在 Start 方法中获取 PlayerHealth 组件
        playerHealth = GetComponent<PlayerHealth>();

        // 健壮性检查:确保找到了组件
        if (playerHealth == null)
        {
            Debug.LogError("PlayerHealth component not found on this GameObject!");
        }
    }

    public void PlayDamageEffect()
    {
        // 调用 PlayerHealth 的方法或访问其公共属性
        if (playerHealth != null)
        {
            Debug.Log("Playing damage effect because health is: " + playerHealth.CurrentHealth);
            // 假设 PlayerHealth 有一个公共属性 CurrentHealth
        }
    }
}

// 假设的 PlayerHealth 脚本
public class PlayerHealth : MonoBehaviour
{
    [SerializeField] private int maxHealth = 100;
    public int CurrentHealth { get; private set; } // 公共只读属性

    void Awake()
    {
        CurrentHealth = maxHealth;
    }

    public void TakeDamage(int amount)
    {
        CurrentHealth -= amount;
        Debug.Log($"Player took {amount} damage. Current health: {CurrentHealth}");
        if (CurrentHealth <= 0)
        {
            Die();
        }

        // 通知 PlayerEffects 播放特效
        PlayerEffects effects = GetComponent<PlayerEffects>();
        if (effects != null)
        {
            effects.PlayDamageEffect(); // 直接调用另一个组件的方法
        }
    }

    void Die()
    {
        Debug.Log("Player Died!");
        // 处理死亡逻辑...
    }
}
  • 获取其他 GameObject 上的组件:

    • 公共变量与检视面板赋值: 在脚本中声明一个公共变量(或使用 [SerializeField] 标记的私有变量),然后在 Unity 编辑器的检视面板 (Inspector) 中将目标游戏对象或其上的组件拖拽过去。这是最推荐的方式之一,因为它清晰、直观且性能较好。

      using UnityEngine;
      
      public class EnemyAI : MonoBehaviour
      {
          [SerializeField] private Transform playerTransform; // 在 Inspector 中拖拽玩家对象
          [SerializeField] private PlayerHealth playerHealth; // 在 Inspector 中拖拽挂有 PlayerHealth 的对象
      
          void Update()
          {
              if (playerTransform != null)
              {
                  // 朝向玩家
                  transform.LookAt(playerTransform);
              }
          }
      
          void AttackPlayer()
          {
               if (playerHealth != null)
               {
                   playerHealth.TakeDamage(10); // 调用其他对象上的组件方法
               }
          }
      }
      
    • GameObject.Find() / FindObjectOfType<T>(): 这些方法可以在整个场景中查找游戏对象或特定类型的组件。强烈不建议Update 等频繁调用的方法中使用它们,因为性能开销较大。最好在 StartAwake 中调用一次并缓存结果。

      using UnityEngine;
      
      public class GameManager : MonoBehaviour
      {
          private PlayerHealth playerHealth;
      
          void Start()
          {
              // 查找场景中第一个 PlayerHealth 组件实例
              playerHealth = FindObjectOfType<PlayerHealth>();
              if (playerHealth == null)
              {
                   Debug.LogError("PlayerHealth not found in the scene!");
              }
      
              // 通过名字查找 GameObject,再获取组件(效率更低,且名字可能变化)
              // GameObject playerObject = GameObject.Find("PlayerObjectName");
              // if (playerObject != null)
              // {
              //     playerHealth = playerObject.GetComponent<PlayerHealth>();
              // }
          }
      }
      
  • 优点: 简单直观,易于理解和调试,性能较好(尤其是 Inspector 赋值和缓存 GetComponent 结果)。

  • 缺点: 增加了脚本之间的耦合度。如果被引用的脚本或游戏对象发生变化(例如重命名、删除或结构调整),可能会导致引用丢失(在 Inspector 中显示为 None)或代码出错(NullReferenceException)。

3.2.2 SendMessage / BroadcastMessage / SendMessageUpwards

这些方法允许脚本调用同一个游戏对象 (SendMessage)、自身及其所有子对象 (BroadcastMessage) 或自身及其所有父对象 (SendMessageUpwards) 上特定名称的方法,无需直接引用。

// 在某个脚本中
void DealDamageToPlayer()
{
    GameObject player = GameObject.Find("Player"); // 假设能找到玩家
    if (player != null)
    {
        // 调用 player GameObject 上所有脚本中的 "TakeDamage" 方法
        // 传递一个整数参数 10
        player.SendMessage("TakeDamage", 10, SendMessageOptions.DontRequireReceiver);
        // SendMessageOptions.DontRequireReceiver 表示如果找不到方法,也不会报错
    }
}

// 在 PlayerHealth 脚本中需要有对应的方法
public void TakeDamage(int amount) // 方法名必须匹配,参数类型也要匹配
{
    // ... 处理伤害 ...
}
  • 优点: 一定程度上降低了耦合,调用方不需要知道接收方的具体脚本类型,只需要知道方法名。
  • 缺点:
    • 性能较差: 底层使用反射,比直接方法调用慢很多。
    • 类型不安全: 方法名是字符串,编译器无法检查拼写错误,容易出错。
    • 重构困难: 如果修改了方法名,需要手动查找并修改所有 SendMessage 调用处。
    • 调试困难: 难以追踪是谁调用了方法。
    • 通常不推荐使用,除非有特殊需求且了解其性能影响。

3.2.3 事件/委托 (Events/Delegates) 与 C# 事件

这是更高级、更推荐的解耦通信方式。它允许一个脚本(发布者)发出事件信号,而其他脚本(订阅者)可以监听并响应这些信号,两者之间无需直接引用。我们将在后续的课程中(第 23、24、28 天)详细学习委托和事件。

  • 优点: 低耦合,发布者和订阅者相互独立;灵活性高,可以有多个订阅者;符合观察者设计模式。
  • 缺点: 相对直接引用,理解和实现稍微复杂一些。

3.2.4 静态变量/单例模式 (Static Variables/Singleton Pattern)

对于需要全局访问的数据或功能(如游戏管理器、配置数据),可以使用静态变量或单例模式。

// 简单的单例模式示例
using UnityEngine;

public class GameManager : MonoBehaviour
{
    // 静态实例,全局唯一
    public static GameManager Instance { get; private set; }

    public int Score { get; private set; }

    void Awake()
    {
        // 实现单例模式的核心逻辑
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject); // 可选:让 GameManager 在场景切换时不被销毁
        }
        else
        {
            Destroy(gameObject); // 如果已有实例,销毁自己
        }
    }

    public void AddScore(int points)
    {
        Score += points;
        Debug.Log("Score: " + Score);
        // 这里可以触发一个事件,通知 UI 更新分数显示
    }
}

// 其他任何脚本都可以通过 GameManager.Instance 访问
public class Enemy : MonoBehaviour
{
    void Die()
    {
        // 通知 GameManager 加分
        GameManager.Instance.AddScore(100);
        Destroy(gameObject);
    }
    // 假设在某个时机调用 Die()
}
  • 优点: 提供全局访问点,方便访问。
  • 缺点: 可能破坏封装,滥用会导致代码难以管理和测试;需要小心处理初始化顺序和生命周期。我们将在第 41 天详细讨论单例模式。

3.3 选择合适的通信方式

没有绝对最好的方式,需要根据具体场景权衡:

  • 紧密相关的组件 (通常在同一个 GameObject 上): GetComponent 或 Inspector 赋值通常足够,简单高效。
  • 不相关的对象,需要解耦: 事件/委托是理想选择。
  • 全局状态或管理器: 单例模式或静态类。
  • 避免使用: SendMessage 系列通常应避免,GameObject.Find 等查找方法应谨慎使用并缓存结果。

核心原则: 优先选择耦合度低、易于维护且性能满足需求的方式。


四、实践:玩家与敌人交互

现在,让我们通过一个简单的实例,将前面学到的组件化设计和脚本通信知识应用起来。我们将创建一个玩家和一个敌人,当玩家走进敌人的触发范围时,敌人会“发现”玩家,并在控制台输出信息。

4.1 场景设定

  1. 创建一个新的 3D Unity 项目。
  2. 在场景中创建一个 Plane 作为地面。
  3. 创建一个 Cube,命名为 “Player”,并给它添加一个 Rigidbody 组件(取消 Use Gravity,勾选 Is Kinematic,或者你也可以实现移动逻辑)。
  4. 再创建一个 Cube,命名为 “Enemy”,给它添加一个 Rigidbody 组件。
  5. 给 “Enemy” 添加一个 Sphere Collider 组件,并勾选 Is Trigger。调整其 Radius,使其代表敌人的“感知范围”。

4.2 创建 Player 脚本

创建一个新的 C# 脚本,命名为 PlayerIdentifier(或者更复杂的如 PlayerHealth),并将其附加到 “Player” 游戏对象上。这个脚本目前可以很简单,只是为了标识玩家。

// PlayerIdentifier.cs
using UnityEngine;

public class PlayerIdentifier : MonoBehaviour
{
    public string playerName = "Hero";

    void Start()
    {
        Debug.Log($"Player '{playerName}' initialized.");
    }

    public void DetectedByEnemy(string enemyName)
    {
        Debug.Log($"Player '{playerName}' has been detected by '{enemyName}'!");
        // 这里可以添加后续逻辑,比如玩家进入战斗状态
    }
}

4.3 创建 Enemy 脚本

创建一个新的 C# 脚本,命名为 EnemyAI,并将其附加到 “Enemy” 游戏对象上。

// EnemyAI.cs
using UnityEngine;

public class EnemyAI : MonoBehaviour
{
    public string enemyName = "Goblin";

    // 当其他 Collider 进入该触发器时调用 (需对方或自身有 Rigidbody)
    private void OnTriggerEnter(Collider other)
    {
        Debug.Log($"Trigger entered by: {other.gameObject.name}"); // 打印进入触发器的对象名字

        // 尝试获取进入触发器的对象上的 PlayerIdentifier 组件
        PlayerIdentifier player = other.GetComponent<PlayerIdentifier>();

        // 检查是否成功获取到了 PlayerIdentifier 组件
        if (player != null)
        {
            // 如果是玩家进入了范围,执行逻辑
            Debug.Log($"Enemy '{enemyName}' found the player: {player.playerName}");

            // 调用玩家脚本上的方法进行通信
            player.DetectedByEnemy(this.enemyName);

            // 在这里可以添加敌人进入攻击状态、追击状态等的逻辑
            // 例如:GetComponent<EnemyMovement>().StartChasing(player.transform);
        }
        else
        {
            Debug.Log($"'{other.gameObject.name}' is not the player.");
        }
    }

    // 当其他 Collider 离开该触发器时调用
    private void OnTriggerExit(Collider other)
    {
        PlayerIdentifier player = other.GetComponent<PlayerIdentifier>();
        if (player != null)
        {
            Debug.Log($"Player '{player.playerName}' left the detection range of '{enemyName}'.");
            // 在这里可以添加敌人停止追击、返回巡逻状态等的逻辑
        }
    }
}

4.4 实现交互逻辑

在上面的 EnemyAI.cs 中,我们已经实现了核心的交互逻辑:

  1. 触发检测: OnTriggerEnter 方法会在有其他 Collider 进入标记为 Is TriggerSphere Collider 时被 Unity 自动调用。参数 other 就是进入范围的那个对象的 Collider 组件。
  2. 身份识别: 我们通过 other.GetComponent<PlayerIdentifier>() 来尝试获取碰撞对象上的 PlayerIdentifier 脚本。如果返回的不是 null,说明进入范围的是挂载了 PlayerIdentifier 脚本的对象,也就是我们的玩家。
  3. 通信:
    • 从敌人到玩家: player.DetectedByEnemy(this.enemyName); 这一行直接调用了获取到的 PlayerIdentifier 脚本实例上的 DetectedByEnemy 方法,并将敌人的名字作为参数传递过去,实现了从敌人到玩家的通信。
    • 从玩家到敌人(潜在): 虽然本例中没有显式展示,但如果玩家需要主动与敌人交互(例如攻击),玩家的攻击脚本可以通过类似的方式(如 GetComponent<EnemyAI>()GetComponent<EnemyHealth>())获取敌人脚本的引用并调用其方法(如 TakeDamage())。

现在运行游戏,并将 “Player” Cube 移动到 “Enemy” Cube 的球形触发器范围内,观察 Console 窗口的输出。你会看到类似以下的日志:

Trigger entered by: Player
Enemy 'Goblin' found the player: Hero
Player 'Hero' has been detected by 'Goblin'!

当你将 “Player” 移出范围时,会看到:

Player 'Hero' left the detection range of 'Goblin'.

4.5 可能遇到的问题与优化

  • NullReferenceException: 最常见的问题是在调用 player.DetectedByEnemy() 之前没有检查 player 是否为 null。如果进入触发器的对象不是玩家(没有 PlayerIdentifier 组件),GetComponent 会返回 null,此时访问 null 的方法或属性就会抛出此异常。务必进行 if (player != null) 检查。

  • 性能:OnTriggerEnter/Stay 中频繁调用 GetComponent 可能有性能开销,尤其当触发器交互频繁时。如果需要持续交互,可以在 OnTriggerEnter 中获取引用并存储起来,在 OnTriggerExit 中清空引用。

  • 标签 (Tag) 与层 (Layer): 在实际项目中,更高效的识别对象的方式是使用标签 (Tag)层 (Layer)。可以在 OnTriggerEnter 中先检查 other.CompareTag("Player")other.gameObject.layer == LayerMask.NameToLayer("PlayerLayer"),这样可以快速过滤掉非玩家对象,只有在确认是玩家后才执行 GetComponent

    // 使用 Tag 优化
    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Player")) // 假设 Player GameObject 的 Tag 设置为 "Player"
        {
            PlayerIdentifier player = other.GetComponent<PlayerIdentifier>();
            if (player != null)
            {
                 Debug.Log($"Enemy '{enemyName}' found the player: {player.playerName}");
                 player.DetectedByEnemy(this.enemyName);
            }
            else
            {
                // 理论上设置了 Tag 应该有对应脚本,但也可能配置错误
                Debug.LogWarning("Object tagged as Player but missing PlayerIdentifier script!");
            }
        }
    }
    

五、总结

本文将 C# 面向对象的理论知识与 Unity 的实践相结合,深入探讨了如何在 Unity 游戏开发中有效运用 OOP 原则。核心要点回顾:

  1. Unity 与 OOP 的融合: Unity 的核心是组件化设计,而 OOP 的封装、单一职责、继承、多态和接口原则极大地赋能了组件的设计与实现,使代码更模块化、可维护、可复用。
  2. 组件化设计: 将游戏对象的功能拆分成独立的、职责单一的组件(脚本),是 Unity 开发的关键思维模式,它提高了代码的灵活性和可扩展性。
  3. 脚本间通信: 组件化后,脚本间的通信变得至关重要。我们学习了几种常用方法:
    • 直接引用 (GetComponent, Inspector 赋值): 简单直接,性能较好,但耦合度高。适用于关系紧密的组件。
    • SendMessage 系列: 性能差,类型不安全,不推荐常规使用。
    • 事件/委托: 低耦合,灵活性高,是复杂交互的推荐方式(后续详解)。
    • 静态/单例: 适用于全局访问点。
      选择哪种方式取决于具体场景下的耦合度、性能和维护性需求。
  4. 实践应用: 通过玩家与敌人交互的实例,我们练习了使用 OnTriggerEnter 检测碰撞,使用 GetComponent 获取其他对象的脚本引用,并直接调用方法实现通信。同时,了解了使用 Tag 或 Layer 进行优化的方法。

掌握如何在 Unity 中应用面向对象思想,特别是组件化设计和脚本间通信,是提升你游戏开发能力的重要一步。它能帮助你构建更大型、更复杂且更易于维护的游戏项目。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2324659.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

3. 第三放平台部署deepseek

有时候我们会发现使用deepseek服务器&#xff0c;异常卡顿&#xff0c;这是由于多方面原因造成的&#xff0c;比如说访问人数过多等。想要解决这个问题&#xff0c;我们可以选择第三方平台进行部署 第三方平台 我们可以选择的第三方平台很多&#xff0c;比如硅基流动、秘塔搜索…

【C++指针】搭建起程序与内存深度交互的桥梁(下)

&#x1f525;&#x1f525; 个人主页 点击&#x1f525;&#x1f525; 每文一诗 &#x1f4aa;&#x1f3fc; 往者不可谏&#xff0c;来者犹可追——《论语微子篇》 译文&#xff1a;过去的事情已经无法挽回&#xff0c;未来的岁月还可以迎头赶上。 目录 C内存模型 new与…

IEEE PDF Xpress校验出现 :字体无法嵌入问题以及pdf版本问题

文章目录 问题描述一、字体嵌入问题首先查看一下&#xff0c;哪些字体没有被嵌入查看window的font文件夹里的字体下载字体的网站修复字体嵌入问题 二、pdf版本不对 问题描述 在处理IEEE的camera ready的时候&#xff0c;提交到IEEE express的文件没有办法通过validate&#xf…

cookie详解

一、cookie出现原因 http是无状态的&#xff0c;浏览器无法记录当前是哪个人浏览的&#xff0c;所以出现了cookie 作用&#xff1a;会话状态管理&#xff08;用户登录状态、购物车、游戏分数&#xff09;、个性化设置&#xff08;主题、自定义设置&#xff09;、浏览器行为跟…

Mayo Clinic Platform在人工智能医疗领域的现状及启示意义研究

一、引言 1.1 研究背景与意义 在科技飞速发展的当下,人工智能(AI)已逐渐渗透至各个行业,医疗领域作为关乎人类生命健康的重要领域,也迎来了人工智能技术带来的深刻变革。人工智能医疗,作为人工智能与医疗行业深度融合的产物,正重塑着全球医疗的格局。 从全球范围来看,…

如何将 Java 应用做成 EXE 的可执行软件

目录 前言一、情景介绍二、实现步骤1. 打 Jar 包2. 编写 bat 批处理文件3. bat 转 exe 前言 最近使用 GUI 帮朋友写了一个软件&#xff0c;为了方便他处理工作上的重复性且很麻烦的事情&#xff0c;程序是使用 Java 写的&#xff0c;就不得不面对一个问题&#xff1a;我必须将…

第一篇:系统分析师首篇

目录 一、目标二、计划三、完成情况1.宏观思维导图2.过程中的团队管理和其它方面的思考 四、意外之喜(最少2点)1.计划内的明确认知和思想的提升标志2.计划外的具体事情提升内容和标志 一、目标 通过参加考试&#xff0c;训练学习能力&#xff0c;而非单纯以拿证为目的。 1.在复…

自动关机监控器软件 - 您的电脑节能助手

## 自动关机监控器 - 您的电脑节能助手 自动关机监控器是一款基于Python开发的实用工具&#xff0c;旨在帮助用户节省电力资源并延长电脑使用寿命。该程序通过监控用户的鼠标和键盘活动&#xff0c;在设定的无活动时间后自动关闭计算机&#xff0c;特别适合需要长时间离开电脑但…

线程概念与控制(中)

线程概念与控制&#xff08;上&#xff09;https://blog.csdn.net/Small_entreprene/article/details/146464905?sharetypeblogdetail&sharerId146464905&sharereferPC&sharesourceSmall_entreprene&sharefrommp_from_link我们经过上一篇的学习&#xff0c;接…

[GXYCTF2019]禁止套娃1 [GitHack] [无参数RCE]

Git基础 Git信息泄露原理解析及利用总结 - FreeBuf网络安全行业门户 CTF中的GIT泄露_ctf git泄露-CSDN博客 Git结构 dirsearch扫出来一大堆东西&#xff08;然而这些并没有什么屁用&#xff09; 但也算起码了解了git结构了吧 /.git/HEAD&#xff1a;表示当前HEAD指针的指…

从ChatGPT到AutoGPT——AI Agent的范式迁移

一、AI Agent的范式迁移 1. ChatGPT的局限性与Agent化需求 单轮对话的“工具属性” vs. 多轮复杂任务的“自主性” ChatGPT 作为强大的生成式AI,虽然能够进行连贯对话,但本质上仍然是“工具型”AI,依赖用户提供明确的指令,而无法自主规划和执行任务。 人类介入成本过高:提…

stock-pandas,一个易用的talib的替代开源库。

原创内容第841篇&#xff0c;专注智能量化投资、个人成长与财富自由。 介绍一个ta-lib的平替——我们来实现一下&#xff0c;最高价突破布林带上轨&#xff0c;和最低价突破布林带下轨的可视化效果&#xff1a; cross_up_upper stock[high].copy()# cross_up_upper 最高价突破…

Spring Cloud Gateway详细介绍简单案例

文章目录 1、Spring Cloud Gateway 详细介绍1.1. 统一入口&#xff08;Single Entry Point&#xff09;1.2. 请求路由&#xff08;Request Routing&#xff09;1.3. 负载均衡&#xff08;Load Balancing&#xff09;1.4. 流量控制&#xff08;Rate Limiting&#xff09;1.5. 身…

鸿蒙原生开发之状态管理V2

一、ArkTS状态变量的定义&#xff1a; State&#xff1a;状态&#xff0c;指驱动UI更新的数据。用户通过触发组件的事件方法&#xff0c;改变状态数据。状态数据的改变&#xff0c;引起UI的重新渲染。 在鸿蒙原生开发中&#xff0c;使用ArkTS开发UI的时候&#xff0c;我们可以…

矩阵中对角线的遍历问题【C++】

1&#xff0c;按对角线进行矩阵排序 题目链接&#xff1a;3446. 按对角线进行矩阵排序 - 力扣&#xff08;LeetCode&#xff09; 【题目描述】 对于一个m*n的矩阵grid&#xff0c;要求对该矩阵进行 变换&#xff0c;使得变换后的矩阵满足&#xff1a; 主对角线右上的所有对角…

[Lc4_dfs] 解数独 | 单词搜索

目录 1.解数独 题解 2.单词搜索 题解 1.解数独 链接&#xff1a;37. 解数独 编写一个程序&#xff0c;通过填充空格来解决数独问题。 数独的解法需 遵循如下规则&#xff1a; 数字 1-9 在每一行只能出现一次。数字 1-9 在每一列只能出现一次。数字 1-9 在每一个以粗实线…

day17 学习笔记

文章目录 前言一、数组的增删改查1.resize函数2.append函数3.insert函数4.delete函数5.argwhere函数6.unique函数 二、统计函数1.amax&#xff0c;amin函数2.ptp函数3.median函数4.mean函数5.average函数6.var&#xff0c;std函数 前言 通过今天的学习&#xff0c;我掌握了num…

自动语音识别(ASR)技术详解

语音识别&#xff08;Automatic Speech Recognition, ASR&#xff09;是人工智能和自然语言处理领域的重要技术&#xff0c;旨在将人类的语音信号转换为对应的文本。近年来&#xff0c;深度学习的突破推动语音识别系统从实验室走入日常生活&#xff0c;为智能助手、实时翻译、医…

git | 版本切换的相关指令

常见指令 git log --oneline #查看历史提交 git tag latest-backup # 对当前的提交进行标记&#xff0c;标记名为latest-backup git checkout -b old-version 55b16aa # 切换到[55b16aa]的提交中&#xff0c;并标记为[old-version]的分支 git checkout master …

19.OpenCV图像二值化

OpenCV图像二值化 图像二值化&#xff08;Binarization&#xff09;是图像预处理中的一种常用技术&#xff0c;其目的是将图像中的像素值分为两个类别——通常是“前景”和“背景”或者说0和255。二值化能够简化图像信息&#xff0c;为后续的形态学处理、边缘检测、目标识别等…