feat: Add FpsComponent and FpsTextComponent (#1595)

This commit is contained in:
Lukas Klingsbo
2022-05-11 19:49:14 +02:00
committed by GitHub
parent 02be4acd87
commit 4c68c2b0a2
14 changed files with 166 additions and 71 deletions

View File

@ -141,7 +141,7 @@ class MyGame extends FlameGame {
children: [
HighScoreDisplay(),
HitPointsDisplay(),
FpsCounter(),
FpsComponent(),
],
),
);

View File

@ -1,24 +1,5 @@
# Debug features
## FPS counter
Flame provides the `FPSCounter` mixin for recording the fps; this mixin can be applied on any class
that extends from `Game`. Once applied you can access the current fps by using the `fps` method,
like shown in the example below.
```dart
class MyGame extends FlameGame with FPSCounter {
static final fpsTextConfig = TextConfig(color: BasicPalette.white.color);
@override
void render(Canvas canvas) {
super.render(canvas);
final fpsCount = fps(120); // The average FPS for the last 120 microseconds.
fpsTextConfig.render(canvas, fpsCount.toString(), Vector2(0, 50));
}
}
```
## FlameGame features
Flame provides some debugging features for the `FlameGame` class. These features are enabled when
@ -29,3 +10,25 @@ boundaries and positions.
To see a working example of the debugging features of the `FlameGame`, check this
[example](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/components/debug_example.dart).
## FPS
The FPS reported from Flame might be a bit lower than what is reported from for example the Flutter
DevTools, depending on which platform you are targeting. The source of truth for how many FPS your
game is running in should be the FPS that we are reporting, since that is what our game loop is
bound by.
### FpsComponent
The `FpsComponent` can be added to anywhere in the component tree and will keep track of how many
FPS that the game is currently rendering in. If you want to display this as text in the game, use
the [](#fpstextcomponent).
### FpsTextComponent
The `FpsTextComponent` is simply a [](../rendering/text.md#textcomponent) that wraps an
[](../rendering/text.md#textcomponent), since you most commonly want to show the current FPS
somewhere when you the [](#fpscomponent) is used.

View File

@ -11,7 +11,7 @@ import 'package:flutter/material.dart' hide Image, Draggable;
enum Shapes { circle, rectangle, polygon }
class MultipleShapesExample extends FlameGame
with HasCollisionDetection, HasDraggables, FPSCounter {
with HasCollisionDetection, HasDraggables {
static const description = '''
An example with many hitboxes that move around on the screen and during
collisions they change color depending on what it is that they have collided
@ -25,10 +25,9 @@ class MultipleShapesExample extends FlameGame
any direction.
''';
final TextPaint fpsTextPaint = TextPaint();
@override
Future<void> onLoad() async {
add(FpsTextComponent(position: Vector2(0, size.y - 24)));
final screenHitbox = ScreenHitbox();
final snowman = CollidableSnowman(
Vector2.all(150),
@ -79,16 +78,6 @@ class MultipleShapesExample extends FlameGame
rng: _rng,
);
}
@override
void render(Canvas canvas) {
super.render(canvas);
fpsTextPaint.render(
canvas,
'${fps(120).toStringAsFixed(2)}fps',
Vector2(0, size.y - 24),
);
}
}
abstract class MyCollidable extends PositionComponent

View File

@ -1,17 +1,13 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
class DebugExample extends FlameGame with FPSCounter {
class DebugExample extends FlameGame {
static const String description = '''
In this example we show what you will see when setting `debugMode = true` on
your game. It is a non-interactive example.
In this example we show what you will see when setting `debugMode = true`
and add the `FPSTextComponent` to your game.
This is a non-interactive example.
''';
static final fpsTextPaint = TextPaint(
style: const TextStyle(color: Color(0xFFFFFFFF)),
);
@override
bool debugMode = true;
@ -36,15 +32,8 @@ class DebugExample extends FlameGame with FPSCounter {
add(flame1);
add(flame2);
add(flame3);
}
@override
void render(Canvas canvas) {
super.render(canvas);
if (debugMode) {
fpsTextPaint.render(canvas, fps(120).toString(), Vector2(0, 50));
}
add(FpsTextComponent(position: Vector2(0, size.y - 24)));
}
}

View File

@ -8,7 +8,7 @@ import 'package:flame/sprite.dart';
import 'package:flame/timer.dart' as flame_timer;
import 'package:flutter/material.dart' hide Image;
class ParticlesExample extends FlameGame with FPSCounter {
class ParticlesExample extends FlameGame {
static const String description = '''
In this example we show how to render a lot of different particles.
''';
@ -25,9 +25,6 @@ class ParticlesExample extends FlameGame with FPSCounter {
Timer? spawnTimer;
final StepTween steppedTween = StepTween(begin: 0, end: 5);
final trafficLight = TrafficLightComponent();
final TextPaint fpsTextPaint = TextPaint(
style: const TextStyle(color: Colors.white),
);
/// Defines the lifespan of all the particles in these examples
final sceneDuration = const Duration(seconds: 1);
@ -482,22 +479,6 @@ class ParticlesExample extends FlameGame with FPSCounter {
);
}
@override
bool debugMode = true;
@override
void render(Canvas canvas) {
super.render(canvas);
if (debugMode) {
fpsTextPaint.render(
canvas,
'${fps(120).toStringAsFixed(2)}fps',
Vector2(0, size.y - 24),
);
}
}
/// Returns random [Vector2] within a virtual grid cell
Vector2 randomCellVector2() {
return (Vector2.random() - Vector2.random())..multiply(cellSize);

View File

@ -6,6 +6,8 @@ export 'src/components/component.dart';
export 'src/components/component_point_pair.dart';
export 'src/components/component_set.dart';
export 'src/components/custom_painter_component.dart';
export 'src/components/fps_component.dart';
export 'src/components/fps_text_component.dart';
export 'src/components/input/joystick_component.dart';
export 'src/components/isometric_tile_map_component.dart';
export 'src/components/mixins/draggable.dart';

View File

@ -0,0 +1,37 @@
import 'dart:collection';
import '../../components.dart';
/// The [FpsComponent] is a non-visual component which you can get the current
/// fps of the game with by calling [fps], once the component has been added to
/// the component tree.
class FpsComponent extends Component {
FpsComponent({
this.windowSize = 60,
});
/// The sliding window size, i.e. the number of game ticks over which the fps
/// measure will be averaged.
final int windowSize;
/// The queue of the recent game tick durations.
/// The length of this queue will not exceed [windowSize].
final Queue<double> window = Queue();
/// The sum of all values in the [window] queue.
double _sum = 0;
@override
void update(double dt) {
window.addLast(dt);
_sum += dt;
if (window.length > windowSize) {
_sum -= window.removeFirst();
}
}
/// Get the current average FPS over the last [windowSize] frames.
double get fps {
return window.isEmpty ? 0 : window.length / _sum;
}
}

View File

@ -0,0 +1,37 @@
import '../../components.dart';
/// The [FpsTextComponent] is a [TextComponent] that writes out the current FPS.
/// It has a [FpsComponent] as a child which does the actual calculations.
class FpsTextComponent<T extends TextRenderer> extends TextComponent {
FpsTextComponent({
int windowSize = 60,
this.decimalPlaces = 0,
T? textRenderer,
Vector2? position,
Vector2? size,
Vector2? scale,
double? angle,
Anchor? anchor,
int? priority,
}) : fpsComponent = FpsComponent(windowSize: windowSize),
super(
textRenderer: textRenderer,
position: position,
size: size,
scale: scale,
angle: angle,
anchor: anchor,
priority: priority ?? double.maxFinite.toInt(),
) {
positionType = PositionType.viewport;
add(fpsComponent);
}
final int decimalPlaces;
final FpsComponent fpsComponent;
@override
void update(double dt) {
text = '${fpsComponent.fps.toStringAsFixed(decimalPlaces)} FPS';
}
}

View File

@ -10,6 +10,7 @@ class GameRenderBox extends RenderBox with WidgetsBindingObserver {
GameLoop? gameLoop;
GameRenderBox(this.buildContext, this.game) {
//ignore: deprecated_member_use_from_same_package
WidgetsBinding.instance!.addTimingsCallback(game.onTimingsCallback);
}

View File

@ -6,6 +6,10 @@ const _maxFrames = 60;
const frameInterval =
Duration(microseconds: Duration.microsecondsPerSecond ~/ _maxFrames);
@Deprecated(
'Use FPSComponent or FPSTextComponent instead. '
'FPSCounter will be removed in v1.3.0',
)
mixin FPSCounter on Game {
List<FrameTiming> _previousTimings = [];

View File

@ -98,6 +98,7 @@ mixin Game {
void lifecycleStateChange(AppLifecycleState state) {}
/// Use for calculating the FPS.
@Deprecated('Use FPSComponent instead, will be removed in v1.3.0')
void onTimingsCallback(List<FrameTiming> timings) {}
/// Method to perform late initialization of the [Game] class.

View File

@ -0,0 +1,52 @@
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:test/test.dart';
void main() {
const _diff = 0.0000000000001;
group('FPSComponent', () {
testWithFlameGame('reports correct FPS for 1 frames', (game) async {
final fpsComponent = FpsComponent();
await game.ensureAdd(fpsComponent);
expect(fpsComponent.fps, 0);
game.update(1 / 60);
expect(fpsComponent.fps, closeTo(60, _diff));
});
testWithFlameGame('reports correct FPS with full window', (game) async {
const windowSize = 30;
final fpsComponent = FpsComponent(windowSize: windowSize);
await game.ensureAdd(fpsComponent);
for (var i = 0; i < windowSize; i++) {
game.update(1 / 60);
}
expect(fpsComponent.fps, closeTo(60, _diff));
});
testWithFlameGame('reports correct FPS with slided window', (game) async {
const windowSize = 30;
final fpsComponent = FpsComponent(windowSize: windowSize);
await game.ensureAdd(fpsComponent);
for (var i = 0; i < 2 * windowSize; i++) {
game.update(1 / 60);
}
expect(fpsComponent.fps, closeTo(60, _diff));
});
testWithFlameGame('reports correct FPS with varying dt', (game) async {
final fpsComponent = FpsComponent();
await game.ensureAdd(fpsComponent);
for (var i = 0; i < fpsComponent.windowSize; i++) {
// Alternating between 50 and 100 FPS
final dt = i.isEven ? 1 / 100 : 1 / 50;
game.update(dt);
}
expect(fpsComponent.fps, closeTo(100 / 1.5, _diff));
});
});
}

View File

@ -16,7 +16,7 @@ void main() {
runApp(GameWidget(game: ExampleGame()));
}
class ExampleGame extends OxygenGame with FPSCounter {
class ExampleGame extends OxygenGame {
@override
Future<void> init() async {
if (kDebugMode) {

View File

@ -24,7 +24,6 @@ class DebugSystem extends BaseSystem {
statusPainter.render(
canvas,
[
'FPS: ${(world!.game as FPSCounter).fps()}',
'Entities: ${world!.entities.length}',
].join('\n'),
Vector2.zero(),