Recreating the Ping Pong game with Windows Forms

Previously, I had written a very basic entry about creating a Ping Pong game with Windows Forms in C # and Visual Basic, this time, I write a fully functional version in Windows Forms.

Creating and configuring the project

The first thing we will do is create the project, this will be created in the section Visual C # -> Windows Classic Desktop -> Windows Forms App (.NET Framework), as follows:

Creando un proyecto Windows Forms
Creando un proyecto Windows Forms

Configuring the form

We will place the main elements in our form.

NameSize
pbPlayer165, 160
pbPlayer265, 160
pbTitleScreenN/A

We put 2 Label elements, and we make the following adjustments:

NameText
lblScore10
lblScore20
N/A

In the end, we should have something like this:

Interfaz Gráfica del Ping Pong
Interfaz Gráfica del Ping Pong

Do not worry much at the moment by the positions, that we will do it under the code.

Defining the size of the container

We will define 2 constants as part of the class, one called ScreenWidth, and another called ScreenHeight, as follows:

    public partial class Form1 : Form
    {
        private const int ScreenWidth = 1024;
        private const int ScreenHeight = 768;
        public Form1()
        {
            InitializeComponent();
        }
    }

Inside the constructor, we will define the new size of our window, through the property ClientSize, to which we will pass the previously defined parameters.

        public Form1()
        {
            InitializeComponent();
            ClientSize = new Size(ScreenWidth, ScreenHeight);
        }

With this, if we run the application, we will have a window defined based on the vouchers assigned to the variables ScreenWidth and ScreenHeight.

Cargando los Sprites

We are interested now, to show the images in the corresponding PictureBox. To do this, we will create a new class called “GameItem”, which will be in charge of managing the speed, position and image of a “playable” element on the screen, and codify it as follows:

    public class GameItem
    {
        public Point Position { get; set; }
        public Point Velocity { get; set; }
        public PictureBox Texture { get; set; }
        public Point Origin
        {
            get
            {
                return new Point(Texture.Width / 2, Texture.Height / 2);
            }
        }
        public void Draw()
        {
            this.Texture.Location = new Point(this.Position.X - this.Origin.X,
                this.Position.Y - this.Origin.Y);
        }
    }

Similarly, we created another class called BallItem, which will inherit from our base class “GameItem”, and which will have a specific method to update the position of our ball.

[quads id=3]

    public class BallItem : GameItem
    {
        public void Update()
        {
            this.Position = new Point(this.Position.X + this.Velocity.X,
                this.Position.Y + this.Velocity.Y);
        }
    }

Returning to the form class, we created 3 variables, 2 of type GameItem, and 1 of type BallItem.

        private const int ScreenWidth = 1024;
        private const int ScreenHeight = 768;
        private GameItem _player1;
        private GameItem _player2;
        private BallItem _ball;

Inside the constructor, we subscribe to the Load event of the form and also call a method that will be called “Initialize”. As part of the Load event, we will call a method called “LoadGraphicsContent”, which we define as follows:

        public Form1()
        {
            InitializeComponent();
            ClientSize = new Size(ScreenWidth, ScreenHeight);
            Initialize();
            this.Load += Form1_Load;
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            LoadGraphicsContent();
        }
        private void LoadGraphicsContent()
        {
            pbPlayer1.Load("Paddle1.png");
            _player1.Texture = pbPlayer1;


            pbPlayer2.Load("Paddle2.png");
            _player2.Texture = pbPlayer2;

            pbBall.Load("Ball.png");
            _ball.Texture = pbBall;
        }
        private void Initialize()
        {            
            _player1 = new GameItem();

            _player2 = new GameItem
            {
                Position = new Point(ScreenWidth - 3, ScreenHeight / 2)
            };
            _ball = new BallItem
            {
                Velocity = new Point(2, 5)
            };
        }

Before running the project, we must download the sprites. These sprites need to be unzipped, and they should be left in the Debug folder.

Colocando los sprites
Colocando los sprites

With this, we will already have our sprites loaded in the form.

Sprites cargados
Sprites cargados

Detect Mouse and Draw Player 1

Before continuing, we will create 5 regions:

  • GamePlay Methods
  • Events
  • Engine Methods
  • Mechanics
  • Collisions
        #region GamePlay Methods
        #endregion
        #region Events
        #endregion
        #region Engine Methods
        #endregion
        #region Mechanics
        #endregion
        #region Collisions
        #endregion

We will place the form load event code in the event region, Initalize and LoadGraphicsContent within Engine Methods.

We add 2 timers as part of the main interface, and assign them the following:

NameEnabledInterval
UpdateTimerTrue16
DrawTimerTrue16

We have to create their respective event handler for the Tick event, which we will take to the event region to have the code sorted. Within the event handler of the click event of the UpdateTimer control, it is necessary to call an undefined method still called UpdateScene, which will contain the logic to carry out the update of the graphical interface. UpdateScene must go in the EngineMethods region.

        private void UpdateTimer_Tick(object sender, EventArgs e)
        {
            UpdateScene();
        }

We will define the UpdateScene method within the Engine Methods region, and from there we will call a new undefined method called UpdatePlayer.

        private void UpdateScene()
        {
            UpdatePlayer();
        }

Within the Mechanics region, we will define the new UpdatePlayer method, where we will define the code, first to control player 1. We begin by defining the constant X position that our sprite will have on stage, that is, because a paddle can only move from top to bottom, therefore, the position in X will always be the same. On the other hand, we will take the value in Y, according to the position of the mouse pointer. Subsequently, we will create a new position according to the values obtained. Finally, we make checks so that our paddle does not leave the limits of the stage.

        private void UpdatePlayer()
        {
            int playerX = 0 + 30;
            int playerY = PointToClient(MousePosition).Y;
            _player1.Position = new Point(playerX, playerY);

            if (_player1.Texture.Bottom >= ScreenHeight)
            {
                _player1.Position = new Point(playerX, ScreenHeight - _player1.Origin.Y - 1);
            }
            else if (_player1.Texture.Top <= 0)
            {
                _player1.Position = new Point(playerX, _player1.Origin.Y + 1);
            }
        }

Finally, we must redraw the control in its new position, so we have to code within the DrawTimer_Tick event handler, a new method called DrawScene.

        private void DrawTimer_Tick(object sender, EventArgs e)
        {
            DrawScene();
        }

DrawScene will be defined within Engine Methods, and will contain a call to the Draw method of the corresponding sprite.

        private void DrawScene()
        {
            _player1.Draw();
        }

If we execute, we will have the paddle of player 1 in action.

Detect keyboard and draw Player 2

Performing the detection of the keys in Windows Forms, is easier these days. To do this, you have to use 2 libraries that are not added by default: “PresentationCore” and “WindowsBase”. With this, we can implement the logic in the UpdatePlayer method, to detect if a key of our preference has been typed, in our case, we will use the keys ‘S' and ‘W', later, we validate if the paddle of player 2 is found on the stage. We must also define a class-level variable called “_currentY”, of type int.

The code to perform the check is as follows:

            if (Keyboard.IsKeyDown(Key.S))
            {
                if(_player2.Texture.Bottom >= ScreenHeight)
                {
                    _currentY -= 0;
                }
                else
                {
                    _currentY += 30;
                }
                _player2.Position = new Point(ScreenWidth - 30, _currentY);
            }
            else if (Keyboard.IsKeyDown(Key.W))
            {
                if (_player2.Texture.Top <= 0)
                {
                    _currentY += 0;
                }
                else
                {
                    _currentY -= 30;
                }

                int player2X = ScreenWidth - 30;
                int player2Y = _currentY;
                _player2.Position = new Point(player2X, player2Y);

            }

Finally, we must not forget to update the drawing of the paddle, in the DrawScene method.

        private void DrawScene()
        {
            _player1.Draw();
            _player2.Draw();
        }

Moving the ball

To move the ball, we must define 3 new methods within the “Mechanics” region, which will be: “ResetBall”, “GenerateBallX” and “GenerateBallY”.

        private void ResetBall()
        {
            _level = 7;
            int velocityY = GenerateBallY();
            int velocityX = GenerateBallX();

            _ball.Position = new Point(ScreenWidth / 2, ScreenHeight / 2);
            _ball.Velocity = new Point(velocityX, velocityY);

            _currentBallX = velocityX;
        }
        private int GenerateBallX()
        {
            _level += 1;
            int velocityX = _level;
            if (_random.Next(2) == 0)
            {
                velocityX *= -1;
            }
            return velocityX;
        }

        private int GenerateBallY()
        {
            _level += (int).5;
            int velocityY = _random.Next(0, _level);
            if (_random.Next(2) == 0)
            {
                velocityY *= -1;
            }
            return velocityY;
        }

So that it does not mark errors with the variables, we will add the missing ones as part of the class.

        private int _level = 7;
        private int _currentBallX;
        private Random _random;

Finally, it is necessary to update the position of the ball, and to draw it through the corresponding methods.

        private void UpdateScene()
        {
            UpdatePlayer();
            _ball.Update();
        }
        private void DrawScene()
        {
            _player1.Draw();
            _player2.Draw();
            _ball.Draw();
        }

Validating the clash of the ball against the paddles and walls

Validating the clash against the paddles

To perform these validations, add 4 points to the sprites, in order to verify if the ball has hit a paddle. For this, we will modify the GameItem class, as follows.

        public Point LeftUpCorner
        {
            get { return new Point(Position.X - Origin.X, Position.Y - Origin.Y); }
        }

        public Point RightUpCorner
        {
            get { return new Point(Position.X + Origin.X, Position.Y - Origin.Y); }
        }
        public Point LeftBottomCorner
        {
            get { return new Point(Position.X - Origin.X, Position.Y + Origin.Y); }
        }

        public Point RightBottomCorner
        {
            get { return new Point(Position.X + Origin.X, Position.Y + Origin.Y); }
        }

And we'll add the CheckPaddleCollisions method to the form class.

        private void CheckPaddleCollision()
        {
            if (_ball.LeftUpCorner.X < _player1.RightUpCorner.X &&
                _ball.LeftBottomCorner.Y > _player1.RightUpCorner.Y &&
                _ball.LeftUpCorner.Y < _player1.RightBottomCorner.Y)
            {
                _currentBallX = GenerateBallX();
                if (_currentBallX < 0)
                {
                    _currentBallX *= -1;
                }
                _ball.Velocity = new Point(_currentBallX, GenerateBallY());
            }

            if (_ball.RightUpCorner.X > _player2.LeftUpCorner.X &&
                _ball.RightBottomCorner.Y > _player2.LeftUpCorner.Y &&
                _ball.RightUpCorner.Y < _player2.LeftBottomCorner.Y)
            {
                _currentBallX = GenerateBallX();
                if (_currentBallX > 0)
                {
                    _currentBallX *= -1;
                }
                _ball.Velocity = new Point(_currentBallX, GenerateBallY());
            }
        }

Finally, we called to call the new method through UpdateScene:

        private void UpdateScene()
        {
            UpdatePlayer();
            _ball.Update();
            CheckPaddleCollision();
        }

Validating the shock against the upper and lower walls

For this validation, we will create a new method called “CheckWallCollision”, which should be as follows:

        private void CheckWallCollision()
        {
            if (pbBall.Bottom >= ScreenHeight)
            {
                _ball.Velocity = new Point(_currentBallX, -BaseBallSpeed);
            }
            else if (pbBall.Top <= 0)
            {
                _ball.Velocity = new Point(_currentBallX, BaseBallSpeed);
            }
        }

Again, add the variable “BaseBallSpeed” as part of the class variables.

        private int _currentBallX;
        private Random _random;
        private const int BaseBallSpeed = 2;

And again, we call the check from the UpdateScene method.

        private void UpdateScene()
        {
            UpdatePlayer();
            _ball.Update();
            CheckPaddleCollision();
            CheckWallCollision();
        }

Validating if the ball has left the scene

For this, you have to validate where the position of the ball is through the following code.

        private void CheckWallOut()
        {
            if (pbBall.Left < 0)
            {
                ResetBall();
                _scorePlayer2 += 1;
                lblScore2.Text = _scorePlayer2.ToString();
            }
            else if (pbBall.Right > ScreenWidth)
            {
                ResetBall();
                _scorePlayer1 += 1;
                lblScore1.Text = _scorePlayer1.ToString();
            }
        }

You have to add the variables that will store the players' scores.

        private int _scorePlayer1;
        private int _scorePlayer2;

Finally, we call the method from UpdateScene.

        private void UpdateScene()
        {
            UpdatePlayer();
            _ball.Update();
            CheckPaddleCollision();
            CheckWallCollision();
            CheckWallOut();
        }

Code in Github

With this we reached the end of the entry, the code is still quite improving, I just wanted to update this entry, because I saw that there were many people interested in the subject. I leave the code in Github with a piece of code that I have not shown here.

Regards!

Deja un comentario

Tu dirección de correo electrónico no será publicada.