← Back to list

Creating an RPG Game with Flutter & Bonfire (Ep. 5) : Producing World Design via Lighting & Objects

Crafting a better atmosphere for your game through parallax backgrounds, lighting systems, and objects.

Trey Hope · 2024-12-14 15:10 · 98 claps · 9.0 min read paywalled
#flutter #bonfire #game-development #code #flame
Open on Medium ↗
Wiki topics: 📱 · Mobile Development 🛠️ · Crafts & DIY

Creating an RPG Game with Flutter & Bonfire (Ep. 5) : Producing World Design via Lighting & Objects

Crafting a better atmosphere for your game through parallax backgrounds, lighting systems, and objects.

YouTube thumbnail

YouTube thumbnail

In this tutorial, we’ll discover how to apply unique background effects to our game, as well as implement sophisticated lighting systems and objects that players can interact with — all of which will add more depth to our RPG.

If you want, you can watch the speed-run version of this video on my youtube.

[embed]

You can also follow along and download this branch from my repo here.

Overview

1. Build a Parallax Background

2. Create Non-Interactive Plant Objects

3. Create Non-Interactive World Objects

4. Create Interactive Chest Object

5. Create Sensor & Lighting Bonfire Object

By the end of this tutorial, the world will have much more depth and allow players to immerse themselves in this visually stunning game!

Finished product.

Finished product.

Step 1. Build a Parallax Background

A parallax effect is the apparent shift in an object’s position when viewed from different angles. In our game, we’ll create a layered background where each layer moves at a different speed, creating the illusion that the player is moving through a real environment. First, open Sprite Fusion. Export the sky, clouds, mountains, and hills as separate PNGs. Set these four layers to hidden on the map.

Create a new layer named Height. Place 10 tiles vertically anywhere on the map. This maintains the map’s height since we are no longer using the background from the map JSON.

Save the map and export it as JSON, then replace the existing files in your project.

Add the four PNG files to the assets/images/background directory in your project. Then update the pubspec.yaml file to include the new directory.

Create a new file called parallax_background.dart in the lib/presentation/game/backgrounds directory. In this file, define final variables for each of the four background components.

// lib/presentation/game/backgrounds/parallax_background.dart

class ParallaxBackground extends GameBackground {
  final _sky = ParallaxImageData('background/sky.png');
  final _clouds = ParallaxImageData('background/clouds.png');
  final _mountains = ParallaxImageData('background/mountains.png');
  final _hills = ParallaxImageData('background/hills.png');
}

Create a method named _addParallax that will handle adding the parallax background components.

// lib/presentation/game/backgrounds/parallax_background.dart

void _addParallax() async {
  final parallaxComponent = await loadParallaxComponent(
    [_sky, _clouds],
    baseVelocity: Vector2(10, 0),
    velocityMultiplierDelta: Vector2(1.5, 1),
  );

  add(parallaxComponent);

  final cameraParallaxComponent = await loadCameraParallaxComponent(
    [_mountains, _hills],
    baseVelocity: Vector2(10, 0),
    velocityMultiplierDelta: Vector2(4, 1),
  );

  add(cameraParallaxComponent);
}

Override the onMount method and call _addParallax to initialize the parallax effect when the component mounts.

// lib/presentation/game/backgrounds/parallax_background.dart

@override
void onMount() {
  _addParallax();
}

In the MyCoolGame class, add the ParallaxBackground by setting it as the background parameter of the BonfireWidget.

// lib/presentation/game/my_cool_game.dart

background: ParallaxBackground()

Test the app. You should now see the parallax effect in action, with each layer moving at a different speed — the sky moves slowest, followed by clouds, then mountains, and finally the hills moving fastest.

Step 2. Create Non-Interactive Plant Objects

Now it’s time to create the plants within the game. These will be objects that won’t have collision detection attached to them, as we want the player to move freely through them. Open Sprite Fusion. Import the plant marker. Create a new layer called Plant. Place the marker in several locations on the map.

Save the map, export it as JSON, and replace the existing files in your project.

Add the 17 plant sprites to the assets/images/plants directory in your project. Then update the pubspec.yaml file to include this new directory.

Create a new plant.dart file in the domain/entities/objects directory. Each plant will use a random sprite numbered between 0 and 17.

// lib/domain/entities/objects/plant.dart

class Plant extends GameDecoration {
  Plant({required super.position})
      : super.withSprite(
          sprite: Sprite.load(
            'vegetation/${Random().nextInt(18)}.png',
          ),
          size: Vector2.all(
            Globals.tileSize,
          ),
        );
}

Update the objectsBuilder map in MyCoolGame to accept the new Plant object.

// lib/presentation/game/my_cool_game.dart

'Plant': (properties) => Plant(
  position: properties,
)

Run the app to see the randomly generated plants scattered throughout the map.

Step 3. Create Non-Interactive World Objects

The plants added a nice touch to the world, but now let’s add a different type of object to our game. This object will behave similarly to the plant but will include collision detection, preventing our player from walking through it. Open Sprite Fusion and import the object marker. Create a new layer called World Object. Place the marker in several locations on the map.

Save the map, export it as JSON, and replace the existing files in your project. Next, add the 6 object sprites to the assets/images/world_object directory. Then update the pubspec.yaml file to include this directory.

In the Vector2Extensions class, update the original sizeToHitbox method to create two separate methods — one for actors and one for objects.

// lib/domain/core/extensions/vector2_extensions.dart

extension Vector2Extensions on Vector2 {
  RectangleHitbox actorHitbox() => RectangleHitbox(
        size: Vector2.all(x / 2),
        position: Vector2(x / 4, y / 2),
        isSolid: true,
      );

  RectangleHitbox objectHitbox() => RectangleHitbox(
        size: Vector2.all(x / 2),
        position: Vector2.all(x / 3),
        isSolid: true,
      );
}

Create a new world_object.dart file in the domain/entities/objects directory. This object will use a randomly selected sprite numbered between 0 and 5. We’ll also add a hitbox to handle collisions.

// lib/domain/entities/objects/world_object.dart

class WorldObject extends GameDecoration {
  WorldObject({required super.position})
      : super.withSprite(
          sprite: Sprite.load(
            'world_object/${Random().nextInt(6)}.png',
          ),
          size: Vector2.all(
            Globals.tileSize,
          ),
        );

  @override
  Future onLoad() {
    add(size.objectHitbox());
    return super.onLoad();
  }
}

Same thing as before, update the objectsBuilder map in MyCoolGame.

// lib/presentation/game/my_cool_game.dart

'World Object': (properties) => WorldObject(
  position: properties,
)

Run the app to see the randomly generated objects scattered throughout your game world.

Step 4. Create Interactive Chest Object

The previous two objects were good for visual display, but we also want the player to be able to interact with some objects within the world. For example, players should be able to open chests to find additional items. Open Sprite Fusion and import the chest marker. Create a new layer called Chest. Place the marker in several locations on the map.

Save the map and export it as JSON to replace the existing project files. Add the 10 chest sprites to the assets/images/chest directory in your project. Then update the pubspec.yaml file to include this new directory.

Create three sprite animation states for the Chest object: closed, opening, and open.

// lib/presentation/game/animations/sprite_animations.dart

class _Chest {
  Future get closed async => SpriteAnimation.spriteList(
    [await Sprite.load('chest/0.png')],
    stepTime: Globals.spriteStepTime,
  );

  Future get opening async => _spriteAnimation(
    count: 10,
    path: 'chest',
  );

  Future get open async => SpriteAnimation.spriteList(
    [await Sprite.load('chest/9.png')],
    stepTime: Globals.spriteStepTime,
  );
}

Create a new chest.dart file in the domain/entities/objects directory. This class will extend GameDecoration and include a Vision mixin.

// lib/domain/entities/objects/chest.dart

class Chest extends GameDecoration with Vision {
  static const _positionBuffer = 16;

  Chest({required Vector2 position})
      : super.withAnimation(
          animation: SpriteAnimations.chest.closed,
          size: Vector2(
            Globals.tileSize + _positionBuffer,
            Globals.tileSize,
          ),
        );

  @override
  Future onLoad() {
    add(size.objectHitbox());
    return super.onLoad();
  }
}

Update the objectsBuilder map in MyCoolGame to accept the new Chest object.

// lib/presentation/game/my_cool_game.dart

'Chest': (properties) => Chest(
  position: properties,
),

Run the app to verify that treasure chests now appear throughout the game world.

Now that we can see the chest in our game, let’s add UI feedback that shows when a chest is close enough for the player to open it. Update the Chest class by adding two new properties: observedPlayer and isOpen.

// lib/domain/entities/objects/chest.dart

bool _observedPlayer = false;
bool isOpen = false;

Update the update method by implementing the seeComponent method. This will trigger callbacks when the Chest is within one tile size of the player.

// lib/domain/entities/objects/chest.dart

@override
void update(double dt) {
  if (gameRef.player != null) {
    seeComponent(
      gameRef.player!,
      observed: (player) => _observedPlayer = true,
      notObserved: () => _observedPlayer = false,
      radiusVision: Globals.tileSize,
    );
  }
  super.update(dt);
}

Update the render method to draw or remove a white outline around the chest based on the observedPlayer state.

// lib/domain/entities/objects/chest.dart

@override
void render(Canvas canvas) {
  super.render(canvas);

  if (_observedPlayer && !isOpen) {
    showAnimationStroke(
      Colors.white,
      2,
      offset: Vector2(1, -1),
    );
  } else {
    hideAnimationStroke();
  }
}

Run the app to test it. You’ll see chests glow with a white highlight whenever your player character gets close to them.

Let’s add the ability for players to open chests. Create a new method called openChest. This method will set the isOpen variable to true, play the opening animation, and then display a toast message.

// lib/domain/entities/objects/chest.dart

void openChest() async {
  if (_observedPlayer) {
    isOpen = true;

    setAnimation(
      await SpriteAnimations.chest.opening,
      loop: false,
      onFinish: () async {
        setAnimation(await SpriteAnimations.chest.open);

        ModalService.showToast(
          title: 'You received a new item!',
          type: ToastificationType.success,
          icon: Icon(MdiIcons.flask),
        );
      },
    );
  }
}

Back in the DwarfWarrior class, add a property for saving the most recent Chest.

// lib/domain/entities/players/dwarf_warrior.dart

Chest? _recentChest;

Add an onCollision override to track the nearest chest when the DwarfWarrior comes into contact with it.

// lib/domain/entities/players/dwarf_warrior.dart

@override
void onCollision(Set intersectionPoints, PositionComponent other) {
  if (other is Chest) {
    _chestClose = other;
  }

  super.onCollision(intersectionPoints, other);
}

Update the xAction method to open the chest when the chest exists and hasn’t been opened yet.

// lib/domain/entities/players/dwarf_warrior.dart

void _xAction() {
  if (_chestClose != null && !_chestClose!.isOpen) {
    _chestClose!.openChest();
  }
}

Test the app to see how you can now walk up to chests and interact with them in the game world.

Step 5. Create Sensor & Lighting Bonfire Object

We’ve covered several different types of objects so far, but there’s one more to add. This object focuses on harming the player while also providing light (despite being outdoors). Open Sprite Fusion. Import the fire marker. Create a new layer called Bonfire. Place the marker in several locations on the map, (preferably in front of the dark tiles).

Save the map and export as JSON to replace the existing project files. Add the 6 bonfire sprites to the assets/images/bonfire directory in your project. Then update the pubspec.yaml file to include this new directory.

Create sprite animations to bring the Bonfire to life.

// lib/presentation/game/animations/sprite_animations.dart

class _Bonfire {
  Future get idle async => _spriteAnimation(
    count: 6,
    path: 'bonfire',
  );
}

Create a new bonfire.dart file in the domain/entities/objects directory. This class will extend GameDecoration with a Sensor mixin.

// lib/domain/entities/objects/bonfire.dart

class Bonfire extends GameDecoration with Sensor {
  static const _positionBuffer = 16;
  static const _damage = 5.0;

  Bonfire({required Vector2 position})
      : super.withAnimation(
          animation: SpriteAnimations.bonfire.idle,
          size: Vector2(
            Globals.tileSize,
            Globals.tileSize + _positionBuffer,
          ),
          position: Vector2(
            position.x,
            position.y - _positionBuffer,
          ),
        ) 
}

Add setSensorInterval to the constructor to control how often the sensor checks for contact.

// lib/domain/entities/objects/bonfire.dart

{
  setSensorInterval(1000);
}

Add setupLighting to the constructor to create a luminous glow around each Bonfire.

// lib/domain/entities/objects/bonfire.dart

setupLighting(
  LightingConfig(
    radius: width,
    color: Colors.yellow.withOpacity(0.3)
  ),
);

Override the onContact method to perform three actions: display damage taken using showDamage, trigger the hurt animation for the DwarfWarrior, and remove life from the DwarfWarrior’s health.

// lib/domain/entities/objects/bonfire.dart

@override
void onContact(DwarfWarrior component) {
  component.showDamage(
    _damage,
    config: const TextStyle(
      color: Colors.black,
      fontWeight: FontWeight.bold,
    ),
  );

  component.playOnceOther(
    other: PlatformAnimationsOther.hurt,
  );

  component.removeLife(_damage);

  super.onContact(component);
}

In MyCoolGame, set the lightingColorGame property on the BonfireWidget.

// lib/presentation/game/my_cool_game.dart

lightingColorGame: Colors.black.withOpacity(0.01),

Rinse and repeat; update the objectsBuilder map in MyCoolGame to accept the new Bonfire object.

// lib/presentation/game/my_cool_game.dart

'Bonfire': (properties) => Bonfire(
  position: properties,
),

Run the app to test it. You should now see animated bonfires throughout the game world.

We’ve successfully enhanced our game world with diverse objects that players can either interact with or pass by.

Thank you for following along with this tutorial! If you found it helpful, please give it a 👏 and share it with others. I’d love to hear your thoughts in the comments below!!


메타데이터
post_id
91a42cabe6de
slug
creating-an-rpg-game-with-flutter-bonfire-ep-5-producing-world-design-via-lighting-objects-91a42cabe6de
url
https://medium.com/@treyhope/creating-an-rpg-game-with-flutter-bonfire-ep-5-producing-world-design-via-lighting-objects-91a42cabe6de
canonical_url
https://medium.com/@treyhope/creating-an-rpg-game-with-flutter-bonfire-ep-5-producing-world-design-via-lighting-objects-91a42cabe6de
author_url
https://medium.com/@treyhope
status
ok
fetched_at
2026-07-21 16:05:26