Merge branch 'develop' into feature/add-joystick-component

This commit is contained in:
Erick
2020-06-29 16:26:51 -03:00
committed by GitHub
27 changed files with 500 additions and 20 deletions

View File

@ -4,6 +4,9 @@
- Add Joystick Component - Add Joystick Component
- Adding BaseGame#markToRemove - Adding BaseGame#markToRemove
- Upgrade tiled and flutter_svg dependencies - Upgrade tiled and flutter_svg dependencies
- onComplete callback for effects
- Adding Layers
- Update tiled dep to 0.5.0 and add support for rotation with improved api
## 0.22.1 ## 0.22.1
- Fix Box2DComponent render priority - Fix Box2DComponent render priority

View File

@ -42,6 +42,7 @@ Check out the [awesome flame repository](https://github.com/flame-engine/awesome
- [Text Rendering](text.md) - [Text Rendering](text.md)
- [Colors and the Palette](palette.md) - [Colors and the Palette](palette.md)
- [Particles](particles.md) - [Particles](particles.md)
- [Layers](layers.md)
* Other Modules * Other Modules
- [Util](util.md) - [Util](util.md)
- [Gamepad](gamepad.md) - [Gamepad](gamepad.md)

View File

@ -14,6 +14,8 @@ If `isInfinite` is false and `isAlternating` is true the effect will go from the
`isInfinite` and `isAlternating` are false by default and then the effect is just applied once, from the beginning of the curve until the end. `isInfinite` and `isAlternating` are false by default and then the effect is just applied once, from the beginning of the curve until the end.
When an effect is completed the callback `onComplete` will be called, it can be set as an optional argument to your effect.
## MoveEffect ## MoveEffect
Applied to `PositionComponent`s, this effect can be used to move the component to a new position, using an [animation curve](https://api.flutter.dev/flutter/animation/Curves-class.html). Applied to `PositionComponent`s, this effect can be used to move the component to a new position, using an [animation curve](https://api.flutter.dev/flutter/animation/Curves-class.html).

View File

@ -65,6 +65,7 @@ class MyGame extends BaseGame with TapDetector {
isInfinite: false, isInfinite: false,
isAlternating: true, isAlternating: true,
offset: 0.5, offset: 0.5,
onComplete: () => print("onComplete callback"),
); );
greenSquare.addEffect(combination); greenSquare.addEffect(combination);
} }

49
doc/examples/layers/.gitignore vendored Normal file
View File

@ -0,0 +1,49 @@
# 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/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Exceptions to above rules.
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
android
ios
macos
web

View File

@ -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: 9d58a87066d1da98eb06826918e4b90cc76ae0ef
channel: master
project_type: app

View File

@ -0,0 +1,3 @@
# layers
Simple project to showcase the layer feature of Flame

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

View File

@ -0,0 +1,82 @@
import 'package:flutter/material.dart' hide Animation;
import 'package:flame/game.dart';
import 'package:flame/sprite.dart';
import 'package:flame/layer/layer.dart';
import 'package:flame/flame.dart';
import 'dart:ui';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Flame.util.fullScreen();
final playerSprite = await Sprite.loadSprite('player.png');
final enemySprite = await Sprite.loadSprite('enemy.png');
final backgroundSprite = await Sprite.loadSprite('background.png');
runApp(LayerGame(playerSprite, enemySprite, backgroundSprite).widget);
}
class GameLayer extends DynamicLayer {
final Sprite playerSprite;
final Sprite enemySprite;
GameLayer(this.playerSprite, this.enemySprite) {
preProcessors.add(ShadowProcessor());
}
@override
void drawLayer() {
playerSprite.renderRect(
canvas,
const Rect.fromLTWH(50, 50, 150, 150),
);
enemySprite.renderRect(
canvas,
const Rect.fromLTWH(250, 150, 100, 50),
);
}
}
class BackgroundLayer extends PreRenderedLayer {
final Sprite sprite;
BackgroundLayer(this.sprite) {
preProcessors.add(ShadowProcessor());
}
@override
void drawLayer() {
sprite.renderRect(
canvas,
const Rect.fromLTWH(50, 200, 300, 150),
);
}
}
class LayerGame extends Game {
Sprite playerSprite;
Sprite enemySprite;
Sprite backgroundSprite;
Layer gameLayer;
Layer backgroundLayer;
LayerGame(this.playerSprite, this.enemySprite, this.backgroundSprite) {
gameLayer = GameLayer(playerSprite, enemySprite);
backgroundLayer = BackgroundLayer(backgroundSprite);
}
@override
void update(double dt) {}
@override
void render(Canvas canvas) {
gameLayer.render(canvas);
backgroundLayer.render(canvas);
}
@override
Color backgroundColor() => const Color(0xFF38607C);
}

View File

@ -0,0 +1,28 @@
name: layers
description: Simple project to showcase the layer feature of Flame
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
flame:
path: ../../../
cupertino_icons: ^0.1.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
assets:
- assets/images/background.png
- assets/images/enemy.png
- assets/images/player.png

File diff suppressed because one or more lines are too long

View File

@ -15,7 +15,7 @@ void main() {
class TiledGame extends BaseGame { class TiledGame extends BaseGame {
TiledGame() { TiledGame() {
final TiledComponent tiledMap = TiledComponent('map.tmx'); final TiledComponent tiledMap = TiledComponent('map.tmx', 16.0);
add(tiledMap); add(tiledMap);
_addCoinsInMap(tiledMap); _addCoinsInMap(tiledMap);
} }
@ -30,8 +30,12 @@ class TiledGame extends BaseGame {
final comp = AnimationComponent( final comp = AnimationComponent(
20.0, 20.0,
20.0, 20.0,
Animation.sequenced('coins.png', 8, Animation.sequenced(
textureWidth: 20, textureHeight: 20), 'coins.png',
8,
textureWidth: 20,
textureHeight: 20,
),
); );
comp.x = obj.x.toDouble(); comp.x = obj.x.toDouble();
comp.y = obj.y.toDouble(); comp.y = obj.y.toDouble();

92
doc/layers.md Normal file
View File

@ -0,0 +1,92 @@
# Layers
Layers are an useful feature that lets you group renderization by context, as well as allow you yo pre-render things. That enables, for example, the renderization in memory of parts of your game that don't change much, like a background, and by doing that, freeing resources for more dynamic content that needs to be rendered every loop cycle.
There are two types of layers on Flame: `DynamicLayer` (for things that are moving or changing) and `PreRenderedLayer` (for things that are static).
## DynamicLayer
Dynamic layers are layers that are rendered every time that they are draw on the canvas. As the name suggests, it is meant for dynamic content and is most useful to group renderizations that are of the same context.
Usage example:
```dart
class GameLayer extends DynamicLayer {
final MyGame game;
GameLayer(this.game);
@override
void drawLayer() {
game.playerSprite.renderRect(
canvas,
game.playerRect,
);
game.enemySprite.renderRect(
canvas,
game.enemyRect,
);
}
}
class MyGame extends Game {
// Other methods ommited...
@override
void render(Canvas canvas) {
gameLayer.render(canvas); // x and y can be provided as optional position arguments
}
}
```
## PreRenderedLayer
Pre-rendered layers are layers that are rendered only once, cached in memory and then just replicated on the game canvas afterwards. They are most useful to cache content that don't change during the game, like a background for example.
Usage example:
```dart
class BackgroundLayer extends PreRenderedLayer {
final Sprite sprite;
BackgroundLayer(this.sprite);
@override
void drawLayer() {
sprite.renderRect(
canvas,
const Rect.fromLTWH(50, 200, 300, 150),
);
}
}
class MyGame extends Game {
// Other methods ommited...
@override
void render(Canvas canvas) {
backgroundLayer.render(canvas); // x and y can be provided as optional position arguments
}
}
```
## Layer Processors
Flame also provides a way to add processors on your layer, which are ways to add effects on the entire layer. At the moment, out of the box, only the `ShadowProcessor` is available, this processor renders a cool back drop shadow on your layer.
To add processors to your layer, just add them to the layer `preProcessors` or `postProcessors` list. For example:
```dart
// Works the same for both DynamicLayer and PreRenderedLayer
class BackgroundLayer extends PreRenderedLayer {
final Sprite sprite;
BackgroundLayer(this.sprite) {
preProcessors.add(ShadowProcessor());
}
@override
void drawLayer() { /* ommited */ }
```
Custom processors can be creted by extending the `LayerProcessor` class.
You can check an working example of layers [here](/doc/examples/layers).

View File

@ -1,7 +1,11 @@
# Tiled # Tiled
[Tiled](https://www.mapeditor.org/) is an awesome tool to design levels and maps. Flame bundles a [dart package](https://pub.dev/packages/tiled) that parses tmx files and implements a very simple component for the map rendering. [Tiled](https://www.mapeditor.org/) is an awesome tool to design levels and maps.
Right now Tiled support on Flame is quite simple and it only renders the map on the screen, other advanced features are not yet supported. Flame bundles a [dart package](https://pub.dev/packages/tiled) that allows you to parse tmx (xml) files and access the tiles, objects and everything in there.
Flame also provides a simple TiledComponent for the map rendering, which renders the tiles on the screen and supports rotations and flips.
Other advanced features are not yet supported but you can easily read the objects and other features of the tmx and add custom behaviour (eg regions for triggers and walking areas, custom animated objects).
You can check a working example [here](/doc/examples/tiled). You can check a working example [here](/doc/examples/tiled).

View File

@ -1,3 +1,4 @@
import 'dart:math' as math;
import 'dart:async'; import 'dart:async';
import 'dart:ui'; import 'dart:ui';
@ -6,6 +7,68 @@ import 'package:flame/flame.dart';
import 'package:flutter/material.dart' show Colors; import 'package:flutter/material.dart' show Colors;
import 'package:tiled/tiled.dart' hide Image; import 'package:tiled/tiled.dart' hide Image;
/// Tiled represents all flips and rotation using three possible flips: horizontal, vertical and diagonal.
/// This class converts that representation to a simpler one, that uses one angle (with pi/2 steps) and two flips (H or V).
/// More reference: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#tile-flipping
class _SimpleFlips {
/// The angle (in steps of pi/2 rads), clockwise, around the center of the tile.
final int angle;
/// Whether to flip across a central vertical axis (passing through the center).
final bool flipH;
/// Whether to flip across a central horizontal axis (passing through the center).
final bool flipV;
_SimpleFlips(this.angle, this.flipH, this.flipV);
/// This is the conversion from the truth table that I drew.
factory _SimpleFlips.fromFlips(Flips flips) {
int angle;
bool flipV, flipH;
if (!flips.diagonally && !flips.vertically && !flips.horizontally) {
angle = 0;
flipV = false;
flipH = false;
} else if (!flips.diagonally && !flips.vertically && flips.horizontally) {
angle = 0;
flipV = false;
flipH = true;
} else if (!flips.diagonally && flips.vertically && !flips.horizontally) {
angle = 0;
flipV = true;
flipH = false;
} else if (!flips.diagonally && flips.vertically && flips.horizontally) {
angle = 2;
flipV = false;
flipH = false;
} else if (flips.diagonally && !flips.vertically && !flips.horizontally) {
angle = 1;
flipV = false;
flipH = true;
} else if (flips.diagonally && !flips.vertically && flips.horizontally) {
angle = 1;
flipV = false;
flipH = false;
} else if (flips.diagonally && flips.vertically && !flips.horizontally) {
angle = 3;
flipV = false;
flipH = false;
} else if (flips.diagonally && flips.vertically && flips.horizontally) {
angle = 1;
flipV = true;
flipH = false;
} else {
// this should be exhaustive
throw 'Invalid combination of booleans: $flips';
}
return _SimpleFlips(angle, flipH, flipV);
}
}
/// This component renders a tile map based on a TMX file from Tiled.
class TiledComponent extends Component { class TiledComponent extends Component {
String filename; String filename;
TileMap map; TileMap map;
@ -13,10 +76,13 @@ class TiledComponent extends Component {
Map<String, Image> images = <String, Image>{}; Map<String, Image> images = <String, Image>{};
Future future; Future future;
bool _loaded = false; bool _loaded = false;
double destTileSize;
static Paint paint = Paint()..color = Colors.white; static Paint paint = Paint()..color = Colors.white;
TiledComponent(this.filename) { /// Creates this TiledComponent with the filename (for the tmx file resource)
/// and destTileSize is the tile size to be rendered (not the tile size in the texture, that one is configured inside Tiled).
TiledComponent(this.filename, this.destTileSize) {
future = _load(); future = _load();
} }
@ -70,12 +136,28 @@ class TiledComponent extends Component {
final image = images[tile.image.source]; final image = images[tile.image.source];
final rect = tile.computeDrawRect(); final rect = tile.computeDrawRect();
final src = Rect.fromLTWH(rect.left.toDouble(), rect.top.toDouble(), final src = Rect.fromLTWH(
rect.width.toDouble(), rect.height.toDouble()); rect.left.toDouble(),
final dst = Rect.fromLTWH(tile.x.toDouble(), tile.y.toDouble(), rect.top.toDouble(),
rect.width.toDouble(), rect.height.toDouble()); rect.width.toDouble(),
rect.height.toDouble(),
);
final dst = Rect.fromLTWH(
tile.x * destTileSize,
tile.y * destTileSize,
destTileSize,
destTileSize,
);
final flips = _SimpleFlips.fromFlips(tile.flips);
c.save();
c.translate(dst.center.dx, dst.center.dy);
c.rotate(flips.angle * math.pi / 2);
c.scale(flips.flipV ? -1.0 : 1.0, flips.flipH ? -1.0 : 1.0);
c.translate(-dst.center.dx, -dst.center.dy);
c.drawImageRect(image, src, dst, paint); c.drawImageRect(image, src, dst, paint);
c.restore();
}); });
}); });
} }
@ -83,6 +165,8 @@ class TiledComponent extends Component {
@override @override
void update(double t) {} void update(double t) {}
/// This returns an object group fetch by name from a given layer.
/// Use this to add custom behaviour to special objects and groups.
Future<ObjectGroup> getObjectGroupFromLayer(String name) { Future<ObjectGroup> getObjectGroupFromLayer(String name) {
return future.then((onValue) { return future.then((onValue) {
return map.objectGroups return map.objectGroups

View File

@ -14,7 +14,8 @@ class CombinedEffect extends PositionComponentEffect {
this.offset = 0.0, this.offset = 0.0,
bool isInfinite = false, bool isInfinite = false,
bool isAlternating = false, bool isAlternating = false,
}) : super(isInfinite, isAlternating) { Function onComplete,
}) : super(isInfinite, isAlternating, onComplete: onComplete) {
final types = effects.map((e) => e.runtimeType); final types = effects.map((e) => e.runtimeType);
assert( assert(
types.toSet().length == types.length, types.toSet().length == types.length,

View File

@ -12,6 +12,7 @@ export './sequence_effect.dart';
abstract class PositionComponentEffect { abstract class PositionComponentEffect {
PositionComponent component; PositionComponent component;
Function() onComplete;
bool _isDisposed = false; bool _isDisposed = false;
bool get isDisposed => _isDisposed; bool get isDisposed => _isDisposed;
@ -37,7 +38,11 @@ abstract class PositionComponentEffect {
/// travel time /// travel time
double get totalTravelTime => travelTime * (isAlternating ? 2 : 1); double get totalTravelTime => travelTime * (isAlternating ? 2 : 1);
PositionComponentEffect(this._initialIsInfinite, this._initialIsAlternating) { PositionComponentEffect(
this._initialIsInfinite,
this._initialIsAlternating, {
this.onComplete,
}) {
isInfinite = _initialIsInfinite; isInfinite = _initialIsInfinite;
isAlternating = _initialIsAlternating; isAlternating = _initialIsAlternating;
} }
@ -53,6 +58,9 @@ abstract class PositionComponentEffect {
if (!hasFinished()) { if (!hasFinished()) {
currentTime += dt * curveDirection + driftTime * driftMultiplier; currentTime += dt * curveDirection + driftTime * driftMultiplier;
percentage = min(1.0, max(0.0, currentTime / travelTime)); percentage = min(1.0, max(0.0, currentTime / travelTime));
if (hasFinished()) {
onComplete?.call();
}
} }
} }

View File

@ -28,7 +28,8 @@ class MoveEffect extends PositionComponentEffect {
this.curve, this.curve,
isInfinite = false, isInfinite = false,
isAlternating = false, isAlternating = false,
}) : super(isInfinite, isAlternating); Function onComplete,
}) : super(isInfinite, isAlternating, onComplete: onComplete);
@override @override
void initialize(_comp) { void initialize(_comp) {

View File

@ -18,7 +18,8 @@ class RotateEffect extends PositionComponentEffect {
this.curve, this.curve,
isInfinite = false, isInfinite = false,
isAlternating = false, isAlternating = false,
}) : super(isInfinite, isAlternating); Function onComplete,
}) : super(isInfinite, isAlternating, onComplete: onComplete);
@override @override
void initialize(_comp) { void initialize(_comp) {

View File

@ -25,7 +25,8 @@ class ScaleEffect extends PositionComponentEffect {
this.curve, this.curve,
isInfinite = false, isInfinite = false,
isAlternating = false, isAlternating = false,
}) : super(isInfinite, isAlternating); Function onComplete,
}) : super(isInfinite, isAlternating, onComplete: onComplete);
@override @override
void initialize(_comp) { void initialize(_comp) {

View File

@ -14,7 +14,8 @@ class SequenceEffect extends PositionComponentEffect {
@required this.effects, @required this.effects,
isInfinite = false, isInfinite = false,
isAlternating = false, isAlternating = false,
}) : super(isInfinite, isAlternating) { Function onComplete,
}) : super(isInfinite, isAlternating, onComplete: onComplete) {
assert( assert(
effects.every((effect) => effect.component == null), effects.every((effect) => effect.component == null),
"No effects can be added to components from the start", "No effects can be added to components from the start",

75
lib/layer/layer.dart Normal file
View File

@ -0,0 +1,75 @@
import 'package:meta/meta.dart';
import 'dart:ui';
import './processors.dart';
export './processors.dart';
abstract class Layer {
List<LayerProcessor> preProcessors = [];
List<LayerProcessor> postProcessors = [];
Picture _picture;
PictureRecorder _recorder;
Canvas _canvas;
@mustCallSuper
void render(Canvas canvas, {double x = 0.0, double y = 0.0}) {
if (_picture == null) {
return;
}
canvas.save();
canvas.translate(x, y);
preProcessors.forEach((p) => p.process(_picture, canvas));
canvas.drawPicture(_picture);
postProcessors.forEach((p) => p.process(_picture, canvas));
canvas.restore();
}
Canvas get canvas {
assert(
_canvas != null,
'Layer is not ready for rendering, call beginRendering first',
);
return _canvas;
}
void beginRendering() {
_recorder = PictureRecorder();
_canvas = Canvas(_recorder);
}
void finishRendering() {
_picture = _recorder.endRecording();
_recorder = null;
_canvas = null;
}
void drawLayer();
}
abstract class PreRenderedLayer extends Layer {
PreRenderedLayer() {
reRender();
}
void reRender() {
beginRendering();
drawLayer();
finishRendering();
}
}
abstract class DynamicLayer extends Layer {
@override
void render(Canvas canvas, {double x = 0.0, double y = 0.0}) {
beginRendering();
drawLayer();
finishRendering();
super.render(canvas, x: x, y: y);
}
}

29
lib/layer/processors.dart Normal file
View File

@ -0,0 +1,29 @@
import 'dart:ui';
abstract class LayerProcessor {
void process(Picture pic, Canvas canvas);
}
class ShadowProcessor extends LayerProcessor {
Paint _shadowPaint;
final Offset offset;
ShadowProcessor({
this.offset = const Offset(10, 10),
double opacity = 0.9,
Color color = const Color(0xFF000000),
}) {
_shadowPaint = Paint()
..colorFilter =
ColorFilter.mode(color.withOpacity(opacity), BlendMode.srcATop);
}
@override
void process(Picture pic, Canvas canvas) {
canvas.saveLayer(Rect.largest, _shadowPaint);
canvas.translate(offset.dx, offset.dy);
canvas.drawPicture(pic);
canvas.restore();
}
}

View File

@ -11,7 +11,7 @@ dependencies:
path_provider: ^1.6.0 path_provider: ^1.6.0
box2d_flame: ^0.4.6 box2d_flame: ^0.4.6
synchronized: ^2.1.0 synchronized: ^2.1.0
tiled: ^0.4.0 tiled: ^0.5.0
convert: ^2.0.1 convert: ^2.0.1
flutter_svg: ^0.18.0 flutter_svg: ^0.18.0
flare_flutter: ^2.0.1 flare_flutter: ^2.0.1

View File

@ -10,7 +10,7 @@ import 'package:test/test.dart';
void main() { void main() {
test('my first widget test', () async { test('my first widget test', () async {
await Flame.init(bundle: TestAssetBundle()); await Flame.init(bundle: TestAssetBundle());
final tiled = TiledComponent('x'); final tiled = TiledComponent('x', 16);
await tiled.future; await tiled.future;
expect(1, equals(1)); expect(1, equals(1));
}); });