Unity 中的设计模式(C#)简述

在这里插入图片描述

在 Unity 环境中,设计模式是游戏开发人员遇到的常见问题的通用解决方案。将它们视为解决游戏开发中特定挑战的经过验证的模板或蓝图。以下是一些简单易懂的设计模式:

1. 单例Singleton=>

  • 单例模式确保一个类只有一个实例,并提供对该实例的全局访问点。
  • 在 Unity 中,您可以使用单例模式来创建应在应用程序的整个生命周期中存在的管理器或控制器的单个实例。例如,GameManager 或 UIManager 可以实现为单例。
   private static AudioManager _instance;

    public static AudioManager Instance => _instance;
    
    private void Awake()
    {
        if (_instance == null)
        {
            _instance = this;
            
        }
        else
        {
            Destroy(gameObject);
        }    
    }

DontDestroyOnLoad:

  • 如果您希望 Singleton 实例在场景之间持续存在,您可能需要使用 DontDestroyOnLoad 来防止它在加载新场景时被销毁。
    修改示例:
private void Awake()
{
    if (_instance == null)
    {
        _instance = this;
        DontDestroyOnLoad(gameObject);
    }
    else
    {
        Destroy(gameObject);
    }
}

单例模式虽然简单且使用广泛,但确实存在一些缺点和问题。以下是一些注意事项:

紧耦合 :
- 使用单例会在代码的不同部分之间引入紧密耦合。依赖于 Singleton 的组件直接依赖于它的存在和行为,这使得在不影响系统其他部分的情况下修改或替换 Singleton 变得更加困难。
直接访问变量和函数:
- 由于Singleton实例通常是通过静态属性(例如AudioManager.Instance)访问的,因此外部脚本可以直接访问和修改变量并调用Singleton类的函数。这可能会导致意想不到的后果,并使控制和管理类的状态变得困难。
全局访问:
- Singleton 提供对其实例的全局访问,允许代码库的任何部分访问其函数和变量。这破坏了封装,因为类的内部细节暴露给整个应用程序,并且任何外部脚本都可能干扰其状态。

2. 服务定位器Service Locator=>

现在,假设您的游戏中有各种服务或管理器,并且您需要一种集中的方式来管理和访问它们。输入服务定位器模式。

在单例模式中:

- 在单例模式中,每个需要单个实例的类(例如 AudioManager、UIManager)通常管理自己的实例。每个类负责确保其自身只有一个实例。

在服务定位器模式中:

- 有一个单独的类(例如 ServiceLocator 类)负责管理各种服务的实例。需要访问这些服务的其他类本身并不创建实例,而是从中央服务定位器请求服务。
- 这样,您的类不会自己创建实例,而是向 ServiceLocator 询问它们所需的服务。
- 需要服务的组件不需要知道如何创建或定位它们,从而促进应用程序不同部分之间的松散耦合。

让我们实现一个简单的服务定位器:

using System;
using System.Collections.Generic;
using UnityEngine;

public class ServiceLocator
{
    private static ServiceLocator _instance;
    private Dictionary<Type, object> services = new Dictionary<Type, object>();

    public static ServiceLocator Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new ServiceLocator();
            }
            return _instance;
        }
    }

    public void RegisterService<T>(T service)
    {
        services[typeof(T)] = service;
    }

    public T GetService<T>()
    {
        return (T)services[typeof(T)];
    }
}
现在,让我们看看如何在 Unity 项目中使用此服务定位器。
假设我们的游戏中有两个管理器:AudioManager 和 GameManager。我们将使用服务定位器,而不是直接创建实例。
public class AudioManager : MonoBehaviour
{
    // AudioManager methods...

    private void Start()
    {
        // Register AudioManager with the Service Locator
        ServiceLocator.Instance.RegisterService(this);  
        // or 
        ServiceLocator.Instance.RegisterService(new AudioManager);   

    }
}



public class GameManager : MonoBehaviour
{
    // GameManager methods...

    private void Start()
    {
        // Register GameManager with the Service Locator
        ServiceLocator.Instance.RegisterService(this);
    }
}
现在,代码的任何部分都可以通过服务定位器访问这些管理器。
public class PlayerController : MonoBehaviour
{
    private AudioManager audioManager;
    private GameManager gameManager;

    private void Start()
    {
        // Access services through the Service Locator
        audioManager = ServiceLocator.Instance.GetService<AudioManager>();
        gameManager = ServiceLocator.Instance.GetService<GameManager>();


        // Use audioManager and gameManager as needed...
        gameManager.StartGame();  
    }

    
}
服务定位器模式是用于管理 Unity 中的依赖项和服务的强大工具。谨慎使用它并根据项目的特定需求考虑依赖注入等替代方案至关重要。
两种模式(单例和服务定位器)都可能会破坏封装,但与全局单例实例相比,服务定位器可能会提供对服务可见性的更多控制。

3.观察者模式Observer Pattern=>

想象一下,您有一款游戏,其中各种游戏元素需要对玩家得分的变化做出反应。观察者模式就像拥有一个通知系统,当特定事件发生时,对象(观察者)可以订阅并接收更新。
让我们考虑一个简单的 Unity 示例,其中我们有一个 ScoreManager 来跟踪玩家的得分,并且我们希望其他游戏元素在得分变化时做出反应。
public class ScoreManager : MonoBehaviour
{
    private int score;

    public delegate void ScoreChangedDelegate(int newScore);
    public static event ScoreChangedDelegate OnScoreChanged;

    public void UpdateScore(int points)
    {
        score += points;
        NotifyObservers();
    }

    private void NotifyObservers()
    {
        if (OnScoreChanged != null)
        {
            OnScoreChanged(score);
        }
    }
}
现在,假设我们有一个 UIManager,想要在分数发生变化时更新分数显示:
public class UIManager : MonoBehaviour
{
    private void OnEnable()
    {
        ScoreManager.OnScoreChanged += UpdateScoreDisplay;
    }

    private void OnDisable()
    {
        ScoreManager.OnScoreChanged -= UpdateScoreDisplay;
    }

    private void UpdateScoreDisplay(int newScore)
    {
        Debug.Log($"Score Updated: {newScore}");
        // Update UI elements with the new score
    }
}
- 在此示例中,UIManager 订阅 ScoreManager 的 OnScoreChanged事件。当分数发生变化时,UIManager 会收到通知并相应地更新 UI。
- 让我们以卫生系统为例。您有一个具有生命值的 Player 类,并且各种 UI 元素和敌人有兴趣了解玩家的生命值何时发生变化。
using System;
using UnityEngine;

public class Player : MonoBehaviour
{
    public event Action<int> OnHealthChanged; // Event to notify observers

    private int health = 100;

    public void TakeDamage(int damage)
    {
        health -= damage;
        OnHealthChanged?.Invoke(health); // Notify observers about the health change
    }
}

public class HealthUI : MonoBehaviour
{
    private void OnEnable()
    {
        // Subscribe to the player's health change event
        FindObjectOfType<Player>().OnHealthChanged += UpdateHealthUI;
    }

    private void OnDisable()
    {
        // UnSubscribe to the player's health change event
        FindObjectOfType<Player>().OnHealthChanged -= UpdateHealthUI;
    }

    private void UpdateHealthUI(int health)
    {
        // Update UI based on the player's health
        Debug.Log("Health UI Updated: " + health);
    }
}
在此示例中,Player 类是主体,HealthUI 类是观察者。当玩家受到伤害时,会触发 OnHealthChanged 事件,并通知所有订阅的观察者。
何时使用:
  • 当对象之间存在一对多依赖关系,并且您想要通知多个对象有关另一个对象的更改时。
  • 当您想要建立一个松散耦合的系统时,其中对象不需要明确地相互了解。
  • 当您有多个对象需要做出反应的事件或状态更改时。
观察者模式通常用于 UI 更新、事件处理或多个对象需要响应另一个对象中的更改而无需在它们之间紧密耦合的任何场景。

4. 状态模式State Pattern=>

假设您有一个游戏角色,根据其状态(例如闲置、行走、跳跃),它的行为会有所不同。状态模式允许您将这些状态表示为单独的类并在它们之间无缝切换。
让我们举一个简单的例子,Unity 中的玩家角色可以处于不同的状态:空闲、行走和跳跃。
// State interface
public interface IPlayerState
{
    void HandleInput(); // How the state reacts to player input
    void PerformAction(); // A state-specific action method
}
// Concrete state for idle
public class IdleState : IPlayerState
{
    public void HandleInput()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Debug.Log("Transitioning to Jumping state");
            Player.Instance.ChangeState(new JumpingState());
        }
        else if (Input.GetAxis("Horizontal") != 0)
        {
            Debug.Log("Transitioning to Walking state");
            Player.Instance.ChangeState(new WalkingState());
        }
    }

    public void PerformAction()
    {
        // Idle state specific   logic
    }
}

// Concrete state for walking
public class WalkingState : IPlayerState
{
    public void HandleInput()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Debug.Log("Transitioning to Jumping state");
            Player.Instance.ChangeState(new JumpingState());
        }
        else if (Input.GetAxis("Horizontal") == 0)
        {
            Debug.Log("Transitioning to Idle state");
            Player.Instance.ChangeState(new IdleState());
        }
    }

    public void PerformAction()
    {
        // Walking state specific   logic
    }
}

// Concrete state for jumping
public class JumpingState : IPlayerState
{
    public void HandleInput()
    {
        if (Input.GetAxis("Horizontal") != 0)
        {
            Debug.Log("Transitioning to Walking state");
            Player.Instance.ChangeState(new WalkingState());
        }
    }

    public void PerformAction()
    {
        // Jumping state specific   logic
    }
}
// Context class (Player)
public class Player : MonoBehaviour
{
    private IPlayerState currentState;

    // Singleton instance for simplicity
    public static Player Instance { get; private set; }

    private void Awake()
    {
        Instance = this;
        ChangeState(new IdleState()); // Initial state
    }

    public void ChangeState(IPlayerState newState)
    {
        currentState = newState;
        Debug.Log($"Changed to {newState.GetType().Name} state");
    }

    private void Update()
    {
        currentState.HandleInput();
        currentState.PerformAction();
    }
}

怎么运行的:

The Player class acts as the context, and it delegates state-specific behavior to different state objects implementing the IPlayerState interface.

Player 类充当上下文,它将特定于状态的行为委托给实现 IPlayerState 接口的不同状态对象。

Each concrete state class (IdleState, WalkingState, JumpingState) handles input and update logic specific to that state.

每个具体状态类(IdleState、WalkingState、JumpingState)处理特定于该状态的输入和更新逻辑。

The Player class can switch between states dynamically, allowing the character to seamlessly transition between idle, walking, and jumping.

Player 类可以在状态之间动态切换,允许角色在闲置、行走和跳跃之间无缝过渡。

何时使用:

  • 当对象的行为取决于其状态时,并且行为必须在运行时动态更改。
  • 当类中有复杂的条件逻辑时,可以通过将每个状态表示为单独的类来简化。

何时不使用:

  • 对于条件语句足以管理对象行为的简单情况。
  • 当状态转换是静态的并且不经常改变时。

5.工厂方法模式Factory Method Pattern=>

  • 想象一下,您正在构建一个具有不同类型敌人的游戏。您有一个基本的敌人类别,但特定类型的敌人(如僵尸或机器人)有不同的创建过程。 Unity 中的工厂方法模式提供了一种将创建这些特定类型的实例的责任委托给其各自的工厂方法的方法。
  • 让我们考虑一个场景,其中有不同类型的敌人,每个敌人都有自己的工厂方法:
using UnityEngine;

// Base Enemy class
public abstract class Enemy : MonoBehaviour
{
    public abstract void Attack();
}

// Concrete Zombie Enemy
public class Zombie : Enemy
{
    public override void Attack()
    {
        Debug.Log("Zombie Attack");
    }
}

// Concrete Robot Enemy
public class Robot : Enemy
{
    public override void Attack()
    {
        Debug.Log("Robot Attack");
    }
}

// Enemy Factory Interface
public interface IEnemyFactory
{
    Enemy CreateEnemy();
}

// Concrete Zombie Factory
public class ZombieFactory : IEnemyFactory
{
    public Enemy CreateEnemy()
    {
        return new Zombie();
    }
}

// Concrete Robot Factory
public class RobotFactory : IEnemyFactory
{
    public Enemy CreateEnemy()
    {
        return new Robot();
    }
}
现在,您可以使用工厂方法来创建敌人的实例,而无需担心创建的具体细节:
public class EnemySpawner : MonoBehaviour
{
    private IEnemyFactory enemyFactory;

    // Set the desired factory at runtime
    public void SetEnemyFactory(IEnemyFactory factory)
    {
        enemyFactory = factory;
    }

    // Spawn an enemy using the chosen factory
    public void SpawnEnemy()
    {
        Enemy enemy = enemyFactory.CreateEnemy();
        enemy.Attack();
    }
}

void Start()
{
    EnemySpawner spawner = new EnemySpawner();

    // Set the Zombie factory and spawn a Zombie
    spawner.SetEnemyFactory(new ZombieFactory());
    spawner.SpawnEnemy();

    // Set the Robot factory and spawn a Robot
    spawner.SetEnemyFactory(new RobotFactory());
    spawner.SpawnEnemy();
}

何时在 Unity 中使用:

  • 当您想要将创建对象的责任委托给其子类时,允许创建过程具有灵活性。

何时不在 Unity 中使用:

  • 当创建过程很简单并且子类之间没有太大差异时。如果创建逻辑简单,直接实例化的方法可能更合适。

快乐编码!

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐