Unity设计模式实战:单例、观察者、状态模式与对象池应用解析
1. 项目概述为什么Unity项目需要设计模式如果你在Unity里写过几个项目尤其是那种功能越加越多、代码越来越乱的你肯定有过这样的体验想改一个角色的攻击逻辑结果发现UI也跟着出错了想加一个新道具得在五六个脚本里手动添加引用新人接手你的代码光是理清哪个脚本管什么就得花上一周。这感觉就像你的代码库变成了一个毛线团牵一发而动全身。“终极Unity开发手册Unity Design Patterns项目结构与使用教程”这个标题指向的正是解决这个问题的核心方法。它不是一个教你写“Hello World”的入门教程而是一份面向中高级开发者的“工程化生存指南”。简单来说设计模式就是前人总结出来的一套解决特定问题的“最佳实践”模板。在Unity开发中它们能帮你把代码从“能跑就行”的草稿状态重构为清晰、可维护、易扩展的工程化结构。为什么这很重要因为Unity的组件化开发模式GameObject MonoBehaviour虽然上手快但也特别容易写出“面条式代码”——所有逻辑都塞在Update里脚本之间相互紧耦合。一个典型的反面教材就是一个叫PlayerController的脚本里面同时处理移动、攻击、血量、UI更新、音效播放还直接引用了五六个其他GameObject。这种代码初期开发快但到了项目中期任何修改都伴随着巨大的风险和调试成本。设计模式的价值就在于提供了一套“语法”让你能用更优雅的方式组织代码。比如用观察者模式来处理事件通信UI和游戏逻辑就不用互相持有引用用状态模式来管理角色的复杂行为待机、移动、攻击、死亡避免用一堆布尔值和if-else用对象池模式来管理频繁创建销毁的子弹、特效大幅提升性能。这不仅仅是让代码“好看”更是为了团队协作、长期维护和项目稳定上线保驾护航。2. 核心设计模式与Unity适配性解析不是所有经典的设计模式都适合Unity。Unity基于C#和组件化架构有其独特的工作流如序列化、预制体、MonoBehaviour生命周期。盲目套用企业级后端开发的设计模式可能会让代码变得过度复杂。因此选择模式时必须考虑其在Unity环境下的“适配性”和“性价比”。2.1 基石模式单例模式与它的“安全变体”单例模式恐怕是Unity里被滥用最多的模式没有之一。它的初衷是确保一个类只有一个实例并提供一个全局访问点。在Unity中这常用于管理全局状态的类如GameManager、AudioManager、UIManager。经典但危险的实现public class GameManager : MonoBehaviour { public static GameManager Instance; private void Awake() { if (Instance ! null Instance ! this) { Destroy(gameObject); // 防止重复创建 } else { Instance this; DontDestroyOnLoad(gameObject); // 跨场景不销毁 } } // ... 其他方法 }注意这是最常见的写法但它有几个致命陷阱1.线程不安全虽然Unity主线程单线程但异步操作可能引发问题2.无法控制初始化时机如果另一个脚本在Awake中访问Instance而此时GameManager的Awake还未执行就会得到null3. 滥用DontDestroyOnLoad可能导致场景中有多个“不朽”的单例引发混乱。推荐的安全变体Lazy Initialization懒加载public class SoundManager : MonoBehaviour { private static SoundManager _instance; private static readonly object _lock new object(); private static bool _applicationIsQuitting false; public static SoundManager Instance { get { if (_applicationIsQuitting) { Debug.LogWarning([SoundManager] Instance already destroyed on application quit. Wont create again.); return null; } lock (_lock) { if (_instance null) { // 先在场景中查找是否已存在 _instance FindObjectOfTypeSoundManager(); if (_instance null) { // 动态创建 GameObject singletonObject new GameObject(typeof(SoundManager).Name); _instance singletonObject.AddComponentSoundManager(); } } return _instance; } } } private void Awake() { // 防止重复实例化 if (_instance ! null _instance ! this) { Destroy(gameObject); return; } _instance this; DontDestroyOnLoad(gameObject); // 初始化音频资源等... } private void OnApplicationQuit() { _applicationIsQuitting true; } private void OnDestroy() { if (_instance this) { _instance null; } } }这个实现通过属性访问器get来获取实例实现了懒加载第一次访问时才创建并加入了锁和退出标志更加健壮。但请记住单例应作为最后的选择。它本质上是全局变量会破坏代码的模块化和可测试性。优先考虑通过依赖注入或服务定位器模式来管理全局服务。2.2 解耦神器观察者模式与C#事件/UnityEvent观察者模式定义了对象间的一种一对多的依赖关系当一个对象主题状态改变时所有依赖它的对象观察者都会得到通知并自动更新。在Unity中这完美解决了脚本间直接调用紧耦合的问题。传统C#事件实现// 主题事件发布者 public class PlayerHealth : MonoBehaviour { public event Actionint OnHealthChanged; // 定义事件 public event Action OnPlayerDied; private int _currentHealth 100; public void TakeDamage(int damage) { _currentHealth - damage; OnHealthChanged?.Invoke(_currentHealth); // 触发事件 if (_currentHealth 0) { OnPlayerDied?.Invoke(); } } } // 观察者事件订阅者 public class HealthUI : MonoBehaviour { [SerializeField] private Text _healthText; private PlayerHealth _playerHealth; private void Start() { _playerHealth FindObjectOfTypePlayerHealth(); if (_playerHealth ! null) { // 订阅事件 _playerHealth.OnHealthChanged UpdateHealthUI; _playerHealth.OnPlayerDied HandlePlayerDeath; } } private void UpdateHealthUI(int newHealth) { _healthText.text $HP: {newHealth}; } private void HandlePlayerDeath() { _healthText.color Color.red; _healthText.text DEAD; } private void OnDestroy() { // 非常重要取消订阅防止内存泄漏 if (_playerHealth ! null) { _playerHealth.OnHealthChanged - UpdateHealthUI; _playerHealth.OnPlayerDied - HandlePlayerDeath; } } }这种方式的优点是类型安全、性能高。但需要手动管理订阅与取消订阅容易遗忘导致内存泄漏。UnityEvent适用于编辑器配置UnityEvent允许你在Inspector面板中可视化地绑定方法非常适合设计师或策划参与配置。public class GameEventTrigger : MonoBehaviour { // 定义一个UnityEvent可以在Inspector中拖拽赋值 public UnityEvent OnTriggerEntered; private void OnTriggerEnter(Collider other) { if (other.CompareTag(Player)) { OnTriggerEntered?.Invoke(); // 触发事件 } } }然后在Inspector中你可以将任何 GameObject 上任何脚本的公有方法无参或一个简单参数拖拽到OnTriggerEntered事件的列表里。这种方式耦合度更低但反射调用有一定性能开销且不适合复杂数据传输。实操心得对于高频触发的事件如每帧更新使用C#事件。对于编辑器友好、配置驱动的一次性事件如触发器、按钮点击使用UnityEvent。可以结合两者用C#事件做核心逻辑通信暴露一个UnityEvent作为对外接口供非程序员配置。2.3 行为管理大师状态模式当你的角色或对象拥有多种状态如待机、移动、跳跃、攻击并且状态间的转换逻辑复杂时一堆bool和if-else会让代码难以维护。状态模式通过将每个状态封装成一个独立的类来解决这个问题。基础实现框架// 状态基类 public abstract class PlayerState { protected PlayerController _player; public PlayerState(PlayerController player) { _player player; } public abstract void EnterState(); public abstract void UpdateState(); public abstract void ExitState(); } // 具体状态待机 public class IdleState : PlayerState { public IdleState(PlayerController player) : base(player) { } public override void EnterState() { _player.Animator.Play(Idle); Debug.Log(进入待机状态); } public override void UpdateState() { // 检查输入决定是否切换到移动或跳跃状态 if (Input.GetAxisRaw(Horizontal) ! 0) { _player.ChangeState(new MoveState(_player)); } if (Input.GetButtonDown(Jump)) { _player.ChangeState(new JumpState(_player)); } } public override void ExitState() { Debug.Log(退出待机状态); } } // 具体状态移动 public class MoveState : PlayerState { public MoveState(PlayerController player) : base(player) { } public override void EnterState() { _player.Animator.Play(Run); } public override void UpdateState() { float moveInput Input.GetAxisRaw(Horizontal); _player.Rigidbody.velocity new Vector2(moveInput * _player.MoveSpeed, _player.Rigidbody.velocity.y); // 状态转换逻辑... if (Mathf.Abs(moveInput) 0.1f) { _player.ChangeState(new IdleState(_player)); } } public override void ExitState() { } } // 上下文/控制器 public class PlayerController : MonoBehaviour { [SerializeField] private float _moveSpeed 5f; public float MoveSpeed _moveSpeed; public Animator Animator { get; private set; } public Rigidbody2D Rigidbody { get; private set; } private PlayerState _currentState; private void Start() { Animator GetComponentAnimator(); Rigidbody GetComponentRigidbody2D(); // 初始状态 _currentState new IdleState(this); _currentState.EnterState(); } private void Update() { _currentState?.UpdateState(); } public void ChangeState(PlayerState newState) { _currentState?.ExitState(); _currentState newState; _currentState.EnterState(); } }优势符合开闭原则新增状态如“蹲下”、“攀爬”只需新建一个类无需修改现有状态类。逻辑清晰每个状态的行为和转换条件都封装在各自类中易于理解和调试。避免状态冲突不再需要管理一堆isMoving,isJumping,isAttacking的布尔值组合。注意事项对于简单状态机少于4个状态转换简单使用枚举和switch语句可能更轻量。状态模式更适合复杂、有层次子状态或需要共享数据的状态机。2.4 性能优化利器对象池模式在射击游戏、特效系统中频繁地Instantiate和Destroy游戏对象是性能杀手。对象池模式预先创建一组对象池使用时从池中取出不用时放回避免重复的内存分配与回收。一个简单的通用对象池实现using System.Collections.Generic; using UnityEngine; public class ObjectPool : MonoBehaviour { [System.Serializable] public class Pool { public string tag; // 用于标识不同种类的对象 public GameObject prefab; public int size; // 初始池大小 } public ListPool pools; public Dictionarystring, QueueGameObject poolDictionary; // 单例化以便全局访问此处仅为示例也可通过依赖注入 public static ObjectPool Instance; private void Awake() { Instance this; poolDictionary new Dictionarystring, QueueGameObject(); foreach (Pool pool in pools) { QueueGameObject objectPool new QueueGameObject(); for (int i 0; i pool.size; i) { GameObject obj Instantiate(pool.prefab); obj.SetActive(false); // 初始设置为非激活 objectPool.Enqueue(obj); } poolDictionary.Add(pool.tag, objectPool); } } // 从池中取出对象 public GameObject SpawnFromPool(string tag, Vector3 position, Quaternion rotation) { if (!poolDictionary.ContainsKey(tag)) { Debug.LogWarning($Pool with tag {tag} doesnt exist.); return null; } // 如果池空了动态扩容可选 if (poolDictionary[tag].Count 0) { Debug.Log($Pool {tag} is empty, instantiating a new one.); GameObject newObj Instantiate(pools.Find(p p.tag tag).prefab); newObj.SetActive(false); poolDictionary[tag].Enqueue(newObj); } GameObject objectToSpawn poolDictionary[tag].Dequeue(); objectToSpawn.SetActive(true); objectToSpawn.transform.position position; objectToSpawn.transform.rotation rotation; // 调用对象上的“OnSpawn”方法进行初始化如果存在 IPooledObject pooledObj objectToSpawn.GetComponentIPooledObject(); pooledObj?.OnObjectSpawn(); return objectToSpawn; } // 将对象放回池中 public void ReturnToPool(string tag, GameObject objectToReturn) { if (!poolDictionary.ContainsKey(tag)) { Debug.LogWarning($Pool with tag {tag} doesnt exist. Destroying object instead.); Destroy(objectToReturn); return; } objectToReturn.SetActive(false); poolDictionary[tag].Enqueue(objectToReturn); } } // 可选的接口用于对象被取出池时的初始化 public interface IPooledObject { void OnObjectSpawn(); } // 使用示例子弹脚本 public class Bullet : MonoBehaviour, IPooledObject { public float speed 10f; public float lifeTime 2f; private float _timer; public void OnObjectSpawn() { _timer lifeTime; // 每次被取出时重置计时器 GetComponentRigidbody().velocity transform.forward * speed; } private void Update() { _timer - Time.deltaTime; if (_timer 0) { // 不再使用Destroy而是放回对象池 ObjectPool.Instance.ReturnToPool(Bullet, this.gameObject); } } private void OnCollisionEnter(Collision collision) { // 碰撞后也放回池中 ObjectPool.Instance.ReturnToPool(Bullet, this.gameObject); } }关键点初始化在Awake中预先实例化所有对象并设为非激活存入队列。取出Spawn从队列头部取出对象激活设置位置/旋转并调用其初始化方法。放回Return将对象设为非激活放回队列尾部。动态扩容当池为空时可以选择即时实例化一个新对象避免游戏卡顿但这违背了预分配的本意。更好的做法是根据游戏数据分析设定一个足够大的初始池大小。实操心得对象池不仅用于子弹、敌人也适用于UI元素如伤害数字、音频源AudioSource、粒子特效。对于粒子系统放回池时别忘了调用ParticleSystem.Clear()和ParticleSystem.Stop(true)来重置状态。3. 构建清晰可维护的Unity项目结构有了设计模式这些“武器”我们还需要一个合理的“战场布局”——即项目结构。一个混乱的文件夹结构是项目失控的开始。下面是一个经过多个项目验证的、适用于中小型Unity项目的推荐结构Assets/ ├── [ProjectName]/ // 以项目名命名的根文件夹避免资产商店插件污染 │ ├── Art/ │ │ ├── Materials/ │ │ ├── Models/ │ │ ├── Textures/ │ │ └── Sprites/ │ ├── Audio/ │ │ ├── Music/ │ │ ├── SFX/ │ │ └── Mixers/ │ ├── Prefabs/ │ │ ├── Characters/ │ │ ├── Environment/ │ │ ├── UI/ │ │ └── VFX/ │ ├── Scenes/ │ │ ├── 0_Bootstrap.unity // 启动场景用于初始化全局管理器 │ │ ├── 1_MainMenu.unity │ │ ├── 2_Level_01.unity │ │ └── _TestScenes/ // 存放测试用场景 │ ├── Scripts/ │ │ ├── Runtime/ // 游戏运行时代码 │ │ │ ├── Core/ // 核心系统单例管理器等 │ │ │ │ ├── GameManager.cs │ │ │ │ ├── AudioManager.cs │ │ │ │ └── PoolManager.cs │ │ │ ├── Data/ // 数据类ScriptableObject │ │ │ │ ├── ScriptableObjects/ │ │ │ │ └── Enums/ │ │ │ ├── Entities/ // 游戏实体玩家、敌人、NPC │ │ │ │ ├── Player/ │ │ │ │ │ ├── States/ // 状态模式的状态类 │ │ │ │ │ ├── PlayerController.cs │ │ │ │ │ └── PlayerHealth.cs │ │ │ │ └── Enemies/ │ │ │ ├── Systems/ // 功能系统输入、战斗、任务等 │ │ │ │ ├── Input/ │ │ │ │ ├── Combat/ │ │ │ │ └── Quest/ │ │ │ ├── UI/ // 用户界面 │ │ │ │ ├── Controllers/ │ │ │ │ ├── Views/ │ │ │ │ └── Widgets/ │ │ │ └── Utilities/ // 通用工具类、扩展方法 │ │ │ ├── Extensions/ │ │ │ ├── Helpers/ │ │ │ └── Editor/ // 编辑器扩展脚本需放在Editor文件夹下 │ │ └── ThirdParty/ // 修改过的第三方插件代码 │ ├── Settings/ // 项目设置文件ScriptableObject │ │ ├── GameSettings.asset │ │ └── InputSettings.asset │ └── StreamingAssets/ // 需要动态加载的资源 ├── Plugins/ // 原生插件、未修改的DLL ├── Standard Assets/ // Unity标准资源如使用 └── External Assets/ // 从Asset Store导入的原始插件便于更新和管理结构设计逻辑以项目名为根将所有项目自有资源包裹在内与外部插件完全隔离。更新或删除插件时不会误伤自己的资源。功能导向而非类型导向传统的Scripts、Models、Textures平铺结构在项目变大后很难导航。新的结构按功能模块如Entities/Player组织该模块所需的所有脚本、预制体、动画控制器都可以放在附近或通过子文件夹关联。这符合“高内聚、低耦合”的原则。Scripts/Runtime 与 Editor分离Runtime下的代码是游戏运行所需的。Editor文件夹及其子文件夹下的脚本只在Unity编辑器中运行用于制作工具、自定义Inspector等。Unity会自动处理这部分代码的编译分离。使用ScriptableObject管理数据将游戏配置如角色属性、武器数据、关卡信息从硬编码中剥离出来创建为.asset文件。这使策划能独立调整数值且支持版本管理。4. 实战应用设计模式重构一个玩家系统假设我们有一个典型的“面条式”玩家控制器脚本我们将使用状态模式和观察者模式对其进行重构。重构前问题代码public class MessyPlayerController : MonoBehaviour { public float moveSpeed 5f; public float jumpForce 10f; private Rigidbody2D rb; private Animator anim; private bool isGrounded; private bool isJumping; private bool isAttacking; private float attackTimer; public int health 100; public HealthBarUI healthBar; // 直接引用UI void Start() { rb GetComponentRigidbody2D(); anim GetComponentAnimator(); healthBar.SetMaxHealth(health); // 直接调用UI } void Update() { // 状态判断混乱条件交织 if (!isAttacking) { float move Input.GetAxis(Horizontal); rb.velocity new Vector2(move * moveSpeed, rb.velocity.y); anim.SetBool(IsRunning, Mathf.Abs(move) 0.1f); if (Input.GetButtonDown(Jump) isGrounded) { rb.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse); isJumping true; anim.SetTrigger(Jump); } } if (Input.GetMouseButtonDown(0) !isAttacking) { isAttacking true; attackTimer 0.5f; anim.SetTrigger(Attack); // 攻击逻辑...可能又涉及伤害计算、检测碰撞等 } if (isAttacking) { attackTimer - Time.deltaTime; if (attackTimer 0) isAttacking false; } // 血量更新直接耦合UI if (Input.GetKeyDown(KeyCode.H)) { TakeDamage(10); } } void TakeDamage(int damage) { health - damage; healthBar.SetHealth(health); // 紧耦合 if (health 0) Die(); } void Die() { /* ... */ } void OnCollisionEnter2D(Collision2D col) { /* 检测地面... */ } }重构后应用状态模式观察者模式1. 定义状态接口与具体状态// IPlayerState.cs public interface IPlayerState { void EnterState(PlayerStateMachine machine); void UpdateState(PlayerStateMachine machine); void ExitState(PlayerStateMachine machine); } // PlayerStateMachine.cs (上下文) public class PlayerStateMachine : MonoBehaviour { private IPlayerState _currentState; public PlayerMovement Movement { get; private set; } public PlayerCombat Combat { get; private set; } public Animator Animator { get; private set; } private void Awake() { Movement GetComponentPlayerMovement(); Combat GetComponentPlayerCombat(); Animator GetComponentAnimator(); // 初始状态 TransitionToState(new IdleState()); } private void Update() _currentState?.UpdateState(this); public void TransitionToState(IPlayerState newState) { _currentState?.ExitState(this); _currentState newState; _currentState.EnterState(this); } } // IdleState.cs public class IdleState : IPlayerState { public void EnterState(PlayerStateMachine machine) { machine.Animator.Play(Idle); } public void UpdateState(PlayerStateMachine machine) { if (Mathf.Abs(Input.GetAxisRaw(Horizontal)) 0.1f) machine.TransitionToState(new MoveState()); if (Input.GetButtonDown(Jump) machine.Movement.IsGrounded) machine.TransitionToState(new JumpState()); if (Input.GetMouseButtonDown(0)) machine.TransitionToState(new AttackState()); } public void ExitState(PlayerStateMachine machine) { } } // MoveState.cs, JumpState.cs, AttackState.cs 类似实现...2. 分离关注点移动、战斗、血量系统// PlayerHealth.cs (使用观察者模式) public class PlayerHealth : MonoBehaviour { public event Actionint OnHealthChanged; public event Action OnDeath; [SerializeField] private int _maxHealth 100; private int _currentHealth; private void Start() _currentHealth _maxHealth; public void TakeDamage(int damage) { _currentHealth Mathf.Max(0, _currentHealth - damage); OnHealthChanged?.Invoke(_currentHealth); // 发布事件 if (_currentHealth 0) OnDeath?.Invoke(); } } // HealthUI.cs (观察者) public class HealthUI : MonoBehaviour { [SerializeField] private Slider _healthSlider; private PlayerHealth _playerHealth; private void Start() { _playerHealth FindObjectOfTypePlayerHealth(); if (_playerHealth ! null) { _healthSlider.maxValue 100; // 可从PlayerHealth获取MaxHealth _healthSlider.value 100; _playerHealth.OnHealthChanged UpdateHealthUI; } } private void UpdateHealthUI(int newHealth) _healthSlider.value newHealth; private void OnDestroy() { /* 取消订阅 */ } }3. 使用ScriptableObject管理玩家数据// PlayerData.asset (ScriptableObject) [CreateAssetMenu(fileName NewPlayerData, menuName Game/Player Data)] public class PlayerData : ScriptableObject { public float moveSpeed 5f; public float jumpForce 10f; public int maxHealth 100; public float attackCooldown 0.5f; // 更多可配置参数... } // 在PlayerMovement等脚本中引用 public class PlayerMovement : MonoBehaviour { [SerializeField] private PlayerData _playerData; // 使用 _playerData.moveSpeed 等 }重构后代码变得清晰且职责单一PlayerStateMachine负责状态流转PlayerMovement只处理物理移动PlayerCombat只处理攻击逻辑PlayerHealth只管理血量并发布事件HealthUI只响应事件更新显示。新增一个“蹲下”状态只需创建CrouchState类并实现接口即可无需修改任何其他状态类的代码。这就是设计模式带来的可维护性和扩展性。5. 常见陷阱、性能考量与进阶模式即使理解了模式在实际应用中仍会踩坑。以下是一些高频问题与解决方案陷阱1单例的隐藏依赖与测试困难单例使类在全局可访问但也隐藏了依赖关系使得单元测试变得极其困难因为你无法轻松地模拟GameManager.Instance。解决方案考虑使用“依赖注入”或“服务定位器”模式。例如创建一个ServiceLocator类它提供注册和获取服务的功能但允许在测试时替换为模拟服务。public static class ServiceLocator { private static DictionaryType, object _services new DictionaryType, object(); public static void RegisterT(T service) _services[typeof(T)] service; public static T GetT() (T)_services[typeof(T)]; public static void Clear() _services.Clear(); } // 在游戏启动时注册 void Awake() { ServiceLocator.RegisterIAudioService(new AudioManager()); } // 在任何需要的地方获取 var audio ServiceLocator.GetIAudioService(); audio.PlaySound(shoot);陷阱2观察者模式的内存泄漏忘记取消订阅事件是C#事件内存泄漏的主要原因。如果观察者对象的生命周期短于发布者而它没有取消订阅发布者就会持有一个对已销毁对象的引用阻止其被垃圾回收。解决方案严格配对在OnEnable/Start订阅在OnDisable/OnDestroy取消订阅。使用弱事件对于某些场景可以考虑使用WeakReference或第三方弱事件库但这会增加复杂性。定义统一的清理接口让所有需要清理事件订阅的组件实现一个ICleanup接口在场景切换或对象销毁时统一调用。陷阱3过度设计Design Pattern Fever新手最容易犯的错误是“手里有把锤子看什么都像钉子”。为一个简单的、只有两个状态的角色实现完整的状态模式或者为仅在一处使用的对象创建复杂的工厂模式都是过度工程化。原则遵循YAGNIYou Ain‘t Gonna Need It和KISSKeep It Simple, Stupid原则。在模式带来的复杂性和它解决的问题的严重性之间做权衡。如果一段简单的switch语句就能清晰、稳定地工作那就不要引入状态模式。性能考量事件 vs UnityEventC#委托/事件在性能上远优于基于序列化和反射的UnityEvent。对于每帧触发或高频触发的事件务必使用C#事件。对象池的大小池大小设置过小会导致运行时频繁扩容实例化产生卡顿。设置过大会增加初始内存占用。需要通过性能分析工具如Unity Profiler监控特定对象的生成频率来设定合理的初始大小和扩容策略。状态模式的更新开销状态模式中每个状态类都是一个独立对象。频繁的状态切换每帧多次会产生微小的GC垃圾回收压力因为旧的State对象会被丢弃。对于极高性能要求的场景如成千上万个实体可以考虑使用“状态数据状态函数”的轻量级实现如用枚举标识状态用Action委托存储当前状态函数。进阶模式探索模型-视图- presenter (MVP) / 模型-视图-视图模型 (MVVM)对于复杂的UI系统如包含大量动态列表、表单的RPG游戏界面可以将UI逻辑与业务逻辑分离。数据Model变化通过Presenter或ViewModel自动同步到视图View极大提升UI代码的可测试性和可维护性。Unity的UniRx、Unity的UI Toolkit数据绑定对此有很好的支持。命令模式用于实现撤销/重做、输入命令队列、网络命令同步等。将操作封装成对象可以参数化、序列化、排队执行。策略模式定义一系列算法如不同的伤害计算方式、AI行为将它们封装起来并且可以互相替换。这让你可以在运行时动态改变对象的行为。掌握设计模式不是一蹴而就的关键在于理解其意图和适用场景而非死记硬背实现。最好的学习方式就是在你的下一个Unity项目中有意识地识别那些“代码坏味道”如过长的函数、紧密的耦合、散弹式修改然后尝试引入合适的设计模式进行重构。开始时可能会觉得繁琐但当你需要添加新功能或调试问题时你会感谢自己当初在架构上投入的精力。

相关新闻

HybridCLR:Unity全平台原生C#热更新终极方案深度解析

HybridCLR:Unity全平台原生C#热更新终极方案深度解析

1. 项目概述:为什么我们需要一个“终极”热更新方案? 做Unity开发的朋友,尤其是负责过线上项目维护的,一定对“热更新”这三个字又爱又恨。爱的是,它能让我们在不重新发布客户端、不打扰玩家的情况下,快速修…

2026/8/7 13:27:40 阅读更多 →
C语言字符串处理实战:状态机算法统计英文单词数

C语言字符串处理实战:状态机算法统计英文单词数

1. 项目缘起:一个看似简单却暗藏玄机的需求最近在辅导一个刚入门C语言的朋友,他遇到了一个经典的课后习题:写一个程序,统计一段英文文本中的单词个数。他一开始觉得这还不简单?不就是数空格吗?结果自己动手…

2026/8/7 9:36:51 阅读更多 →
深入了解广东省建设监理协会网站:赋能行业转型与质量提升的全面指南

深入了解广东省建设监理协会网站:赋能行业转型与质量提升的全面指南

在当今这个建筑行业飞速发展的时代,每一个参与其中的从业者,无论是身披安全帽的一线监理人员,还是坐在办公室里统筹全局的项目经理,亦或是负责把控整体方向的企业高管,都在寻找一种能够让自己更加安心、更加高效的工作方式。我们常说,工程是凝固的音乐,但在这首乐曲的背…

2026/8/7 19:08:24 阅读更多 →

最新新闻

ESP32固件手动加密实战:使用固定密钥保护Flash代码安全

ESP32固件手动加密实战:使用固定密钥保护Flash代码安全

1. 项目概述与核心需求解析最近在折腾一个基于ESP32的智能家居项目,想把固件烧录到设备里,但发现了一个挺头疼的问题:固件在传输和存储时是明文的。这意味着,如果有人从OTA服务器或者直接读取Flash,就能轻易拿到我的核…

2026/8/8 2:05:28 阅读更多 →
UE5 UMG高级UI布局实战:从数据驱动架构到性能优化

UE5 UMG高级UI布局实战:从数据驱动架构到性能优化

1. 项目概述:为什么UE5 UI布局值得你花时间?做UE5项目,UI这块儿,说简单也简单,拖拖控件就能出个界面;说难也难,一旦涉及到复杂的布局、动态适配、性能优化,新手和老手做出来的东西&a…

2026/8/8 2:05:28 阅读更多 →
ComfyUI-Manager下载加速终极指南:解锁多线程下载的完整解决方案

ComfyUI-Manager下载加速终极指南:解锁多线程下载的完整解决方案

ComfyUI-Manager下载加速终极指南:解锁多线程下载的完整解决方案 【免费下载链接】ComfyUI-Manager ComfyUI-Manager is an extension designed to enhance the usability of ComfyUI. It offers management functions to install, remove, disable, and enable var…

2026/8/8 2:05:28 阅读更多 →
从零到精通:用Ryujinx模拟器畅玩Switch游戏的完整指南

从零到精通:用Ryujinx模拟器畅玩Switch游戏的完整指南

从零到精通:用Ryujinx模拟器畅玩Switch游戏的完整指南 【免费下载链接】Ryujinx 用 C# 编写的实验性 Nintendo Switch 模拟器 项目地址: https://gitcode.com/GitHub_Trending/ry/Ryujinx 你是否曾经想过在电脑上体验Switch游戏的魅力?现在&#…

2026/8/8 2:05:28 阅读更多 →
深入解析Void函数:从原理到实战,掌握无返回值函数的设计与应用

深入解析Void函数:从原理到实战,掌握无返回值函数的设计与应用

1. 项目概述:从“空”到“有”的函数世界在编程世界里,函数是构建一切逻辑的基石。我们习惯于函数接收参数、处理数据,然后返回一个结果,就像一台自动售货机,你投入硬币(参数),它吐出…

2026/8/8 2:05:28 阅读更多 →
机械键盘润轴全攻略:从原理到实践,打造极致手感与Hi-Fi音效

机械键盘润轴全攻略:从原理到实践,打造极致手感与Hi-Fi音效

1. 项目概述:从“哒哒哒”到“哒哒哒”的质变如果你玩过机械键盘,肯定对“润轴”这个词不陌生。它就像键盘圈里的一个神秘仪式,新手听着玄乎,老手却乐此不疲。简单来说,润轴就是给机械键盘的轴体内部涂抹润滑脂&#x…

2026/8/8 2:04:28 阅读更多 →

日新闻

AI多智能体时代来临,读懂MCP与A2A架构,抢占企业数字化新风口

AI多智能体时代来临,读懂MCP与A2A架构,抢占企业数字化新风口

当下AI应用飞速普及,无数企业下场搭建智能体系统,可落地阶段难题接踵而至:上下文无限堆积频繁爆栈、AI工具调用准确率低下、Token成本居高不下、企业数据权限混乱暗藏安全隐患……很多团队卡在架构搭建环节,空有前沿技术概念&…

2026/8/8 0:00:07 阅读更多 →
PHP二维码生成终极指南:用chillerlan/php-qrcode打造专业级二维码

PHP二维码生成终极指南:用chillerlan/php-qrcode打造专业级二维码

PHP二维码生成终极指南:用chillerlan/php-qrcode打造专业级二维码 【免费下载链接】php-qrcode A PHP QR Code generator and reader with a user-friendly API. 项目地址: https://gitcode.com/gh_mirrors/ph/php-qrcode 在当今数字时代,二维码已…

2026/8/8 0:00:08 阅读更多 →
UniApp微信小程序隐私保护组件开发:从原理到实战

UniApp微信小程序隐私保护组件开发:从原理到实战

1. 项目缘起:为什么我们需要一个隐私保护通用组件?最近在维护一个基于uniapp开发的微信小程序矩阵时,我遇到了一个非常棘手的问题。随着平台对用户隐私保护的要求越来越严格,几乎每一个新版本发布,或者在某些特定机型&…

2026/8/8 0:00:08 阅读更多 →

周新闻

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

1. 从水管网络到最大流:一个核心问题的诞生想象一下,你是一个城市供水系统的总工程师。你的城市有多个水源(水库),需要通过一个复杂的地下管道网络,将水输送到各个居民区。每条管道都有其最大通水能力&…

2026/8/6 22:02:27 阅读更多 →
基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

2026/8/6 22:02:27 阅读更多 →
MATLAB xcorr函数详解:从互相关原理到四大实战应用

MATLAB xcorr函数详解:从互相关原理到四大实战应用

1. 从一次信号“找茬”说起:为什么我们需要互相关几年前,我在处理一组声学传感器数据时遇到了一个棘手的问题。我有两个麦克风记录了一段相同的音频信号,理论上它们接收到的声音波形应该非常相似,只是由于麦克风位置不同&#xff…

2026/8/7 23:24:08 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/7 17:02:37 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/7 23:54:54 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/7 17:02:36 阅读更多 →