C# - клон тетриса - не удается заставить блок правильно реагировать на комбинации клавиш со стрелками

Я работаю над программированием игры Tetris в Visual C# 2005. Это самая обширная программа, которую я разработал.

Я создаю класс фигур и класс блоков для управления расположением, перемещением и отображением различных фигур тетриса. У меня есть функции moveDown(), moveLeft() и moveRight() для каждой фигуры (и соответствующие логические функции canMoveDown(), canMoveLeft(), canMoveRight(), которые проверяют, можно ли перемещать). Это все прекрасно работает.

Я хочу использовать клавиши со стрелками вниз, вправо и влево, чтобы позволить пользователю перемещать блок, в дополнение к использованию таймера, чтобы форма автоматически падала на один ряд каждые столько миллисекунд.

Я использую обработчик событий KeyDown, чтобы проверить, когда пользователь нажимает клавишу со стрелкой вниз, влево и вправо. Это не так сложно. Проблема в том, что я хочу учесть диагональное движение и хочу, чтобы оно работало максимально плавно. Я пробовал несколько разных подходов к этой проблеме, с разными уровнями успеха. Но я не могу понять это совершенно правильно...

Мой самый успешный подход состоял в том, чтобы использовать три логические переменные, чтобы отслеживать, когда удерживаются клавиши со стрелками вниз, влево и вправо. Я бы установил для логических значений значение true в событии KeyDown и значение false в событии KeyUp. В событии KeyDown я бы также рассказал блоку, как двигаться, используя логические переменные, чтобы проверить, какая комбинация была нажата в данный момент. Это сработало очень хорошо, за исключением одной вещи.

Если я нажму одну из клавиш со стрелками и удержу, затем нажму вторую клавишу со стрелкой и затем отпущу вторую клавишу, блок полностью перестанет двигаться, вместо того чтобы продолжать движение в направлении первой клавиши со стрелкой, которая не была отпущена еще. Я думаю, это потому, что второй ключ вызвал событие KeyDown, и после его запуска событие KeyUp было запущено, а событие KeyDown прекратило срабатывание полностью, даже если первый ключ был запущен.

Я не могу на всю жизнь найти удовлетворительное решение этой проблемы.

Любая помощь будет принята с благодарностью =)

3 ответа

Решение

Большинство игр не ждут событий. Они опрашивают устройство ввода, когда это необходимо, и действуют соответственно. Фактически, если вы когда-нибудь взгляните на 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 миллисекунд он проверяет состояние клавиатуры, а если нажать клавишу, он перемещает ее и делает недействительной, чтобы она могла перерисоваться.

Если вы полагаетесь на повторение клавиш для многократной отправки событий нажатия клавиш для перемещения блока, я не думаю, что вы именно так и захотите это сделать. Блок должен двигаться последовательно независимо от повтора ключа. Поэтому вы не должны перемещать блок во время ключевых событий. Вы должны отслеживать состояние клавиш только во время событий keydown и keyup и управлять движением в другом месте. Фактическое движение должно происходить либо в каком-либо событии таймера (элемент управления таймером запускает события через равные промежутки времени, даже если ничего не происходит), либо у вас должен быть основной цикл, постоянно проверяющий состояние всего и движущиеся объекты при необходимости. Если вы используете вторую опцию, вам нужно будет заглянуть в "DoEvents", потому что если у вас есть код, который постоянно работает, даже не завершая функцию, программа не будет обрабатывать любые другие события, такие как события keyup и keydown. Таким образом, вы хотите вызывать DoEvents в каждом цикле для обработки ключевых событий (в том числе, например, перемещение окна). Вы также можете вызвать System.Threading.Thread.Sleep, если вам не нужно постоянно обрабатывать вещи. Если вы используете элемент управления таймером, вам не нужно беспокоиться об этом.

Извините, никакой конкретной помощи... в конце концов, сегодня субботняя ночь... однако:

Каждому ключу нужно состояние. Когда вы получаете событие нажатия клавиши, вы можете сказать, какая клавиша вышла из строя, и изменить состояние ключа, которое вы поддерживаете. Когда ключ снова возвращается, после просмотра события вы можете сказать, что это было. Изменяйте только состояние ключа, для которого было инициировано событие: т.е. не сбрасывайте все состояния с помощью события keyup, в противном случае состояние ключей, которые вы держите в памяти, будет повреждено, как вы видите.

Другие вопросы по тегам