<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Adetomiwa Ogundiran</title>
    <description>The latest articles on DEV Community by Adetomiwa Ogundiran (@adetomiwaogundiran).</description>
    <link>https://dev.to/adetomiwaogundiran</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3666471%2F6292bbb3-8e06-4689-b8e7-947e005fcfc0.jpeg</url>
      <title>DEV Community: Adetomiwa Ogundiran</title>
      <link>https://dev.to/adetomiwaogundiran</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/adetomiwaogundiran"/>
    <language>en</language>
    <item>
      <title>How I Built a 3D Endless Runner Game in a Weekend Using AI as My Engineering Partner</title>
      <dc:creator>Adetomiwa Ogundiran</dc:creator>
      <pubDate>Thu, 18 Dec 2025 03:41:02 +0000</pubDate>
      <link>https://dev.to/adetomiwaogundiran/how-i-built-a-3d-endless-runner-game-in-a-weekend-using-ai-as-my-engineering-partner-4i90</link>
      <guid>https://dev.to/adetomiwaogundiran/how-i-built-a-3d-endless-runner-game-in-a-weekend-using-ai-as-my-engineering-partner-4i90</guid>
      <description>&lt;h2&gt;
  
  
  A Product Manager’s journey from idea → shipped game, using AI as a true build collaborator
&lt;/h2&gt;

&lt;p&gt;I’m a Product Manager by trade.&lt;br&gt;
I’ve spent years writing PRDs, prioritizing backlogs, running sprints, and partnering with engineers to ship products. But for a long time, I carried a quiet frustration:&lt;br&gt;
I had ideas I could clearly describe — but couldn’t personally build.&lt;br&gt;
Last weekend, that changed.&lt;br&gt;
I built Olè – The Lagos Hustle, a fully playable 3D endless-runner game with a global leaderboard and shipped it to production. Not as a side demo. Not as a Figma prototype. A real, live game.&lt;br&gt;
What unlocked this wasn’t suddenly becoming a game engineer — it was using AI the way a strong PM uses people.&lt;br&gt;
This is the story of how I treated AI like a junior engineer, applied product thinking end-to-end, and closed the gap between vision and execution.&lt;/p&gt;

&lt;p&gt;The Idea: A Product Vision First&lt;br&gt;
I wanted to build something fun, culturally grounded, and immediately playable.&lt;br&gt;
Think Subway Surfers, but set in Lagos — danfo buses, street markets, wire fences, palm trees, local food collectibles. Fast. Chaotic. Familiar.&lt;br&gt;
From a PM lens, I already knew the MVP scope:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Three-lane movement (left / center / right)&lt;/li&gt;
&lt;li&gt;Jump + slide mechanics&lt;/li&gt;
&lt;li&gt;Progressive difficulty curve&lt;/li&gt;
&lt;li&gt;Global leaderboard to drive competition&lt;/li&gt;
&lt;li&gt;Mobile-first controls (thumb-friendly)
What I didn’t have was deep game-engineering experience — and that’s where AI came in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Build: Treating AI Like a Junior Engineer&lt;br&gt;
Instead of asking AI to “make a game,” I worked the way I would with an engineer on my team.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Clear Requirements Beat Clever Prompts&lt;br&gt;
Bad prompt:&lt;br&gt;
“Make me a fun game.”&lt;br&gt;
Good prompt:&lt;br&gt;
“Create a 3D endless runner using Three.js. The player auto-runs forward. Three lanes. Left/right to change lanes. Up to jump, down to slide. Obstacles spawn ahead and recycle.”&lt;br&gt;
I wrote user stories, acceptance criteria, and constraints — just like I would in Jira.&lt;br&gt;
AI didn’t guess what I wanted. It built exactly what I specified.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sprint-Based Iteration (Not One Big Build)&lt;br&gt;
We shipped incrementally:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sprint 1 – Core movement, camera, obstacle spawning&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sprint 2 – Lagos theming (danfos, stalls, environment)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sprint 3 – Scoring + collectibles&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sprint 4 – Leaderboard + persistence&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sprint 5 – UX fixes from real player feedback&lt;br&gt;
Each sprint ended with:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Me playing the game&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Writing bugs like a PM&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Feeding them back to AI as implementation tasks&lt;br&gt;
AI handled execution. I handled prioritization and judgment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;User Feedback → Product Insight → Code Changes&lt;br&gt;
This is where PM thinking mattered most.&lt;br&gt;
Feedback:&lt;br&gt;
“The wire obstacle looks like I can jump over it.”&lt;br&gt;
That’s not a bug — that’s a UX mismatch. Fix wasn’t logic; it was visual affordance.&lt;br&gt;
✅ Solution: add barbed wire + mesh so sliding is visually obvious.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Feedback:&lt;br&gt;
“Around 1100 points, the game feels like it slows down.”&lt;br&gt;
Instead of blindly fixing it, I asked why.&lt;br&gt;
Root cause: an ease-out speed curve that plateaued mid-run.&lt;br&gt;
✅ Solution: linear speed scaling with a cap — consistent difficulty, no perceived slowdown.&lt;br&gt;
This wasn’t AI intuition — it was product diagnosis, implemented by AI.&lt;/p&gt;

&lt;p&gt;Tech Stack (Simple by Design)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frontend: Vanilla JavaScript + Three.js (WebGL)&lt;/li&gt;
&lt;li&gt;Backend: Node.js HTTP server&lt;/li&gt;
&lt;li&gt;Database: PostgreSQL (leaderboard persistence)&lt;/li&gt;
&lt;li&gt;Hosting: Replit (dev + prod)
No React. No bundlers. No over-engineering.
For this product, simplicity shipped faster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What AI Changed for Me as a PM&lt;br&gt;
AI Isn’t Magic — It’s a Teammate&lt;br&gt;
AI didn’t “figure it out.” It responded to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clear requirements&lt;/li&gt;
&lt;li&gt;Defined success&lt;/li&gt;
&lt;li&gt;Structured feedback
Sound familiar? That’s product work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Domain Knowledge Still Matters&lt;br&gt;
AI wrote the code — but it didn’t decide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What makes difficulty feel fair&lt;/li&gt;
&lt;li&gt;Why mobile controls need repositioning&lt;/li&gt;
&lt;li&gt;How obstacles should visually communicate risk
AI executes. PMs decide.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Speed Is the Real Breakthrough&lt;br&gt;
What would’ve taken me months of learning took one weekend.&lt;br&gt;
Not because AI replaces engineers — but because I could focus on what to build, while AI handled how to build it.&lt;/p&gt;

&lt;p&gt;The Results (So Far)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build time: ~20 hours&lt;/li&gt;
&lt;li&gt;Codebase: ~2,000 lines&lt;/li&gt;
&lt;li&gt;Leaderboard players: 50+ in week one&lt;/li&gt;
&lt;li&gt;Iterations: 5 major versions driven by feedback
And most importantly — a shipped product.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try It Yourself&lt;br&gt;
🎮 Play the game: ole-game.com&lt;br&gt;
If you’re a PM with ideas stuck in your head — this is your sign. The gap between product vision and working software has never been smaller.Appendix: Technical Deep Dive for Engineers&lt;/p&gt;

&lt;p&gt;For those who want to look under the hood 👇&lt;/p&gt;

&lt;p&gt;3D Rendering Architecture (Three.js)&lt;/p&gt;

&lt;p&gt;The game uses a fixed player with the world moving toward the camera to create the endless-runner illusion.&lt;/p&gt;

&lt;p&gt;// Object pooling for obstacles&lt;br&gt;
const VISIBLE_DISTANCE = 100;&lt;br&gt;
const SPAWN_DISTANCE = 80;&lt;/p&gt;

&lt;p&gt;function updateObstacles(delta) {&lt;br&gt;
  obstacles.forEach(obstacle =&amp;gt; {&lt;br&gt;
    obstacle.position.z += gameSpeed * delta;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (obstacle.position.z &amp;gt; 10) {
  obstacle.position.z = -SPAWN_DISTANCE;
  obstacle.position.x = lanes[Math.floor(Math.random() * 3)];
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;});&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;This keeps physics simple and avoids unnecessary transforms.&lt;/p&gt;

&lt;p&gt;Collision Detection (Performance-First)&lt;/p&gt;

&lt;p&gt;Bounding boxes instead of mesh collisions:&lt;/p&gt;

&lt;p&gt;function getPlayerBoundingBox() {&lt;br&gt;
  const box = new THREE.Box3().setFromObject(player);&lt;/p&gt;

&lt;p&gt;if (isSliding) {&lt;br&gt;
    box.max.y = box.min.y + (box.max.y - box.min.y) * 0.4;&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;return box;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Sliding dynamically shrinks the hitbox — simple and performant.&lt;/p&gt;

&lt;p&gt;Speed Progression Curve (Bug → Insight → Fix)&lt;br&gt;
// Old (felt like slowdown)&lt;br&gt;
const speedMultiplier = 1 + (maxSpeedBonus * (1 - Math.pow(0.99, score)));&lt;/p&gt;

&lt;p&gt;// New (consistent)&lt;br&gt;
const speedMultiplier = 1 + (score * 0.0005);&lt;br&gt;
const clampedSpeed = Math.min(speedMultiplier, maxSpeedMultiplier);&lt;/p&gt;

&lt;p&gt;Linear scaling + cap = predictable challenge.&lt;/p&gt;

&lt;p&gt;Mobile Touch Controls (UX-Driven)&lt;br&gt;
controlsContainer.style.cssText = &lt;code&gt;&lt;br&gt;
  position: fixed;&lt;br&gt;
  bottom: 15%;&lt;br&gt;
  left: 50%;&lt;br&gt;
  transform: translateX(-50%);&lt;br&gt;
  pointer-events: none;&lt;br&gt;
&lt;/code&gt;;&lt;/p&gt;

&lt;p&gt;button.addEventListener('touchstart', e =&amp;gt; {&lt;br&gt;
  e.preventDefault();&lt;br&gt;
  handleInput(action);&lt;br&gt;
}, { passive: false });&lt;/p&gt;

&lt;p&gt;Thumb-reachable, no accidental scrolls, no delay.&lt;/p&gt;

&lt;p&gt;Leaderboard Persistence (Postgres)&lt;br&gt;
async function updateScore(username, score) {&lt;br&gt;
  const existing = await pool.query(&lt;br&gt;
    'SELECT score FROM leaderboard WHERE LOWER(username)=LOWER($1)',&lt;br&gt;
    [username]&lt;br&gt;
  );&lt;/p&gt;

&lt;p&gt;if (!existing.rows.length || score &amp;gt; existing.rows[0].score) {&lt;br&gt;
    await pool.query(&lt;br&gt;
      'INSERT INTO leaderboard (username, score) VALUES ($1, $2) \&lt;br&gt;
       ON CONFLICT (username) DO UPDATE SET score = EXCLUDED.score',&lt;br&gt;
      [username, score]&lt;br&gt;
    );&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Only personal bests update — no leaderboard spam.&lt;/p&gt;

&lt;p&gt;Weekly Reset (Competitive Seasons)&lt;br&gt;
INSERT INTO leaderboard_archive (username, score, week_ending)&lt;br&gt;
SELECT username, score, CURRENT_DATE FROM leaderboard;&lt;/p&gt;

&lt;p&gt;TRUNCATE leaderboard;&lt;/p&gt;

&lt;p&gt;Performance Optimizations&lt;/p&gt;

&lt;p&gt;Geometry instancing for repeated assets&lt;/p&gt;

&lt;p&gt;Frustum culling (Three.js default)&lt;/p&gt;

&lt;p&gt;Texture atlasing&lt;/p&gt;

&lt;p&gt;requestAnimationFrame-driven loop&lt;/p&gt;

&lt;p&gt;function animate() {&lt;br&gt;
  requestAnimationFrame(animate);&lt;br&gt;
  const delta = clock.getDelta();&lt;/p&gt;

&lt;p&gt;if (!isPaused) {&lt;br&gt;
    updatePlayer(delta);&lt;br&gt;
    updateObstacles(delta);&lt;br&gt;
    checkCollisions();&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;renderer.render(scene, camera);&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;File Structure&lt;br&gt;
/&lt;br&gt;
├── index.html&lt;br&gt;
├── style.css&lt;br&gt;
├── game.js&lt;br&gt;
├── server.js&lt;br&gt;
└── assets/&lt;/p&gt;

&lt;p&gt;No build step. No bundler. Just working code.&lt;/p&gt;

&lt;p&gt;Final Thought&lt;/p&gt;

&lt;p&gt;AI didn’t replace engineering.&lt;/p&gt;

&lt;p&gt;It collapsed the distance between idea and execution — especially for people who already know how to think in systems, tradeoffs, and outcomes.&lt;/p&gt;

&lt;p&gt;If you’re a PM, designer, or builder who’s been waiting to “one day build” — that day is now.&lt;/p&gt;

</description>
      <category>product</category>
      <category>webdev</category>
      <category>gamedev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
