-
-
The web landing page
-
A multi-tier battleground in the middle of the city - the perfect place for a duel
-
Three simple weapons make up an all-time classic loadout
-
The main menu of the controller app
-
As a gunner, your job is to pan the camera, aim, and shoot
-
As a runner, you are responsible for movement, jumping, and crouching
-
Players join the game from their phones, and see themselves on the big screen
-
Teams play in a split-screen format. You can't truly hide from your opponent!
-
The game ends after 5 minutes, or sooner if one team gets 10 kills
-
Every game ends with a stat sheet, and an MVP is declared
-
Two raccoons, one trench coat: the Runner peeks out of the coat, the Gunner wears the hat
-
Land a headshot and the fedora goes flying
-
Lose the fight and the disguise falls apart
-
The Pit: three levels, one rainy city block
-
Where it started: controls and duo mechanics on the whiteboard
Inspiration
Couch co-op games are at their best when everyone is yelling at each other. We wanted to push that further: what if two people had to share one character? In Skill Issue, each team is a single raccoon in a rain-soaked noir city, driven by two phones held sideways like controllers. The Runner tilts their phone to walk, flicks it to jump or crouch, and twists it to sprint. The Gunner turns their phone to aim and taps buttons to fire, aim down sights, reload and switch guns. Neither of you can play alone, and neither of you sees the whole picture. First team to 10 kills wins, or whoever leads when the five-minute clock runs out.
How we built it
- Game: Godot 4.7, split-screen 3D on one shared screen, with a noir look: rainy city map, black-and-white menus, title-card lettering.
- Phone controller: Expo / React Native. The app reads the phone's motion sensors (
DeviceMotion) and turns them into movement, gestures and aim. - Networking: a Node.js WebSocket relay on Render (
wss://relay.skillissue.vodka). The game opens a lobby through our Next.js control plane on Vercel and shows a six-character code. Phones only need that code to join from any network. - Website and data: the landing page at skillissue.vodka serves lobby provisioning and Relay helps with discovery
- AI: after every match, Gemini reads all four players' stats and picks an MVP, then draws them a custom raccoon badge.
The shared protocol (packages/protocol) types every message, and the server, not the client, decides who owns which role. A Gunner can't move the body and a Runner can't shoot.
The motion math
Walking comes from how far you hold the phone tilted away from your neutral pose, with a deadzone and hysteresis so it doesn't flicker:
$$ v = \operatorname{sign}(\theta)\,\left(\operatorname{clamp}!\left(\frac{|\theta| - \theta_{\text{exit}}}{\theta_{\text{full}} - \theta_{\text{exit}}},\,0,\,1\right)\right)^{0.85}, \qquad \theta_{\text{enter}} = 5^\circ,\ \theta_{\text{exit}} = 3.5^\circ,\ \theta_{\text{full}} = 20^\circ $$
The hardest bug was geometric. Gravity only tells you two angles. With the phone tilted back about $45^\circ$, tipping an end down (strafe) and twisting it like a wheel (sprint) both rotate gravity in the screen plane: a full-speed $20^\circ$ strafe looks exactly like a $27^\circ$ twist. So we split each gyroscope sample $\boldsymbol\omega$ into a twist about the screen normal $\hat z$ and a strafe about the level "away" axis $\hat a$. The two axes aren't perpendicular, so with $c = \hat z \cdot \hat a$:
$$ \dot\tau = \frac{\omega_z - c\,(\boldsymbol\omega \cdot \hat a)}{1 - c^2}, \qquad |\tau| \le |\phi_{\text{screen}}| + 4^\circ $$
The bound uses the gravity angle $\phi_{\text{screen}}$, which can't drift, to keep the integrated twist $\tau$ honest. Sprint can't get stuck on, and players never have to recalibrate.
Challenges we ran into
- "Joining your lobby..." forever. React Native's
WebSocket.bufferedAmountisundefined, so our backpressure check silently dropped every message. One?? 0fixed it, after a lot of packet logging. - Phones on different networks. LAN-only play broke at a hackathon, so we moved every lobby onto a public relay and made the code the only thing a phone needs.
- Inverted aim. iOS and Android report gyroscope axes in different orders, so we built a virtual phone simulator,
PhoneRig, to test directions on both platforms without a device in hand. - Latency. We cut it everywhere: sending each sensor sample as soon as it's processed instead of on a 30 Hz timer, dropping a smoothing filter, firing flicks at the turn of the motion, and having the Gunner send exact degrees turned so the camera moves 1:1 with the phone.
- Five people, one repo, one weekend: maps, pickups, HUD, networking, motion, art direction and the end screen all landing at once.
What we learned
- Phone motion sensors are amazing and weird. The OS's fused gravity is clean and lag-free, but raw gyroscope readings drift, and every platform labels its axes differently.
- Treat game input like a protocol: validate everything at the server, never trust the client about who it is.
- Gemini's structured output (a JSON schema with an
enumof valid player IDs) makes an LLM safe to put in the middle of a game loop: it can only answer with a real player.
What's next
Accounts and lifetime stats, more maps, ranked play, and a spectator view for the people watching from the couch.
Built With
- android
- chatgpt
- claude
- gdscript
- gemini
- godot
- ios
- react-native
- typescript
Log in or sign up for Devpost to join the conversation.