游戏开发设计模式
一:单例模式
介绍
-
定义:
Ensure a class has only one instance, and provide a global point of access to it(确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例 [全局访问点]) -
要求:
- 对象不能被外界实例化,即构造函数为
private或protected(可用于继承重写) - 只有一个实例,属于当前类,即此实例为当前类的
static静态成员变量 - 还要提供一个静态方法,向外界提供当前类的实例
- 对象不能被外界实例化,即构造函数为
实现
- 饿汉式
// 特点:在类加载(编译)时立刻进行实例化
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()可以拆分为三步-
分配内存空间
-
初始化对象
-
设置_singleton指向分配的内存地址
- 如果不加volatile,可能会导致即使
_singleton为null,但是_singleton指向了分配的内存地址,导致程序报错 - 例子:A线程执行了1、3还没有执行2,B线程也会判断对象已实例化,导致返回null对象,使用volatile可以避免重排序
-
单例模式的优缺点
- 优点:
- 单例模式只有在第一次请求时被创建,不会自主创建,能节约内存 (降低空间复杂度)
- 单例模式只有一个对象,不用经历对象的多次创建和销毁,能提高性能 (降低时间复杂度)
- 可以随时随地的链接游戏的各个模块,例如你的任何类都可以轻松的调用文件系统
- 缺点:
- 代码的耦合度上升,维护困难
- 代码的扩展难度上升
- 因为单例模式会将属性和方法暴露给其他脚本,当更新单例的行为方法和属性时,就很有可能造成所有调用者出现错误[太过耦合了]
- 总结:单例模式需正确评估风险,清楚他的好处和可能带来的弊端,合理使用这把双刃剑
二:简单工厂模式
介绍
-
定义:简单工厂模式属于类的创建型模型。在简单工厂模式中,可根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。
-
类图:

-
作用:用于创建某一"大类"下面不同类的实例
实现
// 简单工厂类
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");
}
}
简单工厂模式的优缺点
-
优点:
-
减少系统入口点的数量,简化系统的设计,提高系统的可扩展性。
-
封装变化,将对象的创建和使用分离,使得系统的设计更灵活。
-
简化代码结构,提高代码的可读性。
-
-
缺点:
-
增加系统的复杂度。
-
增加系统的维护成本。
-
增加系统的理解难度。
-
三:组合模式
介绍
-
定义:组合模式是一种结构型设计模式,你可以使用它将对象组合成树状结构,从而可以统一地对待单个对象和组合对象。
- 结构型模式:如何将对象和类组装成一个较大结构,并且同时保持结构的灵活和高效性的模式
-
核心结构:
Component(抽象组件) ├── Leaf(叶子节点,具体功能) └── Composite(组合节点,包含子节点)Component:定义叶子和组合的共同接口(如Display,Update等接口函数),可以是一个抽象类(即包含纯虚函数)Leaf:不包含子元素的基本对象(如Transform,SpriteRenderer等对象)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);
}
}
Comments NOTHING