flame-engine / flame

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

Clearing data from memory misbehaving? Flame.images.clear(...) // Flame.images.clearCache() #1309

Closed JKremsner closed 2 years ago

JKremsner commented 2 years ago

Please note that the components used use very big spritesheets.

I for example use a SpriteAnimationGroupComponent.

import 'package:flame/components.dart';
import 'package:flame/flame.dart';
import 'package:flame/sprite.dart';

enum CowState { idle, eat, transition_out_of_eat, die, transition_to_eat, walk }
enum CowColor { brown, black_and_white }

class CowComponent extends SpriteAnimationGroupComponent<CowState> {
  static final Vector2 sourceSize = Vector2(362, 289);
  String colorString;
  CowComponent(CowColor color) {
    colorString = color.toString().split(".")[1];
  }
  @override
  Future<void> onLoad() async {
    super.onLoad();
    final spriteSheet = SpriteSheet(
      image:
          await Flame.images.load('cow/spriteSheets/__${colorString}_cow.png'),
      srcSize: sourceSize,
    );

    final idle = spriteSheet.createAnimation(row: 2, stepTime: 0.1);
    final eat = spriteSheet.createAnimation(row: 1, stepTime: 0.1);
    eat.loop = false;
    eat.onComplete = () {
      current = CowState.transition_out_of_eat;
      eat.reset();
    };
    final transitionOutOfEat =
        spriteSheet.createAnimation(row: 3, stepTime: 0.1);
    transitionOutOfEat.loop = false;
    transitionOutOfEat.onComplete = () {
      current = CowState.idle;
      transitionOutOfEat.reset();
    };
    final die = spriteSheet.createAnimation(row: 0, stepTime: 0.1);
    final transitionToEat = spriteSheet.createAnimation(row: 4, stepTime: 0.1);
    transitionToEat.loop = false;
    transitionToEat.onComplete = () {
      current = CowState.eat;
      transitionToEat.reset();
    };
    final walk = spriteSheet.createAnimation(row: 5, stepTime: 0.1);

    animations = {
      CowState.idle: idle,
      CowState.eat: eat,
      CowState.transition_out_of_eat: transitionOutOfEat,
      CowState.die: die,
      CowState.transition_to_eat: transitionToEat,
      CowState.walk: walk
    };
    current = CowState.idle;
  }
}

As you can see aboth I use Flame.images.load to load the spritesheet into the memory. I use Flutter DevTools to inspect the memory. As soon as this code loads I can see the memory usage increasing. My game includes the following code snippet:

  @override
  void onDetach() {
    super.onDetach();
    Flame.images.clear('cow/spriteSheets/__brown_cow.png');
    Flame.images.clear('cow/spriteSheets/__black_and_white_cow.png');
  }

Now when I detach the game it will clear the 2 spritesheets from the cache but I don't see the memory reducing at all in Flutter DevTools. Even worse: Flame doesn't find the image in the cache anymore (as it should be) and therefore it gets loaded again into the cache as soon as the component/game gets attached again. This causes the memory to slowly flood as I can always see the memory increasing when I load the component but never see it decrease when I clear something from it. The only way to prevent flooding the memory like this is to never clear it from the cache. Obviously that can't be intended. Am I missing something?

If I go even further and use Flame.images.clearCache() I can see some memory getting freed up but loading everything in again uses much more then previously needed causing the same kind of flood.

st-pasha commented 2 years ago

I think the main reason for this is because the documentation for Image class says

/// A class or method that receives an image object must call [dispose] on the
/// handle when it is no longer needed. To create a shareable reference to the
/// underlying image, call [clone]. The method or object that receives
/// the new instance will then be responsible for disposing it, and the
/// underlying image itself will be disposed when all outstanding handles are
/// disposed.

and we never do that. Normally, it's not a problem, because all images are stored in the cache anyways. But if you want to clear something from cache, then that would create a memory leak.

JKremsner commented 2 years ago

You are completely right @st-pasha! If I adept my code, keep a reference to the SpriteSheetImage within the CowComponent and change the code of the onDetach to

@override
  void onDetach() {
    super.onDetach();
    print("detach");
    cows.forEach((cow) {
      cow.spriteSheetImage.dispose();
    });
    Flame.images.clear('cow/spriteSheets/__brown_cow.png');
    Flame.images.clear('cow/spriteSheets/__black_and_white_cow.png');
  }

it works as intended and I see the memory decreasing by the same amount as it increases when I load cows. This painfully means I kinda have to keep references of all the images that I use in components and dispose them manually before clearing them from the flame cache.

How could we improve on that?

spydon commented 2 years ago

Can you try to depend on this branch to verify that this fix fixes it for you?

dependencies:
  flame:
    git:
      url: https://github.com/flame-engine/flame.git
      ref: spydon.dispose-images-when-cleared
      path: packages/flame
JKremsner commented 2 years ago

@spydon Just tested it and you are a hero! Works flawlessly! Thank you!

spydon commented 2 years ago

You don't have to close the issue yet, it will be closed automatically when the PR is merged. :)