◀ BACK TO PORTFOLIO
Alyhjem

Aly-hjem

A 2-player local co-op 3D action-platformer.

Unity C# 3D Local Co-op Physics

▶ THE BRIEF

Aly-hjem started with an idea: a space game setting that felt like one of those browser games you'd stumble across when you were younger. Easy playable, no friction, just pick up the controller and go. The target audience was tweens, 9–12 years old, which meant there was a lot to think about to make it actually work for them. Controls had to be fairly simple. Two kids sitting down together had to be able to figure it out without anyone explaining it to them, and it had to be fun enough that they'd want to keep going. To ground those decisions in something more than gut feel, we did research. We leaned on Self-Determination Theory and Flow Theory to understand what keeps a young player engaged without tipping into frustration, and drew on Piaget and Erikson to think about what's actually age-appropriate at that developmental stage. That research ended up justifying a lot of the more conservative design calls; fewer buttons, a simple and readable playthrough, and prioritising co-op fun over mechanical depth.

▶ WHAT WE BUILT

The core of the game is planet-relative gravity so players walk around the planet. On top of that we built a three-state jump machine (grounded / rising / falling) to drive jump feel and animation transitions, directional combat using dot-product comparisons between input direction and facing paired with Physics.OverlapSphere for hit detection, and a hold-to-revive co-op mechanic that we were particularly happy about: when one player goes down, their partner has to physically stay close and hold the input to bring them back. It turns what would be a frustrating fail-state into a co-op moment instead. The rest of the game have wave-based enemy encounters, a boss fight, achievement system, main menu, and settings, which all had to connect coherently. One thing we made sure of early was an in-game tutorial card UI that pulled its control labels directly from the live input scripts, so onboarding stayed accurate even as the controls changed during development. We made a simple cartoon-like 3D character design in Blender, where we made a base character and from that we made different models so that players can choose different characters that best suit them.

▶ A TECHNICAL CHALLENGE

A technical challenge we had was that player and enemy movement started on Unity's CharacterController, but once planet-relative gravity needed to interact properly with physics, CharacterController really working with it. Therefore, we migrated everything over to Rigidbody, driving velocity manually in FixedUpdate and switching jumping from a fixed positional offset to an impulse-based approach. The trickiest part of that migration was ground detection. The naive version was just checking whether a downward raycast hit the ground layer, but that reintroduced a subtle bug: a player who had just jumped was still close enough to the surface for the ray to return true, so the game would immediately re-flag them as grounded mid jump. And that broke the jump feel entirely and also allowed unintended double jumping. The fix was a !movingUp check alongside the raycast, so we usinged a Vector3.Dot to project the player's current velocity onto the surface normal, and only treating the character as grounded if the ray hits and they aren't still moving away from the surface.

DoodleUp

DoodleUp

A multiplayer drawing-guessing game.

JavaScript WebRTC PeerJS Multiplayer

▶ THE BRIEF

Doodle Party came out of wanting a Pictionary-style game me and my friendes could just send to friends and so we could play together without being in the same room. We wanted a simple multiplayer game where we did not have to install anything, no account creation, no you have to have Discord open. We wanted to have just a a link. I wanted to make it with zero friction, I couldn't just rely on a dedicated backend I'd have to keep running and paying for. So I thought of making the project in a single HTML file.

▶ WHAT WE BUILT

The core of it is peer-to-peer networking through PeerJS, which wraps WebRTC's data channels. That meant players connect directly to each other's browsers instead of everyone routing through a server I'd have to host. To keep the game logic manageable without a real backend, I went with a host-authoritative model, where whichever player's browser starts the room becomes the source of truth for whose turn it is, what the current word is, and how scoring works. Everyone else just sends input (the drawing strokes, guesses) to the host and renders whatever state the host broadcasts back. It's felt simpler to make than trying to keep every peer's state in sync with everyone else's, and it meant I didn't need a server process running 24/7 just to keep a lobby alive. The drawing canvas itself works by streaming stroke data to the host as you draw, which the host then rebroadcasts to everyone else in near real-time, rather than sending full canvas images back and forth.

▶ A TECHNICAL CHALLENGE

A difficult part was NAT traversal. WebRTC can usually punch a direct connection between two peers using STUN to figure out each side's public address, and that worked fine on my own network while testing. But when I sent it to friends to try, some of them just couldn't connect to each other at all, there was no errors, the connection would just hang. The issue was symmetric NATs and stricter firewalls, can be common on some home routers and pretty much any campus or corporate network, where STUN isn't enough to establish a direct peer-to-peer link. Debugging this was frustrating precisely because it worked perfectly for me and failed silently for other people, so I had to lean on logging the ICE candidate types being gathered on each side to actually see what kind of connection path was being attempted and where it was failing. The fix was adding a TURN server as a fallback, so when a direct connection can't be negotiated, traffic gets relayed through the TURN server instead. It's a bit slower and it's an extra piece of infrastructure to rely on, but it meant it worked for all the players instead of it silently doesn't work for some players.