mirror of
https://github.com/flame-engine/flame.git
synced 2025-10-30 00:17:20 +08:00
feat: Add FpsComponent and FpsTextComponent (#1595)
This commit is contained in:
@ -141,7 +141,7 @@ class MyGame extends FlameGame {
|
||||
children: [
|
||||
HighScoreDisplay(),
|
||||
HitPointsDisplay(),
|
||||
FpsCounter(),
|
||||
FpsComponent(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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';
|
||||
|
||||
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;
|
||||
|
||||
GameRenderBox(this.buildContext, this.game) {
|
||||
//ignore: deprecated_member_use_from_same_package
|
||||
WidgetsBinding.instance!.addTimingsCallback(game.onTimingsCallback);
|
||||
}
|
||||
|
||||
|
||||
@ -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 = [];
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
class ExampleGame extends OxygenGame with FPSCounter {
|
||||
class ExampleGame extends OxygenGame {
|
||||
@override
|
||||
Future<void> init() async {
|
||||
if (kDebugMode) {
|
||||
|
||||
@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user