{"slug": "build-a-dinosaur-runner-game-with-deno-pt-3", "title": "Build a dinosaur runner game with Deno, pt. 3", "summary": "This article is the third part of a tutorial series on building a browser-based dinosaur runner game using Deno. It focuses on implementing obstacles and collision detection, teaching readers how to spawn and manage obstacles, detect collisions between the dinosaur and obstacles, and update the game state accordingly. The post includes code examples for setting up obstacle properties, spawning random cactus obstacles, and moving them across the screen while increasing the player's score.", "body_md": "# Build a dinosaur runner game with Deno, pt. 3\n\nThis series of blog posts will guide you through building a simple browser-based dinosaur runner game using Deno.\n\n[Setup a basic project][Game loop, canvas, and controls][Obstacles and collision detection][Databases and global leaderboards]- Player profiles, customization, and live tuning\n- Observability, metrics, and alerting\n\nObstacles and collision detection\n\nIn [Stage 2](/blog/build-a-game-with-deno-2/), we set up the basic game loop and\nrendering for our dinosaur runner game, but it still isn’t really a game,\nthere’s nothing to ‘play’. In Stage 3, we will introduce obstacles for the dino\nto jump over, as well as implement collision detection to determine if the dino\nhits an obstacle, and ends the game.\n\nWhat you’ll Learn\n\nBy the end of this stage, you will have learned:\n\n- How to create and manage obstacles in the game\n- Implementing collision detection between the dino and obstacles\n- Updating game state based on collisions\n\nSetting up the game state\n\nWe’re going to add some new properties to our game state to manage obstacles and\nscoring. Open up your `game.js`\n\nfile and locate the constructor of your\n`DinoGame`\n\nclass. In here we’ll add properties to the game state to set the\ninitial game speed, the frame count that we’ll need to animate the dino and a\nhigh score tracker:\n\n```\nthis.initialGameSpeed = 3;\nthis.frameCount = 0;\nthis.highScore = this.loadHighScore();\n```\n\nScaffolding out the game methods\n\nOur game currently has a `startGame()`\n\nand a `resetGame()`\n\nmethod. We’ll be\nadding several new methods to handle obstacle spawning, updating, collision\ndetection, and scoring.\n\nFor now lets add empty method stubs to the class so we can fill them in later. Add these just below your existing methods:\n\n```\ngameOver() {}\n\nloadHighScore() {}\n\nsaveHighScore() {}\n\nupdateHighScore() {}\n\ndrawGameOver() {}\n```\n\nThe obstacle system\n\nIn game design terms, obstacles are typically represented as objects with properties such as position, size, and type. They have a bounding box that can be used for collision detection.\n\nWe’ll implement a simple obstacle system where obstacles are spawned at random intervals and move towards the dino. The player must jump over these obstacles to avoid collisions. As they successfully avoid obstacles, their score increases, and the game speed ramps up to increase difficulty.\n\nIn our `game.js`\n\n, lets add some more properties to the constructor to manage\nobstacles (these can go just before the `this.init();`\n\ncall):\n\n```\n// Obstacle properties\nthis.obstacles = [];\nthis.obstacleSpawnTimer = 0;\nthis.obstacleSpawnRate = 120;\nthis.minObstacleSpawnRate = 60;\n```\n\nThis creates an empty array to hold active obstacles, a timer to track when to\nspawn the next obstacle, and a spawn rate that determines how often obstacles\nappear. The `minObstacleSpawnRate`\n\nwill ensure that obstacles don’t spawn too\nfrequently as the game speeds up.\n\nin the `startGame()`\n\nmethod, set the obstacle timer to zero so it starts fresh\neach round:\n\n```\nthis.obstacleSpawnTimer = 0;\n```\n\nSpawning and updating obstacles\n\nObstacles are just rectangles that enter from the right edge and move left at\nthe current game speed. Add these methods to the class, they can go below the\n`jump()`\n\nmethod:\n\n``` js\nspawnObstacle() {\n    const obstacleTypes = [\n        { width: 20, height: 40, type: \"cactus-small\" },\n        { width: 25, height: 50, type: \"cactus-medium\" },\n        { width: 30, height: 35, type: \"cactus-wide\" },\n    ];\n\n    const obstacle =\n        obstacleTypes[Math.floor(Math.random() * obstacleTypes.length)];\n\n    this.obstacles.push({\n        x: this.canvas.width,\n        y: this.groundY - obstacle.height,\n        width: obstacle.width,\n        height: obstacle.height,\n        type: obstacle.type,\n    });\n}\n\nupdateObstacles() {\n    if (this.gameState !== \"playing\") return;\n\n    this.obstacleSpawnTimer++;\n    if (this.obstacleSpawnTimer >= this.obstacleSpawnRate) {\n        this.spawnObstacle();\n        this.obstacleSpawnTimer = 0;\n    }\n\n    for (let i = this.obstacles.length - 1; i >= 0; i--) {\n        this.obstacles[i].x -= this.gameSpeed;\n\n        if (this.obstacles[i].x + this.obstacles[i].width < 0) {\n            this.obstacles.splice(i, 1);\n            this.score += 10;\n        }\n    }\n}\n```\n\nHere, we set up some obstacle types with different sizes. The `spawnObstacle()`\n\nmethod randomly selects one and adds it to the obstacles array at the right edge\nof the canvas.\n\nThe `updateObstacles()`\n\nmethod increments the spawn timer and spawns a new\nobstacle when the timer exceeds the spawn rate. It also moves each obstacle left\nby the current game speed and removes any that have moved off-screen, awarding\npoints for each successfully avoided obstacle.\n\nCollision detection and difficulty ramping\n\nCollision detection is how games determine if two objects have come into contact. We can check the bounding boxes of the dino and each obstacle to see if they overlap. If they do, the game ends.\n\nCollision detection just compares the dino’s rectangle with each obstacle’s\nrectangle. If they overlap, the run ends. Immediately after adding\n`updateObstacles`\n\n, drop in these helpers:\n\n```\ncheckCollisions() {\n    if (this.gameState !== \"playing\") return;\n\n    for (let obstacle of this.obstacles) {\n        const isOverlapping =\n            this.dino.x < obstacle.x + obstacle.width &&\n            this.dino.x + this.dino.width > obstacle.x &&\n            this.dino.y < obstacle.y + obstacle.height &&\n            this.dino.y + this.dino.height > obstacle.y;\n\n        if (isOverlapping) {\n            this.gameOver();\n            return;\n        }\n    }\n}\n\nupdateGameDifficulty() {\n    if (this.gameState !== \"playing\") return;\n\n    const difficultyLevel = Math.floor(this.score / 200);\n    this.gameSpeed = this.initialGameSpeed + difficultyLevel * 0.5;\n    this.obstacleSpawnRate = Math.max(\n        this.minObstacleSpawnRate,\n        120 - difficultyLevel * 10,\n    );\n}\n```\n\nThe `difficultyLevel`\n\ncalculation increments every ~200 points, increasing both\nspeed and spawn frequency so the game keeps getting harder the longer the player\nsurvives.\n\nDrawing obstacles\n\nTo make the cacti feel less repetitive, each obstacle type gets a slightly\ndifferent silhouette. Add this method to the class, it can go just below the\n`drawDino()`\n\nmethod:\n\n``` js\ndrawObstacles() {\n    this.ctx.fillStyle = \"olive\";\n\n    for (let obstacle of this.obstacles) {\n        this.ctx.fillRect(\n            obstacle.x,\n            obstacle.y,\n            obstacle.width,\n            obstacle.height,\n        );\n\n        this.ctx.fillStyle = \"darkolivegreen\";\n        if (obstacle.type === \"cactus-small\") {\n            this.ctx.fillRect(obstacle.x - 3, obstacle.y + 10, 6, 4);\n            this.ctx.fillRect(obstacle.x + obstacle.width - 3, obstacle.y + 20, 6, 4);\n        } else if (obstacle.type === \"cactus-medium\") {\n            this.ctx.fillRect(obstacle.x - 4, obstacle.y + 8, 8, 6);\n            this.ctx.fillRect(obstacle.x + obstacle.width - 4, obstacle.y + 15, 8, 6);\n            this.ctx.fillRect(obstacle.x + obstacle.width / 2 - 2, obstacle.y + 25, 4, 8);\n        } else if (obstacle.type === \"cactus-wide\") {\n            this.ctx.fillRect(obstacle.x - 5, obstacle.y + 5, 10, 8);\n            this.ctx.fillRect(obstacle.x + obstacle.width - 5, obstacle.y + 10, 10, 8);\n            this.ctx.fillRect(obstacle.x + obstacle.width / 2 - 3, obstacle.y + 20, 6, 6);\n        }\n\n        this.ctx.fillStyle = \"olive\";\n    }\n}\n```\n\nAnimating the dino\n\nThe dino has two little legs that we can move up and down as it runs to make a\nvery simple running animation. Update the `drawDino()`\n\nmethod to the following:\n\n``` js\ndrawDino() {\n    const legOffset = this.gameState === \"playing\" && !this.dino.isJumping\n        ? (Math.floor(this.frameCount / 10) % 2) * 2\n        : 0;\n\n    this.ctx.fillStyle = \"green\";\n    this.ctx.fillRect(\n        this.dino.x,\n        this.dino.y,\n        this.dino.width,\n        this.dino.height,\n    );\n\n    this.ctx.fillStyle = \"darkgreen\";\n    this.ctx.fillRect(this.dino.x + 25, this.dino.y + 8, 4, 4);\n    this.ctx.fillRect(this.dino.x + 30, this.dino.y + 20, 8, 2);\n\n    if (!this.dino.isJumping) {\n        this.ctx.fillStyle = \"green\";\n        this.ctx.fillRect(\n            this.dino.x + 10,\n            this.dino.y + 40 + legOffset,\n            6,\n            8 - legOffset,\n        );\n        this.ctx.fillRect(\n            this.dino.x + 24,\n            this.dino.y + 40 - legOffset,\n            6,\n            8 + legOffset,\n        );\n    }\n}\n```\n\nRendering the game components\n\nWe now need to update the `render()`\n\nmethod to draw the obstacles and the dino,\nand to overlay the instruction and game-over screens when appropriate:\n\nUpdate the `render()`\n\nmethod so it clears the canvas, calls `drawObstacles()`\n\n,\nthen `drawDino()`\n\n, and overlays the instruction and game-over screens when\nappropriate:\n\n```\n  render() {\n    // Clear canvas\n    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);\n\n    // Draw obstacles\n    this.drawObstacles();\n\n    // Draw dino\n    this.drawDino();\n\n    // Draw instructions if waiting\n    if (this.gameState === \"waiting\") {\n      this.drawInstructions();\n    }\n\n    // Draw game over screen\n    if (this.gameState === \"gameOver\") {\n      this.drawGameOver();\n    }\n  }\n```\n\nHooking it up to physics, game start and reset\n\nWe now need to update the `startGame()`\n\nand `resetGame()`\n\nmethods to ensure the\nobstacle system is properly initialized and cleared when starting or resetting\nthe game.\n\nUpdate the `startGame()`\n\nmethod to reset the obstacles array when the game\nstarts, and set the game speed and frame count to their initial values:\n\n```\nstartGame() {\n  this.gameState = \"playing\";\n  this.score = 0;\n  this.gameSpeed = this.initialGameSpeed;\n  this.obstacles = [];\n  this.obstacleSpawnTimer = 0;\n  this.frameCount = 0;\n  this.updateScore();\n  this.updateStatus(\"\");\n}\n```\n\nThen update the `resetGame()`\n\nmethod to clear out any leftover obstacles so\nrestarting feels instant:\n\n```\nresetGame() {\n  this.gameState = \"waiting\";\n  this.score = 0;\n  this.gameSpeed = this.initialGameSpeed;\n  this.obstacles = [];\n  this.obstacleSpawnTimer = 0;\n  this.frameCount = 0;\n  this.dino.y = this.dino.groundY;\n  this.dino.velocityY = 0;\n  this.dino.isJumping = false;\n  this.updateScore();\n  this.updateStatus(\"Click to Start!\");\n  console.log(\"Game reset!\");\n}\n```\n\nAnd finally, add the obstacle updates and collision checks to the\n`updatePhysics()`\n\nmethod:\n\n```\nupdatePhysics() {\n    if (this.gameState !== \"playing\") return;\n\n    this.frameCount++;\n\n    // Apply gravity\n    this.dino.velocityY += this.gravity;\n    this.dino.y += this.dino.velocityY;\n\n    // Ground collision\n    if (this.dino.y >= this.dino.groundY) {\n      this.dino.y = this.dino.groundY;\n      this.dino.velocityY = 0;\n      this.dino.isJumping = false;\n    }\n\n    // Update score (continuous scoring)\n    this.score += 0.1;\n    this.updateScore();\n\n    // Update obstacles\n    this.updateObstacles();\n\n    // Check collisions\n    this.checkCollisions();\n\n    // Update difficulty\n    this.updateGameDifficulty();\n  }\n```\n\nGame over\n\nSo far we have the dino jumping and obstacles moving, but nothing happens when they collide. Let’s fix that!\n\nWhen a collision is detected, we need to end the game and display the game over\nscreen. Update the `gameOver()`\n\nmethod stub, to set the game state to\n“gameOver”, save the high score, and log a message:\n\n```\ngameOver() {\n   this.gameState = \"gameOver\";\n   this.saveHighScore();\n   this.updateHighScore();\n   this.updateStatus(\"Game Over! Click to restart\");\n   console.log(`Game Over! Final Score: ${Math.floor(this.score)}`);\n }\n```\n\nNext we’ll update the `drawGameOver()`\n\nmethod to overlay a simple game over\nmessage on the canvas:\n\n```\ndrawGameOver() {\n    // Semi-transparent overlay\n    this.ctx.fillStyle = \"rgba(0, 0, 0, 0.8)\";\n    this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);\n\n    // Game Over text\n    this.ctx.fillStyle = \"white\";\n    this.ctx.font = \"36px Arial\";\n    this.ctx.textAlign = \"center\";\n    this.ctx.fillText(\n      \"GAME OVER\",\n      this.canvas.width / 2,\n      this.canvas.height / 2 - 40,\n    );\n\n    // Final score\n    this.ctx.font = \"20px Arial\";\n    this.ctx.fillText(\n      `Final Score: ${Math.floor(this.score)}`,\n      this.canvas.width / 2,\n      this.canvas.height / 2 - 5,\n    );\n\n    // High score\n    if (Math.floor(this.score) === this.highScore && this.highScore > 0) {\n      this.ctx.fillStyle = \"gold\";\n      this.ctx.fillText(\n        \"🏆 NEW HIGH SCORE! 🏆\",\n        this.canvas.width / 2,\n        this.canvas.height / 2 + 25,\n      );\n    } else if (this.highScore > 0) {\n      this.ctx.fillStyle = \"#CCCCCC\";\n      this.ctx.fillText(\n        `High Score: ${this.highScore}`,\n        this.canvas.width / 2,\n        this.canvas.height / 2 + 25,\n      );\n    }\n\n    // Restart instruction\n    this.ctx.fillStyle = \"#FFFFFF\";\n    this.ctx.font = \"16px Arial\";\n    this.ctx.fillText(\n      \"Click or press SPACE to restart\",\n      this.canvas.width / 2,\n      this.canvas.height / 2 + 55,\n    );\n  }\n```\n\nHigh scores\n\nWe want players to feel rewarded for their best runs, so let’s implement a\nsimple high score system using `localStorage`\n\nto store scores. We’ll create some\nmethods to load, save, and update the high score, as well as modify the game\nover and reset logic to incorporate high score tracking. Update the stubbed\nmethods to your `DinoGame`\n\nclass:\n\n```\nloadHighScore() {\n    return parseInt(localStorage.getItem(\"dinoHighScore\")) || 0;\n}\n\nsaveHighScore() {\n    if (Math.floor(this.score) > this.highScore) {\n        this.highScore = Math.floor(this.score);\n        localStorage.setItem(\"dinoHighScore\", this.highScore);\n        console.log(`New High Score: ${this.highScore}!`);\n    }\n}\n\nupdateHighScore() {\n    if (this.highScoreElement) {\n        this.highScoreElement.textContent = this.highScore;\n    }\n}\n```\n\nNow we need to update the `init()`\n\nmethod to call the `updateHighScore()`\n\nmethod\nto and update the high score display when the game starts, add the following\nline to the `init()`\n\nmethod:\n\n```\nthis.updateHighScore();\n```\n\nFinalizing Stage 3\n\nOutside the class we keep the same health check from Stage 2 so you can verify the API route is alive when the page loads:\n\n``` js\nasync function checkHealth() {\n  try {\n    const response = await fetch(\"/api/health\");\n    const data = await response.json();\n    console.log(\"Server health check:\", data);\n  } catch (error) {\n    console.error(\"Health check failed:\", error);\n  }\n}\n\nwindow.addEventListener(\"load\", () => {\n  checkHealth();\n  new DinoGame();\n  console.log(\n    \"Stage 3 complete: Full game with obstacles and collision detection!\",\n  );\n});\n```\n\nWhen you run your game, you should see this!\n\nDeploy your updated game\n\nNow that we have a basic game loop, jumping dino, and score tracking, it’s time to deploy our updated game to the web! In your terminal you can run the following command to update the project you previously deployed in Stage 2:\n\n```\ndeno deploy\n```\n\nOnce deployed, you’ll have a fully functional dinosaur runner game where the dino can jump over obstacles, and the game ends upon collision. You’ll also have a URL that you can share to let others play your game!\n\nStage 4 is coming up soon, where we’ll connect the game to a backend so you can save scores server-side and open the door to multiplayer leaderboards. It’s going to get competitive!\n\n*What else are you building with Deno? Let us know on\nTwitter,\nBluesky, or\nDiscord.*", "url": "https://wpnews.pro/news/build-a-dinosaur-runner-game-with-deno-pt-3", "canonical_source": "https://deno.com/blog/build-a-game-with-deno-3", "published_at": "2025-12-22 15:00:00+00:00", "updated_at": "2026-05-22 12:21:01.214292+00:00", "lang": "en", "topics": ["developer-tools", "open-source"], "entities": ["Deno"], "alternates": {"html": "https://wpnews.pro/news/build-a-dinosaur-runner-game-with-deno-pt-3", "markdown": "https://wpnews.pro/news/build-a-dinosaur-runner-game-with-deno-pt-3.md", "text": "https://wpnews.pro/news/build-a-dinosaur-runner-game-with-deno-pt-3.txt", "jsonld": "https://wpnews.pro/news/build-a-dinosaur-runner-game-with-deno-pt-3.jsonld"}}