James Williams
LinkedInMastodonGithub

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.

Diagram showing the relationship between various Flame components

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.

A spiral drawn using line segments

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 ShapeComponents, 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.

Rectangular flower

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.