Skip to content

WPF Demo 实战:完整的游戏循环

FunGame.Testing/Desktop/GameMapTesting 是一个 WPF 实现的完整回合制演示,展示了 GamingQueue 所有事件的 UI 绑定模式。

项目结构

文件职责
GameMapTesting.cs游戏逻辑核心:创建队列、绑定事件、游戏循环
GameMapController.cs桥接层:将事件请求转发到 UI,管理 UserInputRequester
GameMapViewer.xaml.csUI 层:WPF 地图渲染、选择界面、日志输出
SyncAwaiter.cs同步等待异步 Task 的工具类
TestMap.cs测试用地图
ViewModels.csWPF 数据绑定模型
Converters.csWPF 值转换器

事件绑定总览(WPF Demo 的生产级写法)

csharp
// === 回合生命周期 ===
queue.TurnStartEvent += Queue_TurnStart;
queue.DecideActionEvent += Queue_DecideAction;
queue.TurnEndEvent += Queue_TurnEnd;

// === 交互式选择(玩家控制时触发)===
queue.SelectSkillEvent += Queue_SelectSkill;
queue.SelectItemEvent += Queue_SelectItem;
queue.SelectNormalAttackTargetsEvent += Queue_SelectNormalAttackTargets;
queue.SelectSkillTargetsEvent += Queue_SelectSkillTargets;
queue.SelectNonDirectionalSkillTargetsEvent += Queue_SelectNonDirectionalSkillTargets;
queue.SelectTargetGridEvent += Queue_SelectTargetGrid;
queue.CharacterInquiryEvent += Queue_CharacterInquiry;

// === 状态同步 ===
queue.QueueUpdatedEvent += Queue_QueueUpdated;
queue.CharacterActionTakenEvent += Queue_CharacterActionTaken;
queue.CharacterMoveEvent += Queue_CharacterMove;

每个事件处理器的核心模式相同:

事件触发 → 判断 AI/玩家 → SyncAwaiter.WaitResult(Controller.RequestXxx()) → UI 交互 → 返回结果

各事件处理详解

TurnStart — 回合开始

csharp
private bool Queue_TurnStart(GamingQueue queue, Character character,
    DecisionPoints dp, List<Character> enemys, List<Character> teammates,
    List<Skill> skills, List<Item> items)
{
    // 更新 UI 底部信息面板(决策点、技能列表、物品列表)
    SyncAwaiter.Wait(Controller.UpdateBottomInfoPanel(dp));
    return true;  // true = 继续,false = 取消回合
}

DecideAction — 选择行动类型

csharp
private CharacterActionType Queue_DecideAction(GamingQueue queue,
    Character character, DecisionPoints dp, ...)
{
    if (!IsPlayer_OnlyTest(queue, character))
        return CharacterActionType.None;  // AI 不进入此分支

    // 请求 UI 显示行动按钮
    return SyncAwaiter.WaitResult(
        Controller.RequestActionType(character, items));
}

SelectSkill — 选择技能

csharp
private Skill? Queue_SelectSkill(GamingQueue queue, Character character,
    List<Skill> skills)
{
    if (!IsPlayer_OnlyTest(queue, character)) return null;

    Skill? skill = SyncAwaiter.WaitResult(
        Controller.RequestSkillSelection(character, skills));
    SyncAwaiter.Wait(Controller.ResolveSkillSelection(skill));
    return availableSkills.Any(s => s == skill) ? skill : null;
}

SelectNormalAttackTargets — 选择普攻目标

csharp
private List<Character> Queue_SelectNormalAttackTargets(
    GamingQueue queue, Character character, NormalAttack attack,
    List<Character> allEnemys, List<Character> allTeammates,
    List<Character> enemys, List<Character> teammates,
    List<Grid> attackRange)
{
    if (!IsPlayer_OnlyTest(queue, character)) return [];

    List<Character> targets = SyncAwaiter.WaitResult(
        Controller.RequestTargetSelection(
            character, attack, allEnemys, allTeammates,
            enemys, teammates, attackRange));
    SyncAwaiter.Wait(Controller.ResolveTargetSelection(targets));
    return targets ?? [];
}

SelectSkillTargets — 选择指向性技能目标

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)
{
    if (!IsPlayer_OnlyTest(queue, caster)) return [];

    List<Character> targets = SyncAwaiter.WaitResult(
        Controller.RequestTargetSelection(
            caster, skill, allEnemys, allTeammates,
            enemys, teammates, castRange));
    SyncAwaiter.Wait(Controller.ResolveTargetSelection(targets));
    return targets ?? [];
}

SelectNonDirectionalSkillTargets — 选择非指向性目标(格子)

csharp
private List<Grid> Queue_SelectNonDirectionalSkillTargets(
    GamingQueue queue, Character caster, Skill skill,
    List<Character> enemys, List<Character> teammates,
    List<Grid> castRange)
{
    if (!IsPlayer_OnlyTest(queue, caster)) return [];

    if (queue.Map == null) return [];

    Grid current = queue.CustomData["currentGrid"] as Grid ?? Grid.Empty;
    if (current == Grid.Empty) return [];

    List<Grid> targets = SyncAwaiter.WaitResult(
        Controller.RequestTargetGridsSelection(
            caster, skill, enemys, teammates, current, castRange));
    SyncAwaiter.Wait(Controller.ResolveTargetGridsSelection(targets));
    return targets ?? [];
}

SelectTargetGrid — 选择移动目标

csharp
private Grid Queue_SelectTargetGrid(GamingQueue queue,
    Character character, List<Character> enemys,
    List<Character> teammates, GameMap map, List<Grid> moveRange)
{
    if (!IsPlayer_OnlyTest(queue, character)) return Grid.Empty;

    Grid current = queue.CustomData["currentGrid"] as Grid ?? Grid.Empty;
    if (current == Grid.Empty) return current;

    Grid? target = SyncAwaiter.WaitResult(
        Controller.RequestTargetGridSelection(
            character, current, map.GetGridsByRange(current, character.MOV)));
    SyncAwaiter.Wait(Controller.ResolveTargetGridSelection(target));
    return target ?? Grid.Empty;
}

CharacterInquiry — 询问玩家

csharp
private InquiryResponse Queue_CharacterInquiry(
    GamingQueue queue, Character character,
    DecisionPoints dp, InquiryOptions options)
{
    if (!IsPlayer_OnlyTest(queue, character))
        return new(options);  // AI 默认响应

    return SyncAwaiter.WaitResult(
        Controller.RequestInquiryResponseSelection(options));
}

QueueUpdated — 顺序表更新

csharp
private void Queue_QueueUpdated(GamingQueue queue,
    List<Character> characters, Character character,
    DecisionPoints dp, double hardnessTime,
    QueueUpdatedReason reason, string msg)
{
    // 只在非行动原因时更新(避免频繁刷新)
    if (reason != QueueUpdatedReason.Action)
    {
        SyncAwaiter.Wait(Controller.UpdateQueue(dp));
    }
}

CharacterActionTaken / CharacterMove — 地图同步

csharp
private void Queue_CharacterActionTaken(GamingQueue queue,
    Character actor, DecisionPoints dp,
    CharacterActionType type, RoundRecord record)
{
    SyncAwaiter.Wait(Controller.UpdateCharacterPositionsOnMap());
}

private void Queue_CharacterMove(GamingQueue queue,
    Character actor, DecisionPoints dp, Grid grid)
{
    SyncAwaiter.Wait(Controller.UpdateCharacterPositionsOnMap());
}

Controller 层的 UserInputRequester 实例

csharp
public class GameMapController
{
    // 每种输入类型一个独立的请求器
    private readonly UserInputRequester<Character> _characterSelectionRequester = new();
    private readonly UserInputRequester<InquiryResponse> _inquiryResponseRequester = new();
    private readonly UserInputRequester<Grid> _targetGridSelectionRequester = new();
    private readonly UserInputRequester<CharacterActionType> _actionTypeRequester = new();
    private readonly UserInputRequester<List<Character>> _targetSelectionRequester = new();
    private readonly UserInputRequester<List<Grid>> _targetGridsSelectionRequester = new();
    private readonly UserInputRequester<Skill> _skillSelectionRequester = new();
    private readonly UserInputRequester<Item> _itemSelectionRequester = new();
    private readonly UserInputRequester<bool> _continuePromptRequester = new();
}

模式总结

模式说明
单向通知TurnEndEventDamageToEnemyEvent 等 → SyncAwaiter.Wait(Controller.DoSomething())
交互选择SelectSkillEvent 等 → SyncAwaiter.WaitResult(Controller.RequestXxx()) 阻塞等待用户输入
AI 分流每个选择型事件开头检查 IsPlayer_OnlyTest() → AI 控制时直接跳过 UI
双 Resolve事件处理器调 Request + Resolve(请求 UI 弹出 + 清理 UI 状态)
CustomData 传参queue.CustomData["currentGrid"] 在事件间传递状态

基于 LGPL-3.0 许可证发布