游戏开发设计模式(Design Model in Game Development)

PL-TD 发布于 2025-04-05 25 次阅读


游戏开发设计模式

一:单例模式

介绍

  • 定义Ensure a class has only one instance, and provide a global point of access to it(确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例 [全局访问点]

  • 要求

    1. 对象不能被外界实例化,即构造函数为privateprotected(可用于继承重写)
    2. 只有一个实例属于当前类,即此实例为当前类的static静态成员变量
    3. 还要提供一个静态方法,向外界提供当前类的实例

实现

  • 饿汉式
// 特点:在类加载(编译)时立刻进行实例化
class Singleton
{
    private static Singleton _singleton = new Singleton();
    private Singleton() {} // 私有构造函数

    public static Singleton GetInstance() // 公有调用接口
    {
        return _singleton;
    }
}
  • 懒汉式
// 特点:类加载时不进行初始化,在第一次使用时实例化
class Singleton
{
    // volatile 保证可见性和禁止指令重排
    private static volatile Singleton _singleton; // 单例对象
    private static readonly object _lock = new object(); // 锁对象
    private Singleton() {} // 私有构造函数

    public static Singleton GetInstance()
    {
        // 双重检查锁
        // 第一个 if (_singleton  null) 是为了避免每次都进入锁,提高性能
        if (_singleton  null) {
            lock (_lock) {
                // 只允许一个线程进入锁
                // 第二个 if (_singleton  null) 是确保在多个线程都到达第一个判断时,只能有一个能真正创建实例。
                if (_singleton  null) {
                    _singleton = new Singleton();
                }
            }
        }
        return _singleton;
    }
}
  • 对象前加volatile的作用:保证_singleton的初始化顺序,_singleton = new Singleton()可以拆分为三步

    1. 分配内存空间

    2. 初始化对象

    3. 设置_singleton指向分配的内存地址

    • 如果不加volatile,可能会导致即使_singleton为null,但是_singleton指向了分配的内存地址,导致程序报错
    • 例子:A线程执行了1、3还没有执行2,B线程也会判断对象已实例化,导致返回null对象,使用volatile可以避免重排序

单例模式的优缺点

  • 优点
    1. 单例模式只有在第一次请求时被创建,不会自主创建,能节约内存 (降低空间复杂度)
    2. 单例模式只有一个对象,不用经历对象的多次创建和销毁,能提高性能 (降低时间复杂度)
    3. 可以随时随地的链接游戏的各个模块,例如你的任何类都可以轻松的调用文件系统
  • 缺点
    1. 代码的耦合度上升,维护困难
    2. 代码的扩展难度上升
      • 因为单例模式会将属性和方法暴露给其他脚本,当更新单例的行为方法和属性时,就很有可能造成所有调用者出现错误[太过耦合了]
  • 总结:单例模式需正确评估风险,清楚他的好处和可能带来的弊端,合理使用这把双刃剑

二:简单工厂模式

介绍

  • 定义:简单工厂模式属于类的创建型模型。在简单工厂模式中,可根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类

  • 类图:

  • 作用:用于创建某一"大类"下面不同类的实例

实现

// 简单工厂类
class SimpleFactory
{
    // 通过比较类型type来控制生成对应的产品
    public static Product CreateProduct(string type)
    {
        // 里氏替换
        if("A".Equals(type))
            return new ProductA(); 
        else
            return new ProductB();
    }

    public static void Main(string[] args)
    {
        Product product = SimpleFactory.CreateProduct("A");
        product.Print(); // 多态
    }
}

// 抽象产品父类
abstract class Product
{
     public abstract void Print();
}

// 产品A
class ProductA : Product
{
    public override void Print()
    {
        Console.WriteLine("ProductA");
    }
}

// 产品B
class ProductB : Product
{
    public override void Print()
    {
        Console.WriteLine("ProductB");
    }
}

简单工厂模式的优缺点

  • 优点:

    1. 减少系统入口点的数量,简化系统的设计,提高系统的可扩展性。

    2. 封装变化,将对象的创建和使用分离,使得系统的设计更灵活。

    3. 简化代码结构,提高代码的可读性。

  • 缺点:

    1. 增加系统的复杂度

    2. 增加系统的维护成本

    3. 增加系统的理解难度。

三:组合模式

介绍

  • 定义:组合模式是一种结构型设计模式,你可以使用它将对象组合成树状结构,从而可以统一地对待单个对象和组合对象

    • 结构型模式:如何将对象和类组装成一个较大结构,并且同时保持结构的灵活高效性的模式
  • 核心结构

    Component(抽象组件) ├── Leaf(叶子节点,具体功能) └── Composite(组合节点,包含子节点)

    1. Component:定义叶子和组合的共同接口(如Display,Update等接口函数),可以是一个抽象类(即包含纯虚函数)
    2. Leaf:不包含子元素的基本对象(如Transform,SpriteRenderer等对象)
    3. Composite:是包含子对象和本身对象的容器对象(如Enemy,Player等对象)
  • 三者的关系

    类型 是什么 有什么能力 谁可以包含它
    Component 抽象父类 / 接口 定义统一接口 所有组合结构
    Leaf 实体节点 只有自己的行为 可以被 Composite 包含
    Composite 容器节点 包含多个 Component 可以被其他 Composite 包含

例子实现

  • 实现一个怪物组合AI系统
// 设计
// 一个大型boss,他的行为很复杂,比如可能有以下行为
// 1.巡逻(Patrol)
// 2.发现玩家后追击(Chase)
// 3.近战攻击(MeleeAttack)
// 4.远程魔法攻击(MagicAttack)
// 5.回血(Heal)
// 然后根据不同的场景切换不同的AI组合

// 结构
// MonsterAIComponent(公共父类:抽象组件)
// ├── Leaf:PatrolAI
// ├── Leaf:ChaseAI
// ├── Leaf:MeleeAttackAI
// ├── Leaf:MagicAttackAI
// ├── Leaf:HealAI
// └── Composite:BossAI(组合器,统一管理多个子AI)

// 1.定义抽象基类(Component)
abstract class MonsterAIComponent
{
    public string _name;

    public MonsterAIComponent(string name)
    {
        _name = name;
    }

    public abstract void Update();
}

// 2.定义具体组件节点(Leaf)
class PatrolAI : MonsterAIComponent
{
    public PatrolAI() : base("巡逻")
    {
    }

    public override void Update()
    {
        Console.WriteLine("Boss is Patrolling");
    }
}
class ChaseAI : MonsterAIComponent
{
    public ChaseAI() : base("追击")
    {
    }

    public override void Update()
    {
        Console.WriteLine("Boss is Chasing the Player");
    }
}
class MeleeAttackAI : MonsterAIComponent
{
    public MeleeAttackAI() : base("近战攻击")
    {
    }

    public override void Update()
    {
        Console.WriteLine("Boss is Melee Attacking");
    }
}
class MagicAttackAI : MonsterAIComponent
{
    public MagicAttackAI() : base("远程魔法攻击")
    {
    }

    public override void Update()
    {
        Console.WriteLine("Boss is Magic Attacking");
    }
}
class HealAI : MonsterAIComponent
{
    public HealAI() : base("回血")
    {
    }

    public override void Update()
    {
        Console.WriteLine("Boss is Healing");
    }
}

// 3.定义组合节点(Composite)
class BossAI : MonsterAIComponent
{
    private List<MonsterAIComponent> _subAIs = new List<MonsterAIComponent>();
    private int _health = 100;

    public BossAI() : base("BossAI")
    {

    }

    // 在树型结构中添加Component
    public void AddAI(MonsterAIComponent ai)
    {
        _subAIs.Add(ai);
    }

    // 逻辑判定
    public override void Update()
    {
        Console.WriteLine($"Boss health: {_health}");

        // 判断Boss行为逻辑
        if (_health > 70)
        {
            // 正常模式
            FindAndExecute("巡逻");
        }
        else if (_health > 40)
        {
            // 战斗模式
            FindAndExecute("追击");
            FindAndExecute("近战攻击");
        }
        else
        {
            // 重伤模式
            FindAndExecute("远程魔法攻击");
            FindAndExecute("回血");
        }
    }

    // 切换形态
    private void FindAndExecute(string aiName)
    {
        foreach (var ai in _subAIs)
        {
            if (ai._name  aiName)
            {
                ai.Update(); // 调用更新行为
                break;
            }
        }
    }

    // 受伤
    public void TakeDamage(int damage)
    {
        _health -= damage;
        if (_health <= 0)
        {
            Console.WriteLine("Boss is Dead");
        }
    }

}
  • 测试结果:

组合模式的优缺点

  • 👍优点

    • 扩展性极强:新增AI模块,完全不影响主流程。

    • 灵活切换行为:可以根据状态组合不同的子AI

    • 层次清晰:Boss作为Composite,对外统一入口;细节内部处理。

    • 维护简单:出问题只需要改某一个子AI模块

  • 👎 缺点

    • 不容易限制子节点的类型(如某些组合节点可能只能包含特定类型)
    • 对于过度复杂的层级,调试和维护可能变难
  • 游戏开发中的意义

    • 架构清晰:特别是在大型项目中,组合模式能让场景和对象组织井井有条
    • 组件化开发:便于拆分功能,模块复用性强
    • 提升开发效率:尤其是在引擎开发、UI系统、技能系统等中非常高效。
  • 🧠 总结:组合模式让你能像操作单个对象那样操作一组对象,非常适合用于游戏开发中树形结构的数据建模和统一操作调用

继承 VS 组合🎯

  • 对比
比较维度 继承(Inheritance) 组合模式(Composite Pattern)
核心思想 “是一个”(is-a) “包含一个”(has-a)
结构关系 类与类之间的层次结构(单一继承或多重继承) 对象之间的树形结构(部分-整体)
耦合度 高耦合,子类依赖父类 低耦合,各组件独立,可自由组合
扩展性 修改或新增功能需要改动父类或子类 添加新功能只需组合新组件
灵活性 相对死板,结构固定 非常灵活,易于动态添加/移除组件
运行时结构 编译时决定结构 运行时可动态组合对象结构
适用场景 有明显“种类”关系的系统(比如不同的怪物类型) 有层级结构的系统(比如UI、场景树)
示例 Player继承Character类 GameObject中包含多个子组件
  • 使用场景
使用继承的场景 ✅ 使用组合模式的场景 ✅
子类是父类的一种(is-a) 对象需要动态扩展功能(has-a)
结构固定、行为清晰 需要频繁地添加、移除组件
类之间行为差异明显 行为可以拆分成多个小功能单元
少量、稳定的类层级 系统复杂、功能组合变化多
  • 总结:继承是静态的分类结构【即“是什么”】,组合是动态的能力组织方式【即“能做什么”】。
    • 即继承只适合“固定”的行为,组合模式适合"变化多"、"组合复杂"的系统

四:命令模式

介绍

  • 定义:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。简单说,命令模式就是把一个请求(行为/操作)封装成一个对象

  • 命令模式的重点

    • "做什么"(方法调用)变成了"一个对象"(Command对象)
    • 调用者不关心怎么实现,只负责执行命令
    • 可以延迟执行储存历史撤销操作

实现

  • 例子:实现向前走,向后走和跳跃解耦和撤销
using System.Numerics;
using System.Threading.Tasks.Dataflow;

namespace Command
{
    // 定义:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。

    // 1.定义一个Command接口
    public interface ICommand
    {
        // 函数默认为public
        void Execute();
        void Undo();
    }

    // 2.定义一个接收者Player
    public class Player : MonoBehaviour
    {
        public void Move(Vector3 direction)
        {
            transform.position += direction;
        }

        public void Jump()
        {

        }
    }

    // 3.创建具体命令
    public class MoveForward : ICommand
    {
        private Player _player;
        private Vector3 _moveDistance = new Vector3(0, 0, 1);

        MoveForward(Player player)
        {
            this._player = player;
        }

        public void Execute()
        {
            _player.Move(_moveDistance);
        }

        public void Undo()
        {
            _player.Move(-_moveDistance);
        }
    }

    public class MoveBackward : ICommand
    {
        private Player _player;
        private Vector3 _moveDistance = new Vector3(0, 0, -1);

        MoveBackward(Player player)
        {
            this._player = player;
        }

        public void Execute()
        {
            _player.Move(_moveDistance);
        }

        public void Undo()
        {
            _player.Move(-_moveDistance);
        }
    }

    public class Jump : ICommand
    {
        private Player _player;

        Jump(Player player)
        {
            this._player = player;
        }

        public void Execute()
        {
            _player.Jump();
        }

        public void Undo()
        {
            throw new NotImplementedException(); // 跳跃无法撤销
        }
    }

    // 4.定义一个调用者
    public class InputHandler : MonoBehaviour
    {
        private Stack<ICommand> commandHistory = new Stack<ICommand>();
        private Player player;

        private void Start()
        {
            player = GetComponent<Player>();
        }

        private void Update()
        {
            if (Input.GetKeyDown(KeyCode.W))
            {
                ICommand command = new MoveForwardCommand(player);
                command.Execute();
                commandHistory.Push(command);
            }

            if (Input.GetKeyDown(KeyCode.S))
            {
                ICommand command = new MoveBackCommand(player);
                command.Execute();
                commandHistory.Push(command);
            }

            if (Input.GetKeyDown(KeyCode.Space))
            {
                ICommand command = new JumpCommand(player);
                command.Execute();
                commandHistory.Push(command);
            }

            if (Input.GetKeyDown(KeyCode.Z)) // 撤销上一个动作
            {
                if (commandHistory.Count > 0)
                {
                    ICommand lastCommand = commandHistory.Pop();
                    lastCommand.Undo();
                }
            }
        }
    }
}

常见应用场景

场景 命令模式带来的好处
角色控制 不同动作封装成命令,方便管理
技能释放 技能可以排队执行,组合技能
撤销/重做 命令可以反向执行,轻松实现 undo
游戏录像回放 记录命令历史,重播玩家操作
UI操作 按钮点击对应不同命令
  • 适用场景:
    • 游戏动作比较多,需要统一管理。
    • 需要撤销/重做功能。
    • UI按钮触发各种复杂行为(比如不同按钮绑定不同命令)。
    • 想做宏操作(比如一次执行一连串动作,技能连招)。
    • 游戏需要支持录制与回放玩家操作。

命令模式的优缺点

优点 缺点
解耦调用者和接收者 类数量增加(每个命令一个类)
支持记录/撤销/回放等功能 简单操作用命令有点重
容易扩展新动作,不改原有逻辑
  • 总结:命令模式就是把动作对象化历史可追溯,逻辑更清晰

五:观察者模式

介绍

  • 定义:观察者模式(Observer Pattern)是一种行为设计模式,定义了对象之间的一种一对多的依赖关系,使得当一个对象(主题Subject)状态发生变化时,所有依赖它的对象(观察者Observer)都会收到通知并自动更新

  • 结构

角色 游戏里对应什么
Subject(主题) 事件源==,比如角色对象、关卡管理器
Observer(观察者) 各种监听逻辑,比如UI界面、音效控制器、AI管理器
Event(事件) 通常会定义一个事件系统或者消息系统来传递信息

实现

// 这次以股票监测作为例子
// 假设我有一个显式器monitor和一个广告牌BillBoard
// 他们两个一同检测股票(Stock)的变化情况

// 我们先写一下常规能够想到的编写方式
class Monitor
{
    public void Print(int m)
    {
        Console.WriteLine("Monitor: " + m);
    }
}
class BillBoard
{
    public void display(int m)
    {
        Console.WriteLine("BillBoard: " + m);
    }
}
class Stock
{
    private int _price = 20;
    Monitor _monitor;
    BillBoard _billBoard;

    public Stock(Monitor monitor, BillBoard billBoard)
    {
        _monitor = monitor;
        _billBoard = billBoard;
    }
    // 表示股价变更
    public void SetPrice(int nM)
    {
        _monitor.Print(nM);
        _billBoard.display(nM);
    }
}
  • 这种实现方式有什么问题呢?
    • 导致了Stock类和Monitor和BillBoard类的紧耦合,即Stock类的稳定性依赖于Monitor和BillBoard类的稳定性
    • Monitor和BillBoard类发生改变或者加入新的Observer时就会导致Stock被频繁修改,导致代码的可维护性降低
// 下面我们使用观察者模式重新实现一下代码
// 为Monitor和BillBoard类添加一个统一的Observer父类/接口
public abstract class Observer
{
    public Stock _stock; // 创建一个Stock的引用对象
    public Observer(Stock stock)
    {
        _stock = stock;
        _stock.Attach(this);
    }
    public abstract void Update(int m);
}
public class Monitor : Observer
{
    public Monitor(Stock stock) : base(stock) // 复用父类构造函数
    { }
    public void Print(int m)
    {
        Console.WriteLine("Monitor: " + m);
    }

    // 行为放在统一的Update函数中
    public override void Update(int m)
    {
        Print(m);
    }
}
public class BillBoard : Observer 
{
    public BillBoard(Stock stock) : base(stock)
    {

    }
    public void display(int m)
    {
        Console.WriteLine("BillBoard: " + m);
    }

    public override void Update(int m)
    {
        display(m);
    }
}
public class Stock
{
    private int _price = 0;
    private List<Observer> _observerList = new List<Observer>();
    public void Attach(Observer o)
    {
        _observerList.Add(o); // 相当于注册
    }
    // 未具体写出销毁时的逻辑
    public void Detach(Observer o) 
    {
        _observerList.Remove(o); // 相当于注销
    }

    public void Notify() // 通知函数
    {
        foreach (var observer in _observerList)
        {
            observer.Update(_price);
        }
    }

    public void SetPrice(int nM)
    {
        _price = nM;
        Notify();
    }
}
public class Test
{
    static void Main(string[] args)
    {
        Stock stock = new Stock();
        Monitor monitor = new Monitor(stock);
        BillBoard billBoard = new BillBoard(stock);
        stock.SetPrice(100);
    }
}
此作者没有提供个人介绍
最后更新于 2025-04-05