Skip to content

Commit

Permalink
feat: Component tree for the devtools extension tab (#3094)
Browse files Browse the repository at this point in the history
This PR creates a devtools extension tab for Flame where you can:
 * Turn on `debugMode` for a chosen component or the whole tree
 * See the component tree
 * Play, pause and step the game loop.

[Screencast from 2024-03-24
22-09-55.webm](https://github.com/flame-engine/flame/assets/744771/82217afb-e096-4375-a892-b59e1be6d7ec)
  • Loading branch information
spydon committed Mar 25, 2024
1 parent b1f0198 commit bf5d68e
Show file tree
Hide file tree
Showing 14 changed files with 407 additions and 48 deletions.
8 changes: 8 additions & 0 deletions doc/flame/other/debug.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ To see a working example of the debugging features of the `FlameGame`, check thi
[example](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/components/debug_example.dart).


## Devtools extension

If you open the [Flutter DevTools](https://docs.flutter.dev/tools/devtools/overview), you will see a
new tab called "Flame". This tab will show you information about the current game, for example a
visualization of the component tree, the ability to play, pause and step the game, information
about the selected component, and more.


## FPS

The FPS reported from Flame might be a bit lower than what is reported from for example the Flutter
Expand Down
11 changes: 11 additions & 0 deletions melos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ command:
branch: main
# Generates a link to a prefilled GitHub release creation page.
releaseUrl: true

bootstrap:
environment:
sdk: ">=3.0.0 <4.0.0"
Expand All @@ -24,6 +25,10 @@ command:
dartdoc: ^6.3.0
mocktail: ^1.0.1
test: any

publish:
hooks:
pre: melos devtools-build

scripts:
lint:all:
Expand Down Expand Up @@ -118,3 +123,9 @@ scripts:
packageFilters:
scope: flame_devtools
description: Builds the devtools and copies the build directory to the Flame package.

devtools-simulator:
run: melos exec -- flutter run -d chrome --dart-define=use_simulated_environment=true
packageFilters:
scope: flame_devtools
description: Runs the devtools in the simulated mode.
1 change: 1 addition & 0 deletions packages/flame/.pubignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!extension/**/*
2 changes: 2 additions & 0 deletions packages/flame/lib/devtools.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export 'src/devtools/connectors/component_tree_connector.dart'
show ComponentTreeNode;
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import 'dart:convert';
import 'dart:developer';

import 'package:flame/components.dart';
import 'package:flame/src/devtools/dev_tools_connector.dart';

/// The [ComponentTreeConnector] is responsible for reporting the component
/// tree of the game to the devtools extension.
class ComponentTreeConnector extends DevToolsConnector {
@override
void init() {
// Get the component tree of the game.
registerExtension(
'ext.flame_devtools.getComponentTree',
(method, parameters) async {
final componentTree = ComponentTreeNode.fromComponent(game);

return ServiceExtensionResponse.result(
json.encode({
'component_tree': componentTree.toJson(),
}),
);
},
);
}
}

/// This should only be used internally by the devtools extension.
class ComponentTreeNode {
final int id;
final String name;
final String toStringText;
final List<ComponentTreeNode> children;

ComponentTreeNode(this.id, this.name, this.toStringText, this.children);

ComponentTreeNode.fromComponent(Component component)
: this(
component.hashCode,
component.runtimeType.toString(),
component.toString(),
component.children.map(ComponentTreeNode.fromComponent).toList(),
);

ComponentTreeNode.fromJson(Map<String, dynamic> json)
: this(
json['id'] as int,
json['name'] as String,
json['toString'] as String,
(json['children'] as List)
.map((e) => ComponentTreeNode.fromJson(e as Map<String, dynamic>))
.toList(),
);

Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'toString': toStringText,
'children': children.map((e) => e.toJson()).toList(),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,75 @@ import 'dart:convert';
import 'dart:developer';

import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/src/devtools/dev_tools_connector.dart';
import 'package:flutter/foundation.dart';

/// The [DebugModeConnector] is responsible for reporting and setting the
/// `debugMode` of the game from the devtools extension.
class DebugModeConnector extends DevToolsConnector {
var _debugModeNotifier = ValueNotifier<bool>(false);

@override
void init() {
// Get the current `debugMode`.
// Get the `debugMode` for a component in the tree.
// If no id is provided, the `debugMode` for the entire game will be
// returned.
registerExtension(
'ext.flame_devtools.getDebugMode',
(method, parameters) async {
final id = int.tryParse(parameters['id'] ?? '') ?? game.hashCode;
return ServiceExtensionResponse.result(
json.encode({
'debug_mode': _debugModeNotifier.value,
'id': id,
'debug_mode': _getDebugMode(id),
}),
);
},
);

// Set the `debugMode` for all components in the tree.
// Set the `debugMode` for a component in the tree.
// If no id is provided, the `debugMode` will be set for the entire game.
registerExtension(
'ext.flame_devtools.setDebugMode',
(method, parameters) async {
final id = int.tryParse(parameters['id'] ?? '');
final debugMode = bool.parse(parameters['debug_mode'] ?? 'false');
_debugModeNotifier.value = debugMode;
_setDebugMode(debugMode, id: id);
return ServiceExtensionResponse.result(
json.encode({
'id': id,
'debug_mode': debugMode,
}),
);
},
);
}

@override
void initGame(FlameGame game) {
super.initGame(game);
_debugModeNotifier = ValueNotifier<bool>(game.debugMode);
_debugModeNotifier.addListener(() {
final newDebugMode = _debugModeNotifier.value;
game.propagateToChildren<Component>(
(c) {
c.debugMode = newDebugMode;
return true;
},
includeSelf: true,
);
});
bool _getDebugMode(int id) {
var debugMode = false;
game.propagateToChildren<Component>(
(c) {
if (c.hashCode == id) {
debugMode = c.debugMode;
return false;
}
return true;
},
includeSelf: true,
);
return debugMode;
}

@override
void disposeGame() {
_debugModeNotifier.dispose();
void _setDebugMode(bool debugMode, {int? id}) {
game.propagateToChildren<Component>(
(c) {
if (id == null) {
c.debugMode = debugMode;
return true;
} else if (c.hashCode == id) {
c.debugMode = debugMode;
return false;
}
return true;
},
includeSelf: true,
);
}
}
2 changes: 2 additions & 0 deletions packages/flame/lib/src/devtools/dev_tools_service.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flame/game.dart';
import 'package:flame/src/devtools/connectors/component_count_connector.dart';
import 'package:flame/src/devtools/connectors/component_tree_connector.dart';
import 'package:flame/src/devtools/connectors/debug_mode_connector.dart';
import 'package:flame/src/devtools/connectors/game_loop_connector.dart';
import 'package:flame/src/devtools/dev_tools_connector.dart';
Expand Down Expand Up @@ -32,6 +33,7 @@ class DevToolsService {
final connectors = [
DebugModeConnector(),
ComponentCountConnector(),
ComponentTreeConnector(),
GameLoopConnector(),
];

Expand Down
27 changes: 27 additions & 0 deletions packages/flame_devtools/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
<!-- markdownlint-disable MD013 -->
<p align="center">
<a href="https://flame-engine.org">
<img alt="flame" width="200px" src="https://user-images.githubusercontent.com/6718144/101553774-3bc7b000-39ad-11eb-8a6a-de2daa31bd64.png">
</a>
</p>

<p align="center">
A Flutter-based game engine.
</p>

<p align="center">
<a title="Pub" href="https://pub.dev/packages/flame"><img src="https://img.shields.io/pub/v/flame.svg?style=popout"/></a>
<a title="Test" href="https://github.com/flame-engine/flame/actions?query=workflow%3Acicd+branch%3Amain"><img src="https://github.com/flame-engine/flame/workflows/cicd/badge.svg?branch=main&event=push"/></a>
<a title="Discord" href="https://discord.gg/pxrBmy4"><img src="https://img.shields.io/discord/509714518008528896.svg"/></a>
<a title="Melos" href="https://github.com/invertase/melos"><img src="https://img.shields.io/badge/maintained%20with-melos-f700ff.svg"/></a>
</p>

---
<!-- markdownlint-enable MD013 -->

# flame_devtools

A DevTools extension for Flame games. To use it you just have to run your
Expand All @@ -18,3 +39,9 @@ To develop things from the Flame side, create a new `DevToolsConnector` which
registers the new extension end points so that you can communicate with Flame
from the devtools extension. Don't forget to add the new connector to the
list of connectors in the `DevToolsService` class.

If you want to run with the devtools extension with the simulated mode for
faster development, you can use `melos devtools-simulate` to start the
simulated environment and run the devtools extension in the browser.
Remember that you have to manually enter the Dart VM Service Connection URI
in the simulated devtools environment.
7 changes: 3 additions & 4 deletions packages/flame_devtools/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:devtools_extensions/devtools_extensions.dart';
import 'package:flame_devtools/widgets/component_counter.dart';
import 'package:flame_devtools/widgets/component_tree.dart';
import 'package:flame_devtools/widgets/debug_mode_button.dart';
import 'package:flame_devtools/widgets/game_loop_controls.dart';
import 'package:flutter/material.dart';
Expand All @@ -16,15 +16,14 @@ class FlameDevTools extends StatelessWidget {
return DevToolsExtension(
child: Column(
children: [
const GameLoopControls(),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const ComponentCounter(),
const GameLoopControls(),
const DebugModeButton(),
].withSpacing(),
),
const Expanded(child: ComponentTree()),
].withSpacing(),
),
);
Expand Down
23 changes: 19 additions & 4 deletions packages/flame_devtools/lib/repository.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:devtools_extensions/devtools_extensions.dart';
import 'package:flame/devtools.dart';

sealed class Repository {
Repository._();
Expand All @@ -11,19 +12,33 @@ sealed class Repository {
return componentCountResponse.json!['component_count'] as int;
}

static Future<bool> swapDebugMode() async {
final nextDebugMode = !(await getDebugMode());
static Future<ComponentTreeNode> getComponentTree() async {
final componentTreeResponse =
await serviceManager.callServiceExtensionOnMainIsolate(
'ext.flame_devtools.getComponentTree',
);
return ComponentTreeNode.fromJson(
componentTreeResponse.json!['component_tree'] as Map<String, dynamic>,
);
}

static Future<bool> swapDebugMode({int? id}) async {
final nextDebugMode = !(await getDebugMode(id: id));
await serviceManager.callServiceExtensionOnMainIsolate(
'ext.flame_devtools.setDebugMode',
args: {'debug_mode': nextDebugMode},
args: {
'debug_mode': nextDebugMode,
'id': id,
},
);
return nextDebugMode;
}

static Future<bool> getDebugMode() async {
static Future<bool> getDebugMode({int? id}) async {
final debugModeResponse =
await serviceManager.callServiceExtensionOnMainIsolate(
'ext.flame_devtools.getDebugMode',
args: {'id': id},
);
return debugModeResponse.json!['debug_mode'] as bool;
}
Expand Down
Loading

0 comments on commit bf5d68e

Please sign in to comment.