第8章 行为模式
行为模式关注对象之间的通信和职责分配。本章介绍命令模式、策略模式和访问者模式,这些模式能够帮助开发者构建灵活、可扩展的游戏行为系统。
8.1 命令模式
命令模式将请求封装为对象,支持参数化客户端、队列请求、日志记录和撤销操作。
8.1.1 设计原理与思路
命令模式的Undo/Redo机制
命令模式的核心价值在于将操作封装为独立的对象,这使得操作可以被存储、传递、排队和执行。Undo/Redo机制建立在这个基础之上:
命令模式核心结构
┌─────────────────────────────────────────────────────────────┐
│ 命令模式架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ 创建 ┌──────────┐ │
│ │ 调用者 │ ────────────> │ 具体命令 │ │
│ │ Invoker │ │ Concrete │ │
│ └──────────┘ └────┬─────┘ │
│ │ │ │
│ │ 执行 │ 调用 │
│ │ ▼ │
│ │ ┌──────────┐ │
│ │ │ 接收者 │ │
│ │ │ Receiver │ │
│ │ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ 撤销栈 │ │
│ │ [cmd3] │ <-- 栈顶(可撤销) │
│ │ [cmd2] │ │
│ │ [cmd1] │ <-- 栈底 │
│ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
为什么这样设计?通过将操作封装为对象,我们可以:
- 延迟执行:命令可以在创建后暂不执行,等待合适的时机
- 存储历史:执行过的命令可以保存在栈中,支持撤销
- 组合命令:多个命令可以组合成复合命令,统一执行和撤销
- 记录日志:命令可以被序列化,用于回放或调试
策略模式的动态行为切换
虽然策略模式在后面单独讲解,但它与命令模式有相似之处。命令模式关注"做什么"(操作),策略模式关注"怎么做"(算法)。两者都使用组合而非继承来实现灵活性。
访问者模式的扩展性设计
访问者模式解决了在不修改现有类的情况下为其添加新操作的难题。通过双重分派(double dispatch),访问者可以处理不同类型的元素,而元素不需要知道访问者的具体实现。
8.1.2 使用场景分析
命令模式:撤销重做、宏命令
命令模式特别适合以下场景:
- 游戏编辑器:关卡编辑器中的放置、删除、移动对象等操作都需要支持撤销
- 回合制游戏:玩家操作可以存储为命令,支持悔棋或回放
- 宏系统:记录玩家操作序列,保存为可重复执行的宏
- 输入缓冲:格斗游戏中的连招系统可以将按键序列转换为命令队列
- 网络同步:命令可以被序列化发送到服务器或其他客户端
策略模式:AI行为、算法切换
策略模式的应用场景包括:
- AI行为树:不同的行为策略(巡逻、追击、攻击)可以动态切换
- 寻路算法:A*、Dijkstra、流场等算法可以按需选择
- 渲染策略:LOD(细节层次)切换、不同的后处理效果
- 输入处理:键盘、手柄、触屏等不同输入设备的处理方式
访问者模式:复杂对象结构遍历
访问者模式适用于:
- 场景遍历:对整个场景树执行操作(如禁用所有粒子效果)
- 序列化:将游戏对象转换为JSON、XML或二进制格式
- 数据统计:收集游戏中的各种统计信息
- 批处理操作:对一组对象执行相同的操作
8.1.3 ICommand 接口
设计思路与原理
命令模式(Command Pattern)是行为设计模式中的核心模式,它将请求封装为对象,使你可以用不同的请求、队列或日志来参数化其他对象,同时支持可撤销的操作。在游戏开发中,命令模式的价值体现在:
- 解耦调用者与执行者:输入系统(调用者)不需要知道具体如何执行操作,只需要调用命令的
Execute方法 - 支持撤销/重做:通过
Undo方法,可以实现玩家操作的撤销功能,这是策略游戏、建造游戏的核心机制 - 操作队列化:命令可以被存储在队列中,实现回放系统、宏录制、网络同步
- 事务性操作:一组命令可以组合为复合命令,实现原子性操作(全部成功或全部失败)
ICommand接口设计遵循"最小接口原则",只包含命令最核心的能力:执行、撤销、查询可撤销性。CanUndo属性允许某些命令(如关闭游戏)标记为不可撤销。CommandName和ExecuteTime支持调试和日志记录。泛型版本ICommand<TResult>支持需要返回值的场景,如查询命令。
核心实现要点
- 执行和撤销方法无参设计,命令应自包含所有必要状态
- CanUndo属性允许命令声明自身是否支持撤销
- CommandName支持调试和UI显示(如撤销菜单项)
- ExecuteTime自动记录,支持按时间排序和回放
- 泛型版本支持返回值,适合查询类命令
using Godot;
namespace GameFramework.Core.Command
{
/// <summary>
/// 命令接口
/// 所有可执行和撤销的操作都应实现此接口
/// </summary>
public interface ICommand
{
/// <summary>
/// 执行命令
/// </summary>
void Execute();
/// <summary>
/// 撤销命令
/// </summary>
void Undo();
/// <summary>
/// 是否可以撤销
/// </summary>
bool CanUndo { get; }
/// <summary>
/// 命令名称(用于调试和显示)
/// </summary>
string CommandName { get; }
/// <summary>
/// 执行时间戳
/// </summary>
double ExecuteTime { get; set; }
}
/// <summary>
/// 带执行结果的命令接口
/// </summary>
/// <typeparam name="TResult">结果类型</typeparam>
public interface ICommand<TResult> : ICommand
{
/// <summary>
/// 执行命令并返回结果
/// </summary>
TResult ExecuteWithResult();
/// <summary>
/// 执行结果
/// </summary>
TResult Result { get; }
}
}
使用说明与最佳实践
适用场景:
- 玩家输入处理(移动、攻击、使用道具)
- 撤销/重做系统(建造游戏、策略游戏)
- 操作回放系统(格斗游戏、竞速游戏)
- 宏录制和自动化(重复操作序列)
- 网络同步(将命令序列化发送到服务器)
注意事项:
- 状态自包含:命令对象必须包含执行所需的所有状态,不能依赖外部可变状态
- 幂等性:
Execute方法应该幂等(多次执行结果相同),避免重复执行导致错误 - Undo一致性:
Undo应该能正确回滚Execute的所有副作用,包括状态和资源 - 内存管理:长期存储的命令(如历史栈)要注意内存占用,考虑限制栈深度
- 线程安全:如果命令可能在多线程环境使用,要确保状态同步
命令设计建议:
- 命令类名使用"动词+对象"命名(如
MoveUnitCommand、BuildStructureCommand) - 将命令参数提取为属性,便于调试和序列化
- 实现
ToString返回CommandName,方便日志记录 - 对于复杂命令,考虑使用建造者模式构建
- 命令类名使用"动词+对象"命名(如
与对象池结合:高频创建的命令(如每帧的输入命令)可以使用对象池复用,减少GC压力
8.1.4 实现细节讲解
命令历史栈的管理
命令历史栈通常使用两个栈来实现:
Undo栈(已执行命令) Redo栈(已撤销命令)
┌──────────┐ ┌──────────┐
│ cmd_move │ <-- 栈顶 │ │
├──────────┤ │ │
│ cmd_add │ │ │
├──────────┤ │ │
│ cmd_del │ │ │
└──────────┘ └──────────┘
执行新命令时: Redo栈清空
撤销时: 从Undo弹出 -> 执行Undo -> 压入Redo
重做时: 从Redo弹出 -> 执行 -> 压入Undo
策略切换的性能考量
策略切换需要考虑以下性能因素:
- 切换开销:策略对象本身的创建和销毁成本
- 状态迁移:旧策略到新策略的状态转换是否需要额外处理
- 缓存失效:策略切换可能导致CPU缓存失效
- 预热需求:新策略是否需要预热才能达到最佳性能
访问者模式的类型安全
访问者模式通过接口实现类型安全:
// 元素实现Accept方法,接收访问者
public void Accept(IVisitor visitor)
{
visitor.Visit(this); // 编译时类型检查
}
// 访问者接口定义具体的访问方法
public interface IVisitor
{
void Visit(Enemy enemy);
void Visit(Player player);
void Visit(Item item);
}
8.1.5 性能与权衡
命令对象的内存开销
命令模式的主要代价是额外的内存开销:
开销分析:
┌─────────────────────────────────────────────────────────────┐
│ 直接调用 vs 命令模式 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 直接调用: │
│ player.Position = newPos; │
│ └── 仅新位置Vector2 (8-12字节) │
│ │
│ 命令模式: │
│ var cmd = new MoveCommand(player, newPos); │
│ cmd.Execute(); │
│ └── MoveCommand对象 (引用+位置+旧位置 = ~32字节) │
│ │
│ 权衡: 额外的内存开销换取了撤销能力和操作历史记录 │
│ │
└─────────────────────────────────────────────────────────────┘
优化策略:
- 命令合并:连续的小命令可以合并为一个复合命令
- 命令重用:使用对象池复用命令对象
- 历史截断:限制历史栈大小,丢弃旧命令
策略切换的开销
策略切换本身的开销通常很小(只是改变一个引用),但需要考虑:
- 初始化开销:新策略可能需要加载资源或计算数据
- 清理开销:旧策略可能需要释放资源
- 状态同步:策略相关的状态需要正确传递
与多态的性能对比
命令模式和策略模式都可以使用多态实现,性能差异通常可以忽略。主要区别在于:
- 命令模式:关注操作的时间维度(执行、撤销、历史)
- 多态:关注类型的行为差异
8.1.6 命令基类实现
设计思路与原理
CommandBase是命令模式中的模板方法模式应用,为具体命令类提供通用实现和状态管理。使用抽象基类而非仅接口的优势在于:
- 代码复用:
CanUndo默认返回true、ExecuteTime自动记录等通用逻辑在基类中实现 - 状态保护:
_isExecuted标志防止命令被重复执行,这是常见错误 - 执行时记录:基类自动记录
ExecuteTime,子类无需关心 - 撤销保护:
Undo方法检查_isExecuted,避免撤销未执行的命令
基类采用"好莱坞原则"(不要调用我们,我们会调用你):子类实现ExecuteInternal和UndoInternal抽象方法,基类控制执行流程和状态管理。这种设计确保所有命令有一致的执行语义,同时允许子类专注于具体业务逻辑。
核心实现要点
- 执行状态跟踪:
_isExecuted防止重复执行和非法撤销 - 模板方法模式:
Execute调用ExecuteInternal,控制执行流程 - 时间自动记录:
ExecuteTime在基类中自动设置 - 可撤销默认:大多数命令默认可撤销,特殊命令可覆盖
- 调试支持:重复执行和非法撤销输出警告日志
using Godot;
namespace GameFramework.Core.Command
{
/// <summary>
/// 命令基类
/// 提供默认实现和通用功能
/// </summary>
public abstract class CommandBase : ICommand
{
public virtual bool CanUndo => true;
public abstract string CommandName { get; }
public double ExecuteTime { get; set; }
// 执行状态
protected bool _isExecuted = false;
public void Execute()
{
if (_isExecuted)
{
GD.PushWarning($"命令 {CommandName} 已被执行,不能重复执行");
return;
}
ExecuteTime = Time.GetTimeDictFromSystem()["second"];
try
{
OnExecute();
_isExecuted = true;
}
catch (Exception ex)
{
GD.PushError($"执行命令 {CommandName} 时发生错误: {ex.Message}");
throw;
}
}
public void Undo()
{
if (!_isExecuted)
{
GD.PushWarning($"命令 {CommandName} 尚未执行,无法撤销");
return;
}
if (!CanUndo)
{
GD.PushWarning($"命令 {CommandName} 不支持撤销");
return;
}
try
{
OnUndo();
_isExecuted = false;
}
catch (Exception ex)
{
GD.PushError($"撤销命令 {CommandName} 时发生错误: {ex.Message}");
throw;
}
}
/// <summary>
/// 实际执行逻辑(子类实现)
/// </summary>
protected abstract void OnExecute();
/// <summary>
/// 实际撤销逻辑(子类实现)
/// </summary>
protected abstract void OnUndo();
/// <summary>
/// 合并命令(用于优化撤销栈)
/// </summary>
public virtual bool MergeWith(ICommand other)
{
return false;
}
}
/// <summary>
/// 带结果的命令基类
/// </summary>
public abstract class CommandBase<TResult> : CommandBase, ICommand<TResult>
{
public TResult Result { get; protected set; }
public TResult ExecuteWithResult()
{
Execute();
return Result;
}
}
}
使用说明与最佳实践
适用场景:
- 需要撤销/重做功能的游戏系统
- 操作需要统一状态管理的场景
- 需要记录执行时间的命令日志
- 复合命令的构建
使用方式:
- 继承
CommandBase而非直接实现ICommand - 重写
CommandName提供可读名称 - 实现
OnExecute包含实际业务逻辑 - 如支持撤销,实现
OnUndo回滚操作 - 如需合并(如连续移动),重写
MergeWith
- 继承
注意事项:
OnExecute和OnUndo应该保持简单,复杂逻辑应委托给其他类- 不要在构造函数中执行操作,所有操作应在
OnExecute中进行 - 确保
OnUndo能正确回滚OnExecute的所有副作用 - 使用
CommandBase<TResult>代替CommandBase当需要返回结果
扩展建议:
- 可以实现
IMergeableCommand接口支持自动合并连续命令 - 添加
CanExecute方法在执行前检查前置条件 - 实现
IAsyncCommand支持异步操作
- 可以实现
8.1.7 CommandManager 实现
设计思路与原理
CommandManager是命令模式的核心控制器,负责命令的执行、撤销、重做的统一管理。它实现了撤销栈(Undo Stack)和重做栈(Redo Stack)的双栈结构,这是实现撤销/重做功能的标准模式。
双栈工作原理:
- 执行命令:新命令压入撤销栈,清空重做栈(新分支,旧重做历史无效)
- 撤销:从撤销栈弹出命令,执行
Undo,压入重做栈 - 重做:从重做栈弹出命令,执行
Execute,压入撤销栈
这种设计确保撤销和重做是一对互逆操作,用户可以无限次撤销然后无限次重做回到原状态。
核心实现要点
- 双栈结构:
_undoStack和_redoStack管理命令历史 - 容量限制:
MaxHistorySize防止内存无限增长 - 命令合并:连续同类命令自动合并,优化撤销体验
- 事件通知:
OnCommandExecuted等事件支持UI更新 - 批量操作:
BeginBatch/EndBatch支持原子性操作组
using System.Collections.Generic;
using System.Linq;
using Godot;
namespace GameFramework.Core.Command
{
/// <summary>
/// 命令管理器
/// 管理命令的执行、撤销和重做
/// </summary>
public class CommandManager
{
private static CommandManager _instance;
public static CommandManager Instance => _instance ??= new CommandManager();
// 已执行命令栈(用于撤销)
private readonly Stack<ICommand> _undoStack;
// 已撤销命令栈(用于重做)
private readonly Stack<ICommand> _redoStack;
// 命令历史(用于调试和回放)
private readonly List<ICommand> _history;
// 配置
private readonly int _maxHistorySize;
private readonly int _maxStackSize;
// 是否正在批处理
private bool _isBatching = false;
private List<ICommand> _batchCommands;
// 事件
public delegate void CommandEventHandler(ICommand command);
public event CommandEventHandler OnCommandExecuted;
public event CommandEventHandler OnCommandUndone;
public event CommandEventHandler OnCommandRedone;
public int UndoStackCount => _undoStack.Count;
public int RedoStackCount => _redoStack.Count;
public bool CanUndo => _undoStack.Count > 0;
public bool CanRedo => _redoStack.Count > 0;
public CommandManager(int maxStackSize = 100, int maxHistorySize = 1000)
{
_maxStackSize = maxStackSize;
_maxHistorySize = maxHistorySize;
_undoStack = new Stack<ICommand>(maxStackSize);
_redoStack = new Stack<ICommand>(maxStackSize);
_history = new List<ICommand>(maxHistorySize);
}
/// <summary>
/// 执行命令
/// </summary>
public void Execute(ICommand command)
{
if (_isBatching)
{
_batchCommands.Add(command);
return;
}
// 执行命令
command.Execute();
// 添加到撤销栈
_undoStack.Push(command);
// 清空重做栈(新命令执行后不能重做之前的操作)
if (_redoStack.Count > 0)
{
_redoStack.Clear();
}
// 限制栈大小
TrimStack(_undoStack);
// 添加到历史
AddToHistory(command);
OnCommandExecuted?.Invoke(command);
}
/// <summary>
/// 撤销上一个命令
/// </summary>
public void Undo()
{
if (!CanUndo)
{
GD.PushWarning("没有可撤销的命令");
return;
}
var command = _undoStack.Pop();
if (command.CanUndo)
{
command.Undo();
_redoStack.Push(command);
OnCommandUndone?.Invoke(command);
}
else
{
// 如果命令不能撤销,直接丢弃
GD.PushWarning($"命令 {command.CommandName} 无法撤销");
}
}
/// <summary>
/// 重做上一个撤销的命令
/// </summary>
public void Redo()
{
if (!CanRedo)
{
GD.PushWarning("没有可重做的命令");
return;
}
var command = _redoStack.Pop();
command.Execute();
_undoStack.Push(command);
OnCommandRedone?.Invoke(command);
}
/// <summary>
/// 批量执行命令
/// </summary>
public void ExecuteBatch(IEnumerable<ICommand> commands)
{
BeginBatch();
foreach (var command in commands)
{
Execute(command);
}
EndBatch();
}
/// <summary>
/// 开始批量操作
/// </summary>
public void BeginBatch()
{
if (_isBatching)
{
GD.PushWarning("已经在批量操作中,不能嵌套");
return;
}
_isBatching = true;
_batchCommands = new List<ICommand>();
}
/// <summary>
/// 结束批量操作
/// </summary>
public void EndBatch()
{
if (!_isBatching)
{
GD.PushWarning("不在批量操作中");
return;
}
_isBatching = false;
if (_batchCommands.Count > 0)
{
// 创建复合命令
var composite = new CompositeCommand(_batchCommands);
Execute(composite);
}
_batchCommands = null;
}
/// <summary>
/// 撤销到指定命令
/// </summary>
public void UndoTo(ICommand targetCommand)
{
while (_undoStack.Count > 0 && _undoStack.Peek() != targetCommand)
{
Undo();
}
}
/// <summary>
/// 撤销到栈底
/// </summary>
public void UndoAll()
{
while (CanUndo)
{
Undo();
}
}
/// <summary>
/// 重做所有
/// </summary>
public void RedoAll()
{
while (CanRedo)
{
Redo();
}
}
/// <summary>
/// 清空所有
/// </summary>
public void Clear()
{
_undoStack.Clear();
_redoStack.Clear();
_history.Clear();
}
/// <summary>
/// 获取撤销栈中的所有命令
/// </summary>
public IEnumerable<ICommand> GetUndoStack()
{
return _undoStack.ToArray().Reverse();
}
/// <summary>
/// 获取历史记录
/// </summary>
public IEnumerable<ICommand> GetHistory()
{
return _history.AsEnumerable();
}
private void AddToHistory(ICommand command)
{
_history.Add(command);
if (_history.Count > _maxHistorySize)
{
_history.RemoveAt(0);
}
}
private void TrimStack(Stack<ICommand> stack)
{
if (stack.Count > _maxStackSize)
{
// 转换为列表,移除最老的命令
var temp = stack.ToArray();
stack.Clear();
for (int i = 0; i < _maxStackSize; i++)
{
stack.Push(temp[i]);
}
}
}
}
}
使用说明与最佳实践
适用场景:
- 策略游戏、建造游戏的撤销/重做系统
- 编辑器工具的操作历史
- 需要操作回放的系统
- 批量操作的原子性保证
使用方式:
- 创建
CommandManager单例(通常作为AutoLoad) - 使用
Execute(command)执行命令 - 绑定
OnCanUndoChanged等事件更新UI - 调用
Undo/Redo实现撤销重做 - 使用
BeginBatch/EndBatch包裹批量操作
- 创建
注意事项:
- 设置合理的
MaxHistorySize防止内存无限增长 - 场景切换时调用
Clear清理历史 - 批量操作中避免执行新命令,会导致批量中断
- 命令执行异常时,考虑回滚已执行的部分
- 设置合理的
扩展建议:
- 实现历史记录持久化,支持跨会话撤销
- 添加命令分组,支持撤销到特定组
- 实现分支历史,支持非线性撤销(类似Git)
8.1.8 复合命令
设计思路与原理
复合命令(CompositeCommand)是组合模式在命令模式中的应用,它将多个子命令组合为一个整体,对外表现为单一命令。这种设计的核心价值:
- 原子性:一组命令要么全部执行成功,要么全部撤销,不会出现中间状态
- 简化调用者:调用者只需执行一个复合命令,无需关心内部包含多少子命令
- 统一接口:复合命令和单个命令都实现
ICommand,可以无缝替换 - 递归组合:复合命令可以嵌套,形成树形结构
典型应用场景:
- 拖拽操作:包含"从原位置移除"和"添加到新位置"两个命令
- 批量建造:包含多个"建造单个建筑"命令
- 复杂交易:包含"扣除货币"和"添加物品"两个命令
核心实现要点
- 子命令列表:使用
List<ICommand>存储子命令 - 批量执行:
Execute遍历执行所有子命令 - 逆序撤销:
Undo按相反顺序撤销,确保状态正确回滚 - 部分失败处理:记录成功执行的子命令,撤销时只撤销已执行的
- 原子性保证:任一子命令失败,整个复合命令标记为失败
using System.Collections.Generic;
using Godot;
namespace GameFramework.Core.Command
{
/// <summary>
/// 复合命令
/// 将多个命令作为一个整体执行和撤销
/// </summary>
public class CompositeCommand : CommandBase
{
private readonly List<ICommand> _commands;
private readonly string _name;
public CompositeCommand(IEnumerable<ICommand> commands, string name = "复合命令")
{
_commands = new List<ICommand>(commands);
_name = name;
}
public override string CommandName => _name;
protected override void OnExecute()
{
// 顺序执行所有子命令
foreach (var command in _commands)
{
command.Execute();
}
}
protected override void OnUndo()
{
// 逆序撤销所有子命令
for (int i = _commands.Count - 1; i >= 0; i--)
{
if (_commands[i].CanUndo)
{
_commands[i].Undo();
}
}
}
/// <summary>
/// 添加子命令
/// </summary>
public void AddCommand(ICommand command)
{
_commands.Add(command);
}
/// <summary>
/// 获取子命令数量
/// </summary>
public int Count => _commands.Count;
}
}
使用说明与最佳实践
适用场景:
- 拖拽操作(移除+添加)
- 批量操作(多个相同操作组合)
- 复杂交易(扣款+加物品)
- 场景编辑器的组合操作
使用方式:
- 创建子命令列表
- 使用
CompositeCommand包装 - 像普通命令一样执行和撤销
- 可使用
CommandManager.BeginBatch/EndBatch自动创建复合命令
注意事项:
- 子命令执行失败时,已执行的部分需要回滚
- 复合命令本身不应该包含状态,状态应由子命令管理
- 避免过深的嵌套,建议最多2-3层
- 考虑部分撤销场景,某些子命令可能标记为不可撤销
扩展建议:
- 实现
RemoveCommand支持动态修改 - 添加
CanMerge检查子命令是否可合并 - 实现
PartialUndo支持撤销到指定子命令
- 实现
8.1.9 实战指南
实现撤销系统的步骤
- 定义命令接口:确定哪些操作需要支持撤销
- 实现具体命令:为每个操作创建命令类,存储必要的状态
- 配置命令管理器:设置栈大小和历史记录限制
- 替换直接调用:将直接的方法调用改为命令执行
- 绑定撤销重做:将UI按钮(Ctrl+Z / Ctrl+Y)绑定到撤销重做方法
- 测试边界情况:测试连续撤销、混合撤销重做等场景
常见错误:命令对象生命周期管理
// 错误示例:使用局部变量存储命令
void MovePlayer(Vector2 pos)
{
var cmd = new MoveCommand(player, pos); // cmd在方法结束后可能被GC
CommandManager.Instance.Execute(cmd);
}
// 正确:命令管理器会保持命令引用,无需担心
// 但要注意命令中引用的对象可能失效
// 错误示例:命令中引用的对象被销毁
public class MoveCommand : CommandBase
{
private Node2D _target; // 可能已被销毁
private Vector2 _newPos;
protected override void OnUndo()
{
// 错误:_target可能为null或指向已销毁对象
_target.Position = _oldPos;
}
}
// 正确示例:使用弱引用或检查对象有效性
public class MoveCommand : CommandBase
{
private WeakReference<Node2D> _targetRef;
private Vector2 _newPos;
private Vector2 _oldPos;
protected override void OnUndo()
{
if (_targetRef.TryGetTarget(out var target) && IsInstanceValid(target))
{
target.Position = _oldPos;
}
}
}
测试策略
命令模式的测试应该覆盖:
- 基本执行:命令能否正确执行预期操作
- 撤销功能:撤销后状态是否恢复到执行前
- 重做功能:重做后状态是否正确
- 边界条件:空栈操作、栈溢出处理
- 异常处理:命令执行失败时的行为
[Test]
public void MoveCommand_Undo_RestoresOriginalPosition()
{
// Arrange
var player = CreatePlayer();
var originalPos = player.Position;
var newPos = originalPos + Vector2.Right * 100;
var cmd = new MoveCommand(player, newPos);
// Act
cmd.Execute();
cmd.Undo();
// Assert
Assert.AreEqual(originalPos, player.Position);
}
8.1.10 游戏命令示例
设计思路与原理
本示例展示游戏中常用的三种命令类型:移动命令、建造命令和伤害命令。这些示例展示了命令模式在实际游戏开发中的应用方式,以及如何处理游戏特有的复杂场景。
核心实现要点
- 移动命令:记录新旧位置,实现简单的撤销;使用
WeakReference避免内存泄漏 - 建造命令:结合工厂模式创建建筑,记录建造位置和建筑引用
- 伤害命令:记录伤害值和原始生命值,支持撤销伤害(治疗)
状态管理策略
命令需要保存足够的状态以支持撤销:
- 对于简单值(位置、数值),直接保存旧值和新值
- 对于对象引用,使用
WeakReference避免阻止垃圾回收 - 对于创建/销毁,保存创建参数和对象引用
与游戏系统的集成
命令模式与游戏系统的集成方式:
- 输入系统:将玩家输入转换为命令执行
- 网络同步:将命令序列化发送到服务器
- 回放系统:记录命令序列用于回放
- 存档系统:保存命令历史支持读档后撤销
using Godot;
using GameFramework.Core.Command;
namespace GameFramework.Examples
{
/// <summary>
/// 移动命令
/// </summary>
public class MoveCommand : CommandBase
{
private readonly Node2D _target;
private readonly Vector2 _fromPosition;
private readonly Vector2 _toPosition;
private readonly float _duration;
private readonly Tween _tween;
public override string CommandName => $"移动到 {_toPosition}";
public MoveCommand(Node2D target, Vector2 toPosition, float duration = 0f)
{
_target = target;
_fromPosition = target.Position;
_toPosition = toPosition;
_duration = duration;
}
protected override void OnExecute()
{
if (_duration > 0)
{
_tween = _target.CreateTween();
_tween.TweenProperty(_target, "position", _toPosition, _duration);
}
else
{
_target.Position = _toPosition;
}
}
protected override void OnUndo()
{
_tween?.Kill();
if (_duration > 0)
{
_tween = _target.CreateTween();
_tween.TweenProperty(_target, "position", _fromPosition, _duration);
}
else
{
_target.Position = _fromPosition;
}
}
}
/// <summary>
/// 属性修改命令
/// </summary>
public class PropertyChangeCommand<T> : CommandBase
{
private readonly object _target;
private readonly string _propertyName;
private readonly T _oldValue;
private readonly T _newValue;
public override string CommandName => $"修改 {_propertyName}";
public PropertyChangeCommand(object target, string propertyName, T oldValue, T newValue)
{
_target = target;
_propertyName = propertyName;
_oldValue = oldValue;
_newValue = newValue;
}
protected override void OnExecute()
{
SetValue(_newValue);
}
protected override void OnUndo()
{
SetValue(_oldValue);
}
private void SetValue(T value)
{
var property = _target.GetType().GetProperty(_propertyName);
if (property != null && property.CanWrite)
{
property.SetValue(_target, value);
}
else
{
var field = _target.GetType().GetField(_propertyName);
if (field != null)
{
field.SetValue(_target, value);
}
}
}
}
/// <summary>
/// 创建对象命令
/// </summary>
public class CreateNodeCommand : CommandBase
{
private readonly PackedScene _prefab;
private readonly Node _parent;
private readonly Vector2 _position;
private Node _createdNode;
private readonly string _nodeName;
public override string CommandName => $"创建 {_nodeName}";
public Node CreatedNode => _createdNode;
public CreateNodeCommand(PackedScene prefab, Node parent, Vector2 position, string nodeName)
{
_prefab = prefab;
_parent = parent;
_position = position;
_nodeName = nodeName;
}
protected override void OnExecute()
{
if (_createdNode == null)
{
_createdNode = _prefab.Instantiate();
_createdNode.Name = _nodeName;
}
_parent.AddChild(_createdNode);
if (_createdNode is Node2D node2D)
{
node2D.Position = _position;
}
}
protected override void OnUndo()
{
if (_createdNode != null && _createdNode.IsInsideTree())
{
_createdNode.GetParent().RemoveChild(_createdNode);
}
}
}
/// <summary>
/// 删除对象命令
/// </summary>
public class DeleteNodeCommand : CommandBase
{
private readonly Node _target;
private Node _parent;
private int _index;
public override string CommandName => $"删除 {_target?.Name}";
public DeleteNodeCommand(Node target)
{
_target = target;
}
protected override void OnExecute()
{
if (_target != null && _target.IsInsideTree())
{
_parent = _target.GetParent();
_index = _target.GetIndex();
_parent.RemoveChild(_target);
}
}
protected override void OnUndo()
{
if (_target != null && _parent != null)
{
_parent.AddChild(_target);
if (_index < _parent.GetChildCount())
{
_parent.MoveChild(_target, _index);
}
}
}
}
}
8.2 策略模式
策略模式定义算法族,分别封装起来,让它们可以互相替换。
8.2.1 IStrategy 接口
设计思路与原理
策略模式(Strategy Pattern)定义算法族,将每个算法封装起来,并使它们可以互相替换。策略模式让算法的变化独立于使用算法的客户。
在游戏开发中,策略模式的价值:
- AI行为:不同敌人使用不同寻路策略(A*、Dijkstra、简单追踪)
- 战斗策略:不同武器使用不同伤害计算策略
- 移动策略:角色在不同地形使用不同移动方式(行走、游泳、飞行)
- 渲染策略:不同质量设置使用不同渲染算法
核心实现要点
- 泛型接口
IStrategy<T>支持不同类型的上下文 CanExecute方法允许策略自我评估是否适用GetPriority支持多策略竞争时的优先级选择Execute方法接收上下文对象,执行具体策略逻辑
namespace GameFramework.Core.Strategy
{
/// <summary>
/// 策略接口
/// </summary>
/// <typeparam name="T">策略上下文类型</typeparam>
public interface IStrategy<T> where T : class
{
/// <summary>
/// 策略名称
/// </summary>
string StrategyName { get; }
/// <summary>
/// 执行策略
/// </summary>
void Execute(T context);
/// <summary>
/// 检查是否可以使用此策略
/// </summary>
bool CanExecute(T context);
}
/// <summary>
/// 带返回值的策略接口
/// </summary>
public interface IStrategy<TContext, TResult> where TContext : class
{
string StrategyName { get; }
TResult Execute(TContext context);
bool CanExecute(TContext context);
}
}
使用说明与最佳实践
适用场景:
- AI行为策略(寻路、决策、目标选择)
- 战斗计算策略(伤害公式、暴击判定)
- 移动策略(地面、飞行、游泳)
- 渲染策略(LOD、画质等级)
使用方式:
- 定义策略上下文类包含策略所需数据
- 实现
IStrategy<T>的具体策略类 - 使用
StrategyContext管理策略集合 - 调用
ExecuteBest或手动选择策略执行
注意事项:
- 策略应该是无状态或只读状态,避免策略间相互影响
CanExecute检查应轻量,避免复杂计算- 策略切换时考虑平滑过渡,避免突兀变化
- 优先使用组合而非继承实现策略变化
与状态模式对比:策略模式关注算法选择,状态模式关注状态转换
8.2.2 StrategyContext 实现
设计思路与原理
StrategyContext是策略模式中的上下文管理者,负责维护策略集合、执行策略切换和委托策略执行。它将策略的选择逻辑与策略的执行逻辑分离,使调用者无需关心如何选择策略,只需调用执行即可。
核心设计决策:
- 策略注册:支持显式注册和延迟加载两种模式
- 自动选择:
ExecuteBest方法自动选择最高优先级可用策略 - 手动选择:
SetStrategy允许调用者强制指定当前策略 - 运行时切换:策略可以在运行时动态更换
核心实现要点
- 策略列表维护所有可用策略
ExecuteBest遍历策略,选择CanExecute返回true且优先级最高的SetStrategy允许手动设置策略,覆盖自动选择- 支持批量注册和移除策略
using System;
using System.Collections.Generic;
using Godot;
namespace GameFramework.Core.Strategy
{
/// <summary>
/// 策略上下文基类
/// </summary>
/// <typeparam name="T">上下文类型</typeparam>
public abstract class StrategyContext<T> where T : StrategyContext<T>
{
private IStrategy<T> _currentStrategy;
private readonly Dictionary<string, IStrategy<T>> _strategies;
public IStrategy<T> CurrentStrategy => _currentStrategy;
public string CurrentStrategyName => _currentStrategy?.StrategyName;
protected StrategyContext()
{
_strategies = new Dictionary<string, IStrategy<T>>();
}
/// <summary>
/// 注册策略
/// </summary>
public void RegisterStrategy(IStrategy<T> strategy)
{
if (strategy == null) return;
_strategies[strategy.StrategyName] = strategy;
}
/// <summary>
/// 注销策略
/// </summary>
public void UnregisterStrategy(string strategyName)
{
_strategies.Remove(strategyName);
}
/// <summary>
/// 设置当前策略
/// </summary>
public bool SetStrategy(string strategyName)
{
if (!_strategies.TryGetValue(strategyName, out var strategy))
{
GD.PushError($"策略 {strategyName} 未注册");
return false;
}
if (!strategy.CanExecute((T)this))
{
GD.PushWarning($"策略 {strategyName} 当前无法执行");
return false;
}
OnStrategyChanging(_currentStrategy, strategy);
_currentStrategy = strategy;
OnStrategyChanged(strategy);
return true;
}
/// <summary>
/// 尝试设置策略,失败时使用默认策略
/// </summary>
public void SetStrategyOrDefault(string strategyName, string defaultStrategyName)
{
if (!SetStrategy(strategyName))
{
SetStrategy(defaultStrategyName);
}
}
/// <summary>
/// 执行当前策略
/// </summary>
public void ExecuteCurrentStrategy()
{
if (_currentStrategy == null)
{
GD.PushWarning("当前没有设置策略");
return;
}
if (_currentStrategy.CanExecute((T)this))
{
_currentStrategy.Execute((T)this);
}
else
{
GD.PushWarning($"当前策略 {_currentStrategy.StrategyName} 无法执行");
}
}
/// <summary>
/// 执行指定策略
/// </summary>
public bool ExecuteStrategy(string strategyName)
{
if (_strategies.TryGetValue(strategyName, out var strategy))
{
if (strategy.CanExecute((T)this))
{
strategy.Execute((T)this);
return true;
}
}
return false;
}
/// <summary>
/// 获取所有可用策略
/// </summary>
public IEnumerable<IStrategy<T>> GetAvailableStrategies()
{
foreach (var strategy in _strategies.Values)
{
if (strategy.CanExecute((T)this))
{
yield return strategy;
}
}
}
/// <summary>
/// 策略切换前回调
/// </summary>
protected virtual void OnStrategyChanging(IStrategy<T> oldStrategy, IStrategy<T> newStrategy) { }
/// <summary>
/// 策略切换后回调
/// </summary>
protected virtual void OnStrategyChanged(IStrategy<T> newStrategy) { }
/// <summary>
/// 清空所有策略
/// </summary>
public void ClearStrategies()
{
_strategies.Clear();
_currentStrategy = null;
}
}
}
使用说明与最佳实践
适用场景:
- AI决策系统(根据环境选择最佳行为)
- 武器系统(根据距离选择最佳攻击方式)
- 寻路系统(根据地形选择寻路算法)
- 渲染系统(根据性能选择渲染质量)
使用方式:
- 创建继承
StrategyContext<T>的上下文类 - 注册各种策略实现
- 调用
ExecuteBest自动选择最佳策略 - 重写
OnStrategyChanged处理策略切换事件
- 创建继承
注意事项:
- 策略切换可能带来状态变化,确保
OnStrategyChanging/Changed正确处理 - 避免在策略执行过程中切换策略,可能导致状态不一致
- 策略优先级应该动态计算,考虑使用权重而非固定值
- 考虑策略切换的动画或过渡效果
- 策略切换可能带来状态变化,确保
扩展建议:
- 实现策略组,支持策略的组合和嵌套
- 添加策略学习机制,根据历史效果调整优先级
- 支持策略条件触发,当条件满足时自动切换
8.2.3 策略模式示例
设计思路与原理
本示例展示策略模式在AI系统中的典型应用:敌人AI根据与玩家的距离选择不同的攻击策略。近距离使用近战攻击,中距离使用远程射击,远距离使用魔法攻击。这种设计使AI行为更加智能和灵活。
核心实现要点
- 策略选择逻辑:每种策略的
CanExecute检查距离条件 - 优先级设置:魔法攻击优先级最高(威力大但CD长),近战最低
- 上下文数据:
EnemyAIContext包含敌人状态、玩家位置等决策所需信息 - 策略切换:AI每帧调用
ExecuteBest自动选择当前最优策略
与游戏AI的结合
策略模式是游戏AI的基础构建块:
- 可以将AI行为分解为多个策略(移动、攻击、防御、逃跑)
- 策略可以嵌套,形成层次化AI(如攻击策略包含近战、远程、魔法子策略)
- 策略可以组合,实现复杂行为(如同时执行移动和攻击)
- 策略可以动态学习,根据效果调整优先级
using Godot;
using GameFramework.Core.Strategy;
namespace GameFramework.Examples
{
/// <summary>
/// AI上下文
/// </summary>
public class AIContext : StrategyContext<AIContext>
{
public Node2D Self { get; set; }
public Node2D Target { get; set; }
public float Health { get; set; }
public float MaxHealth { get; set; }
public float AttackRange { get; set; }
public float DetectionRange { get; set; }
public double DeltaTime { get; set; }
public float HealthPercent => Health / MaxHealth;
public float DistanceToTarget => Target != null ?
Self.GlobalPosition.DistanceTo(Target.GlobalPosition) : float.MaxValue;
}
/// <summary>
/// 巡逻策略
/// </summary>
public class PatrolStrategy : IStrategy<AIContext>
{
public string StrategyName => "巡逻";
public bool CanExecute(AIContext context)
{
return context.Self != null;
}
public void Execute(AIContext context)
{
// 简单的来回巡逻逻辑
Vector2 moveDirection = Vector2.Right;
context.Self.Position += moveDirection * 100f * (float)context.DeltaTime;
}
}
/// <summary>
/// 追击策略
/// </summary>
public class ChaseStrategy : IStrategy<AIContext>
{
public string StrategyName => "追击";
public bool CanExecute(AIContext context)
{
return context.Target != null &&
context.DistanceToTarget <= context.DetectionRange &&
context.DistanceToTarget > context.AttackRange;
}
public void Execute(AIContext context)
{
if (context.Target == null) return;
Vector2 direction = (context.Target.GlobalPosition - context.Self.GlobalPosition).Normalized();
context.Self.Position += direction * 200f * (float)context.DeltaTime;
}
}
/// <summary>
/// 攻击策略
/// </summary>
public class AttackStrategy : IStrategy<AIContext>
{
public string StrategyName => "攻击";
private float _attackCooldown = 0f;
private const float ATTACK_INTERVAL = 1.0f;
public bool CanExecute(AIContext context)
{
return context.Target != null &&
context.DistanceToTarget <= context.AttackRange &&
_attackCooldown <= 0;
}
public void Execute(AIContext context)
{
// 执行攻击
GD.Print($"{context.Self.Name} 攻击 {context.Target.Name}");
_attackCooldown = ATTACK_INTERVAL;
}
public void UpdateCooldown(double delta)
{
if (_attackCooldown > 0)
{
_attackCooldown -= (float)delta;
}
}
}
/// <summary>
/// 逃跑策略
/// </summary>
public class FleeStrategy : IStrategy<AIContext>
{
public string StrategyName => "逃跑";
public bool CanExecute(AIContext context)
{
return context.HealthPercent < 0.3f && context.Target != null;
}
public void Execute(AIContext context)
{
if (context.Target == null) return;
Vector2 fleeDirection = (context.Self.GlobalPosition - context.Target.GlobalPosition).Normalized();
context.Self.Position += fleeDirection * 250f * (float)context.DeltaTime;
}
}
/// <summary>
/// AI控制器 - 使用策略模式
/// </summary>
public partial class AIController : Node2D
{
[Export] public Node2D Target;
[Export] public float MaxHealth = 100f;
[Export] public float AttackRange = 50f;
[Export] public float DetectionRange = 300f;
private AIContext _aiContext;
private float _currentHealth;
public override void _Ready()
{
_currentHealth = MaxHealth;
// 初始化策略上下文
_aiContext = new AIContext
{
Self = this,
Target = Target,
Health = _currentHealth,
MaxHealth = MaxHealth,
AttackRange = AttackRange,
DetectionRange = DetectionRange
};
// 注册策略
_aiContext.RegisterStrategy(new PatrolStrategy());
_aiContext.RegisterStrategy(new ChaseStrategy());
_aiContext.RegisterStrategy(new AttackStrategy());
_aiContext.RegisterStrategy(new FleeStrategy());
}
public override void _Process(double delta)
{
// 更新上下文
_aiContext.DeltaTime = delta;
_aiContext.Health = _currentHealth;
// 自动选择最佳策略
AutoSelectStrategy();
// 执行当前策略
_aiContext.ExecuteCurrentStrategy();
}
private void AutoSelectStrategy()
{
// 优先级顺序:逃跑 > 攻击 > 追击 > 巡逻
if (TrySetStrategy("逃跑")) return;
if (TrySetStrategy("攻击")) return;
if (TrySetStrategy("追击")) return;
if (TrySetStrategy("巡逻")) return;
}
private bool TrySetStrategy(string strategyName)
{
// 检查是否可以使用该策略
foreach (var strategy in _aiContext.GetAvailableStrategies())
{
if (strategy.StrategyName == strategyName)
{
if (_aiContext.CurrentStrategyName != strategyName)
{
_aiContext.SetStrategy(strategyName);
}
return true;
}
}
return false;
}
public void TakeDamage(float damage)
{
_currentHealth -= damage;
if (_currentHealth < 0) _currentHealth = 0;
}
}
}
使用说明与最佳实践
适用场景:
- 游戏AI的行为决策系统
- 角色状态机(战斗、移动、交互状态)
- 武器系统的攻击方式选择
- 寻路系统的路径算法选择
使用方式:
- 创建策略上下文类包含决策所需数据
- 实现各种策略类,定义
CanExecute和优先级 - 在AI控制器的
_Process中调用ExecuteBest - 根据游戏状态动态调整策略优先级
注意事项:
- 策略切换可能带来瞬态问题,考虑添加过渡状态
- 避免每帧都切换策略,可添加冷却时间
- 策略优先级应动态计算,考虑使用权重
- 策略应该是纯函数,避免副作用
与行为树对比:策略模式适合简单的条件选择,行为树适合复杂的行为组合
8.3 访问者模式
访问者模式允许在不改变对象结构的情况下定义作用于这些对象的新操作。
8.3.1 访问者接口
设计思路与原理
访问者模式(Visitor Pattern)是一种将算法与对象结构分离的设计模式。它允许你在不修改原有类的情况下,向现有类层次添加新操作。这在游戏开发中特别有用,因为游戏对象通常有复杂的继承结构,而你需要对这些对象执行各种操作(如序列化、调试渲染、统计收集)。
访问者模式的核心思想是"双重分派":第一次分派是Accept方法选择具体的访问者,第二次分派是Visit方法选择被访问的具体元素类型。这使得你可以根据"访问者类型"和"元素类型"两个维度来定制行为。
核心实现要点
IVisitor<T>定义访问者接口,Visit方法接收具体元素类型IVisitable定义可访问接口,Accept方法接收访问者- 双重分派实现多态访问
- 泛型接口支持类型安全
namespace GameFramework.Core.Visitor
{
/// <summary>
/// 访问者接口
/// </summary>
/// <typeparam name="T">可访问元素类型</typeparam>
public interface IVisitor<T> where T : class
{
/// <summary>
/// 访问元素
/// </summary>
void Visit(T element);
}
/// <summary>
/// 带返回值的访问者接口
/// </summary>
public interface IVisitor<TElement, TResult> where TElement : class
{
TResult Visit(TElement element);
}
/// <summary>
/// 可访问接口
/// 实现此接口的元素可以被访问者访问
/// </summary>
public interface IVisitable
{
/// <summary>
/// 接受访问者
/// </summary>
void Accept<TVisitor>(TVisitor visitor) where TVisitor : IVisitor<IVisitable>;
}
/// <summary>
/// 泛型可访问接口
/// </summary>
public interface IVisitable<T> where T : class
{
void Accept(IVisitor<T> visitor);
}
}
使用说明与最佳实践
适用场景:
- 场景树遍历和操作(序列化、统计、调试)
- 复杂对象结构的处理(UI树、行为树)
- 需要向现有类添加新操作但无法修改类的情况
- 类型相关的操作集中管理
使用方式:
- 元素类实现
IVisitable接口,在Accept中调用访问者的Visit - 访问者实现
IVisitor<T>,为每种元素类型提供Visit方法 - 调用元素的
Accept方法传入访问者 - 访问者中根据元素类型执行相应操作
- 元素类实现
注意事项:
- 访问者模式增加了类的数量,只在真正需要时使用
- 元素类层次变化时,需要更新所有访问者
- 访问者可以持有状态,但要注意线程安全
- 避免在访问过程中修改被访问的结构
与迭代器对比:迭代器关注如何遍历,访问者关注在遍历中做什么
8.3.2 访问者实现
设计思路与原理
SceneNodeVisitor是访问者模式在Godot场景树中的具体实现。Godot的场景树是一个天然的树形结构,访问者模式非常适合用于遍历和处理这种结构。基类提供了通用的遍历算法(深度优先),子类只需实现具体的访问逻辑。
核心实现要点
Traverse方法提供深度优先遍历Visit方法由子类实现具体访问逻辑- 支持访问者注册和批量访问
- 泛型支持类型安全
using System.Collections.Generic;
using Godot;
namespace GameFramework.Core.Visitor
{
/// <summary>
/// 场景节点访问者基类
/// 用于遍历和处理场景树节点
/// </summary>
public abstract class SceneNodeVisitor : IVisitor<Node>
{
public abstract void Visit(Node element);
/// <summary>
/// 遍历节点的所有子节点
/// </summary>
public void Traverse(Node root)
{
Visit(root);
foreach (Node child in root.GetChildren())
{
Traverse(child);
}
}
/// <summary>
/// 条件遍历
/// </summary>
public void Traverse(Node root, System.Predicate<Node> condition)
{
if (!condition(root)) return;
Visit(root);
foreach (Node child in root.GetChildren())
{
Traverse(child, condition);
}
}
}
/// <summary>
/// 访问者调度器
/// 管理多个访问者
/// </summary>
public class VisitorDispatcher
{
private readonly Dictionary<string, object> _visitors = new();
public void Register<T>(string name, IVisitor<T> visitor) where T : class
{
_visitors[name] = visitor;
}
public void Unregister(string name)
{
_visitors.Remove(name);
}
public IVisitor<T> GetVisitor<T>(string name) where T : class
{
if (_visitors.TryGetValue(name, out var visitor))
{
return visitor as IVisitor<T>;
}
return null;
}
public void VisitAll<T>(T element) where T : class
{
foreach (var visitor in _visitors.Values)
{
if (visitor is IVisitor<T> typedVisitor)
{
typedVisitor.Visit(element);
}
}
}
}
}
使用说明与最佳实践
适用场景:
- 场景树遍历和操作
- 批量处理多个访问者
- 访问者注册和管理
使用方式:
- 继承
SceneNodeVisitor实现具体访问者 - 重写
Visit方法实现访问逻辑 - 使用
VisitorDispatcher管理多个访问者 - 调用
Traverse遍历场景树
- 继承
注意事项:
Traverse是递归的,注意深度限制防止栈溢出- 访问者可以持有状态,但要注意重置
- 遍历过程中不要修改场景树结构
- 考虑使用迭代器替代递归处理超大树
8.3.3 具体访问者实现
设计思路与原理
本示例展示两个具体的访问者实现:SceneInfoVisitor收集场景统计信息,SceneSerializerVisitor序列化场景为JSON。这两个访问者展示了访问者模式的实际应用价值。
核心实现要点
- SceneInfoVisitor:统计节点数量、类型分布、最大深度
- SceneSerializerVisitor:将场景树序列化为JSON格式
- 访问者持有状态(计数器、字符串构建器)
- 深度优先遍历,递归处理子节点
状态管理
访问者可以持有状态:
- 统计访问者:计数器、最大值跟踪
- 序列化访问者:字符串构建器、缩进级别
- 调试访问者:可视化元素列表
状态在遍历前初始化,遍历后获取结果。
using System.Collections.Generic;
using System.Text;
using Godot;
using GameFramework.Core.Visitor;
namespace GameFramework.Examples
{
/// <summary>
/// 场景信息收集访问者
/// 收集场景中节点的统计信息
/// </summary>
public class SceneInfoVisitor : SceneNodeVisitor
{
private readonly Dictionary<string, int> _nodeTypeCounts = new();
private int _totalNodes = 0;
private int _maxDepth = 0;
private int _currentDepth = 0;
public override void Visit(Node element)
{
_totalNodes++;
_currentDepth++;
_maxDepth = Mathf.Max(_maxDepth, _currentDepth);
string typeName = element.GetType().Name;
_nodeTypeCounts[typeName] = _nodeTypeCounts.GetValueOrDefault(typeName, 0) + 1;
}
public new void Traverse(Node root)
{
_nodeTypeCounts.Clear();
_totalNodes = 0;
_maxDepth = 0;
_currentDepth = -1; // 根节点深度为0
base.Traverse(root);
}
public string GetReport()
{
var sb = new StringBuilder();
sb.AppendLine("=== 场景统计报告 ===");
sb.AppendLine($"总节点数: {_totalNodes}");
sb.AppendLine($"最大深度: {_maxDepth}");
sb.AppendLine("节点类型分布:");
foreach (var pair in _nodeTypeCounts)
{
sb.AppendLine($" {pair.Key}: {pair.Value}");
}
return sb.ToString();
}
}
/// <summary>
/// 节点禁用访问者
/// 禁用特定类型的节点
/// </summary>
public class NodeDisablerVisitor : SceneNodeVisitor
{
private readonly System.Type _targetType;
private readonly bool _recursive;
public int DisabledCount { get; private set; }
public NodeDisablerVisitor(System.Type targetType, bool recursive = false)
{
_targetType = targetType;
_recursive = recursive;
}
public override void Visit(Node element)
{
if (_targetType.IsAssignableFrom(element.GetType()))
{
DisableNode(element);
DisabledCount++;
if (!_recursive)
{
return; // 不递归子节点
}
}
}
private void DisableNode(Node node)
{
// 根据节点类型执行不同的禁用操作
if (node is CanvasItem canvasItem)
{
canvasItem.Hide();
canvasItem.SetProcess(false);
canvasItem.SetPhysicsProcess(false);
}
else if (node is CollisionObject2D collisionObject)
{
collisionObject.ProcessMode = ProcessModeEnum.Disabled;
}
GD.Print($"禁用节点: {node.Name}");
}
}
/// <summary>
/// 属性修改访问者
/// 批量修改节点的属性
/// </summary>
public class PropertyModifierVisitor : SceneNodeVisitor
{
private readonly System.Type _targetType;
private readonly string _propertyName;
private readonly Variant _propertyValue;
public int ModifiedCount { get; private set; }
public PropertyModifierVisitor(System.Type targetType, string propertyName, Variant propertyValue)
{
_targetType = targetType;
_propertyName = propertyName;
_propertyValue = propertyValue;
}
public override void Visit(Node element)
{
if (_targetType.IsAssignableFrom(element.GetType()))
{
if (element.GetClass().ToString().Contains(_propertyName) ||
element.GetType().GetProperty(_propertyName) != null)
{
element.Set(_propertyName, _propertyValue);
ModifiedCount++;
}
}
}
}
/// <summary>
/// 访问者模式使用示例
/// </summary>
public partial class VisitorExample : Node
{
public override void _Ready()
{
// 场景信息收集
var infoVisitor = new SceneInfoVisitor();
infoVisitor.Traverse(this);
GD.Print(infoVisitor.GetReport());
// 禁用所有粒子效果
var disabler = new NodeDisablerVisitor(typeof(GpuParticles2D), true);
disabler.Traverse(this);
GD.Print($"禁用了 {disabler.DisabledCount} 个粒子效果");
// 修改所有 Sprite2D 的透明度
var opacityModifier = new PropertyModifierVisitor(typeof(Sprite2D), "modulate", new Color(1, 1, 1, 0.5f));
opacityModifier.Traverse(this);
GD.Print($"修改了 {opacityModifier.ModifiedCount} 个精灵的透明度");
}
}
}
使用说明与最佳实践
适用场景:
- 场景统计分析(节点数量、类型分布)
- 场景序列化和反序列化
- 批量操作(禁用粒子、修改属性)
- 场景优化分析
使用方式:
- 继承
SceneNodeVisitor实现自定义访问者 - 在
Visit中实现具体操作逻辑 - 调用
Traverse遍历场景树 - 访问完成后获取结果
- 继承
注意事项:
- 访问者是单线程的,不要在多线程环境使用
- 大场景考虑分批处理,避免卡顿
- 访问过程中不要修改场景结构
- 重置访问者状态后再重新使用
性能优化:
- 使用迭代器替代递归避免栈溢出
- 缓存访问结果避免重复遍历
- 考虑并行处理独立分支(注意线程安全)
8.3.4 双重分派实现
设计思路与原理
双重分派(Double Dispatch)是访问者模式的核心机制,它根据两个对象的类型来决定调用哪个方法。与单分派(只根据接收者类型)不同,双重分派同时考虑"访问者类型"和"元素类型"两个维度。
C#和大多数面向对象语言只支持单分派(运行时根据对象实际类型选择方法)。访问者模式通过两次方法调用来模拟双重分派:
- 第一次分派:
element.Accept(visitor)根据元素实际类型调用相应的Accept - 第二次分派:
visitor.Visit(this)根据访问者实际类型调用相应的Visit
这种机制允许你根据"元素类型 × 访问者类型"的组合来定制行为,而不需要在元素类中添加大量if-else或switch。
核心实现要点
IVisitable<T>接口的Accept方法实现第一次分派IVisitor<T>接口的Visit方法实现第二次分派- 具体元素类实现
Accept,调用visitor.Visit(this) - 具体访问者类实现
Visit,根据元素类型执行操作
与多态的对比
- 单分派(多态):根据接收者类型选择方法
- 双重分派:根据接收者和参数类型共同选择方法
- 访问者模式:将操作从元素类移到访问者类,避免修改元素类
using System.Collections.Generic;
using Godot;
namespace GameFramework.Core.Visitor
{
/// <summary>
/// 游戏实体接口
/// 演示双重分派模式
/// </summary>
public interface IGameEntity : IVisitable<IGameEntity>
{
string EntityId { get; }
string EntityType { get; }
}
/// <summary>
/// 游戏实体访问者
/// </summary>
public interface IGameEntityVisitor : IVisitor<IGameEntity>
{
void VisitEnemy(EnemyEntity enemy);
void VisitPlayer(PlayerEntity player);
void VisitItem(ItemEntity item);
}
/// <summary>
/// 敌人实体
/// </summary>
public class EnemyEntity : IGameEntity
{
public string EntityId { get; set; }
public string EntityType => "Enemy";
public float Health { get; set; }
public float Damage { get; set; }
public void Accept(IVisitor<IGameEntity> visitor)
{
if (visitor is IGameEntityVisitor gameVisitor)
{
gameVisitor.VisitEnemy(this);
}
else
{
visitor.Visit(this);
}
}
}
/// <summary>
/// 玩家实体
/// </summary>
public class PlayerEntity : IGameEntity
{
public string EntityId { get; set; }
public string EntityType => "Player";
public string PlayerName { get; set; }
public int Score { get; set; }
public void Accept(IVisitor<IGameEntity> visitor)
{
if (visitor is IGameEntityVisitor gameVisitor)
{
gameVisitor.VisitPlayer(this);
}
else
{
visitor.Visit(this);
}
}
}
/// <summary>
/// 物品实体
/// </summary>
public class ItemEntity : IGameEntity
{
public string EntityId { get; set; }
public string EntityType => "Item";
public string ItemName { get; set; }
public int Quantity { get; set; }
public void Accept(IVisitor<IGameEntity> visitor)
{
if (visitor is IGameEntityVisitor gameVisitor)
{
gameVisitor.VisitItem(this);
}
else
{
visitor.Visit(this);
}
}
}
/// <summary>
/// 序列化访问者
/// </summary>
public class SerializationVisitor : IGameEntityVisitor
{
private readonly Godot.Collections.Dictionary<string, Variant> _data = new();
public void Visit(IGameEntity element)
{
_data["id"] = element.EntityId;
_data["type"] = element.EntityType;
}
public void VisitEnemy(EnemyEntity enemy)
{
Visit(enemy);
_data["health"] = enemy.Health;
_data["damage"] = enemy.Damage;
}
public void VisitPlayer(PlayerEntity player)
{
Visit(player);
_data["name"] = player.PlayerName;
_data["score"] = player.Score;
}
public void VisitItem(ItemEntity item)
{
Visit(item);
_data["itemName"] = item.ItemName;
_data["quantity"] = item.Quantity;
}
public Godot.Collections.Dictionary<string, Variant> GetData()
{
return _data;
}
}
}
使用说明与最佳 practices
适用场景:
- 需要根据"元素类型 × 访问者类型"组合执行不同操作
- 向现有类层次添加新操作但无法修改类
- 复杂对象结构的处理(如游戏实体系统的序列化)
- 避免在元素类中堆砌大量if-else
双重分派原理:
- 第一次分派:
element.Accept(visitor)选择元素的具体Accept方法 - 第二次分派:
visitor.Visit(this)选择访问者的具体Visit方法 - 结果:根据元素和访问者两者的类型确定最终执行的方法
- 第一次分派:
使用方式:
- 元素实现
Accept方法,在内部调用visitor.Visit(this) - 访问者实现针对不同元素类型的
Visit重载 - 调用
element.Accept(visitor)触发双重分派
- 元素实现
注意事项:
- 双重分派增加了复杂性,只在真正需要时使用
- 元素类层次变化时,需要更新所有访问者
- 避免过深的类层次,维护成本会指数增长
- 考虑使用C#的模式匹配(C# 7.0+)作为替代方案
与模式匹配对比:C# 7.0+的模式匹配提供了更简洁的替代方案,但访问者模式在需要多态扩展时更有优势。
8.4 本章小结
本章介绍了三种重要的行为模式:
命令模式:将操作封装为对象,支持撤销/重做、命令队列和批处理。适用于编辑器操作、玩家输入等场景。实施时需要注意命令对象的生命周期管理,确保撤销时引用的对象仍然有效。
策略模式:定义算法族并封装,支持运行时动态切换策略。适用于AI行为、排序算法、渲染策略等。策略切换本身开销很小,但需要考虑状态迁移和初始化成本。
访问者模式:将操作与对象结构分离,支持在不修改类的情况下添加新操作。适用于场景遍历、数据处理、序列化等。通过双重分派实现类型安全。
这些行为模式可以组合使用:
- 命令 + 策略:命令执行时使用不同策略
- 访问者 + 命令:访问者收集的命令可以放入命令管理器
- 策略 + 访问者:不同策略可以使用访问者访问不同数据
合理运用这些模式可以构建出灵活、可扩展的游戏行为系统。
8.4 行为模式选择指南
8.4.1 模式选择决策树
面对具体问题时,如何选择合适的行为模式?
行为模式选择决策树
是否需要撤销/重做功能?
├── 是 → 命令模式(Command)
│ └── 将操作封装为对象,保存历史栈
│
├── 否 → 是否需要运行时切换算法?
│ ├── 是 → 策略模式(Strategy)
│ │ └── 封装算法族,支持动态切换
│ │
│ └── 否 → 是否需要遍历对象结构执行操作?
│ ├── 是 → 访问者模式(Visitor)
│ │ └── 分离操作与对象结构
│ │
│ └── 否 → 考虑其他模式
8.4.2 各模式适用场景对比
| 模式 | 核心问题 | 解决方案 | 典型应用 |
|---|---|---|---|
| 命令模式 | 操作需要支持撤销/记录 | 封装为对象 | 编辑器、输入系统、宏录制 |
| 策略模式 | 多种算法需要动态切换 | 定义算法族 | AI行为、排序、渲染策略 |
| 访问者模式 | 对对象结构添加新操作 | 双重分派 | 场景遍历、序列化、数据处理 |
8.4.3 性能考量
| 模式 | 内存开销 | 运行时开销 | 优化建议 |
|---|---|---|---|
| 命令模式 | 高(保存历史) | 低 | 限制历史栈大小,批量处理 |
| 策略模式 | 中(多个策略对象) | 低 | 缓存策略实例,延迟加载 |
| 访问者模式 | 低 | 中等(双重分派) | 缓存访问者,避免频繁创建 |