Building a Car Race Game with Flutter and Flame [Part 1]

Amitsingh
8 min readJun 4, 2023

--

Introduction:

Flame is like a secret weapon for creating games in Flutter. It’s a game engine built specifically for Flutter apps, and it simplifies the process of building 2D games. With Flame, you can easily handle game loops, rendering graphics, and handling user input. It’s like having a cheat code that makes game development in Flutter faster and more fun.

Setting Up the Project:

We will use Flame and Flame Audio package in our project :

If You want to download complete code first then check this repo:

want to play game?:

https://carerace-1d32c.web.app/#/

Game Structure and Game Loop:

Game Structure:

Think of a game as a well-organized party. Just like a party has different elements such as guests, decorations, and activities, a game also has its own components. In a game, you have things like characters, objects, backgrounds, and rules that define how the game works. These components work together to create an enjoyable experience for the players.

Game Loop:

The game loop is like the heartbeat of a game. It’s a continuous process that keeps everything in the game running smoothly. Imagine you’re playing a game where you control a car. The game loop ensures that the car keeps moving, the obstacles keep coming, and everything happens in sync.

Creating the Game Elements:

Background

Now we will start to define our game background Where all the objects would be placed later.

class BackGround extends ParallaxComponent<CarRace> {
double backgroundSpeed = 2; // Initial speed value
@override
FutureOr<void> onLoad() async {
parallax = await gameRef.loadParallax(
[
ParallaxImageData('game/road1.png'),
ParallaxImageData('game/road1.png'),
],
fill: LayerFill.width,
repeat: ImageRepeat.repeat,
baseVelocity: Vector2(0, -70 * backgroundSpeed.toDouble()),
velocityMultiplierDelta: Vector2(0, 1.2 * backgroundSpeed),
);
}
}

Background class extends the ParallaxComponent class. The ParallaxComponent class is part of the Flame game engine and helps us create scrolling backgrounds in our game.

Background! class is like the artist of the game, responsible for creating a scrolling background that gives our game some swag.

Inside the Background class, we have a speed demon called backgroundSpeed. This dude determines how fast the background scrolls. We set it to 2, so the background moves at a moderate pace. Not too slow, not too fast, just right!

Now, let’s talk about the onLoad method. It's like the magic trick that happens when the background is loaded. Flames and sparks fly, and everything falls into place. Inside this method, we handle the setup for our background. It's like the DJ playing the right beats to set the mood.

Player

So, imagine we have a cool character in our game called the “Player.” This character can move left, right, or stay in the center.

enum PlayerState {
left,
right,
center,
}

let’s define our playerState as of now we have three state, our car can move left,right and stay on center of background.

class Player extends SpriteGroupComponent<PlayerState>
with HasGameRef<CarRace>, KeyboardHandler, CollisionCallbacks {
Player({
required this.character,
this.moveLeftRightSpeed = 700,
}) : super(
size: Vector2(79, 109),
anchor: Anchor.center,
priority: 1,
);
double moveLeftRightSpeed;
Character character;

int _hAxisInput = 0;
final int movingLeftInput = -1;
final int movingRightInput = 1;
Vector2 _velocity = Vector2.zero();

@override
FutureOr<void> onLoad() async {
await super.onLoad();
await add(CircleHitbox());
await _loadCharacterSprites();
current = PlayerState.center;
}

@override
void update(double dt) {
if (gameRef.gameManager.isIntro || gameRef.gameManager.isGameOver) return;

_velocity.x = _hAxisInput * moveLeftRightSpeed;

final double marioHorizontalCenter = size.x / 2;

if (position.x < marioHorizontalCenter) {
position.x = gameRef.size.x - (marioHorizontalCenter);
}
if (position.x > gameRef.size.x - (marioHorizontalCenter)) {
position.x = marioHorizontalCenter;
}

position += _velocity * dt;

super.update(dt);
}

@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollision(intersectionPoints, other);

gameRef.onLose();
return;
}

@override
bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
_hAxisInput = 0;

if (keysPressed.contains(LogicalKeyboardKey.arrowLeft)) {
moveLeft();
}

if (keysPressed.contains(LogicalKeyboardKey.arrowRight)) {
moveRight();
}

return true;
}

void moveLeft() {
_hAxisInput = 0;

current = PlayerState.left;

_hAxisInput += movingLeftInput;
}

void moveRight() {
_hAxisInput = 0; // by default not going left or right

current = PlayerState.right;

_hAxisInput += movingRightInput;
}

void resetDirection() {
_hAxisInput = 0;
}

void reset() {
_velocity = Vector2.zero();
current = PlayerState.center;
}

void resetPosition() {
position = Vector2(
(gameRef.size.x - size.x) / 2,
(gameRef.size.y - size.y) / 2,
);
}

Future<void> _loadCharacterSprites() async {
final left = await gameRef.loadSprite('game/${character.name}.png');
final right = await gameRef.loadSprite('game/${character.name}.png');
final center = await gameRef.loadSprite('game/${character.name}.png');

sprites = <PlayerState, Sprite>{
PlayerState.left: left,
PlayerState.right: right,
PlayerState.center: center,
};
}
}

Our Player class extends something called SpriteGroupComponent<PlayerState>. Think of it as the choreographer who manages the dance moves of our character.

Now, let’s talk about some interesting moves our Player can make:

  • They can move left or right at a certain speed, controlled by the moveLeftRightSpeed variable. It's like giving our character some turbo boost or slow-mo powers!
  • The onLoad method is where the Player gets ready to rock and roll. We load their sprites, set up hitboxes for collisions, and start them at the center. It's like their backstage preparation for the big performance!

During the game, the update method keeps our character moving smoothly. They can't go off-screen, so we make sure they stay within bounds. It's like keeping them on the dance floor, so they don't get lost in the crowd!

But hey, collisions can happen! If our Player collides with something, like an enemy or an obstacle, the onCollision method is triggered. It's like a "Uh-oh, you messed up!" moment, and we call the onLose function to handle the consequences.

Now, let’s talk about the dance moves controlled by the keyboard:

  • The onKeyEvent method listens for key events. If the left or right arrow keys are pressed, we call the corresponding move functions: moveLeft or moveRight. It's like our Player taking dance instructions from the keyboard!

Inside the move functions, we set the appropriate direction for our Player’s dance move and update the hAxisInput variable accordingly. It's like telling our character which way to sway and groove!

We also have some other handy functions like resetDirection, reset, and resetPosition that help manage the dance routine. They reset certain variables or position the Player back to the starting point. It's like pressing the "reset" button to start the dance from scratch!

Finally, we load the character sprites in the _loadCharacterSprites method. We load different sprites for the left, right, and center positions. It's like giving our Player a wardrobe of dance costumes to wear during the performance!

PlatForm and our Enemy

Now we will define our Enemy who will kill us in game

But let’s define first our platform:

abstract class Competitor<T> extends SpriteGroupComponent<T>
with HasGameRef<CarRace>, CollisionCallbacks {
final hitbox = RectangleHitbox();

double direction = 1;
final Vector2 _velocity = Vector2.zero();
double speed = 150;

Competitor({
super.position,
}) : super(
size: Vector2.all(80),
priority: 2,
);

@override
Future<void>? onLoad() async {
await super.onLoad();

await add(hitbox);

final points = getRandomPostionOfEnemy();

position = Vector2(points.xPoint, points.yPoint);
}

void _move(double dt) {
_velocity.y = direction * speed;

position += _velocity * dt;
}

@override
void update(double dt) {
_move(dt);
super.update(dt);
}

({double xPoint, double yPoint}) getRandomPostionOfEnemy() {
final random = Random();
final randomXPoint =
50 + random.nextInt((gameRef.size.x.toInt() - 100) - 50);

final randomYPoint = 50 + random.nextInt(60 - 50);

return (
xPoint: randomXPoint.toDouble(),
yPoint: randomYPoint.toDouble(),
);
}
}

Imagine you’re creating a game where you have competitors that move around. To make things easier, We created a class called Competitor. This class will be the base for all your competitors in the game.

Now, each competitor needs to have a hitbox, which is like an invisible area that represents their shape and size. You use a RectangleHitbox to define this area.

To give your competitors some movement, we added variables like direction, velocity, and speed. These variables control how the competitors move. It’s like saying, “Hey, move in this direction with this speed!”

In the onLoad method, we set up the competitor. we added the hitbox to it and set its initial position. The position is determined by a random point generated by the getRandomPositionOfEnemy function. This adds some randomness to the game, making the competitors appear in different places each time you play.

During the game, the update method is called repeatedly. It updates the competitor’s position based on its velocity and direction. This ensures that the competitor keeps moving smoothly on the screen.

The getRandomPositionOfEnemy function generates random X and Y coordinates within specified ranges. This way, each competitor appears at a different position, making the game more dynamic and unpredictable.

Overall, the Competitor class provides a basic structure for creating moving competitors in your game. You can extend this class and customize it further to add more specific behaviors and features to your competitors.

Now let’s define our first Enemy:

class EnemyPlatform extends Competitor<EnemyPlatformState> {
EnemyPlatform({super.position});

final List<String> enemy = [
'enemy_1',
'enemy_2',
'enemy_3',
'enemy_4',
'enemy_5'
];

@override
Future<void>? onLoad() async {
int enemyIndex = Random().nextInt(enemy.length);

String enemySprite = enemy[enemyIndex];

sprites = <EnemyPlatformState, Sprite>{
EnemyPlatformState.only:
await gameRef.loadSprite('game/$enemySprite.png'),
};

current = EnemyPlatformState.only;

return super.onLoad();
}
}

Imagine we have enemies in your game that are also platforms. They move around and can be collided with. To create these enemy platforms, we use a class called EnemyPlatform.

The EnemyPlatform class extends the Competitor class. Competitor is like a base class that provides the basic behavior for moving competitors in our game.

Inside the EnemyPlatform class, we have a list of enemy names. Each enemy has a unique sprite (like an image) associated with it.

In the onLoad method, something exciting happens! we randomly choose an enemy from the list. It’s like spinning a wheel and getting a surprise enemy!

Once we have the chosen enemy, we load its sprite image using the gameRef. It’s like putting on the enemy’s costume to make them look scary or funny.

Then, we assign the loaded sprite to the current state of the enemy platform. It’s like saying, “Hey, this enemy platform should use this sprite right now!”

Finally, we call the super.onLoad() method to finish setting up the enemy platform and make it ready to be used in the game.

Conclusion:

In this first part of the tutorial, we covered the basics of creating a car race game using Flutter and Flame. We explored concepts such as game structure, game loop, backgrounds, and player movement. We also introduced the use of components, collision detection, and handling user input.

With the completion of this part, you have a foundation for building a car race game. Stay tuned for part 2, where we will cover more features, such as audio, obstacles, and scoring. Get ready to take your game to the next level!

Part 2: https://medium.com/@amitsingh506142/building-a-car-race-game-with-flutter-and-flame-part-2-7137e4de0ce1

LinkeDin: https://www.linkedin.com/in/amit-singh-023055193/

StackOverflow: https://stackoverflow.com/users/13051247/amit-singh

Github: https://github.com/amitsingh6391

--

--