A modern, full‑screen remake of the classic Pong arcade game. Rebuilt from scratch with smooth physics, glowing motion trails, dynamic paddle spin, an AI opponent, animated menus, and a fully‑featured audio system.
This project isn’t just Pong. It’s a compact 2D game engine demonstrating real‑time rendering, delta‑time physics, state‑driven UI, and responsive design.
- Classic Gameplay: Experience the timeless fun of ping-pong with modern enhancements, including smooth animations and responsive controls.
- Keyboard Support: Play using your keyboard.
- Resizable and Pausable: Enjoy a flexible gameplay experience that can be paused at any time, allowing players to take breaks without losing progress.
- Single and Multiplayer Modes: Challenge yourself against a computer player or compete with friends, making the game versatile for different play styles.
- Understand the basics of game mechanics and physics, including how to simulate movement and collisions.
- Gain hands-on experience with game development concepts, such as state management and event handling.
- Learn how to implement user input handling, game states, sound effects, and graphical rendering.
Top | Keyboard Controls | AudioPlayer Module | Cleanuptick Walkthrough | Fixed Audio Playback Issues
| Key | Action |
|---|---|
| F11 | Toggle fullscreen mode |
| F | Toggle fullscreen mode (secondary shortcut) |
| Escape | Context‑sensitive: fullscreen exit, pause, or return to start screen |
| Key | Action |
|---|---|
| Up / W | Move selection up |
| Down / S | Move selection down |
| 1 / NumPad1 | Select “1‑Player” |
| 2 / NumPad2 | Select “2‑Player” |
| Space / Enter / S | Confirm selection and start match |
| Escape | Exit the game |
| Key | Action |
|---|---|
| W | Move left paddle up |
| S | Move left paddle down |
| Up Arrow | Move right paddle up (2‑Player mode only) |
| Down Arrow | Move right paddle down (2‑Player mode only) |
| P | Pause the game |
| Pause / Break | Pause the game |
| MediaPlayPause | Pause the game |
| Escape | Pause the game (windowed mode) or exit fullscreen |
| Key | Action |
|---|---|
| Up Arrow | Move menu selection up |
| Down Arrow | Move menu selection down |
| Enter / Space | Activate selected menu option |
| P | Resume game |
| Pause / Break | Resume game |
| MediaPlayPause | Resume game |
| R | Resume game |
| N | Start a new match |
| Q | Quit to Start Screen |
| Escape | Quit to Start Screen |
| Key | Action |
|---|---|
| Space / Enter | Return to Start Screen |
Top | Keyboard Controls | AudioPlayer Module | Cleanuptick Walkthrough | Fixed Audio Playback Issues
Why Our PONG Game’s Sound Became Garbled Over Time
During development, we discovered that our PONG game’s audio would slowly degrade during long play sessions. After 10–20 minutes, sound effects became distorted, crackly, or randomly cut off. This wasn’t a bug in our game logic. It was a limitation of the legacy Windows MCI (winmm.dll) audio subsystem.
We investigated the root causes and implemented a full set of fixes that stabilized audio playback without replacing the entire audio engine.
MCI is a 1990s-era multimedia API, and while it still works, it has several well‑documented failure modes. Our game happened to hit all of them.
Our overlapping playback system opened 24 copies of each sound:
Private ReadOnly OverlapSuffixes As String() =
Enumerable.Range(0, 24).Select(Function(i) Chr(Asc("A") + i).ToString()).ToArray()Calling:
AddOverlapping("select", "select.wav")created:
selectA
selectB
selectC
...
selectX
selectY
selectZ
With ~10 sound effects, this meant 240 active audio devices.
MCI cannot handle this load. Buffers corrupt, handles leak, and playback becomes garbled.
We relied on:
status alias modeBut MCI often reports:
not ready
stopped
even when the device is still internally active.
This caused our overlapping logic:
If Not IsPlaying(aliasName) Then PlaySound(aliasName)to reuse aliases that were still busy → corrupted audio buffers.
We simulated mixing by opening many copies of the same WAV.
MCI was never designed for this, and rapid overlapping playback (menu navigation, paddle hits, ball bounces) overwhelmed the mixer.
Even after calling:
Send("close alias")Windows often keeps the underlying device open until the process exits.
Over time, this leads to distortion and playback failure.
Our biggest culprit:
Public Sub PlayOverlapping(baseName As String)
For Each suffix In OverlapSuffixes
Dim aliasName = Normalize(baseName & suffix)
If Not IsPlaying(aliasName) Then
PlaySound(aliasName)
Exit Sub
End If
Next
End SubRapid sound effects burned through aliases faster than MCI could release them.
We only closed aliases when the game exited.
After 10–20 minutes, MCI was already corrupted.
Commands like:
play alias notify
pause alias notifyare known to cause buffer corruption unless you handle MCI notifications. We didn’t.
We lowered the channel count from 24 → 8:
Private ReadOnly OverlapSuffixes As String() =
{"A", "B", "C", "D", "E", "F", "G", "H"}This alone dramatically reduced corruption.
We replaced:
play soundName notifywith:
play soundNameThis eliminated a major source of buffer corruption.
We added:
stopbeforeseek- alias health checks
- corruption recovery
- stable overlapping playback logic
This prevented buffer reuse issues.
We implemented a timer that:
- Saves alias file paths and volume
- Stops all aliases
- Closes all aliases
- Reopens them fresh
- Restores volume
- Restarts looping sounds
This prevents long‑term corruption and keeps MCI stable indefinitely.
After applying all fixes:
- No more garbled audio
- No more crackling after long sessions
- No more stuck aliases
- No more corrupted overlapping playback
- No need to replace the audio engine
Our PONG game now runs for hours with stable sound.
Top | Keyboard Controls | AudioPlayer Module | Cleanuptick Walkthrough | Fixed: Audio Playback Issues
The AudioPlayer module is our custom sound engine built on top of the legacy Windows MCI (Media Control Interface) subsystem.
Although MCI is decades old, this module wraps it in a safe, stable, modernized interface suitable for real‑time game audio in our PONG project.
This document explains what the module does, how it works, and why certain design decisions were made, including the fixes that prevent long‑session audio corruption.
The module provides:
- Loading WAV or MP3 files into named aliases
- Playing, looping, pausing, and stopping sounds
- Overlapping playback (multiple rapid sound effects)
- Volume control
- Automatic cleanup every 5 minutes
- Full recovery of alias state (file path + volume)
- Loop restoration after cleanup
- Safe MCI usage that avoids corruption
This module is globally accessible and requires no instantiation.
We bind directly to mciSendStringW, the Unicode version of the MCI command interface.
All audio operations are performed by sending text commands like:
open
play
pause
stop
status
close
This is the foundation of the module.
We maintain:
- A
HashSetof active aliases - A fixed set of suffixes (
A–H) for overlapping playback - A cleanup timer that refreshes MCI state every 5 minutes
The overlapping suffixes allow rapid sound effects (menu clicks, paddle hits) without corrupting MCI.
The cleanup timer is created only when the first audio command is sent.
This avoids unnecessary initialization and ensures cleanup is always active.
Ensures alias names contain no spaces.
Sends an MCI command and returns success/failure.
Sends an MCI command and returns the string result.
These helpers centralize all MCI communication.
Loads a WAV or MP3 file and assigns it an alias.
Stops, rewinds, and plays a sound once.
Same as PlaySound, but loops indefinitely.
Pauses playback.
Sets volume from 0–1000 (MCI’s native range).
Checks whether a sound is currently playing.
These functions form the basic building blocks of the audio system.
Loads 8 copies of the same sound:
selectA
selectB
...
selectH
This allows rapid overlapping playback without corrupting MCI.
Finds the first non‑playing alias and plays it.
Sets volume for all overlapping channels.
This simulates mixing in an API that does not support mixing.
Stops and closes all aliases used when exiting the game.
The cleanup routine:
- Saves alias name, file path, and volume
- Stops and closes all aliases
- Reopens each alias fresh
- Restores volume
- Restarts looping sounds
This prevents:
- buffer corruption
- alias leakage
- distorted audio
- long‑session instability
This is the fix that made MCI stable enough for real‑time gameplay.
MCI is old.
It leaks handles, corrupts buffers, and becomes unstable after long sessions. Especially when simulating mixing with overlapping playback.
Our cleanup system resets MCI’s internal state before corruption accumulates.
This is the key reason our audio engine stays stable for hours.
The AudioPlayer module transforms the fragile MCI subsystem into a reliable game audio engine by adding:
- Safe command handling
- Overlapping playback
- Looping support
- Volume control
- Automatic corruption recovery
- Long‑session stability
Despite MCI’s age, this module makes it robust enough for a modern WinForms game.
Top | Keyboard Controls | AudioPlayer Module | Cleanuptick Walkthrough | Fixed Audio Playback Issues
Private Sub CleanupTick(sender As Object, e As EventArgs)Defines the cleanup routine that runs every 5 minutes via a timer.
' Store alias info before closingComment: we’re about to gather all data needed to reopen aliases later.
Dim reopenList As New List(Of (aliasName As String, filePath As String, volume As Integer))Creates a list of tuples, each holding:
aliasName- the MCI aliasfilePath- the WAV or MP3 file pathvolume- the current volume
A tuple is a lightweight way to store multiple values together without creating a class or structure.
Example:
(aliasName:="selectA", filePath:="sounds/select.wav", volume:=700)This is a single item containing three fields.
We use tuples to temporarily store alias data during cleanup.
This lets us close everything and reopen it cleanly.
' Collect file paths + volume levelsComment: next loop gathers alias metadata.
For Each aliasName In Aliases.ToList()Iterates over a copy of the alias list so we can safely modify the original later.
Dim path = Query($"info {aliasName} file")Asks MCI for the file path associated with this alias.
If String.IsNullOrWhiteSpace(path) Then Continue ForIf MCI returns nothing, skip this alias.
Dim volStr = Query($"status {aliasName} volume")Queries MCI for the alias’s current volume.
Dim vol As Integer = 500 ' default fallbackSets a default volume in case parsing fails.
Integer.TryParse(volStr, vol)Attempts to convert the volume string into an integer.
reopenList.Add((aliasName, path, vol))Adds a tuple containing all three values to reopenList.
NextEnds the metadata‑collection loop.
' Close all aliasesComment: next loop shuts down all audio devices.
For Each aliasName In Aliases.ToList()Iterates over a copy of the alias list again.
Send($"stop {aliasName}")Stops playback for the alias.
Send($"close {aliasName}")Closes the alias’s audio device.
Aliases.Remove(aliasName)Removes the alias from the active alias set.
NextEnds the closing loop.
' Reopen aliasesComment: now we reopen everything using the stored tuple data.
For Each item In reopenListIterates over each tuple we saved earlier.
If Send($"open ""{item.filePath}"" alias {item.aliasName}") ThenReopens the WAV or MP3 file using its original alias name.
Aliases.Add(item.aliasName)Adds the alias back to the active alias set.
End IfEnds the conditional reopen block.
NextEnds the reopen loop.
' Restore volume levelsComment: next loop restores each alias’s original volume.
For Each item In reopenListIterates over each tuple again.
SetVolume(item.aliasName, item.volume)Restores the volume stored in the tuple.
NextEnds the volume‑restore loop.
' Restart any looping sounds (only once)Comment: looping sounds (like background music) need to be restarted manually.
Form1.RestartLoops()Calls back into the main form to restart any looping audio.
End SubEnds the cleanup routine.
Top | Keyboard Controls | AudioPlayer Module | Cleanuptick Walkthrough | Fixed Audio Playback Issues