Skip to content

Godot 暂停处理

概述

Godot 暂停处理系统是 GFramework.Godot 中连接框架暂停管理与 Godot 引擎暂停机制的核心组件。它提供了暂停栈管理、分组暂停、嵌套暂停等功能,让你可以在 Godot 项目中使用 GFramework 的暂停系统。

通过 Godot 暂停处理系统,你可以实现精细的暂停控制,支持游戏逻辑暂停、UI 暂停、动画暂停等多种场景,同时保持与 Godot SceneTree 暂停机制的完美兼容。

主要特性

  • 暂停栈管理(支持嵌套暂停)
  • 分组暂停(Global、Gameplay、Animation、Audio 等)
  • 与 Godot SceneTree.Paused 集成
  • 暂停处理器机制
  • 暂停作用域(支持 using 语法)
  • 线程安全的暂停管理

核心概念

暂停栈管理器

IPauseStackManager 管理游戏中的暂停状态:

csharp
public interface IPauseStackManager : IContextUtility
{
    // 推入暂停请求
    PauseToken Push(string reason, PauseGroup group = PauseGroup.Global);

    // 弹出暂停请求
    bool Pop(PauseToken token);

    // 查询是否暂停
    bool IsPaused(PauseGroup group = PauseGroup.Global);

    // 获取暂停深度
    int GetPauseDepth(PauseGroup group = PauseGroup.Global);

    // 暂停状态变化事件
    event EventHandler<PauseStateChangedEventArgs>? OnPauseStateChanged;
}

暂停组

PauseGroup 定义不同的暂停作用域:

csharp
public enum PauseGroup
{
    Global = 0,      // 全局暂停(影响所有系统)
    Gameplay = 1,    // 游戏逻辑暂停(不影响 UI)
    Animation = 2,   // 动画暂停
    Audio = 3,       // 音频暂停
    Custom1 = 10,    // 自定义组 1
    Custom2 = 11,    // 自定义组 2
    Custom3 = 12     // 自定义组 3
}

暂停令牌

PauseToken 唯一标识一个暂停请求:

csharp
public readonly struct PauseToken
{
    public Guid Id { get; }
    public bool IsValid { get; }
}

Godot 暂停处理器

GodotPauseHandler 响应暂停栈状态变化,控制 SceneTree.Paused:

csharp
public class GodotPauseHandler : IPauseHandler
{
    public int Priority => 0;

    public void OnPauseStateChanged(PauseGroup group, bool isPaused)
    {
        // 只有 Global 组影响 Godot 的全局暂停
        if (group == PauseGroup.Global)
        {
            _tree.Paused = isPaused;
        }
    }
}

基本用法

设置暂停系统

csharp
using GFramework.Godot.Architecture;
using GFramework.Godot.Pause;
using GFramework.Core.Pause;

public class GameArchitecture : AbstractArchitecture
{
    protected override void InstallModules()
    {
        // 注册暂停栈管理器
        var pauseManager = new PauseStackManager();
        RegisterUtility<IPauseStackManager>(pauseManager);

        // 注册 Godot 暂停处理器
        var pauseHandler = new GodotPauseHandler(GetTree());
        pauseManager.RegisterHandler(pauseHandler);
    }
}

基本暂停和恢复

csharp
using Godot;
using GFramework.Godot.Extensions;

public partial class PauseMenu : Control
{
    private PauseToken _pauseToken;

    public void ShowPauseMenu()
    {
        // 暂停游戏
        var pauseManager = this.GetUtility<IPauseStackManager>();
        _pauseToken = pauseManager.Push("Pause menu opened");

        Show();
    }

    public void HidePauseMenu()
    {
        // 恢复游戏
        var pauseManager = this.GetUtility<IPauseStackManager>();
        pauseManager.Pop(_pauseToken);

        Hide();
    }
}

使用暂停作用域

csharp
using Godot;
using GFramework.Godot.Extensions;

public partial class DialogBox : Control
{
    public async void ShowDialog()
    {
        var pauseManager = this.GetUtility<IPauseStackManager>();

        // 使用 using 语法自动管理暂停
        using (pauseManager.PauseScope("Dialog shown"))
        {
            Show();
            await ToSignal(GetTree().CreateTimer(3.0f), "timeout");
            Hide();
        } // 自动恢复
    }
}

查询暂停状态

csharp
using Godot;
using GFramework.Godot.Extensions;

public partial class GameController : Node
{
    public override void _Process(double delta)
    {
        var pauseManager = this.GetUtility<IPauseStackManager>();

        // 检查是否暂停
        if (pauseManager.IsPaused())
        {
            GD.Print("游戏已暂停");
            return;
        }

        // 游戏逻辑
        UpdateGame(delta);
    }

    private void UpdateGame(double delta)
    {
        // 游戏更新逻辑
    }
}

高级用法

分组暂停

csharp
using Godot;
using GFramework.Godot.Extensions;

public partial class GameManager : Node
{
    private PauseToken _gameplayPauseToken;
    private PauseToken _animationPauseToken;

    // 只暂停游戏逻辑,UI 仍然可以交互
    public void PauseGameplay()
    {
        var pauseManager = this.GetUtility<IPauseStackManager>();
        _gameplayPauseToken = pauseManager.Push("Gameplay paused", PauseGroup.Gameplay);
    }

    public void ResumeGameplay()
    {
        var pauseManager = this.GetUtility<IPauseStackManager>();
        pauseManager.Pop(_gameplayPauseToken);
    }

    // 只暂停动画
    public void PauseAnimations()
    {
        var pauseManager = this.GetUtility<IPauseStackManager>();
        _animationPauseToken = pauseManager.Push("Animations paused", PauseGroup.Animation);
    }

    public void ResumeAnimations()
    {
        var pauseManager = this.GetUtility<IPauseStackManager>();
        pauseManager.Pop(_animationPauseToken);
    }

    // 检查特定组的暂停状态
    public bool IsGameplayPaused()
    {
        var pauseManager = this.GetUtility<IPauseStackManager>();
        return pauseManager.IsPaused(PauseGroup.Gameplay);
    }
}

嵌套暂停

csharp
using Godot;
using GFramework.Godot.Extensions;

public partial class GameScene : Node
{
    public async void ShowNestedDialogs()
    {
        var pauseManager = this.GetUtility<IPauseStackManager>();

        // 第一层暂停
        using (pauseManager.PauseScope("First dialog"))
        {
            GD.Print($"暂停深度: {pauseManager.GetPauseDepth()}"); // 输出: 1
            ShowDialog("第一个对话框");
            await ToSignal(GetTree().CreateTimer(2.0f), "timeout");

            // 第二层暂停
            using (pauseManager.PauseScope("Second dialog"))
            {
                GD.Print($"暂停深度: {pauseManager.GetPauseDepth()}"); // 输出: 2
                ShowDialog("第二个对话框");
                await ToSignal(GetTree().CreateTimer(2.0f), "timeout");
            }

            GD.Print($"暂停深度: {pauseManager.GetPauseDepth()}"); // 输出: 1
        }

        GD.Print($"暂停深度: {pauseManager.GetPauseDepth()}"); // 输出: 0
    }

    private void ShowDialog(string message)
    {
        GD.Print(message);
    }
}

自定义暂停处理器

csharp
using GFramework.Core.Abstractions.Pause;
using Godot;

// 自定义动画暂停处理器
public class AnimationPauseHandler : IPauseHandler
{
    private readonly AnimationPlayer _animationPlayer;

    public AnimationPauseHandler(AnimationPlayer animationPlayer)
    {
        _animationPlayer = animationPlayer;
    }

    public int Priority => 10;

    public void OnPauseStateChanged(PauseGroup group, bool isPaused)
    {
        // 只响应 Animation 组
        if (group == PauseGroup.Animation)
        {
            if (isPaused)
            {
                _animationPlayer.Pause();
                GD.Print("动画已暂停");
            }
            else
            {
                _animationPlayer.Play();
                GD.Print("动画已恢复");
            }
        }
    }
}

// 注册自定义处理器
public partial class GameController : Node
{
    public override void _Ready()
    {
        var pauseManager = this.GetUtility<IPauseStackManager>();
        var animationPlayer = GetNode<AnimationPlayer>("AnimationPlayer");

        var animationHandler = new AnimationPauseHandler(animationPlayer);
        pauseManager.RegisterHandler(animationHandler);
    }
}

音频暂停处理器

csharp
using GFramework.Core.Abstractions.Pause;
using Godot;

public class AudioPauseHandler : IPauseHandler
{
    private readonly AudioStreamPlayer _musicPlayer;
    private readonly AudioStreamPlayer _sfxPlayer;

    public AudioPauseHandler(AudioStreamPlayer musicPlayer, AudioStreamPlayer sfxPlayer)
    {
        _musicPlayer = musicPlayer;
        _sfxPlayer = sfxPlayer;
    }

    public int Priority => 20;

    public void OnPauseStateChanged(PauseGroup group, bool isPaused)
    {
        if (group == PauseGroup.Audio || group == PauseGroup.Global)
        {
            if (isPaused)
            {
                _musicPlayer.StreamPaused = true;
                _sfxPlayer.StreamPaused = true;
            }
            else
            {
                _musicPlayer.StreamPaused = false;
                _sfxPlayer.StreamPaused = false;
            }
        }
    }
}

节点暂停模式控制

csharp
using Godot;
using GFramework.Godot.Extensions;

public partial class GameNode : Node
{
    public override void _Ready()
    {
        // 设置节点在暂停时的行为

        // 暂停时停止处理
        ProcessMode = ProcessModeEnum.Pausable;

        // 暂停时继续处理(用于 UI)
        // ProcessMode = ProcessModeEnum.Always;

        // 暂停时停止,且子节点也停止
        // ProcessMode = ProcessModeEnum.Inherit;
    }

    public override void _Process(double delta)
    {
        // 当 SceneTree.Paused = true 且 ProcessMode = Pausable 时
        // 此方法不会被调用
        UpdateGameLogic(delta);
    }

    private void UpdateGameLogic(double delta)
    {
        // 游戏逻辑
    }
}

UI 在暂停时继续工作

csharp
using Godot;
using GFramework.Godot.Extensions;

public partial class PauseMenuUI : Control
{
    public override void _Ready()
    {
        // UI 在游戏暂停时仍然可以交互
        ProcessMode = ProcessModeEnum.Always;

        GetNode<Button>("ResumeButton").Pressed += OnResumePressed;
        GetNode<Button>("QuitButton").Pressed += OnQuitPressed;
    }

    private void OnResumePressed()
    {
        var pauseManager = this.GetUtility<IPauseStackManager>();

        // 获取所有暂停原因
        var reasons = pauseManager.GetPauseReasons();
        GD.Print($"当前暂停原因: {string.Join(", ", reasons)}");

        // 清空所有暂停
        pauseManager.ClearAll();

        Hide();
    }

    private void OnQuitPressed()
    {
        GetTree().Quit();
    }
}

监听暂停状态变化

csharp
using Godot;
using GFramework.Godot.Extensions;

public partial class PauseIndicator : Label
{
    public override void _Ready()
    {
        var pauseManager = this.GetUtility<IPauseStackManager>();

        // 订阅暂停状态变化事件
        pauseManager.OnPauseStateChanged += OnPauseStateChanged;
    }

    public override void _ExitTree()
    {
        var pauseManager = this.GetUtility<IPauseStackManager>();
        pauseManager.OnPauseStateChanged -= OnPauseStateChanged;
    }

    private void OnPauseStateChanged(object? sender, PauseStateChangedEventArgs e)
    {
        if (e.Group == PauseGroup.Global)
        {
            Text = e.IsPaused ? "游戏已暂停" : "游戏运行中";
            Visible = e.IsPaused;
        }
    }
}

调试暂停状态

csharp
using Godot;
using GFramework.Godot.Extensions;

public partial class PauseDebugger : Node
{
    public override void _Ready()
    {
        var pauseManager = this.GetUtility<IPauseStackManager>();
        pauseManager.OnPauseStateChanged += OnPauseStateChanged;
    }

    private void OnPauseStateChanged(object? sender, PauseStateChangedEventArgs e)
    {
        var pauseManager = this.GetUtility<IPauseStackManager>();

        GD.Print($"=== 暂停状态变化 ===");
        GD.Print($"组: {e.Group}");
        GD.Print($"状态: {(e.IsPaused ? "暂停" : "恢复")}");
        GD.Print($"深度: {pauseManager.GetPauseDepth(e.Group)}");

        var reasons = pauseManager.GetPauseReasons(e.Group);
        if (reasons.Count > 0)
        {
            GD.Print($"原因:");
            foreach (var reason in reasons)
            {
                GD.Print($"  - {reason}");
            }
        }
    }

    public override void _Input(InputEvent @event)
    {
        // 按 F12 显示所有暂停状态
        if (@event is InputEventKey keyEvent && keyEvent.Pressed && keyEvent.Keycode == Key.F12)
        {
            PrintAllPauseStates();
        }
    }

    private void PrintAllPauseStates()
    {
        var pauseManager = this.GetUtility<IPauseStackManager>();

        GD.Print("=== 所有暂停状态 ===");

        foreach (PauseGroup group in Enum.GetValues(typeof(PauseGroup)))
        {
            var isPaused = pauseManager.IsPaused(group);
            var depth = pauseManager.GetPauseDepth(group);

            if (depth > 0)
            {
                GD.Print($"{group}: 暂停 (深度: {depth})");
                var reasons = pauseManager.GetPauseReasons(group);
                foreach (var reason in reasons)
                {
                    GD.Print($"  - {reason}");
                }
            }
        }
    }
}

最佳实践

  1. 使用暂停作用域管理生命周期:避免忘记恢复

    csharp
    using (pauseManager.PauseScope("Dialog")) { ... }
    var token = pauseManager.Push("Dialog"); // 可能忘记 Pop
  2. 为暂停提供清晰的原因:便于调试

    csharp
    ✓ pauseManager.Push("Inventory opened");
    ✗ pauseManager.Push("pause"); // 原因不明确
  3. 使用正确的暂停组:避免影响不该暂停的系统

    csharp
    ✓ pauseManager.Push("Menu", PauseGroup.Gameplay); // 只暂停游戏逻辑
    ✗ pauseManager.Push("Menu", PauseGroup.Global); // 暂停所有系统包括 UI
  4. UI 节点设置 ProcessMode.Always:确保 UI 在暂停时可用

    csharp
    public override void _Ready()
    {
        ProcessMode = ProcessModeEnum.Always;
    }
  5. 游戏逻辑节点设置 ProcessMode.Pausable:确保暂停时停止

    csharp
    public override void _Ready()
    {
        ProcessMode = ProcessModeEnum.Pausable;
    }
  6. 保存暂停令牌以便恢复:确保能正确恢复暂停

    csharp
    private PauseToken _pauseToken;
    
    public void Pause()
    {
        _pauseToken = pauseManager.Push("Paused");
    }
    
    public void Resume()
    {
        pauseManager.Pop(_pauseToken);
    }
  7. 使用事件监听暂停状态:实现响应式 UI

    csharp
    pauseManager.OnPauseStateChanged += (_, e) =>
    {
        UpdateUI(e.IsPaused);
    };
  8. 清理时注销事件监听:避免内存泄漏

    csharp
    public override void _ExitTree()
    {
        pauseManager.OnPauseStateChanged -= OnPauseStateChanged;
    }

常见问题

问题:如何暂停游戏但保持 UI 可交互?

解答: 使用 PauseGroup.Gameplay 而不是 PauseGroup.Global

csharp
// 只暂停游戏逻辑
pauseManager.Push("Menu opened", PauseGroup.Gameplay);

// UI 节点设置为 Always
public override void _Ready()
{
    ProcessMode = ProcessModeEnum.Always;
}

问题:嵌套暂停如何工作?

解答: 暂停栈支持嵌套,需要所有 Pop 才能完全恢复:

csharp
var token1 = pauseManager.Push("First");  // 深度: 1, 暂停
var token2 = pauseManager.Push("Second"); // 深度: 2, 仍然暂停

pauseManager.Pop(token1); // 深度: 1, 仍然暂停
pauseManager.Pop(token2); // 深度: 0, 恢复

问题:如何实现自定义暂停行为?

解答: 实现 IPauseHandler 接口并注册:

csharp
public class CustomPauseHandler : IPauseHandler
{
    public int Priority => 0;

    public void OnPauseStateChanged(PauseGroup group, bool isPaused)
    {
        // 自定义暂停逻辑
    }
}

pauseManager.RegisterHandler(new CustomPauseHandler());

问题:暂停处理器的优先级如何工作?

解答: 数值越小优先级越高,按优先级顺序调用:

csharp
handler1.Priority = 0;  // 最先调用
handler2.Priority = 10; // 其次调用
handler3.Priority = 20; // 最后调用

问题:如何清空所有暂停?

解答: 使用 ClearAll()ClearGroup()

csharp
// 清空所有组的暂停
pauseManager.ClearAll();

// 只清空特定组
pauseManager.ClearGroup(PauseGroup.Gameplay);

问题:暂停系统是线程安全的吗?

解答: 是的,PauseStackManager 使用 ReaderWriterLockSlim 确保线程安全:

csharp
// 可以在多个线程中安全调用
Task.Run(() => pauseManager.Push("Thread 1"));
Task.Run(() => pauseManager.Push("Thread 2"));

问题:如何调试暂停问题?

解答: 使用暂停状态查询方法:

csharp
// 检查是否暂停
bool isPaused = pauseManager.IsPaused(PauseGroup.Global);

// 获取暂停深度
int depth = pauseManager.GetPauseDepth(PauseGroup.Global);

// 获取所有暂停原因
var reasons = pauseManager.GetPauseReasons(PauseGroup.Global);
foreach (var reason in reasons)
{
    GD.Print($"暂停原因: {reason}");
}

相关文档

基于 Apache 2.0 许可证发布