I'm trying to make a snake game in WPF and I decided to use a grid to display the board.
The snake is supposed to move its x and y position changing the grid column and grid row property. To achieve this I made a SnakePlayer
class, a Food
class.
In the MainWindow
I call the game loop every 200ms and I listen to the keyboard to set the snake direction.
The issue is, even though the snake x, y position changes correctly in the code ( I tested this ),
the snake changes in position are not visualized because it keeps staying in the initial position.
SnakePlayer Class:
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);
}
}
}
Food Class:
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);
}
}
}
MainWindow:
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>
While I don't fully understand your desired UX for this game, I made a few changes that are producing more meaningful results.
The main reason why your UI isn't updating is because you are never changing your GenerateBodyPart's position. It is always equal to its initial value. Instead of passing the "tail" which never changes, you should be passing the "Head" which has the new position.
Change this:
To be this (notice that I removed the
static
keyword to get to the Head):Also, your "Directions" are incorrect for UP and DOWN. It should be this:
After making those changes, the UI was at least updating the snake position. Watch video. I don't know exactly how you want the snake to display, but that's for you to figure out later. :)
Have fun coding!
Here is my full source code for reference: Download here