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
类(组件),然后派生出MeleeEnemy
和RangedEnemy
,它们继承通用属性(如生命值)并重写(Override)攻击方法 (Attack()
) 来实现不同的攻击行为。 - 接口 (Interfaces): 接口在定义组件间的“契约”时非常有用,尤其是在需要解耦通信的场景。例如,任何可以被攻击的对象(玩家、敌人、可破坏的障碍物)都可以实现一个
IAttackable
接口,该接口定义了一个ReceiveDamage(int amount)
方法。这样,攻击方代码只需要知道目标是否是IAttackable
,而不需要关心其具体类型。
二、核心知识点:组件化设计
理解了 Unity 的哲学后,我们来深入探讨组件化设计的具体实践。
2.1 什么是组件?
在 Unity 中,组件可以看作是附加到游戏对象上的功能模块或行为单元。
- 内置组件: Unity 提供了大量内置组件,如
Transform
,Mesh Renderer
,Collider
,Rigidbody
,Animator
等,它们提供了游戏开发所需的基础功能。 - 脚本组件: 我们通过 C# 编写的、继承自
MonoBehaviour
的类,就是自定义的脚本组件。它们是我们实现游戏特定逻辑的主要方式。
把游戏对象想象成一个空的乐高底板,而组件就是各种形状和功能的乐高积木。通过将不同的积木(组件)拼装到底板(游戏对象)上,我们就能创造出丰富多彩的游戏实体。
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
等频繁调用的方法中使用它们,因为性能开销较大。最好在Start
或Awake
中调用一次并缓存结果。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 场景设定
- 创建一个新的 3D Unity 项目。
- 在场景中创建一个 Plane 作为地面。
- 创建一个 Cube,命名为 “Player”,并给它添加一个
Rigidbody
组件(取消 Use Gravity,勾选 Is Kinematic,或者你也可以实现移动逻辑)。 - 再创建一个 Cube,命名为 “Enemy”,给它添加一个
Rigidbody
组件。 - 给 “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
中,我们已经实现了核心的交互逻辑:
- 触发检测:
OnTriggerEnter
方法会在有其他Collider
进入标记为Is Trigger
的Sphere Collider
时被 Unity 自动调用。参数other
就是进入范围的那个对象的Collider
组件。 - 身份识别: 我们通过
other.GetComponent<PlayerIdentifier>()
来尝试获取碰撞对象上的PlayerIdentifier
脚本。如果返回的不是null
,说明进入范围的是挂载了PlayerIdentifier
脚本的对象,也就是我们的玩家。 - 通信:
- 从敌人到玩家:
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 原则。核心要点回顾:
- Unity 与 OOP 的融合: Unity 的核心是组件化设计,而 OOP 的封装、单一职责、继承、多态和接口原则极大地赋能了组件的设计与实现,使代码更模块化、可维护、可复用。
- 组件化设计: 将游戏对象的功能拆分成独立的、职责单一的组件(脚本),是 Unity 开发的关键思维模式,它提高了代码的灵活性和可扩展性。
- 脚本间通信: 组件化后,脚本间的通信变得至关重要。我们学习了几种常用方法:
- 直接引用 (
GetComponent
, Inspector 赋值): 简单直接,性能较好,但耦合度高。适用于关系紧密的组件。 SendMessage
系列: 性能差,类型不安全,不推荐常规使用。- 事件/委托: 低耦合,灵活性高,是复杂交互的推荐方式(后续详解)。
- 静态/单例: 适用于全局访问点。
选择哪种方式取决于具体场景下的耦合度、性能和维护性需求。
- 直接引用 (
- 实践应用: 通过玩家与敌人交互的实例,我们练习了使用
OnTriggerEnter
检测碰撞,使用GetComponent
获取其他对象的脚本引用,并直接调用方法实现通信。同时,了解了使用 Tag 或 Layer 进行优化的方法。
掌握如何在 Unity 中应用面向对象思想,特别是组件化设计和脚本间通信,是提升你游戏开发能力的重要一步。它能帮助你构建更大型、更复杂且更易于维护的游戏项目。