Color Mixer Game – Made with Flame Game Engine

Color Mixer is a minimalist game made with Flame engine. It is a simple game template developed to try some features of the under development game engine.

How to play:

  • Tap 2 color cards in the bottom to mix them into a new color (average of r,g, and b values).
  • Player becomes the same color of the mixed one.
  • If the player’s color is the same as a line, it flies toward the block with the same color.
  • The game is over if the player hits a block with different color or a block marked as DANGER.

Download source code

Folder Structure

Preview snippet of game.dart, background.dart, and player.dart below. The rest can be found on Github.

game.dart

import 'dart:ui';

import 'package:color_mixer/components/actors/card.dart';
import 'package:color_mixer/components/actors/obstacle.dart';
import 'package:color_mixer/components/actors/player.dart';
import 'package:color_mixer/components/background.dart';
import 'package:color_mixer/components/level_info.dart';
import 'package:color_mixer/utils/color_helper.dart';
import 'package:color_mixer/utils/sound_helper.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart' show Colors;

class GameManager extends BaseGame with HasTapableComponents {
  Size screenSize;
  double halfWidth;
  double halfHeight;

  List<ColorCard> cards = [];
  double cardSize = 60;
  int cardPerRow = 2;
  double spacer = 20.0;
  double marginX = 50.0; //left and right margin

  double playerSize = 20;
  double playerSpeed = 24;

  List<Color> objectives = [];

  //game logic variables: color card index in card list
  int level = 1;
  List<int> selectedIndexes = [];


  //Player & enemies
  Player player;
  Obstacles obstacles;
  bool gameOver = false;

  @override
  Future<void> onLoad() {
    print('Game Start');
    _calculateScreenSize();
    SoundHelper.bgmStop();
    SoundHelper.bgm();

    //game background
    add(Background());
    add(PlayerBackground());

    //add card list to a grid
    _addColorCards();
    calculateObjectives();

    //player
    var pX = (screenSize.width / 2 - playerSize).floorToDouble();
    var pY = (screenSize.height / 2 - playerSize).floorToDouble() + 50;
    player = Player(Vector2(pX, pY), Vector2.all(playerSize), Colors.blue[400]);
    add(player);

    player.addEffect(MoveEffect(
        path: [
          Vector2(player.x, player.y - 100),
        ],
        speed: playerSpeed * 10,
        onComplete: () {
        }));

    //enemies & helpers
    obstacles = Obstacles();
    add(obstacles);

    //add level info
    add(LevelInfo());

    return super.onLoad();
  }

  void _calculateScreenSize() {
    screenSize = Size(canvasSize.toOffset().dx, canvasSize.toOffset().dy);
    halfWidth = (screenSize.width / 2).floorToDouble();
    halfHeight = (screenSize.height / 2).floorToDouble();
  }

  @override
  void onResize(Vector2 canvasSize) {
    super.onResize(canvasSize);
  }

  @override
  void render(Canvas canvas) {
    super.render(canvas);
  }

  @override
  void update(double dt) {
    if (gameOver) {
      overlays.add("ResetMenu");
    }

    super.update(dt);
  }

  void _addColorCards() {
    double gridStartX = (screenSize.width -
            (cardSize * cardPerRow + spacer * (cardPerRow - 1))) /
        2;
    double gridStartY = screenSize.height / 2;
    Vector2 gridPosition = Vector2(gridStartX, gridStartY);

    //generate colors first
    List<Color> colors = ColorGenerator.generate(cardPerRow * cardPerRow);

    for (var i = 0; i < cardPerRow * cardPerRow; i++) {
      //increase position.y every cardPerRow rows
      if (i % cardPerRow == 0) {
        gridPosition = Vector2(gridStartX, gridPosition.y + spacer + cardSize);
      }

      //add color card to game
      ColorCard card = ColorCard(i, colors[i], cardSize.floorToDouble());
      card.position = gridPosition;
      cards.add(card);
      add(card);

      //move position.x of grid placeholder for next card
      gridPosition += Vector2(card.width + spacer, 0);
    }
  }

  void selectColor(int index, bool toggle) {
    if (toggle) {
      selectedIndexes.add(index);
    } else {
      selectedIndexes.remove(index); //remove where index = value
    }

    //mix colors if there are 2 selected cards
    if (selectedIndexes.length >= 2) {
      //mix color
      var newColorSource = ColorGenerator.randomColor();
      var mixedColor = ColorGenerator.mix(
          cards[selectedIndexes[0]].color, cards[selectedIndexes[1]].color);
      cards[selectedIndexes[0]].updateColor(newColorSource, true);
      cards[selectedIndexes[1]].updateColor(mixedColor, false);

      //update color to player
      player.updatePlayerColor(mixedColor);

      var newPost = _posOfObstacleWithSameColor();
      if (newPost != Vector2(0, 0)) {
        player.moveTo(newPost);
      }
    }
  }



  void resetSelectedColors() {
    //reset selected indexes after mixing
    if (selectedIndexes.length >= 2) {
      selectedIndexes = [];
    }
  }

  void resetCardColors(){
    //change card color positions
    List<Color> colors = ColorGenerator.generate(cardPerRow * cardPerRow);
    colors.shuffle();
    for(var i = 0; i < cards.length; i++){
      cards[i].color = colors[i];
    }

  }

  Vector2 _posOfObstacleWithSameColor() {
    for (var o in obstacles.items) {
      if (player.color == o.color) {
        return Vector2((o.x + o.width / 2) - playerSize / 2, player.y);
      }
    }
    return Vector2(0, 0);
  }

  void calculateObjectives() {
    objectives = [];
    //get all available colors
    List<Color> availableColors = [];
    for (var c in cards) {
      availableColors.add(c.color);
    }
    //calculate random mix
    availableColors.shuffle();
    for (var i = 0; i < availableColors.length; i++) {
      for (var j = 0; j < availableColors.length; j++) {
        var mix = ColorGenerator.mix(availableColors[i], availableColors[j]);
        if (!objectives.contains(mix)) {
          objectives.add(mix);
        }
      }
    }
  }

  void killPlayer() {
    gameOver = true;
    player.isDead = true;
    player.deadAnimation();

    //disable card on die
    for(var card in cards){
      card.color = card.color.withOpacity(0.5);
      card.selectable = false;
    }
  }

  void passPlayer() {
    obstacles.stop(true);
    player.passAnimation();
  }
}

background.dart

import 'dart:math';
import 'dart:ui';

import 'package:color_mixer/screens/game.dart';
import 'package:color_mixer/utils/color_helper.dart';
import 'package:flame/components.dart';
import 'package:flutter/material.dart' show Colors;

class Background extends PositionComponent {
  final Color color = ColorGenerator.backgroundColor;

  @override
  void render(Canvas canvas) {
    Rect bgRect = Rect.largest;
    Paint bgPaint = Paint();
    bgPaint.color = color;
    canvas.drawRect(bgRect, bgPaint);
    super.render(canvas);
  }
}

class PlayerBackground extends PositionComponent with HasGameRef<GameManager> {
  final Color color = ColorGenerator.backgroundColor;

  @override
  Future<void> onLoad() {

    var deadPoint = gameRef.halfHeight - 30;
    for(var i = 0; i < 10; i ++){
      var posX = Random().nextDouble() * gameRef.screenSize.width;
      var posY = Random().nextDouble() * (gameRef.halfHeight - 60);
      var circle = CircleBackground(Vector2(posX, posY), deadPoint);
      addChild(circle);
    }

    return super.onLoad();
  }

  @override
  void render(Canvas canvas) {
    //background
    Rect bgRect = Rect.fromLTWH(0, 0, gameRef.screenSize.width,
        (gameRef.screenSize.height / 2).floorToDouble());
    canvas.drawRect(bgRect, Paint()..color = color);

    //ground
    Rect ground = Rect.fromLTWH(
        0,
        (gameRef.screenSize.height / 2).floorToDouble(),
        gameRef.screenSize.width,
        1);
    canvas.drawRect(ground, Paint()..color = Colors.white.withOpacity(0.15));

    super.render(canvas);
  }
}

class CircleBackground extends PositionComponent {
  final Color color = Colors.white;
  double deadPoint = 0;
  List<double> sizeList = [20, 25, 30, 35, 40];

  CircleBackground(Vector2 position, double deadPoint) {
    this.size =
        Vector2(sizeList[Random().nextInt(4)], sizeList[Random().nextInt(4)]);
    this.deadPoint = deadPoint;
    this.position = position;
  }

  @override
  void render(Canvas canvas) {
    canvas.drawCircle(
        position.toOffset(), 12, Paint()..color = color.withOpacity(0.15));
    super.render(canvas);
  }

  @override
  void update(double dt) {
    if (y < deadPoint) {
      //create random size after reaching dead point
      size =
          Vector2(sizeList[Random().nextInt(4)], sizeList[Random().nextInt(4)]);
      y += 10 * dt;
    } else {
      y = -50;
    }
    super.update(dt);
  }
}

player.dart

import 'dart:math';
import 'dart:ui';

import 'package:color_mixer/screens/game.dart';
import 'package:color_mixer/utils/sound_helper.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/extensions.dart';
import 'package:flame/particles.dart';
import 'package:flutter/material.dart';

class Player extends PositionComponent with HasGameRef<GameManager> {
  Color color;
  bool isDead = false;
  List<TrailComponent> trails = [];
  List<double> trailScales = [2/3, 1/2 , 1/3];

  double get widthInScreen => width + x;

  Player(Vector2 position, Vector2 size, Color color) {
    this.position = position;
    this.color = color;
    this.size = size;
    this.isDead = false;
  }

  @override
  Future<void> onLoad() {
    initTrail();
    return super.onLoad();
  }

  void initTrail() {


    var cPosY = height + 2;
    for( var i = 0; i < 3; i++){
      var scale = trailScales[i];
      var c = TrailComponent(Size(width * scale, height * trailScales[i]), color.withOpacity(scale) ,
          (width - width * scale) / 2, cPosY);
      trails.add(c);
      addChild(ParticleComponent(
          particle: ComponentParticle(lifespan: 10000, component: c)));
      cPosY += height * scale + 2;
    }

  }

  @override
  void render(Canvas canvas) {
    var shadow = Rect.fromLTWH(x - 1, y - 1, width + 2, height + 2);
    canvas.drawRect(shadow, Paint()..color = Colors.blue[50]);

    var rect = Rect.fromLTWH(x, y, width, height);
    canvas.drawRect(rect, paint());

    var innerSize = (size.x / 2).floorToDouble();

    var shadow2 = Rect.fromLTWH(x + innerSize / 2 - 1, y + innerSize / 2 - 1,
        innerSize + 2, innerSize + 2);
    canvas.drawRect(shadow2, Paint()..color = Colors.blue[50]);

    var rectInner = Rect.fromLTWH(
        x + innerSize / 2, y + innerSize / 2, innerSize, innerSize);
    canvas.drawRect(rectInner, paint());

    super.render(canvas);
  }

  Paint paint() {
    return Paint()..color = this.color;
  }

  @override
  void update(double dt) {
    super.update(dt);
  }

  void updatePlayerColor(Color mixedColor){
      color = mixedColor;
      for(var i = 0; i < trails.length; i++){
        trails[i].color = mixedColor.withOpacity(trailScales[i]);
      }
  }

  void moveTo(Vector2 newPos) {
    addEffect(MoveEffect(
      path: [
        newPos,
      ],
      speed: gameRef.playerSpeed * 10,
    ));
  }

  void deadAnimation() {
    SoundHelper.dead();
    if(isDead){
      isDead = false;
      hitAnimation();
      addEffect(MoveEffect(
          path: [
            Vector2(x, y + 50),
          ],
          speed: gameRef.playerSpeed * 20,
      ));

    }

  }

  void passAnimation() {
    SoundHelper.shoot();
    addEffect(MoveEffect(
      path: [
        Vector2(x, y - 30),
      ],
      curve: Curves.ease,
      isAlternating: true,
      speed: gameRef.playerSpeed * 20,
    ));
  }

  void hitAnimation() {
    //remove trail
    removeTrail();
    //play hit animation
    Random rnd = Random();
    Function randomOffset = () => Offset(
          rnd.nextDouble() * 300 - 100,
          rnd.nextDouble() * 200 - 100,
        );
    addChild(ParticleComponent(
        particle: Particle.generate(
            count: 10,
            generator: (i) => AcceleratedParticle(
                position: Offset(width / 2, 0),
                acceleration: randomOffset(),
                child: CircleParticle(paint: Paint()..color = color)))));
  }

  void removeTrail(){
    if(trails.isNotEmpty){
      for(var trail in trails){
        trail.size = Size.zero;
      }
    }
  }
}

class TrailComponent extends Component {
  Size size;
  double left;
  double top;
  Color color;

  TrailComponent(Size size, Color color, double left, double top) {
    this.size = size;
    this.left = left;
    this.top = top;
    this.color = color;
  }

  @override
  Future<void> onLoad() {
    return super.onLoad();
  }

  void render(Canvas c) {
    c.drawRect(Rect.fromLTWH(left, top, size.width, size.height),
        Paint()..color = color);
  }

  void update(double dt) {

  }
}

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top