C# - 俄罗斯方块克隆版 - 方块不能正确响应箭头键组合

5

我正在使用Visual C# 2005编写俄罗斯方块游戏,这是我设计的最复杂的程序。

我创建了一个形状类和一个块类来控制不同俄罗斯方块的位置、移动和显示。我为每个形状都有moveDown()、moveLeft()和moveRight()函数(还有相应的canMoveDown()、canMoveLeft()和canMoveRight()布尔函数来验证是否可以移动)。这一切都很好地运行着。

我希望使用向下、向右和向左箭头键让用户移动块,还要使用定时器让形状自动下落一行,每隔几毫秒就下落一次。

我使用KeyDown事件处理程序来检查用户何时按下向下、向左和向右箭头键。这并不难。问题在于,我希望允许对角线运动,并且希望它尽可能平滑地工作。我尝试过多种不同的方法来解决这个问题,但成功的程度各不相同。但我还没有完全做到...

我最成功的方法是使用三个布尔变量来跟踪向下、向左和向右箭头键何时被按下。我会在KeyDown事件中将布尔值设置为true,在KeyUp事件中将其设置为false。在KeyDown事件中,我还会告诉块如何移动,使用布尔变量来检查当前被按下的组合。这个方法非常有效,除了一个问题。

如果我按下其中一个箭头键并保持按下状态,然后按下第二个箭头键,然后释放第二个键,块将完全停止移动,而不是继续朝着第一个未释放的箭头键的方向移动。我认为这是因为第二个键触发了KeyDown事件,在其释放时触发了KeyUp事件,并且KeyDown事件完全停止触发,即使第一个键已经被触发。

我无法找到令人满意的解决方案。

任何帮助都将不胜感激 =)

3个回答

10

大多数游戏不会等待事件。它们会在必要时轮询输入设备并相应地行动。事实上,如果你看一下XNA,你会发现有一个Keyboard.GetState()方法(或Gamepad.GetState()),你可以在更新过程中调用它,并根据结果更新你的游戏逻辑。当使用Windows.Forms时,没有现成的方法来做到这一点,但是你可以使用P/Invoke GetKeyBoardState()函数来利用它。好处是,你可以同时轮询多个按键,并因此对多个按键按下作出反应。这里有一个我在网上找到的简单类来帮助实现这个:

http://sanity-free.org/17/obtaining_key_state_info_in_dotnet_csharp_getkeystate_implementation.html

为了演示,我编写了一个简单的Windows应用程序,基本上根据键盘输入移动球。它使用我提供的类来轮询键盘状态。如果你同时按住两个键,你会注意到它会对角线移动。
首先是Ball.cs:
    public class Ball
    {
        private Brush brush;

        public float X { get; set; }
        public float Y { get; set; }
        public float DX { get; set; }
        public float DY { get; set; }
        public Color Color { get; set; }
        public float Size { get; set; }

        public void Draw(Graphics g)
        {
            if (this.brush == null)
            {
                this.brush = new SolidBrush(this.Color);
            }
            g.FillEllipse(this.brush, X, Y, Size, Size);
        }

        public void MoveRight()
        {
            this.X += DX;
        }

        public void MoveLeft()
        {
            this.X -= this.DX;
        }

        public void MoveUp()
        {
            this.Y -= this.DY;
        }

        public void MoveDown()
        {
            this.Y += this.DY;
        }
    }

这里需要翻译的内容是:

真的没有什么花哨的东西......

然后这里是Form1代码:

    public partial class Form1 : Form
    {
        private Ball ball;
        private Timer timer;
        public Form1()
        {
            InitializeComponent();
            this.ball = new Ball
            {
                X = 10f,
                Y = 10f,
                DX = 2f,
                DY = 2f,
                Color = Color.Red,
                Size = 10f
            };
            this.timer = new Timer();
            timer.Interval = 20;
            timer.Tick += new EventHandler(timer_Tick);
            timer.Start();
        }

        void timer_Tick(object sender, EventArgs e)
        {
            var left = KeyboardInfo.GetKeyState(Keys.Left);
            var right = KeyboardInfo.GetKeyState(Keys.Right);
            var up = KeyboardInfo.GetKeyState(Keys.Up);
            var down = KeyboardInfo.GetKeyState(Keys.Down);

            if (left.IsPressed)
            {
                ball.MoveLeft();
                this.Invalidate();
            }

            if (right.IsPressed)
            {
                ball.MoveRight();
                this.Invalidate();
            }

            if (up.IsPressed)
            {
                ball.MoveUp();
                this.Invalidate();
            }

            if (down.IsPressed)
            {
                ball.MoveDown();
                this.Invalidate();
            }


        }


        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            if (this.ball != null)
            {
                this.ball.Draw(e.Graphics);
            }
        }
    }

一个简单的小应用程序。只创建一个球和一个计时器。每20毫秒,它检查键盘状态,如果按下了某个键,它就移动它并无效化,以便可以重新绘制。


非常感谢,我尝试了你的代码和链接中的类,效果非常好。在编译前,我不得不对它进行了大量修改,但我认为这主要是因为我使用的是Visual Studio 2005而不是2008。我在网上搜索了很久,但找不到任何像这样好用的东西。我希望它也能帮助其他人。 - Daniel Waltrip
1
哎呀,这是我的错。你确实提到了你在使用VS2005,我没有注意到。是的,我太习惯使用自动属性、对象初始化器和“var”关键字了。对不起!很高兴它能为你工作。不过,你应该看看XNA游戏开发,它有很多很棒的开箱即用功能。 - BFree

1
如果您依赖按键重复来重复发送按下事件以使块移动,我认为这不是您想要的方式。块应该独立于按键重复而一致移动。因此,在按键事件期间不应该移动块。您只应在keydown和keyup事件期间跟踪按键状态,并在其他地方处理移动。实际的移动应该发生在某种定时器事件中(定时器控件即使没有任何操作也会定期触发事件),或者您应该有一个主循环不断检查所有内容的状态,并在适当时移动对象。如果您使用第二个选项,则需要了解“DoEvents”,因为如果您有代码不断运行而从未完成函数,则程序将不会处理任何其他事件,例如keyup和keydown事件。因此,您需要在每个循环内调用DoEvents以处理按键事件(以及其他诸如移动窗口之类的事情)。如果您使用定时器控件,则不必担心任何问题。

0

抱歉,暂时没有具体的帮助……毕竟今晚是周六。但是:

每个键都需要一个状态。当你收到按键按下的事件时,可以知道哪个键被按下,并修改你正在维护的键状态。当键再次弹起时,同样地,从事件中可以知道它是哪个键。只修改事件被触发的键的状态:即不要重置所有状态与keyup事件,否则你记忆中持有的键的状态会被破坏,正如你现在看到的。


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