Gameplay Programmer with a published mobile game, AR/XR solutions, and combat systems — specializing in Unity, Unreal Engine 5, and clean production-quality C#/C++.
T-shaped developer — deep in gameplay mechanics and systems, broad across mobile, AR/XR, tools, and AI.
I'm a Gameplay Programmer based in Toronto. I've built and published a hyper-casual mobile game to Google Play with real analytics, monetization, and cloud save pipelines — not a demo, a live product with players.
I have professional experience at Liminal VR/AR (Unity + AWS AR asset pipeline, 200MB → 45MB app size) and Oneros Tech (Photon multiplayer, 45% latency reduction). Currently completing a Post-Graduate Certificate in Applied AI at George Brown College.
I like games with bluffing mechanics and real-time co-op. If something's broken, I dig until I find it — and I don't settle until it's right.
Selected work with real technical breakdowns — not just what it looks like, but how and why it was built.
A hyper-casual mobile game fully architected and published to Google Play. A live product with real players, analytics, monetization, cloud saves, and cross-platform builds — profiled from 28fps to stable 60fps on mid-range devices.
Vehicle queue handoff at spline position 48 → 54
The core challenge: queue multiple vehicles on a single road, let the player control only the front one, and keep everything else auto-driving without stutter. Each vehicle follows a Curvy Splines path with Mathf.Lerp acceleration — zero physics overhead.
// VehicleController.cs — smooth spline acceleration if (isMoving || IsPassed) currentSpeed = Mathf.Lerp(currentSpeed, targetSpeed, accelerationRate * Time.deltaTime); else currentSpeed = Mathf.Lerp(currentSpeed, 0.0f, decelerationRate * 10.0f * Time.deltaTime);
Control hands off at spline position 48. When the active vehicle crosses position 54, it cascades a push to every other vehicle — a data-driven queue with no hardcoded offsets.
// GameManager.cs — cascade queue update public void UpdateVehiclesTargetPos(float addTargetPos, GameObject instigator) { for (int i = 0; i < CurrentVehiclesOnMap.Count; i++) if (CurrentVehiclesOnMap[i] != instigator) CurrentVehiclesOnMap[i].GetComponent<VehicleController>().targetPos += addTargetPos; }
Every ad event logs to two analytics systems simultaneously — Firebase Analytics with structured Parameter[] arrays AND GameAnalytics SDK — giving cross-platform redundancy and independent reporting dashboards.
Google Play Games cloud save uses ConflictResolutionStrategy.UseMostRecentlySaved with an internet connectivity check before any write — prevents corrupt saves on flaky connections.
Six fully themed maps (Carnival, City, Port, WildWest, SciFi, Plain) — each with their own light arrays, train models, and building window materials — all toggled via a single enum-switch call.
The day/night system reads actual system time via System.DateTime.Now.Hour — not a designer-set bool. The "Auto" theme makes the game world literally reflect the player's real-world time of day.
// GameManager.cs — real-world DateTime drives day/night case "Auto": isDay = System.DateTime.Now.Hour >= 7 && System.DateTime.Now.Hour <= 18; break;
Framerate target is set dynamically at runtime by reading the device's actual hardware refresh rate — not hardcoded. Mobile gets 30fps to preserve battery, PC via Google Play Games gets 60fps, and high-refresh-rate devices get their full native rate.
// GameManager.cs — adaptive framerate from hardware Application.targetFrameRate = useHigherFramerate ? Mathf.RoundToInt(Screen.currentResolution.refreshRateRatio.value) : isGPG_PC() ? 60 : 30;
One axe-grab mechanic came from a reference tutorial. Everything else — dual-system lock-on, attack magnetism, quadratic Bézier axe return, Physics.Linecast hit detection, animation hitstop, directional hit reactions, bone-attached blood decals, procedural compass HUD, Rage mode with HDRP screen tint — designed and built from scratch.
Reticle snaps to nearest enemy by screen-center angle, not distance
Two separate systems run in parallel and neither knows the other exists. EnemyLockOn handles camera and reticle; PlayerAttackPush silently slides the player toward the target during attacks only.
Target selection uses Vector3.Angle(cam.forward, dir) — finds the enemy closest to screen center, not nearest by distance. A LOS raycast rejects enemies behind walls before confirming lock.
// EnemyLockOn.cs — screen-center angle selection float _angle = Vector3.Angle(cam.forward, dir); if (_angle < closestAngle) { closestTarget = nearbyTargets[i].transform; } // Crosshair scales with distance lockOnCanvas.localScale = Vector3.one * ((cam.position - pos).magnitude * crossHair_Scale); // PlayerAttackPush.cs — weapon-range-aware magnetism noticeZone = PlayerController.instance.isAxeEquipped ? 5 : 10; if (noticeZone == 5 && dis >= 1.6f) transform.position = Vector3.Lerp(transform.position, currentTarget.position, Time.deltaTime * 5f);
Quadratic Bézier arc — the curve is entirely code, no animation
The return follows a hand-rolled quadratic Bézier curve through a designer-placed midpoint — not a straight lerp. Gives the arc its iconic curved flight path entirely in code.
// PlayerController.cs — quadratic Bézier formula public Vector3 GetQuadraticCurvePoint(float t, Vector3 p0, Vector3 p1, Vector3 p2) { float u = 1 - t; return (u*u * p0) + (2 * u * t * p1) + (t*t * p2); } Axe.transform.position = GetQuadraticCurvePoint(returnTime, pullPos, CurvePoint.transform.position, AxeLoc.transform.position); returnTime += Time.deltaTime * 1.5f;
Hit detection uses Physics.Linecast between two blade-tip Transforms. Fires only while isDamage == true (animation events), with HitCount + RecoveryTime preventing multi-frame registrations.
// WeaponCollision.cs — animation hitstop (the juice) IEnumerator SpeedRegain() { PlayerController.instance.ThirdPersonAnimator.speed = ContactSpeedTime.x; // freeze yield return new WaitForSeconds(ContactSpeedTime.y); PlayerController.instance.ThirdPersonAnimator.speed = initSpeed; // restore } // Blades Heavy Attack — slow-mo cinematic chain FXHandler.instance.BladeHeavyAttackCamera(); ToggleSlowMo(1); // Time.timeScale = 0.2f yield return new WaitForSeconds(.2f); ToggleSlowMo(0); Blade1.transform.DOLocalMove(Vector3.zero, 0.1f);
On hit: animation hitstop, directional hit reaction (HitLeft/HitRight/HitGut), blood decal on nearest skeleton bone, and weapon-specific audio through independent AudioMixer groups — all fire simultaneously.
Entire input layer uses Unity's New Input System with InputActionAsset — zero KeyCode polling. OnControlsChanged() auto-detects keyboard vs. gamepad to switch HUD layouts at runtime.
// Stick combination input logic if (isRightStickPressed && isLeftStickPressed && PlayerStats.instance.canRage) StartCoroutine(SpecialAbility()); // Both sticks = RAGE else if (isRightStickPressed && !isLeftStickPressed) isEnemyLockOn = true; // Right only = LOCK-ON
The compass HUD is fully procedural — 5 direction labels and 5 dividers repositioned every frame from Camera.main.eulerAngles.y. Quest waypoint clamps to ±400px with overflow arrows when offscreen. Audio uses 6 independent AudioSources each routed to their own AudioMixerGroup.
Two-character co-op puzzle game in UE5. As technical head, designed and implemented the core C++ gameplay architecture — dual-player merge/split system, color-state machine, health pooling, and camera behavior using 3D math. Managed Git workflow for a 5-person team across a 10-week delivery cycle with 12 completed levels.
VectorInterpTo drives the physical merge into a shared position
Blue and Red players move independently until Merge — physically interpolating to a shared position via VectorInterpTo toward TargetMergePos. On merge, individual health pools collapse into a single Purple pool.
Damage routing fires BPI_SetOverlayState (Blueprint Interface) to cleanly decouple health logic from the character BP. If health ≤ 0: DoRagdoll → AfterDeath → respawn via SetWorldLocationAndRotation to stored player start. No scene reload.
Merge conflicts reduced by 70% through a documented Git branching strategy — feature branches per team member, protected main, and weekly integration reviews. Blueprint Interfaces kept each developer's work isolated with no direct cross-BP dependencies.
Applied strong 3D math to movement, interaction, and camera behaviour, reducing rework on level scripting by 30%. Delivered clean C++ interfaces ready for future online services — the kind of forward-looking architecture that saves teams from expensive rewrites.
Built entirely from scratch — no tutorial, just the original game as inspiration. Implemented the orthographic impossible geometry illusion, rotatable structure mechanics, and path-following navigation logic that create Monument Valley's signature feel using only Unity and C#.
90° snap rotation recalculates all walkable path conditions in real-time
The illusion relies on an orthographic camera — parallel projection removes depth cues, making geometrically separate platforms appear connected when their 2D screen positions align. Rotatable pivots snap to 90° increments using DOTween with RotateMode.WorldAxisAdd and Ease.OutBack for a satisfying overshoot.
// GameManager.cs — 90° snap with DOTween public void RotateMainPlatdform(int multiplier) { pivots[0].DOComplete(); pivots[0].DORotate(new Vector3(0, 90 * multiplier, 0), .6f, RotateMode.WorldAxisAdd) .SetEase(Ease.OutBack); }
After every rotation, PathCondition structs are re-evaluated each Update() tick. Each condition checks if a pivot's eulerAngles matches a target angle — only then marking those tile connections active = true. Platforms that look connected only become walkable when their angles satisfy the condition.
// GameManager.cs — condition-gated path activation (runs every frame) foreach (PathCondition pc in pathConditions) { int count = 0; for (int i = 0; i < pc.conditions.Count; i++) if (pc.conditions[i].conditionObject.eulerAngles == pc.conditions[i].eulerAngle) count++; foreach (SinglePath sp in pc.paths) sp.block.possiblePaths[sp.index].active = (count == pc.conditions.Count); }
Movement uses a custom BFS traversal over the tile graph. On click, FindPath() explores outward from the player's tile — only following edges marked active = true. Because rotation changes active edges in real-time, the walkable graph updates every frame before the player commits to a move.
// PlayerController.cs — BFS graph traversal void ExploreCube(List<Transform> nextCubes, List<Transform> visited) { Transform current = nextCubes.First(); nextCubes.Remove(current); if (current == clickedCube) return; foreach (WalkPath path in current.GetComponent<Walkable>().possiblePaths) { if (!visited.Contains(path.target) && path.active) { nextCubes.Add(path.target); path.target.GetComponent<Walkable>().previousBlock = current; } } visited.Add(current); if (nextCubes.Any()) ExploreCube(nextCubes, visited); }
BuildPath() traces back through previousBlock references to reconstruct the route. FollowPath() chains DOTween DOMove + DOLookAt per tile — stair tiles get 1.5× travel time. The player parents to moving ground tiles so it rides platform rotations naturally.
// PlayerController.cs — sequenced path follow for (int i = finalPath.Count - 1; i > 0; i--) { float time = finalPath[i].GetComponent<Walkable>().isStair ? 1.5f : 1; s.Append(transform.DOMove(finalPath[i].GetComponent<Walkable>().GetWalkPoint(), .2f * time).SetEase(Ease.Linear)); if (!finalPath[i].GetComponent<Walkable>().dontRotate) s.Join(transform.DOLookAt(finalPath[i].position, .1f, AxisConstraint.Y, Vector3.up)); }
Camera dolly + orthographic zoom driven entirely in code — no Timeline, no Animator
Level completion is a fully code-driven cinematic. A DOTween Sequence with chained SetDelay fades UI layers in staggered order, while both orthographic cameras simultaneously dolly back and zoom out via DOVirtual.Float driving orthographicSize directly.
// GameManager.cs — camera dolly + orthographic zoom cameras[0].DOMove(new Vector3(16, 35f, -20), 10f).SetEase(Ease.OutBack); DOVirtual.Float(13, 6, 10, v => cameras[0].GetComponent<Camera>().orthographicSize = v); // Staggered UI reveal with 3s delay DOTween.Sequence().SetDelay(3) .Append(UIManager.Instance.infoText.GetComponent<Image>().DOColor(new Color(1,1,1,1), 1)) .Append(UIManager.Instance.infoText.transform.GetChild(0).GetComponent<Image>().DOColor(Color.white, 1));
Buttons fade to alpha 0 via DOColor and become non-interactable simultaneously. Player movement locks via gamecomplete = true. Zero animation assets — the entire closing cinematic is one C# method.
Proficiency earned through production code, competition projects, and real optimisation work.
Professional roles with measurable impact on live products.
Actively looking for Gameplay Programmer and Tools Developer roles in Toronto, Montreal, Vancouver, or remote worldwide. If you're building something exciting, I'd love to hear about it.