PeaHeads

Inspiration

The KnightHacks 2026 theme was Bloom vs Blight. We wanted a shooter where your body is the controller, and pea plants that spit peas fit the theme. One rule shaped the whole project: dodging with your real head is the game. We cut the keyboard fallback for the head on purpose. If you want to avoid a pea, you have to move.

The story grew out of the theme. The Hollow Grove's Sporelights hold the forest's life, and the Rot is killing them. Planting yourself is how a pea draws power from the ground, which is why you can only spit while rooted.

What it does

Up to four players join a match and get split evenly into Bloom (yellow) and Blight (purple).

  • Move with WASD. Press Space to plant, and it takes a full second to root and a full second to pull back out. You can't walk while planted, and you can't shoot unless you are planted.
  • Dodge by moving your real head. Your pea's head leans sideways, rises and ducks, and the camera rides inside it.
  • Shoot by opening your mouth. You have to close it again before the next shot, and there's a short cooldown, so there's no full auto.
  • Zoom by closing one eye. Zoomed shots also fly straight for longer before gravity pulls them down.
  • Score by knocking out rivals or by staying planted next to a glowing Sporelight until you collect it. Sporelights also heal you. First to 20 points wins, or whoever leads when the 2 minute clock runs out.

The arena reacts to who's winning. Where someone gets knocked out, the killer's color spreads over the ground: flowers for Bloom, glowing purple mushrooms for Blight. Fog thickens as the leader gets close to winning.

When you get knocked out, you see a killcam. It splits the screen into "THE SHOT" and "THE FACE": your opponent's view of the hit next to their actual face as they made it.

How we built it

Head tracking (Python). head_tracker.py uses MediaPipe Face Mesh on the webcam. Every frame it sends one small JSON packet to Unity over local UDP: head offset, whether the mouth is open, and whether exactly one eye is closed. It also sends cropped face frames on a second port for the killcam. The tracker only reports what your face is doing. Unity decides what that means given whether you're planted, on cooldown, or dead. It calibrates on the first face it sees, and you can recalibrate with C.

Game (Unity 6, URP). The camera is first person and sits inside the pea's head. Your own head renders only its shadow, so it doesn't block the view. The plant is built from generated meshes. A bending stem connects the dirt mound to the head, and the snout flares and the eyelid shuts to match what the other player is doing with their face. Planting runs on a small state machine (Unplanted, Planting, Planted, Unplanting), which we covered with EditMode tests along with the other pure logic.

Multiplayer (Netcode for GameObjects). The host is authoritative over health, scoring, peas and damage. Each player owns their own movement and head pose, so your camera reacts to your webcam right away and never waits on the network. Players fire through an RPC, and the host spawns and moves the pea.

Killcam. Every client keeps a rolling few seconds of its own view and its face feed. On a kill, the killer packs about 3 seconds into small chunks and sends them to the victim, who plays them back in slow motion. The face clip is the only video that ever leaves your machine, and each player can turn it off in the pause menu.

Everything else. The menus, HUD, minimap, lobby and five-slide story intro are all built in code, as are the arena layout and the materials. All music and sound effects come from a Python script, so we can regenerate them any time instead of hunting for assets.

Challenges

  • Webcam frame rate. At 720p the camera's default format only gave us about 10 fps. We switched to MJPG to get 30. The tracker also picks the highest-resolution camera, so a USB webcam wins over a laptop's built-in one.
  • Making dodging feel good. At 1:1 your on-screen head barely moves. We multiply head movement by 5 and sweep the head against walls so leaning can't push your camera through cover.
  • Fair hitboxes. Everyone tunes their lean differently. We sync the final head position in meters after the wall check, so a player's own settings can't change where they can be hit.
  • Reading gestures reliably. A face doesn't give clean button presses. Mouth open has to hold for 2 frames and a wink for 3 before they count. The game fires only on the change from closed to open, so holding your mouth open doesn't spray peas.
  • Killcam cost. Capturing and encoding video on the killer's machine could cost frames right at the moment of the kill. We moved the GPU readbacks to a queue and the JPEG encoding onto worker threads. Playback decodes ahead of time within a small per-frame budget.
  • Builds vs the editor. Unity's built-in default material shows up magenta in builds, so anything we built from primitives at runtime needed our own materials.
  • Performance. A quality governor watches frame time in the arena and steps down bloom, anti-aliasing and render scale one level at a time until the game holds its target.

What we learned

  • Treat gestures as states, not events. "Mouth is open" is a level, and the game watches for it to change.
  • Keep the vision code simple. Putting all the gameplay rules in Unity let us change them without touching the tracker.
  • Write the contract down. A short written spec for the UDP packet let the CV and game sides work in parallel without waiting on each other.
  • Exaggerated movement feels more natural than accurate movement.
  • Get two machines talking early. We tested netcode with a cube in the first two hours, before any real gameplay existed.

Built With

Share this project:

Updates

Submission history