5

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

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

我想使用向下、向右和向左箭头键让用户移动块,除了使用计时器让形状每隔这么多毫秒自动下降一行。

我正在使用 KeyDown 事件处理程序来检查用户何时按下向下、向左和向右箭头键。这并不难。问题是我想允许对角线运动,并且我希望它尽可能顺利地工作。我尝试了很多不同的方法来解决这个问题,并取得了不同程度的成功。但我不能完全正确...

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

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

我终其一生都无法为这个问题找到满意的解决方案。

任何帮助将不胜感激 =)

4

3 回答 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 毫秒,它检查一次键盘状态,如果按下一个键,它会移动它并使其无效,以便它可以重新绘制。

于 2009-05-24T02:45:37.467 回答
1

如果您依靠按键重复来重复发送按键事件以使块移动,我认为这不是您想要的方式。该块应始终独立于键重复移动。因此,您不应该在关键事件期间移动块。您应该只在 keydown 和 keyup 事件期间跟踪键的状态,并在其他地方处理移动。实际的移动应该发生在某种计时器事件中(即使没有发生任何事情,计时器控件也会定期触发事件),或者您应该有一个主循环不断检查一切的状态并在适当的时候移动对象。如果您使用第二个选项,您将需要查看“DoEvents”,因为如果您的代码一直在运行而没有完成功能,程序不会处理任何其他事件,例如 keyup 和 keydown 事件。因此,您可能希望在每个循环中调用 DoEvents 来处理关键事件(以及移动窗口等其他事情)。如果您不需要如此频繁地处理事情,您可能还想调用 System.Threading.Thread.Sleep。如果您使用计时器控件,则不必担心这些。

于 2009-05-24T02:44:21.353 回答
0

抱歉,没有具体的帮助……毕竟是星期六晚上……但是:

每个键都需要一个状态。当您收到 keydown 事件时,您可以判断哪个键已关闭,并修改您正在维护的 keystate。当钥匙再次出现时,通过查看事件,您可以分辨出它是哪一个。仅修改触发事件的键的状态:即不要使用 keyup 事件重置所有状态,否则您在内存中持有的键的状态将被破坏,如您所见。

于 2009-05-24T00:28:54.323 回答