八、设计模式


文档摘要

八、设计模式 八、 Unity3D中的设计模式 引言 在软件工程领域,设计模式是解决常见设计问题的可重用解决方案。它们是经过时间考验的最佳实践,能够帮助我们编写出更易于理解、维护和扩展的代码。在Unity3D游戏开发中,设计模式同样扮演着至关重要的角色。合理运用设计模式可以提升代码质量,优化项目结构,并提高开发效率。 8.1 设计模式概述 设计模式并非具体的代码或库,而是一套通用的解决问题的模板和指导思想。它描述了在特定上下文中反复出现的设计问题,并提供了一种经过验证的解决方案。学习和应用设计模式能够带来诸多益处: 提高代码可重用性: 设计模式提炼了通用的解决方案,可以在不同的项目中重复使用,减少重复劳动。 增强代码可维护性: 遵循设计模式的代码结构清晰,逻辑明确,易于理解和修改。

八、设计模式

八、 Unity3D中的设计模式

引言

在软件工程领域,设计模式是解决常见设计问题的可重用解决方案。它们是经过时间考验的最佳实践,能够帮助我们编写出更易于理解、维护和扩展的代码。在Unity3D游戏开发中,设计模式同样扮演着至关重要的角色。合理运用设计模式可以提升代码质量,优化项目结构,并提高开发效率。

8.1 设计模式概述

设计模式并非具体的代码或库,而是一套通用的解决问题的模板和指导思想。它描述了在特定上下文中反复出现的设计问题,并提供了一种经过验证的解决方案。学习和应用设计模式能够带来诸多益处:

  • 提高代码可重用性: 设计模式提炼了通用的解决方案,可以在不同的项目中重复使用,减少重复劳动。

  • 增强代码可维护性: 遵循设计模式的代码结构清晰,逻辑明确,易于理解和修改。

  • 提升代码可扩展性: 设计模式通常考虑了未来的变化和扩展,使代码更容易适应新的需求。

  • 促进团队沟通: 设计模式是通用的技术语言,能够帮助团队成员更好地沟通和协作。

  • 避免重复发明轮子: 设计模式是前人经验的总结,可以避免在解决已知问题上浪费时间。

设计模式通常被分为三大类:

  • 创建型模式 (Creational Patterns): 关注对象的创建机制,将对象的实例化过程抽象化,使代码更加灵活和独立于具体的对象创建方式。

  • 结构型模式 (Structural Patterns): 关注类和对象的组合,通过不同的组合方式来构建更大的结构,解决类或对象的组合问题。

  • 行为型模式 (Behavioral Patterns): 关注对象之间的交互和职责分配,定义对象之间的算法和职责分配,提高对象之间的协作性和灵活性。

在Unity3D开发中,我们经常会遇到各种复杂的设计问题,例如:如何有效地管理游戏对象?如何处理用户输入?如何实现复杂的AI行为?如何优化资源加载?设计模式能够为这些问题提供有效的解决方案,帮助我们构建更高效、更可靠的游戏系统。

8.2 创建型模式 (Creational Patterns)

创建型模式主要用于处理对象的创建过程。在Unity3D中,游戏对象和组件的创建是非常频繁的操作。合理使用创建型模式可以简化对象的创建流程,提高代码的灵活性和可维护性。

8.2.1 单例模式 (Singleton Pattern)

概念: 单例模式确保一个类只有一个实例,并提供一个全局访问点来访问该实例。

结构:

C# 代码示例 (Unity3D):

using UnityEngine; public class GameManager : MonoBehaviour { private static GameManager _instance; public static GameManager Instance { get { if (_instance == null) { _instance = FindObjectOfType<GameManager>(); if (_instance == null) { GameObject gameManagerObject = new GameObject("GameManager"); _instance = gameManagerObject.AddComponent<GameManager>(); } } return _instance; } } private void Awake() { if (_instance != null && _instance != this) { Destroy(gameObject); // 确保只有一个实例 return; } _instance = this; DontDestroyOnLoad(gameObject); // 场景切换不销毁 // 初始化游戏管理器 InitializeGame(); } private void InitializeGame() { Debug.Log("GameManager initialized."); // 执行游戏初始化逻辑 } public void StartGame() { Debug.Log("Game started!"); // 开始游戏逻辑 } public void PauseGame() { Debug.Log("Game paused."); // 暂停游戏逻辑 } }

代码详解:

  • _instance: 静态私有变量,用于存储单例实例。

  • Instance 属性: 静态公共属性,提供全局访问点。

    • get 访问器:

      • 检查 _instance 是否为空。

      • 如果为空,尝试在场景中查找 GameManager 组件。

      • 如果场景中不存在,动态创建一个 GameObject 并添加 GameManager 组件。

      • 返回 _instance

  • Awake() 方法:

    • 确保只有一个 GameManager 实例存在。如果已经存在其他实例,则销毁自身。

    • 使用 DontDestroyOnLoad() 防止在场景切换时被销毁。

    • 调用 InitializeGame() 进行初始化。

  • 私有构造函数 (虽然此处未使用,但通常单例模式的构造函数应该是私有的,防止外部直接创建实例)。

Unity3D 应用场景:

  • 游戏管理器 (GameManager): 管理游戏全局状态、资源、玩家信息等。

  • 音频管理器 (AudioManager): 控制游戏音效和背景音乐。

  • 输入管理器 (InputManager): 处理用户输入事件。

  • 资源管理器 (ResourceManager): 加载和管理游戏资源。

优点:

  • 全局唯一访问点: 方便在任何地方访问单例实例。

  • 节省资源: 避免重复创建对象,节省内存和性能。

  • 易于管理全局状态: 单例模式可以方便地管理全局共享的数据和状态。

缺点:

  • 违反单一职责原则: 单例类通常承担过多的职责,不易于维护和测试。

  • 紧耦合: 全局访问点可能导致代码之间的高度耦合,降低代码的灵活性。

  • 难以测试: 单例实例的全局性使得单元测试变得更加困难。

  • 可能导致全局状态污染: 不恰当的使用单例模式可能导致全局状态的混乱和不可预测性。

使用注意事项:

  • 谨慎使用单例模式,只在真正需要全局唯一实例的场景下使用。

  • 避免单例类承担过多的职责,保持其职责单一。

  • 注意单例模式可能带来的耦合性和测试困难。

  • 考虑使用依赖注入等技术来替代单例模式,以降低耦合性。

8.2.2 对象池模式 (Object Pool Pattern)

概念: 对象池模式维护一组可重用的对象,避免频繁创建和销毁对象,从而提高性能,尤其适用于创建和销毁对象开销较大的情况。

结构:

C# 代码示例 (Unity3D):

using UnityEngine; using System.Collections.Generic; public class BulletPool : MonoBehaviour { public GameObject bulletPrefab; public int poolSize = 20; private List<GameObject> pooledObjects; void Start() { pooledObjects = new List<GameObject>(); for (int i = 0; i < poolSize; i++) { GameObject obj = Instantiate(bulletPrefab); obj.SetActive(false); // 初始状态设为不激活 pooledObjects.Add(obj); } } public GameObject GetBullet() { for (int i = 0; i < pooledObjects.Count; i++) { if (!pooledObjects[i].activeInHierarchy) { return pooledObjects[i]; // 返回未激活的对象 } } // 如果池中没有可用对象,可以考虑扩展池子或者返回null GameObject obj = Instantiate(bulletPrefab); // 扩展池子 (可选) pooledObjects.Add(obj); return obj; } public void ReturnBullet(GameObject bullet) { bullet.SetActive(false); // 返回对象时设为不激活 // 可以重置对象的状态,例如位置、速度等 (可选) } }

代码详解:

  • bulletPrefab: 子弹预制体,用于创建子弹对象。

  • poolSize: 对象池初始大小。

  • pooledObjects: List 存储池中的子弹对象。

  • Start() 方法:

    • 初始化 pooledObjects 列表。

    • 预先创建 poolSize 个子弹对象,并将其设为不激活状态,添加到池中。

  • GetBullet() 方法:

    • 遍历 pooledObjects 列表,查找未激活的子弹对象。

    • 如果找到,返回该对象。

    • 如果池中没有可用对象,可以选择扩展池子(创建新的子弹对象并添加到池中)或者返回 null

  • ReturnBullet() 方法:

    • 将使用完毕的子弹对象设为不激活状态,放回对象池。

    • 可以选择重置对象的状态,例如位置、速度等,以便下次重用。

Unity3D 应用场景:

  • 子弹对象池: 游戏中频繁发射的子弹。

  • 特效对象池: 爆炸特效、粒子特效等。

  • 敌人对象池: 大量生成的敌人单位。

  • UI 元素对象池: 动态创建和销毁的 UI 元素,例如列表项。

优点:

  • 提高性能: 避免频繁创建和销毁对象,减少垃圾回收压力,提升游戏性能。

  • 内存优化: 限制了游戏中对象的数量,避免内存过度增长。

  • 对象重用: 可以重用已创建的对象,提高资源利用率。

缺点:

  • 预先分配内存: 对象池需要在启动时预先分配一定数量的对象,可能会占用一定的初始内存。

  • 对象池管理复杂性: 需要维护对象池的创建、获取、返回等逻辑,增加代码复杂性。

  • 不适用于所有对象: 对于生命周期较长或者数量较少的对象,对象池模式可能并不适用。

使用注意事项:

  • 合理设置对象池的大小,避免过大或过小。

  • ReturnObject() 方法中,根据需要重置对象的状态。

  • 对象池适用于频繁创建和销毁,且创建开销较大的对象。

  • 可以根据实际情况动态扩展对象池的大小。

  • 考虑使用 Unity 的 Pool 组件或者第三方对象池插件来简化对象池的实现。

8.3 结构型模式 (Structural Patterns)

结构型模式关注如何组合类和对象以形成更大的结构。在Unity3D中,游戏场景通常由大量的游戏对象和组件组成,结构型模式可以帮助我们更好地组织和管理这些复杂的结构。

8.3.1 适配器模式 (Adapter Pattern)

概念: 适配器模式将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

结构:

C# 代码示例 (Unity3D):

假设我们有一个旧的第三方支付系统 LegacyPaymentSystem,它的接口与我们现在的游戏系统不兼容。我们需要使用适配器模式来适配它。

// 旧的第三方支付系统接口 public class LegacyPaymentSystem { public void LegacyPay(string userAccount, float amount) { Debug.Log($"Legacy Payment System: Paying {amount} for user {userAccount}"); // ... 旧系统的支付逻辑 ... } } // 目标接口:我们游戏系统期望的支付接口 public interface IPaymentGateway { void Pay(string userId, decimal price); } // 适配器:将 LegacyPaymentSystem 适配到 IPaymentGateway 接口 public class LegacyPaymentAdapter : IPaymentGateway { private LegacyPaymentSystem _legacySystem; public LegacyPaymentAdapter(LegacyPaymentSystem legacySystem) { _legacySystem = legacySystem; } public void Pay(string userId, decimal price) { // 将游戏系统的支付请求转换为旧系统的支付请求 _legacySystem.LegacyPay(userId, (float)price); } } // 客户端代码 public class PaymentManager : MonoBehaviour { private IPaymentGateway _paymentGateway; void Start() { // 初始化支付网关,可以使用适配器来适配旧系统 _paymentGateway = new LegacyPaymentAdapter(new LegacyPaymentSystem()); // 使用适配器 // _paymentGateway = new NewPaymentGateway(); // 如果有新的支付系统,可以轻松切换 } public void PurchaseItem(string itemId, string userId, decimal itemPrice) { Debug.Log($"Processing purchase of item {itemId} for user {userId} at price {itemPrice}"); _paymentGateway.Pay(userId, itemPrice); // 使用统一的支付接口 } }

代码详解:

  • LegacyPaymentSystem: 旧的第三方支付系统,接口为 LegacyPay(string userAccount, float amount)

  • IPaymentGateway: 目标接口,游戏系统期望的支付接口,定义了 Pay(string userId, decimal price) 方法。

  • LegacyPaymentAdapter: 适配器类,实现了 IPaymentGateway 接口。

    • 内部组合了 LegacyPaymentSystem 实例。

    • Pay() 方法将游戏系统的支付请求参数转换为 LegacyPaymentSystem 的参数,并调用 LegacyPay() 方法。

  • PaymentManager: 客户端代码,使用 IPaymentGateway 接口进行支付操作。

    • 可以通过切换不同的 IPaymentGateway 实现类来使用不同的支付系统,而无需修改客户端代码。

Unity3D 应用场景:

  • 适配第三方库或插件: 当第三方库或插件的接口与现有系统不兼容时,可以使用适配器模式进行适配。

  • 兼容旧代码: 当需要集成旧代码到新系统中时,可以使用适配器模式来适配旧代码的接口。

  • 统一不同系统的接口: 当需要集成多个不同的系统,但希望使用统一的接口进行访问时,可以使用适配器模式。

优点:

  • 提高代码复用性: 可以重用已有的类,而无需修改其接口。

  • 提高系统灵活性: 可以轻松切换不同的适配器,而无需修改客户端代码。

  • 解耦客户端和具体实现: 客户端代码只需要依赖目标接口,而无需关心具体的实现细节。

缺点:

  • 增加代码复杂性: 引入适配器类会增加代码的类数量和复杂性。

  • 可能存在性能损耗: 适配器模式可能会引入额外的间接层,可能存在一定的性能损耗。

使用注意事项:

  • 适配器模式适用于接口不兼容的情况,如果接口只是略有不同,可以考虑直接修改代码。

  • 适配器类应该尽可能简单,只负责接口转换,避免承担过多的职责。

  • 考虑使用对象适配器或类适配器,根据实际情况选择合适的适配器类型。

8.3.2 装饰器模式 (Decorator Pattern)

概念: 装饰器模式动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式比生成子类更为灵活。

结构:

C# 代码示例 (Unity3D):

假设我们有一个 Player 组件,我们需要动态地给玩家添加不同的能力,例如加速、隐身、护盾等。可以使用装饰器模式来实现。

// 组件接口:玩家能力 public interface IPlayerAbility { void ApplyAbility(); } // 具体组件:基础玩家能力 public class BasePlayerAbility : IPlayerAbility { public virtual void ApplyAbility() { Debug.Log("Base player ability activated."); // 基础玩家能力逻辑 } } // 装饰器抽象类 public abstract class AbilityDecorator : IPlayerAbility { protected IPlayerAbility _ability; // 组合被装饰的组件 public AbilityDecorator(IPlayerAbility ability) { _ability = ability; } public virtual void ApplyAbility() { _ability.ApplyAbility(); // 调用被装饰组件的能力 } } // 具体装饰器:加速能力 public class SpeedBoostDecorator : AbilityDecorator { private float _speedMultiplier; public SpeedBoostDecorator(IPlayerAbility ability, float speedMultiplier) : base(ability) { _speedMultiplier = speedMultiplier; } public override void ApplyAbility() { base.ApplyAbility(); // 先执行基础能力 Debug.Log($"Speed boost activated! Speed multiplier: {_speedMultiplier}"); // 应用加速能力逻辑,例如修改玩家移动速度 } } // 具体装饰器:护盾能力 public class ShieldDecorator : AbilityDecorator { private float _shieldValue; public ShieldDecorator(IPlayerAbility ability, float shieldValue) : base(ability) { _shieldValue = shieldValue; } public override void ApplyAbility() { base.ApplyAbility(); // 先执行基础能力 Debug.Log($"Shield activated! Shield value: {_shieldValue}"); // 应用护盾能力逻辑,例如增加玩家防御力 } } // 客户端代码 (Player 组件) public class Player : MonoBehaviour { private IPlayerAbility _currentAbility; void Start() { // 初始能力为基础能力 _currentAbility = new BasePlayerAbility(); } void Update() { if (Input.GetKeyDown(KeyCode.Space)) { ActivateAbility(); } if (Input.GetKeyDown(KeyCode.Alpha1)) { // 添加加速能力装饰器 _currentAbility = new SpeedBoostDecorator(_currentAbility, 1.5f); } if (Input.GetKeyDown(KeyCode.Alpha2)) { // 添加护盾能力装饰器 _currentAbility = new ShieldDecorator(_currentAbility, 100f); } } void ActivateAbility() { _currentAbility.ApplyAbility(); // 调用当前能力 } }

代码详解:

  • IPlayerAbility: 组件接口,定义了玩家能力 ApplyAbility() 方法。

  • BasePlayerAbility: 具体组件,实现了基础玩家能力。

  • AbilityDecorator: 装饰器抽象类,继承自 IPlayerAbility,并组合了 IPlayerAbility 类型的 _ability 成员变量。

    • ApplyAbility() 方法调用被装饰组件的 ApplyAbility() 方法,实现功能的叠加。
  • SpeedBoostDecoratorShieldDecorator: 具体装饰器,继承自 AbilityDecorator

    • ApplyAbility() 方法中,先调用基类的 ApplyAbility() 方法,再添加额外的装饰功能。
  • Player 组件:客户端代码,使用 IPlayerAbility 接口来管理玩家能力。

    • 可以动态地添加和组合不同的装饰器,为玩家添加不同的能力。

Unity3D 应用场景:

  • 为游戏对象添加动态效果: 例如,给武器添加火焰特效、冰冻特效等。

  • 实现角色技能的组合: 例如,将不同的技能效果组合起来,形成更复杂的技能。

  • 动态修改组件的行为: 例如,根据游戏状态动态地修改敌人的 AI 行为。

  • UI 元素样式装饰: 例如,给按钮添加边框、背景色、阴影等装饰效果。

优点:

  • 比继承更灵活: 可以在运行时动态地添加和移除装饰器,而无需创建新的子类。

  • 遵循开闭原则: 可以在不修改原有代码的情况下,扩展对象的功能。

  • 避免类爆炸: 使用装饰器模式可以避免因为功能的组合而产生大量的子类。

缺点:

  • 增加代码复杂性: 引入装饰器类会增加代码的类数量和复杂性。

  • 可能导致对象层次结构复杂: 过多的装饰器嵌套可能会导致对象层次结构变得复杂,不易于理解和维护。

  • 装饰器顺序影响结果: 装饰器的应用顺序可能会影响最终的结果,需要仔细考虑装饰器的顺序。

使用注意事项:

  • 装饰器模式适用于需要动态地、透明地给对象添加职责的场景。

  • 装饰器类应该尽可能简单,只负责添加装饰功能,避免承担过多的职责。

  • 考虑装饰器的顺序对结果的影响,合理安排装饰器的应用顺序。

  • 避免过度使用装饰器,导致对象层次结构过于复杂。

8.4 行为型模式 (Behavioral Patterns)

行为型模式关注对象之间的交互和职责分配。在Unity3D中,游戏逻辑通常涉及到大量的对象交互和复杂的行为控制,行为型模式可以帮助我们更好地组织和管理这些交互和行为。

8.4.1 观察者模式 (Observer Pattern)

概念: 观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。当主题对象的状态发生改变时,所有观察者对象都会收到通知并更新自身。

结构:

C# 代码示例 (Unity3D):

在Unity3D中,我们可以使用委托 (delegate) 和事件 (event) 来实现观察者模式。假设我们需要在玩家生命值发生变化时通知 UI 更新和音效播放器。

using UnityEngine; using System; using UnityEngine.UI; // 主题:玩家生命值管理器 public class PlayerHealth : MonoBehaviour { public int maxHealth = 100; private int _currentHealth; // 事件:生命值改变时触发 public event Action<int> OnHealthChanged; public int CurrentHealth { get { return _currentHealth; } set { _currentHealth = Mathf.Clamp(value, 0, maxHealth); OnHealthChanged?.Invoke(_currentHealth); // 触发事件,通知观察者 if (_currentHealth <= 0) { Die(); } } } void Start() { _currentHealth = maxHealth; } public void TakeDamage(int damage) { CurrentHealth -= damage; Debug.Log($"Player took {damage} damage. Current health: {_currentHealth}"); } void Die() { Debug.Log("Player died!"); // 死亡逻辑 } } // 观察者 1:UI 生命值显示 public class HealthUI : MonoBehaviour { public Text healthText; void Start() { PlayerHealth playerHealth = FindObjectOfType<PlayerHealth>(); if (playerHealth != null) { playerHealth.OnHealthChanged += UpdateHealthUI; // 注册观察者 UpdateHealthUI(playerHealth.CurrentHealth); // 初始更新 } } void UpdateHealthUI(int currentHealth) { healthText.text = $"Health: {currentHealth}"; } } // 观察者 2:音效播放器 public class HealthSoundPlayer : MonoBehaviour { public AudioClip damageSound; public AudioClip deathSound; private AudioSource _audioSource; void Start() { _audioSource = GetComponent<AudioSource>(); PlayerHealth playerHealth = FindObjectOfType<PlayerHealth>(); if (playerHealth != null) { playerHealth.OnHealthChanged += PlayHealthSound; // 注册观察者 } } void PlayHealthSound(int currentHealth) { if (currentHealth < FindObjectOfType<PlayerHealth>().CurrentHealth) // 受到伤害 { _audioSource.PlayOneShot(damageSound); } if (currentHealth <= 0) // 死亡 { _audioSource.PlayOneShot(deathSound); } } }

代码详解:

  • PlayerHealth: 主题 (Subject) 类,管理玩家生命值。

    • OnHealthChanged: 事件,当生命值改变时触发。

    • CurrentHealth 属性:设置生命值时,触发 OnHealthChanged 事件。

  • HealthUI: 观察者 (Observer) 类,负责更新 UI 生命值显示。

    • Start() 方法中,获取 PlayerHealth 组件,并注册 UpdateHealthUI 方法到 OnHealthChanged 事件。

    • UpdateHealthUI() 方法更新 UI 文本。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U