diff --git a/lib/game/blocs/game_bloc.dart b/lib/game/blocs/game_bloc.dart index 89c2f29..df87bdf 100644 --- a/lib/game/blocs/game_bloc.dart +++ b/lib/game/blocs/game_bloc.dart @@ -19,7 +19,7 @@ class GameBloc extends Bloc { GameBloc({ @required this.random, this.flameManager, - this.snakeInitialLength = 4, + this.snakeInitialLength = 9, }) : assert(random != null), assert(snakeInitialLength >= 4) { add(LoadAssetsEvent()); diff --git a/lib/game/flame/flame_manager.dart b/lib/game/flame/flame_manager.dart index 5bf9bc4..d6ef951 100644 --- a/lib/game/flame/flame_manager.dart +++ b/lib/game/flame/flame_manager.dart @@ -17,6 +17,10 @@ class FlameManager with IFlameManager { await Flame.images.loadAll([ 'food/food.png', 'food/red_food.png', + 'snake/head.png', + 'snake/body.png', + 'snake/body_curve.png', + 'snake/tail.png', ]); } } diff --git a/lib/game/renderer/board_component.dart b/lib/game/renderer/board_component.dart new file mode 100644 index 0000000..7d48fc3 --- /dev/null +++ b/lib/game/renderer/board_component.dart @@ -0,0 +1,23 @@ +import 'package:flame/anchor.dart'; +import 'package:flame/components/component.dart'; +import 'package:flame/sprite.dart'; +import 'package:flutter/widgets.dart'; + +/// Renders a single sprite based on [SpriteComponent] on the game board. +class BoardComponent extends SpriteComponent { + /// Convenient constructor. + BoardComponent(String fileName, double tileSize) { + width = tileSize; + height = tileSize; + sprite = Sprite(fileName); + anchor = Anchor.center; + } + + @override + void render(Canvas canvas) { + canvas.save(); + canvas.translate(width * 0.5, height * 0.5); + super.render(canvas); + canvas.restore(); + } +} diff --git a/lib/game/renderer/food_renderer.dart b/lib/game/renderer/food_renderer.dart deleted file mode 100644 index c8aaa6e..0000000 --- a/lib/game/renderer/food_renderer.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flame/sprite.dart'; -import 'package:flutter/widgets.dart'; - -import '../models/food.dart'; - -import 'sprite_renderer.dart'; - -/// Used to render the [Food]. -class FoodRenderer with SpriteRenderer { - /// Conveninent construtor. - FoodRenderer(this.tileSize) : _sprite = Sprite('food/food.png'); - - /// The tile size. - final double tileSize; - - final Sprite _sprite; - Rect _rect; - - /// Update the Food location. If null, it won't draw nothing. - void updateFood(Food food) { - _rect = food != null - ? Rect.fromLTWH( - food.x * tileSize, - food.y * tileSize, - tileSize, - tileSize, - ) - : null; - } - - @override - void render(Canvas canvas) { - if (_rect != null) { - _sprite.renderRect(canvas, _rect); - } - } - - @override - void update(double dt) {} -} diff --git a/lib/game/renderer/game_renderer.dart b/lib/game/renderer/game_renderer.dart index 8148a0e..80df230 100644 --- a/lib/game/renderer/game_renderer.dart +++ b/lib/game/renderer/game_renderer.dart @@ -7,8 +7,8 @@ import '../../ui/colors.dart'; import '../models/board.dart'; import '../models/food.dart'; import '../models/snake.dart'; -import '../renderer/snake_renderer.dart'; -import 'food_renderer.dart'; +import 'board_component.dart'; +import 'snake_component.dart'; /// Main game render. Used to render the game screeen. class GameRenderer extends Game { @@ -20,8 +20,8 @@ class GameRenderer extends Game { @required this.board, @required this.tileSize, }) { - _foodSprite = FoodRenderer(tileSize); - _snakeRenderer = SnakeRenderer(tileSize); + _food = BoardComponent('food/food.png', tileSize); + _snake = SnakeComponent(tileSize); } /// The real screen size. @@ -33,17 +33,21 @@ class GameRenderer extends Game { /// The tile size used to render the food and snake. final double tileSize; - FoodRenderer _foodSprite; - SnakeRenderer _snakeRenderer; + BoardComponent _food; + SnakeComponent _snake; /// Called to update the [Food] position. void updateFood(Food food) { - _foodSprite.updateFood(food); + if (food != null) { + _food + ..x = food.x * tileSize + ..y = food.y * tileSize; + } } /// Called to update the [Snake] positions. void updateSnake(Snake snake) { - _snakeRenderer.updateSnake(snake); + _snake.updateSnake(snake); } @override @@ -53,8 +57,8 @@ class GameRenderer extends Game { _drawBackground(canvas); - _foodSprite.render(canvas); - _snakeRenderer.render(canvas); + _food.render(canvas); + _snake.render(canvas); canvas.restore(); } diff --git a/lib/game/renderer/snake_component.dart b/lib/game/renderer/snake_component.dart new file mode 100644 index 0000000..2f51d32 --- /dev/null +++ b/lib/game/renderer/snake_component.dart @@ -0,0 +1,112 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flame/components/component.dart'; + +import '../models/snake.dart'; +import '../models/vec2d.dart'; +import '../renderer/board_component.dart'; + +/// Renders the snake using [SpriteComponent] for each part of it. +class SnakeComponent extends Component { + /// Convenient constructor. + SnakeComponent(this.tileSize); + + /// The tile size. Use to draw the snake. + final double tileSize; + + List _snakeBody; + + BoardComponent _buildHead(Vec2d current, Vec2d prev) { + final imageRotation = pi / 2; + final sameAxis = prev.x == current.x; + + final mustInvest = atan2(current.x - prev.x, current.y - prev.y) <= 0.0; + + return BoardComponent('snake/head.png', tileSize) + ..x = tileSize * current.x + ..y = tileSize * current.y + ..angle = (sameAxis ? 0.0 : imageRotation) + (mustInvest ? pi : 0.0); + } + + BoardComponent _buildTail(Vec2d current, Vec2d prev) { + final imageRotation = pi / 2; + final sameAxis = prev.x == current.x; + + final mustInvest = atan2(current.x - prev.x, current.y - prev.y) > 0.0; + + return BoardComponent('snake/tail.png', tileSize) + ..x = tileSize * current.x + ..y = tileSize * current.y + ..angle = (sameAxis ? 0.0 : imageRotation) + (mustInvest ? pi : 0.0); + } + + BoardComponent _buildBody(Vec2d current, Vec2d prev, Vec2d next) { + final imageRotation = pi / 2; + + BoardComponent component; + + final diffAngle = atan2(next.x - current.x, next.y - current.y) - + atan2(prev.x - current.x, prev.y - current.y); + + if (asin(sin(diffAngle)).abs() == (pi / 2)) { + final isClockwise = asin(sin(diffAngle)) < 0.0; + final rotate = isClockwise ? 0.0 : imageRotation; + + component = BoardComponent('snake/body_curve.png', tileSize); + component.angle = -atan2(next.x - current.x, next.y - current.y) + rotate; + } else { + component = BoardComponent('snake/body.png', tileSize); + final sameAxis = prev.x == current.x; + component.angle = sameAxis ? 0.0 : imageRotation; + } + + return component + ..x = tileSize * current.x + ..y = tileSize * current.y; + } + + /// Update the snake position using [snake] info. + void updateSnake(Snake snake) { + if (snake != null) { + final snakeBody = snake.body.toList(); + _snakeBody = [ + _buildHead( + snakeBody[0], + snakeBody[1], + ), + ]; + + for (var i = 1; i <= snakeBody.length - 2; i++) { + _snakeBody.add( + _buildBody( + snakeBody[i], + snakeBody[i - 1], + snakeBody[i + 1], + ), + ); + } + + _snakeBody.add(_buildTail( + snakeBody.last, + snakeBody[snakeBody.length - 2], + )); + } else { + _snakeBody = null; + } + } + + @override + void render(Canvas canvas) { + if (_snakeBody != null) { + for (var part in _snakeBody.skip(1)) { + part.render(canvas); + } + + _snakeBody.first.render(canvas); + } + } + + @override + void update(double dt) {} +} diff --git a/lib/game/renderer/snake_renderer.dart b/lib/game/renderer/snake_renderer.dart deleted file mode 100644 index 8d2a63a..0000000 --- a/lib/game/renderer/snake_renderer.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'dart:ui'; - -import 'package:flame/sprite.dart'; - -import '../models/snake.dart'; -import 'sprite_renderer.dart'; - -/// Used to render the [Snake]. -class SnakeRenderer with SpriteRenderer { - /// Conveninent construtor. - SnakeRenderer(this.tileSize) - : _head = Sprite('snake/head.png'), - _body = Sprite('snake/body.png'), - _curve = Sprite('snake/body_curve.png'), - _tail = Sprite('snake/tail.png'); - - /// The tile size. - final double tileSize; - - final Sprite _head; - final Sprite _body; - final Sprite _curve; - final Sprite _tail; - - List _rect; - - /// Update the Snake position. If null, it won't draw nothing. - void updateSnake(Snake snake) { - if (snake != null) { - _rect = snake.body - .map( - (it) => Rect.fromLTWH( - it.x * tileSize, - it.y * tileSize, - tileSize, - tileSize, - ), - ) - .toList(); - } else { - _rect = null; - } - } - - @override - void render(Canvas canvas) { - if (_rect != null) { - for (var rect in _rect) { - _body.renderRect(canvas, rect); - } - } - } - - @override - void update(double dt) {} -} diff --git a/lib/game/renderer/sprite_renderer.dart b/lib/game/renderer/sprite_renderer.dart deleted file mode 100644 index 907349e..0000000 --- a/lib/game/renderer/sprite_renderer.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:flutter/widgets.dart'; - -mixin SpriteRenderer { - void render(Canvas canvas); - - void update(double dt); -} diff --git a/lib/ui/colors.dart b/lib/ui/colors.dart index 20cc250..a7c88b8 100644 --- a/lib/ui/colors.dart +++ b/lib/ui/colors.dart @@ -11,7 +11,7 @@ class GameColors { static const primaryDark = Color(0xFF1A2629); /// The color of the unplayable area. - static const voidBackground = Color(0xFF000000); + static const voidBackground = Color(0xFFFF0000); /// The background of the playable area. static const background = Color(0xFFB0B0B0);