flame-engine / forge2d

A Dart port of Box2D
BSD 3-Clause "New" or "Revised" License
181 stars 26 forks source link

Exception: Concurrent modification during iteration when body containing joints is destroyed #34

Closed brenodt closed 2 years ago

brenodt commented 2 years ago

I'm using Flame & Forge2d draw some components based on the state of my application. There's also the option to filter such state to display only a certain type of data, which in turn means the Forge2DGame rebuilds to reflect the new data source.

In my simulation, I have the components drawn into a tree-like structure, where children are tied to their parent through a Joint. Whenever my game gets rebuilt (and, in the process, destroyed) I get the following exception: The following ConcurrentModificationError was thrown building _BodyBuilder: Concurrent modification during iteration: Instance(length:2) of '_GrowableList'.

It seems that the culprit is forge2d/lib/src/dynamics/world.dart:144:

  // Delete the attached joints.
  for (final joint in body.joints) {
    destroyListener?.onDestroyJoint(joint);
    destroyJoint(joint);
  }

Maybe something in the line of this would work to fix it?

Here's a minimal reproducible example:

import 'dart:math' as math;

import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame_forge2d/body_component.dart';
import 'package:flame_forge2d/forge2d_game.dart';
import 'package:flutter/material.dart';
import 'package:forge2d/forge2d.dart';

void main() {
  runApp(const MaterialApp(home: SampleScreen()));
}

class SampleScreen extends StatefulWidget {
  const SampleScreen({Key? key}) : super(key: key);

  @override
  State<SampleScreen> createState() => _SampleScreenState();
}

class _SampleScreenState extends State<SampleScreen> {
  int childrenCount = 3;

  void incr() => setState(() => childrenCount += 1);

  void decr() => setState(() => childrenCount -= 1);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        alignment: AlignmentDirectional.topEnd,
        children: [
          GameWidget(game: SampleGame(childrenCount)),
          Positioned(
            bottom: 20,
            right: 20,
            child: Row(
              children: [
                IconButton(onPressed: incr, icon: const Icon(Icons.add)),
                IconButton(onPressed: decr, icon: const Icon(Icons.remove)),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class SampleGame extends Forge2DGame with HasTappables {
  SampleGame(this.count) : super(gravity: Vector2(0, 0));
  final int count;

  @override
  Color backgroundColor() => Colors.transparent;

  @override
  Future<void> onLoad() async {
    await super.onLoad();
    final worldCenter = screenToWorld(size * camera.zoom / 2);
    add(SampleComponent(worldCenter, radius: 8, numberOfChildren: count));
  }
}

class SampleComponent extends BodyComponent with Tappable {
  SampleComponent(
    this._position, {
    required this.radius,
    this.numberOfChildren = 0,
    this.parentComponent,
  }) {
    paint = Paint()..color = Colors.teal;
  }

  final double radius;
  final Vector2 _position;
  final int numberOfChildren;
  final SampleComponent? parentComponent;

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

    for (var i = 0; i < numberOfChildren; i++) {
      final angle = i * (2 * math.pi / numberOfChildren);

      var polar = Vector2(math.cos(angle), math.sin(angle));
      final scaled = polar.clone()..scaleTo((radius + radius / 3));

      var component = SampleComponent(_position - scaled,
          radius: radius / 3, parentComponent: this);

      gameRef.add(component);
    }
  }

  @override
  Body createBody() {
    final bodyDef = BodyDef()
      ..userData = this
      ..angularDamping = 0.8
      ..position = _position
      ..type = BodyType.dynamic;

    final shape = CircleShape();
    shape.radius = radius;

    final fixtureDef = FixtureDef(shape)
      ..restitution = 0.8
      ..density = 1.0
      ..friction = 0.4;

    final body = world.createBody(bodyDef)..createFixture(fixtureDef);

    if (parentComponent != null) {
      final jointDef = DistanceJointDef();
      jointDef.initialize(
        body,
        parentComponent!.body,
        body.position,
        parentComponent!.body.position,
      );
      world.createJoint(jointDef);
    }

    return body;
  }
}

Full Stacktrace

======== Exception caught by widgets library =======================================================
The following ConcurrentModificationError was thrown building _BodyBuilder:
Concurrent modification during iteration: Instance(length:2) of '_GrowableList'.

When the exception was thrown, this was the stack: 
#0      ListIterator.moveNext (dart:_internal/iterable.dart:336:7)
#1      World.destroyBody (package:forge2d/src/dynamics/world.dart:144:30)
#2      BodyComponent.onRemove (package:flame_forge2d/body_component.dart:146:11)
#3      Component.onRemove.<anonymous closure> (package:flame/src/components/component.dart:208:13)
#4      IterableMixin.forEach (dart:collection/iterable.dart:45:35)
#5      Component.onRemove (package:flame/src/components/component.dart:207:14)
#6      _GameWidgetState.didUpdateWidget (package:flame/src/game/game_widget/game_widget.dart:201:22)
#7      StatefulElement.update (package:flutter/src/widgets/framework.dart:4943:57)
#8      Element.updateChild (package:flutter/src/widgets/framework.dart:3501:15)
#9      RenderObjectElement.updateChildren (package:flutter/src/widgets/framework.dart:5787:32)
#10     MultiChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6445:17)
#11     Element.updateChild (package:flutter/src/widgets/framework.dart:3501:15)
#12     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4780:16)
#13     Element.rebuild (package:flutter/src/widgets/framework.dart:4477:5)
#14     StatelessElement.update (package:flutter/src/widgets/framework.dart:4834:5)
#15     Element.updateChild (package:flutter/src/widgets/framework.dart:3501:15)
#16     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4780:16)
#17     Element.rebuild (package:flutter/src/widgets/framework.dart:4477:5)
#18     ProxyElement.update (package:flutter/src/widgets/framework.dart:5108:5)
#19     Element.updateChild (package:flutter/src/widgets/framework.dart:3501:15)
#20     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4780:16)
#21     Element.rebuild (package:flutter/src/widgets/framework.dart:4477:5)
#22     ProxyElement.update (package:flutter/src/widgets/framework.dart:5108:5)
#23     Element.updateChild (package:flutter/src/widgets/framework.dart:3501:15)
#24     RenderObjectElement.updateChildren (package:flutter/src/widgets/framework.dart:5787:32)
#25     MultiChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6445:17)
#26     Element.updateChild (package:flutter/src/widgets/framework.dart:3501:15)
#27     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4780:16)
#28     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:4928:11)
#29     Element.rebuild (package:flutter/src/widgets/framework.dart:4477:5)
#30     StatefulElement.update (package:flutter/src/widgets/framework.dart:4960:5)
#31     Element.updateChild (package:flutter/src/widgets/framework.dart:3501:15)
#32     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4780:16)
#33     Element.rebuild (package:flutter/src/widgets/framework.dart:4477:5)
#34     ProxyElement.update (package:flutter/src/widgets/framework.dart:5108:5)
#35     Element.updateChild (package:flutter/src/widgets/framework.dart:3501:15)
#36     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4780:16)
#37     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:4928:11)
#38     Element.rebuild (package:flutter/src/widgets/framework.dart:4477:5)
#39     StatefulElement.update (package:flutter/src/widgets/framework.dart:4960:5)
#40     Element.updateChild (package:flutter/src/widgets/framework.dart:3501:15)
#41     SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6291:14)
#42     Element.updateChild (package:flutter/src/widgets/framework.dart:3501:15)
#43     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4780:16)
#44     Element.rebuild (package:flutter/src/widgets/framework.dart:4477:5)
#45     StatelessElement.update (package:flutter/src/widgets/framework.dart:4834:5)
#46     Element.updateChild (package:flutter/src/widgets/framework.dart:3501:15)
#47     SingleChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6291:14)
#48     Element.updateChild (package:flutter/src/widgets/framework.dart:3501:15)
#49     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4780:16)
#50     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:4928:11)
#51     Element.rebuild (package:flutter/src/widgets/framework.dart:4477:5)
#52     StatefulElement.update (package:flutter/src/widgets/framework.dart:4960:5)
#53     Element.updateChild (package:flutter/src/widgets/framework.dart:3501:15)
#54     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4780:16)
#55     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:4928:11)
#56     Element.rebuild (package:flutter/src/widgets/framework.dart:4477:5)
#57     StatefulElement.update (package:flutter/src/widgets/framework.dart:4960:5)
#58     Element.updateChild (package:flutter/src/widgets/framework.dart:3501:15)
#59     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4780:16)
#60     Element.rebuild (package:flutter/src/widgets/framework.dart:4477:5)
#61     ProxyElement.update (package:flutter/src/widgets/framework.dart:5108:5)
#62     Element.updateChild (package:flutter/src/widgets/framework.dart:3501:15)
#63     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4780:16)
#64     Element.rebuild (package:flutter/src/widgets/framework.dart:4477:5)
#65     StatelessElement.update (package:flutter/src/widgets/framework.dart:4834:5)
#66     Element.updateChild (package:flutter/src/widgets/framework.dart:3501:15)
#67     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4780:16)
#68     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:4928:11)
#69     Element.rebuild (package:flutter/src/widgets/framework.dart:4477:5)
#70     StatefulElement.update (package:flutter/src/widgets/framework.dart:4960:5)
#71     Element.updateChild (package:flutter/src/widgets/framework.dart:3501:15)
#72     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4780:16)
#73     Element.rebuild (package:flutter/src/widgets/framework.dart:4477:5)
#74     ProxyElement.update (package:flutter/src/widgets/framework.dart:5108:5)
#75     Element.updateChild (package:flutter/src/widgets/framework.dart:3501:15)
#76     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4780:16)
#77     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:4928:11)
#78     Element.rebuild (package:flutter/src/widgets/framework.dart:4477:5)
#79     StatefulElement.update (package:flutter/src/widgets/framework.dart:4960:5)
#80     Element.updateChild (package:flutter/src/widgets/framework.dart:3501:15)
#81     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4780:16)
#82     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:4928:11)
#83     Element.rebuild (package:flutter/src/widgets/framework.dart:4477:5)
#84     BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2659:19)
#85     WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:882:21)
#86     RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:363:5)
#87     SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1144:15)
#88     SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1081:9)
#89     SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:995:5)
#93     _invoke (dart:ui/hooks.dart:151:10)
#94     PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:308:5)
#95     _drawFrame (dart:ui/hooks.dart:115:31)
(elided 3 frames from dart:async)
====================================================================================================

Thanks for the great package! Let me know if any further info is needed.

spydon commented 2 years ago

How sloppy, we had this issue some time ago with contacts, which are removed right below the joints. We could do the same quick fix there. https://github.com/flame-engine/forge2d/blob/main/lib/src/dynamics/world.dart#L149-L153

spydon commented 2 years ago

Could you try depending on the branch in the PR @brenodt to confirm that it is solved?

brenodt commented 2 years ago

@spydon I can confirm it fixes the issue. Thanks!

brenodt commented 2 years ago

Hey, I was wondering if there's any estimation to when this will be merged. As my project depends on flame_forge2d I can't make use of the branch while it's not released.

spydon commented 2 years ago

You can make use of it with dependency_overrides:

dependency_overrides:
  flame:
    git:
      url: https://github.com/flame-engine/forge2d.git
      ref: main

If you want to speed up the process a bit you can just write a simple test for the fix that I did and push up a PR and I can do a release tonight.

spydon commented 2 years ago

Added a test for the fix and released 0.8.2