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 *

By continuing to use the site, you agree to the use of cookies. more information

The cookie settings on this website are set to "allow cookies" to give you the best browsing experience possible. If you continue to use this website without changing your cookie settings or you click "Accept" below then you are consenting to this.

Close