Search code examples
flutterflame

Navigate to a flutter widget from a flame game without losing the game state


My flame game concept is that a player moves, and when the player collides with a star object, a pop up flutter window contains a question shall appear, and when the user closes it, the game state and player position will be restored.How can I do so?

I tried Navigator.push but it doesn't work, it says that no context available :(

Also I tried a different way, which is the overlay, but the Star class cannot access the overlays property, I wrote a method in the main game class "PlayerWithBG" but calling it throws an exception...

This is the code inside Star class

import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/sprite.dart';
import 'package:flutter/material.dart';
import 'package:khalooq/Game/PlayerWithBG.dart';
import '../Questions/firstLevelQuestions.dart';
import 'helpers/directions.dart';

class Star extends SpriteAnimationComponent
    with HasGameRef, CollisionCallbacks {
  Star(Vector2 position)
      : super(size: Vector2.all(50), position: position, anchor: Anchor.center);

  final gameObject = PlayerWithBG();

  late final SpriteAnimation _rotateAnimation;
  final double _animationSpeed = .3;
  Direction direction = Direction.right;

  @override
  Future<void> onLoad() async {
    super.onLoad();
    await _loadAnimations().then((_) => {animation = _rotateAnimation});
    add(CircleHitbox());
  }

  Future<void> _loadAnimations() async {
    final spriteSheet = SpriteSheet.fromColumnsAndRows(
        image: await gameRef.images.load('stars1.png'), columns: 10, rows: 1);

    _rotateAnimation = spriteSheet.createAnimation(
        row: 0, stepTime: _animationSpeed, from: 0, to: 4);
  }

  @override
  void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollision(intersectionPoints, other);
    // gameObject.showQuestion();
    // calling the showQuestion() method throws an exception in the method
    /*
     * Exception has occurred.
      _AssertionError ('package:flame/src/game/overlay_manager.dart': Failed assertion: line 51 pos 7: '_builders.containsKey(name)': Trying to add an unknown overlay "Question")
     */

    removeFromParent();
    //This is what I want to do--------------------------------------------------
    // Navigator.push(
    //     context, MaterialPageRoute(builder: (context) => firstLevelQuestion()));
  }
}

And this is the code inside the main game class "PlayerWithBG"

import 'dart:ui';

import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flutter/services.dart';
import 'Player.dart';
import 'GameBG.dart';
import 'package:flutter/material.dart';

import 'Star.dart';
import 'helpers/directions.dart';

class PlayerWithBG extends FlameGame
    with KeyboardEvents, HasCollisionDetection, TapDetector {
  Player _player = Player();
  GameBG _gameBG = GameBG();

  @override
  Future<void> onLoad() async {
    super.onLoad();
    await add(_gameBG);
    double bgWidth = _gameBG.size.x;
    double bghight = _gameBG.size.y;
    await add(_player);
    _player.position = Vector2(bgWidth * 0.05, bghight * 0.95);
    camera.followComponent(_player,
        worldBounds: Rect.fromLTRB(0, 0, _gameBG.size.x, _gameBG.size.y));
    //--------------------------------------------------------------------------------------
    //Stars are the elements that should open the question when the player collides with them
    add(Star(Vector2(bgWidth * 0.10, bghight * 0.95)));
    add(Star(Vector2(bgWidth * 0.30, bghight * 0.95)));
  }

  void showQuestion() {
    if (overlays.isActive('Question')) {
      overlays.remove('Question');
      resumeEngine();
    } else {
      overlays.add('Question');
      pauseEngine();
    }
  }

  onArrowKeyChanged(Direction direction) {
    _player.direction = direction;
  }
}

And here where I call the game widget with a temp overlay container

          GameWidget(
            game: game..paused = false,
            //here is the overlayer to render the question--------------------
            overlayBuilderMap: {
              'Question': (BuildContext context, PlayerWithBG game) {
                return Center(
                  child: Container(
                    width: 100,
                    height: 100,
                    color: Colors.orange,
                    child: Center(child: Text('This is a question')),
                  ),
                );
              },
            },
            //-----------------------------------------------------------------
          ),

Solution

  • You can do this in a few different ways:

    1.Use the overlays system

    Just like you tried, but to access the overlays you can use the fact that you have the HasGameRef mixin on your component, which gives your access to the gameRef variable and thus gameRef.overlays.add/remove.

    You can read more about that here.

    2. Use the RouterComponent

    This technique is slightly more advanced, but it is definitely worth exploring if you will have a lot of different screens and scenes.

    Example:

    final router = RouterComponent(
      routes: {
        'ok-dialog': OverlayRoute(
          (context, game) {
            return Center(
              child: DecoratedContainer(...),
            );
          },
        ),  // OverlayRoute
        'confirm-dialog': OverlayRoute.existing(),
      },
    );
    
    router.pushNamed('ok-dialog);
    

    You can read a lot more about that here.

    3. Use the normal Flutter navigation

    The last, but least ergonomic way, is to use Flutter's built-in navigation (or another navigation package). You can get the context in the same way as you accessed the overlays, when you have the HasGameRef mixin:

    gameRef.buildContext;
    

    Depending on how you do the navigation you might have to use the GameWidget.controlled constructor when you create your GameWidget.