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 building AR/XR pipelines, optimizing cloud asset delivery, and architecting multiplayer systems across multiple studios. I hold two Post-Graduate Certificates from George Brown College — Applied AI Solutions Development and Digital Design: Game Design.
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.