You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在C# WinForms中获取玩家下一步走法且不阻塞UI

国际象棋Windows Forms程序中HumanPlayer.GetMove()方法的实现问题

我正在用C# Windows Forms编写一款国际象棋程序,需要在自定义的HumanPlayer类中实现GetMove()方法——该方法要通过玩家点击棋盘UI上的两个不同格子,返回一个ChessMove对象。希望能得到实现建议,若我有概念误解也请指出。

尝试的同步代码实现

class HumanPlayer : Player
{
    private Coords _selected;
    public HumanPlayer(PieceColour colour) : base(colour) 
    {
        _selected = new Coords();
    }

    public override ChessMove GetMove(Board board) 
    {
        Coords Start = new Coords();
        board.RaiseSquareClicked += ReceiveStartSquareClickInfo;
        // 想要等待事件触发后再继续执行
        Start = _selected;
        board.RaiseSquareClicked -= ReceiveStartSquareClickInfo;

        Coords End = new Coords();
        board.RaiseSquareClicked += ReceiveEndSquareClickInfo;
        // 想要等待事件触发后再继续执行
        End =  _selected;
        board.RaiseSquareClicked -= ReceiveEndSquareClickInfo;
        return new ChessMove(Start, End);
    }

    public void ReceiveStartSquareClickInfo(object sender, SquareClickedEventArgs e)
    {
        _selected = e.Square.Coords;
    }

    public void ReceiveEndSquareClickInfo(object sender, SquareClickedEventArgs e)
    {
        _selected = e.Square.Coords;
    }
}

这段代码的核心问题:注册事件后立刻读取_selected,此时玩家尚未点击,_selected还是初始值,完全达不到等待点击的效果。

尝试异步实现时遇到的问题

我试过用AutoResetEvent.WaitOne()和Set(),但直接导致UI停止响应——因为它阻塞了UI线程,事件根本无法触发。

之后尝试用async/await,但越弄越乱,代码无法正常工作:

public async void Play()
{
    _currentPlayer = _players[0];
    _currentTurn = 1;
    while (!GameOver())
    {
        ChessMove move= await _currentPlayer.GetMove(_board);
        if (_currentPlayer == _players[1])
        {
            _currentTurn += 1;
            _currentPlayer = _players[0];
        }
        else
        {
            _currentPlayer = _players[1];
        }
    }
}

class HumanPlayer : Player
{
    private Coords _selected;
    private TaskCompletionSource<bool> _squareClicked;
    public HumanPlayer(PieceColour colour) : base(colour) 
    {
        _selected = new Coords();
    }

    public override async Task<ChessMove> GetMove(Board board) 
    {
        Coords Start = new Coords();
        _squareClicked = new TaskCompletionSource<bool>();
        board.RaiseSquareClicked += ReceiveStartSquareClickInfo;
        _squareClicked.Task.Wait();
        Start = _selected;
        board.RaiseSquareClicked -= ReceiveStartSquareClickInfo;

        Coords End = new Coords();
        _squareClicked = new TaskCompletionSource<bool>();
        board.RaiseSquareClicked += ReceiveEndSquareClickInfo;
        _squareClicked.Task.Wait();
        End =  _selected;
        board.RaiseSquareClicked -= ReceiveEndSquareClickInfo;
        return new ChessMove(Start, End);
    }
    public async void ReceiveStartSquareClickInfo(object sender, SquareClickedEventArgs e)
    {
        _squareClicked.SetResult(true);
        _selected = e.Square.Coords;
    }

    public async void ReceiveEndSquareClickInfo(object sender, SquareClickedEventArgs e)
    {
        _squareClicked.SetResult(true);
        _selected = e.Square.Coords;
    }
}

这段代码的核心错误:

  1. 用_squareClicked.Task.Wait()阻塞了UI线程,和AutoResetEvent的问题一样,UI卡死,事件无法触发。
  2. 用TaskCompletionSource<bool>需要额外维护_selected字段,冗余且容易出错。
  3. 事件处理方法用async void完全没必要,还可能导致未捕获异常。

正确实现方案及概念讲解

核心概念:async/await与UI线程

Windows Forms的UI操作必须在UI线程执行,await的作用是:当等待的任务未完成时,释放UI线程让它处理用户交互(比如点击),任务完成后再回到原线程继续执行后续代码。绝对不能用Task.Wait()或Task.Result阻塞UI线程,否则会导致UI卡死。

修正后的代码实现

我们可以用TaskCompletionSource<Coords>直接捕获点击的坐标,不需要额外的字段:

// 修正后的Play方法(async void只适合UI事件处理,这里改成async Task更规范)
public async Task Play()
{
    _currentPlayer = _players[0];
    _currentTurn = 1;
    while (!GameOver())
    {
        ChessMove move = await _currentPlayer.GetMove(_board);
        // 执行移动逻辑...
        // 切换玩家
        _currentPlayer = _currentPlayer == _players[0] ? _players[1] : _players[0];
        if (_currentPlayer == _players[0])
            _currentTurn += 1;
    }
}

class HumanPlayer : Player
{
    public HumanPlayer(PieceColour colour) : base(colour) { }

    public override async Task<ChessMove> GetMove(Board board)
    {
        // 等待第一个点击(起始格子)
        var startCoords = await WaitForSquareClick(board);
        // 等待第二个点击(目标格子)
        var endCoords = await WaitForSquareClick(board);
        
        return new ChessMove(startCoords, endCoords);
    }

    // 封装等待单个格子点击的逻辑
    private Task<Coords> WaitForSquareClick(Board board)
    {
        var tcs = new TaskCompletionSource<Coords>();
        // 定义事件处理方法
        void Handler(object sender, SquareClickedEventArgs e)
        {
            // 触发后立即注销事件,避免重复触发
            board.RaiseSquareClicked -= Handler;
            // 设置任务结果,结束等待
            tcs.SetResult(e.Square.Coords);
        }
        // 注册事件
        board.RaiseSquareClicked += Handler;
        // 返回任务,等待点击
        return tcs.Task;
    }
}

代码说明

  1. 封装等待逻辑:把等待单个格子点击的逻辑抽成WaitForSquareClick方法,代码更简洁复用。
  2. TaskCompletionSource:直接将点击的坐标作为任务结果,不需要额外维护状态字段。
  3. 事件注销:在事件触发后立即注销,避免后续点击再次触发该处理方法,同时防止内存泄漏。
  4. 避免阻塞:全程用await等待任务,不会阻塞UI线程,玩家点击时UI能正常响应。

额外注意事项

  • 如果需要判断玩家点击的起始格子是否属于自己的棋子,可以在Handler方法里先校验,不符合条件就不设置TaskCompletionSource的结果,让玩家重新点击。
  • 可以给WaitForSquareClick添加超时逻辑(比如玩家长时间不点击自动结束回合),用Task.Delay和Task.WhenAny实现。

内容的提问来源于stack exchange,提问作者Shinglington

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 10:37:00