diff --git a/doc/flame/components.md b/doc/flame/components.md index 1921868d9..0f04a2f67 100644 --- a/doc/flame/components.md +++ b/doc/flame/components.md @@ -141,7 +141,7 @@ class MyGame extends FlameGame { children: [ HighScoreDisplay(), HitPointsDisplay(), - FpsCounter(), + FpsComponent(), ], ), ); diff --git a/doc/flame/other/debug.md b/doc/flame/other/debug.md index 1b2f5a99a..40854a8f3 100644 --- a/doc/flame/other/debug.md +++ b/doc/flame/other/debug.md @@ -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. diff --git a/examples/lib/stories/collision_detection/multiple_shapes_example.dart b/examples/lib/stories/collision_detection/multiple_shapes_example.dart index 7c482305a..8a8c957fa 100644 --- a/examples/lib/stories/collision_detection/multiple_shapes_example.dart +++ b/examples/lib/stories/collision_detection/multiple_shapes_example.dart @@ -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 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 diff --git a/examples/lib/stories/components/debug_example.dart b/examples/lib/stories/components/debug_example.dart index fedaf62e2..f4e866a05 100644 --- a/examples/lib/stories/components/debug_example.dart +++ b/examples/lib/stories/components/debug_example.dart @@ -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))); } } diff --git a/examples/lib/stories/rendering/particles_example.dart b/examples/lib/stories/rendering/particles_example.dart index 73a5280fb..9c3b367c6 100644 --- a/examples/lib/stories/rendering/particles_example.dart +++ b/examples/lib/stories/rendering/particles_example.dart @@ -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); diff --git a/packages/flame/lib/components.dart b/packages/flame/lib/components.dart index e1d609d42..8de9e9bf8 100644 --- a/packages/flame/lib/components.dart +++ b/packages/flame/lib/components.dart @@ -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'; diff --git a/packages/flame/lib/src/components/fps_component.dart b/packages/flame/lib/src/components/fps_component.dart new file mode 100644 index 000000000..527b43c7c --- /dev/null +++ b/packages/flame/lib/src/components/fps_component.dart @@ -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 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; + } +} diff --git a/packages/flame/lib/src/components/fps_text_component.dart b/packages/flame/lib/src/components/fps_text_component.dart new file mode 100644 index 000000000..f8665d55c --- /dev/null +++ b/packages/flame/lib/src/components/fps_text_component.dart @@ -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 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'; + } +} diff --git a/packages/flame/lib/src/game/game_render_box.dart b/packages/flame/lib/src/game/game_render_box.dart index 32f783270..09f48dd0c 100644 --- a/packages/flame/lib/src/game/game_render_box.dart +++ b/packages/flame/lib/src/game/game_render_box.dart @@ -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); } diff --git a/packages/flame/lib/src/game/mixins/fps_counter.dart b/packages/flame/lib/src/game/mixins/fps_counter.dart index 916af6f6d..ed39a6967 100644 --- a/packages/flame/lib/src/game/mixins/fps_counter.dart +++ b/packages/flame/lib/src/game/mixins/fps_counter.dart @@ -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 _previousTimings = []; diff --git a/packages/flame/lib/src/game/mixins/game.dart b/packages/flame/lib/src/game/mixins/game.dart index 38438e8c9..0c3641824 100644 --- a/packages/flame/lib/src/game/mixins/game.dart +++ b/packages/flame/lib/src/game/mixins/game.dart @@ -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 timings) {} /// Method to perform late initialization of the [Game] class. diff --git a/packages/flame/test/components/fps_component_test.dart b/packages/flame/test/components/fps_component_test.dart new file mode 100644 index 000000000..d9749fd30 --- /dev/null +++ b/packages/flame/test/components/fps_component_test.dart @@ -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)); + }); + }); +} diff --git a/packages/flame_oxygen/example/lib/main.dart b/packages/flame_oxygen/example/lib/main.dart index bc4be70d5..7b01c8f31 100644 --- a/packages/flame_oxygen/example/lib/main.dart +++ b/packages/flame_oxygen/example/lib/main.dart @@ -16,7 +16,7 @@ void main() { runApp(GameWidget(game: ExampleGame())); } -class ExampleGame extends OxygenGame with FPSCounter { +class ExampleGame extends OxygenGame { @override Future init() async { if (kDebugMode) { diff --git a/packages/flame_oxygen/example/lib/system/debug_system.dart b/packages/flame_oxygen/example/lib/system/debug_system.dart index 90b767ea7..55736b5f7 100644 --- a/packages/flame_oxygen/example/lib/system/debug_system.dart +++ b/packages/flame_oxygen/example/lib/system/debug_system.dart @@ -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(),