Skip to content

交互式事件:同步等待 UI 输入

选择型事件(SelectSkillEventSelectSkillTargetsEvent 等)在同步方法中被调用,但 UI 输入是异步的。FunGame 使用两个关键模式来解决这个问题。


SyncAwaiter:同步等待异步 Task

SyncAwaiter 使用 ManualResetEventSlim 在同步上下文中安全等待 Task 完成,避免死锁。

csharp
public static class SyncAwaiter
{
    /// <summary>
    /// 在同步方法中安全等待一个 Task 完成并获取结果
    /// </summary>
    public static T WaitResult<T>(Task<T> task)
    {
        if (task.IsCompleted) return task.Result;

        using ManualResetEventSlim mres = new(false);
        task.ContinueWith(_ => mres.Set(), TaskScheduler.Default);
        mres.Wait();      // 阻塞当前线程直到 task 完成
        return task.Result; // 安全访问(不会死锁)
    }

    /// <summary>
    /// 无返回值版本
    /// </summary>
    public static void Wait(Task task)
    {
        if (task.IsCompleted) return;
        using ManualResetEventSlim mres = new(false);
        task.ContinueWith(_ => mres.Set(), TaskScheduler.Default);
        mres.Wait();
    }
}

为什么不用 .Result 直接访问?

在 WPF/WinForms 的同步上下文中,直接访问 task.Result 会导致死锁——因为 await 需要回到 UI 线程,但 UI 线程被 .Result 阻塞了。ManualResetEventSlim 在线程池上等待,不会占用 UI 线程。


UserInputRequester<T>:通用的异步输入请求器

csharp
/// <summary>
/// 管理异步用户输入请求的辅助类
/// </summary>
public class UserInputRequester<T>
{
    private TaskCompletionSource<T?>? _tcs;

    /// <summary>
    /// 请求用户输入,阻塞直到 UI 返回
    /// </summary>
    public async Task<T?> RequestInput(Action<Action<T?>> uiPromptAction)
    {
        _tcs = new TaskCompletionSource<T?>();
        // 将 ResolveInput 作为回调传给 UI
        uiPromptAction(ResolveInput);
        return await _tcs.Task;
    }

    /// <summary>
    /// UI 调用此方法返回结果
    /// </summary>
    public void ResolveInput(T? result)
    {
        _tcs?.TrySetResult(result);
        _tcs = null;
    }
}

工作流程

事件处理器(同步)                        UI 线程(异步)
     │                                       │
     ├─ RequestInput(uiAction) ──────────→   │
     │  (创建 TaskCompletionSource)          │
     │  (调用 uiAction,传入 ResolveInput)    │
     │                                       ├─ 显示选择界面
     │  [阻塞在 WaitResult()]                 │
     │                                       ├─ 用户点击确定
     │                                       ├─ 调用 ResolveInput(result)
     │  ←─── Task 完成 ────────────────────  │
     │                                       ├─ 隐藏选择界面
     ├─ 返回 result                           │

完整示例:选择技能目标

事件处理器(同步上下文)

csharp
private List<Character> Queue_SelectSkillTargets(
    GamingQueue queue, Character caster, Skill skill,
    List<Character> allEnemys, List<Character> allTeammates,
    List<Character> enemys, List<Character> teammates,
    List<Grid> castRange)
{
    // 事件处理器是同步的,但 UI 是异步的
    // 使用 SyncAwaiter.WaitResult 桥接
    List<Character>? selectedTargets = SyncAwaiter.WaitResult(
        Controller.RequestTargetSelection(
            caster, skill, allEnemys, allTeammates,
            enemys, teammates, castRange));

    return selectedTargets ?? [];
}

Controller:启动 UI 请求

csharp
// 创建请求器实例
private readonly UserInputRequester<List<Character>> _targetSelectionRequester = new();

public async Task<List<Character>> RequestTargetSelection(
    Character character, ISkill skill,
    List<Character> allEnemys, List<Character> allTeammates,
    List<Character> enemys, List<Character> teammates,
    List<Grid> range)
{
    List<Character> selectable = skill.GetSelectableTargets(
        character, allEnemys, allTeammates, enemys, teammates);

    List<Character> targets = await _targetSelectionRequester.RequestInput(
        async (callback) => await UI.InvokeAsync(() =>
            UI.ShowTargetSelectionUI(character, skill, selectable,
                enemys, teammates, range, callback))
    ) ?? [];

    // 只返回可选范围内的目标(安全过滤)
    return targets.Where(selectable.Contains).ToList();
}

Controller:接收 UI 结果

csharp
public async Task ResolveTargetSelection(List<Character> targetIds)
{
    _targetSelectionRequester.ResolveInput(targetIds);
    await UI.InvokeAsync(() => UI.HideTargetSelectionUI());
}

UI:显示选择界面并回调

csharp
public void ShowTargetSelectionUI(Character character, ISkill skill,
    List<Character> selectable, List<Character> enemys,
    List<Character> teammates, List<Grid> range,
    Action<List<Character>> callback)
{
    _resolveTargetSelection = callback;  // 保存回调

    // 高亮可选目标
    foreach (Character c in selectable)
        HighlightCharacter(c, isSelectable: true);

    // 显示确认/取消按钮
    ConfirmButton.Visibility = Visibility.Visible;
    CancelButton.Visibility = Visibility.Visible;
}

// 用户点击确认
private void ConfirmButton_Click(object sender, RoutedEventArgs e)
{
    _resolveTargetSelection?.Invoke(SelectedTargets.ToList());
}

完整数据流示意

GamingQueue.ProcessTurn()
  │  [同步调用链]
  ├─ OnSelectSkillTargetsEvent()
  │    └─ Queue_SelectSkillTargets()          ← 同步事件处理器
  │         └─ SyncAwaiter.WaitResult(task)    ← 阻塞等待
  │              │
  │              └─ Controller.RequestTargetSelection()  ← async Task
  │                   └─ UserInputRequester.RequestInput()
  │                        └─ await tcs.Task   ← 在 UI 线程 await
  │                             │
  │                             └─ UI.ShowTargetSelectionUI(callback)
  │                                  │
  │                           [用户操作 UI...]
  │                                  │
  │                             UI 调用 callback(targets)
  │                                  │
  │                             ←── tcs.TrySetResult()
  │                        ←── Task 完成
  │              ←── mres.Set()
  │         ←── 返回 targets
  │    ←── 返回 targets
  │  ProcessTurn 继续执行技能

基于 LGPL-3.0 许可证发布