如何允许在 C# 中使用鼠标进行异步控制?
How to allow asynchronous control using the mouse in C#?
我对 C# 比较陌生,对异步编程来说更陌生。我正在使用 Windows Forms 从头开始开发一个简单的 2D 游戏(并且在编写我自己的游戏引擎的过程中),并且似乎在我的异步移动编程中遇到了障碍。虽然此代码旨在用于游戏,但我希望将异步处理用于工作目的,这也是我在业余时间为了娱乐而开始编写此代码的部分原因。最初我的运动是同步的并且运行良好,但我希望能够在行程中途更改运动目的地,但无法同步进行。我一直在尝试使用此处的各种帖子和 Microsoft 官方文档将同步方法转换为异步方法,但到目前为止还没有运气让它按预期工作。我试图减少代码并尽可能多地清理它而不掩盖问题。以下是我希望代码执行的操作的详细信息以及问题的摘要。
代码需要什么?
相关代码的当前唯一意图是接受鼠标右键单击 window 并将 "unit" 从其当前位置移动到单击的位置,同时仍然允许您单击 window 以根据需要更改位置(想想 RTS 风格的移动)。
我一直遇到的问题是什么?
我对这段代码问题的根本原因只有猜测,但症状是在执行移动方法几次后程序变慢,最后死机。我通过调试 运行 它,我注意到 ram 使用量随着每次使用而慢慢增加,每次使用都会显着增加 CPU 使用率,只需单击几下即可达到 100%。机芯本身运行良好,但系统出现故障使其无法使用。
我做了什么来尝试解决这个问题?
在转换为异步过程中,我重写了大部分移动代码 3 次,试图使其正常工作。如果有人想查看这些版本之间的更改,我有该项目的完整历史记录。我从使用导致跨线程异常的后台工作程序到实现当前任务的原始版本,该版本存在处理问题并且取消过程对最终这段代码不起作用。我认为增加 CPU 和 ram 使用是由于任务没有正确处理并添加了处理命令,但这似乎并没有多大帮助。我已经尝试尽可能多地制作静态的,这样它就不能自我复制,但这没有效果。几个星期以来,我一直在独自处理这个问题,但一直未能找到解决我的问题的答案。
代码
总结
下面是代码功能的快速摘要列表,包括已删除的代码。
- 创建 "play field"(占满整个 window 的空白表格)和单元(32px 彩色方形图片框)。
- 点击 "Good Guy" 将他设置为已选中。
- 单击 "play field" 后,使用异步任务将选定的单元移动到单击的位置。
- 完成任务后处理不需要的资源。
调用鼠标点击事件及接收方法
// 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 仍然有一个缓慢增加的蠕变。如果你点击,它会异步工作,但无法取消之前的移动,它们只是重叠(例如,如果你在同一个地方点击多次,它移动得更快,如果你在直接相反的方向点击一次,它会停在那里,直到你给另一个单击以便一个可以克服另一个。)
感谢@user1672994 的帮助,我发现了为什么一切都坏了。我需要对代码进行以下更改才能使其正常工作:
- 在 main 方法中,我将
loopRunning
变量初始化为 false
,出于我仍然不知道的原因,它会在每次鼠标单击事件时将其重置为 false
。所以为了纠正这个我只是把它变成了 属性.
- 在循环开始时,我将
playerUnit.Invoke
更改为 playerUnit.BeginInvoke
。
- 我为 while 循环添加了一个带有
break
的 else
语句。
- 我不得不更改
while
循环条件,因为它不会在抛出异常时取消,所以我现在只使用 loopRunning
变量作为条件。
- 最后,虽然我仍在完成此过程,但如果调用取消,则需要使用新的
CancellationTokenSource()
。
我对 C# 比较陌生,对异步编程来说更陌生。我正在使用 Windows Forms 从头开始开发一个简单的 2D 游戏(并且在编写我自己的游戏引擎的过程中),并且似乎在我的异步移动编程中遇到了障碍。虽然此代码旨在用于游戏,但我希望将异步处理用于工作目的,这也是我在业余时间为了娱乐而开始编写此代码的部分原因。最初我的运动是同步的并且运行良好,但我希望能够在行程中途更改运动目的地,但无法同步进行。我一直在尝试使用此处的各种帖子和 Microsoft 官方文档将同步方法转换为异步方法,但到目前为止还没有运气让它按预期工作。我试图减少代码并尽可能多地清理它而不掩盖问题。以下是我希望代码执行的操作的详细信息以及问题的摘要。
代码需要什么?
相关代码的当前唯一意图是接受鼠标右键单击 window 并将 "unit" 从其当前位置移动到单击的位置,同时仍然允许您单击 window 以根据需要更改位置(想想 RTS 风格的移动)。
我一直遇到的问题是什么?
我对这段代码问题的根本原因只有猜测,但症状是在执行移动方法几次后程序变慢,最后死机。我通过调试 运行 它,我注意到 ram 使用量随着每次使用而慢慢增加,每次使用都会显着增加 CPU 使用率,只需单击几下即可达到 100%。机芯本身运行良好,但系统出现故障使其无法使用。
我做了什么来尝试解决这个问题?
在转换为异步过程中,我重写了大部分移动代码 3 次,试图使其正常工作。如果有人想查看这些版本之间的更改,我有该项目的完整历史记录。我从使用导致跨线程异常的后台工作程序到实现当前任务的原始版本,该版本存在处理问题并且取消过程对最终这段代码不起作用。我认为增加 CPU 和 ram 使用是由于任务没有正确处理并添加了处理命令,但这似乎并没有多大帮助。我已经尝试尽可能多地制作静态的,这样它就不能自我复制,但这没有效果。几个星期以来,我一直在独自处理这个问题,但一直未能找到解决我的问题的答案。
代码
总结
下面是代码功能的快速摘要列表,包括已删除的代码。
- 创建 "play field"(占满整个 window 的空白表格)和单元(32px 彩色方形图片框)。
- 点击 "Good Guy" 将他设置为已选中。
- 单击 "play field" 后,使用异步任务将选定的单元移动到单击的位置。
- 完成任务后处理不需要的资源。
调用鼠标点击事件及接收方法
// 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 仍然有一个缓慢增加的蠕变。如果你点击,它会异步工作,但无法取消之前的移动,它们只是重叠(例如,如果你在同一个地方点击多次,它移动得更快,如果你在直接相反的方向点击一次,它会停在那里,直到你给另一个单击以便一个可以克服另一个。)
感谢@user1672994 的帮助,我发现了为什么一切都坏了。我需要对代码进行以下更改才能使其正常工作:
- 在 main 方法中,我将
loopRunning
变量初始化为false
,出于我仍然不知道的原因,它会在每次鼠标单击事件时将其重置为false
。所以为了纠正这个我只是把它变成了 属性. - 在循环开始时,我将
playerUnit.Invoke
更改为playerUnit.BeginInvoke
。 - 我为 while 循环添加了一个带有
break
的else
语句。 - 我不得不更改
while
循环条件,因为它不会在抛出异常时取消,所以我现在只使用loopRunning
变量作为条件。 - 最后,虽然我仍在完成此过程,但如果调用取消,则需要使用新的
CancellationTokenSource()
。