Pongs is a game developed in a weeks time 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.
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.
There is a settings page in this game. The settings are...
Ball/Paddle/Wall/Background Colors - The different shapes in the game that can have their color changed. Changing the colors on the settings page changes the colors of the board in real time:
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
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.
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. The UpdateLocations function updates the variables P1Left, P1Right, P1Up, P1Down, and their counterparts depending on the string given.
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.
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()
{
if (sbkGameEngine.CanBallMove && sbkGameEngine.GamePlayable)
{
Canvas.SetTop(Ball, Canvas.GetTop(Ball) + sbkGameEngine.VMovement);
Canvas.SetLeft(Ball, Canvas.GetLeft(Ball) + sbkGameEngine.HMovement);
UpdateLocations("ball");
}
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;
}
}
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 = Game.P1Up;
int P1Bottom = Game.P1Down;
int P1Left = Game.P1Left;
int P1Right = Game.P1Right;
int P2Top = Game.P2Up;
int P2Bottom = Game.P2Down;
int P2Left = Game.P2Left;
int P2Right = Game.P2Right;
int BallTop = Game.BallUp;
int BallBottom = Game.BallDown;
int BallLeft = Game.BallLeft;
int BallRight = Game.BallRight;
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(Game.Ball, 25, (int)(Game.Height - (Game.BottomWall.Height * 2) - 5), 0, 0, true, true, false, false) == false)
{
VMovement *= -1;
Console.Beep(70, 5);
}
if (BallLeft >= mGuiReference.Board.Width)
{
P1Score++;
Game.ReDraw();
log.Info("Player 1 scored!");
if (P1Score == SliderInfo.RoundsToWin)
{
P1Wins = true;
}
}
if (BallLeft + 15 <= 0)
{
P2Score++;
Game.ReDraw();
log.Info("Player 2 scored!");
if (P2Score == SliderInfo.RoundsToWin)
{
P2Wins = 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)
{
if (AllowedKeys.Contains(e.Key))
{
if (i == 0)
{
KeysPressed.Add(e.Key);
CanBallMove = true;
}
}
}
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)
{
if (KeysPressed.Contains(e.Key))
{
KeysPressed.Remove(e.Key);
}
}
This function removes the let go key and runs when a key is pressed.
public void PressedKeys()
{
if (GamePlayable)
{
if (KeysPressed.Contains(Key.Up) && BoundaryCheck(Game.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(Game.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(Game.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(Game.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(Game.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(Game.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(Game.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(Game.paddle1, 25, (int)mGuiReference.Board.Height - 78, 0, (int)mGuiReference.Board.Width / 2, false, false, false, true) == true)
{
x1 += 2;
}
}
}
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 Color PaddleColor { get; set; }
public static Color WallColor { get; set; }
The slider info class contains some 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"/>
<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 examples of 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.
Data Binding
SelectedColor="{Binding WallColor, Mode=TwoWay}"
The above is a sample of color binding in XAML. SelectedColor is the color that was selected on the color picker. You can see that it's bound to WallColor, as in the curly brackets it says Binding WallColor. WallColor can be replaced with any other variable and the color picker will instead be bound to that variable, assuming that that variable accepts an color input. The Mode being TwoWay allows for changes in the color picker to change the variable and changes in the variable to change the color picker. There are 4 types of data binding.
1. OneWay - Only changes in the source property (such as a color picker or slider) affect the variable
2. TwoWay - Changes in the variable affect the source property and changes in the source property affect the variable
3. OneWayToSource - Only changes in the variable affect the source property
4. OneTime - Only updates the source property once when initializing the application using the variable
These 4 types all have their own use cases, but I only every used TwoWay in my projects, as the others were not needed for any of my game's functionalities.
Shaking the Screen
By shaking the screen when a player scores a point, it creates visual feedback for the players, allowing them to feel a higher sense of accomplishment. While seeming easy to code at first, it comes with several obstacles.
To create this effect, I used storyboarding since it was a simple way to make the animation play out. The storyboard looks like this.
<Storyboard RepeatBehavior="0:0:1" Name="DownRight">
<DoubleAnimation Storyboard.TargetName="Window"
Storyboard.TargetProperty="(Window.Left)" From="{Binding WindowLeft, Mode=TwoWay}" To="{Binding Path=WindowLeftTo, Mode=TwoWay}"
Duration="0:0:0:0.03" BeginTime="0:0:0" AutoReverse="true" RepeatBehavior="3x" FillBehavior="Stop"/>
<DoubleAnimation Storyboard.TargetName="Window"
Storyboard.TargetProperty="(Window.Top)" From="{Binding WindowTop, Mode=TwoWay}" To="{Binding Path=WindowTopTo, Mode=TwoWay}"
Duration="0:0:0:0.03" BeginTime="0:0:0" AutoReverse="true" RepeatBehavior="3x" FillBehavior="Stop"/>
</Storyboard>
This storyboard moves the window up and down as well as left to right using two double animations. The double animations require the inputs From, the place where the window should start the animation, and to, where the window should end the animation. I used the window's left and top values for this, and for the to I used a variable that was the left of the window + 10 and the top of the window + 10.
To get these values, you may think that you could use "Application.Current.MainWindow.Left" and "Application.Current.MainWindow.Top". However, due to the way WPF was made, if the window was never moves, it would return NaN. This is important because this makes the code crash when the animation was run if the window was never moved. Because of this, I had to use a different method to obtain the coordinates of the window.
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
public event PropertyChangedEventHandler PropertyChanged;
This code creates a rect that gains the coordinates of the window. Thus allows for the code to find the coordinates of the rect, which is the same as the coordinates of the window. and use for the from and to parameters in the storyboard.
To get the new coordinates of the window, when the window opens or is moved, it calls the following function.
private void GetNewLocation()
{
RECT rect;
IntPtr windowHandle = new WindowInteropHelper(Window).Handle;
GetWindowRect(Process.GetCurrentProcess().MainWindowHandle, out rect);
WindowLeft = rect.Left;
WindowTop = rect.Top;
PropertyChanged(this, new PropertyChangedEventArgs(nameof(WindowLeft)));
PropertyChanged(this, new PropertyChangedEventArgs(nameof(WindowLeftTo)));
PropertyChanged(this, new PropertyChangedEventArgs(nameof(WindowTop)));
PropertyChanged(this, new PropertyChangedEventArgs(nameof(WindowTopTo)));
}
This function creates the rect using the new properties of the window and then changes the values of the variables used in the XAML corresponding to the change. The end result looks like this.
Balloons
Once the game finishes, balloons fly from the ground. To make this effect, several new methods were used.
Thread CreateBalloons = new Thread(BalloonRunner);
CreateBalloons.Start();
public void BalloonRunner()
{
sbkGameEngine.BalloonRun = true;
while (sbkGameEngine.BalloonRun)
{
if (Balloons)
{
Dispatcher.Invoke(() =>
CreateBalloon()
);
}
Dispatcher.Invoke(() =>
MoveBalloons()
);
Thread.SpinWait(1000000);
}
}
This thread calls the BalloonRunner method, which repeatedly calls CreateBalloon and MoveBalloon. However, it only calls CreateBalloon if the Balloon boolean is set to true, which is only done when the game finishes.
Interval -= 10;
if (Interval < 1)
{
...
Interval = RandomInt.Next(90, 150);
}
The above code uses the int Interval to only run the code every couple of calls. This allows for the balloons not to fill up the screen too quickly, and to take their time.
if (NumberOfBalloons == 99)
{
Balloons = false;
}
This code checks if the variable NumberOfBalloons is over 99, and if so, will end the thread by disabling the runner. NumberOfBalloons goes up by one every time a balloon is created.
int BalloonColor = RandomInt.Next(1, 6);
switch (BalloonColor)
{
case 1:
BalloonImage.ImageSource = new BitmapImage(new Uri("pack://application:,,,/PongsSBK;component/pictures/RedBalloon.png"));
break;
case 2:
BalloonImage.ImageSource = new BitmapImage(new Uri("pack://application:,,,/PongsSBK;component/pictures/OrangeBalloon.png"));
break;
case 3:
BalloonImage.ImageSource = new BitmapImage(new Uri("pack://application:,,,/PongsSBK;component/pictures/YellowBalloon.png"));
break;
case 4:
BalloonImage.ImageSource = new BitmapImage(new Uri("pack://application:,,,/PongsSBK;component/pictures/GreenBalloon.png"));
break;
case 5:
BalloonImage.ImageSource = new BitmapImage(new Uri("pack://application:,,,/PongsSBK;component/pictures/BlueBalloon.png"));
break;
}
The code uses the switch function to replace 5 if blocks. RandomInt creates a random integer from 1-5 and then the switch function checks which number it got and adds the corresponding balloon color to the brush BalloonColor.
Rectangle NewBalloon = new Rectangle
{
Tag = "Balloon",
Width = 37,
Height = 47,
Fill = BalloonImage
};
Canvas.SetTop(NewBalloon, (int)((System.Windows.Controls.Panel)Application.Current.MainWindow.Content).ActualHeight);
Canvas.SetLeft(NewBalloon, RandomInt.Next(0, (int)((System.Windows.Controls.Panel)Application.Current.MainWindow.Content).ActualWidth) - (NewBalloon.Width / 2));
Board.Children.Add(NewBalloon);
This code creates a rectangles using a fill of the balloon brush from before. This allows for the rectangle to display the image of the balloon, creating a balloon.
private void MoveBalloons()
{
foreach (var x in Board.Children.OfType<Rectangle>())
{
if ((string)x.Tag == "Balloon")
{
Canvas.SetTop(x, Canvas.GetTop(x) - speed);
Canvas.SetLeft(x, Canvas.GetLeft(x) - (1 * RandomInt.Next(-1, 2)));
}
if (Canvas.GetTop(x) < 0 - x.Height)
{
itemRemover.Add(x);
}
}
foreach (Rectangle x in itemRemover)
{
Board.Children.Remove(x);
}
}
The MoveBalloons methord uses the foreach function to move each balloon. It first checks every rectangle if it has the tag "balloon", which only the balloons have, and then it moves it up and randomly to the left. Once a balloon surpasses a height, it's added to list itemRemover. The next foreach function check every rectnagle if it's on top of the window, and deletes if it does.
Doing all off this creates balloons that fly when the game completes. The game completes when a person gets a certain amount of points, defaulted to 5.
Running the Application
v2.0 -- June 2 2024 - Added color pickers to settings and updated code, settings, and code explanations.
v2.2 -- June 16 2024 - Added screen shake and added more code explanations.