Closed DavidChZh closed 2 months ago
To confirm your doubt about invisible bodies just check the count of world.physicalWorld.bodies
list.
To confirm your doubt about invisible bodies just check the count of
world.physicalWorld.bodies
list.
Thank you very much! I checked world.physicalWorld.bodies and found the hidden bodies. Strangely, the userData property parent of the hidden bodies is null, while the parent of a normal body points to the world. It does not exist in the world, which means it cannot be removed. I find it hard to understand how it is generated
Strangely, the userData property parent of the hidden bodies is null, while the parent of a normal body points to the world.
The userData
shouldn't automatically point to anything. Ideally you should be the one who sets userData
while creating the body and fixtures. I think the only problematic thing is, somehow some of the forge2d bodies are getting disconnected from their BodyComponent
, that is why you don't see them being rendered even though they are still there.
Will it be possible for you to create a simple reproducible example for this? Also, for the expanding behavior, maybe you can create multiple fixtures with varying scale and keep only 1 of them as non-sensor. Then instead of creating and destroying the bodies, you can just change the isSensor
flag for the fixtures such that 1 bigger fixture gradually becomes non-sensor and rest of them remain sensors. I think it will be a bit more efficient on the physics engine than spawning multiple bigger bodies.
Strangely, the userData property parent of the hidden bodies is null, while the parent of a normal body points to the world.
The
userData
shouldn't automatically point to anything. Ideally you should be the one who setsuserData
while creating the body and fixtures. I think the only problematic thing is, somehow some of the forge2d bodies are getting disconnected from theirBodyComponent
, that is why you don't see them being rendered even though they are still there.Will it be possible for you to create a simple reproducible example for this? Also, for the expanding behavior, maybe you can create multiple fixtures with varying scale and keep only 1 of them as non-sensor. Then instead of creating and destroying the bodies, you can just change the
isSensor
flag for the fixtures such that 1 bigger fixture gradually becomes non-sensor and rest of them remain sensors. I think it will be a bit more efficient on the physics engine than spawning multiple bigger bodies.
Thank you very much, it helps me a lot. My current approach is to use destroyBody to remove disconnected bodies when they are detected. When I have some free time, I will try to use multiple fixtures and control them through isSensor to achieve the enlargement effect. I have currently observed that this problem occurs when BodyComponent is created and removed at a high frequency.
Strangely, the userData property parent of the hidden bodies is null, while the parent of a normal body points to the world.
The
userData
shouldn't automatically point to anything. Ideally you should be the one who setsuserData
while creating the body and fixtures. I think the only problematic thing is, somehow some of the forge2d bodies are getting disconnected from theirBodyComponent
, that is why you don't see them being rendered even though they are still there.Will it be possible for you to create a simple reproducible example for this? Also, for the expanding behavior, maybe you can create multiple fixtures with varying scale and keep only 1 of them as non-sensor. Then instead of creating and destroying the bodies, you can just change the
isSensor
flag for the fixtures such that 1 bigger fixture gradually becomes non-sensor and rest of them remain sensors. I think it will be a bit more efficient on the physics engine than spawning multiple bigger bodies.
Hello, I am trying to implement it by controlling the isSensor flag, and I encountered a problem. The fixture I obtained through body.fixtures cannot modify isSensor, and _isSensor is a private variable. Is my usage wrong?
Use setSensor
, it is using the old school way of setting it before the modern variable setter overrides were available.
Use
setSensor
, it is using the old school way of setting it before the modern variable setter overrides were available.
It works fine. Thanks very much
Use
setSensor
, it is using the old school way of setting it before the modern variable setter overrides were available.It works fine. Thanks very much
invisible bodies
When I used isSensor to mark and realized the function of making the sphere bigger, invisible bodies no longer appeared on the simulator, but occasionally appeared on Apple's real machine. So I used world.destroyBody while using isSensor to completely avoid this problem. I wrote an example that does not use isSensor. Please take a look
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/foundation.dart';
class SpriteBodyExample extends Forge2DGame {
static const String description = '''
In this example we show how to add a sprite on top of a `BodyComponent`.
Tap the screen to add more pizzas.
''';
SpriteBodyExample()
: super(
gravity: Vector2(0, 100.0),
world: SpriteBodyWorld(),
);
}
class SpriteBodyWorld extends Forge2DWorld
with TapCallbacks, HasGameReference<Forge2DGame> {
@override
Future<void> onLoad() async {
super.onLoad();
addAll(createBoundaries(game, strokeWidth: 0));
}
@override
void onTapDown(TapDownEvent info) {
super.onTapDown(info);
final position = info.localPosition;
expansion(position);
}
expansion(Vector2 position) async {
for (var i = 0; i <= 4; i++) {
Pizza pizza = Pizza(position, size: Vector2(10, 15));
game.world.add(pizza);
await Future.delayed(const Duration(milliseconds: 10), () {});
if (i != 4) {
pizza.removeFromParent();
}
}
}
}
class Pizza extends BodyComponent with TapCallbacks {
final Vector2 initialPosition;
final Vector2 size;
Pizza(
this.initialPosition, {
Vector2? size,
}) : size = size ?? Vector2(2, 3);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await game.loadSprite('pizza.png');
renderBody = false;
add(
SpriteComponent(
sprite: sprite,
size: size,
anchor: Anchor.center,
),
);
}
removeInvalidBody() {
var invalidBody = game.world.physicsWorld.bodies
.where((body) =>
body.userData != null &&
body.userData is Pizza &&
(body.userData as Pizza).parent == null)
.toList();
for (var i = 0; i < invalidBody.length; i++) {
game.world.destroyBody(invalidBody[i]);
debugPrint("无效移除$i");
}
}
@override
void onTapUp(TapUpEvent event) {
removeInvalidBody();
super.onTapUp(event);
}
@override
Body createBody() {
final shape = PolygonShape();
final vertices = [
Vector2(-size.x / 2, size.y / 2),
Vector2(size.x / 2, size.y / 2),
Vector2(0, -size.y / 2),
];
shape.set(vertices);
final fixtureDef = FixtureDef(
shape,
restitution: 0,
friction: 1,
);
final bodyDef = BodyDef(
userData: this,
position: initialPosition,
angle: (initialPosition.x + initialPosition.y) / 2 * pi,
type: BodyType.dynamic,
);
Body body = world.createBody(bodyDef);
body.createFixture(fixtureDef);
// body.createFixture(fixtureDef1);
return body;
}
}
List<Wall> createBoundaries(Forge2DGame game, {double? strokeWidth}) {
final visibleRect = game.camera.visibleWorldRect;
final topLeft = visibleRect.topLeft.toVector2();
final topRight = visibleRect.topRight.toVector2();
final bottomRight = visibleRect.bottomRight.toVector2();
final bottomLeft = visibleRect.bottomLeft.toVector2();
return [
Wall(topLeft, topRight, strokeWidth: strokeWidth),
Wall(topRight, bottomRight, strokeWidth: strokeWidth),
Wall(bottomLeft, bottomRight, strokeWidth: strokeWidth),
Wall(topLeft, bottomLeft, strokeWidth: strokeWidth),
];
}
class Wall extends BodyComponent {
final Vector2 start;
final Vector2 end;
final double strokeWidth;
Wall(this.start, this.end, {double? strokeWidth})
: strokeWidth = strokeWidth ?? 1;
@override
Body createBody() {
final shape = EdgeShape()..set(start, end);
final fixtureDef = FixtureDef(shape, friction: 0.3);
final bodyDef = BodyDef(
userData: this, // To be able to determine object in collision
position: Vector2.zero(),
);
paint.strokeWidth = strokeWidth;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
I think you left out the most important part of the problem in your original question. You never mentioned how exactly you were adding and removing the components. I'm pretty sure that the problem is caused by the use of Future.delayed
. It must be messing up with the component lifecycle as it is running out of sync with the game loop. Instead of that you should be using something like Timer
or TimerComponent
to make your code remain in sync with the game loop. You can even calculate the elapsed time manually in the update method and call add/remove after fixed intervals.
I haven't looked closely at what you're doing but SpawnComponent
and RemoveEffect
could be used for adding and removing respectively, in addition to DevKage's suggestions.
I think you left out the most important part of the problem in your original question. You never mentioned how exactly you were adding and removing the components. I'm pretty sure that the problem is caused by the use of
Future.delayed
. It must be messing up with the component lifecycle as it is running out of sync with the game loop. Instead of that you should be using something likeTimer
orTimerComponent
to make your code remain in sync with the game loop. You can even calculate the elapsed time manually in the update method and call add/remove after fixed intervals.
I tried using a Timer, but the problem persisted.
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/foundation.dart';
import 'dart:async' as async;
class SpriteBodyExample extends Forge2DGame {
static const String description = '''
In this example we show how to add a sprite on top of a `BodyComponent`.
Tap the screen to add more pizzas.
''';
SpriteBodyExample()
: super(
gravity: Vector2(0, 100.0),
world: SpriteBodyWorld(),
);
}
class SpriteBodyWorld extends Forge2DWorld
with TapCallbacks, HasGameReference<Forge2DGame> {
@override
Future<void> onLoad() async {
super.onLoad();
addAll(createBoundaries(game, strokeWidth: 0));
}
@override
void onTapDown(TapDownEvent info) {
super.onTapDown(info);
final position = info.localPosition;
expansion(position);
}
expansion(Vector2 position) {
int i = 0;
Pizza? pizza;
async.Timer.periodic(const Duration(milliseconds: 10), (timer) {
if (i == 4) timer.cancel();
pizza?.removeFromParent();
pizza = Pizza(position, size: Vector2(10, 15));
game.world.add(pizza!);
i++;
});
}
}
class Pizza extends BodyComponent with TapCallbacks {
final Vector2 initialPosition;
final Vector2 size;
Pizza(
this.initialPosition, {
Vector2? size,
}) : size = size ?? Vector2(2, 3);
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await game.loadSprite('pizza.png');
renderBody = false;
add(
SpriteComponent(
sprite: sprite,
size: size,
anchor: Anchor.center,
),
);
}
removeInvalidBody() {
var invalidBody = game.world.physicsWorld.bodies
.where((body) =>
body.userData != null &&
body.userData is Pizza &&
(body.userData as Pizza).parent == null)
.toList();
for (var i = 0; i < invalidBody.length; i++) {
game.world.destroyBody(invalidBody[i]);
debugPrint("pizza无效移除$i");
}
}
@override
void onTapUp(TapUpEvent event) {
removeInvalidBody();
super.onTapUp(event);
}
@override
Body createBody() {
final shape = PolygonShape();
final vertices = [
Vector2(-size.x / 2, size.y / 2),
Vector2(size.x / 2, size.y / 2),
Vector2(0, -size.y / 2),
];
shape.set(vertices);
final fixtureDef = FixtureDef(
shape,
restitution: 0,
friction: 1,
);
final bodyDef = BodyDef(
userData: this,
position: initialPosition,
angle: (initialPosition.x + initialPosition.y) / 2 * pi,
type: BodyType.dynamic,
);
Body body = world.createBody(bodyDef);
body.createFixture(fixtureDef);
// body.createFixture(fixtureDef1);
return body;
}
}
List<Wall> createBoundaries(Forge2DGame game, {double? strokeWidth}) {
final visibleRect = game.camera.visibleWorldRect;
final topLeft = visibleRect.topLeft.toVector2();
final topRight = visibleRect.topRight.toVector2();
final bottomRight = visibleRect.bottomRight.toVector2();
final bottomLeft = visibleRect.bottomLeft.toVector2();
return [
Wall(topLeft, topRight, strokeWidth: strokeWidth),
Wall(topRight, bottomRight, strokeWidth: strokeWidth),
Wall(bottomLeft, bottomRight, strokeWidth: strokeWidth),
Wall(topLeft, bottomLeft, strokeWidth: strokeWidth),
];
}
class Wall extends BodyComponent {
final Vector2 start;
final Vector2 end;
final double strokeWidth;
Wall(this.start, this.end, {double? strokeWidth})
: strokeWidth = strokeWidth ?? 1;
@override
Body createBody() {
final shape = EdgeShape()..set(start, end);
final fixtureDef = FixtureDef(shape, friction: 0.3);
final bodyDef = BodyDef(
userData: this, // To be able to determine object in collision
position: Vector2.zero(),
);
paint.strokeWidth = strokeWidth;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}
I haven't looked closely at what you're doing but
SpawnComponent
andRemoveEffect
could be used for adding and removing respectively, in addition to DevKage's suggestions.
SpawnComponent generates a PositionComponent, but I need to generate a BodyComponent, which seems to be unusable.
I tried using a Timer, but the problem persisted.
By Timer I meant the Flame's Timer
I tried using a Timer, but the problem persisted.
By Timer I meant the Flame's Timer
It is indeed as you said. After changing to flame timer, this problem no longer occurs.
async.Timer? timer;
expansion(Vector2 position) {
int i = 0;
Pizza? pizza;
timer = async.Timer(
0.01,
onTick: () {
pizza?.removeFromParent();
pizza = Pizza(position, size: Vector2(10, 15));
game.world.add(pizza!);
debugPrint("pizza$i");
if (i == 4) timer?.stop();
i++;
},
repeat: true,
);
}
It is just that when my limit time setting is reduced, the following error will be reported. It runs normally at 1s or 0.1s. If it is 0.01s, it will be reported. The smaller the interval, the higher the frequency.
E/flutter ( 9436): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: 'package:flame/src/components/mixins/has_game_reference.dart': Failed assertion: line 32 pos 7: 'game != null': Could not find Game instance: the component is detached from the component tree
E/flutter ( 9436): #0 _AssertionError._doThrowNew (dart:core-patch/errors_patch.dart:51:61)
E/flutter ( 9436): #1 _AssertionError._throwNew (dart:core-patch/errors_patch.dart:40:5)
E/flutter ( 9436): #2 HasGameReference._findGameAndCheck (package:flame/src/components/mixins/has_game_reference.dart:32:7)
E/flutter ( 9436): #3 HasGameReference.game (package:flame/src/components/mixins/has_game_reference.dart:19:27)
E/flutter ( 9436): #4 BodyComponent.world (package:flame_forge2d/body_component.dart:79:29)
E/flutter ( 9436): #5 Pizza.createBody (package:flame_hello/forge2D_example.dart:127:17)
E/flutter ( 9436): #6 BodyComponent.onLoad (package:flame_forge2d/body_component.dart:76:12)
E/flutter ( 9436): <asynchronous suspension>
E/flutter ( 9436): #7 Pizza.onLoad (package:flame_hello/forge2D_example.dart:73:5)
E/flutter ( 9436): <asynchronous suspension>
E/flutter ( 9436): #8 Component._startLoading.<anonymous closure> (package:flame/src/components/core/component.dart:858:32)
E/flutter ( 9436): <asynchronous suspension>
It runs normally at 1s or 0.1s. If it is 0.01s, it will be reported. The smaller the interval, the higher the frequency.
0.01s is way too small of an interval. For a game running at 60 FPS, 1 frame will take ~0.016 seconds on an average. If you are trying to add and remove something at a higher frequency than that, it won't be even visible. Also, add and remove don't happen instantly. It will take at least 1 frame for those operations to get processed. So if your code is trying change the state faster than what the game is capable of, it will cause such issues.
It runs normally at 1s or 0.1s. If it is 0.01s, it will be reported. The smaller the interval, the higher the frequency.
0.01s is way too small of an interval. For a game running at 60 FPS, 1 frame will take ~0.016 seconds on an average. If you are trying to add and remove something at a higher frequency than that, it won't be even visible. Also, add and remove don't happen instantly. It will take at least 1 frame for those operations to get processed. So if your code is trying change the state faster than what the game is capable of, it will cause such issues.
You are awesome and totally answered all my questions!
I debugged again today for some reasons. I still used Future.delayed to create a delay, but set the time to be greater than 16, and the invisible bodies problem did not appear. So I came to the following conclusions: When the interval time is set to less than 16 milliseconds. When using Future.delayed, the console does not output errors, but the invisible bodies problem occurs When using Flame's Timer, although errors are output, invisible bodies do not appear So Future.delayed does not disrupt the life cycle, but when developing games, in order to avoid some strange problems, try to use Flame's Timer
I debugged again today for some reasons. I still used Future.delayed to create a delay, but set the time to be greater than 16, and the invisible bodies problem did not appear. So I came to the following conclusions: When the interval time is set to less than 16 milliseconds. When using Future.delayed, the console does not output errors, but the invisible bodies problem occurs When using Flame's Timer, although errors are output, invisible bodies do not appear So Future.delayed does not disrupt the life cycle, but when developing games, in order to avoid some strange problems, try to use Flame's Timer
Just to add to that, the 16 ms number was used just as an example. It is not necessary that all flame games will always be running at 60 FPS. It all depends on the hardware and the game itself.
Also, it is an average FPS. So it is quite possible that some frames take more than 16 ms and some take less. Interval values closer to the average frame times will always run the risk of facing this problem.
What happened?
As you can see in the video, in my current business, I need to achieve the effect of a polygonal rigid body becoming larger. I achieve this by creating and removing rigid bodies multiple times. When I frequently create rigid bodies and quickly remove them, some invisible rigid bodies will be generated. This phenomenon is sporadic but very frequent, and it is more likely to occur when multiple rigid bodies are simultaneously enlarged. From the log, the total number of rigid bodies and the total number displayed on the screen are consistent. I am confused about where these invisible rigid bodies come from. I think these invisible ones may not be rigid bodies, because I have turned on the debug mode. If they are rigid bodies, they should be displayed on the screen. I really can't find a reason that can explain it. Do you have any ideas to provide?
What do you expect?
No more invisible rigid bodies are generated
How can we reproduce this?
No response
What steps should take to fix this?
No response
Do have an example of where the bug occurs?
No response
Relevant log output
No response
Execute in a terminal and put output into the code block below
Affected platforms
Android
Other information
Are you interested in working on a PR for this?