如何在C#中使用鼠标实现异步控制?

3
我对C#和异步编程都比较陌生。我正在从头开始编写一个简单的2D游戏(并在此过程中编写自己的游戏引擎),使用Windows Forms,并似乎在异步编程方面遇到了问题。尽管这段代码是为游戏使用而设计的,但我希望将异步处理用于工作目的,这也是我在业余时间编程的原因之一。最初我的移动是同步的,并且工作得非常完美,但我希望能够在旅行中更改移动目的地,无法同步执行此操作。我一直在尝试使用此处的各种帖子和官方Microsoft文档将同步方法转换为异步方法,但迄今为止还没有成功使其按预期工作。我已经尽可能地减少了代码并清理了代码,而不会掩盖问题。以下是我期望代码执行的详细信息以及问题的摘要。
代码的预期结果是什么?
该代码的唯一当前目的是接受鼠标在窗口上的右键单击,并将“单位”从其当前位置移动到单击的位置,同时仍然允许您在窗口中单击以根据需要更改位置(类似于RTS风格的移动)。
我遇到的问题是什么?
对于这段代码,我只有猜测其根本原因的想法,但症状是,在执行移动方法仅几次后,程序会变慢,最终冻结。我通过调试运行它,并注意到RAM使用量随着每次使用而缓慢增加,并且每次使用都会显着增加CPU使用率,在仅仅几次点击后就达到100%。移动本身很好用,但系统变得非常缓慢,使其无法使用。
我为解决问题做了什么?
在三次尝试将同步代码转换为异步代码期间,我已经重写了大部分移动代码,以尝试使其正常工作。如果有人想要查看这些版本之间发生了什么变化,我有一个完整的项目历史记录。我从使用背景工作者开始,导致跨线程异常,然后实施了当前任务的原始版本,该版本存在处理问题和取消进程无法正常工作的问题,最终使用了这段代码。我认为不断增加的CPU和RAM使用量是由于任务未正确处理而导致的,并添加了处理命令,但似乎并没有太大帮助。我尝试使尽可能多的内容静态化,以便它不能复制自己,但这没有任何效果。我一直在专注于解决这个问题,已经几周了,但仍未能找到解决我的问题的答案。
代码
以下是代码的快速摘要列表,包括已删除的代码。
  1. 创建“游戏场”(一个空白的白色表单,占据整个窗口)和单位(32px彩色正方形picturebox)。
  2. 点击“好人”后将其设置为选定状态。
  3. 在“游戏场”上点击后,使用异步任务将所选单位移动到点击位置。
  4. 任务完成后处理不需要的资源。

调用鼠标单击事件和接收方法

// Mouse click event for moving the "Good Guy" to the clicked location on the play field.

playField.MouseClick += (sender1, e1) => PlayFieldOnMouseClickAsync(e1, goodGuy, loopRunning);


// The receiving method for the mouse click event handler.
private static async void PlayFieldOnMouseClickAsync(MouseEventArgs e1
                                                   , Control playerUnit
                                                   , bool loopRunning) {
    switch (e1.Button) {    
// Right click only moves selected units.
        case MouseButtons.Right:
            if (!UnitSelected) return;

// The variables needed for moving units.
            var MoveDistance = new Point(MousePosition.X - playerUnit.Location.X, MousePosition.Y - playerUnit.Location.Y);
            var speedX            = 1;
            var speedY            = 1;
            var moveNegativeXFlag = false;
            var moveNegativeYFlag = false;
            var movementLoopTokenSource = new CancellationTokenSource();

// These two checks determine if the unit needs to be moved positively or negatively along the play field before converting the distance to its absolute value for easy math manipulation later.
            if (moveDistance.X < 0) {
                speedX            = -1;
                moveNegativeXFlag = true;
            }

            if (moveDistance.Y < 0) {
                speedY            = -1;
                moveNegativeYFlag = true;
            }

            moveDistance.X = Math.Abs(moveDistance.X);
            moveDistance.Y = Math.Abs(moveDistance.Y);

// If the loop is already running then this cancels the loop before starting a new loop.
            if (loopRunning) {
                movementLoopTokenSource.Cancel();
            }

// This toggles the state of running loop flag but may not work on subsequent runs if the loop doesn't finish.
            loopRunning = !loopRunning;
            var loopCancellationToken = movementLoopTokenSource.Token;

// Prepares the movement task to be called.
            var movementLoop = MovementLoopTask(loopCancellationToken
                                              , speedX   
                                              , speedY
                                              , moveNegativeXFlag
                                              , moveNegativeYFlag
                                              , playerUnit
                                              , moveDistance);

// Starts the movement loop and upon finish sets the flag back and disposes unneeded items.
            await movementLoop;
            loopRunning = false;
            movementLoop.Dispose();
            movementLoopTokenSource.Dispose();
            break;
    }
}

执行动作任务

// After being called this asynchronously performs movement for the selected unit.
public static async Task MovementLoopTask(
                                          CancellationToken loopCancellationToken
                                        , int               speedX
                                        , int               speedY
                                        , bool              moveNegativeXFlag
                                        , bool              moveNegativeYFlag
                                        , Control           playerUnit
                                        , Point             moveDistance) {
// An in-line call for the creating a loop task.
    await Task.Run(async () => {
        while (true) {
            await Task.Delay(10);
            loopCancellationToken.ThrowIfCancellationRequested();

// This is the actual loop that performs the moving by running both switchs until no more movement is needed.
            if (speedX != 0 || speedY != 0) {
                playerUnit.BeginInvoke(new Action(() => playerUnit.Location = new Point(playerUnit.Location.X + speedX 
                                                                                 , playerUnit.Location.Y + speedY)));

// This switch checks the X distance and stops X movement upon reaching the desired location by setting the movement speed to 0.
                switch (moveNegativeXFlag) {
                    case false when moveDistance.X > 16:
                        moveDistance.X -= Math.Abs(speedX);
                        break;

                    case false:
                        speedX = 0;
                        break;

                    default:
                        if (moveDistance.X > -16)
                            moveDistance.X -= Math.Abs(speedX);
                        else
                            speedX = 0;
                            break;
                }

// This switch checks the Y distance and stops Y movement upon reaching the desired location by setting the movement speed to 0.
                switch (moveNegativeYFlag) {
                    case false when moveDistance.Y > 16:
                        moveDistance.Y -= Math.Abs(speedY);
                        break;

                    case false:
                        speedY = 0;
                        break;

                    default:
                        if (moveDistance.Y > -16)
                            moveDistance.Y -= Math.Abs(speedY);
                        else
                            speedY = 0;
                            break;
                }
            } else {
                break;
            }
       }
   }
 , loopCancellationToken);
}

编辑后的结果:

代码现在有所改善,但仍存在问题。程序不再减速并占用CPU使用率,除非您点击得非常快。RAM仍然会缓慢增加。如果您点击,它会异步工作,但无法取消先前的移动,它们只是重叠(例如,如果您在同一位置多次单击,则移动速度更快,如果您在直接相反的方向上单击一次,则它将停留在那里,直到您再次单击以便一个可以克服另一个。)


两个要点 - 1) 检查在流程过程中是否多次订阅了 PlayField 上的 MouseClick 事件?2) 使用 BeginInvoke 而不是 Invoke。同时,在 while 循环中加入 await Task.Delay(10),以验证线程如果有空闲时间会发生什么。10 是一个任意的数字,您可以根据自己的分析来设置它,如果不需要则不使用。 - user1672994
你已经验证了点1——多事件订阅吗? - user1672994
@user1672994 我做了以下更改(我将编辑问题以反映这一点),但它仍然存在问题,我将描述:
  1. Invoke 更改为 BeginInvoke
  2. while 循环的开头添加了 await Task.Delay(10);
  3. 因为第二点需要,我在 Task.Run 的 lambda 表达式之前添加了 async
- Skittleman
另外,我没有看到 while 循环的任何中断条件。这个例程会从 while 循环中出来吗? - user1672994
@user1672994,我已经验证了多个事件订阅,一切正常。你说得对,我在while循环中没有正确的break,在之前的版本中我有它,但可能在尝试取消任务的一些示例代码时丢失了它。编辑:在按照编辑后的代码添加break后,一切仍然保持不变。 - Skittleman
1个回答

0
感谢@user1672994的帮助,我找到了一切故障的原因。我需要对代码进行以下更改才能使其正常工作:
  1. 在主方法中,我将loopRunning变量初始化为false,但由于某些原因,每次鼠标单击事件都会将其重置为false。所以为了纠正这个问题,我只是将它变成了属性。
  2. 在循环开始时,我将playerUnit.Invoke更改为playerUnit.BeginInvoke
  3. 我添加了一个带有breakelse语句,用于while循环。
  4. 我不得不更改while循环条件,因为当抛出异常时它不会取消,所以现在我只使用loopRunning变量作为条件。
  5. 最后,虽然我仍在完成这个过程中,如果调用取消操作,需要使用新的CancellationTokenSource()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接