Pongs is a 2 player game in which 2 players move their paddles simultaneously to hit a ball back and forth. Player 1 uses keys 'W', 'A', 'S', and 'D' for up, down, left and right respectively and Player 2 uses arrow keys. Both players attempt to hit the ball to opponents side. If the ball goes past a player's side of the screen, their opponent gains a point.
Quick Demo: click here
Source Code: click here
Description
Pongs is a game where you can move your paddle using the keys W for up movement, A, S, D (likewise) and arrow keys to hit the ball to the other side.
Figure 1 - Gif of game being played
If the ball goes past your side of the screen, the other person gets a point. The game goes on forever and keeps track of your score.
Figure 2 - Directions page from game
Settings
In this game, there are also customizable settings such as…
Figure 3 - Settings page from game
Ball Speed - Changes the speed at which the ball moves
Ball Size - Changes the size of the ball
Paddle Speed - Changes the speed at which the paddles move
Paddle Size - Changes the size of the paddles
Rounds to Win - The amount of wins a player needs to win the game
Ball/Paddle/Wall/Background Colors - The different shapes in the game that can have their color changed. Changing the colors on the settings page to different shades of green would allow for the game to like like this:
The color picker on the settings page also changes color when you change the color of the game.
There are also pause and restart buttons which either pause the game until clicked again or restart the game respectively
Figure 4 - Pause and restart button from game
Background
This game was made using WPF and C# on Visual Studio in the span of one week. It was made to obtain the basics of WPF and how to use C# code behind to create dynamic objects in the code.
Code Explanation
Drawing the board
One key part of my code is drawing everything on the board, such as the paddle and the ball. This is done through 3 different functions.
public void DrawPaddle(Rectangle Paddle, double x, double y)
{
log.Info("DrawPaddle Start");
PaddleColor = new SolidColorBrush(SliderInfo.PaddleColor);
Paddle.Width = 1.5 * SliderInfo.PaddleSize;
Paddle.Height = 7.8 * SliderInfo.PaddleSize;
Paddle.Fill = Brushes.Black;
Paddle.Stroke = PaddleColor;
Paddle.StrokeThickness = 4;
Canvas.SetTop(Paddle, y);
Canvas.SetLeft(Paddle, x);
log.Info("DrawPaddle End");
}
The above code draws the paddle onto the board. It is drawn separate from the other shapes since it is the only shape that moves via player input, and also requires inputs, such as which paddle and the x and y coordinates of it.
public void ReDraw()
{
log.Info("ReDraw Start");
BallColor = new SolidColorBrush(SliderInfo.BallColor);
WallColor = new SolidColorBrush(SliderInfo.WallColor);
BackgroundColor = new SolidColorBrush(SliderInfo.BackgroundColor);
if (WindowState == WindowState.Maximized)
{
Window.Height = (int)System.Windows.SystemParameters.PrimaryScreenHeight;
Window.Width = (int)System.Windows.SystemParameters.PrimaryScreenWidth;
}
Board.Width = Window.Width;
Board.Height = Window.Height;
sbkGameEngine.y1 = Board.Height / 2 - (paddle1.Height / 2);
sbkGameEngine.y2 = sbkGameEngine.y1;
sbkGameEngine.x2 = Board.Width - 32;
sbkGameEngine.x1 = 0;
Ball.Width = 1.5 * SliderInfo.BallSize;
Ball.Height = 1.5 * SliderInfo.BallSize;
Ball.Fill = BallColor;
Ball.Stroke = BallColor;
Ball.StrokeThickness = 4;
Canvas.SetTop(Ball, Board.Height / 2 - Ball.Height / 2);
Canvas.SetLeft(Ball, Board.Width / 2 - Ball.Width / 2);
Menu.Width = Board.Width;
if (Board.Width - (SettingsMenu.Width + About.Width + Help.Width) - (1.5 * RestartButton.Width + PauseButton.Width) - 2 > 0)
{
Spacer.Width = Board.Width - (SettingsMenu.Width + About.Width + Help.Width) - (1.5 * RestartButton.Width + PauseButton.Width) - 2;
}
P1Scoreboard.Text = "" + sbkGameEngine.P1Score;
P2Scoreboard.Text = "" + sbkGameEngine.P2Score;
sbkGameEngine.CanBallMove = false;
ReDrawUnmoving();
log.Info("ReDraw End");
}
This function draws all of the unmoving shapes, as well as the menu and board. This is done to initially set up the board and to reset the board when necessary.
private void ReDrawUnmoving()
{
log.Info("ReDrawUnmoving Start");
Boundary.Stroke = WallColor;
Boundary.StrokeThickness = 3;
Boundary.X1 = Board.Width / 2;
Boundary.X2 = Board.Width / 2;
Boundary.Y1 = 0;
Boundary.Y2 = Board.Height;
BottomWall.Width = Board.Width;
BottomWall.Height = 24;
BottomWall.Fill = WallColor;
BottomWall.Stroke = WallColor;
BottomWall.StrokeThickness = 4;
Canvas.SetTop(BottomWall, Board.Height - 63);
Canvas.SetLeft(BottomWall, 0);
Menu.BorderBrush = WallColor;
Spacer.BorderBrush = WallColor;
Menu.Background = WallColor;
Spacer.Background = WallColor;
Board.Background = BackgroundColor;
log.Info("ReDrawUnmoving End");
}
This function does the same thing as ReDraw(), just this time drawing the unmoving parts.
These functions allow for the board to be drawn, which a is key element of my game. This is because knowing where everything is on the board is necessary for the player to react in the right way.
Moving the Ball
Another key part of my game is the movement of the ball. This is because the main point of the game is to try and block the ball from moving into your side of the field.
public void BallMovement()
{
log.Info("BallMovement Start");
if (sbkGameEngine.CanBallMove && sbkGameEngine.GamePlayable)
{
Canvas.SetTop(Ball, Canvas.GetTop(Ball) + sbkGameEngine.VMovement);
Canvas.SetLeft(Ball, Canvas.GetLeft(Ball) + sbkGameEngine.HMovement);
}
if (sbkGameEngine.P1Wins)
{
WhoWon_.Text = "Player 1 Wins!";
WhoWon_.Visibility = Visibility.Visible;
RestartText.Visibility = Visibility.Visible;
OnPause(Ball, a);
sbkGameEngine.i = 2;
}
if (sbkGameEngine.P2Wins)
{
WhoWon_.Text = "Player 2 Wins!";
WhoWon_.Visibility = Visibility.Visible;
RestartText.Visibility = Visibility.Visible;
OnPause(Ball, a);
sbkGameEngine.i = 2;
}
log.Info("BallMovement End");
}
This function only takes the variables from the sbkGameEngine class to move the ball. It doesn't do any calculations itself and just does what the engine tells it to do via the variables that are changed by the sbkGameEngine.
public void BallMovement()
{
log.Info("BallMovement Start");
if (GamePlayable)
{
int P1Top = (int)Canvas.GetTop(mGuiReference.paddle1);
int P1Bottom = (int)(Canvas.GetTop(mGuiReference.paddle1) + mGuiReference.paddle1.Height);
int P1Left = (int)Canvas.GetLeft(mGuiReference.paddle1);
int P1Right = (int)(Canvas.GetLeft(mGuiReference.paddle1) + mGuiReference.paddle1.Width);
int P2Top = (int)Canvas.GetTop(mGuiReference.paddle2);
int P2Bottom = (int)(Canvas.GetTop(mGuiReference.paddle2) + mGuiReference.paddle2.Height);
int P2Left = (int)Canvas.GetLeft(mGuiReference.paddle2);
int P2Right = (int)(Canvas.GetLeft(mGuiReference.paddle2) + mGuiReference.paddle2.Width);
int BallTop = (int)Canvas.GetTop(mGuiReference.Ball);
int BallBottom = (int)(Canvas.GetTop(mGuiReference.Ball) + mGuiReference.Ball.Height);
int BallLeft = (int)Canvas.GetLeft(mGuiReference.Ball);
int BallRight = (int)(Canvas.GetLeft(mGuiReference.Ball) + mGuiReference.Ball.Width);
if ((P2Bottom > BallTop && P2Top < BallBottom && BallLeft < P2Right && BallRight > P2Left && HMovement == 1) || (P1Bottom > BallTop && P1Top < BallBottom && BallLeft < P1Right && BallRight > P1Left && HMovement == -1))
{
HMovement *= -1;
Console.Beep(37, 10);
}
if (BoundaryCheck(mGuiReference.Ball, 25, (int)(mGuiReference.Board.Height - (mGuiReference.BottomWall.Height * 2) - 5), 0, 0, true, true, false, false) == false)
{
VMovement *= -1;
Console.Beep(70, 5);
}
if (Canvas.GetLeft(mGuiReference.Ball) >= mGuiReference.Board.Width)
{
P1Score++;
mGuiReference.ReDraw();
log.Info("Player 1 scored!");
if (P1Score == SliderInfo.RoundsToWin)
{
P1Wins = true;
}
}
if (Canvas.GetLeft(mGuiReference.Ball) + 15 <= 0)
{
P2Score++;
mGuiReference.ReDraw();
log.Info("Player 2 scored!");
if (P2Score == SliderInfo.RoundsToWin)
{
P1Wins = true;
}
}
}
log.Info("BallMovement End");
}
This function is the one actually doing the calculations. It changes the variables controlling which direction the ball is moving when certain conditions are met, like swapping directions after hitting a paddle.
Both these functions combine to move the ball around and allow for the ball to interact with it's environment.
Paddle Movement
Another element necessary to the creation of the game is the movement of the paddles, since it is the only player controlled shape.
public void OnKeyDown(object sender, KeyEventArgs e)
{
log.Info("OnKeyDown Start");
if (AllowedKeys.Contains(e.Key))
{
if (i == 0)
{
KeysPressed.Add(e.Key);
CanBallMove = true;
}
}
log.Info("OnKeyDown End");
}
This function, when a key is pressed, adds that key to a hashset. The hashset will be later used to identify which keys are being pressed and which are not.
public void OnKeyUp(object sender, KeyEventArgs e)
{
log.Info("OnKeyUp Start");
if (KeysPressed.Contains(e.Key))
{
KeysPressed.Remove(e.Key);
}
log.Info("OnKeyUp End");
}
This function removes the let go key and runs when a key is pressed.
public void PressedKeys()
{
log.Info("PressedKeys Start");
if (GamePlayable)
{
if (KeysPressed.Contains(Key.Up) && BoundaryCheck(mGuiReference.paddle2, 25, (int)mGuiReference.Board.Height - 78, (int)mGuiReference.Board.Width / 2, (int)mGuiReference.Board.Width, true, false, false, false) == true)
{
y2 -= 2;
}
if (KeysPressed.Contains(Key.W) && BoundaryCheck(mGuiReference.paddle1, 25, (int)mGuiReference.Board.Height - 78, (int)mGuiReference.Board.Width / 2, (int)mGuiReference.Board.Width, true, false, false, false) == true)
{
y1 -= 2;
}
if (KeysPressed.Contains(Key.Down) && BoundaryCheck(mGuiReference.paddle2, 25, (int)mGuiReference.Board.Height - 78, (int)mGuiReference.Board.Width / 2, (int)mGuiReference.Board.Width, false, true, false, false) == true)
{
y2 += 2;
}
if (KeysPressed.Contains(Key.S) && BoundaryCheck(mGuiReference.paddle1, 25, (int)mGuiReference.Board.Height - 78, (int)mGuiReference.Board.Width / 2, (int)mGuiReference.Board.Width, false, true, false, false) == true)
{
y1 += 2;
}
if (KeysPressed.Contains(Key.Left) && BoundaryCheck(mGuiReference.paddle2, 25, (int)mGuiReference.Board.Height - 78, (int)mGuiReference.Board.Width / 2, (int)mGuiReference.Board.Width / 2, false, false, true, false) == true)
{
x2 -= 2;
}
if (KeysPressed.Contains(Key.A) && BoundaryCheck(mGuiReference.paddle1, 25, (int)mGuiReference.Board.Height - 78, 0, (int)mGuiReference.Board.Width / 2, false, false, true, false) == true)
{
x1 -= 2;
}
if (KeysPressed.Contains(Key.Right) && BoundaryCheck(mGuiReference.paddle2, 25, (int)mGuiReference.Board.Height - 78, 0, (int)mGuiReference.Board.Width - 20, false, false, false, true) == true)
{
x2 += 2;
}
if (KeysPressed.Contains(Key.D) && BoundaryCheck(mGuiReference.paddle1, 25, (int)mGuiReference.Board.Height - 78, 0, (int)mGuiReference.Board.Width / 2, false, false, false, true) == true)
{
x1 += 2;
}
}
log.Info("PressedKeys End");
}
The above function is run repeatedly by a different function and checks if any keys are in the hashset. If a key is in the hashset, it will do the corresponding move, such as moving the player 2 paddle up when the up key is in the hashset.
These all combine to allow player key inputs to be able to correspond to actions taken by the paddles in the game.
Settings Page
The settings page requires sliders that change variables upon the slider changing so that whatever the slider says to happen happens. This is done using XAML and data binding.
public static double BallSpeed { get; set; }
public static double BallSize { get; set; }
public static double PaddleSpeed { get; set; }
public static double PaddleSize { get; set; }
public static double RoundsToWin { get; set; }
public static Color BallColor { get; set; }
public static Color BackgroundColor { get; set; }
public static Color PaddleColor { get; set; }
public static Color WallColor { get; set; }
The slider info class contains all of the variables for the sliders to act upon. These variables will be the ones that change when sliders are shifted.
<Slider x:Name="BallSpeedSlider" Margin="136,72,0,0" Maximum="9" Minimum="2" IsSnapToTickEnabled="True" Value="{Binding BallSpeed, Mode=TwoWay}" ValueChanged="OnValueChanged" HorizontalAlignment="Left" Width="226" Height="123" VerticalAlignment="Top" Grid.ColumnSpan="2"/>
<Slider x:Name="BallSizeSlider" Margin="334,72,0,0" Maximum="50" Minimum="1" IsSnapToTickEnabled="True" Value="{Binding BallSize, Mode=TwoWay}" ValueChanged="OnValueChanged" HorizontalAlignment="Left" Width="226" Height="123" VerticalAlignment="Top" Grid.Column="1"/>
<Slider x:Name="PaddleSpeedSlider" Margin="136,153,0,0" Maximum="9" Minimum="2" IsSnapToTickEnabled="True" Value="{Binding PaddleSpeed, Mode=TwoWay}" ValueChanged="OnValueChanged" HorizontalAlignment="Left" Width="226" Height="123" VerticalAlignment="Top" Grid.ColumnSpan="2"/>
<Slider x:Name="PaddleSizeSlider" Margin="334,151,0,0" Maximum="50" Minimum="1" IsSnapToTickEnabled="True" Value="{Binding PaddleSize, Mode=TwoWay}" ValueChanged="OnValueChanged" HorizontalAlignment="Left" Width="226" Height="123" VerticalAlignment="Top" Grid.Column="1"/>
<xctk:ColorPicker x:Name="Ball_Color_Picker" Margin="33,339,0,0" Height="23" VerticalAlignment="Top" HorizontalAlignment="Left" Width="101" ShowDropDownButton = "False" ShowTabHeaders="False" ColorMode="ColorCanvas" SelectedColor="{Binding BallColor, Mode=TwoWay}"/>
<xctk:ColorPicker x:Name="Background_Color_Picker" Margin="425,339,0,0" Grid.Column="1" Height="23" VerticalAlignment="Top" HorizontalAlignment="Left" Width="110" ShowDropDownButton = "False" ShowTabHeaders="False" ColorMode="ColorCanvas" SelectedColor="{Binding BackgroundColor, Mode=TwoWay}"/>
<xctk:ColorPicker x:Name="Paddle_Color_Picker" Margin="230,339,0,0" Height="23" VerticalAlignment="Top" HorizontalAlignment="Left" Width="101" ShowDropDownButton = "False" ShowTabHeaders="False" ColorMode="ColorCanvas" SelectedColor="{Binding PaddleColor, Mode=TwoWay}" Grid.ColumnSpan="2"/>
<xctk:ColorPicker x:Name="Wall_Color_Picker" Margin="207,339,0,0" Height="23" VerticalAlignment="Top" HorizontalAlignment="Left" Width="101" ShowDropDownButton = "False" ShowTabHeaders="False" ColorMode="ColorCanvas" SelectedColor="{Binding WallColor, Mode=TwoWay}" Grid.Column="1"/>
The above are sliders and color pickers made in XAML that change the variables using data binding. Data binding binds the value of a slider to a variable, and setting the bode of the binding to TwoWay allows for when the slider is changed, the variable is too and vice versa.
These variables are connected to attributes like the speed of the ball or the color of the paddle such that when a slider or color picker is changed, the attribute changes as well. This allows the settings page to function in an uncomplex manner.
Running the Application
- Go to the GitHub link: click here
- Go to the folder labeled "Quick Demo"
- Download the "ZippedDemo.zip"
- Unzip the file
- Run Pongs.exe
History
v1.0 -- May 5th 2024 - First version
v1.1 -- May 22nd 2024 - Revised abstract
v1.2 -- May 23rd 2024 - Updated steps to run the application
v1.3 -- May 24th 2024 - Updated code
v1.4 -- May 27 2024 - Added settings header
v1.5 -- May 29 2024 - Added figure numbers and descriptions
v1.6 -- May 31 2024 - Fixed gif that was previously not loading
v2.0 -- June 2 2024 - Added color pickers to settings and updated code, settings, and code explanations.
References
1. https://stackoverflow.com/
2. https://learn.microsoft.com/en-us/dotnet/
If you found this article helpful / interesting, please don't forget to Vote!
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.