Last week I covered the theory of a clean Git workflow: branches, commits, and pull requests. If you haven’t read that one, start there. This article is the practical follow-up. We’re going to apply that exact git workflow to a complete project.

I’m using Pong because everyone knows what it is. The systems are small and the scope is contained. That makes it a good vehicle for demonstrating the workflow without the game design getting in the way.

This is a demonstration of the workflow, not a complete Pong tutorial. Some implementation details are simplified or skipped. The goal is to show how branches, commits, and pull requests map to real development decisions, not to cover every line of code.

GitHub setup (before any branches)

Before you open Unity, set up your repository.

  • Create a new repo on GitHub.
  • Check the box to initialize with a README.
  • Add a .gitignore using the Unity template GitHub provides. This keeps Unity’s auto-generated files out of your commit history.
  • Clone the repo locally.
  • Connect your local project to origin and pull the README and .gitignore down.

You’re now on main with two files. That’s your baseline. Nothing has been built yet, and that’s correct.

This walkthrough uses Unity, but nothing in the workflow is engine-specific. The branching structure, commit format, and PR process work the same regardless of what you’re building in.

Setting up the branch structure

With the repo initialized, create your development branch directly from main.

git checkout -b development
git push -u origin development

From this point forward, main only changes when something is ready to ship. development is where finished feature branches land. All active work happens on feature branches off of development.

feature/project-setup

Branch from development.

chore: initialize Unity project with 2D template
chore: configure folder structure (Scripts, Prefabs, Scenes, Materials, Fonts, Art)
fix: correct default physics settings for 2D project
chore: create and configure main game scene

Open a pull request from feature/project-setup into development. Review the diff. Confirm the folder structure looks right and there are no merge conflicts. Merge.

feature/input

Branch from development.

feat: add InputManager to handle player one and player two keyboard input
feat: expose input axes as readable movement direction for other systems
refactor: rename input axis variables to match Unity input axis naming convention

The refactor: this is the first time you’ll see a non-feat commit inside an active feature branch. My rule is simple: if the branch hasn’t merged yet, the fix stays on the branch. No need to open a separate branch for a correction made during active development on the same system.

Open a PR from feature/input into development. Review. Merge.

feature/paddle

Branch from development.

feat: add Paddle script with movement speed and clamped vertical bounds
feat: create paddle prefab with BoxCollider2D and Rigidbody2D
fix: correct paddle bounds clamp to account for sprite height offset
feat: place player one and player two paddles in scene with input bindings

Open a PR from feature/paddle into development. Review. Merge.

art/paddle

Branch from development.

art: add custom paddle sprites to replace default white rectangles
art: update paddle prefab to use new sprites
fix: adjust paddle collider size to match new sprite dimensions

Art branches live separately from feature branches by design. A dedicated artist can work on art/paddle at the same time a programmer is working on feature/ball. Nothing collides. Both branches merge into development independently when they’re done.

Open a PR from art/paddle into development. Review. Merge.

feature/ball

Branch from development.

feat: add Ball script with initial velocity and serve direction
feat: add physics-based bounce on collision with paddles and walls
refactor: extract serve direction logic into separate method for clarity
feat: add speed increase on each paddle hit to escalate difficulty
feat: create ball prefab with CircleCollider2D and Rigidbody2D
fix: cap maximum ball speed to prevent tunneling at high velocities
feat: configure physics material on ball for zero friction and full bounce
feat: place ball in scene

Open a PR from feature/ball into development. Review. Merge.

art/ball

Branch from development.

art: add custom ball sprite to replace default white square
art: update ball prefab to use new sprite

Open a PR from art/ball into development. Review. Merge.

feature/scoring

Branch from development.

feat: add goal trigger colliders on left and right walls
feat: add ScoreManager to track and update player scores
fix: correct goal trigger layer mask to exclude paddle collisions
feat: add ball reset and re-serve logic on score

Open a PR from feature/scoring into development. Review. Merge.

bugfix/ball-clip-on-score

Branch from development.

fix: prevent ball from clipping through goal collider at high speed by switching to continuous collision detection

This is the first time a dedicated bugfix/ branch appears. The reason is timing. The ball clipping bug wasn’t discovered until after feature/ball had already merged into development. At that point, the branch is closed. The bug lives in development now, not in any active feature branch. The correct move is a new branch, scoped to just the fix, merged back into development when it’s done.

Open a PR from bugfix/ball-clip-on-score into development. Review. Merge.

feature/ui

Branch from development.

feat: add score display UI for player one and player two
feat: add win condition screen with player name and final score
refactor: move score update calls into ScoreManager events instead of direct UI references
feat: connect ScoreManager events to UI score display and win screen

Open a PR from feature/ui into development. Review. Merge.

art/ui-font

Branch from development.

art: import custom font for score and win screen display
art: apply custom font to all UI text elements
fix: adjust font size and line height on win screen to prevent text overflow

Open a PR from art/ui-font into development. Review. Merge.

feature/game-loop

Branch from development.

feat: add GameManager to control match state (waiting, playing, game over)
feat: add serve state that holds ball until player input
fix: prevent serve from triggering before game state is fully initialized
feat: add win condition check and transition to game over state
refactor: consolidate state transition logic into single GameManager method
feat: add restart flow from game over back to serve state

Open a PR from feature/game-loop into development. Review. Merge.

Shipping: development into main

The game is complete. Every feature branch is merged. development represents a full, working build of Pong.

Before opening the final PR, bump the version number. This is a chore: commit directly on development because it isn’t a feature and it isn’t a fix. It’s bookkeeping. The version number tells anyone who picks up the project exactly what state it’s in.

chore: bump version to 1.0.0

Now open a PR from development into main. This is the last review before the build is in the hands of players. The diff covers every change made across the entire project. Review it thoroughly. This is the last chance to catch anything before it ships. Merge.

main holds a shipped game. Everything is completely separate. The entire development history lives in development and its branches, exactly where it belongs.

The full branch list

Here’s what the complete branch history looks like from the outside:

main
development
feature/project-setup
feature/input
feature/paddle
art/paddle
feature/ball
art/ball
feature/scoring
bugfix/ball-clip-on-score
feature/ui
art/ui-font
feature/game-loop

You can read the entire development arc of the project from that list. Setup, input, the two game objects, scoring, a bug that came in after the fact, UI, art, the game loop. No branch name requires explanation. That’s the goal.

The Git Workflow in Practice

The workflow you just walked through is the same one I use on every project. Branches keep systems separated. Commits make it easy to see exactly what changed and when. Pull requests give you a review step before anything becomes permanent.

That separation forces you to think about where one system ends and another begins before you write a single line. That habit builds better software architecture over time.

It also sets you up for collaboration. If you bring on an artist or another developer mid-project, the structure is already there. Each person works in their own branch. Nothing interferes until it’s ready to merge.

The Pong project is small. But the workflow scales. The same structure that organized twelve branches here will organize a hundred on a larger project.

Categories: Tutorials

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *