docs: Chapter 3 of the Klondike game tutorial (#1515)

This commit is contained in:
Pasha Stetsenko
2022-04-11 14:27:54 -07:00
committed by GitHub
parent 2171a11937
commit cf393761a1
19 changed files with 1064 additions and 123 deletions

View File

@ -10,7 +10,7 @@ button.flutter-app-button {
font-size: 1.1em;
font-weight: bold;
line-height: 1em;
margin-right: 1em;
margin: 0 1em 1em 0;
min-height: 26pt;
min-width: 120pt;
}

View File

@ -138,7 +138,7 @@ class FlutterAppDirective(SphinxDirective):
)
if not need_compiling:
return
self.logger.info('Compiling Flutter app ' + self.app_name)
self.logger.info('Compiling Flutter app [%s]' % self.app_name)
self._compile_source()
self._copy_compiled()
self._create_index_html()
@ -150,7 +150,7 @@ class FlutterAppDirective(SphinxDirective):
def _compile_source(self):
try:
subprocess.run(
['flutter', 'build', 'web', '--web-renderer', 'html'],
['flutter', 'build', 'web'],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
cwd=self.source_dir,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 468 KiB

After

Width:  |  Height:  |  Size: 538 KiB

View File

@ -1,6 +1,7 @@
import 'dart:html'; // ignore: avoid_web_libraries_in_flutter
import 'package:flutter/widgets.dart';
import 'step2/main.dart' as step2;
import 'step3/main.dart' as step3;
void main() {
var page = window.location.search ?? '';
@ -12,6 +13,10 @@ void main() {
step2.main();
break;
case 'step3':
step3.main();
break;
default:
runApp(
Directionality(

View File

@ -1,5 +1,6 @@
import 'package:flame/components.dart';
import 'package:flame/experimental.dart';
import 'package:flame/flame.dart';
import 'package:flame/game.dart';
import 'components/foundation.dart';
@ -8,31 +9,33 @@ import 'components/stock.dart';
import 'components/waste.dart';
class KlondikeGame extends FlameGame {
final double cardGap = 175.0;
final double cardWidth = 1000.0;
final double cardHeight = 1400.0;
static const double cardGap = 175.0;
static const double cardWidth = 1000.0;
static const double cardHeight = 1400.0;
static const double cardRadius = 100.0;
static final Vector2 cardSize = Vector2(cardWidth, cardHeight);
@override
Future<void> onLoad() async {
await images.load('klondike-sprites.png');
await Flame.images.load('klondike-sprites.png');
final stock = Stock()
..size = Vector2(cardWidth, cardHeight)
..size = cardSize
..position = Vector2(cardGap, cardGap);
final waste = Waste()
..size = Vector2(cardWidth * 1.5, cardHeight)
..size = cardSize
..position = Vector2(cardWidth + 2 * cardGap, cardGap);
final foundations = List.generate(
4,
(i) => Foundation()
..size = Vector2(cardWidth, cardHeight)
..size = cardSize
..position =
Vector2((i + 3) * (cardWidth + cardGap) + cardGap, cardGap),
);
final piles = List.generate(
7,
(i) => Pile()
..size = Vector2(cardWidth, cardHeight)
..size = cardSize
..position = Vector2(
cardGap + i * (cardWidth + cardGap),
cardHeight + 2 * cardGap,
@ -44,13 +47,21 @@ class KlondikeGame extends FlameGame {
..add(waste)
..addAll(foundations)
..addAll(piles);
add(world);
final camera = CameraComponent(world: world)
..viewfinder.visibleGameSize =
Vector2(cardWidth * 7 + cardGap * 8, 4 * cardHeight + 3 * cardGap)
..viewfinder.position = Vector2(cardWidth * 3.5 + cardGap * 4, 0)
..viewfinder.anchor = Anchor.topCenter;
add(world);
add(camera);
}
}
Sprite klondikeSprite(double x, double y, double width, double height) {
return Sprite(
Flame.images.fromCache('klondike-sprites.png'),
srcPosition: Vector2(x, y),
srcSize: Vector2(width, height),
);
}

View File

@ -0,0 +1,209 @@
import 'dart:math';
import 'dart:ui';
import 'package:flame/components.dart';
import '../klondike_game.dart';
import '../rank.dart';
import '../suit.dart';
class Card extends PositionComponent {
Card(int intRank, int intSuit)
: rank = Rank.fromInt(intRank),
suit = Suit.fromInt(intSuit),
_faceUp = false,
super(size: KlondikeGame.cardSize);
final Rank rank;
final Suit suit;
bool _faceUp;
bool get isFaceUp => _faceUp;
void flip() => _faceUp = !_faceUp;
@override
String toString() => rank.label + suit.label; // e.g. "Q♠" or "10♦"
@override
void render(Canvas canvas) {
if (_faceUp) {
_renderFront(canvas);
} else {
_renderBack(canvas);
}
}
static final Paint backBackgroundPaint = Paint()
..color = const Color(0xff380c02);
static final Paint backBorderPaint1 = Paint()
..color = const Color(0xffdbaf58)
..style = PaintingStyle.stroke
..strokeWidth = 10;
static final Paint backBorderPaint2 = Paint()
..color = const Color(0x5CEF971B)
..style = PaintingStyle.stroke
..strokeWidth = 35;
static final RRect cardRRect = RRect.fromRectAndRadius(
KlondikeGame.cardSize.toRect(),
const Radius.circular(KlondikeGame.cardRadius),
);
static final RRect backRRectInner = cardRRect.deflate(40);
static late final Sprite flameSprite = klondikeSprite(1367, 6, 357, 501);
void _renderBack(Canvas canvas) {
canvas.drawRRect(cardRRect, backBackgroundPaint);
canvas.drawRRect(cardRRect, backBorderPaint1);
canvas.drawRRect(backRRectInner, backBorderPaint2);
flameSprite.render(canvas, position: size / 2, anchor: Anchor.center);
}
static final Paint frontBackgroundPaint = Paint()
..color = const Color(0xff000000);
static final Paint redBorderPaint = Paint()
..color = const Color(0xffece8a3)
..style = PaintingStyle.stroke
..strokeWidth = 10;
static final Paint blackBorderPaint = Paint()
..color = const Color(0xff7ab2e8)
..style = PaintingStyle.stroke
..strokeWidth = 10;
static final blueFilter = Paint()
..colorFilter = const ColorFilter.mode(
Color(0x880d8bff),
BlendMode.srcATop,
);
static late final Sprite redJack = klondikeSprite(81, 565, 562, 488);
static late final Sprite redQueen = klondikeSprite(717, 541, 486, 515);
static late final Sprite redKing = klondikeSprite(1305, 532, 407, 549);
static late final Sprite blackJack = klondikeSprite(81, 565, 562, 488)
..paint = blueFilter;
static late final Sprite blackQueen = klondikeSprite(717, 541, 486, 515)
..paint = blueFilter;
static late final Sprite blackKing = klondikeSprite(1305, 532, 407, 549)
..paint = blueFilter;
void _renderFront(Canvas canvas) {
canvas.drawRRect(cardRRect, frontBackgroundPaint);
canvas.drawRRect(
cardRRect,
suit.isRed ? redBorderPaint : blackBorderPaint,
);
final rankSprite = suit.isBlack ? rank.blackSprite : rank.redSprite;
final suitSprite = suit.sprite;
_drawSprite(canvas, rankSprite, 0.1, 0.08);
_drawSprite(canvas, suitSprite, 0.1, 0.18, scale: 0.5);
_drawSprite(canvas, rankSprite, 0.1, 0.08, rotate: true);
_drawSprite(canvas, suitSprite, 0.1, 0.18, scale: 0.5, rotate: true);
switch (rank.value) {
case 1:
_drawSprite(canvas, suitSprite, 0.5, 0.5, scale: 2.5);
break;
case 2:
_drawSprite(canvas, suitSprite, 0.5, 0.25);
_drawSprite(canvas, suitSprite, 0.5, 0.25, rotate: true);
break;
case 3:
_drawSprite(canvas, suitSprite, 0.5, 0.2);
_drawSprite(canvas, suitSprite, 0.5, 0.5);
_drawSprite(canvas, suitSprite, 0.5, 0.2, rotate: true);
break;
case 4:
_drawSprite(canvas, suitSprite, 0.3, 0.25);
_drawSprite(canvas, suitSprite, 0.7, 0.25);
_drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);
break;
case 5:
_drawSprite(canvas, suitSprite, 0.3, 0.25);
_drawSprite(canvas, suitSprite, 0.7, 0.25);
_drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);
_drawSprite(canvas, suitSprite, 0.5, 0.5);
break;
case 6:
_drawSprite(canvas, suitSprite, 0.3, 0.25);
_drawSprite(canvas, suitSprite, 0.7, 0.25);
_drawSprite(canvas, suitSprite, 0.3, 0.5);
_drawSprite(canvas, suitSprite, 0.7, 0.5);
_drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);
break;
case 7:
_drawSprite(canvas, suitSprite, 0.3, 0.2);
_drawSprite(canvas, suitSprite, 0.7, 0.2);
_drawSprite(canvas, suitSprite, 0.5, 0.35);
_drawSprite(canvas, suitSprite, 0.3, 0.5);
_drawSprite(canvas, suitSprite, 0.7, 0.5);
_drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);
break;
case 8:
_drawSprite(canvas, suitSprite, 0.3, 0.2);
_drawSprite(canvas, suitSprite, 0.7, 0.2);
_drawSprite(canvas, suitSprite, 0.5, 0.35);
_drawSprite(canvas, suitSprite, 0.3, 0.5);
_drawSprite(canvas, suitSprite, 0.7, 0.5);
_drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);
_drawSprite(canvas, suitSprite, 0.5, 0.35, rotate: true);
break;
case 9:
_drawSprite(canvas, suitSprite, 0.3, 0.2);
_drawSprite(canvas, suitSprite, 0.7, 0.2);
_drawSprite(canvas, suitSprite, 0.5, 0.3);
_drawSprite(canvas, suitSprite, 0.3, 0.4);
_drawSprite(canvas, suitSprite, 0.7, 0.4);
_drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);
_drawSprite(canvas, suitSprite, 0.3, 0.4, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.4, rotate: true);
break;
case 10:
_drawSprite(canvas, suitSprite, 0.3, 0.2);
_drawSprite(canvas, suitSprite, 0.7, 0.2);
_drawSprite(canvas, suitSprite, 0.5, 0.3);
_drawSprite(canvas, suitSprite, 0.3, 0.4);
_drawSprite(canvas, suitSprite, 0.7, 0.4);
_drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);
_drawSprite(canvas, suitSprite, 0.5, 0.3, rotate: true);
_drawSprite(canvas, suitSprite, 0.3, 0.4, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.4, rotate: true);
break;
case 11:
_drawSprite(canvas, suit.isRed ? redJack : blackJack, 0.5, 0.5);
break;
case 12:
_drawSprite(canvas, suit.isRed ? redQueen : blackQueen, 0.5, 0.5);
break;
case 13:
_drawSprite(canvas, suit.isRed ? redKing : blackKing, 0.5, 0.5);
break;
}
}
void _drawSprite(
Canvas canvas,
Sprite sprite,
double relativeX,
double relativeY, {
double scale = 1,
bool rotate = false,
}) {
if (rotate) {
canvas.save();
canvas.translate(size.x / 2, size.y / 2);
canvas.rotate(pi);
canvas.translate(-size.x / 2, -size.y / 2);
}
sprite.render(
canvas,
position: Vector2(relativeX * size.x, relativeY * size.y),
anchor: Anchor.center,
size: sprite.srcSize.scaled(scale),
);
if (rotate) {
canvas.restore();
}
}
}

View File

@ -0,0 +1,6 @@
import 'package:flame/components.dart';
class Foundation extends PositionComponent {
@override
bool get debugMode => true;
}

View File

@ -0,0 +1,6 @@
import 'package:flame/components.dart';
class Pile extends PositionComponent {
@override
bool get debugMode => true;
}

View File

@ -0,0 +1,6 @@
import 'package:flame/components.dart';
class Stock extends PositionComponent {
@override
bool get debugMode => true;
}

View File

@ -0,0 +1,6 @@
import 'package:flame/components.dart';
class Waste extends PositionComponent {
@override
bool get debugMode => true;
}

View File

@ -0,0 +1,83 @@
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame/experimental.dart';
import 'package:flame/flame.dart';
import 'package:flame/game.dart';
import 'components/card.dart';
import 'components/foundation.dart';
import 'components/pile.dart';
import 'components/stock.dart';
import 'components/waste.dart';
class KlondikeGame extends FlameGame {
static const double cardGap = 175.0;
static const double cardWidth = 1000.0;
static const double cardHeight = 1400.0;
static const double cardRadius = 100.0;
static final Vector2 cardSize = Vector2(cardWidth, cardHeight);
@override
Future<void> onLoad() async {
await Flame.images.load('klondike-sprites.png');
final stock = Stock()
..size = cardSize
..position = Vector2(cardGap, cardGap);
final waste = Waste()
..size = cardSize
..position = Vector2(cardWidth + 2 * cardGap, cardGap);
final foundations = List.generate(
4,
(i) => Foundation()
..size = cardSize
..position =
Vector2((i + 3) * (cardWidth + cardGap) + cardGap, cardGap),
);
final piles = List.generate(
7,
(i) => Pile()
..size = cardSize
..position = Vector2(
cardGap + i * (cardWidth + cardGap),
cardHeight + 2 * cardGap,
),
);
final world = World()
..add(stock)
..add(waste)
..addAll(foundations)
..addAll(piles);
add(world);
final camera = CameraComponent(world: world)
..viewfinder.visibleGameSize =
Vector2(cardWidth * 7 + cardGap * 8, 4 * cardHeight + 3 * cardGap)
..viewfinder.position = Vector2(cardWidth * 3.5 + cardGap * 4, 0)
..viewfinder.anchor = Anchor.topCenter;
add(camera);
final random = Random();
for (var i = 0; i < 7; i++) {
for (var j = 0; j < 4; j++) {
final card = Card(random.nextInt(13) + 1, random.nextInt(4))
..position = Vector2(100 + i * 1150, 100 + j * 1500)
..addToParent(world);
// flip the card face-up with 90% probability
if (random.nextDouble() < 0.9) {
card.flip();
}
}
}
}
}
Sprite klondikeSprite(double x, double y, double width, double height) {
return Sprite(
Flame.images.fromCache('klondike-sprites.png'),
srcPosition: Vector2(x, y),
srcSize: Vector2(width, height),
);
}

View File

@ -0,0 +1,9 @@
import 'package:flame/game.dart';
import 'package:flutter/widgets.dart';
import 'klondike_game.dart';
void main() {
final game = KlondikeGame();
runApp(GameWidget(game: game));
}

View File

@ -0,0 +1,44 @@
import 'package:flame/components.dart';
import 'package:flutter/foundation.dart';
import 'klondike_game.dart';
@immutable
class Rank {
factory Rank.fromInt(int value) {
assert(value >= 1 && value <= 13);
return _singletons[value - 1];
}
Rank._(
this.value,
this.label,
double x1,
double y1,
double x2,
double y2,
double w,
double h,
) : redSprite = klondikeSprite(x1, y1, w, h),
blackSprite = klondikeSprite(x2, y2, w, h);
final int value;
final String label;
final Sprite redSprite;
final Sprite blackSprite;
static late final List<Rank> _singletons = [
Rank._(1, 'A', 335, 164, 789, 161, 120, 129),
Rank._(2, '2', 20, 19, 15, 322, 83, 125),
Rank._(3, '3', 122, 19, 117, 322, 80, 127),
Rank._(4, '4', 213, 12, 208, 315, 93, 132),
Rank._(5, '5', 314, 21, 309, 324, 85, 125),
Rank._(6, '6', 419, 17, 414, 320, 84, 129),
Rank._(7, '7', 509, 21, 505, 324, 92, 128),
Rank._(8, '8', 612, 19, 607, 322, 78, 127),
Rank._(9, '9', 709, 19, 704, 322, 84, 130),
Rank._(10, '10', 810, 20, 805, 322, 137, 127),
Rank._(11, 'J', 15, 170, 469, 167, 56, 126),
Rank._(12, 'Q', 92, 168, 547, 165, 132, 128),
Rank._(13, 'K', 243, 170, 696, 167, 92, 123),
];
}

View File

@ -0,0 +1,29 @@
import 'package:flame/sprite.dart';
import 'package:flutter/foundation.dart';
import 'klondike_game.dart';
@immutable
class Suit {
factory Suit.fromInt(int index) {
assert(index >= 0 && index <= 3);
return _singletons[index];
}
Suit._(this.value, this.label, double x, double y, double w, double h)
: sprite = klondikeSprite(x, y, w, h);
final int value;
final String label;
final Sprite sprite;
static late final List<Suit> _singletons = [
Suit._(0, '', 1176, 17, 172, 183),
Suit._(1, '', 973, 14, 177, 182),
Suit._(2, '', 974, 226, 184, 172),
Suit._(3, '', 1178, 220, 176, 182),
];
/// Hearts and Diamonds are red, while Clubs and Spades are black.
bool get isRed => value <= 1;
bool get isBlack => value >= 2;
}

View File

@ -15,4 +15,6 @@ with the [Dart] programming language.
1. Preparation <step1.md>
2. Scaffolding <step2.md>
3. Cards <step3.md>
[To be continued]... <tbc.md>
```

View File

@ -30,7 +30,7 @@ shown below:
Here you can see both the general layout of the game, as well as names of
various objects. These names are the [standard terminology] for solitaire games.
Which is really lucky, because normally figuring out good names for various
classes is a quite challenging task.
classes is quite a challenging task.
Looking at this sketch, we can already imagine the high-level structure of the
game. Obviously, there will be a `Card` class, but also the `Stock` class, the
@ -47,34 +47,34 @@ In such a simple game as Klondike we won't need lots of fancy graphics, but
still some sprites will be needed in order to draw the cards.
In order to prepare the graphic assets, I first took a physical playing card and
measured it to be 63mm × 88mm, which is the ratio of approximately `1.4`. Thus,
I decided that my in-game cards should be rendered at 1000×1400 pixels, and I
should draw all my images with this scale in mind.
measured it to be 63mm × 88mm, which is the ratio of approximately `10:14`.
Thus, I decided that my in-game cards should be rendered at 1000×1400 pixels,
and I should draw all my images with this scale in mind.
Note that the exact pixel dimensions are somewhat irrelevant here, since the
images will in the end be scaled up or down, according to the device's actual
resolution. Here I'm using probably a bigger resolution than necessary for
phones, but it would also work nicely for larger devices like an iPad.
And now, without further ado, the graphic assets for the Klondike game (don't
judge too harshly, I'm not an artist):
And now, without further ado, here's my graphic asset for the Klondike game
(I'm not an artist, so don't judge too harshly):
![](app/assets/images/klondike-sprites.png)
Right-click the image, choose "Save as...", and store it in the `assets/images`
folder of the project. At this point our project structure looks like this
folder of the project. At this point our project's structure looks like this
(there are other files too, of course, but these are the important ones):
```text
klondike/
├─assets/
└─images/
└─klondike-sprites.png
├─lib/
│ └─main.dart
└─pubspec.yaml
├─assets/
└─images/
└─klondike-sprites.png
├─lib/
│ └─main.dart
└─pubspec.yaml
```
By the way, this kind of file is called the _spritesheet_: it's just a
By the way, this kind of file is called the **spritesheet**: it's just a
collection of multiple independent images in a single file. We are using a
spritesheet here for the simple reason that loading a single large image is
faster than many small images. In addition, rendering sprites that were
@ -85,8 +85,11 @@ Here are the contents of my spritesheet:
- Numerals 2, 3, 4, ..., K, A. In theory, we could have rendered these in the
game as text strings, but then we would need to also include a font as an
asset -- seems simpler to just have them as images instead.
- Suit marks: ♠, ♥, ♦, ♣. Again, we could have used Unicode characters for
- Suit marks: ♥, ♦, ♣, ♠. Again, we could have used Unicode characters for
these, but images are much easier to position precisely.
* In case you're wondering why these are yellow/blue instead of red/black
-- turns out, black symbols don't look very nice on a dark background,
so I had to adjust the color scheme.
- Flame logo, for use on the backs of the cards.
- Pictures of a Jack, a Queen, and a King. Normally there would be four times
more of these, with a different character for each suite, but I got too

View File

@ -1,13 +1,13 @@
# 2. Scaffolding
In this section we will use broad strokes in order to outline the main elements
of the game. This includes the main game class, and the general layout.
In this section we will use broad strokes to outline the main elements of the
game. This includes the main game class, and the general layout.
## KlondikeGame
In Flame universe, the `FlameGame` class is the cornerstone of any game. This
class runs the game loop, dispatches events, owns all the components that
In Flame universe, the **FlameGame** class is the cornerstone of most games.
This class runs the game loop, dispatches events, owns all the components that
comprise the game (the component tree), and usually also serves as the central
repository for the game's state.
@ -20,7 +20,7 @@ import 'package:flame/game.dart';
class KlondikeGame extends FlameGame {
@override
Future<void> onLoad() async {
await Images.load('klondike-sprites.png');
await Flame.images.load('klondike-sprites.png');
}
}
```
@ -33,6 +33,27 @@ into the game; but we will be adding more soon. Any image or other resource that
you want to use in the game needs to be loaded first, which is a relatively slow
I/O operation, hence the need for `await` keyword.
I am loading the image into the global `Flame.images` cache here. An alternative
approach is to load it into the `Game.images` cache instead, but then it would
have been more difficult to access that image from other classes.
Also note that I am `await`ing the image to finish loading before initializing
anything else in the game. This is for convenience: it means that by the time
all other components are initialized, they can assume the spritesheet is already
loaded. We can even add a helper function to extract a sprite from the common
spritesheet:
```dart
Sprite klondikeSprite(double x, double y, double width, double height) {
return Sprite(
Flame.images.fromCache('klondike-sprites.png'),
srcPosition: Vector2(x, y),
srcSize: Vector2(width, height),
);
}
```
This helper function won't be needed in this chapter, but will be used
extensively in the next.
Let's incorporate this class into the project so that it isn't orphaned. Open
the `main.dart` find the line which says `final game = FlameGame();` and replace
the `FlameGame` with `KlondikeGame`. You will need to import the class too.
@ -70,6 +91,7 @@ file write
import 'package:flame/components.dart';
class Stock extends PositionComponent {
@override
bool get debugMode => true;
}
```
@ -79,10 +101,28 @@ that has a position and size). We also turn on the debug mode for this class so
that we can see it on the screen even though we don't have any rendering logic
yet.
Likewise, create three more files `components/foundation.dart`,
`components/pile.dart`, and `components/waste.dart`. For now all four classes
will have exactly the same logic inside, we'll be adding more functionality into
those classes in subsequent chapters.
Likewise, create three more classes `Foundation`, `Pile` and `Waste`, each in
its corresponding file. For now all four classes will have exactly the same
logic inside, we'll be adding more functionality into those classes in
subsequent chapters.
At this moment the directory structure of your game should look like this:
```text
klondike/
├─assets/
│ └─images/
│ └─klondike-sprites.png
├─lib/
│ ├─components/
│ │ ├─foundation.dart
│ │ ├─pile.dart
│ │ ├─stock.dart
│ │ └─waste.dart
│ ├─klondike_game.dart
│ └─main.dart
├─analysis_options.yaml
└─pubspec.yaml
```
## Game structure
@ -94,7 +134,7 @@ There exist multiple approaches here, which differ in their complexity,
extendability, and overall philosophy. The approach that we will be taking in
this tutorial is based on using the [World] component, together with a [Camera].
The idea behind this approach is the following: imagine that your game world
The idea behind this approach is the following: imagine that your game **world**
exists independently from the device, that it exists already in our heads, and
on the sketch, even though we haven't done any coding yet. This world will have
a certain size, and each element in the world will have certain coordinates. It
@ -106,9 +146,9 @@ pixel resolution of the screen.
All elements that are part of the world will be added to the `World` component,
and the `World` component will be then added to the game.
The second part of the overall structure is a camera (`CameraComponent`). The
purpose of the camera is to be able to look at the world, to make sure that it
renders at the right size on the screen of the user's device.
The second part of the overall structure is a **camera** (`CameraComponent`).
The purpose of the camera is to be able to look at the world, to make sure that
it renders at the right size on the screen of the user's device.
Thus, the overall structure of the component tree will look approximately like
this:
@ -122,6 +162,12 @@ KlondikeGame
└─ CameraComponent
```
```{note}
The **Camera** system described in this tutorial is different from the
"official" camera available as a property of the `FlameGame` class. The latter
may become deprecated in the future.
```
For this game I've been drawing my image assets having in mind the dimension of
a single card at 1000×1400 pixels. So, this will serve as the reference size for
determining the overall layout. Another important measurement that affects the
@ -132,134 +178,128 @@ the vertical and horizontal inter-card distance will be the same, and the
minimum padding between the cards and the edges of the screen will also be equal
to `cardGap`.
Alright, let's put all this together and implement our `KlondikeGame` class:
Alright, let's put all this together and implement our `KlondikeGame` class.
First, we declare several global constants which describe the dimensions of a
card and the distance between cards. We declare them as constants because we are
not planning to change these values during the game:
```dart
class KlondikeGame extends FlameGame {
final double cardGap = 175.0;
final double cardWidth = 1000.0;
final double cardHeight = 1400.0;
@override
Future<void> onLoad() async {
await images.load('klondike-sprites.png');
static const double cardWidth = 1000.0;
static const double cardHeight = 1400.0;
static const double cardGap = 175.0;
static const double cardRadius = 100.0;
static final Vector2 cardSize = Vector2(cardWidth, cardHeight);
```
Next, we will create a `Stock` component, the `Waste`, four `Foundation`s and
seven `Pile`s, setting their sizes and positions in the world. The positions
are calculated using simple arithmetics. This should all happen inside the
`onLoad` method, after loading the spritesheet:
```dart
final stock = Stock()
..size = Vector2(cardWidth, cardHeight)
..size = cardSize
..position = Vector2(cardGap, cardGap);
final waste = Waste()
..size = Vector2(cardWidth * 1.5, cardHeight)
..size = cardSize
..position = Vector2(cardWidth + 2 * cardGap, cardGap);
final foundations = List.generate(
4,
(i) => Foundation()
..size = Vector2(cardWidth, cardHeight)
..size = cardSize
..position =
Vector2((i + 3) * (cardWidth + cardGap) + cardGap, cardGap),
);
final piles = List.generate(
7,
(i) => Pile()
..size = Vector2(cardWidth, cardHeight)
..size = cardSize
..position = Vector2(
cardGap + i * (cardWidth + cardGap),
cardHeight + 2 * cardGap,
),
);
```
Then we create the main `World` component, add to it all the components that
we just created, and finally add the `world` to the game.
```dart
final world = World()
..add(stock)
..add(waste)
..addAll(foundations)
..addAll(piles);
add(world);
```
```{note}
You may be wondering when you need to `await` the result of `add()`, and when
you don't. The short answer is: usually you don't need to wait, but if you want
to, then it won't hurt either.
If you check the documentation for `.add()` method, you'll see that the returned
future only waits until the component is finished loading, not until it is
actually mounted to the game. As such, you only have to wait for the future from
`.add()` if your logic requires that the component is fully loaded before it can
proceed. This is not very common.
If you don't `await` the future from `.add()`, then the component will be added
to the game anyways, and in the same amount of time.
```
Lastly, we create a camera object to look at the `world`. Internally, the camera
consists of two parts: a **viewport** and a **viewfinder**. The default viewport
is `MaxViewport`, which takes up the entire available screen size -- this is
exactly what we need for our game, so no need to change anything. The
viewfinder, on the other hand, needs to be set up to properly take the
dimensions of the underlying world into account.
We want the entire card layout to be visible on the screen without the need to
scroll. In order to accomplish this, we specify that we want the entire world
size (which is `7*cardWidth + 8*cardGap` by `4*cardHeight + 3*cardGap`) to be
able to fit into the screen. The `.visibleGameSize` setting ensures that no
matter the size of the device, the zoom level will be adjusted such that the
specified chunk of the game world will be visible.
The game size calculation is obtained like this: there are 7 cards in the
tableau and 6 gaps between them, add 2 more "gaps" to account for padding, and
you get the width of `7*cardWidth + 8*cardGap`. Vertically, there are two rows
of cards, but in the bottom row we need some extra space to be able to display
a tall pile -- by my rough estimate, thrice the height of a card is sufficient
for this -- which gives the total height of the game world as
`4*cardHeight + 3*cardGap`.
Next, we specify which part of the world will be in the "center" of the
viewport. In this case I specify that the "center" of the viewport should
be at the top center of the screen, and the corresponding point within
the game world is at coordinates `[(7*cardWidth + 8*cardGap)/2, 0]`.
The reason for such choice for the viewfinder's position and anchor is
because of how we want it to respond if the game size becomes too wide or
too tall: in case of too wide we want it to be centered on the screen,
but if the screen is too tall, we want the content to be aligned at the
top.
```dart
final camera = CameraComponent(world: world)
..viewfinder.visibleGameSize =
Vector2(cardWidth * 7 + cardGap * 8, 4 * cardHeight + 3 * cardGap)
..viewfinder.position = Vector2(cardWidth * 3.5 + cardGap * 4, 0)
..viewfinder.anchor = Anchor.topCenter;
add(camera);
}
}
```
Let's review what's happening here:
* First, we declare constants `cardWidth`, `cardHeight`, and `cardGap` which
describe the size of a card and the distance between cards.
* Then, there is the `onLoad` method that we have had before. It starts with
loading the main image asset, as before (though we are not using it yet).
* After that, we create components `stock`, `waste`, etc., setting their size
and position in the world. The positions are calculated using simple
arithmetics.
* Then we create the main `World` component, add to it all the components
that we just created, and finally add the `world` to the game.
* Lastly, we create a camera object to look at the `world`. Internally, the
camera consists of two parts: a viewport and a viewfinder. The default
viewport is `MaxViewport`, which takes up the entire available screen size --
this is exactly what we need for our game, so no need to change anything. The
viewfinder, on the other hand, needs to be set up to properly take into
account the dimensions of the underlying world.
- We want the entire card layout to be visible on the screen without the
need to scroll. In order to accomplish this, we specify that we want the
entire world size (which is `7*cardWidth + 8*cardGap` by
`4*cardHeight + 3*cardGap`) to be able to fit into the screen. The
`.visibleGameSize` setting ensures that no matter the size of the device,
the zoom level will be adjusted such that the specified chunk of the game
world will be visible.
+ The game size calculation is obtained like this: there are 7 cards in
the tableau and 6 gaps between them, add 2 more "gaps" to account for
padding, and you get the width of `7*cardWidth + 8*cardGap`.
Vertically, there are two rows of cards, but in the bottom row we
need some extra space to be able to display a tall pile -- by my
rough estimate, thrice the height of a card is sufficient for this --
which gives the total height of the game world as
`4*cardHeight + 3*cardGap`.
- Next, we specify which part of the world will be in the "center" of the
viewport. In this case I specify that the "center" of the viewport should
be at the top center of the screen, and the corresponding point within
the game world is at coordinates `[(7*cardWidth + 8*cardGap)/2, 0]`.
The reason for such choice for the viewfinder's position and anchor is
because of how we want it to respond if the game size becomes too wide or
too tall: in case of too wide we want it to be centered on the screen,
but if the screen is too tall, we want the content to be aligned at the
top.
* As a side note, you may be wondering when you need to `await` the result
of `add()`, and when you don't.
The short answer is: usually you don't need to wait, but if you want to, then
it won't hurt either.
If you check the documentation for `.add()` method, you'll see that the
returned future only waits until the component is finished loading, not until
it is actually mounted to the game. As such, you only have to wait for the
future from `.add()` if your logic requires that the component is fully
loaded before it can proceed. This is not very common.
If you don't `await` the future from `.add()`, then the component will be
added to the game anyways, and in the same amount of time.
If you run the game now, you should see the placeholders for where the various
components will be. If you are running the game in the browser, try resizing the
window and see how the game responds to this.
And this is it with this step -- we've created the basic game structure upon
which everything else will be built. In the next step, we'll learn how to render
the card objects, which are the most important visual objects in this game.
```{flutter-app}
:sources: ../tutorials/klondike/app
:page: step2
:show: popup code
```
And this is it with this step -- we've created the basic game structure upon
which everything else will be built. In the next step, we'll learn how to render
the card objects, which are the most important visual objects in this game.
[World]: ../../flame/camera_component.md#world
[Camera]: ../../flame/camera_component.md#cameracomponent

View File

@ -0,0 +1,479 @@
# Cards
In this chapter we will begin implementing the most visible component in the
game -- the **Card** component, which corresponds to a single real-life card.
There will be 52 `Card` objects in the game.
Each card has a **rank** (from 1 to 13, where 1 is an Ace, and 13 is a King)
and a **suit** (from 0 to 3: hearts ♥, diamonds ♦, clubs ♣, and spades ♠).
Also, each card will have a boolean flag **faceUp**, which controls whether
the card is currently facing up or down. This property is important both for
rendering, and for certain aspects of the gameplay logic.
The rank and the suit are simple properties of a card, they aren't components,
so we need to make a decision on how to represent them. There are several
possibilities: either as a simple `int`, or as an `enum`, or as objects. The
choice will depend on what operations we need to perform with them. For the
rank, we will need to be able to tell whether one rank is one higher/lower than
another rank. Also, we need to produce the text label and a sprite corresponding
to the given rank. For suits, we need to know whether two suits are of different
colors, and also produce a text label and a sprite. Given these requirements,
I decided to represent both `Rank` and `Suit` as classes.
## Suit
Create file `suit.dart` and declare an `@immutable class Suit` there, with no
parent. The `@immutable` annotation here is just a hint for us that the objects
of this class should not be modified after creation.
Next, we define the factory constructor for the class: `Suit.fromInt(i)`. We
use a factory constructor here in order to enforce the singleton pattern for
the class: instead of creating a new object every time, we are returning one
of the pre-built objects that we store in the `_singletons` list:
```dart
factory Suit.fromInt(int index) {
assert(index >= 0 && index <= 3);
return _singletons[index];
}
```
After that, there is a private constructor `Suit._()`. This constructor
initializes the main properties of each `Suit` object: the numeric value, the
string label, and the sprite object which we will later use to draw the suit
symbol on the canvas. The sprite object is initialized using the
`klondikeSprite()` function that we created in the previous chapter:
```dart
Suit._(this.value, this.label, double x, double y, double w, double h)
: sprite = klondikeSprite(x, y, w, h);
final int value;
final String label;
final Sprite sprite;
```
Then comes the static list of all `Suit` objects in the game. Note that we
define it as `late`, meaning that it will be only initialized the first time
it is needed. This is important: as we seen above, the constructor tries to
retrieve an image from the global cache, so it can only be invoked after the
image is loaded into the cache.
```dart
static late final List<Suit> _singletons = [
Suit._(0, '♥', 1176, 17, 172, 183),
Suit._(1, '♦', 973, 14, 177, 182),
Suit._(2, '♣', 974, 226, 184, 172),
Suit._(3, '♠', 1178, 220, 176, 182),
];
```
The last four numbers in the constructor are the coordinates of the sprite
image within the spritesheet `klondike-sprites.png`. If you're wondering how I
obtained these numbers, the answer is that I used a free online service
[spritecow.com] -- it's a handy tool for locating sprites within a spritesheet.
Lastly, I have simple getters to determine the "color" of a suit. This will be
needed later when we need to enforce the rule that cards can only be placed
into columns by alternating colors.
```dart
/// Hearts and Diamonds are red, while Clubs and Spades are black.
bool get isRed => value <= 1;
bool get isBlack => value >= 2;
```
## Rank
The `Rank` class is very similar to `Suit`. The main difference is that `Rank`
contains two sprites instead of one, separately for ranks of "red" and "black"
colors. The full code for the `Rank` class is as follows:
```dart
import 'package:flame/components.dart';
import 'package:flame/flame.dart';
import 'package:flutter/foundation.dart';
@immutable
class Rank {
factory Rank.of(int value) {
assert(value >= 1 && value <= 13);
return _singletons[value - 1];
}
Rank._(
this.value,
this.label,
double x1,
double y1,
double x2,
double y2,
double w,
double h,
) : redSprite = klondikeSprite(x1, y1, w, h),
blackSprite = klondikeSprite(x2, y2, w, h);
final int value;
final String label;
final Sprite redSprite;
final Sprite blackSprite;
static late final List<Rank> _singletons = [
Rank._(1, 'A', 335, 164, 789, 161, 120, 129),
Rank._(2, '2', 20, 19, 15, 322, 83, 125),
Rank._(3, '3', 122, 19, 117, 322, 80, 127),
Rank._(4, '4', 213, 12, 208, 315, 93, 132),
Rank._(5, '5', 314, 21, 309, 324, 85, 125),
Rank._(6, '6', 419, 17, 414, 320, 84, 129),
Rank._(7, '7', 509, 21, 505, 324, 92, 128),
Rank._(8, '8', 612, 19, 607, 322, 78, 127),
Rank._(9, '9', 709, 19, 704, 322, 84, 130),
Rank._(10, '10', 810, 20, 805, 322, 137, 127),
Rank._(11, 'J', 15, 170, 469, 167, 56, 126),
Rank._(12, 'Q', 92, 168, 547, 165, 132, 128),
Rank._(13, 'K', 243, 170, 696, 167, 92, 123),
];
}
```
## Card component
Now that we have the `Rank` and the `Suit` classes, we can finally start
implementing the **Card** component. Create file `components/card.dart` and
declare the `Card` class extending from the `PositionComponent`:
```dart
class Card extends PositionComponent {}
```
The constructor of the class will take integer rank and suit, and make the
card initially facing down. Also, we initialize the size of the component to
be equal to the `cardSize` constant defined in the `KlondikeGame` class:
```dart
Card(int intRank, int intSuit)
: rank = Rank.fromInt(intRank),
suit = Suit.fromInt(intSuit),
_faceUp = false,
super(size: KlondikeGame.cardSize);
final Rank rank;
final Suit suit;
bool _faceUp;
```
The `_faceUp` property is private (indicated by the underscore) and non-final,
meaning that it can change during the lifetime of a card. We should create some
public accessors and mutators for this variable:
```dart
bool get isFaceUp => _faceUp;
void flip() => _faceUp = !_faceUp;
```
Lastly, let's add a simple `toString()` implementation, which may turn out to
be useful when we need to debug the game:
```dart
@override
String toString() => rank.label + suit.label; // e.g. "Q♠" or "10♦"
```
Before we proceed with implementing the rendering, we need to add some cards
into the game. Head over to the `KlondikeGame` class and add the following at
the bottom of the `onLoad` method:
```dart
final random = Random();
for (var i = 0; i < 7; i++) {
for (var j = 0; j < 4; j++) {
final card = Card(random.nextInt(13) + 1, random.nextInt(4))
..position = Vector2(100 + i * 1150, 100 + j * 1500)
..addToParent(world);
if (random.nextDouble() < 0.9) { // flip face up with 90% probability
card.flip();
}
}
}
```
This snippet is a temporary code -- we will remove it in the next chapter --
but for now it lays down 28 random cards on the table, most of them facing up.
### Rendering
In order to be able to see a card, we need to implement its `render()` method.
Since the card has two distinct states -- face up or down -- we will
implement rendering for these two states separately. Add the following methods
into the `Card` class:
```dart
@override
void render(Canvas canvas) {
if (_faceUp) {
_renderFront(canvas);
} else {
_renderBack(canvas);
}
}
void _renderFront(Canvas canvas) {}
void _renderBack(Canvas canvas) {}
```
### renderBack()
Since rendering the back of a card is simpler, we will do it first.
The `render()` method of a `PositionComponent` operates in a local coordinate
system, which means we don't need to worry about where the card is located on
the screen. This local coordinate system has the origin at the top-left corner
of the component, and extends to the right by `width` and down by `height`
pixels.
There is a lot of artistic freedom in how to draw the back of a card, but my
implementation contains a solid background, a border, a flame logo in the
middle, and another decorative border:
```dart
void _renderBack(Canvas canvas) {
canvas.drawRRect(cardRRect, backBackgroundPaint);
canvas.drawRRect(cardRRect, backBorderPaint1);
canvas.drawRRect(backRRectInner, backBorderPaint2);
flameSprite.render(canvas, position: size / 2, anchor: Anchor.center);
}
```
The most interesting part here is the rendering of a sprite: we want to
render it in the middle (`size/2`), and we use `Anchor.center` to tell the
engine that we want the _center_ of the sprite to be at that point.
Various properties used in the `_renderBack()` method are defined as follows:
```dart
static final Paint backBackgroundPaint = Paint()
..color = const Color(0xff380c02);
static final Paint backBorderPaint1 = Paint()
..color = const Color(0xffdbaf58)
..style = PaintingStyle.stroke
..strokeWidth = 10;
static final Paint backBorderPaint2 = Paint()
..color = const Color(0x5CEF971B)
..style = PaintingStyle.stroke
..strokeWidth = 35;
static final RRect cardRRect = RRect.fromRectAndRadius(
KlondikeGame.cardSize.toRect(),
const Radius.circular(KlondikeGame.cardRadius),
);
static final RRect backRRectInner = cardRRect.deflate(40);
static late final Sprite flameSprite = klondikeSprite(1367, 6, 357, 501);
```
I declared these properties as static because they will all be the same across
all 52 card objects, so we might as well save some resources by having them
initialized only once.
### renderFront()
When rendering the face of a card, we will follow the standard card design: the
rank and the suit in two opposite corners, plus the number of pips equal to the
rank value. The court cards (jack, queen, king) will have special images in the
center.
As before, we begin by declaring some constants that will be used for rendering.
The background of a card will be black, whereas the border will be different
depending on whether the card is of a "red" suit or "black":
```dart
static final Paint frontBackgroundPaint = Paint()
..color = const Color(0xff000000);
static final Paint redBorderPaint = Paint()
..color = const Color(0xffece8a3)
..style = PaintingStyle.stroke
..strokeWidth = 10;
static final Paint blackBorderPaint = Paint()
..color = const Color(0xff7ab2e8)
..style = PaintingStyle.stroke
..strokeWidth = 10;
```
Next, we also need the images for the court cards:
```dart
static late final Sprite redJack = klondikeSprite(81, 565, 562, 488);
static late final Sprite redQueen = klondikeSprite(717, 541, 486, 515);
static late final Sprite redKing = klondikeSprite(1305, 532, 407, 549);
```
Note that I'm calling these sprites `redJack`, `redQueen`, and `redKing`. This
is because, after some trial, I found that the images that I have don't look
very well on black-suit cards. So what I decided to do is to take these images
and _tint_ them with a blueish hue. Tinting of a sprite can be achieved by
using a paint with `colorFilter` set to the specified color and the `srcATop`
blending mode:
```dart
static final blueFilter = Paint()
..colorFilter = const ColorFilter.mode(
Color(0x880d8bff),
BlendMode.srcATop,
);
static late final Sprite blackJack = klondikeSprite(81, 565, 562, 488)
..paint = blueFilter;
static late final Sprite blackQueen = klondikeSprite(717, 541, 486, 515)
..paint = blueFilter;
static late final Sprite blackKing = klondikeSprite(1305, 532, 407, 549)
..paint = blueFilter;
```
Now we can start coding the render method itself. First, draw the background
and the card border:
```dart
void _renderFront(Canvas canvas) {
canvas.drawRRect(cardRRect, frontBackgroundPaint);
canvas.drawRRect(
cardRRect,
suit.isRed ? redBorderPaint : blackBorderPaint,
);
}
```
In order to draw the rest of the card, I need one more helper method. This
method will draw the provided sprite on the canvas at the specified place (the
location is relative to the dimensions of the card). The sprite can be
optionally scaled. In addition, if flag `rotate=true` is passed, the sprite
will be drawn as if it was rotated 180º around the center of the card:
```dart
void _drawSprite(
Canvas canvas,
Sprite sprite,
double relativeX,
double relativeY, {
double scale = 1,
bool rotate = false,
}) {
if (rotate) {
canvas.save();
canvas.translate(size.x / 2, size.y / 2);
canvas.rotate(pi);
canvas.translate(-size.x / 2, -size.y / 2);
}
sprite.render(
canvas,
position: Vector2(relativeX * size.x, relativeY * size.y),
anchor: Anchor.center,
size: sprite.srcSize.scaled(scale),
);
if (rotate) {
canvas.restore();
}
}
```
Let's draw the rank and the suit symbols in the corners of the card. Add the
following to the `_renderFront()` method:
```dart
final rankSprite = suit.isBlack ? rank.blackSprite : rank.redSprite;
final suitSprite = suit.sprite;
_drawSprite(canvas, rankSprite, 0.1, 0.08);
_drawSprite(canvas, rankSprite, 0.1, 0.08, rotate: true);
_drawSprite(canvas, suitSprite, 0.1, 0.18, scale: 0.5);
_drawSprite(canvas, suitSprite, 0.1, 0.18, scale: 0.5, rotate: true);
```
The middle of the card is rendered in the same manner: we will create a big
switch statement on the card's rank, and draw pips accordingly. The code
below may seem long, but it is actually quite repetitive and consists only
of drawing various sprites in different places on the card's face:
```dart
switch (rank.value) {
case 1:
_drawSprite(canvas, suitSprite, 0.5, 0.5, scale: 2.5);
break;
case 2:
_drawSprite(canvas, suitSprite, 0.5, 0.25);
_drawSprite(canvas, suitSprite, 0.5, 0.25, rotate: true);
break;
case 3:
_drawSprite(canvas, suitSprite, 0.5, 0.2);
_drawSprite(canvas, suitSprite, 0.5, 0.5);
_drawSprite(canvas, suitSprite, 0.5, 0.2, rotate: true);
break;
case 4:
_drawSprite(canvas, suitSprite, 0.3, 0.25);
_drawSprite(canvas, suitSprite, 0.7, 0.25);
_drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);
break;
case 5:
_drawSprite(canvas, suitSprite, 0.3, 0.25);
_drawSprite(canvas, suitSprite, 0.7, 0.25);
_drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);
_drawSprite(canvas, suitSprite, 0.5, 0.5);
break;
case 6:
_drawSprite(canvas, suitSprite, 0.3, 0.25);
_drawSprite(canvas, suitSprite, 0.7, 0.25);
_drawSprite(canvas, suitSprite, 0.3, 0.5);
_drawSprite(canvas, suitSprite, 0.7, 0.5);
_drawSprite(canvas, suitSprite, 0.3, 0.25, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.25, rotate: true);
break;
case 7:
_drawSprite(canvas, suitSprite, 0.3, 0.2);
_drawSprite(canvas, suitSprite, 0.7, 0.2);
_drawSprite(canvas, suitSprite, 0.5, 0.35);
_drawSprite(canvas, suitSprite, 0.3, 0.5);
_drawSprite(canvas, suitSprite, 0.7, 0.5);
_drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);
break;
case 8:
_drawSprite(canvas, suitSprite, 0.3, 0.2);
_drawSprite(canvas, suitSprite, 0.7, 0.2);
_drawSprite(canvas, suitSprite, 0.5, 0.35);
_drawSprite(canvas, suitSprite, 0.3, 0.5);
_drawSprite(canvas, suitSprite, 0.7, 0.5);
_drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);
_drawSprite(canvas, suitSprite, 0.5, 0.35, rotate: true);
break;
case 9:
_drawSprite(canvas, suitSprite, 0.3, 0.2);
_drawSprite(canvas, suitSprite, 0.7, 0.2);
_drawSprite(canvas, suitSprite, 0.5, 0.3);
_drawSprite(canvas, suitSprite, 0.3, 0.4);
_drawSprite(canvas, suitSprite, 0.7, 0.4);
_drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);
_drawSprite(canvas, suitSprite, 0.3, 0.4, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.4, rotate: true);
break;
case 10:
_drawSprite(canvas, suitSprite, 0.3, 0.2);
_drawSprite(canvas, suitSprite, 0.7, 0.2);
_drawSprite(canvas, suitSprite, 0.5, 0.3);
_drawSprite(canvas, suitSprite, 0.3, 0.4);
_drawSprite(canvas, suitSprite, 0.7, 0.4);
_drawSprite(canvas, suitSprite, 0.3, 0.2, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.2, rotate: true);
_drawSprite(canvas, suitSprite, 0.5, 0.3, rotate: true);
_drawSprite(canvas, suitSprite, 0.3, 0.4, rotate: true);
_drawSprite(canvas, suitSprite, 0.7, 0.4, rotate: true);
break;
case 11:
_drawSprite(canvas, suit.isRed? redJack : blackJack, 0.5, 0.5);
break;
case 12:
_drawSprite(canvas, suit.isRed? redQueen : blackQueen, 0.5, 0.5);
break;
case 13:
_drawSprite(canvas, suit.isRed? redKing : blackKing, 0.5, 0.5);
break;
}
```
And this is it with the rendering of the `Card` component. If you run the code
now, you would see four rows of cards neatly spread on the table. Refreshing
the page will lay down a new set of cards. Remember that we have laid these
cards in this way only temporarily, in order to be able to check that rendering
works properly.
In the next chapter we will discuss how to implement interactions with the
cards, that is, how to make them draggable and tappable.
```{flutter-app}
:sources: ../tutorials/klondike/app
:page: step3
:show: popup code
```
[spritecow.com]: http://www.spritecow.com/

View File

@ -0,0 +1,3 @@
# To be continued
This tutorial is not finished yet, stay tuned for the updates.