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) { } }