Skip to content

Commit b603e2b

Browse files
authored
Merge branch 'main' into luan.input-callbacks
2 parents cdba613 + 9081a0f commit b603e2b

246 files changed

Lines changed: 5838 additions & 2624 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/.cspell/dart_dictionary.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ dartdoc # documentation tool for dart
44
dartdocs # plural of dartdoc
55
endtemplate # Use @endtemplate to close a @template block in dartdoc
66
pubspec # dependency and configuration file of every Dart project
7+
unawaited # dart:async helper to mark a Future as intentionally not awaited

.github/.cspell/words_dictionary.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ renderable
2424
rerasterize
2525
rescan
2626
Roboto
27+
subclassing
2728
tappable
2829
thumbstick
2930
trackpad

.github/workflows/cicd.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ jobs:
2929
with:
3030
flutter-version: ${{env.FLUTTER_MIN_VERSION}}
3131
- uses: bluefireteam/melos-action@v3
32+
# flame_3d always requires the latest stable, since flutter_gpu is still
33+
# unstable, so it is only analyzed in the analyze-latest job.
3234
- name: "Analyze with lowest supported version"
33-
uses: invertase/github-action-dart-analyzer@v3
34-
with:
35-
fatal-infos: true
35+
run: melos exec --ignore="flame_3d*" -- dart analyze --fatal-infos .
3636

3737
analyze-latest:
3838
runs-on: ubuntu-latest

doc/bridge_packages/flame_behaviors/getting_started.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ For instance a `TimerComponent` can implement a time-based behavioral activity:
9494
class MyBehavior extends Behavior {
9595
@override
9696
Future<void> onLoad() async {
97-
await add(TimerComponent(period: 5, repeat: true, onTick: _onTick));
97+
add(TimerComponent(period: 5, repeat: true, onTick: _onTick));
9898
}
9999
100100
void _onTick() {

doc/bridge_packages/flame_bloc/bloc.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ We can do that by using `FlameBlocProvider` component:
2222
class MyGame extends FlameGame {
2323
@override
2424
Future<void> onLoad() async {
25-
await add(
25+
add(
2626
FlameBlocProvider<PlayerInventoryBloc, PlayerInventoryState>(
2727
create: () => PlayerInventoryBloc(),
2828
children: [
@@ -44,7 +44,7 @@ fashion:
4444
class MyGame extends FlameGame {
4545
@override
4646
Future<void> onLoad() async {
47-
await add(
47+
add(
4848
FlameMultiBlocProvider(
4949
providers: [
5050
FlameBlocProvider<PlayerInventoryBloc, PlayerInventoryState>(
@@ -72,7 +72,7 @@ By using `FlameBlocListener` component:
7272
class Player extends PositionComponent {
7373
@override
7474
Future<void> onLoad() async {
75-
await add(
75+
add(
7676
FlameBlocListener<PlayerInventoryBloc, PlayerInventoryState>(
7777
listener: (state) {
7878
updateGear(state);

doc/bridge_packages/flame_forge2d/flame_forge2d.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
```{toctree}
44
Overview <forge2d.md>
55
Joints <joints.md>
6+
Migration <migration.md>
67
```

doc/bridge_packages/flame_forge2d/forge2d.md

Lines changed: 185 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,42 @@
11
# Forge2D
22

3-
Blue Fire maintains a ported version of the Box2D physics engine and our
4-
version is called Forge2D.
3+
Blue Fire maintains Forge2D, Dart bindings for the [Box2D](https://box2d.org/) physics engine
4+
(native on mobile and desktop, WebAssembly on the web).
55

66
If you want to use Forge2D specifically for Flame you should use our bridge library
77
[flame_forge2d](https://github.com/flame-engine/flame/tree/main/packages/flame_forge2d) and if you
88
just want to use it in a Dart project you can use the
99
[forge2d](https://github.com/flame-engine/forge2d) library directly.
1010

11-
To use it in your game you just need to add `flame_forge2d` to your
12-
`pubspec.yaml`, as can be seen in the [Forge2D
13-
[example](https://github.com/flame-engine/flame/tree/main/packages/flame_forge2d/example)
14-
and the pub. dev [installation
15-
instructions](https://pub.dev/packages/flame_forge2d)](<https://pub.dev/packages/flame_forge2d>).
11+
To use it in your game you just need to add `flame_forge2d` to your `pubspec.yaml`, as can be
12+
seen in the
13+
[Forge2D example](https://github.com/flame-engine/flame/tree/main/packages/flame_forge2d/example)
14+
and the pub.dev [installation instructions](https://pub.dev/packages/flame_forge2d).
15+
16+
Since Forge2D runs Box2D as native code, a C toolchain is required when building for native
17+
platforms (Xcode on iOS/macOS, the NDK on Android, Visual Studio Build Tools on Windows and
18+
clang or gcc on Linux). On the web a bundled WebAssembly build of Box2D is used instead.
19+
20+
Forge2D has to be initialized with `await initializeForge2D()` before any physics world is
21+
created, which on the web is what loads that WebAssembly module. `Forge2DGame` awaits this in its
22+
`onLoad`, so games don't have to do anything, but that also means that a `Forge2DGame` subclass
23+
which overrides `onLoad` has to await `super.onLoad()` before it creates any bodies:
24+
25+
```dart
26+
class MyGame extends Forge2DGame {
27+
@override
28+
Future<void> onLoad() async {
29+
await super.onLoad(); // Not awaiting this breaks the game on the web.
30+
world.add(MyBody());
31+
}
32+
}
33+
```
34+
35+
If you create a `Forge2DWorld` or a raw Forge2D `World` outside of a `Forge2DGame`, await
36+
`initializeForge2D()` yourself first, or the world creation will throw on the web.
37+
38+
If you are upgrading an existing game from flame_forge2d 0.19, see the
39+
[migration guide](migration.md).
1640

1741

1842
## Forge2DGame
@@ -23,11 +47,18 @@ If you are going to use Forge2D in your project it can be a good idea to use the
2347
It is called `Forge2DGame` and supports both the special Forge2D components called `BodyComponents`
2448
as well as normal Flame components.
2549

26-
`Forge2DGame` has a built-in `CameraComponent` and has a zoom level set to 10 by default, so your
27-
components will be a lot bigger than in a normal Flame game. This is due to the speed limit in the
28-
`Forge2D` world, which you would hit very quickly if you are using it with `zoom = 1.0`. You can
29-
easily change the zoom level either by calling `super(zoom: yourZoom)` in your constructor or
30-
doing `game.cameraComponent.viewfinder.zoom = yourZoom;` at a later stage.
50+
`Forge2DGame` has a built-in `CameraComponent` that uses a `Forge2DViewfinder`. The physics world
51+
is measured in meters, and the viewfinder renders one meter as `metersToPixels` pixels, which is
52+
100 by default. Lay your world out in meters at a realistic scale and let `metersToPixels` decide
53+
how big that is on screen; see [](#units-and-scale) for why the two are kept apart.
54+
55+
You can change the scale either by calling `super(metersToPixels: yourScale)` in your constructor
56+
or by doing `game.metersToPixels = yourScale;` at a later stage.
57+
58+
The `zoom` of the viewfinder is applied on top of `metersToPixels` and defaults to 1, so it is free
59+
for what it is normally used for: zooming the camera in and out. Everything except the rendering
60+
stays in meters, so body positions, `camera.viewfinder.position`, `camera.visibleWorldRect` and the
61+
local positions that events report are all still expressed in meters.
3162

3263
If you are previously familiar with Box2D it can be good to know that the whole concept of the
3364
Box2d world is mapped to `world` in the `Forge2DGame` component and every `Body` that you want to
@@ -48,6 +79,94 @@ A simple `Forge2DGame` implementation example can be seen in the
4879
[examples folder](https://github.com/flame-engine/flame/tree/main/packages/flame_forge2d/example).
4980

5081

82+
## Units and scale
83+
84+
Forge2D is Box2D, and Box2D is tuned for meters, kilograms and seconds. Lay your world out in
85+
meters at a realistic scale, aiming to keep moving bodies roughly between 0.1 and 10 of them, with
86+
1 meter being the sweet spot. How large that is on screen is a separate decision, and it is what
87+
`metersToPixels` is for.
88+
89+
```{note}
90+
If you used flame_forge2d before the Box2D v3 migration, this is a
91+
change of advice. The old version had a hard `maxTranslation` of 2
92+
meters per step, so about 120 m/s, and the docs told you to lay the
93+
world out much smaller than a meter to stay under it. That limit is
94+
now `WorldDef.maximumLinearSpeed`, which defaults to 400 m/s and is
95+
settable per world through `Forge2DWorld(definition: WorldDef(...))`.
96+
When you pass a definition, also pass the `gravity` argument (or set
97+
`WorldDef.gravity` explicitly), because the definition's default is
98+
Box2D's y-up `(0, -10)` rather than Flame's y-down `(0, 10)`.
99+
There is no longer a reason to shrink the world, and there are good
100+
reasons not to.
101+
```
102+
103+
104+
### Why a shrunken world misbehaves
105+
106+
A handful of Box2D's tolerances are absolute lengths rather than fractions of the shapes they
107+
apply to, so in a world laid out at a much smaller scale than a meter they stop being negligible
108+
and start dominating:
109+
110+
| Tolerance | Default | What it does in a world only a meter across |
111+
| --- | --- | --- |
112+
| `Tolerances.speculativeDistance` | 0.02 m | contacts are reported across 2% of the world |
113+
| `WorldDef.restitutionThreshold` | 1 m/s | nothing ever bounces |
114+
| `WorldDef.hitEventThreshold` | 1 m/s | no hit events are ever generated |
115+
| `BodyDef.sleepThreshold` | 0.05 m/s | bodies fall asleep while still moving |
116+
| `WorldDef.maxContactPushSpeed` | 3 m/s | overlapping bodies are pushed apart violently |
117+
| `Tolerances.aabbMargin` | 0.05 m | broadphase bounds dwarf the shapes |
118+
119+
The first one is the one that gets reported as a bug. Box2D creates contact points for shapes that
120+
are approaching but have not touched yet, which is what stops fast bodies from passing through
121+
things and removes most collision jitter. It also means `beginContact` fires while there is still
122+
a visible gap of up to `Tolerances.speculativeDistance`. A body that is not comfortably larger
123+
than that is permanently in contact with its neighbors. flame_forge2d prints a debug-mode warning
124+
once when it notices a moving body that small.
125+
126+
127+
### Scaling a world up
128+
129+
If your world is currently too small, scale it up and scale gravity with it. That last part is the
130+
one that is easy to miss: scaling lengths alone makes everything look like it is moving in
131+
treacle, while scaling lengths and gravity by the same factor leaves the timing of the simulation
132+
completely unchanged. For a length scale factor of `S`:
133+
134+
| Quantity | Scale by |
135+
| --- | --- |
136+
| lengths, positions, radii, velocities, gravity, accelerations | `S` |
137+
| densities, friction, restitution, damping, angular velocities | `1`, unchanged |
138+
| masses | `` |
139+
| forces, linear impulses | `` |
140+
| torques, rotational inertia, angular impulses | `S⁴` |
141+
| **time** | **`1`, unchanged** |
142+
143+
So a world that was 1 meter tall with a 0.02 m ball and a gravity of 9.81 becomes a world 10
144+
meters tall with a 0.2 m ball and a gravity of 98.1, behaving identically but comfortably inside
145+
the range Box2D is tuned for. Divide `metersToPixels` by the same factor to keep it the same size
146+
on screen.
147+
148+
149+
### When the layout cannot change
150+
151+
When scaling the world is not practical, tell Box2D how many of your length units make up a meter
152+
and every tolerance in the first table above moves with it:
153+
154+
```dart
155+
class MyGame extends Forge2DGame {
156+
MyGame() : super(lengthUnitsPerMeter: 0.04);
157+
}
158+
```
159+
160+
A good rule of thumb is the height of your player character: if it is 0.04 units tall and you
161+
think of it as a person, pass 0.04. You are then responsible for gravity, densities and forces
162+
being sensible at that scale, using the same table.
163+
164+
This is a process-wide setting inside Box2D that cannot change once a physics world exists, so it
165+
can only be passed to the constructor, and several games running at the same time have to agree on
166+
it. A game that asks for a different value than one already in effect throws a `StateError` rather
167+
than quietly corrupting the simulation.
168+
169+
51170
## Forge2DWorld
52171

53172
The `Forge2DWorld` is a the world that all your [`BodyComponent`]s live in. In the `Forge2DGame`
@@ -59,19 +178,41 @@ to the `Forge2DGame` instance's `world` property, `game.world = Forge2DWorld()`.
59178

60179
If you would like to re-use a world later and have it keep its physics state you have to make sure
61180
that the bodies aren't destroyed when the world is removed from the game. You can do this by
62-
setting `world.destroyOnRemove` to false, like `game.world.destroyOnRemove = false;`.
181+
setting `world.destroyBodiesOnRemove` to false, like `game.world.destroyBodiesOnRemove = false;`.
182+
183+
The underlying Forge2D physics world is available as `world.physicsWorld`, which you can use to
184+
access the parts of the Forge2D API that `Forge2DWorld` doesn't wrap, like creating joints or
185+
polling the raw event streams.
63186

64187

65188
## BodyComponent
66189

67190
The `BodyComponent` is a wrapper for the `Forge2D` body, which is the body that the physics engine
68-
is interacting with. To create a `BodyComponent` you can either:
191+
is interacting with. A body carries one or more `Shape`s, which are created from a
192+
`ShapeGeometry` (`Circle`, `Capsule`, `Segment` or `Polygon`, plus chains via
193+
`body.createChain`) and an optional `ShapeDef` that holds the surface material (friction,
194+
restitution), density, filter, and event flags.
195+
196+
To create a `BodyComponent` you can either:
69197

70198
- override `createBody()` and create and return your created body;
71199
- use the default `createBody()` implementation by passing a `BodyDef` instance (and optionally a
72-
list of `FixtureDef` instances) to the BodyComponent's constructor;
200+
list of `ShapeSpec` instances, which pair a `ShapeGeometry` with an optional `ShapeDef`) to the
201+
BodyComponent's constructor;
73202
- use the default `createBody()` implementation and assign a `BodyDef` instance to `this.bodyDef`,
74-
and optionally a list of `FixtureDef` instances to `this.fixtureDefs`.
203+
and optionally a list of `ShapeSpec` instances to `this.shapeSpecs`.
204+
205+
```dart
206+
final ball = BodyComponent(
207+
bodyDef: BodyDef(type: BodyType.dynamic),
208+
shapeSpecs: [
209+
ShapeSpec(
210+
Circle(radius: 0.5),
211+
ShapeDef(material: SurfaceMaterial(restitution: 0.8)),
212+
),
213+
],
214+
);
215+
```
75216

76217
The `BodyComponent` is by default having `renderBody = true`, since otherwise, it wouldn't show
77218
anything after you have created a `Body` and added the `BodyComponent` to the game. If you want to
@@ -90,16 +231,18 @@ So instead of `add(Weapon()))`, `world.add(Weapon())` should be used (as below),
90231
should also of course initially be added to the world.
91232

92233
```dart
93-
class Weapon extends BodyComponent {
234+
class Weapon extends BodyComponent {
94235
@override
95-
void onLoad() {
96-
...
236+
Future<void> onLoad() async {
237+
await super.onLoad();
238+
// ...
97239
}
98240
}
99241
100-
class Player extends BodyComponent {
242+
class Player extends BodyComponent {
101243
@override
102-
void onLoad() {
244+
Future<void> onLoad() async {
245+
await super.onLoad();
103246
world.add(Weapon());
104247
}
105248
}
@@ -114,9 +257,9 @@ to avoid some tunneling problems.
114257

115258
`Forge2DGame` provides a simple out-of-the-box solution to propagate contact events.
116259

117-
Contact events occur whenever two `Fixture`s meet each other. These events allow listening when
118-
these `Fixture`s begin to come in contact (`beginContact`) and cease being in contact
119-
(`endContact`).
260+
Contact events occur whenever two `Shape`s meet each other. These events allow listening when
261+
these `Shape`s begin to come in contact (`beginContact`) and cease being in contact
262+
(`endContact`). Sensor overlaps are delivered through the same callbacks.
120263

121264
There are multiple ways to listen to these events. One common way is to use the `ContactCallbacks`
122265
class as a mixin in the `BodyComponent` where you are interested in these events.
@@ -133,13 +276,18 @@ class Ball extends BodyComponent with ContactCallbacks {
133276
}
134277
```
135278

136-
For the above to work, the `Ball`'s `body.userData` or contacting `fixture.userData` must be
137-
set to a `ContactCallback`. And if `Wall` is a `BodyComponent` it's `body.userData` or contacting
138-
`fixture.userData` must be set to `Wall`.
279+
For the above to work, the `Ball`'s `body.userData` or contacting `shape.userData` must be
280+
set to a `ContactCallbacks`. And if `Wall` is a `BodyComponent` its `body.userData` or contacting
281+
`shape.userData` must be set to `Wall`.
139282

140283
If `userData` is `null` the contact events are ignored, it is `null` by default.
141284

142-
A convenient way of setting `userData` is to assign it when creating the body. For example:
285+
Forge2D only generates events for shapes that have opted in to them, so the involved shapes also
286+
need `ShapeDef.enableContactEvents` set to true (and `ShapeDef.enableSensorEvents` for sensors
287+
and their visitors). The default `createBody()` implementation of `BodyComponent` enables these
288+
flags automatically for shapes created through `shapeSpecs` when a `ContactCallbacks` is present
289+
in the body's or shape's userData, but if you override `createBody()` you need to set them
290+
yourself:
143291

144292
```dart
145293
class Ball extends BodyComponent with ContactCallbacks {
@@ -151,14 +299,22 @@ class Ball extends BodyComponent with ContactCallbacks {
151299
final bodyDef = BodyDef(
152300
userData: this,
153301
);
302+
final shapeDef = ShapeDef(
303+
enableContactEvents: true,
304+
);
154305
...
155306
}
156307
157308
}
158309
```
159310

160311
Every time `Ball` and `Wall` begin to come in contact `beginContact` will be called, and once the
161-
fixtures cease being in contact, `endContact` will be called.
312+
shapes cease being in contact, `endContact` will be called.
313+
314+
The old `preSolve` and `postSolve` callbacks no longer exist. To disable a contact before it is
315+
solved (for example for one-sided platforms), use `world.preSolveCallback` together with
316+
`ShapeDef.enablePreSolveEvents`. To measure impact strength, enable `ShapeDef.enableHitEvents`
317+
and poll `world.physicsWorld.contactEvents.hit`.
162318

163319
An implementation example can be seen in the [Flame Forge2D
164320
example](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/bridge_libraries/flame_forge2d/utils/balls.dart).

0 commit comments

Comments
 (0)