flame-engine / flame

A Flutter based game engine.
https://flame-engine.org
MIT License
9.29k stars 913 forks source link

Unable to remove Flame util Gesture Recognizer #174

Closed seth35us closed 5 years ago

seth35us commented 5 years ago

I am navigating between a main page and several games. I am unable to remove the gesture recognizer when navigating away from the game widget. I have tried both methods below and created a singleton class, but the gesture recognizer won't go away. Flame.util.removeGestureRecognizer(tapper); tapper.dispose();

I am including a basic implementation to recreate the issue. As you can see the OnTap event is still occurring on the main page. I have confirmed that there is only one instance of the game running and the gesture recognizer is only registered once. I tried to remove the gesture recognizer on every tap event, just for good luck, but it didn't work. I am pasting the game class in addition to the zipped file.

import 'dart:ui';

import 'package:flame/flame.dart'; import 'package:flame/game.dart'; import 'package:flutter/gestures.dart';

class MathBlocksGame extends BaseGame { static final MathBlocksGame _singleton = new MathBlocksGame._internal(); MathBlocksGame._internal() { print('MathBlocksGame contructor called'); } static MathBlocksGame get instance => _singleton; bool isInitialized = false; TapGestureRecognizer tapper;

factory MathBlocksGame() { if (_singleton.isInitialized) return _singleton;

_singleton._initTapHandlers();
return _singleton;

}

void _initTapHandlers() { print("Init tap handler ${super.toString() + " " + hashCode.toString()}"); tapper = new TapGestureRecognizer(); Flame.util.addGestureRecognizer(new TapGestureRecognizer() ..onTapDown = (TapDownDetails evt) => handleInput(evt.globalPosition)); isInitialized = true; }

void dispose() { Flame.util.removeGestureRecognizer(tapper); tapper.dispose(); }

void handleInput(Offset globalPosition) { print("On Tap Down ${super.toString() + " " + hashCode.toString()}");

Flame.util.removeGestureRecognizer(tapper);
tapper.dispose();

} }

delete_me.zip

r1sim commented 5 years ago

I had a similar issue with a PanGestureRecognizer a few hours ago. I figured out that everything works as expected when setting the event handler to null.

Try removing the GestureRecognizer like this:

tapper.onTapDown = null;
Flame.util.removeGestureRecognizer(tapper);

You don't have to dispose the tapper because that's what the removeGestureRecognizer method does. (check https://github.com/flame-engine/flame/blob/master/lib/util.dart#L133)

Anyway, this isn't really intuitive if you ask me and I think that the removeGestureRecognizer method should also set the handlers to null.

seth35us commented 5 years ago

Thanks @r1sim, but this didn't work. I tried

      _tapper.onTapDown = null;
      Flame.util.removeGestureRecognizer(_tapper);

      _tapper.dispose();
      _tapper = null;
      _tapDownCallBack = null;

The _tapper and _tapDownCallBack are defined only once in the init of a singleton class. I know you said that I don't have to dispose the tapper, but it didn't make a difference either way, so I left it in for good luck. I will remove it later if I can figure out how to get rid of the tapper.

_tapper = new TapGestureRecognizer();
_tapDownCallBack = (TapDownDetails evt) => handleInput(evt.globalPosition);
r1sim commented 5 years ago

I checked your example project and got it to work. You passed a new TapGestureRecognizer instance instead of tapper when calling the addGestureRecognizer method.

So when you change:

void _initTapHandlers() {
print("Init tap handler ${super.toString() + " " + hashCode.toString()}");
tapper = new TapGestureRecognizer();
Flame.util.addGestureRecognizer(new TapGestureRecognizer()
..onTapDown = (TapDownDetails evt) => handleInput(evt.globalPosition));
isInitialized = true;
}

to:

void _initTapHandlers() {
  print("Init tap handler ${super.toString() + " " + hashCode.toString()}");
  tapper = new TapGestureRecognizer();
  tapper.onTapDown = handleInput;
  Flame.util.addGestureRecognizer(tapper);
  isInitialized = true;
}

and change handleInput(Offset globalPosition) to handleInput(TapDownDetails details) it should work.

seth35us commented 5 years ago

@r1sim Works perfectly! Thank you for your help.

seth35us commented 5 years ago

@r1sim One other issue. When I add the addGestureRecognizer the second time the tap is extremely delayed and sometimes does not even fire.

Here is the relevant code:

  factory MathBlocksGame({MathBlocksUIState uiState}) {
    _singleton.ui = uiState;
    _singleton.ui.currentScreen = UIScreen.home;
    _singleton.initialize();
    if (_singleton.isTapperInitialized) return _singleton;
    _singleton._initTapHandlers();

    return _singleton;
  }

void _initTapHandlers() {
    print("Init tap handler ${super.toString() + " " + hashCode.toString()}");
    _tapper = new TapGestureRecognizer();
    _tapper.onTapDown = handleInput;

    Flame.util.addGestureRecognizer(_tapper);
    isTapperInitialized = true;
  }

 void dispose() {
    _disposeTapper();
  }

  void _disposeTapper() {
    if (_tapper != null) {
      print(
          "Dispose tap handler ${super.toString() + " " + hashCode.toString()}");
       _tapper.onTapDown = null;
       Flame.util.removeGestureRecognizer(_tapper);
    }

    isTapperInitialized = false;
  }

void handleInput(TapDownDetails details) { 
   print("On Tap Down ${super.toString() + " " + hashCode.toString()}");
}
erickzanardo commented 5 years ago

@seth35us The addGestureRecognizer can be a little cryptic sometimes and lead to situations like the one you are facing, we are even thinking on maybe deprecate that method and create a new API.

In the meantime, I strongly suggest you using GestureDetector widget to wrap your game and use it to detect the gestures, since it is a widget, the callbacks should be destroyed alongside the widget, when it is removed from the widget tree, you can see an example of that here: https://github.com/fireslime/rogue_shooter/blob/master/lib/main.dart#L27

seth35us commented 5 years ago

Great suggestion @erickzanardo. Here is my final code:

void initState() {
    super.initState();
    _initialize();
  }

Future _initialize() async {
    gameUI = MathBlocksUI();
    game = MathBlocksGame(uiState: gameUI.state);
    gameUI.state.game = game;
    setState(() {});
}

Widget build(BuildContext context) {
    return Scaffold(
        body: (game == null || gameUI == null)
          ? Align(alignment: Alignment.center, child: CircularProgressIndicator())
          : Stack(
          fit: StackFit.expand,
          children: <Widget>[
            Positioned.fill(
              child: GestureDetector(
                behavior: HitTestBehavior.opaque,
                onTapDown: game.handleInput,
                child: game.widget,
              ),
            ),
            Positioned.fill(
              child: gameUI,
            ),
          ],
        ));
  }