Creating Your First Web Game

Creating Your First Web Game

In the previous chapters, you learned what Flame Engine is and how to set up Flutter and Flame for browser game development. Now it is time to build your first real web game.

In this chapter, you will create a simple Dino Jump game inspired by endless runner games. The player controls a dinosaur that jumps over obstacles while the game speed slowly increases over time.

This project teaches many important game development concepts such as physics, gravity, jumping, collisions, game loops, sprites, score systems, difficulty balancing, and user interface design.

Even though this is a simple game, the same ideas are used in professional browser games.

What You Will Build

The game contains a running dinosaur character placed on the ground.

Obstacles move from right to left across the screen.

The player clicks the screen or presses the keyboard to make the dinosaur jump.

If the dinosaur hits an obstacle, the game ends.

As time passes, the game becomes faster and more difficult.

A score counter increases while the player survives.

Understanding the Core Parts of a Game

Before writing code, it is important to understand the major systems inside games.

Game Loop

The game loop is the heart of every video game. It is a continuous cycle that runs very fast while the game is open. During each cycle, the game engine calculates movement and redraws the screen.

Flame uses an update method to handle this loop. This method runs many times per second to keep the game alive.

@override
void update(double dt) {
  super.update(dt);
  // Game logic goes here
}

The dt value stands for delta time. This tells the game how much time passed since the last update. Using this value ensures that your game runs at the same speed on every computer.

Physics

Physics controls movement and forces inside the game world.

In this project, gravity pulls the dinosaur downward after jumping.

Without gravity, the dinosaur would float forever in the air.

Collision Detection

Collision detection checks if two objects touch each other.

When the dinosaur touches an obstacle, the game detects a hit and ends the round.

Sprites

Sprites are images used as game objects.

The dinosaur image, obstacle image, and background image are all sprites.

Difficulty Scaling

Games become more exciting when difficulty slowly increases.

In endless runner games, obstacles usually move faster over time.

This forces players to react quicker.

Setting Up Assets

Create these folders inside your project.

assets/
  images/

Add these images inside the images folder.

Register the assets inside pubspec.yaml.

flutter:
  # This registers the assets folder in your project
  assets:
    - assets/images/

Understanding the Game Design

The dinosaur stays near the left side of the screen.

Obstacles continuously appear from the right side and move left.

The player must jump before touching the obstacle.

If the dinosaur survives longer, the score increases.

The game speed slowly becomes faster which creates tension and challenge.

Full Game Code

Replace your main.dart file with this complete project code.

import 'dart:math';

import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';

void main() {
  // Start the Dino Game
  runApp(
    GameWidget(
      game: DinoGame(),
    ),
  );
}

// The main game class handling logic and events
class DinoGame extends FlameGame with TapDetector, KeyboardEvents {

  late SpriteComponent dino;
  late SpriteComponent cactus;
  late SpriteComponent background;

  // Vertical position of the ground
  double groundY = 300;

  double velocityY = 0;
  double gravity = 900;
  bool isJumping = false;

  double cactusSpeed = 250;

  int score = 0;

  final Random random = Random();

  late TextComponent scoreText;

  @override
  // Load images and prepare game objects
  Future<void> onLoad() async {

    background = SpriteComponent()
      ..sprite = await loadSprite('background.png')
      ..size = size
      ..position = Vector2(0, 0);

    add(background);

    dino = SpriteComponent()
      ..sprite = await loadSprite('dino.png')
      ..size = Vector2(80, 80)
      ..position = Vector2(100, groundY);

    add(dino);

    cactus = SpriteComponent()
      ..sprite = await loadSprite('cactus.png')
      ..size = Vector2(60, 80)
      ..position = Vector2(size.x + 200, groundY);

    add(cactus);

    scoreText = TextComponent(
      text: 'Score: 0',
      position: Vector2(20, 20),
      textRenderer: TextPaint(
        style: const TextStyle(
          color: Colors.white,
          fontSize: 28,
          fontWeight: FontWeight.bold,
        ),
      ),
    );

    add(scoreText);
  }

  @override
  // Update game physics and logic every frame
  void update(double dt) {
    super.update(dt);

    // Apply gravity to the dinosaur
    velocityY += gravity * dt;
    dino.position.y += velocityY * dt;

    // Prevent the dinosaur from falling through the floor
    if (dino.position.y >= groundY) {
      dino.position.y = groundY;
      velocityY = 0;
      isJumping = false;
    }

    // Move the cactus toward the left
    cactus.position.x -= cactusSpeed * dt;

    // Reset the cactus when it leaves the screen
    if (cactus.position.x < -100) {
      cactus.position.x = size.x + random.nextInt(300).toDouble();

      score += 1;

      // Increase speed as the score goes up
      cactusSpeed += 15;

      scoreText.text = 'Score: $score';
    }

    // Check for collisions between dino and cactus
    bool hitX =
        dino.position.x < cactus.position.x + cactus.size.x &&
        dino.position.x + dino.size.x > cactus.position.x;

    bool hitY =
        dino.position.y < cactus.position.y + cactus.size.y &&
        dino.position.y + dino.size.y > cactus.position.y;

    if (hitX && hitY) {
      // Pause the game if a collision happens
      pauseEngine();

      overlays.add('GameOver');
    }
  }

  // Make the dinosaur jump upward
  void jump() {
    if (!isJumping) {
      velocityY = -500;
      isJumping = true;
    }
  }

  @override
  // Handle mouse clicks or screen taps
  void onTap() {
    jump();
  }

  @override
  // Handle keyboard button presses
  KeyEventResult onKeyEvent(
    KeyEvent event,
    Set<LogicalKeyboardKey> keysPressed,
  ) {

    if (event is KeyDownEvent &&
        event.logicalKey == LogicalKeyboardKey.space) {
      jump();
    }

    return KeyEventResult.handled;
  }
}

Understanding the Main Function

The main function starts the Flutter application.

GameWidget displays the Flame game on the screen.

DinoGame becomes the main controller of the game world.

Understanding the Background

Games look much better with backgrounds.

Without backgrounds, the game feels empty and unfinished.

The background sprite fills the screen and creates visual atmosphere.

Even simple backgrounds make games feel more professional.

Understanding Sprites

Sprites are the visual images that you see inside a game. Every character or object that appears on the screen is usually a sprite. In this project, the dinosaur and the cactus are both sprites.

In Flame, you use a SpriteComponent to show these images. A component is like a container that holds the image and knows where it should be placed on the screen.

dino = SpriteComponent()
  ..sprite = await loadSprite('dino.png')
  ..size = Vector2(80, 80)
  ..position = Vector2(100, groundY);

The code above tells the game engine to load the dino image. It also sets the size and the starting position of the dinosaur. Without sprites, your game would just be a blank screen with no graphics.

Understanding Gravity

Gravity constantly pulls objects downward.

In this project, gravity increases the vertical speed every frame.

This creates natural jumping movement.

Without gravity, jumps would feel fake and unrealistic.

The code below applies gravity every frame.

// This adds gravity to the vertical speed
velocityY += gravity * dt;

Gravity increases downward velocity over time.

The dt value keeps movement smooth across different computers and browsers.

Understanding Jump Physics

Jumping involves moving the dinosaur upward and then letting gravity pull it back down. In digital games, we use vertical velocity to control this movement.

When the player jumps, we set a negative velocity. This moves the character toward the top of the screen because screen coordinates start from zero at the top.

void jump() {
  if (!isJumping) {
    velocityY = -500; // Move up fast
    isJumping = true; // Prevent double jump
  }
}

After the velocity is set, the update method slowly adds gravity to pull the dinosaur back to the ground. This creates a realistic arc that looks like a real jump.

Preventing Double Jumping

Players should not jump infinitely in the air.

The isJumping variable prevents repeated jumping while airborne.

This creates balanced gameplay.

Understanding Obstacles

Obstacles continuously move toward the dinosaur.

When the cactus leaves the screen, it returns from the right side again.

This creates the endless runner effect.

// This moves the cactus obstacle toward the left side
cactus.position.x -= cactusSpeed * dt;

The obstacle moves left every frame.

Increasing Difficulty

Games are more fun when they become harder as you play. This prevents the game from becoming boring. We increase the difficulty by making the obstacles move faster.

Every time the cactus goes off the screen and resets, we add a small amount of speed to the cactus.

if (cactus.position.x < -100) {
  cactus.position.x = size.x + random.nextInt(300).toDouble();
  score += 1;
  cactusSpeed += 15; // Increase speed
}

This small change forces the player to jump sooner and react faster. Over time, the game becomes very challenging and tests the skills of the player.

Understanding Collision Detection

Collision detection is how the game knows when two objects hit each other. In this game, we need to know if the dinosaur touches the cactus. If they touch, the game must end.

The game engine checks the position and size of both objects. If the boxes around the objects overlap, a collision occurs.

bool hitX = dino.position.x < cactus.position.x + cactus.size.x &&
            dino.position.x + dino.size.x > cactus.position.x;

bool hitY = dino.position.y < cactus.position.y + cactus.size.y &&
            dino.position.y + dino.size.y > cactus.position.y;

if (hitX && hitY) {
  pauseEngine(); // Stop the game
}

When both the horizontal and vertical positions overlap, it means a hit happened. We stop the game engine immediately so the player can see that the game is over.

Understanding Score Systems

Players enjoy games more when they can track progress.

The score increases every time the dinosaur successfully avoids an obstacle.

The text updates on the screen in real time.

Score systems encourage replayability because players want better results.

Creating Beautiful UI

Good games need clean and readable user interfaces.

Large text with strong contrast improves visibility.

Background images create atmosphere.

Proper spacing makes the screen easier to understand.

Even simple UI improvements can make a game feel polished.

Adding Mobile and Desktop Controls

Browser games should support different devices.

This project supports both mouse clicks and keyboard controls.

Mobile players can tap the screen.

Desktop players can press the space key.

Supporting multiple input methods improves accessibility.

Why This Game is Important

Even though this project looks simple, it teaches the foundation of many larger games.

Endless runners, platform games, action games, and arcade games all use similar systems.

Learning movement, collisions, physics, and rendering gives you the skills needed for advanced projects later.

Ideas for Improvement

Conclusion

In this chapter, you created your first complete browser game using Flutter and Flame.

You learned how game loops work, how physics creates jumping movement, how collision systems detect hits, how sprites display graphics, and how difficulty scaling keeps gameplay exciting.

You also learned how to create basic UI systems, support keyboard and touch controls, and organize game logic inside Flame.

This project is the beginning of your game development journey. The next chapters will expand these ideas into larger and more advanced systems.

← Previous Chapter Next Chapter 5 →