在我的游戏中,替代Application.DoEvents要使用什么?

4
我正在使用C# WinForms创建一个太空侵略者游戏,编写玩家大炮的移动代码时,我创建了以下事件处理程序:
private void Game_Screen_KeyDown(object sender, KeyEventArgs e)
{
    for (int i = 0; i < 500; i++)
    {

        if (e.KeyCode == Keys.Left)
        {
            cannonBox.Location = new Point(cannonBox.Left - 2, cannonBox.Top); //Changes location of cannonBox to a new location to the left
            Application.DoEvents();
            System.Threading.Thread.Sleep(10); //Delays the movement by couple milliseconds to stop instant movement
        }

        if (e.KeyCode == Keys.Right)
        {
            cannonBox.Location = new Point(cannonBox.Left + 2, cannonBox.Top); //Changes location of cannonBox to a new location to the right
            Application.DoEvents();
            System.Threading.Thread.Sleep(10); //Delays the movement by couple milliseconds to stop instant movement
        }

        if (e.KeyCode == Keys.Up)
        {
            createLaser(); //Calls the method whenever Up arrow key is pressed
        }
    }
}

但是在阅读关于C#中此方法不可靠的不同网站后,我将确保从此不再使用它。在此情况下,除了Application.DoEvents之外,还有哪些替代方法?


4
游戏循环是应用Application.DoEvents()并不是根本错误的少数几个例子之一,但它应该放在循环中,而不是事件处理程序中。请谷歌“windows forms game loop”以查找示例代码。 - Hans Passant
请点击此处:https://gamedev.stackexchange.com/q/67651。 - Sinatr
3个回答

6
我建议将该事件处理程序改为async,并使用await Task.Delay()代替Thread.Sleep():
private async void Game_Screen_KeyDown(object sender, KeyEventArgs e)
{
    for (int i = 0; i < 500; i++)
    {
        if (e.KeyCode == Keys.Left)
        {
            cannonBox.Location = new Point(cannonBox.Left - 2, cannonBox.Top); //Changes location of cannonBox to a new location to the left
            await Task.Delay(10); //Delays the movement by couple milliseconds to stop instant movement
        }

        if (e.KeyCode == Keys.Right)
        {
            cannonBox.Location = new Point(cannonBox.Left + 2, cannonBox.Top); //Changes location of cannonBox to a new location to the right
            await Task.Delay(10); //Delays the movement by couple milliseconds to stop instant movement
        }

        if (e.KeyCode == Keys.Up)
        {
            createLaser(); //Calls the method whenever Up arrow key is pressed
        }
    }
}

这样,控制流会返回给调用者,你的UI线程有时间处理其他事件(因此不需要使用Application.DoEvents())。大约在10毫秒后,控制将返回并且执行该处理程序继续进行。可能需要更多的微调,因为现在当然可以在方法未完成时按下更多按键。如何处理取决于周围环境。您可以声明一个标志来信号当前执行并拒绝进一步的方法输入(没有线程安全性需要,因为所有操作都按顺序在UI线程上进行)。或者,您可以将按键排队而不是拒绝重新进入,并在另一个事件中处理它们,例如“空闲”事件(就像评论中Lasse所建议的那样)。
请注意,事件处理程序是很少情况下可以使用async而不返回Task的。

感谢改进!现在它运行得比以前顺畅了许多,而且没有使用Application.DoEvents :) - user8721727
请注意,如果在该循环完成之前您成功按下左键(或它被注册为按下,自动重复?),那么您将实际上同时运行两个这样的循环。 500次10毫秒是5秒钟,这足以让类似这样的事情发生。 - Lasse V. Karlsen
你可以简单地记录玩家想要向左移动,然后将实际的移动代码移到其他事件中,比如“空闲”之类的事件,然后在松开按键时记录玩家不再想向左移动。 - Lasse V. Karlsen

2

使用一个计时器,每20毫秒调用一次游戏处理。

KeyDown/KeyUp事件中只需更改当前状态,该状态将被游戏处理使用。

示例代码:

[Flags]
public enum ActionState
{
    MoveLeft,
    MeveRight,
    FireLaser,
}

// stores the current state
private ActionState _actionState;

// set action state
private void Game_Screen_KeyDown(object sender, KeyEventArgs e)
{
    switch ( e.KeyCode )
    {
        case Keys.Left:
            _actionState |= ActionState.MoveLeft;
            break;
        case Keys.Right:
            _actionState |= ActionState.MoveRight;
            break;
        case Keys.Up:
            _actionState |= ActionState.FireLaser;
            break;
        default:
            break;
    }
}

// remove action state
private void Game_Screen_KeyUp(object sender, KeyEventArgs e)
{
    switch ( e.KeyCode )
    {
        case Keys.Left:
            _actionState &= ~ActionState.MoveLeft;
            break;
        case Keys.Right:
            _actionState &= ~ActionState.MoveRight;
            break;
        case Keys.Up:
            _actionState &= ~ActionState.FireLaser;
            break;
        default:
            break;
    }
}

// called from a timer every 20 milliseconds
private void Game_Screen_LoopTimer_Tick(object sender, EventArgs e)
{
    if ( _actionState.HasFlag( ActionState.MoveLeft ) && !_actionState.HasFlag( ActionState.MoveRight ) )
    {
        cannonBox.Location = new Point(cannonBox.Left - 2, cannonBox.Top); //Changes location of cannonBox to a new location to the left
    }
    if ( _actionState.HasFlag( ActionState.MoveRight ) && !_actionState.HasFlag( ActionState.MoveLeft ) )
    {
        cannonBox.Location = new Point(cannonBox.Left + 2, cannonBox.Top); //Changes location of cannonBox to a new location to the right
    }
    if ( _actionState.HasFlag( ActionState.FireLaser ) )
    {
        createLaser(); //Calls the method whenever Up arrow key is pressed
    }
}

0

Application.DoEvents() 中断了你的方法的执行,UI线程将处理其事件(包括重绘UI)。根据我的经验,在正确的位置使用它是没有问题的...

使用“异步”模式(如René Vogt建议的)是制作响应式UI的最佳实践。

然而,你必须问自己是否需要一个循环来检查500次按键是否为左、右或上。特别是因为这个循环似乎是由按键按下事件触发的...

如果你在主线程中创建一个'while(true)'循环,并从那里调用Application.DoEvents,可能会更容易一些。

或者你可以对按键按下事件做出反应,并逐个完成动作。 =>按左箭头 - 移动一个单位向左 - 再按左箭头 - 再移动一个单位向左...以此类推。

https://msdn.microsoft.com/en-us//library/system.windows.forms.application.doevents(v=vs.110).aspx


“在主函数中循环” - 这里的“main”是什么?你是指这个吗? - Sinatr
更或少是的。Main方法是C#应用程序的入口点。 https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/main-and-command-args/ - Mat

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