Как исправить строку и столбцы сетки, которые не обновляются в wpf?

Я пытаюсь сделать змеиную игру в WPF и решил использовать сетку для отображения доски. Предполагается, что змея перемещает свои позиции x и y, изменяя свойства столбца сетки и строки сетки. Для этого я сделал SnakePlayerкласс, а Foodучебный класс. в MainWindowЯ вызываю игровой цикл каждые 200 мс и слушаю клавиатуру, чтобы установить направление змеи. Проблема в том, что хотя позиция змеи x, y изменяется правильно в коде (я проверял это), изменения позиции змеи не визуализируются, потому что она остается в исходном положении.

Класс SnakePlayer:

      namespace Snake
{
    internal class SnakePlayer
    {
        // keeps track of the current direction and makes the snake keep moving
        public (int x, int y) Acceleration = (x: 0, y: 1);
        //rappresents the coordinate of each snake part
        private readonly List<(int x, int y)> Body = new();
        public (int x, int y) Head;
        public SnakePlayer(int NUMBER_OF_ROWS, int NUMBER_OF_COLUMNS)
        {
            int x = Convert.ToInt32((NUMBER_OF_ROWS - 1) / 2);
            int y = Convert.ToInt32((NUMBER_OF_COLUMNS - 1) / 2);
            Body.Add((x, y));
            Head = Body.ElementAt(0);
        }
        public void UpdatePosition()
        {
            for (int i = Body.Count - 2; i >= 0; i--)
            {
                (int x, int y) = Body.ElementAt(i);
                Body[i + 1] = (x, y);
            }
            MoveHead();
        }
        private void MoveHead()
        {
            // for example if acceleration is (1,0) the head keeps going to the right each time the method is called 
            Head.x += Acceleration.x;
            Head.y += Acceleration.y;
        }
        public void Show(Grid gameGrid)
        {
            /* 
             * i basically erase all the previous snake parts and
             * then draw new elements at the new positions 
            */
            gameGrid.Children.Clear();
            Body.ForEach(tail =>
            {
                Border element = GenerateBodyPart(tail.x, tail.y);
                gameGrid.Children.Add(element);
            });
        }
        private static Border GenerateBodyPart(int x, int y)
        {
            static void AddStyles(Border elem)
            {
                elem.HorizontalAlignment = HorizontalAlignment.Stretch;
                elem.VerticalAlignment = VerticalAlignment.Stretch;
                elem.CornerRadius = new CornerRadius(5);
                elem.Background = Brushes.Green;
            }
            Border elem = new();
            AddStyles(elem);
            Grid.SetColumn(elem, x);
            Grid.SetRow(elem, y);
            return elem;
        }
        public void Grow()
        {
            var prevHead = (Head.x,Head.y);
            AddFromBottomOfList(Body,prevHead);
        }
        public bool Eats((int x, int y) position)
        {
            return Head.x == position.x && Head.y == position.y;
        }
        public void SetAcceleration(int x, int y)
        {
            Acceleration.x = x;
            Acceleration.y = y;
            UpdatePosition();
        }
        public bool Dies(Grid gameGrid)
        {
            bool IsOutOfBounds(List<(int x, int y)> Body)
            {
                int mapWidth = gameGrid.ColumnDefinitions.Count;
                int mapHeight = gameGrid.RowDefinitions.Count;
                return Body.Any(tail => tail.x > mapWidth || tail.y > mapHeight || tail.x < 0 || tail.y < 0);
            }
            bool HitsItsSelf(List<(int x, int y)> Body)
            {
                return Body.Any((tail) =>
                {
                    bool isHead = Body.IndexOf(tail) == 0;
                    if (isHead) return false;
                    return Head.x == tail.x && Head.y == tail.y;
                });
            }
            return IsOutOfBounds(Body) || HitsItsSelf(Body);
        }
        public bool HasElementAt(int x, int y)
        {
            return Body.Any(tail => tail.x == x && tail.y == y);
        }
        private static void AddFromBottomOfList<T>(List<T> List,T Element)
        {
            List<T> ListCopy = new();
            ListCopy.Add(Element);
            ListCopy.AddRange(List);
            List.Clear();
            List.AddRange(ListCopy);
        }
    }
}

Класс еды:

      namespace Snake
{
    internal class Food
    {
        public readonly SnakePlayer snake;
        public (int x, int y) Position { get; private set; }
        public Food(SnakePlayer snake, Grid gameGrid)
        {
            this.snake = snake;
            Position = GetInitialPosition(gameGrid);
            Show(gameGrid);
        }
        private (int x, int y) GetInitialPosition(Grid gameGrid)
        {
            (int x, int y) getRandomPosition()
            {
                static int RandomPositionBetween(int min, int max)
                {
                    Random random = new();
                    return random.Next(min, max);
                }
                int cols = gameGrid.ColumnDefinitions.Count;
                int rows = gameGrid.RowDefinitions.Count;
                int x = RandomPositionBetween(0, cols);
                int y = RandomPositionBetween(0, rows);
                return (x, y);
            }
            var position = getRandomPosition();
            if (snake.HasElementAt(position.x, position.y)) return GetInitialPosition(gameGrid);
            return position;
        }
        public void Show(Grid gameGrid)
        {
            static void AddStyles(Border elem)
            {
                elem.HorizontalAlignment = HorizontalAlignment.Stretch;
                elem.VerticalAlignment = VerticalAlignment.Stretch;
                elem.CornerRadius = new CornerRadius(500);
                elem.Background = Brushes.Red;
            }
            Border elem = new();
            AddStyles(elem);
            Grid.SetColumn(elem, Position.x);
            Grid.SetRow(elem, Position.y);
            gameGrid.Children.Add(elem);
        }
    }
}

Главное окно:

      namespace Snake
{
    public partial class MainWindow : Window
    {
        const int NUMBER_OF_ROWS = 15, NUMBER_OF_COLUMNS = 15;
        private readonly SnakePlayer snake;
        private Food food;
        private readonly DispatcherTimer Loop;
        public MainWindow()
        {
            InitializeComponent();
            CreateBoard();
            snake = new SnakePlayer(NUMBER_OF_ROWS, NUMBER_OF_COLUMNS);
            food = new Food(snake, GameGrid);
            GameGrid.Focus();
            GameGrid.KeyDown += (sender, e) => OnKeySelection(e);
            Loop = SetInterval(GameLoop, 200);
        }
        private void GameLoop()
        {
            snake.UpdatePosition();
            snake.Show(GameGrid);
            food.Show(GameGrid);
            if (snake.Eats(food.Position))
            {
                food = new Food(snake, GameGrid);
                snake.Grow();
            }
            else if (snake.Dies(GameGrid))
            {
                Loop.Stop();
                snake.UpdatePosition();
                ResetMap();
                ShowEndGameMessage("You Died");
            }
        }
        private void OnKeySelection(KeyEventArgs e)
        {
            if(e.Key == Key.Escape)
            {
                Close();
                return;
            }
            var DIRECTIONS = new
            {
                UP = (0, 1),
                LEFT = (-1, 0),
                DOWN = (0, -1),
                RIGHT = (1, 0),
            };
            Dictionary<string, (int x, int y)> acceptableKeys = new()
            {
                { "W", DIRECTIONS.UP },
                { "UP", DIRECTIONS.UP },
                { "A", DIRECTIONS.LEFT },
                { "LEFT", DIRECTIONS.LEFT },
                { "S", DIRECTIONS.DOWN },
                { "DOWN", DIRECTIONS.DOWN },
                { "D", DIRECTIONS.RIGHT },
                { "RIGHT", DIRECTIONS.RIGHT }
            };
            string key = e.Key.ToString().ToUpper().Trim();
            if (!acceptableKeys.ContainsKey(key)) return;
            (int x, int y) = acceptableKeys[key];
            snake.SetAcceleration(x, y);
        }
        private void CreateBoard() 
        {
            for (int i = 0; i < NUMBER_OF_ROWS; i++)
                GameGrid.RowDefinitions.Add(new RowDefinition());
            for (int i = 0; i < NUMBER_OF_COLUMNS; i++)
                GameGrid.ColumnDefinitions.Add(new ColumnDefinition());
        }
        private void ResetMap()
        {
            GameGrid.Children.Clear();
            GameGrid.RowDefinitions.Clear();
            GameGrid.ColumnDefinitions.Clear();
        }
        private void ShowEndGameMessage(string message)
        {
            TextBlock endGameMessage = new();
            endGameMessage.Text = message;
            endGameMessage.HorizontalAlignment = HorizontalAlignment.Center;
            endGameMessage.VerticalAlignment = VerticalAlignment.Center;
            endGameMessage.Foreground = Brushes.White;
            GameGrid.Children.Clear();
            GameGrid.Children.Add(endGameMessage);
        }
        private static DispatcherTimer SetInterval(Action cb, int ms)
        {
            DispatcherTimer dispatcherTimer = new();
            dispatcherTimer.Interval = TimeSpan.FromMilliseconds(ms);
            dispatcherTimer.Tick += (sender, e) => cb();
            dispatcherTimer.Start();
            return dispatcherTimer;
        }
    }
}

MainWindow.xaml:

      <Window x:Class="Snake.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Snake"
        mc:Ignorable="d"
        WindowStyle="None"
        Background="Transparent"
        WindowStartupLocation="CenterScreen"
        Title="MainWindow" Height="600" Width="600" ResizeMode="NoResize" AllowsTransparency="True">
    <Border CornerRadius="20" Height="600" Width="600" Background="#FF0D1922">
        <Grid x:Name="GameGrid" Focusable="True" ShowGridLines="False"/>
    </Border>
</Window>

1 ответ

Хотя я не совсем понимаю желаемый вами UX для этой игры, я внес несколько изменений, которые дают более значимые результаты.

Основная причина, по которой ваш пользовательский интерфейс не обновляется, заключается в том, что вы никогда не меняете положение своего GenerateBodyPart. Он всегда равен своему начальному значению. Вместо того, чтобы проходить «хвост», который никогда не меняется, вы должны проходить «голову», которая имеет новую позицию.

Измените это:

      private static Border GenerateBodyPart(int x, int y)
{
    ....
    Grid.SetColumn(elem, tail.x);
    Grid.SetRow(elem, tail.y);
    return elem;
}

Чтобы быть этим (обратите внимание, что я удалил ключевое слово, чтобы добраться до головы):

      private Border GenerateBodyPart(int x, int y)
{
    ....
    Grid.SetColumn(elem, Head.x);
    Grid.SetRow(elem, Head.y);
    return elem;
}

Кроме того, ваши «направления» неверны для ВВЕРХ и ВНИЗ. Это должно быть так:

      var DIRECTIONS = new
{
    UP = (0, -1),
    LEFT = (-1, 0),
    DOWN = (0, 1),
    RIGHT = (1, 0),
};

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

Получайте удовольствие от кодирования!

Вот мой полный исходный код для справки: Скачать здесь

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