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.
- dino.png
- cactus.png
- background.png
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
- Add running animations
- Add sound effects
- Create multiple obstacle types
- Add clouds and moving backgrounds
- Create a restart button
- Add high score saving
- Add particle effects while jumping
- Create day and night cycles
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.