NashTech Insights

Make a mini game by Flutter Flame

Picture of thachphamngoc
thachphamngoc
Table of Contents
photograph of men having conversation seating on chair

What is Flutter Flame?

Flame is a cross-platform game library for Flutter, enabling developers to build simple and powerful mobile and web games. Built on top of the Flutter framework, Flame provides an easy and efficient approach to creating games with high performance across multiple devices.

Features of Flame

Sprites and Animation: Flame offers easy-to-use features for handling sprites and animations. This allows developers to create visually appealing characters and graphics effects for their games.

Simple Controls: Flame provides simple control tools such as touch, swipe, and accelerometer support for interacting with games. This ensures intuitive gameplay experiences for players.

High Performance: Flame is optimized for high performance on mobile devices, delivering smooth frame rates and quick response times. This ensures a seamless gaming experience even on lower-end devices.

Audio and Music Support: Flame provides APIs for loading and playing sound and music files, making games more immersive and engaging.

Installation

Add the flame package as a dependency in your pubspec.yaml by running the following command:


					flutter pub add flame
				

Some main classes that are commonly used to build games

GameWidget: GameWidget is a widget provided by Flame that allows you to integrate a Flame game into your Flutter application. It serves as the bridge between your Flutter UI and the Flame game engine.

FlameGame: This is the base class for creating games in Flame. It provides a framework for managing game loops, rendering graphics, handling input events, and managing game state.

Component: The Component class represents any object that can be rendered or updated in the game world. It is the base class for all game elements such as sprites, animations, and UI elements.

SpriteComponent: This class extends Component and represents a graphical sprite in the game world. It is used to display images, animations, or other graphical elements.

AnimationComponent: This class extends SpriteComponent and represents an animated sprite. It is used to display sprite animations created from a series of images.

TextComponent: This class extends Component and represents a text element in the game world. It is used to display text strings with customizable fonts, sizes, and colors.

CameraComponent: This class represents the camera in the game world. It is used to define the view area and position of the camera, and it can be used to implement scrolling and zooming effects.

InputComponent: This class extends Component and represents an input area in the game world. It is used to handle input events such as taps, swipes, and keyboard input.

Example game by Flame

To run the code sample, please prepare an environment to work with Flutter by downloading Flutter SDK. It also need an IDE like Android Studio or Visual Studio Code for coding

Steps to run the sample:

1. Create a new Flutter project on Android Studio

2. Add Flame library by command line


					flutter pub add flame
				

3. In main.dart file, copy below code and paste them in the file


					import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:async';
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/events.dart';
import 'dart:math' as math;
import 'package:flutter_animate/flutter_animate.dart';
const gameWidth = 820.0;
const gameHeight = 1600.0;
const ballRadius = gameWidth * 0.06;
const movingBoxSize = gameWidth * 0.12;
void main() {
  runApp(const GameApp());
}
class GameApp extends StatefulWidget {         
  const GameApp({super.key});
  @override                                                   
  State createState() => _GameAppState();
}
class _GameAppState extends State {
  late final DodgeTheBoxes game;
  @override
  void initState() {
    super.initState();
    game = DodgeTheBoxes();
  }                                                     
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        textTheme: GoogleFonts.pressStart2pTextTheme().apply(
          bodyColor: const Color(0xff184e77),
          displayColor: const Color(0xff184e77),
        ),
      ),
      home: Scaffold(
        body: Container(
          decoration: const BoxDecoration(
            gradient: LinearGradient(
              begin: Alignment.topCenter,
              end: Alignment.bottomCenter,
              colors: [
                Color(0xffa9d6e5),
                Color(0xfff2e8cf),
              ],
            ),
          ),
          child: SafeArea(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Center(
                child: Column(                                
                  children: [
                    ScoreCard(score: game.score),
                    Expanded(
                      child: FittedBox(
                        child: SizedBox(
                          width: gameWidth,
                          height: gameHeight,
                          child: GameWidget(
                            game: game,
                            overlayBuilderMap: {
                              PlayState.welcome.name: (context, game) =>
                              const OverlayScreen(
                                title: 'TAP TO PLAY',
                                subtitle: 'Tap to bounce',
                              ),
                              PlayState.gameOver.name: (context, game) =>
                              const OverlayScreen(
                                title: 'G A M E   O V E R',
                                subtitle: 'Tap to Play Again',
                              ),
                            },
                          ),
                        ),
                      ),
                    ),
                  ],
                ),                                            
              ),
            ),
          ),
        ),
      ),
    );
  }
}
class PlayArea extends RectangleComponent with HasGameReference {
  PlayArea()
      : super(
    paint: Paint()..color = const Color(0xfff2e8cf),
    children: [RectangleHitbox()],                       
  );
  @override
  FutureOr onLoad() async {
    super.onLoad();
    size = Vector2(game.width, game.height);
  }
}
enum PlayState { welcome, playing, gameOver }            
class DodgeTheBoxes extends FlameGame
    with HasCollisionDetection, KeyboardEvents, TapDetector {
  DodgeTheBoxes()
      : super(
    camera: CameraComponent.withFixedResolution(
      width: gameWidth,
      height: gameHeight,
    ),
  );
  final ValueNotifier score = ValueNotifier(0);          
  final rand = math.Random();
  double get width => size.x;
  double get height => size.y;
  late PlayState _playState;
  PlayState get playState => _playState;
  set playState(PlayState playState) {
    _playState = playState;
    switch (playState) {
      case PlayState.welcome:
      case PlayState.gameOver:
        overlays.add(playState.name);
        break;
      case PlayState.playing:
        overlays.remove(PlayState.welcome.name);
        overlays.remove(PlayState.gameOver.name);
    }
  }
  @override
  FutureOr onLoad() async {
    super.onLoad();
    camera.viewfinder.anchor = Anchor.topLeft;
    world.add(PlayArea());
    playState = PlayState.welcome;
  }
  void bounceTheBall() {
    List list = world.children.query();
    if(list.isNotEmpty) {
      list[0].bounce();
    }
  }
  void startGame() {
    if (playState == PlayState.playing) return;
    world.removeAll(world.children.query());
    world.removeAll(world.children.query());
    world.removeAll(world.children.query());
    world.removeAll(world.children.query());
    playState = PlayState.playing;
    score.value = 0;                                            
    world.add(Ball(Vector2(width / 3, height / 1.5 - ballRadius / 2)));
    world.add(Ground(Vector2(0, height / 1.5), Vector2(width, 30)));
    world.add(MovingBox(width, height / 1.5 - movingBoxSize));
    world.add(RectTrigger(Vector2(width / 3 - ballRadius, 0), Vector2(10, height)));
  }
  @override
  void onTap() {
    super.onTap();
    startGame();
    bounceTheBall();
  }
  @override
  Color backgroundColor() => const Color(0xfff2e8cf);
}
class Ball extends CircleComponent with CollisionCallbacks {
  bool isFalling = true;
  double velocityY = 0;
  final gravity = 600;
  Ball(Vector2 position) : super(
    radius: ballRadius,
    anchor: Anchor.center,
    position: position,
    paint: Paint()
      ..color = const Color(0xff1e6091)
      ..style = PaintingStyle.fill,
    children: [RectangleHitbox()],);
  void bounce() {
    isFalling = true;
    velocityY = -500;
  }
  @override
  void update(double dt) {
    super.update(dt);
    // Falling by gravity
    if(isFalling) {
      velocityY += dt * gravity;
      position.y += velocityY * dt;
    }
  }
  @override
  void onCollisionStart(Set intersectionPoints, PositionComponent other) {
    if(other is Ground) {
      isFalling = false;
      velocityY = 0;
      position.y = other.position.y - size.y / 2;
    }
    super.onCollisionStart(intersectionPoints, other);
  }
}
class RectTrigger extends RectangleComponent with CollisionCallbacks, HasGameReference {
  RectTrigger(Vector2 position, Vector2 size) :
        super(position: position, size:  size,
        anchor: Anchor.topRight,
        paint: Paint()
          ..color = const Color(0x001e6091)
          ..style = PaintingStyle.fill,
        children: [RectangleHitbox()],
      );
  @override
  void onCollisionStart(Set intersectionPoints, PositionComponent other) {
    game.score.value += 1;
    super.onCollisionStart(intersectionPoints, other);
  }
}
class Ground extends RectangleComponent with CollisionCallbacks {
  Ground(Vector2 position, Vector2 size) : super(
    size: size,
    anchor: Anchor.topLeft,
    position: position,
    paint: Paint()
      ..color = const Color(0xff163832)
      ..style = PaintingStyle.fill,
    children: [RectangleHitbox()],);
}
class MovingBox extends RectangleComponent with CollisionCallbacks, HasGameReference {
  double startX, groundY;
  MovingBox(this.startX, this.groundY) : super(
    size: Vector2(movingBoxSize, movingBoxSize),
    anchor: Anchor.topLeft,
    position: Vector2(startX, groundY),
    paint: Paint()
      ..color = const Color(0xfff44336)
      ..style = PaintingStyle.fill,
    children: [RectangleHitbox()],);
  @override
  void update(double dt) {
    super.update(dt);
    position.x -= 400 * dt;
    if(position.x + size.x < 0) {
      position.x = startX;
      if(math.Random().nextBool()) {
        position.y = groundY;
      } else {
        position.y = groundY - movingBoxSize * 1.5;
      }
    }
  }
  @override
  void onCollisionStart(Set intersectionPoints, PositionComponent other) {
    if(other is Ball) {
      add(RemoveEffect(
          delay: 0.35,
          onComplete: () { 
            game.playState = PlayState.gameOver;
          }));
    }
    super.onCollisionStart(intersectionPoints, other);
  }
}
class ScoreCard extends StatelessWidget {
  const ScoreCard({
    super.key,
    required this.score,
  });
  final ValueNotifier score;
  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder(
      valueListenable: score,
      builder: (context, score, child) {
        return Padding(
          padding: const EdgeInsets.fromLTRB(12, 6, 12, 18),
          child: Text(
            'Score: $score'.toUpperCase(),
            style: Theme.of(context).textTheme.titleLarge!,
          ),
        );
      },
    );
  }
}
class OverlayScreen extends StatelessWidget {
  const OverlayScreen({
    super.key,
    required this.title,
    required this.subtitle,
  });
  final String title;
  final String subtitle;
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: const Alignment(0, -0.15),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text(
            title,
            style: Theme.of(context).textTheme.headlineLarge,
          ).animate().slideY(duration: 750.ms, begin: -3, end: 0),
          const SizedBox(height: 16),
          Text(
            subtitle,
            style: Theme.of(context).textTheme.headlineSmall,
          )
              .animate(onPlay: (controller) => controller.repeat())
              .fadeIn(duration: 1.seconds)
              .then()
              .fadeOut(duration: 1.seconds),
        ],
      ),
    );
  }
}
				

4. Run the project on Simulator or Real device

 Explain some components in the code


					class DodgeTheBoxes extends FlameGame
    with HasCollisionDetection, KeyboardEvents, TapDetector {
    ...
}
				

In the code sample, the DodgeTheBoxes class is the core class of the game. It inherits from FlameGame, which is a class provided by the Flame library for creating games. In the onLoad method, the game initializes its camera, sets up the play area (PlayArea), and sets the initial state to welcome. It adds overlays for the welcome and game over screens. The playState property manages the state of the game. It can be welcome, playing, or gameOver. Depending on the state, different overlay screens are shown.
The DodgeTheBoxes class creates and manages various game elements such as the ball, ground, moving box, and trigger area. These elements are added to the game world.
Gameplay Logic: Methods like bounceTheBall and startGame handle gameplay logic such as starting the game, bouncing the ball, and updating the score.


					class PlayArea extends RectangleComponent with HasGameReference {
  ...
}
class Ball extends CircleComponent with CollisionCallbacks {
    ...
}
class RectTrigger extends RectangleComponent with CollisionCallbacks, 
    HasGameReference {
    ...    
}
class Ground extends RectangleComponent with CollisionCallbacks {
  Ground(Vector2 position, Vector2 size) : super(
    ...
}
class MovingBox extends RectangleComponent with CollisionCallbacks, 
    HasGameReference {
    ...
}
				

The PlayArea, Ball, RectTrigger, Ground, MovingBox classes represent the graphical component in a game. They are used to create and manage shapes within the game world. They include collision detection functionality, allowing them to interact with other game elements. This enables developers to implement collision-based gameplay mechanics involving objects.

Future of Flame and Community Involvement

  1. Upcoming Features and Improvements:

Flame is continuously evolving, with developers actively working on new features and improvements to enhance its capabilities. Some of the anticipated developments include:

Enhanced Graphics: Future versions of Flame may introduce advanced graphics features, such as support for shaders, particle effects, and dynamic lighting, to enable developers to create even more visually stunning games.

Expanded Platform Support: While Flame already supports multiple platforms, including mobile and web, efforts are underway to further expand platform compatibility. This may involve optimizing Flame for emerging platforms or integrating with additional technologies.

Performance Enhancements: Developers are constantly striving to improve Flame’s performance, making optimizations to reduce resource usage and enhance overall efficiency. This ensures that games built with Flame continue to run smoothly on a wide range of devices.

New Tools and Utilities: Future releases of Flame may introduce new tools and utilities to streamline game development processes, such as debugging tools, asset management systems, and integration with external services.

  1. Community Strengths:

The Flame community plays a crucial role in the ongoing development and success of the library. Some key strengths of the community include:

Active Collaboration: Developers within the Flame community actively collaborate on projects, sharing code, ideas, and resources to help each other overcome challenges and achieve their goals.

Supportive Ecosystem: The Flame community is known for its supportive and inclusive nature, welcoming developers of all skill levels and backgrounds. Whether you’re a beginner seeking guidance or an experienced developer sharing your expertise, there’s a place for you in the Flame community.

Feedback and Contribution: Community members provide valuable feedback to the Flame development team, helping to identify bugs, suggest new features, and shape the direction of the library. Additionally, many developers contribute code, documentation, and tutorials to the Flame ecosystem, enriching the experience for everyone.

Educational Resources: The Flame community produces a wealth of educational resources, including tutorials, articles, videos, and sample projects, to help newcomers get started with game development using Flame. These resources play a crucial role in growing the community and empowering aspiring game developers.

Picture of thachphamngoc

thachphamngoc

Leave a Comment

Your email address will not be published. Required fields are marked *

Suggested Article