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>