如何修复WPF中网格行和列不更新的问题?

3
我正在尝试在WPF中制作贪吃蛇游戏,我决定使用网格来显示游戏板。这条蛇应该通过改变网格列和网格行属性来移动其x和y位置。为了实现这一点,我创建了一个SnakePlayer类和一个Food类,在MainWindow中每200ms调用一次游戏循环,并监听键盘以设置蛇的方向。然而问题是,即使蛇的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>

在更新蛇位置后,你尝试过调用GameGrid.UpdateLayout()吗?如果这不起作用,你可能需要在UI线程上进行该调用。这只是一个简单的问题,可以使用UI调度程序来实现包装:Dispatcher.Invoke(() => GameGrid.UpdateLayout()); - Tam Bui
@TamBui 很不幸,它似乎无法工作。 - RANDOM NAME
我没有看到你的方法cb()在哪里。那是在更新什么吗? - Tam Bui
@TamBui 作为第一个参数传递给函数(我在构造函数中传递了 GameLoop 函数),实际上,我进行了一些测试,它确实每200毫秒调用一次该函数,但是可视化更改却没有发生。 - RANDOM NAME
1个回答

1
“虽然我不完全理解您对这个游戏所需的用户体验,但我做了一些改动,产生了更有意义的结果。

您的用户界面(UI)没有更新的主要原因是,您从未更改过GenerateBodyPart的位置。它始终等于其初始值。您应该传递具有新位置的“Head”,而不是永远不会改变的“tail”。

将此更改为:


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

要做到这一点(请注意,我去掉了 static 关键字才能到达 Head):
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),
};

在进行这些更改后,用户界面至少更新了蛇的位置。观看视频。我不知道你想如何显示蛇,但这是你以后需要解决的问题。 :)
编码愉快!
这里是我完整的源代码供参考:在此下载

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