Skip to content

Latest commit

 

History

History
837 lines (456 loc) · 28.1 KB

README.md

File metadata and controls

837 lines (456 loc) · 28.1 KB

P🏓NG

011

P🏓NG is a simulation of Table Tennis, a recreational activity and an Olympic sport since 1988, is also known by the term "ping-pong" or just "pong". This project is special to me as it was developed with my son, Joey, making it a true father-and-son collaboration.

This repository is designed to help new game developers learn the fundamentals of game programming and design through a classic game.

Features

  • Classic Gameplay: Experience the timeless fun of pong with modern enhancements.
  • Keyboard and Controller Support: Play using your keyboard ⌨️ or Xbox controllers 🎮 , complete with vibration feedback.
  • Resizable and Pausable: Enjoy a flexible gameplay experience that can be paused at any time.
  • Single and Multiplayer Modes: Challenge yourself against a computer player or compete with friends.

Learning Objectives

  • Understand the basics of game mechanics and physics.
  • Gain hands-on experience with VB.NET and game development concepts.
  • Learn how to implement user input handling, game states, and sound effects.


Keyboard Controls | XBox Controllers | Code Walk Through | Top


Getting Started

To begin, you'll need to set up your development environment and clone the repository to get the game files.

🛠️ Set Up Your Environment

014

Visual Studio is an Integrated Development Environment (IDE) created by Microsoft.

It is a powerful software development tool that provides a comprehensive set of features and tools for building a wide range of applications.

This is the IDE I use to make P🏓NG and I recommend that you use.

005

Grab Visual Studio Community—it's free and perfect for indie devs.

Install Visual Studio from here: https://visualstudio.microsoft.com/downloads/ and include the .NET Desktop Development workload.

🧬📦 Clone the Repository

Click the "Code" button.

Copy the repository's URL.

010

Open Visual Studio.

Click "Clone a repository".

006

Paste the repository URL into the location field.

Click the "Clone" button.

008

Once the cloning process is complete, you will have your own local copy of the game that you can run and modify on your computer.

Dive into the code, experiment, and enhance the game while learning valuable programming skills!


Getting Started | XBox Controllers | Code Walk Through | Top


⌨️ Keyboard Controls

To play using your keyboard, use the following controls:

  • Player 1 ( 🏓 Left Paddle )

    • W : Move Up
    • S : Move Down
  • Player 2 ( Right Paddle 🏓 )

    • Up Arrow ⬆️ : Move Up
    • Down Arrow ⬇️ : Move Down
  • Pause/Resume Game

    • Pause : Pause or Resume the game
    • Escape : From pause screen Resets the game

Getting Started | Code Walk Through | Top


🎮 Xbox Controllers

To play using your Xbox controllers, use the following controls:

  • 🏓 Paddle Movement 🏓

    • Thumbstick Up or DPad Up : Move Up
    • Thumbstick Down or DPad Down : Move Down
  • Pause/Resume Game

    • Start : Pause or Resume the game
    • Back : From pause screen Resets the game

016


Getting Started | Keyboard Controls | XBox Controllers | Top


📄 Code Walk Through

Here, we'll take a deep dive into the game's code and architecture, guiding you through the essential components that make up this classic table tennis simulation. Whether you're a beginner looking to understand the fundamentals of game programming or an experienced developer seeking to enhance your skills, this walkthrough will provide you with valuable insights.

We'll explore key areas such as game mechanics, user input handling, collision detection, and rendering graphics. Each section will break down the code into manageable parts, explaining the purpose and functionality behind them. By the end of this walkthrough, you'll have a solid understanding of how to create and modify a game like P🏓NG, empowering you to implement your own ideas and features. Let’s get started!


Imports and Class Declaration

Enumerations

Game Object Structure

Game State Variables

Game Loop

Update Game Logic

Input Handling

Collision Detection

Game State Transitions

Rendering Graphics

Sound Management


Imports and Class Declaration

Imports System.Threading
Imports System.Numerics
Imports System.ComponentModel
Imports System.Runtime.InteropServices
Imports System.Text
Imports System.IO

Public Class Form1
  • The code begins by importing necessary namespaces for threading, mathematical operations (like vectors), interop services for using unmanaged code, and file handling.
  • The class Form1 is declared, which will contain all the game logic.

Getting Started | Keyboard Controls | XBox Controllers | Code Walk Through | Top


Enumerations

Private Enum GameStateEnum
    StartScreen
    Instructions
    Serve
    Playing
    EndScreen
    Pause
End Enum
  • GameStateEnum defines different states of the game (e.g., Start Screen, Playing, End Screen).

Getting Started | Keyboard Controls | XBox Controllers | Code Walk Through | Top


Game Object Structure

Private Structure GameObject
    Public Position As Vector2
    Public Acceleration As Vector2
    Public Velocity As Vector2
    Public MaxVelocity As Vector2
    Public Rect As Rectangle
End Structure
  • GameObject structure is defined to represent game entities like the ball and paddles. It includes properties for position, acceleration, velocity, maximum velocity, and the rectangle that represents its dimensions.

Getting Started | Keyboard Controls | XBox Controllers | Code Walk Through | Top


Game State Variables

Private GameState As GameStateEnum = GameStateEnum.StartScreen
Private Serving As ServeStateEnum = ServeStateEnum.LeftPaddle
Private ServSpeed As Single = 500
Private Winner As WinStateEnum = WinStateEnum.LeftPaddle
Private NumberOfPlayers As Integer = 2
  • These variables track the current game state, which paddle is serving, the speed of the serve, the winner, and the number of players.

Getting Started | Keyboard Controls | XBox Controllers | Code Walk Through | Top


Game Loop

The game loop is a critical component of the P🏓NG game, ensuring that the game runs smoothly and updates in real-time. It consists of two main subroutines: UpdateGame and DrawGame.

  • UpdateGame: This subroutine is called at regular intervals to update the game's state. It processes input, updates game objects, checks for collisions, and transitions between game states. The loop continuously calls this subroutine to ensure that the game responds to player actions and progresses logically.

    Private Sub UpdateGame()
        Select Case GameState
            Case GameStateEnum.Playing
                UpdatePlaying()
            Case GameStateEnum.StartScreen
                UpdateStartScreen()
            Case GameStateEnum.Instructions
                UpdateInstructions()
            Case GameStateEnum.Serve
                UpdateServe()
            Case GameStateEnum.Pause
                UpdatePause()
            Case GameStateEnum.EndScreen
                UpdateEndScreen()
        End Select
    End Sub
  • DrawGame: This subroutine handles rendering the game's graphics. It draws all the game elements, such as paddles, the ball, and scores, onto the screen. The DrawGame subroutine is called after UpdateGame to ensure that the latest game state is displayed.

    Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
        DrawGame() ' Draw all game elements
        Buffer.Render(e.Graphics) ' Show buffer on form
        Buffer.Dispose() ' Release memory used by buffer
        Buffer = Nothing ' Create new buffer
        Buffer = Context.Allocate(CreateGraphics(), ClientRectangle)
        UpdateFrameCounter() ' Update frame counter
    End Sub
  • Repeat: The game loop continuously repeats the UpdateGame and DrawGame subroutines to maintain a seamless gameplay experience. This repetition ensures that the game remains responsive to user inputs and provides a fluid visual experience.

Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick

    UpdateGame()

    Refresh()

End Sub
  • This subroutine is called at regular intervals (set by a timer). It updates the game state and refreshes the display.

Code Breakdown

Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick: This subroutine is executed at regular intervals, controlled by a timer named Timer1. The Tick event is triggered by the timer at specified intervals. sender is the source of the event, and e is the event data.

UpdateGame(): This line calls the UpdateGame method, which contains the logic for updating the game's state. This might include moving paddles, updating scores, checking for collisions, etc.

Refresh(): This line refreshes the display, causing the form to redraw its contents. It ensures that any changes made by UpdateGame are visually updated on the screen.

This subroutine keeps the game running smoothly by continuously updating the game state and refreshing the display at regular intervals. Essentially, it's the heartbeat of your game loop.

By effectively managing the game loop, P🏓NG achieves smooth gameplay and a responsive user experience.


Getting Started | Keyboard Controls | XBox Controllers | Code Walk Through | Top


Update Game Logic

    Private Sub UpdateGame()

        Select Case GameState

            Case GameStateEnum.Playing

                UpdatePlaying()

            Case GameStateEnum.StartScreen

                UpdateStartScreen()

            Case GameStateEnum.Instructions

                UpdateInstructions()

            Case GameStateEnum.Serve

                UpdateServe()

            Case GameStateEnum.Pause

                UpdatePause()

            Case GameStateEnum.EndScreen

                UpdateEndScreen()

        End Select

    End Sub
  • The UpdateGame method uses a Select Case statement to determine what actions to take based on the current game state. It handles input, updates game objects, and checks for game conditions.

Getting Started | Keyboard Controls | XBox Controllers | Code Walk Through | Top


Input Handling

    Private Sub UpdateLeftPaddleKeyboard()

        If WKeyDown Then

            MoveLeftPaddleUp()

        ElseIf SKeyDown Then

            MoveLeftPaddleDown()

        Else

            If Not Connected(0) Then

                DecelerateLeftPaddle()

                If ApplyLeftPaddleEnglish Then

                    ApplyLeftPaddleEnglish = False

                    'Send ball to the right.
                    Ball.Velocity.X = ServSpeed
                    Ball.Velocity.Y = 0

                End If

            End If

        End If

    End Sub
  • This method checks if specific keys are pressed (W/S for the left paddle) and moves the paddle accordingly.

Code Breakdown

Private Sub UpdateLeftPaddleKeyboard(): This line defines a private subroutine named UpdateLeftPaddleKeyboard. It's responsible for updating the movement of the left paddle based on keyboard input.

If WKeyDown Then: Checks if the W key is pressed. If true, it calls MoveLeftPaddleUp() to move the left paddle up.

ElseIf SKeyDown Then: If the W key is not pressed, it checks if the S key is pressed. If true, it calls MoveLeftPaddleDown() to move the left paddle down.

Else: If neither the W key nor the S key is pressed, it goes into this block.

If Not Connected(0) Then: Checks if the first controller is not connected. If true, it proceeds with the following steps.

DecelerateLeftPaddle(): Calls a function to slow down the left paddle.

If ApplyLeftPaddleEnglish Then: Checks if the flag to apply spin (English) to the ball is set. If true, it proceeds with the following steps.

ApplyLeftPaddleEnglish = False: Resets the flag for applying spin.

Ball.Velocity.X = ServSpeed: Sets the ball's horizontal velocity to a serving speed.

Ball.Velocity.Y = 0: Sets the ball's vertical velocity to 0, ensuring it moves straight.

This subroutine effectively manages the left paddle’s movement based on keyboard input, including handling cases where the controller isn't connected.


Getting Started | Keyboard Controls | XBox Controllers | Code Walk Through | Top


Collision Detection

 Private Sub CheckForLeftPaddleHits()

     ' Did the ball hit the left paddle?
     If Ball.Rect.IntersectsWith(LeftPaddle.Rect) Then
         ' Yes, the ball did hit the left paddle.

         PlaySound("hit")

         ' Stop the ball's movement.
         Ball.Velocity.X = 0
         Ball.Velocity.Y = 0

         ' Set the ball's position to the right edge of the left paddle, plus an extra 5 pixels.
         Ball.Rect.X = LeftPaddle.Rect.X + LeftPaddle.Rect.Width + 5

         ' Update the ball's position to match the new rectangle position.
         Ball.Position.X = Ball.Rect.X

         ' Set a flag to indicate that the ball should have some spin (English) applied.
         ApplyLeftPaddleEnglish = True

         ' Trigger a vibration effect on the left side of the left paddle controller "0".
         VibrateLeft(0, 42000)

     End If

 End Sub
  • This method checks if the ball intersects with the left paddle, handling the logic for bouncing the ball back and updating its velocity.

Code Breakdown

Private Sub CheckForLeftPaddleHits(): This line starts a subroutine called CheckForLeftPaddleHits. It's a private subroutine, so it's only accessible within the current module or class.

If Ball.Rect.IntersectsWith(LeftPaddle.Rect) Then: This line checks if the ball's rectangle intersects with the left paddle's rectangle. Essentially, it checks if the ball has hit the left paddle.

PlaySound("hit"): If the ball hits the left paddle, this line plays a sound named "hit" to provide audio feedback.

Ball.Velocity.X = 0 and Ball.Velocity.Y = 0: These lines stop the ball's movement by setting its horizontal and vertical velocity to 0.

Ball.Rect.X = LeftPaddle.Rect.X + LeftPaddle.Rect.Width + 5: This line sets the ball's position to the right edge of the left paddle, plus an additional 5 pixels, to ensure the ball doesn't overlap the paddle.

Ball.Position.X = Ball.Rect.X: This line updates the ball's position to match the new rectangle position, keeping everything in sync.

ApplyLeftPaddleEnglish = True: This line sets a flag indicating that the ball should have some spin (English) applied, possibly affecting its movement after hitting the paddle.

VibrateLeft(0, 42000): This line triggers a vibration effect on the left side of the left paddle controller, adding a tactile response to the hit.

All these steps together create a comprehensive check for collisions between the ball and the left paddle and apply appropriate responses like stopping the ball, playing sounds, and updating positions.


     Private Sub CheckForRightPaddleHits()

        ' Did the ball hit the right paddle?
        If Ball.Rect.IntersectsWith(RightPaddle.Rect) Then
            ' Yes, the ball did hit the right paddle.

            PlaySound("hit")

            ' Stop the ball's movement.
            Ball.Velocity.X = 0
            Ball.Velocity.Y = 0

            ' Moves the ball to the left of the right paddle, ensuring there’s a 5-pixel gap.
            Ball.Rect.X = RightPaddle.Rect.X - (Ball.Rect.Width + 5)

            ' Update the ball's position to match the new rectangle position.
            Ball.Position.X = Ball.Rect.X

            ' Is the number of players two?
            If NumberOfPlayers = 2 Then
                ' Yes, the number of players is two.

                ' Set a flag to indicate that the ball should have some spin (English) applied.
                ApplyRightPaddleEnglish = True

                ' Trigger a vibration effect on the left side of the right paddle controller "1".
                VibrateLeft(1, 42000)

            Else
                ' No, the number of players is not two.

                DoComputerPlayerEnglish()

            End If

        End If

    End Sub
  • This method checks if the ball intersects with the right paddle, handling the logic for bouncing the ball back and updating its velocity.

Code Breakdown

Private Sub CheckForRightPaddleHits(): Starts a subroutine to check if the ball hits the right paddle.

If Ball.Rect.IntersectsWith(RightPaddle.Rect) Then: Checks if the ball’s rectangle intersects with the right paddle’s rectangle.

PlaySound("hit"): Plays the sound named "hit" when the ball hits the paddle.

Ball.Velocity.X = 0 and Ball.Velocity.Y = 0: Stops the ball's movement by setting its horizontal and vertical velocity to 0.

Ball.Rect.X = RightPaddle.Rect.X - (Ball.Rect.Width + 5): Moves the ball to the left of the right paddle, leaving a 5-pixel gap.

Ball.Position.X = Ball.Rect.X: Updates the ball’s position to match the new rectangle position.

If NumberOfPlayers = 2 Then: Checks if there are two players.

ApplyRightPaddleEnglish = True: Sets a flag to apply spin (English) to the ball.

VibrateLeft(1, 42000): Triggers a vibration effect on the right paddle’s controller.

DoComputerPlayerEnglish(): Calls a method for the computer player’s spin if there isn’t a second player.


Getting Started | Keyboard Controls | XBox Controllers | Code Walk Through | Top


Game State Transitions

    Private Sub CheckforEndGame()

        ' Did left paddle reach winning score?
        If LeftPaddleScore >= 10 Then
            ' Yes, left paddle did reach winning score.

            ' Set winner to left paddle.
            Winner = WinStateEnum.LeftPaddle

            ' Reset the frame counter.
            FlashCount = 0

            ' Change game state to end screen.
            GameState = GameStateEnum.EndScreen

            PlayWinningSound()

        End If

        ' Did right paddle reach winning score?
        If RightPaddleScore >= 10 Then
            ' Yes, right paddle did reach winning score.

            ' Set winner to right paddle.
            Winner = WinStateEnum.RightPaddle

            ' Reset frame counter.
            FlashCount = 0

            ' Change game state to end screen.
            GameState = GameStateEnum.EndScreen

            PlayWinningSound()

        End If

    End Sub
  • This method checks if a player has reached the winning score and transitions the game state to the end screen if so.

Code Breakdown

Private Sub CheckforEndGame(): This defines a private subroutine named CheckforEndGame. It’s responsible for checking if any player has reached the winning score.

If LeftPaddleScore >= 10 Then: Checks if the left paddle’s score is 10 or more.

Winner = WinStateEnum.LeftPaddle: If the left paddle has reached the winning score, it sets the winner to the left paddle.

FlashCount = 0: Resets the frame counter, for the end game animation.

GameState = GameStateEnum.EndScreen: Changes the game state to the end screen, where the end game events are handled.

PlayWinningSound(): Plays a sound to celebrate the win.

If RightPaddleScore >= 10 Then: Similarly checks if the right paddle has reached the winning score.

Winner = WinStateEnum.RightPaddle: Sets the winner to the right paddle if it has reached the winning score.

FlashCount = 0: Resets the frame counter again.

GameState = GameStateEnum.EndScreen: Changes the game state to the end screen.

PlayWinningSound(): Plays the winning sound.

This method ensures the game transitions smoothly to the end screen when either player reaches the winning score.


Getting Started | Keyboard Controls | XBox Controllers | Code Walk Through | Top


Rendering Graphics

    Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)

        DrawGame()

        'Show buffer on form.
        Buffer.Render(e.Graphics)

        'Release memory used by buffer.
        Buffer.Dispose()
        Buffer = Nothing

        'Create new buffer.
        Buffer = Context.Allocate(CreateGraphics(), ClientRectangle)

        UpdateFrameCounter()

    End Sub
  • This method is responsible for rendering the game graphics. It calls the DrawGame method to draw all game elements and then renders the buffer to the screen.

Code Breakdown

Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs): This line starts the OnPaint method, which is responsible for painting the game's graphics. Overrides means it replaces the base class's OnPaint method. e is a PaintEventArgs object that gives you access to the graphics object.

DrawGame(): This line calls a method named DrawGame. This method contains the logic for drawing all the game elements like paddles, balls, and scores.

Buffer.Render(e.Graphics): This line tells the game to display the image from a buffer (a temporary storage) onto the screen using e.Graphics.

Buffer.Dispose(): This line frees up memory used by the buffer. It's a cleanup step to ensure your program doesn’t use more memory than it needs.

Buffer = Nothing: This line sets the buffer to Nothing (null), ensuring it’s completely reset and no longer in use.

Buffer = Context.Allocate(CreateGraphics(), ClientRectangle): This line creates a new buffer. Context.Allocate sets up the buffer to match the size of the game's window (ClientRectangle).

UpdateFrameCounter(): This line updates a counter that keeps track of how many frames have been displayed. It's useful for performance monitoring or displaying the frame rate.


Getting Started | Keyboard Controls | XBox Controllers | Code Walk Through | Top


Sound Management

Private Function AddSound(SoundName As String, FilePath As String) As Boolean

    If Not String.IsNullOrWhiteSpace(SoundName) AndAlso IO.File.Exists(FilePath) Then

        Dim CommandOpen As String = $"open ""{FilePath}"" alias {SoundName}"

        Dim ReturnString As New StringBuilder(128)

        If mciSendStringW(CommandOpen, ReturnString, 0, IntPtr.Zero) = 0 Then

            Array.Resize(Sounds, Sounds.Length + 1)

            Sounds(Sounds.Length - 1) = SoundName

            Return True

        End If

    End If

    Return False

End Function
  • Loading Sounds: The game loads sound files from specified paths and associates them with names for easy reference.

Code Breakdown

Private Function AddSound(SoundName As String, FilePath As String) As Boolean: This line defines a function named AddSound. It takes two parameters: SoundName and FilePath. The function returns a Boolean value (True or False).

If Not String.IsNullOrWhiteSpace(SoundName) AndAlso IO.File.Exists(FilePath) Then: This line checks if SoundName is not empty and if the file at FilePath exists.

Dim CommandOpen As String = $"open ""{FilePath}"" alias {SoundName}": This line creates a command string to open the sound file with a specific alias name.

Dim ReturnString As New StringBuilder(128): This line creates a new StringBuilder object with a capacity of 128 characters to store the response from the command.

If mciSendStringW(CommandOpen, ReturnString, 0, IntPtr.Zero) = 0 Then: This line sends the command to open the sound file. If the command is successful (returns 0), it proceeds to the next steps.

Array.Resize(Sounds, Sounds.Length + 1): This line resizes the Sounds array to accommodate one more element.

Sounds(Sounds.Length - 1) = SoundName: This line adds the SoundName to the last position of the Sounds array.

Return True: This line returns True, indicating that the sound was successfully added.

End If: This line marks the end of the If block.

Return False: This line returns False if the sound could not be added.

End Function: This line marks the end of the function.

This function checks if the sound file exists, opens it, and adds it to an array of sounds for your game. If any step fails, it returns False.


Private Function PlaySound(SoundName As String) As Boolean

    If Sounds IsNot Nothing AndAlso Sounds.Contains(SoundName) Then

        Dim CommandSeekToStart As String = $"seek {SoundName} to start"

        Dim ReturnString As New StringBuilder(128)

        mciSendStringW(CommandSeekToStart, ReturnString, 0, IntPtr.Zero)

        Dim CommandPlay As String = $"play {SoundName} notify"

        If mciSendStringW(CommandPlay, ReturnString, 0, Me.Handle) = 0 Then

            Return True

        End If

    End If

    Return False

End Function
  • Playing Sounds: This function is provided to play specific sounds at appropriate game events (e.g., ball hits, scoring).

  • Volume Control: The volume of sounds can be adjusted, allowing for a customizable audio experience.

  • Looping Sounds: Background music can be looped to create an immersive atmosphere.


Getting Started | Keyboard Controls | XBox Controllers | Code Walk Through | Top


019

https://youtube.com/shorts/_W-7EHnru2w?si=H6w0GIQnZGHYtB_t


A Special Thanks

A heartfelt thank you to my son, Joey, for his invaluable help in developing P🏓NG. This project is a testament to our teamwork and shared passion for coding! 017


Join the Conversation

I invite all players and developers to share their experiences, thoughts, and feedback on P🏓NG. Your insights are essential for making this project even better. Let’s collaborate and create something amazing together!

You can explore my work and projects on my GitHub: github.com/JoeLumbley and check out my tutorials on my YouTube channel: youtube.com/@codewithjoe6074.


Happy coding!