第10章 Ui架构

UI架构是游戏开发中至关重要的部分。本章介绍MVVM模式在Godot中的实现、UI状态机和弹窗栈管理,帮助开发者构建清晰、可维护的UI系统。

10.1 MVVM在Godot中的实现

10.1.0 设计原理与思路

MVVM模式的核心概念

MVVM(Model-View-ViewModel)是一种将UI逻辑与业务逻辑分离的架构模式,其设计理念可用以下层次结构表示:

MVVM架构层次

┌─────────────────────────────────────┐
│              View(视图)            │
│  - Godot Control节点                │
│  - 负责UI布局和视觉效果             │
│  - 不包含业务逻辑                   │
└──────────────┬──────────────────────┘
               │ 数据绑定
               ▼
┌─────────────────────────────────────┐
│          ViewModel(视图模型)       │
│  - 业务逻辑的抽象                   │
│  - 数据转换和格式化                 │
│  - 属性变更通知(PropertyChanged)  │
└──────────────┬──────────────────────┘
               │ 调用
               ▼
┌─────────────────────────────────────┐
│            Model(模型)             │
│  - 游戏核心数据和状态               │
│  - 数据库/存档访问                  │
│  - 纯业务逻辑                       │
└─────────────────────────────────────┘

为什么在游戏开发中使用MVVM?

传统UI代码往往存在以下问题:

传统UI开发的问题

1. 代码重复
   - 多个界面显示相同数据时,复制粘贴更新逻辑
   - 一处修改,多处遗漏

2. 测试困难
   - UI逻辑与业务逻辑混合
   - 需要启动完整游戏才能测试

3. 维护成本高
   - 界面调整时可能破坏业务逻辑
   - 新成员理解代码门槛高

MVVM通过数据绑定机制解决这些问题:

  • View只关心如何显示数据,不关心数据来源
  • ViewModel只提供数据和命令,不关心UI细节
  • Model只管理业务状态,不知道UI存在

数据绑定的实现机制

本框架实现的数据绑定基于C#的INotifyPropertyChanged模式,但在Godot环境中做了适配:

数据绑定流程

1. View订阅ViewModel的PropertyChanged事件
   viewModel.PropertyChanged += OnPropertyChanged;

2. ViewModel数据变更时触发事件
   SetProperty(value) -> OnPropertyChanged(nameof(Property))

3. View响应事件更新UI
   OnPropertyChanged -> 更新对应控件的显示

4. 双向绑定时,View输入触发ViewModel更新
   UI事件 -> UpdateSource -> 更新ViewModel属性

UI状态管理的复杂性

游戏UI状态管理面临独特挑战:

游戏UI状态复杂性示例

主菜单
├── 开始游戏
│   └── 存档选择 -> 载入游戏 -> 游戏场景
├── 设置
│   ├── 图形设置 -> 分辨率/画质
│   ├── 音频设置 -> 音量调节
│   └── 控制设置 -> 键位绑定
├── 成就
│   └── 成就详情弹窗
└── 退出 -> 确认弹窗

游戏场景
├── HUD(始终显示)
├── 暂停菜单(模态)
│   ├── 继续
│   ├── 设置(子页面)
│   └── 返回主菜单(确认弹窗)
├── 背包(可叠加)
└── 任务日志(可叠加)

状态管理挑战:
1. 状态嵌套:设置界面可从主菜单和暂停菜单进入
2. 弹窗叠加:背包和任务日志可同时打开
3. 返回逻辑:从子页面返回而非直接关闭
4. 状态恢复:从设置返回时恢复之前的位置

10.1.1 使用场景分析

MVVM模式适用场景

  1. 复杂数据界面

    • 角色属性面板:多属性实时更新
    • 装备对比界面:左右分栏,数据同步
    • 背包系统:物品数据与格子UI绑定

    为什么适合MVVM:数据与显示分离,一处数据变更自动更新所有相关UI

  2. 配置/设置界面

    • 图形设置选项与配置文件双向绑定
    • 滑块值实时预览,确认后才保存
  3. 数据驱动的列表

    • 排行榜列表
    • 任务列表
    • 商店商品列表

    为什么适合MVVM:列表项数据变化自动刷新,无需手动管理UI更新

UI状态机适用场景

  1. 多页面流程

    • 新手引导:步骤1 -> 步骤2 -> 步骤3
    • 装备打造:选择材料 -> 确认属性 -> 打造动画 -> 结果展示
    • 剧情对话:对话1 -> 选项 -> 对话2A/对话2B
  2. 游戏状态切换

    • 主菜单 -> 加载 -> 游戏场景
    • 游戏场景 -> 暂停 -> 设置 -> 暂停 -> 游戏场景
    • 战斗开始 -> 玩家回合 -> 敌人回合 -> 回合结束

弹窗栈适用场景

  1. 模态框管理

    • 游戏中打开暂停菜单
    • 暂停菜单中打开设置弹窗
    • 设置中点击恢复默认弹出确认对话框
  2. 通知队列

    • 成就解锁提示(多个排队显示)
    • 任务完成通知
    • 系统公告
  3. 层级覆盖

    • 底层:游戏场景
    • 中层:HUD
    • 上层:菜单
    • 顶层:弹窗/通知

10.1.2 实现细节讲解

属性变更通知机制

ViewModelBase的核心是属性变更通知,实现细节如下:

// CallerMemberName自动获取调用属性的名称
protected bool SetProperty<T>(T value, [CallerMemberName] string propertyName = "")
{
    // 1. 检查值是否真正改变(避免无意义的通知)
    if (_propertyValues.TryGetValue(propertyName, out var oldValue))
    {
        if (EqualityComparer<T>.Default.Equals((T)oldValue, value))
        {
            return false; // 值未改变,不通知
        }
    }

    // 2. 存储新值
    _propertyValues[propertyName] = value;

    // 3. 批量更新模式优化
    if (_batchUpdate)
    {
        _changedProperties.Add(propertyName); // 延迟通知
    }
    else
    {
        OnPropertyChanged(propertyName); // 立即通知
    }

    return true;
}

为什么需要批量更新模式?

// 场景:一次性修改多个相关属性
public void LevelUp()
{
    BeginBatchUpdate();  // 开始批量更新

    Level++;            // 不立即通知
    MaxHealth += 10;    // 不立即通知
    Health = MaxHealth; // 不立即通知
    Attack += 5;        // 不立即通知

    EndBatchUpdate();   // 一次性通知所有变更
    // 避免4次UI刷新,合并为1次
}

UI状态机的层次设计

状态机支持层次化状态管理:

层次状态结构

StateMachine(根)
├── MainMenuState(主菜单)
│   ├── MainMenu_Main(主界面)
│   ├── MainMenu_Settings(设置页面)
│   └── MainMenu_Credits(制作名单)
├── GameplayState(游戏场景)
│   ├── Gameplay_Normal(正常游戏)
│   ├── Gameplay_Pause(暂停)
│   │   ├── Pause_Main(暂停主界面)
│   │   └── Pause_Settings(暂停中的设置)
│   └── Gameplay_Dialog(对话中)
└── LoadingState(加载中)

设计要点:
1. 子状态继承父状态的规则
2. 状态转换可配置条件(如"游戏中不能打开主菜单")
3. 状态栈支持任意层级返回

弹窗优先级管理

弹窗栈的优先级系统确保重要弹窗不被忽略:

// 优先级定义
public enum PopupPriority
{
    Lowest = -100,   // 普通提示
    Low = -50,       // 成就通知
    Normal = 0,      // 普通弹窗
    High = 50,       // 警告提示
    Critical = 100   // 错误/确认对话框
}

// 优先级管理逻辑
public void ShowPopup(IPopup popup)
{
    if (_popupStack.Count >= MaxVisiblePopups)
    {
        // 如果新弹窗优先级更高,移除优先级最低的
        if (!TryMakeSpaceForPopup(popup))
        {
            _popupQueue.Enqueue(popup); // 进入等待队列
            return;
        }
    }
    // ...显示弹窗
}

10.1.3 性能与权衡

数据绑定的开销

数据绑定带来便利的同时也有性能成本:

绑定性能分析

单次绑定开销:
  - 内存:存储绑定关系对象(约50-100字节)
  - CPU:订阅事件 + 初始值同步

运行时开销:
  - 属性变更时触发事件(委托调用)
  - 反射获取属性值(如使用反射绑定)

优化策略:
  1. 避免过度绑定
     - 静态文本不需要绑定
     - 一次性初始化的数据使用OneTime绑定

  2. 批量更新
     - 使用BeginBatchUpdate/EndBatchUpdate

  3. 延迟更新
     - 不在每帧更新绑定数据
     - 只在数据真正变化时更新

UI更新频率控制

// 反模式:每帧更新绑定数据
public override void _Process(double delta)
{
    // 错误!每帧触发PropertyChanged
    ViewModel.Health = Player.CurrentHealth;
}

// 正确做法:仅在值变化时更新
public void OnPlayerDamaged(float damage)
{
    // 只在受伤时触发更新
    ViewModel.Health = Player.CurrentHealth;
}

与命令模式的结合

MVVM与命令模式结合实现可撤销操作:

// 命令基类
public abstract class UICommand
{
    public abstract void Execute();
    public abstract void Undo();
}

// ViewModel中的命令
public class PlayerViewModel : ViewModelBase
{
    // 命令属性
    public ICommand LevelUpCommand { get; }

    public PlayerViewModel()
    {
        LevelUpCommand = new RelayCommand(
            execute: () => LevelUp(),
            canExecute: () => Experience >= MaxExperience
        );
    }
}

// 可撤销的命令实现
public class ChangeEquipmentCommand : UICommand
{
    private readonly PlayerViewModel _vm;
    private readonly string _oldEquipment;
    private readonly string _newEquipment;

    public override void Execute()
    {
        _vm.EquippedWeapon = _newEquipment;
    }

    public override void Undo()
    {
        _vm.EquippedWeapon = _oldEquipment;
    }
}

// 命令历史管理(实现撤销功能)
public class CommandHistory
{
    private Stack<UICommand> _undoStack = new();

    public void ExecuteCommand(UICommand command)
    {
        command.Execute();
        _undoStack.Push(command);
    }

    public void Undo()
    {
        if (_undoStack.Count > 0)
        {
            _undoStack.Pop().Undo();
        }
    }
}

10.1.4 实战指南

实施步骤

  1. 创建ViewModel类
// 1. 继承ViewModelBase
public partial class InventoryViewModel : ViewModelBase
{
    // 2. 定义可绑定属性
    public int Gold
    {
        get => GetProperty<int>();
        set => SetProperty(value);
    }

    // 3. 定义计算属性
    public string GoldText => $"金币: {Gold}";

    // 4. 实现初始化
    public override void Initialize()
    {
        Gold = 1000;
    }
}
  1. 创建View并绑定
public partial class InventoryPanel : BoundControl
{
    private Label _goldLabel;
    private InventoryViewModel _vm;

    public override void _Ready()
    {
        _goldLabel = GetNode<Label>("GoldLabel");

        // 创建并设置ViewModel
        _vm = new InventoryViewModel();
        _vm.Initialize();
        SetViewModel(_vm);
    }

    protected override void SetupBindings()
    {
        // 订阅属性变更事件
        _vm.PropertyChanged += (sender, e) =>
        {
            if (e.PropertyName == nameof(InventoryViewModel.GoldText))
            {
                _goldLabel.Text = _vm.GoldText;
            }
        };

        // 初始更新
        _goldLabel.Text = _vm.GoldText;
    }
}
  1. 在View中响应用户输入
// 购买按钮点击
private void OnBuyButtonPressed()
{
    // 调用ViewModel方法,不直接操作数据
    _vm.SpendGold(100);
}

// ViewModel中处理业务逻辑
public void SpendGold(int amount)
{
    if (Gold >= amount)
    {
        Gold -= amount;
        // PropertyChanged自动触发,UI自动更新
    }
}

常见错误:内存泄漏

// 错误示例:未取消事件订阅
public partial class PlayerPanel : Control
{
    private PlayerViewModel _vm;

    public override void _Ready()
    {
        _vm = new PlayerViewModel();
        _vm.PropertyChanged += OnPropertyChanged; // 订阅
    }

    // 缺少 _ExitTree 取消订阅!
}

// 正确做法:在_ExitTree中取消订阅
public override void _ExitTree()
{
    if (_vm != null)
    {
        _vm.PropertyChanged -= OnPropertyChanged;
    }
}

使用BoundControl基类自动管理

// BoundControl已封装订阅管理
public abstract partial class BoundControl : Control
{
    public override void _ExitTree()
    {
        if (ViewModel != null)
        {
            ViewModel.PropertyChanged -= OnViewModelPropertyChanged;
            BindingManager.Instance?.UnbindViewModel(ViewModel);
        }
    }
}

动画性能优化

// 反模式:使用SetProcess每帧更新动画
public override void _Process(double delta)
{
    // 错误!CPU消耗大
    Position += Velocity * delta;
}

// 正确做法:使用Tween
public void AnimateShow()
{
    var tween = CreateTween();
    tween.SetTrans(Tween.TransitionType.Quad);
    tween.SetEase(Tween.EaseType.Out);
    tween.TweenProperty(this, "position", targetPosition, 0.3f);
    // Tween由引擎优化,性能更好
}

// 使用AnimationPlayer复用动画
public override void _Ready()
{
    var animator = GetNode<AnimationPlayer>("AnimationPlayer");
    animator.Play("Show"); // 复用预定义动画
}

UI元素池化

对于频繁创建销毁的UI元素(如列表项),使用对象池:

public class UIObjectPool<T> where T : Control
{
    private Queue<T> _pool = new();
    private PackedScene _prefab;

    public T Get()
    {
        if (_pool.Count > 0)
        {
            return _pool.Dequeue();
        }
        return _prefab.Instantiate<T>();
    }

    public void Return(T obj)
    {
        obj.Hide();
        _pool.Enqueue(obj);
    }
}

// 使用示例
public partial class ItemList : Control
{
    private UIObjectPool<ItemSlot> _slotPool;

    public void RefreshList(List<ItemData> items)
    {
        // 回收旧槽位
        foreach (var slot in _activeSlots)
        {
            _slotPool.Return(slot);
        }
        _activeSlots.Clear();

        // 获取新槽位
        foreach (var item in items)
        {
            var slot = _slotPool.Get();
            slot.SetData(item);
            slot.Show();
            _activeSlots.Add(slot);
        }
    }
}

MVVM(Model-View-ViewModel)模式将UI逻辑与业务逻辑分离,提高代码的可测试性和可维护性。

10.1.1 ViewModelBase 实现

设计思路与原理

ViewModelBase是MVVM模式的基础抽象类,提供属性变更通知的核心机制,实现View与ViewModel的数据绑定。

核心设计原则:

  1. INotifyPropertyChanged:实现标准接口,让UI能够监听属性变化
  2. 自动通知SetProperty方法在值改变时自动触发事件
  3. 批量更新BeginUpdate/EndUpdate支持批量属性更新,减少UI刷新次数
  4. 属性名自动获取:使用CallerMemberName自动获取调用属性名

核心实现要点

  1. 属性变更事件PropertyChanged事件是数据绑定的核心
  2. 泛型Set方法SetProperty<T>提供类型安全的属性设置
  3. 更新状态跟踪_isUpdating标志防止批量更新期间的重复通知
  4. 延迟通知:批量更新结束后统一触发通知,提升性能

使用说明与最佳 practices

  • 适用场景:需要数据绑定的ViewModel基类
  • 注意事项
    1. 属性setter必须调用SetProperty而非直接赋值
    2. 计算属性需在依赖属性变化时手动触发通知
    3. 避免在属性变更事件中执行耗时操作
  • 性能考虑:大量属性同时变更时使用批量更新模式
  • 扩展建议:可添加ICommand接口支持、验证错误集合等
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Godot;

namespace GameFramework.UI.MVVM
{
    /// <summary>
    /// 属性变更事件参数
    /// </summary>
    public class PropertyChangedEventArgs : EventArgs
    {
        public string PropertyName { get; }

        public PropertyChangedEventArgs(string propertyName)
        {
            PropertyName = propertyName;
        }
    }

    /// <summary>
    /// ViewModel基类
    /// 实现属性变更通知机制
    /// </summary>
    public abstract class ViewModelBase : RefCounted
    {
        // 属性变更事件
        public event EventHandler<PropertyChangedEventArgs> PropertyChanged;

        // 属性值存储
        private readonly Dictionary<string, object> _propertyValues = new();

        // 批量更新模式
        private bool _batchUpdate = false;
        private readonly HashSet<string> _changedProperties = new();

        /// <summary>
        /// 获取属性值
        /// </summary>
        protected T GetProperty<T>([CallerMemberName] string propertyName = "")
        {
            if (_propertyValues.TryGetValue(propertyName, out var value))
            {
                return (T)value;
            }
            return default;
        }

        /// <summary>
        /// 设置属性值并触发变更通知
        /// </summary>
        protected bool SetProperty<T>(T value, [CallerMemberName] string propertyName = "")
        {
            if (_propertyValues.TryGetValue(propertyName, out var oldValue))
            {
                if (EqualityComparer<T>.Default.Equals((T)oldValue, value))
                {
                    return false;
                }
            }

            _propertyValues[propertyName] = value;

            if (_batchUpdate)
            {
                _changedProperties.Add(propertyName);
            }
            else
            {
                OnPropertyChanged(propertyName);
            }

            return true;
        }

        /// <summary>
        /// 触发属性变更事件
        /// </summary>
        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        /// <summary>
        /// 开始批量更新
        /// </summary>
        public void BeginBatchUpdate()
        {
            _batchUpdate = true;
            _changedProperties.Clear();
        }

        /// <summary>
        /// 结束批量更新
        /// </summary>
        public void EndBatchUpdate()
        {
            _batchUpdate = false;

            // 触发所有变更的属性
            foreach (var propertyName in _changedProperties)
            {
                OnPropertyChanged(propertyName);
            }

            _changedProperties.Clear();
        }

        /// <summary>
        /// 验证ViewModel
        /// </summary>
        public virtual bool Validate()
        {
            return true;
        }

        /// <summary>
        /// 初始化ViewModel
        /// </summary>
        public virtual void Initialize() { }

        /// <summary>
        /// 清理ViewModel
        /// </summary>
        public virtual void Cleanup() { }
    }

    /// <summary>
    /// 带验证的ViewModel基类
    /// </summary>
    public abstract class ValidatableViewModelBase : ViewModelBase
    {
        // 错误信息
        private readonly Dictionary<string, string> _errors = new();

        // 是否有错误
        public bool HasErrors => _errors.Count > 0;

        // 错误变更事件
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        /// <summary>
        /// 获取属性的错误信息
        /// </summary>
        public string GetError(string propertyName)
        {
            return _errors.GetValueOrDefault(propertyName, null);
        }

        /// <summary>
        /// 设置属性的错误信息
        /// </summary>
        protected void SetError(string propertyName, string error)
        {
            if (string.IsNullOrEmpty(error))
            {
                _errors.Remove(propertyName);
            }
            else
            {
                _errors[propertyName] = error;
            }

            ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
        }

        /// <summary>
        /// 清除所有错误
        /// </summary>
        protected void ClearErrors()
        {
            var properties = new List<string>(_errors.Keys);
            _errors.Clear();

            foreach (var property in properties)
            {
                ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(property));
            }
        }
    }

    /// <summary>
    /// 数据错误变更事件参数
    /// </summary>
    public class DataErrorsChangedEventArgs : EventArgs
    {
        public string PropertyName { get; }

        public DataErrorsChangedEventArgs(string propertyName)
        {
            PropertyName = propertyName;
        }
    }
}

10.1.2 数据绑定

设计思路与原理

BindingSystem提供ViewModel属性与Godot UI控件之间的自动同步机制。

绑定模式设计:

  1. OneWay:ViewModel变化自动更新UI(如显示玩家名称)
  2. TwoWay:双向同步,UI输入也同步回ViewModel(如输入框)
  3. OneTime:仅初始化时绑定一次(如静态标题)

转换器支持:

  • 类型转换:bool转Visibility、float转string等
  • 格式化:数字格式化、日期格式化
  • 条件转换:根据值返回不同结果(如HP<30%变红色)

核心实现要点

  1. 泛型绑定Bind<T>支持不同类型属性的类型安全绑定
  2. Godot信号集成:利用Godot的ValueChanged信号实现双向绑定
  3. 弱引用:使用WeakReference避免内存泄漏
  4. 自动清理:节点退出场景树时自动解除绑定

使用说明与最佳 practices

  • 适用场景:需要动态更新的UI元素(血条、分数、状态显示)
  • 注意事项
    1. 双向绑定注意避免循环触发(值变化→更新UI→触发事件→更新值)
    2. 复杂转换逻辑建议放在ViewModel中而非转换器
    3. 列表数据使用ObservableCollection实现自动更新
  • 性能考虑:频繁变化的数值考虑节流(Throttling)
  • 扩展建议:可添加验证绑定、错误提示绑定
using System;
using System.Collections.Generic;
using Godot;

namespace GameFramework.UI.MVVM
{
    /// <summary>
    /// 绑定模式
    /// </summary>
    public enum BindingMode
    {
        /// <summary>
        /// 单向绑定:ViewModel -> View
        /// </summary>
        OneWay,

        /// <summary>
        /// 双向绑定:ViewModel <-> View
        /// </summary>
        TwoWay,

        /// <summary>
        /// 单向到源:View -> ViewModel
        /// </summary>
        OneWayToSource,

        /// <summary>
        /// 一次性绑定
        /// </summary>
        OneTime
    }

    /// <summary>
    /// 绑定表达式
    /// </summary>
    public class BindingExpression
    {
        public string PropertyName { get; }
        public BindingMode Mode { get; }
        public Func<object, object> Converter { get; }
        public Func<object, object> ConverterBack { get; }

        public BindingExpression(string propertyName, BindingMode mode = BindingMode.OneWay,
            Func<object, object> converter = null, Func<object, object> converterBack = null)
        {
            PropertyName = propertyName;
            Mode = mode;
            Converter = converter;
            ConverterBack = converterBack;
        }
    }

    /// <summary>
    /// 数据绑定管理器
    /// </summary>
    public partial class BindingManager : Node
    {
        private static BindingManager _instance;
        public static BindingManager Instance => _instance;

        // 存储所有绑定关系
        private readonly List<BindingEntry> _bindings = new();

        // 绑定条目
        private class BindingEntry
        {
            public ViewModelBase ViewModel { get; set; }
            public Node Target { get; set; }
            public string ViewModelProperty { get; set; }
            public string TargetProperty { get; set; }
            public BindingMode Mode { get; set; }
            public Func<object, object> Converter { get; set; }
            public Func<object, object> ConverterBack { get; set; }
        }

        public override void _EnterTree()
        {
            if (_instance == null)
            {
                _instance = this;
            }
        }

        /// <summary>
        /// 创建绑定
        /// </summary>
        public void Bind(ViewModelBase viewModel, Node target,
            string viewModelProperty, string targetProperty,
            BindingMode mode = BindingMode.OneWay,
            Func<object, object> converter = null,
            Func<object, object> converterBack = null)
        {
            var entry = new BindingEntry
            {
                ViewModel = viewModel,
                Target = target,
                ViewModelProperty = viewModelProperty,
                TargetProperty = targetProperty,
                Mode = mode,
                Converter = converter,
                ConverterBack = converterBack
            };

            _bindings.Add(entry);

            // 订阅ViewModel属性变更
            viewModel.PropertyChanged += (sender, e) =>
            {
                if (e.PropertyName == viewModelProperty)
                {
                    UpdateTarget(entry);
                }
            };

            // 初始更新
            UpdateTarget(entry);
        }

        /// <summary>
        /// 解除绑定
        /// </summary>
        public void Unbind(ViewModelBase viewModel, Node target)
        {
            _bindings.RemoveAll(b => b.ViewModel == viewModel && b.Target == target);
        }

        /// <summary>
        /// 解除ViewModel的所有绑定
        /// </summary>
        public void UnbindViewModel(ViewModelBase viewModel)
        {
            _bindings.RemoveAll(b => b.ViewModel == viewModel);
        }

        /// <summary>
        /// 解除目标的所有绑定
        /// </summary>
        public void UnbindTarget(Node target)
        {
            _bindings.RemoveAll(b => b.Target == target);
        }

        /// <summary>
        /// 更新目标(ViewModel -> View)
        /// </summary>
        private void UpdateTarget(BindingEntry entry)
        {
            if (entry.Target == null || !GodotObject.IsInstanceValid(entry.Target))
            {
                return;
            }

            var value = entry.ViewModel.GetType().GetProperty(entry.ViewModelProperty)?.GetValue(entry.ViewModel);

            if (entry.Converter != null)
            {
                value = entry.Converter(value);
            }

            // 使用Godot的Set方法设置属性
            entry.Target.Set(entry.TargetProperty, Variant.From(value));
        }

        /// <summary>
        /// 更新源(View -> ViewModel)
        /// </summary>
        public void UpdateSource(BindingEntry entry, object value)
        {
            if (entry.Mode == BindingMode.TwoWay || entry.Mode == BindingMode.OneWayToSource)
            {
                if (entry.ConverterBack != null)
                {
                    value = entry.ConverterBack(value);
                }

                var property = entry.ViewModel.GetType().GetProperty(entry.ViewModelProperty);
                if (property != null && property.CanWrite)
                {
                    property.SetValue(entry.ViewModel, value);
                }
            }
        }
    }

    /// <summary>
    /// 绑定的控件基类
    /// </summary>
    public abstract partial class BoundControl : Control
    {
        protected ViewModelBase ViewModel { get; set; }

        /// <summary>
        /// 设置ViewModel
        /// </summary>
        public virtual void SetViewModel(ViewModelBase viewModel)
        {
            // 解除旧绑定
            if (ViewModel != null)
            {
                BindingManager.Instance?.UnbindViewModel(ViewModel);
            }

            ViewModel = viewModel;

            if (viewModel != null)
            {
                viewModel.PropertyChanged += OnViewModelPropertyChanged;
                SetupBindings();
            }
        }

        /// <summary>
        /// 设置数据绑定
        /// </summary>
        protected abstract void SetupBindings();

        /// <summary>
        /// ViewModel属性变更回调
        /// </summary>
        protected virtual void OnViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            // 子类重写
        }

        public override void _ExitTree()
        {
            if (ViewModel != null)
            {
                ViewModel.PropertyChanged -= OnViewModelPropertyChanged;
                BindingManager.Instance?.UnbindViewModel(ViewModel);
            }
        }
    }
}

10.1.3 MVVM使用示例

设计思路与原理

本示例展示一个完整的玩家状态面板MVVM实现,包含ViewModel定义、数据属性和对应的Godot场景。

示例结构:

  1. PlayerViewModel:封装玩家数据的ViewModel
  2. 属性设计:名称、等级、经验、生命值等游戏核心数据
  3. 计算属性:经验百分比、健康状态描述等派生值
  4. View场景:Godot场景树中的UI控件绑定

核心实现要点

  1. 属性封装:使用SetProperty实现属性变更通知
  2. 命令模式ICommand封装加经验、受伤等操作
  3. 场景绑定:在_Ready中建立ViewModel与控件的绑定关系
  4. 生命周期_ExitTree中清理绑定防止内存泄漏

使用说明与最佳 practices

  • 适用场景:角色状态面板、背包界面、设置面板等复杂UI
  • 注意事项
    1. ViewModel应独立于Godot节点,便于单元测试
    2. 业务逻辑放在ViewModel,视图逻辑放在View
    3. 避免在ViewModel中直接操作节点引用
  • 性能考虑:大量数据变化时考虑批量更新
  • 扩展建议:可添加数据验证、Undo/Redo支持
using Godot;
using GameFramework.UI.MVVM;

namespace GameFramework.Examples
{
    /// <summary>
    /// 玩家ViewModel
    /// </summary>
    public partial class PlayerViewModel : ViewModelBase
    {
        private string _playerName;
        private int _level;
        private int _experience;
        private int _maxExperience;
        private float _health;
        private float _maxHealth;
        private int _gold;

        public string PlayerName
        {
            get => GetProperty<string>();
            set
            {
                if (SetProperty(value))
                {
                    // 触发相关属性变更
                    OnPropertyChanged(nameof(DisplayName));
                }
            }
        }

        public int Level
        {
            get => GetProperty<int>();
            set
            {
                if (SetProperty(value))
                {
                    OnPropertyChanged(nameof(DisplayLevel));
                }
            }
        }

        public int Experience
        {
            get => GetProperty<int>();
            set => SetProperty(value);
        }

        public int MaxExperience
        {
            get => GetProperty<int>();
            set
            {
                if (SetProperty(value))
                {
                    OnPropertyChanged(nameof(ExperiencePercent));
                }
            }
        }

        public float Health
        {
            get => GetProperty<float>();
            set
            {
                if (SetProperty(value))
                {
                    OnPropertyChanged(nameof(HealthPercent));
                    OnPropertyChanged(nameof(HealthText));
                }
            }
        }

        public float MaxHealth
        {
            get => GetProperty<float>();
            set
            {
                if (SetProperty(value))
                {
                    OnPropertyChanged(nameof(HealthPercent));
                    OnPropertyChanged(nameof(HealthText));
                }
            }
        }

        public int Gold
        {
            get => GetProperty<int>();
            set => SetProperty(value);
        }

        // 计算属性
        public string DisplayName => $"Lv.{Level} {PlayerName}";
        public string DisplayLevel => $"等级 {Level}";
        public float ExperiencePercent => MaxExperience > 0 ? (float)Experience / MaxExperience : 0;
        public float HealthPercent => MaxHealth > 0 ? Health / MaxHealth : 0;
        public string HealthText => $"{Health:F0}/{MaxHealth:F0}";

        public override void Initialize()
        {
            // 初始化默认值
            PlayerName = "冒险者";
            Level = 1;
            Experience = 0;
            MaxExperience = 100;
            Health = 100;
            MaxHealth = 100;
            Gold = 0;
        }

        // 命令
        public void LevelUp()
        {
            Level++;
            MaxExperience = Level * 100;
            Experience = 0;
            MaxHealth = 100 + Level * 10;
            Health = MaxHealth;
        }

        public void TakeDamage(float damage)
        {
            Health = Mathf.Max(0, Health - damage);
        }

        public void Heal(float amount)
        {
            Health = Mathf.Min(MaxHealth, Health + amount);
        }

        public void AddExperience(int exp)
        {
            Experience += exp;
            if (Experience >= MaxExperience)
            {
                LevelUp();
            }
        }
    }

    /// <summary>
    /// 玩家状态面板View
    /// </summary>
    public partial class PlayerStatusPanel : BoundControl
    {
        // UI元素
        private Label _nameLabel;
        private Label _levelLabel;
        private ProgressBar _experienceBar;
        private ProgressBar _healthBar;
        private Label _healthText;
        private Label _goldLabel;

        public override void _Ready()
        {
            // 获取UI元素引用
            _nameLabel = GetNode<Label>("NameLabel");
            _levelLabel = GetNode<Label>("LevelLabel");
            _experienceBar = GetNode<ProgressBar>("ExperienceBar");
            _healthBar = GetNode<ProgressBar>("HealthBar");
            _healthText = GetNode<Label>("HealthBar/HealthText");
            _goldLabel = GetNode<Label>("GoldLabel");

            // 创建ViewModel并设置
            var viewModel = new PlayerViewModel();
            viewModel.Initialize();
            SetViewModel(viewModel);
        }

        protected override void SetupBindings()
        {
            if (ViewModel == null) return;

            var playerVm = ViewModel as PlayerViewModel;

            // 使用Godot的信号进行绑定
            playerVm.PropertyChanged += (sender, e) =>
            {
                CallDeferred(nameof(UpdateUI), e.PropertyName);
            };

            // 初始更新
            UpdateAllUI();
        }

        private void UpdateAllUI()
        {
            var vm = ViewModel as PlayerViewModel;
            if (vm == null) return;

            _nameLabel.Text = vm.DisplayName;
            _levelLabel.Text = vm.DisplayLevel;
            _experienceBar.Value = vm.ExperiencePercent * 100;
            _healthBar.Value = vm.HealthPercent * 100;
            _healthText.Text = vm.HealthText;
            _goldLabel.Text = $"金币: {vm.Gold}";
        }

        private void UpdateUI(string propertyName)
        {
            var vm = ViewModel as PlayerViewModel;
            if (vm == null) return;

            switch (propertyName)
            {
                case nameof(PlayerViewModel.DisplayName):
                    _nameLabel.Text = vm.DisplayName;
                    break;
                case nameof(PlayerViewModel.DisplayLevel):
                    _levelLabel.Text = vm.DisplayLevel;
                    break;
                case nameof(PlayerViewModel.ExperiencePercent):
                    _experienceBar.Value = vm.ExperiencePercent * 100;
                    break;
                case nameof(PlayerViewModel.HealthPercent):
                    _healthBar.Value = vm.HealthPercent * 100;
                    break;
                case nameof(PlayerViewModel.HealthText):
                    _healthText.Text = vm.HealthText;
                    break;
                case nameof(PlayerViewModel.Gold):
                    _goldLabel.Text = $"金币: {vm.Gold}";
                    break;
            }
        }

        protected override void OnViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            UpdateUI(e.PropertyName);
        }
    }
}

10.1.5 MVVM在Godot中的特殊性

Godot的节点系统与传统UI框架(如WPF、UWP)有很大不同,需要特别考虑如何适配MVVM模式。

Godot信号系统与数据绑定

Godot的信号(Signal)系统可以用来实现数据绑定:

传统MVVM数据绑定 vs Godot信号绑定

传统MVVM(WPF):
View ←→ 数据绑定 ←→ ViewModel
- 基于属性的自动同步
- 需要INotifyPropertyChanged

Godot信号绑定:
View ←→ 信号订阅 ←→ ViewModel
- 基于事件的显式连接
- 使用Godot信号或C#事件

场景树生命周期对ViewModel的影响

Godot的节点生命周期需要特别处理:

  • 实例化时:ViewModel可能需要异步初始化
  • _EnterTree:建立信号连接
  • _Ready:完成ViewModel绑定
  • _ExitTree:断开信号,释放引用
  • QueueFree:确保ViewModel正确清理

导出属性与依赖属性的对比

特性Godot [Export]WPF DependencyProperty
序列化自动需配置
编辑器支持内置需附加属性
变更通知需手动自动
绑定支持需额外实现原生支持
性能轻量较重

Godot MVVM最佳实践

  1. 使用部分类分离逻辑:将UI逻辑放在.cs文件中,场景文件保持纯净
  2. 延迟ViewModel初始化:在_Ready中而非构造函数中初始化
  3. 正确处理信号连接:在_EnterTree连接,在_ExitTree断开
  4. 避免循环引用:ViewModel不应直接持有View的强引用

10.2 UI状态机

UI状态机用于管理界面状态之间的转换,特别适合菜单系统和游戏状态切换。

10.2.0 设计原理与思路

为什么UI需要状态机

游戏UI通常包含多个界面(主菜单、设置、背包、商店等),这些界面之间的导航关系复杂:

UI导航示例:

主菜单
├── 开始游戏 → 选择存档 → 加载游戏 → 游戏主界面
├── 设置
│   ├── 音频设置
│   ├── 图形设置
│   ├── 控制设置
│   └── 返回主菜单
├── 成就系统
│   └── 返回主菜单
└── 退出游戏

不使用状态机的问题:

  • 界面切换逻辑分散在各个UI类中
  • 返回键处理复杂,难以管理导航历史
  • 界面堆叠关系混乱
  • 状态转换条件不明确

状态机 vs 页面导航

特性页面导航状态机
适用场景简单线性流程复杂多分支流程
历史管理需要额外实现内置支持
状态检查手动判断自动验证CanEnter/CanExit
代码组织分散集中管理
可扩展性

UI状态机的层次设计

复杂UI需要分层状态机:

UI状态机层次:

顶层:游戏状态
├── MainMenuState(主菜单状态)
├── PlayingState(游戏状态)
└── PausedState(暂停状态)

中层:菜单状态(以MainMenu为例)
├── MainScreenState(主界面)
├── SettingsState(设置界面)
├── AchievementsState(成就界面)
└── CreditsState( credits界面)

底层:子界面状态(以Settings为例)
├── AudioSettingsState
├── GraphicsSettingsState
└── ControlsSettingsState

历史记录与后退栈管理

UI状态机需要支持"返回"操作:

  • 栈式历史:后进先出,适合线性导航
  • 图式历史:记录完整路径,支持任意跳转
  • 混合模式:主要路径用栈,特殊跳转用图

10.2.1 UIStateMachine 实现

设计思路与原理

UIStateMachine是专为游戏UI设计的状态机,管理界面之间的切换逻辑和层级关系。

与游戏状态机的区别:

  1. 层级结构:UI状态有父子关系(如菜单下有子菜单)
  2. 覆盖关系:新状态可能覆盖而非完全替换旧状态(如弹窗)
  3. 返回逻辑:支持返回上一状态(如点击返回按钮)
  4. 动画协调:状态切换时触发过渡动画

核心组件:

  • IUIState接口:定义状态的生命周期方法
  • UIStateStack:管理状态栈,支持Push/Pop操作
  • 状态工厂:根据状态名创建对应状态实例

核心实现要点

  1. 栈式管理:使用Stack<IUIState>实现后进先出的状态切换
  2. 生命周期:Enter→Update→Exit的完整生命周期管理
  3. 数据传递OnEnter参数支持状态间传递数据
  4. 遮罩控制:自动管理下层状态的交互遮罩

使用说明与最佳 practices

  • 适用场景:多层级菜单、游戏流程、界面导航系统
  • 注意事项
    1. 避免状态循环引用(A→B→A导致无法返回)
    2. 状态切换时及时清理事件监听防止内存泄漏
    3. 考虑异常情况的状态回退(如加载失败)
  • 性能考虑:频繁切换的状态考虑对象池复用
  • 扩展建议:可添加状态历史记录支持多步返回
using System;
using System.Collections.Generic;
using Godot;

namespace GameFramework.UI.StateMachine
{
    /// <summary>
    /// UI状态接口
    /// </summary>
    public interface IUIState
    {
        /// <summary>
        /// 状态名称
        /// </summary>
        string StateName { get; }

        /// <summary>
        /// 进入状态
        /// </summary>
        void OnEnter(IUIState previousState, object data = null);

        /// <summary>
        /// 退出状态
        /// </summary>
        void OnExit(IUIState nextState);

        /// <summary>
        /// 状态更新
        /// </summary>
        void OnUpdate(double deltaTime);

        /// <summary>
        /// 是否可以进入此状态
        /// </summary>
        bool CanEnter(IUIState currentState);

        /// <summary>
        /// 是否可以从此状态退出
        /// </summary>
        bool CanExit(IUIState nextState);
    }

    /// <summary>
    /// UI状态基类
    /// </summary>
    public abstract class UIStateBase : IUIState
    {
        public abstract string StateName { get; }

        public virtual void OnEnter(IUIState previousState, object data = null) { }
        public virtual void OnExit(IUIState nextState) { }
        public virtual void OnUpdate(double deltaTime) { }
        public virtual bool CanEnter(IUIState currentState) => true;
        public virtual bool CanExit(IUIState nextState) => true;
    }

    /// <summary>
    /// UI状态转换
    /// </summary>
    public class UIStateTransition
    {
        public string FromState { get; set; }
        public string ToState { get; set; }
        public Func<bool> Condition { get; set; }
        public Action OnTransition { get; set; }
        public float TransitionDuration { get; set; }
    }

    /// <summary>
    /// UI状态机
    /// </summary>
    public partial class UIStateMachine : Node
    {
        // 状态字典
        private readonly Dictionary<string, IUIState> _states = new();

        // 状态转换规则
        private readonly List<UIStateTransition> _transitions = new();

        // 当前状态
        public IUIState CurrentState { get; private set; }

        // 状态栈(用于返回功能)
        private readonly Stack<IUIState> _stateStack = new();

        // 事件
        [Signal]
        public delegate void StateChangedEventHandler(string fromState, string toState);

        [Signal]
        public delegate void StateChangeFailedEventHandler(string fromState, string toState, string reason);

        // 是否在转换中
        public bool IsTransitioning { get; private set; }

        // 转换动画节点
        private AnimationPlayer _transitionAnimator;

        public override void _Ready()
        {
            _transitionAnimator = new AnimationPlayer();
            AddChild(_transitionAnimator);
        }

        public override void _Process(double delta)
        {
            if (CurrentState != null)
            {
                CurrentState.OnUpdate(delta);
            }
        }

        /// <summary>
        /// 注册状态
        /// </summary>
        public void RegisterState(IUIState state)
        {
            if (state == null)
            {
                GD.PushError("无法注册null状态");
                return;
            }

            _states[state.StateName] = state;
        }

        /// <summary>
        /// 注销状态
        /// </summary>
        public void UnregisterState(string stateName)
        {
            if (CurrentState?.StateName == stateName)
            {
                GD.PushWarning("无法注销当前状态");
                return;
            }

            _states.Remove(stateName);
        }

        /// <summary>
        /// 添加状态转换规则
        /// </summary>
        public void AddTransition(string fromState, string toState, Func<bool> condition = null, Action onTransition = null)
        {
            _transitions.Add(new UIStateTransition
            {
                FromState = fromState,
                ToState = toState,
                Condition = condition,
                OnTransition = onTransition
            });
        }

        /// <summary>
        /// 添加任意状态到指定状态的转换
        /// </summary>
        public void AddAnyTransition(string toState, Func<bool> condition = null, Action onTransition = null)
        {
            _transitions.Add(new UIStateTransition
            {
                FromState = "*",
                ToState = toState,
                Condition = condition,
                OnTransition = onTransition
            });
        }

        /// <summary>
        /// 切换到指定状态
        /// </summary>
        public bool ChangeState(string stateName, object data = null, bool pushToStack = true)
        {
            if (IsTransitioning)
            {
                EmitSignal(SignalName.StateChangeFailed,
                    CurrentState?.StateName ?? "None", stateName, "正在转换中");
                return false;
            }

            if (!_states.TryGetValue(stateName, out var newState))
            {
                EmitSignal(SignalName.StateChangeFailed,
                    CurrentState?.StateName ?? "None", stateName, "状态不存在");
                return false;
            }

            // 检查转换规则
            if (!CanTransitionTo(newState))
            {
                EmitSignal(SignalName.StateChangeFailed,
                    CurrentState?.StateName ?? "None", stateName, "转换条件不满足");
                return false;
            }

            // 检查状态自身条件
            if (CurrentState != null && !newState.CanEnter(CurrentState))
            {
                EmitSignal(SignalName.StateChangeFailed,
                    CurrentState?.StateName ?? "None", stateName, "目标状态拒绝进入");
                return false;
            }

            if (CurrentState != null && !CurrentState.CanExit(newState))
            {
                EmitSignal(SignalName.StateChangeFailed,
                    CurrentState?.StateName ?? "None", stateName, "当前状态拒绝退出");
                return false;
            }

            // 执行状态切换
            PerformStateChange(newState, data, pushToStack);
            return true;
        }

        /// <summary>
        /// 返回上一个状态
        /// </summary>
        public bool GoBack(object data = null)
        {
            if (_stateStack.Count == 0)
            {
                return false;
            }

            var previousState = _stateStack.Pop();
            return ChangeState(previousState.StateName, data, false);
        }

        /// <summary>
        /// 检查是否可以转换到指定状态
        /// </summary>
        private bool CanTransitionTo(IUIState newState)
        {
            string currentStateName = CurrentState?.StateName ?? "";

            foreach (var transition in _transitions)
            {
                bool fromMatch = transition.FromState == currentStateName ||
                                  transition.FromState == "*";
                bool toMatch = transition.ToState == newState.StateName;

                if (fromMatch && toMatch)
                {
                    return transition.Condition?.Invoke() ?? true;
                }
            }

            // 默认允许(如果没有特定规则)
            return true;
        }

        /// <summary>
        /// 执行状态切换
        /// </summary>
        private void PerformStateChange(IUIState newState, object data, bool pushToStack)
        {
            IsTransitioning = true;

            string fromStateName = CurrentState?.StateName ?? "None";

            // 退出当前状态
            if (CurrentState != null)
            {
                CurrentState.OnExit(newState);

                if (pushToStack)
                {
                    _stateStack.Push(CurrentState);
                }
            }

            var previousState = CurrentState;
            CurrentState = newState;

            // 进入新状态
            CurrentState.OnEnter(previousState, data);

            IsTransitioning = false;

            EmitSignal(SignalName.StateChanged, fromStateName, newState.StateName);

            GD.Print($"UI状态切换: {fromStateName} -> {newState.StateName}");
        }

        /// <summary>
        /// 获取状态
        /// </summary>
        public IUIState GetState(string stateName)
        {
            return _states.GetValueOrDefault(stateName);
        }

        /// <summary>
        /// 检查是否处于某状态
        /// </summary>
        public bool IsInState(string stateName)
        {
            return CurrentState?.StateName == stateName;
        }

        /// <summary>
        /// 清空状态栈
        /// </summary>
        public void ClearStack()
        {
            _stateStack.Clear();
        }

        /// <summary>
        /// 重置状态机
        /// </summary>
        public void Reset()
        {
            CurrentState?.OnExit(null);
            CurrentState = null;
            ClearStack();
            IsTransitioning = false;
        }
    }
}

10.2.2 界面切换动画

设计思路与原理

AnimatedUIState扩展基础状态类,集成Godot的Tween动画系统,实现流畅的界面切换效果。

动画设计原则:

  1. 一致性:同类界面使用相同动画(如所有弹窗都淡入淡出)
  2. 流畅性:动画时长通常0.2-0.5秒,太快突兀、太慢拖沓
  3. 方向感:新界面从右滑入暗示前进,从下滑入暗示弹出
  4. 取消处理:快速切换时旧动画应优雅取消

动画类型:

  • 淡入淡出:最通用,适合大多数界面
  • 滑动:暗示层级关系,如菜单侧滑
  • 缩放:强调弹窗、提示的重要性
  • 组合:多种效果叠加创造更丰富的体验

核心实现要点

  1. Tween动画:使用Godot的Tween系统实现平滑过渡
  2. 回调保证:动画完成后执行状态切换回调
  3. 状态保护:动画播放期间防止重复触发
  4. 自定义扩展:虚方法允许子类定义特定动画

使用说明与最佳 practices

  • 适用场景:所有需要过渡效果的UI状态
  • 注意事项
    1. 动画期间应屏蔽用户输入防止误操作
    2. 低性能设备可考虑减少动画或降低帧率
    3. 提供设置选项允许玩家关闭动画
  • 性能考虑:使用Tween而非AnimationPlayer减少内存占用
  • 扩展建议:可添加音效配合、粒子效果等增强反馈
using Godot;
using GameFramework.UI.StateMachine;

namespace GameFramework.UI.StateMachine
{
    /// <summary>
    /// 带动画的UI状态
    /// </summary>
    public partial class AnimatedUIState : UIStateBase
    {
        // 对应的UI面板
        public Control Panel { get; private set; }

        // 动画配置
        public float EnterDuration { get; set; } = 0.3f;
        public float ExitDuration { get; set; } = 0.2f;
        public Tween.TransitionType TransitionType { get; set; } = Tween.TransitionType.Quad;
        public Tween.EaseType EaseType { get; set; } = Tween.EaseType.Out;

        // 进入动画类型
        public EnterAnimationType EnterAnimation { get; set; } = EnterAnimationType.FadeIn;
        public ExitAnimationType ExitAnimation { get; set; } = ExitAnimationType.FadeOut;

        public enum EnterAnimationType
        {
            FadeIn,
            SlideFromLeft,
            SlideFromRight,
            SlideFromTop,
            SlideFromBottom,
            ScaleIn,
            PopIn
        }

        public enum ExitAnimationType
        {
            FadeOut,
            SlideToLeft,
            SlideToRight,
            SlideToTop,
            SlideToBottom,
            ScaleOut,
            PopOut
        }

        public AnimatedUIState(string stateName, Control panel)
        {
            _stateName = stateName;
            Panel = panel;
        }

        private string _stateName;
        public override string StateName => _stateName;

        private Tween _activeTween;

        public override void OnEnter(IUIState previousState, object data = null)
        {
            base.OnEnter(previousState, data);

            if (Panel == null) return;

            // 停止之前的动画
            _activeTween?.Kill();

            // 准备进入
            Panel.Show();
            Panel.Modulate = new Color(1, 1, 1, 1);
            Panel.Scale = Vector2.One;
            Panel.Position = Vector2.Zero;

            // 创建进入动画
            _activeTween = Panel.CreateTween();
            _activeTween.SetTrans(TransitionType);
            _activeTween.SetEase(EaseType);

            SetupEnterAnimation(_activeTween);

            _activeTween.Play();
        }

        public override void OnExit(IUIState nextState)
        {
            base.OnExit(nextState);

            if (Panel == null) return;

            // 停止之前的动画
            _activeTween?.Kill();

            // 创建退出动画
            _activeTween = Panel.CreateTween();
            _activeTween.SetTrans(TransitionType);
            _activeTween.SetEase(EaseType);

            SetupExitAnimation(_activeTween);

            _activeTween.Finished += () =>
            {
                Panel.Hide();
            };

            _activeTween.Play();
        }

        private void SetupEnterAnimation(Tween tween)
        {
            var screenSize = Panel.GetViewportRect().Size;

            switch (EnterAnimation)
            {
                case EnterAnimationType.FadeIn:
                    Panel.Modulate = new Color(1, 1, 1, 0);
                    tween.TweenProperty(Panel, "modulate", new Color(1, 1, 1, 1), EnterDuration);
                    break;

                case EnterAnimationType.SlideFromLeft:
                    Panel.Position = new Vector2(-screenSize.X, 0);
                    tween.TweenProperty(Panel, "position", Vector2.Zero, EnterDuration);
                    break;

                case EnterAnimationType.SlideFromRight:
                    Panel.Position = new Vector2(screenSize.X, 0);
                    tween.TweenProperty(Panel, "position", Vector2.Zero, EnterDuration);
                    break;

                case EnterAnimationType.SlideFromTop:
                    Panel.Position = new Vector2(0, -screenSize.Y);
                    tween.TweenProperty(Panel, "position", Vector2.Zero, EnterDuration);
                    break;

                case EnterAnimationType.SlideFromBottom:
                    Panel.Position = new Vector2(0, screenSize.Y);
                    tween.TweenProperty(Panel, "position", Vector2.Zero, EnterDuration);
                    break;

                case EnterAnimationType.ScaleIn:
                    Panel.Scale = Vector2.Zero;
                    Panel.PivotOffset = Panel.Size / 2;
                    tween.TweenProperty(Panel, "scale", Vector2.One, EnterDuration);
                    break;

                case EnterAnimationType.PopIn:
                    Panel.Scale = Vector2.Zero;
                    Panel.PivotOffset = Panel.Size / 2;
                    tween.TweenProperty(Panel, "scale", new Vector2(1.1f, 1.1f), EnterDuration * 0.6f);
                    tween.TweenProperty(Panel, "scale", Vector2.One, EnterDuration * 0.4f);
                    break;
            }
        }

        private void SetupExitAnimation(Tween tween)
        {
            var screenSize = Panel.GetViewportRect().Size;

            switch (ExitAnimation)
            {
                case ExitAnimationType.FadeOut:
                    tween.TweenProperty(Panel, "modulate", new Color(1, 1, 1, 0), ExitDuration);
                    break;

                case ExitAnimationType.SlideToLeft:
                    tween.TweenProperty(Panel, "position", new Vector2(-screenSize.X, 0), ExitDuration);
                    break;

                case ExitAnimationType.SlideToRight:
                    tween.TweenProperty(Panel, "position", new Vector2(screenSize.X, 0), ExitDuration);
                    break;

                case ExitAnimationType.SlideToTop:
                    tween.TweenProperty(Panel, "position", new Vector2(0, -screenSize.Y), ExitDuration);
                    break;

                case ExitAnimationType.SlideToBottom:
                    tween.TweenProperty(Panel, "position", new Vector2(0, screenSize.Y), ExitDuration);
                    break;

                case ExitAnimationType.ScaleOut:
                    Panel.PivotOffset = Panel.Size / 2;
                    tween.TweenProperty(Panel, "scale", Vector2.Zero, ExitDuration);
                    break;

                case ExitAnimationType.PopOut:
                    Panel.PivotOffset = Panel.Size / 2;
                    tween.TweenProperty(Panel, "scale", new Vector2(1.1f, 1.1f), ExitDuration * 0.3f);
                    tween.TweenProperty(Panel, "scale", Vector2.Zero, ExitDuration * 0.7f);
                    break;
            }
        }
    }

    /// <summary>
    /// 过渡效果管理器
    /// </summary>
    public partial class TransitionManager : CanvasLayer
    {
        [Export] public Color FadeColor = new Color(0, 0, 0, 1);
        [Export] public float DefaultDuration = 0.5f;

        private ColorRect _fadeRect;
        private AnimationPlayer _animator;

        public override void _Ready()
        {
            Layer = 100; // 确保在最上层

            _fadeRect = new ColorRect();
            _fadeRect.Color = new Color(FadeColor, 0);
            _fadeRect.SetAnchorsPreset(Control.LayoutPreset.FullRect);
            AddChild(_fadeRect);

            _animator = new AnimationPlayer();
            AddChild(_animator);

            // 创建淡入淡出动画
            CreateFadeAnimations();
        }

        private void CreateFadeAnimations()
        {
            // 淡入动画
            var fadeIn = new Animation();
            fadeIn.ResourceName = "FadeIn";
            fadeIn.Length = 1.0f;

            var fadeInTrack = fadeIn.AddTrack(Animation.TrackType.Value);
            fadeIn.TrackSetPath(fadeInTrack, new NodePath("ColorRect:color"));
            fadeIn.TrackInsertKey(fadeInTrack, 0, new Color(FadeColor, 0));
            fadeIn.TrackInsertKey(fadeInTrack, 1, FadeColor);

            // 淡出动画
            var fadeOut = new Animation();
            fadeOut.ResourceName = "FadeOut";
            fadeOut.Length = 1.0f;

            var fadeOutTrack = fadeOut.AddTrack(Animation.TrackType.Value);
            fadeOut.TrackSetPath(fadeOutTrack, new NodePath("ColorRect:color"));
            fadeOut.TrackInsertKey(fadeOutTrack, 0, FadeColor);
            fadeOut.TrackInsertKey(fadeOutTrack, 1, new Color(FadeColor, 0));

            _animator.AddAnimationLibrary("", new AnimationLibrary());
            _animator.GetAnimationLibrary("").AddAnimation("FadeIn", fadeIn);
            _animator.GetAnimationLibrary("").AddAnimation("FadeOut", fadeOut);
        }

        /// <summary>
        /// 播放淡入效果
        /// </summary>
        public void FadeIn(float duration = -1)
        {
            if (duration > 0)
            {
                _animator.SpeedScale = 1.0f / duration;
            }
            _animator.Play("FadeIn");
        }

        /// <summary>
        /// 播放淡出效果
        /// </summary>
        public void FadeOut(float duration = -1)
        {
            if (duration > 0)
            {
                _animator.SpeedScale = 1.0f / duration;
            }
            _animator.Play("FadeOut");
        }

        /// <summary>
        /// 播放过渡效果
        /// </summary>
        public async void PlayTransition(Action middleAction, float fadeDuration = -1)
        {
            float duration = fadeDuration > 0 ? fadeDuration : DefaultDuration;

            FadeIn(duration);
            await ToSignal(_animator, AnimationPlayer.SignalName.AnimationFinished);

            middleAction?.Invoke();

            FadeOut(duration);
        }
    }
}

10.3 弹窗栈管理

弹窗栈管理用于处理游戏中弹窗的显示顺序和返回逻辑。

10.3.1 PopupStack 实现

设计思路与原理

PopupStack管理弹窗的层级关系,确保后弹出的窗口显示在上层,支持按顺序关闭。

弹窗管理挑战:

  1. 层级冲突:多个弹窗同时显示时的Z-Index管理
  2. 返回键处理:Android/手柄的返回键应该关闭最上层弹窗
  3. 焦点控制:弹窗出现时背景UI应失去交互焦点
  4. 动画叠加:多个弹窗动画需要协调播放

栈式解决方案:

  • LIFO顺序:后打开的弹窗先关闭
  • 自动层级:根据栈深度自动计算Z-Index
  • 背景遮罩:自动管理遮罩层的显示/隐藏
  • 事件拦截:最上层弹窗拦截点击和返回键

核心实现要点

  1. 泛型栈结构Stack<IPopup>存储弹窗实例
  2. 优先级支持:紧急弹窗可插队显示
  3. 模态控制:模态弹窗阻止下层交互
  4. 异步等待ShowPopupAsync支持等待弹窗关闭

使用说明与最佳 practices

  • 适用场景:确认对话框、提示消息、设置面板、商店界面
  • 注意事项
    1. 避免弹窗嵌套过深(超过3层体验差)
    2. 模态弹窗应明确告诉用户如何关闭
    3. 考虑弹窗在屏幕边缘的适配
  • 性能考虑:弹窗对象池复用减少实例化开销
  • 扩展建议:可添加弹窗队列,避免同时弹出多个
using System.Collections.Generic;
using Godot;

namespace GameFramework.UI.Popup
{
    /// <summary>
    /// 弹窗接口
    /// </summary>
    public interface IPopup
    {
        /// <summary>
        /// 弹窗ID
        /// </summary>
        string PopupId { get; }

        /// <summary>
        /// 是否为模态弹窗
        /// </summary>
        bool IsModal { get; }

        /// <summary>
        /// 是否可以被其他弹窗覆盖
        /// </summary>
        bool CanBeOverridden { get; }

        /// <summary>
        /// 优先级(越高越优先显示)
        /// </summary>
        int Priority { get; }

        /// <summary>
        /// 显示弹窗
        /// </summary>
        void ShowPopup(object data = null);

        /// <summary>
        /// 隐藏弹窗
        /// </summary>
        void HidePopup();

        /// <summary>
        /// 关闭弹窗
        /// </summary>
        void ClosePopup();

        /// <summary>
        /// 弹窗关闭回调
        /// </summary>
        void OnPopupClosed();
    }

    /// <summary>
    /// 弹窗基类
    /// </summary>
    public abstract partial class PopupBase : Control, IPopup
    {
        [Export] public string PopupId { get; set; }
        [Export] public bool IsModal { get; set; } = true;
        [Export] public bool CanBeOverridden { get; set; } = true;
        [Export] public int Priority { get; set; } = 0;

        // 关闭按钮
        protected Button CloseButton { get; set; }

        // 背景遮罩
        protected ColorRect Backdrop { get; set; }

        // 是否正在显示
        public bool IsShowing { get; protected set; }

        // 弹窗结果
        public object Result { get; protected set; }

        public override void _Ready()
        {
            // 创建背景遮罩
            if (IsModal)
            {
                CreateBackdrop();
            }

            // 设置默认ID
            if (string.IsNullOrEmpty(PopupId))
            {
                PopupId = Name;
            }

            // 初始隐藏
            Hide();
            IsShowing = false;

            // 连接关闭按钮
            CloseButton = GetNodeOrNull<Button>("CloseButton");
            if (CloseButton != null)
            {
                CloseButton.Pressed += OnCloseButtonPressed;
            }

            // 连接输入事件
            GuiInput += OnGuiInput;
        }

        /// <summary>
        /// 创建背景遮罩
        /// </summary>
        private void CreateBackdrop()
        {
            Backdrop = new ColorRect();
            Backdrop.Color = new Color(0, 0, 0, 0.7f);
            Backdrop.SetAnchorsPreset(Control.LayoutPreset.FullRect);
            Backdrop.ZIndex = -1;

            // 点击遮罩关闭
            Backdrop.GuiInput += (@event) =>
            {
                if (@event is InputEventMouseButton mouseButton &&
                    mouseButton.ButtonIndex == MouseButton.Left &&
                    mouseButton.Pressed)
                {
                    if (CanBeOverridden)
                    {
                        ClosePopup();
                    }
                }
            };

            AddChild(Backdrop);
        }

        public virtual void ShowPopup(object data = null)
        {
            Show();
            IsShowing = true;

            // 播放显示动画
            PlayShowAnimation();

            OnPopupShown(data);
        }

        public virtual void HidePopup()
        {
            IsShowing = false;

            // 播放隐藏动画
            PlayHideAnimation();

            OnPopupHidden();
        }

        public virtual void ClosePopup()
        {
            PopupStack.Instance?.ClosePopup(this);
        }

        public virtual void OnPopupClosed()
        {
            Hide();
            IsShowing = false;
        }

        /// <summary>
        /// 弹窗显示时调用
        /// </summary>
        protected virtual void OnPopupShown(object data) { }

        /// <summary>
        /// 弹窗隐藏时调用
        /// </summary>
        protected virtual void OnPopupHidden() { }

        /// <summary>
        /// 关闭按钮按下
        /// </summary>
        private void OnCloseButtonPressed()
        {
            ClosePopup();
        }

        /// <summary>
        /// GUI输入事件
        /// </summary>
        private void OnGuiInput(InputEvent @event)
        {
            if (@event is InputEventKey keyEvent && keyEvent.Pressed)
            {
                if (keyEvent.Keycode == Key.Escape && CanBeOverridden)
                {
                    ClosePopup();
                }
            }
        }

        /// <summary>
        /// 播放显示动画
        /// </summary>
        protected virtual void PlayShowAnimation()
        {
            Modulate = new Color(1, 1, 1, 0);
            Scale = new Vector2(0.9f, 0.9f);

            var tween = CreateTween();
            tween.SetParallel(true);
            tween.TweenProperty(this, "modulate", new Color(1, 1, 1, 1), 0.2f);
            tween.TweenProperty(this, "scale", Vector2.One, 0.2f);
        }

        /// <summary>
        /// 播放隐藏动画
        /// </summary>
        protected virtual void PlayHideAnimation()
        {
            var tween = CreateTween();
            tween.SetParallel(true);
            tween.TweenProperty(this, "modulate", new Color(1, 1, 1, 0), 0.15f);
            tween.TweenProperty(this, "scale", new Vector2(0.9f, 0.9f), 0.15f);

            tween.Finished += () =>
            {
                Hide();
            };
        }

        /// <summary>
        /// 设置结果
        /// </summary>
        protected void SetResult(object result)
        {
            Result = result;
        }
    }

    /// <summary>
    /// 弹窗栈管理器
    /// </summary>
    public partial class PopupStack : CanvasLayer
    {
        private static PopupStack _instance;
        public static PopupStack Instance => _instance;

        // 弹窗栈
        private readonly Stack<IPopup> _popupStack = new();

        // 弹窗队列(等待显示的弹窗)
        private readonly Queue<IPopup> _popupQueue = new();

        // 弹窗容器
        private Control _popupContainer;

        // 最大同时显示的弹窗数
        [Export] public int MaxVisiblePopups = 3;

        // 是否暂停游戏
        [Export] public bool PauseGameOnPopup = true;

        // 当前是否有模态弹窗
        public bool HasModalPopup
        {
            get
            {
                foreach (var popup in _popupStack)
                {
                    if (popup.IsModal)
                        return true;
                }
                return false;
            }
        }

        public override void _EnterTree()
        {
            if (_instance == null)
            {
                _instance = this;
            }
        }

        public override void _Ready()
        {
            Layer = 50; // UI层之上

            _popupContainer = new Control();
            _popupContainer.SetAnchorsPreset(Control.LayoutPreset.FullRect);
            _popupContainer.MouseFilter = Control.MouseFilterEnum.Pass;
            AddChild(_popupContainer);
        }

        /// <summary>
        /// 显示弹窗
        /// </summary>
        public void ShowPopup(IPopup popup, object data = null)
        {
            if (popup == null) return;

            // 检查是否已在栈中
            if (_popupStack.Contains(popup))
            {
                GD.PushWarning($"弹窗 {popup.PopupId} 已在显示中");
                return;
            }

            // 检查是否超出最大显示数
            if (_popupStack.Count >= MaxVisiblePopups)
            {
                // 如果新弹窗优先级更高,移除优先级最低的
                if (!TryMakeSpaceForPopup(popup))
                {
                    _popupQueue.Enqueue(popup);
                    return;
                }
            }

            // 添加到栈
            _popupStack.Push(popup);

            // 添加到容器
            if (popup is Control control)
            {
                if (control.GetParent() != _popupContainer)
                {
                    _popupContainer.AddChild(control);
                }
            }

            // 暂停下层交互
            UpdateInputPassthrough();

            // 显示弹窗
            popup.ShowPopup(data);

            // 暂停游戏
            if (PauseGameOnPopup && popup.IsModal)
            {
                GetTree().Paused = true;
            }

            GD.Print($"显示弹窗: {popup.PopupId},当前栈大小: {_popupStack.Count}");
        }

        /// <summary>
        /// 关闭指定弹窗
        /// </summary>
        public void ClosePopup(IPopup popup)
        {
            if (popup == null) return;

            // 如果目标弹窗不在栈顶,先关闭栈顶的
            if (_popupStack.Count > 0 && _popupStack.Peek() != popup)
            {
                // 找到并移除目标弹窗
                var tempStack = new Stack<IPopup>();
                while (_popupStack.Count > 0)
                {
                    var current = _popupStack.Pop();
                    if (current == popup)
                    {
                        current.OnPopupClosed();
                        break;
                    }
                    tempStack.Push(current);
                }

                // 恢复其他弹窗
                while (tempStack.Count > 0)
                {
                    _popupStack.Push(tempStack.Pop());
                }
            }
            else if (_popupStack.Count > 0)
            {
                // 目标弹窗在栈顶
                _popupStack.Pop();
                popup.OnPopupClosed();
            }

            // 从队列中移除
            var queueList = new List<IPopup>(_popupQueue);
            queueList.Remove(popup);
            _popupQueue.Clear();
            foreach (var queuedPopup in queueList)
            {
                _popupQueue.Enqueue(queuedPopup);
            }

            // 更新输入传递
            UpdateInputPassthrough();

            // 恢复游戏
            if (PauseGameOnPopup && !HasModalPopup)
            {
                GetTree().Paused = false;
            }

            // 检查队列
            ProcessQueue();

            GD.Print($"关闭弹窗: {popup.PopupId},当前栈大小: {_popupStack.Count}");
        }

        /// <summary>
        /// 关闭栈顶弹窗
        /// </summary>
        public void CloseTopPopup()
        {
            if (_popupStack.Count > 0)
            {
                var popup = _popupStack.Peek();
                if (popup.CanBeOverridden)
                {
                    ClosePopup(popup);
                }
            }
        }

        /// <summary>
        /// 关闭所有弹窗
        /// </summary>
        public void CloseAllPopups()
        {
            while (_popupStack.Count > 0)
            {
                var popup = _popupStack.Pop();
                popup.OnPopupClosed();
            }

            _popupQueue.Clear();

            UpdateInputPassthrough();

            if (PauseGameOnPopup)
            {
                GetTree().Paused = false;
            }
        }

        /// <summary>
        /// 获取栈顶弹窗
        /// </summary>
        public IPopup GetTopPopup()
        {
            return _popupStack.Count > 0 ? _popupStack.Peek() : null;
        }

        /// <summary>
        /// 检查弹窗是否在栈中
        /// </summary>
        public bool IsPopupInStack(string popupId)
        {
            foreach (var popup in _popupStack)
            {
                if (popup.PopupId == popupId)
                    return true;
            }
            return false;
        }

        /// <summary>
        /// 尝试为新弹窗腾出空间
        /// </summary>
        private bool TryMakeSpaceForPopup(IPopup newPopup)
        {
            // 找到优先级最低的可以被覆盖的弹窗
            var tempStack = new Stack<IPopup>();
            IPopup lowestPriorityPopup = null;

            while (_popupStack.Count > 0)
            {
                var current = _popupStack.Pop();
                if (current.CanBeOverridden && (lowestPriorityPopup == null || current.Priority < lowestPriorityPopup.Priority))
                {
                    if (lowestPriorityPopup != null)
                    {
                        tempStack.Push(lowestPriorityPopup);
                    }
                    lowestPriorityPopup = current;
                }
                else
                {
                    tempStack.Push(current);
                }
            }

            // 恢复栈
            while (tempStack.Count > 0)
            {
                _popupStack.Push(tempStack.Pop());
            }

            // 如果新弹窗优先级更高,关闭最低优先级的
            if (lowestPriorityPopup != null && newPopup.Priority > lowestPriorityPopup.Priority)
            {
                ClosePopup(lowestPriorityPopup);
                return true;
            }

            return false;
        }

        /// <summary>
        /// 处理等待队列
        /// </summary>
        private void ProcessQueue()
        {
            while (_popupQueue.Count > 0 && _popupStack.Count < MaxVisiblePopups)
            {
                var popup = _popupQueue.Dequeue();
                ShowPopup(popup);
            }
        }

        /// <summary>
        /// 更新输入传递
        /// </summary>
        private void UpdateInputPassthrough()
        {
            if (_popupStack.Count == 0)
            {
                _popupContainer.MouseFilter = Control.MouseFilterEnum.Pass;
                return;
            }

            // 只有栈顶的模态弹窗需要拦截输入
            var topPopup = _popupStack.Peek();
            if (topPopup.IsModal)
            {
                _popupContainer.MouseFilter = Control.MouseFilterEnum.Stop;
            }
            else
            {
                _popupContainer.MouseFilter = Control.MouseFilterEnum.Pass;
            }
        }

        public override void _Input(InputEvent @event)
        {
            // ESC键关闭顶层弹窗
            if (@event is InputEventKey keyEvent && keyEvent.Pressed && keyEvent.Keycode == Key.Escape)
            {
                CloseTopPopup();
            }
        }
    }
}

10.3.2 模态框管理

设计思路与原理

模态框是一种强制用户必须处理的弹窗,会阻止用户与背后内容的交互,适用于重要确认和选择。

模态设计原则:

  1. 明确目的:模态框应该让用户清楚需要做什么决定
  2. 简洁文案:标题和描述要简短明了
  3. 行动突出:主要操作按钮应醒目(如蓝色)
  4. 安全出口:取消/关闭按钮要明确,防止用户 trapped

常用模态类型:

  • 确认对话框:删除、退出等不可逆操作确认
  • 输入对话框:需要用户输入文本或数值
  • 选择对话框:从多个选项中选择一个
  • 通知对话框:重要信息提示,只有一个确定按钮

核心实现要点

  1. 回调机制:使用Action<bool>Task<bool>返回用户选择
  2. 默认值:确定/取消的默认焦点应根据场景设置
  3. 快捷操作:支持Enter确认、ESC取消的键盘操作
  4. 视觉区分:确认和取消按钮使用不同颜色区分重要性

使用说明与最佳 practices

  • 适用场景:删除确认、退出确认、重要设置变更、错误提示
  • 注意事项
    1. 不要滥用模态框,频繁打断用户很烦人
    2. 避免模态框上再弹模态框
    3. 移动端要考虑屏幕大小适配
  • 性能考虑:模态框通常较轻量,无需对象池
  • 扩展建议:可添加图标支持、倒计时自动关闭等特性
using Godot;
using GameFramework.UI.Popup;

namespace GameFramework.UI.Popup
{
    /// <summary>
    /// 确认对话框
    /// </summary>
    public partial class ConfirmDialog : PopupBase
    {
        [Export] public string Title { get; set; } = "确认";
        [Export] public string Message { get; set; } = "确定要执行此操作吗?";
        [Export] public string ConfirmText { get; set; } = "确定";
        [Export] public string CancelText { get; set; } = "取消";

        private Label _titleLabel;
        private Label _messageLabel;
        private Button _confirmButton;
        private Button _cancelButton;

        // 回调
        public delegate void ConfirmCallback(bool confirmed);
        public ConfirmCallback OnConfirm;

        public override void _Ready()
        {
            base._Ready();

            // 创建UI
            CreateUI();

            // 设置文本
            UpdateText();
        }

        private void CreateUI()
        {
            // 面板容器
            var panel = new PanelContainer();
            panel.SetAnchorsPreset(Control.LayoutPreset.Center);
            panel.CustomMinimumSize = new Vector2(400, 200);
            AddChild(panel);

            // 垂直布局
            var vbox = new VBoxContainer();
            vbox.SetAnchorsPreset(Control.LayoutPreset.FullRect);
            vbox.AddThemeConstantOverride("separation", 20);
            vbox.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
            vbox.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
            panel.AddChild(vbox);

            // 标题
            _titleLabel = new Label();
            _titleLabel.HorizontalAlignment = HorizontalAlignment.Center;
            _titleLabel.AddThemeFontSizeOverride("font_size", 24);
            vbox.AddChild(_titleLabel);

            // 消息
            _messageLabel = new Label();
            _messageLabel.HorizontalAlignment = HorizontalAlignment.Center;
            _messageLabel.AutowrapMode = TextServer.AutowrapMode.Word;
            _messageLabel.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
            vbox.AddChild(_messageLabel);

            // 按钮容器
            var hbox = new HBoxContainer();
            hbox.Alignment = BoxContainer.AlignmentMode.Center;
            hbox.AddThemeConstantOverride("separation", 20);
            vbox.AddChild(hbox);

            // 确定按钮
            _confirmButton = new Button();
            _confirmButton.Pressed += OnConfirmPressed;
            hbox.AddChild(_confirmButton);

            // 取消按钮
            _cancelButton = new Button();
            _cancelButton.Pressed += OnCancelPressed;
            hbox.AddChild(_cancelButton);
        }

        private void UpdateText()
        {
            _titleLabel.Text = Title;
            _messageLabel.Text = Message;
            _confirmButton.Text = ConfirmText;
            _cancelButton.Text = CancelText;
        }

        private void OnConfirmPressed()
        {
            SetResult(true);
            OnConfirm?.Invoke(true);
            ClosePopup();
        }

        private void OnCancelPressed()
        {
            SetResult(false);
            OnConfirm?.Invoke(false);
            ClosePopup();
        }

        /// <summary>
        /// 静态方法:显示确认对话框
        /// </summary>
        public static void Show(string title, string message, ConfirmCallback callback,
            string confirmText = "确定", string cancelText = "取消")
        {
            var dialog = new ConfirmDialog
            {
                Title = title,
                Message = message,
                ConfirmText = confirmText,
                CancelText = cancelText,
                OnConfirm = callback
            };

            PopupStack.Instance?.ShowPopup(dialog);
        }
    }

    /// <summary>
    /// 提示对话框
    /// </summary>
    public partial class AlertDialog : PopupBase
    {
        [Export] public string Title { get; set; } = "提示";
        [Export] public string Message { get; set; } = "";
        [Export] public string OkText { get; set; } = "确定";
        [Export] public AlertType Type { get; set; } = AlertType.Info;

        public enum AlertType
        {
            Info,
            Warning,
            Error,
            Success
        }

        private Label _titleLabel;
        private Label _messageLabel;
        private Button _okButton;
        private TextureRect _iconRect;

        public delegate void AlertCallback();
        public AlertCallback OnOk;

        public override void _Ready()
        {
            base._Ready();

            CreateUI();
            UpdateText();
            UpdateStyle();
        }

        private void CreateUI()
        {
            var panel = new PanelContainer();
            panel.SetAnchorsPreset(Control.LayoutPreset.Center);
            panel.CustomMinimumSize = new Vector2(350, 180);
            AddChild(panel);

            var margin = new MarginContainer();
            margin.AddThemeConstantOverride("margin_left", 20);
            margin.AddThemeConstantOverride("margin_right", 20);
            margin.AddThemeConstantOverride("margin_top", 20);
            margin.AddThemeConstantOverride("margin_bottom", 20);
            panel.AddChild(margin);

            var vbox = new VBoxContainer();
            vbox.AddThemeConstantOverride("separation", 15);
            margin.AddChild(vbox);

            // 标题行
            var titleHBox = new HBoxContainer();
            titleHBox.AddThemeConstantOverride("separation", 10);
            vbox.AddChild(titleHBox);

            _iconRect = new TextureRect();
            _iconRect.CustomMinimumSize = new Vector2(32, 32);
            _iconRect.ExpandMode = TextureRect.ExpandModeEnum.FitWidthProportional;
            titleHBox.AddChild(_iconRect);

            _titleLabel = new Label();
            _titleLabel.AddThemeFontSizeOverride("font_size", 22);
            titleHBox.AddChild(_titleLabel);

            // 消息
            _messageLabel = new Label();
            _messageLabel.AutowrapMode = TextServer.AutowrapMode.Word;
            _messageLabel.SizeFlagsVertical = Control.SizeFlags.ExpandFill;
            vbox.AddChild(_messageLabel);

            // 确定按钮
            _okButton = new Button();
            _okButton.Pressed += OnOkPressed;
            _okButton.SizeFlagsHorizontal = Control.SizeFlags.ShrinkCenter;
            vbox.AddChild(_okButton);
        }

        private void UpdateText()
        {
            _titleLabel.Text = Title;
            _messageLabel.Text = Message;
            _okButton.Text = OkText;
        }

        private void UpdateStyle()
        {
            // 根据类型设置颜色和图标
            Color color;
            string iconPath;

            switch (Type)
            {
                case AlertType.Warning:
                    color = Colors.Yellow;
                    iconPath = "res://assets/icons/warning.png";
                    break;
                case AlertType.Error:
                    color = Colors.Red;
                    iconPath = "res://assets/icons/error.png";
                    break;
                case AlertType.Success:
                    color = Colors.Green;
                    iconPath = "res://assets/icons/success.png";
                    break;
                default:
                    color = Colors.Blue;
                    iconPath = "res://assets/icons/info.png";
                    break;
            }

            _titleLabel.Modulate = color;

            // 加载图标
            if (ResourceLoader.Exists(iconPath))
            {
                _iconRect.Texture = ResourceLoader.Load<Texture2D>(iconPath);
            }
        }

        private void OnOkPressed()
        {
            OnOk?.Invoke();
            ClosePopup();
        }

        public static void Show(string title, string message, AlertType type = AlertType.Info, AlertCallback callback = null)
        {
            var dialog = new AlertDialog
            {
                Title = title,
                Message = message,
                Type = type,
                OnOk = callback
            };

            PopupStack.Instance?.ShowPopup(dialog);
        }

        public static void ShowInfo(string title, string message, AlertCallback callback = null)
        {
            Show(title, message, AlertType.Info, callback);
        }

        public static void ShowWarning(string title, string message, AlertCallback callback = null)
        {
            Show(title, message, AlertType.Warning, callback);
        }

        public static void ShowError(string title, string message, AlertCallback callback = null)
        {
            Show(title, message, AlertType.Error, callback);
        }

        public static void ShowSuccess(string title, string message, AlertCallback callback = null)
        {
            Show(title, message, AlertType.Success, callback);
        }
    }
}

10.4 本章小结

本章介绍了UI架构的三大核心组件:

  1. MVVM模式:通过ViewModelBase实现属性变更通知,使用数据绑定机制将UI与数据分离,提高代码的可测试性和可维护性。

  2. UI状态机:管理界面状态之间的转换,支持动画过渡、状态栈和转换规则,适用于复杂的菜单系统和游戏状态管理。

  3. 弹窗栈管理:处理弹窗的显示顺序和生命周期,支持模态弹窗、优先级管理和队列机制。

这些组件可以组合使用:

  • MVVM + 状态机:每个UI状态使用MVVM管理数据
  • 弹窗 + MVVM:弹窗内部使用MVVM模式
  • 状态机 + 弹窗:状态切换时自动处理弹窗

合理运用这些架构模式可以构建出清晰、灵活、易于维护的UI系统。