mirror of
https://github.com/flame-engine/flame.git
synced 2025-10-30 08:27:36 +08:00
feat: Add FpsComponent and FpsTextComponent (#1595)
This commit is contained in:
@ -141,7 +141,7 @@ class MyGame extends FlameGame {
|
|||||||
children: [
|
children: [
|
||||||
HighScoreDisplay(),
|
HighScoreDisplay(),
|
||||||
HitPointsDisplay(),
|
HitPointsDisplay(),
|
||||||
FpsCounter(),
|
FpsComponent(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,24 +1,5 @@
|
|||||||
# Debug features
|
# 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
|
## FlameGame features
|
||||||
|
|
||||||
Flame provides some debugging features for the `FlameGame` class. These features are enabled when
|
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
|
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).
|
[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.
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import 'package:flutter/material.dart' hide Image, Draggable;
|
|||||||
enum Shapes { circle, rectangle, polygon }
|
enum Shapes { circle, rectangle, polygon }
|
||||||
|
|
||||||
class MultipleShapesExample extends FlameGame
|
class MultipleShapesExample extends FlameGame
|
||||||
with HasCollisionDetection, HasDraggables, FPSCounter {
|
with HasCollisionDetection, HasDraggables {
|
||||||
static const description = '''
|
static const description = '''
|
||||||
An example with many hitboxes that move around on the screen and during
|
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
|
collisions they change color depending on what it is that they have collided
|
||||||
@ -25,10 +25,9 @@ class MultipleShapesExample extends FlameGame
|
|||||||
any direction.
|
any direction.
|
||||||
''';
|
''';
|
||||||
|
|
||||||
final TextPaint fpsTextPaint = TextPaint();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onLoad() async {
|
Future<void> onLoad() async {
|
||||||
|
add(FpsTextComponent(position: Vector2(0, size.y - 24)));
|
||||||
final screenHitbox = ScreenHitbox();
|
final screenHitbox = ScreenHitbox();
|
||||||
final snowman = CollidableSnowman(
|
final snowman = CollidableSnowman(
|
||||||
Vector2.all(150),
|
Vector2.all(150),
|
||||||
@ -79,16 +78,6 @@ class MultipleShapesExample extends FlameGame
|
|||||||
rng: _rng,
|
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
|
abstract class MyCollidable extends PositionComponent
|
||||||
|
|||||||
@ -1,17 +1,13 @@
|
|||||||
import 'package:flame/components.dart';
|
import 'package:flame/components.dart';
|
||||||
import 'package:flame/game.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 = '''
|
static const String description = '''
|
||||||
In this example we show what you will see when setting `debugMode = true` on
|
In this example we show what you will see when setting `debugMode = true`
|
||||||
your game. It is a non-interactive example.
|
and add the `FPSTextComponent` to your game.
|
||||||
|
This is a non-interactive example.
|
||||||
''';
|
''';
|
||||||
|
|
||||||
static final fpsTextPaint = TextPaint(
|
|
||||||
style: const TextStyle(color: Color(0xFFFFFFFF)),
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool debugMode = true;
|
bool debugMode = true;
|
||||||
|
|
||||||
@ -36,15 +32,8 @@ class DebugExample extends FlameGame with FPSCounter {
|
|||||||
add(flame1);
|
add(flame1);
|
||||||
add(flame2);
|
add(flame2);
|
||||||
add(flame3);
|
add(flame3);
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
add(FpsTextComponent(position: Vector2(0, size.y - 24)));
|
||||||
void render(Canvas canvas) {
|
|
||||||
super.render(canvas);
|
|
||||||
|
|
||||||
if (debugMode) {
|
|
||||||
fpsTextPaint.render(canvas, fps(120).toString(), Vector2(0, 50));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import 'package:flame/sprite.dart';
|
|||||||
import 'package:flame/timer.dart' as flame_timer;
|
import 'package:flame/timer.dart' as flame_timer;
|
||||||
import 'package:flutter/material.dart' hide Image;
|
import 'package:flutter/material.dart' hide Image;
|
||||||
|
|
||||||
class ParticlesExample extends FlameGame with FPSCounter {
|
class ParticlesExample extends FlameGame {
|
||||||
static const String description = '''
|
static const String description = '''
|
||||||
In this example we show how to render a lot of different particles.
|
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;
|
Timer? spawnTimer;
|
||||||
final StepTween steppedTween = StepTween(begin: 0, end: 5);
|
final StepTween steppedTween = StepTween(begin: 0, end: 5);
|
||||||
final trafficLight = TrafficLightComponent();
|
final trafficLight = TrafficLightComponent();
|
||||||
final TextPaint fpsTextPaint = TextPaint(
|
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Defines the lifespan of all the particles in these examples
|
/// Defines the lifespan of all the particles in these examples
|
||||||
final sceneDuration = const Duration(seconds: 1);
|
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
|
/// Returns random [Vector2] within a virtual grid cell
|
||||||
Vector2 randomCellVector2() {
|
Vector2 randomCellVector2() {
|
||||||
return (Vector2.random() - Vector2.random())..multiply(cellSize);
|
return (Vector2.random() - Vector2.random())..multiply(cellSize);
|
||||||
|
|||||||
@ -6,6 +6,8 @@ export 'src/components/component.dart';
|
|||||||
export 'src/components/component_point_pair.dart';
|
export 'src/components/component_point_pair.dart';
|
||||||
export 'src/components/component_set.dart';
|
export 'src/components/component_set.dart';
|
||||||
export 'src/components/custom_painter_component.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/input/joystick_component.dart';
|
||||||
export 'src/components/isometric_tile_map_component.dart';
|
export 'src/components/isometric_tile_map_component.dart';
|
||||||
export 'src/components/mixins/draggable.dart';
|
export 'src/components/mixins/draggable.dart';
|
||||||
|
|||||||
37
packages/flame/lib/src/components/fps_component.dart
Normal file
37
packages/flame/lib/src/components/fps_component.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
packages/flame/lib/src/components/fps_text_component.dart
Normal file
37
packages/flame/lib/src/components/fps_text_component.dart
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,6 +10,7 @@ class GameRenderBox extends RenderBox with WidgetsBindingObserver {
|
|||||||
GameLoop? gameLoop;
|
GameLoop? gameLoop;
|
||||||
|
|
||||||
GameRenderBox(this.buildContext, this.game) {
|
GameRenderBox(this.buildContext, this.game) {
|
||||||
|
//ignore: deprecated_member_use_from_same_package
|
||||||
WidgetsBinding.instance!.addTimingsCallback(game.onTimingsCallback);
|
WidgetsBinding.instance!.addTimingsCallback(game.onTimingsCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,10 @@ const _maxFrames = 60;
|
|||||||
const frameInterval =
|
const frameInterval =
|
||||||
Duration(microseconds: Duration.microsecondsPerSecond ~/ _maxFrames);
|
Duration(microseconds: Duration.microsecondsPerSecond ~/ _maxFrames);
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
'Use FPSComponent or FPSTextComponent instead. '
|
||||||
|
'FPSCounter will be removed in v1.3.0',
|
||||||
|
)
|
||||||
mixin FPSCounter on Game {
|
mixin FPSCounter on Game {
|
||||||
List<FrameTiming> _previousTimings = [];
|
List<FrameTiming> _previousTimings = [];
|
||||||
|
|
||||||
|
|||||||
@ -98,6 +98,7 @@ mixin Game {
|
|||||||
void lifecycleStateChange(AppLifecycleState state) {}
|
void lifecycleStateChange(AppLifecycleState state) {}
|
||||||
|
|
||||||
/// Use for calculating the FPS.
|
/// Use for calculating the FPS.
|
||||||
|
@Deprecated('Use FPSComponent instead, will be removed in v1.3.0')
|
||||||
void onTimingsCallback(List<FrameTiming> timings) {}
|
void onTimingsCallback(List<FrameTiming> timings) {}
|
||||||
|
|
||||||
/// Method to perform late initialization of the [Game] class.
|
/// Method to perform late initialization of the [Game] class.
|
||||||
|
|||||||
52
packages/flame/test/components/fps_component_test.dart
Normal file
52
packages/flame/test/components/fps_component_test.dart
Normal 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));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -16,7 +16,7 @@ void main() {
|
|||||||
runApp(GameWidget(game: ExampleGame()));
|
runApp(GameWidget(game: ExampleGame()));
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExampleGame extends OxygenGame with FPSCounter {
|
class ExampleGame extends OxygenGame {
|
||||||
@override
|
@override
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
|
|||||||
@ -24,7 +24,6 @@ class DebugSystem extends BaseSystem {
|
|||||||
statusPainter.render(
|
statusPainter.render(
|
||||||
canvas,
|
canvas,
|
||||||
[
|
[
|
||||||
'FPS: ${(world!.game as FPSCounter).fps()}',
|
|
||||||
'Entities: ${world!.entities.length}',
|
'Entities: ${world!.entities.length}',
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
Vector2.zero(),
|
Vector2.zero(),
|
||||||
|
|||||||
Reference in New Issue
Block a user