diff --git a/CHANGELOG.md b/CHANGELOG.md index 50704ed0e..ca4d57057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,10 @@ ## [next] + +## 0.17.0 - Fixing FlareAnimation API to match convention - Fixing FlareComponent renderization - Added default render fucntion for Box2D ChainShape +- New GestureDetector API to Game ## 0.16.1 - Added `Bgm` class for easy looping background music management. diff --git a/FAQ.md b/FAQ.md index 3f2f44ed0..9181d280a 100644 --- a/FAQ.md +++ b/FAQ.md @@ -52,6 +52,12 @@ If you are using `BaseGame`, you have a `camera` attribute that allows you to of For a more in-depth tutorial on how the camera works (in general & in Flame) and how to use it, check [erickzanardo](https://github.com/erickzanardo)'s [excellent tutorial](https://fireslime.xyz/articles/20190911_Basic_Camera_Usage_In_Flame.html), published via FireSlime. +## How to handle touch events on your game? + +You can always use all the Widgets and features that Flutter already provide for that, but Flame wraps gesture detector callbacks on its base Game class so it can ben a little easier to handle those events, you can find more about it on the input documentation page: + +https://flame-engine.org/docs/input.md + ## Other questions? Didn't find what you needed here? Please head to [FireSlime's Discord channel](https://discord.gg/pxrBmy4) where the community might help you with more questions. diff --git a/README.md b/README.md index aba0efb6d..218a08da5 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Just drop it in your `pubspec.yaml`: ```yaml dependencies: - flame: ^0.16.1 + flame: ^0.17.0 ``` And start using it! @@ -263,21 +263,26 @@ A very simple `BaseGame` implementation example can be seen below: ### Input -In order to handle user input, you can use the libraries provided by Flutter for regular apps: [Gesture Recognizers](https://flutter.io/gestures/). +Inside `package:flame/gestures.dart` you can find a whole set of `mixin` which can be included on your game class instance to be able to receive touch input events -However, in order to bind them, use the `Flame.util.addGestureRecognizer` method; in doing so, you'll make sure they are properly unbound when the game widget is not being rendered, and so the rest of your screens will work appropriately. - -For example, to add a tap listener ("on click"): +__Example__ ```dart - Flame.util.addGestureRecognizer(new TapGestureRecognizer() - ..onTapDown = (TapDownDetails evt) => game.handleInput(evt.globalPosition.dx, evt.globalPosition.dy)); +class MyGame extends Game with TapDetector { + // Other methods ommited + + @override + void onTapDown(TapDownDetails details) { + print("Player tap down on ${details.globalPosition.dx} - ${details.globalPosition.dy}"); + } + + @override + void onTapUp(TapUpDetails details) { + print("Player tap up on ${details.globalPosition.dx} - ${details.globalPosition.dy}"); + } +} ``` -Where `game` is a reference to your game object and `handleInput` is a method you create to handle the input inside your game. - -If your game doesn't have other screens, just call this after your `runApp` call, in the `main` method. - [Complete Input Guide](doc/input.md) ## Credits diff --git a/doc/README.md b/doc/README.md index 766a4f669..877ee3c6c 100644 --- a/doc/README.md +++ b/doc/README.md @@ -31,7 +31,7 @@ Put the pub package as your dependency by dropping the following in your `pubspe ```yaml dependencies: - flame: ^0.16.1 + flame: ^0.17.0 ``` And start using it! diff --git a/doc/examples/gestures/.gitignore b/doc/examples/gestures/.gitignore new file mode 100644 index 000000000..ac4a90645 --- /dev/null +++ b/doc/examples/gestures/.gitignore @@ -0,0 +1,72 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +/build/ + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/doc/examples/gestures/.metadata b/doc/examples/gestures/.metadata new file mode 100644 index 000000000..a11a679fc --- /dev/null +++ b/doc/examples/gestures/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 841707365a9be08f2219cbafc52c52d6af5355aa + channel: master + +project_type: app diff --git a/doc/examples/gestures/README.md b/doc/examples/gestures/README.md new file mode 100644 index 000000000..58ae0bc8a --- /dev/null +++ b/doc/examples/gestures/README.md @@ -0,0 +1,3 @@ +# gestures + +A flame game showcasing the use of gestures callbacks diff --git a/doc/examples/gestures/lib/main.dart b/doc/examples/gestures/lib/main.dart new file mode 100644 index 000000000..e138355b0 --- /dev/null +++ b/doc/examples/gestures/lib/main.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flame/game.dart'; +import 'package:flame/gestures.dart'; + +void main() { + final game = MyGame(); + runApp(game.widget); +} + +class MyGame extends Game with TapDetector, DoubleTapDetector, PanDetector { + final _whitePaint = Paint()..color = const Color(0xFFFFFFFF); + final _bluePaint = Paint()..color = const Color(0xFF0000FF); + final _greenPaint = Paint()..color = const Color(0xFF00FF00); + + Paint _paint; + + Rect _rect = const Rect.fromLTWH(50, 50, 50, 50); + + MyGame() { + _paint = _whitePaint; + } + + @override + void onTap() { + _paint = _paint == _whitePaint ? _bluePaint : _whitePaint; + } + + @override + void onDoubleTap() { + _paint = _greenPaint; + } + + @override + void onPanUpdate(DragUpdateDetails details) { + _rect = _rect.translate(details.delta.dx, details.delta.dy); + } + + @override + void update(double dt) {} + + @override + void render(Canvas canvas) { + canvas.drawRect(_rect, _paint); + } +} diff --git a/doc/examples/gestures/pubspec.yaml b/doc/examples/gestures/pubspec.yaml new file mode 100644 index 000000000..5a74dc912 --- /dev/null +++ b/doc/examples/gestures/pubspec.yaml @@ -0,0 +1,16 @@ +name: gestures +description: A flame game showcasing the use of gestures callbacks + +version: 1.0.0+1 + +environment: + sdk: ">=2.1.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + flame: + path: ../../../ + +flutter: + uses-material-design: false diff --git a/doc/input.md b/doc/input.md index 8bd47d56d..2cacfb30b 100644 --- a/doc/input.md +++ b/doc/input.md @@ -1,41 +1,84 @@ # Input -In order to handle user input, you can use the libraries provided by Flutter for regular apps: [Gesture Recognizers](https://flutter.io/gestures/). - -However, in order to bind them, use the `Flame.util.addGestureRecognizer` method; in doing so, you'll make sure they are properly unbound when the game widget is not being rendered, and so the rest of your screens will work appropriately. - -For example, to add a tap listener ("on click"): +Inside `package:flame/gestures.dart` you can find a whole set of `mixin` which can be included on your game class instance to be able to receive touch input events. Bellow you can see the full list of these `mixin`s and its methods: ```dart - Flame.util.addGestureRecognizer(new TapGestureRecognizer() - ..onTapDown = (TapDownDetails evt) => game.handleInput(evt.globalPosition.dx, evt.globalPosition.dy)); +- TapDetector + - onTap + - onTapCancel + - onTapDown + - onTapUp + +- SecondaryTapDetector + - onSecondaryTapDown + - onSecondaryTapUp + - onSecondaryTapCancel + +- DoubleTapDetector + - onDoubleTap + +- LongPressDetector + - onLongPress + - onLongPressStart + - onLongPressMoveUpdate + - onLongPressUp + - onLongPressEnd + +- VerticalDragDetector + - onVerticalDragDown + - onVerticalDragStart + - onVerticalDragUpdate + - onVerticalDragEnd + - onVerticalDragCancel + +- HorizontalDragDetector + - onHorizontalDragDown + - onHorizontalDragStart + - onHorizontalDragUpdate + - onHorizontalDragEnd + - onHorizontalDragCancel + +- ForcePressDetector + - onForcePressStart + - onForcePressPeak + - onForcePressUpdate + - onForcePressEnd + +- PanDetector + - onPanDown + - onPanStart + - onPanUpdate + - onPanEnd + - onPanCancel + +- ScaleDetector + - onScaleStart + - onScaleUpdate + - onScaleEnd ``` -Where `game` is a reference to your game object and `handleInput` is a method you create to handle the input inside your game. +Many of these detectors can conflict with each other, for example, you can't register both Vertical and Horizontal drags, so not all of then can be used together. -If your game doesn't have other screens, just call this after your `runApp` call, in the `main` method. +All of these methods are basically a mirror from the callbacks available on the [GestureDetector widget](https://api.flutter.dev/flutter/widgets/GestureDetector-class.html), you can also read more about Flutter's gestures [here](https://api.flutter.dev/flutter/gestures/gestures-library.html). -Here are some example of more complex Gesture Recognizers: +## Example ```dart - MyGame() { - // other init... +class MyGame extends Game with TapDetector { + // Other methods ommited - Flame.util.addGestureRecognizer(createDragRecognizer()); - Flame.util.addGestureRecognizer(createTapRecognizer()); - } + @override + void onTapDown(TapDownDetails details) { + print("Player tap down on ${details.globalPosition.dx} - ${details.globalPosition.dy}"); + } - GestureRecognizer createDragRecognizer() { - return new ImmediateMultiDragGestureRecognizer() - ..onStart = (Offset position) => this.handleDrag(position); - } - - TapGestureRecognizer createTapRecognizer() { - return new TapGestureRecognizer() - ..onTapUp = (TapUpDetails details) => this.handleTap(details.globalPosition);; - } + @override + void onTapUp(TapUpDetails details) { + print("Player tap up on ${details.globalPosition.dx} - ${details.globalPosition.dy}"); + } +} ``` -__ATTENTION:__ `Flame.util.addGestureRecognizer` must be called after the `runApp`, otherwise Flutter's `GestureBinding` will not be initialized yet and exceptions will occur. +You can also check a more complete example [here](/doc/examples/gestures). ## Tapable components diff --git a/doc/util.md b/doc/util.md index 0be121789..addb53dee 100644 --- a/doc/util.md +++ b/doc/util.md @@ -55,11 +55,14 @@ Flame provides a simple utility class to help you handle countdowns and event li __Countdown example:__ ```dart -import 'package:flame/game.dart'; -import 'package:flame/time.dart'; -import 'package:flame/text_config.dart'; +import 'dart:ui'; -class MyGame extends MyGame { +import 'package:flame/game.dart'; +import 'package:flame/position.dart'; +import 'package:flame/text_config.dart'; +import 'package:flame/time.dart'; + +class MyGame extends Game { final TextConfig textConfig = TextConfig(color: const Color(0xFFFFFFFF)); final countdown = Timer(2); @@ -77,7 +80,8 @@ class MyGame extends MyGame { @override void render(Canvas canvas) { - textConfig.render(canvas, "Countdown: ${countdown.current.toString()}", Position(10, 100)); + textConfig.render(canvas, "Countdown: ${countdown.current.toString()}", + Position(10, 100)); } } @@ -86,11 +90,14 @@ class MyGame extends MyGame { __Interval example:__ ```dart -import 'package:flame/game.dart'; -import 'package:flame/time.dart'; -import 'package:flame/text_config.dart'; +import 'dart:ui'; -class MyGame extends MyGame { +import 'package:flame/game.dart'; +import 'package:flame/position.dart'; +import 'package:flame/text_config.dart'; +import 'package:flame/time.dart'; + +class MyGame extends Game { final TextConfig textConfig = TextConfig(color: const Color(0xFFFFFFFF)); Timer interval; @@ -105,6 +112,7 @@ class MyGame extends MyGame { @override void update(double dt) { + interval.update(dt); } @override diff --git a/lib/game.dart b/lib/game.dart index b27faf1b1..7ee28b628 100644 --- a/lib/game.dart +++ b/lib/game.dart @@ -12,31 +12,14 @@ import 'package:ordered_set/ordered_set.dart'; import 'components/component.dart'; import 'components/mixins/has_game_ref.dart'; import 'components/mixins/tapable.dart'; -import 'flame.dart'; import 'position.dart'; +import 'gestures.dart'; /// Represents a generic game. /// /// Subclass this to implement the [update] and [render] methods. /// Flame will deal with calling these methods properly when the game's widget is rendered. abstract class Game { - TapGestureRecognizer _createTapGestureRecognizer() => TapGestureRecognizer() - ..onTapUp = (TapUpDetails details) { - onTapUp(details); - } - ..onTapDown = (TapDownDetails details) { - onTapDown(details); - } - ..onTapCancel = () { - onTapCancel(); - }; - - void onTapCancel() {} - void onTapDown(TapDownDetails details) {} - void onTapUp(TapUpDetails details) {} - - TapGestureRecognizer _gestureRecognizer; - // Widget Builder for this Game final builder = WidgetBuilder(); @@ -67,26 +50,162 @@ abstract class Game { Widget get widget => builder.build(this); // Called when the Game widget is attached - void onAttach() { - if (_gestureRecognizer != null) { - Flame.util.removeGestureRecognizer(_gestureRecognizer); - } - _gestureRecognizer = _createTapGestureRecognizer(); - Flame.util.addGestureRecognizer(_gestureRecognizer); - } + void onAttach() {} // Called when the Game widget is detached - void onDetach() { - if (_gestureRecognizer != null) { - Flame.util.removeGestureRecognizer(_gestureRecognizer); - } - } + void onDetach() {} } class WidgetBuilder { Offset offset = Offset.zero; - Widget build(Game game) => Directionality( - textDirection: TextDirection.ltr, child: EmbeddedGameWidget(game)); + + Widget build(Game game) { + return GestureDetector( + // Taps + onTap: game is TapDetector ? () => (game as TapDetector).onTap() : null, + onTapCancel: game is TapDetector + ? () => (game as TapDetector).onTapCancel() + : null, + onTapDown: game is TapDetector + ? (TapDownDetails d) => (game as TapDetector).onTapDown(d) + : null, + onTapUp: game is TapDetector + ? (TapUpDetails d) => (game as TapDetector).onTapUp(d) + : null, + + // Secondary taps + onSecondaryTapDown: game is SecondaryTapDetector + ? (TapDownDetails d) => + (game as SecondaryTapDetector).onSecondaryTapDown(d) + : null, + onSecondaryTapUp: game is SecondaryTapDetector + ? (TapUpDetails d) => + (game as SecondaryTapDetector).onSecondaryTapUp(d) + : null, + onSecondaryTapCancel: game is SecondaryTapDetector + ? () => (game as SecondaryTapDetector).onSecondaryTapCancel() + : null, + + // Double tap + onDoubleTap: game is DoubleTapDetector + ? () => (game as DoubleTapDetector).onDoubleTap() + : null, + + // Long presses + onLongPress: game is LongPressDetector + ? () => (game as LongPressDetector).onLongPress() + : null, + onLongPressStart: game is LongPressDetector + ? (LongPressStartDetails d) => + (game as LongPressDetector).onLongPressStart(d) + : null, + onLongPressMoveUpdate: game is LongPressDetector + ? (LongPressMoveUpdateDetails d) => + (game as LongPressDetector).onLongPressMoveUpdate(d) + : null, + onLongPressUp: game is LongPressDetector + ? () => (game as LongPressDetector).onLongPressUp() + : null, + onLongPressEnd: game is LongPressDetector + ? (LongPressEndDetails d) => + (game as LongPressDetector).onLongPressEnd(d) + : null, + + // Vertical drag + onVerticalDragDown: game is VerticalDragDetector + ? (DragDownDetails d) => + (game as VerticalDragDetector).onVerticalDragDown(d) + : null, + onVerticalDragStart: game is VerticalDragDetector + ? (DragStartDetails d) => + (game as VerticalDragDetector).onVerticalDragStart(d) + : null, + onVerticalDragUpdate: game is VerticalDragDetector + ? (DragUpdateDetails d) => + (game as VerticalDragDetector).onVerticalDragUpdate(d) + : null, + onVerticalDragEnd: game is VerticalDragDetector + ? (DragEndDetails d) => + (game as VerticalDragDetector).onVerticalDragEnd(d) + : null, + onVerticalDragCancel: game is VerticalDragDetector + ? () => (game as VerticalDragDetector).onVerticalDragCancel() + : null, + + // Horizontal drag + onHorizontalDragDown: game is HorizontalDragDetector + ? (DragDownDetails d) => + (game as HorizontalDragDetector).onHorizontalDragDown(d) + : null, + onHorizontalDragStart: game is HorizontalDragDetector + ? (DragStartDetails d) => + (game as HorizontalDragDetector).onHorizontalDragStart(d) + : null, + onHorizontalDragUpdate: game is HorizontalDragDetector + ? (DragUpdateDetails d) => + (game as HorizontalDragDetector).onHorizontalDragUpdate(d) + : null, + onHorizontalDragEnd: game is HorizontalDragDetector + ? (DragEndDetails d) => + (game as HorizontalDragDetector).onHorizontalDragEnd(d) + : null, + onHorizontalDragCancel: game is HorizontalDragDetector + ? () => (game as HorizontalDragDetector).onHorizontalDragCancel() + : null, + + // Force presses + onForcePressStart: game is ForcePressDetector + ? (ForcePressDetails d) => + (game as ForcePressDetector).onForcePressStart(d) + : null, + onForcePressPeak: game is ForcePressDetector + ? (ForcePressDetails d) => + (game as ForcePressDetector).onForcePressPeak(d) + : null, + onForcePressUpdate: game is ForcePressDetector + ? (ForcePressDetails d) => + (game as ForcePressDetector).onForcePressUpdate(d) + : null, + onForcePressEnd: game is ForcePressDetector + ? (ForcePressDetails d) => + (game as ForcePressDetector).onForcePressEnd(d) + : null, + + // Pan + onPanDown: game is PanDetector + ? (DragDownDetails d) => (game as PanDetector).onPanDown(d) + : null, + onPanStart: game is PanDetector + ? (DragStartDetails d) => (game as PanDetector).onPanStart(d) + : null, + onPanUpdate: game is PanDetector + ? (DragUpdateDetails d) => (game as PanDetector).onPanUpdate(d) + : null, + onPanEnd: game is PanDetector + ? (DragEndDetails d) => (game as PanDetector).onPanEnd(d) + : null, + onPanCancel: game is PanDetector + ? () => (game as PanDetector).onPanCancel() + : null, + + // Scales + onScaleStart: game is ScaleDetector + ? (ScaleStartDetails d) => (game as ScaleDetector).onScaleStart(d) + : null, + onScaleUpdate: game is ScaleDetector + ? (ScaleUpdateDetails d) => (game as ScaleDetector).onScaleUpdate(d) + : null, + onScaleEnd: game is ScaleDetector + ? (ScaleEndDetails d) => (game as ScaleDetector).onScaleEnd(d) + : null, + + child: Container( + color: const Color(0xFF000000), + child: Directionality( + textDirection: TextDirection.ltr, + child: EmbeddedGameWidget(game))), + ); + } } /// This is a more complete and opinionated implementation of Game. @@ -94,7 +213,7 @@ class WidgetBuilder { /// It still needs to be subclasses to add your game logic, but the [update], [render] and [resize] methods have default implementations. /// This is the recommended structure to use for most games. /// It is based on the Component system. -abstract class BaseGame extends Game { +abstract class BaseGame extends Game with TapDetector { /// The list of components to be updated and rendered by the base game. OrderedSet components = OrderedSet(Comparing.on((c) => c.priority())); diff --git a/lib/gestures.dart b/lib/gestures.dart new file mode 100644 index 000000000..e2f3339da --- /dev/null +++ b/lib/gestures.dart @@ -0,0 +1,63 @@ +import 'package:flutter/gestures.dart'; + +mixin TapDetector { + void onTap() {} + void onTapCancel() {} + void onTapDown(TapDownDetails details) {} + void onTapUp(TapUpDetails details) {} +} + +mixin SecondaryTapDetector { + void onSecondaryTapDown(TapDownDetails details) {} + void onSecondaryTapUp(TapUpDetails details) {} + void onSecondaryTapCancel() {} +} + +mixin DoubleTapDetector { + void onDoubleTap() {} +} + +mixin LongPressDetector { + void onLongPress() {} + void onLongPressStart(LongPressStartDetails details) {} + void onLongPressMoveUpdate(LongPressMoveUpdateDetails details) {} + void onLongPressUp() {} + void onLongPressEnd(LongPressEndDetails details) {} +} + +mixin VerticalDragDetector { + void onVerticalDragDown(DragDownDetails details) {} + void onVerticalDragStart(DragStartDetails details) {} + void onVerticalDragUpdate(DragUpdateDetails details) {} + void onVerticalDragEnd(DragEndDetails details) {} + void onVerticalDragCancel() {} +} + +mixin HorizontalDragDetector { + void onHorizontalDragDown(DragDownDetails details) {} + void onHorizontalDragStart(DragStartDetails details) {} + void onHorizontalDragUpdate(DragUpdateDetails details) {} + void onHorizontalDragEnd(DragEndDetails details) {} + void onHorizontalDragCancel() {} +} + +mixin ForcePressDetector { + void onForcePressStart(ForcePressDetails details) {} + void onForcePressPeak(ForcePressDetails details) {} + void onForcePressUpdate(ForcePressDetails details) {} + void onForcePressEnd(ForcePressDetails details) {} +} + +mixin PanDetector { + void onPanDown(DragDownDetails details) {} + void onPanStart(DragStartDetails details) {} + void onPanUpdate(DragUpdateDetails details) {} + void onPanEnd(DragEndDetails details) {} + void onPanCancel() {} +} + +mixin ScaleDetector { + void onScaleStart(ScaleStartDetails details) {} + void onScaleUpdate(ScaleUpdateDetails details) {} + void onScaleEnd(ScaleEndDetails details) {} +} diff --git a/lib/util.dart b/lib/util.dart index 12a7ae30a..f8f874337 100644 --- a/lib/util.dart +++ b/lib/util.dart @@ -113,6 +113,10 @@ class Util { /// This properly binds a gesture recognizer to your game. /// /// Use this in order to get it to work in case your app also contains other widgets. + /// + /// Read more at: https://flame-engine.org/docs/input.md + /// + /// @Deprecated('This method can lead to confuse behaviour, use the gestures methods provided by the Game class') void addGestureRecognizer(GestureRecognizer recognizer) { if (GestureBinding.instance == null) { throw Exception( diff --git a/pubspec.yaml b/pubspec.yaml index 797d2bed9..43bad566b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flame description: A minimalist Flutter game engine, provides a nice set of somewhat independent modules you can choose from. -version: 0.16.1 +version: 0.17.0 author: Luan Nico homepage: https://github.com/flame-engine/flame