Click here to Skip to main content
15,910,234 members
Articles / Desktop Programming / WPF

WPF based exciting Pong Game

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
2 Jun 2024MIT5 min read 15.6K   18   10
This is a multi player Pong game written in C#/ WPF using Visual Studio 2022
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.

Image 1
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.

Image 2
Figure 2 - Directions page from game

Settings

In this game, there are also customizable settings such as…

Image 3
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:Image 4

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

Image 5
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(/*object? sender, EventArgs e*/)
        {
            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

  1. Go to the GitHub link: click here
  2. Go to the folder labeled "Quick Demo"
  3. Download the "ZippedDemo.zip"
  4. Unzip the file
  5. 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! 

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionComputer Player Pin
Hyland Computer Systems24-May-24 4:09
Hyland Computer Systems24-May-24 4:09 
AnswerRe: Computer Player Pin
Sbk12324-May-24 9:35
Sbk12324-May-24 9:35 
GeneralMy vote of 5 Pin
theDiscountCodes24-May-24 1:42
professionaltheDiscountCodes24-May-24 1:42 
GeneralRe: My vote of 5 Pin
Sbk12324-May-24 9:24
Sbk12324-May-24 9:24 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA22-May-24 21:12
professionalȘtefan-Mihai MOGA22-May-24 21:12 
GeneralRe: My vote of 5 Pin
Sbk12323-May-24 9:42
Sbk12323-May-24 9:42 
QuestionMy vote of 5 Pin
Peter Huber SG19-May-24 23:58
mvaPeter Huber SG19-May-24 23:58 
AnswerRe: My vote of 5 Pin
Sbk12320-May-24 12:35
Sbk12320-May-24 12:35 
GeneralRe: My vote of 5 Pin
Peter Huber SG20-May-24 15:45
mvaPeter Huber SG20-May-24 15:45 
GeneralRe: My vote of 5 Pin
Sbk12322-May-24 10:59
Sbk12322-May-24 10:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.