Creating New Components with Flame
In the last post, we worked through drawing and styling a couple of basic components in Flame. In this post, we'll create some new ShapeComponents and composite components.
How ShapeComponents are derived
The components we've used so far, RectangleComponent
, CircleComponent
, TextComponent
all descend from PositionComponent
allowing them to be moved around the screen, scaled, or rotated. CircleComponent
and RectangleComponent
inherit the ability to represent a geometric shape with a size and rotation angle from ShapeComponent
.
Two of the shapes that were missing when I started working with the library are lines and arcs.
A ShapeComponent must implement two functions: render(Canvas canvas)
that draws your shape and containsPoint(Vector2 p)
that will determine if a given point is within the shape's hitbox.
The Canvas is from the dart:ui
package in core Dart. There are a lot of methods that aren't exposed directly to Flame.
Creating an Arc
There's already a function for drawing an arc in Flutter Canvas `drawArc`` that we can use encapsulate into a new ShapeComponent.
import 'dart:math';
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
class Arc extends ShapeComponent {
Vector2 pos;
double radius = 5.0;
double startAngle = 0.0;
/* Start angle begins at right center
and moves counterclockwise
+----270---+
| |
180 0
| |
+----90----+
*/
double sweepAngle = 360.0;
bool isClosed = true;
Paint paint = Paint()..color = Colors.green;
Arc({required this.pos, required this.radius, required this.startAngle, required this.sweepAngle, required this.paint}) : super(
position: pos,
) {
add(CircleHitbox(position:this.pos, radius:this.radius));
}
@override
bool containsPoint(Vector2 p) {
// TODO: implement containsPoint
throw UnimplementedError();
}
@override
void render(Canvas canvas) {
Rect r = Rect.fromLTWH(pos.x, pos.y, radius, radius);
canvas.drawArc(r, startAngle * pi/180, sweepAngle * pi/180, isClosed, paint);
}
}
The Arc class acts mostly as a wrapper for the properties needed for canvas.drawArc
. I'm not doing any collision detection so the implementation of containsPoint
is empty.
After creation, we can use the Arc like any other ShapeComponent.
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'arc.dart';
class CirclesAndArcs extends FlameGame {
final strokePaint = Paint()
..color = Colors.red
..style = PaintingStyle.stroke;
@override
Future onLoad() async {
add(
CircleComponent(radius: 8.0, position: Vector2.all(100),
paint:Paint()..color = Colors.white));
var arc = Arc(pos:Vector2(150,100), radius: 50, startAngle:270.0, sweepAngle:270.0, paint:Paint()..color = Colors.green);
add(arc);
var arc2 = Arc(pos:Vector2(200,100), radius: 50, startAngle:0.0, sweepAngle:90.0, paint:strokePaint);
arc2.isClosed = false;
add(arc2);
}
}
Creating Lines
If I needed to draw a single line, RectangleComponent
would work just fine. For a component that joins every new segment to the end of the last segment, you need a new ShapeComponent.
The implementation of a Line and Polyline are similar to that of an arc, just enough to wrap the Canvas methods. LineShape draws a single line and Polyline draws a continuous line using a list of Vector2 vertices using each successive point to draw a line from the previous point.
import 'package:flame/src/game/notifying_vector2.dart';
import 'package:flame/components.dart';
import 'package:flame/geometry.dart';
import 'package:flutter/material.dart';
class LineShape extends ShapeComponent {
LineSegment segment;
NotifyingVector2 _position;
double angle = 0.0;
final DEFAULT_LINE = LineSegment(Vector2(0.0, 0.0), Vector2(1.0, 0.0));
LineShape(this.segment, this._position, this.angle)
: super(angle: angle, position: _position, size: Vector2.all(1));
@override
bool containsPoint(Vector2 point) {
return false;
}
@override
void render(Canvas canvas) {
canvas.drawLine(segment.from.toOffset(), segment.to.toOffset(), paint);
}
}
class Polyline extends ShapeComponent {
var points = [];
Paint paint = Paint()..color = Colors.red;
Polyline(this.points, this.paint);
@override
bool containsPoint(Vector2 p) {
return false;
}
@override
void render(Canvas canvas) {
for (int i = 0; i < points.length - 2; i++) {
canvas.drawLine(points[i].toOffset(), points[i + 1].toOffset(), paint);
}
}
}
import 'package:flame/game.dart';
import 'package:flame/palette.dart';
import 'package:ud405_flame/stories/new_shape_components/line.dart';
class DrawASpiral extends FlameGame {
final COILS = 20;
@override
Future onLoad() async {
final green = BasicPalette.green.paint();
final screenWidth = canvasSize.x;
final screenHeight = canvasSize.y;
final xStep = screenWidth / 2 / COILS;
final yStep = screenHeight / 2 / COILS;
var points = [];
for (int i = 0; i < COILS; i++) {
final xOffset = xStep * i;
final yOffset = yStep * i;
var point1 = Vector2((xOffset - xStep), yOffset);
var point2 = Vector2((screenWidth - xOffset), yOffset);
var point3 = Vector2((screenWidth - xOffset), (screenHeight - yOffset));
var point4 = Vector2(xOffset, (screenHeight - yOffset));
var point5 = Vector2(xOffset, (yOffset + yStep));
points.addAll([point1, point2, point3, point4, point5]);
}
add(Polyline(points, green)); //, paint:green));
}
}
Creating a composite Component
Next, we will create a reusable composite component composed of several ShapeComponent
s, a flower.
Our flower consists of a number of rectangles: - a long green rectangle for the stem, - two rotated green rectangles for the leaves, and, - set of rotated rectangles acting as petals.
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
class RectangularFlower extends FlameGame {
final gradientPaint = Paint()
..shader = ui.Gradient.sweep(
Offset(0, 0),
[
Colors.yellow.withOpacity(0.8),
Colors.purple.withOpacity(0.4),
Colors.deepPurple.withOpacity(0.9),
],
[
0.0,
0.3,
1.0,
],
);
@override
Future onLoad() async {
add(RectFlower(Vector2(100,0.0), Paint()..color = Colors.yellow));
add(RectFlower(Vector2(300, 0), gradientPaint));
}
}
class RectFlower extends PositionComponent {
var green = Paint()..color = Colors.green;
var darkGreen = Paint()..color = const Color(0xDD00CC32);
var white = Paint()..color = Colors.white;
var yellow = Paint()..color = const Color(0xFFEBE834);
RectFlower(Vector2 position, Paint petalPaint){
// Draw stem
var stem = RectangleComponent(position: Vector2(48 + position.x, 100 + position.y), size: Vector2(10, 200), paint: green);
add(stem);
// Draw leaves
var leaf1 = RectangleComponent(position: Vector2(40 + position.x , 180 + position.y), size: Vector2(40, 40), angle: 45, paint:darkGreen);
var leaf2 = RectangleComponent(position: Vector2(85 + position.x, 230 + position.y), size: Vector2(40, 40), angle: 45, paint: darkGreen);
add(leaf1);
add(leaf2);
// Draw petals
const PETALS_COUNT = 20;
for (int i = 0; i < PETALS_COUNT; i++) {
var petal = RectangleComponent(position: Vector2(50 + position.x, 100 + position.y),
size: Vector2(40, 40), angle: 360 * i/PETALS_COUNT, paint:petalPaint);
add(petal);
}
}
}
RectFlower
bases the locations of its child components rather arbitrarily from the top left corner. In its current state, the component isn't interactive and won't be able to respond to any bounds checking because the size defaults to zero.
In the game class, we can instantiate a couple of flowers and give their petals each a different paint.
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
class RectangularFlower extends FlameGame {
final gradientPaint = Paint()
..shader = ui.Gradient.sweep(
Offset(0, 0),
[
Colors.yellow.withOpacity(0.8),
Colors.purple.withOpacity(0.4),
Colors.deepPurple.withOpacity(0.9),
],
[
0.0,
0.3,
1.0,
],
);
@override
Future onLoad() async {
add(RectFlower(Vector2(100,0.0), Paint()..color = Colors.yellow));
add(RectFlower(Vector2(300, 0), gradientPaint));
}
}
class RectFlower extends PositionComponent {
var green = Paint()..color = Colors.green;
var darkGreen = Paint()..color = const Color(0xDD00CC32);
var white = Paint()..color = Colors.white;
var yellow = Paint()..color = const Color(0xFFEBE834);
RectFlower(Vector2 position, Paint petalPaint){
// Draw stem
var stem = RectangleComponent(position: Vector2(48 + position.x, 100 + position.y), size: Vector2(10, 200), paint: green);
add(stem);
// Draw leaves
var leaf1 = RectangleComponent(position: Vector2(40 + position.x , 180 + position.y), size: Vector2(40, 40), angle: 45, paint:darkGreen);
var leaf2 = RectangleComponent(position: Vector2(85 + position.x, 230 + position.y), size: Vector2(40, 40), angle: 45, paint: darkGreen);
add(leaf1);
add(leaf2);
// Draw petals
const PETALS_COUNT = 20;
for (int i = 0; i < PETALS_COUNT; i++) {
var petal = RectangleComponent(position: Vector2(50 + position.x, 100 + position.y),
size: Vector2(40, 40), angle: 360 * i/PETALS_COUNT, paint:petalPaint);
add(petal);
}
}
}
Next time, we'll create some fractals.