mirror of
https://github.com/flame-engine/flame.git
synced 2025-10-30 08:27:36 +08:00
docs: Tutorial for making a Klondike card game, steps 1 & 2 (#1477)
This commit is contained in:
@ -50,12 +50,14 @@ button.flutter-app-button:after {
|
||||
|
||||
#flutter-app-overlay.active iframe {
|
||||
border: none;
|
||||
box-shadow: 0px 0px 9px 3px black;
|
||||
box-shadow: 0px 0px 30px 0px #ffffff26;
|
||||
display: none;
|
||||
height: 80vh;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 80vw;
|
||||
}
|
||||
|
||||
#flutter-app-overlay.active iframe.active {
|
||||
@ -77,6 +79,7 @@ button.flutter-app-button:after {
|
||||
text-align: center;
|
||||
top: 10%;
|
||||
width: 30px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#flutter-app-close-button:hover {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from docutils import nodes
|
||||
@ -73,14 +74,14 @@ class FlutterAppDirective(SphinxDirective):
|
||||
self._process_show_option()
|
||||
self._process_sources_option()
|
||||
self.source_build_dir = os.path.join(self.source_dir, 'build', 'web')
|
||||
self.app_name = os.path.basename(self.source_dir)
|
||||
self.app_name = self._get_app_name()
|
||||
self.html_dir = '_static/apps/' + self.app_name
|
||||
self.target_dir = os.path.abspath(
|
||||
os.path.join('..', '_build', 'html', self.html_dir))
|
||||
self._ensure_compiled()
|
||||
|
||||
page = self.options.get('page', '')
|
||||
iframe_url = self.html_dir + '/index.html?' + page
|
||||
iframe_url = '/' + self.html_dir + '/index.html?' + page
|
||||
result = []
|
||||
if 'popup' in self.modes:
|
||||
result.append(Button(
|
||||
@ -117,6 +118,10 @@ class FlutterAppDirective(SphinxDirective):
|
||||
assert not abspath.endswith('/')
|
||||
self.source_dir = abspath
|
||||
|
||||
def _get_app_name(self):
|
||||
src = os.path.relpath(self.source_dir)
|
||||
return '-'.join(word for word in re.split(r'\W', src) if word)
|
||||
|
||||
def _ensure_compiled(self):
|
||||
need_compiling = (
|
||||
('popup' in self.modes or 'widget' in self.modes) and
|
||||
@ -143,8 +148,8 @@ class FlutterAppDirective(SphinxDirective):
|
||||
check=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
cmd = e.cmd.join(' ')
|
||||
raise self.severe(
|
||||
cmd = ' '.join(e.cmd)
|
||||
raise self.error(
|
||||
f'Command `{cmd}` returned with exit status {e.returncode}\n' +
|
||||
e.output.decode('utf-8'),
|
||||
)
|
||||
|
||||
@ -71,6 +71,14 @@ div.expander {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
p + ul {
|
||||
margin-top: -0.5em;
|
||||
}
|
||||
|
||||
ul ul.simple p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
|
||||
/*----------------------------------------------------------------------------*
|
||||
* Top navigation bar
|
||||
@ -587,6 +595,7 @@ pre {
|
||||
border-radius: 0.4em;
|
||||
box-shadow: 0px 0px 3px black;
|
||||
color: #888;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 85%;
|
||||
line-height: 125%;
|
||||
margin: 0;
|
||||
|
||||
BIN
doc/images/tutorials/klondike-sketch.webp
Normal file
BIN
doc/images/tutorials/klondike-sketch.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
1
doc/tutorials/klondike/app/analysis_options.yaml
Normal file
1
doc/tutorials/klondike/app/analysis_options.yaml
Normal file
@ -0,0 +1 @@
|
||||
include: package:flame_lint/analysis_options.yaml
|
||||
BIN
doc/tutorials/klondike/app/assets/images/klondike-sprites.png
Normal file
BIN
doc/tutorials/klondike/app/assets/images/klondike-sprites.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 468 KiB |
23
doc/tutorials/klondike/app/lib/main.dart
Normal file
23
doc/tutorials/klondike/app/lib/main.dart
Normal file
@ -0,0 +1,23 @@
|
||||
import 'dart:html'; // ignore: avoid_web_libraries_in_flutter
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'step2/main.dart' as step2;
|
||||
|
||||
void main() {
|
||||
var page = window.location.search ?? '';
|
||||
if (page.startsWith('?')) {
|
||||
page = page.substring(1);
|
||||
}
|
||||
switch (page) {
|
||||
case 'step2':
|
||||
step2.main();
|
||||
break;
|
||||
|
||||
default:
|
||||
runApp(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Text('Error: unknown page name "$page"'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
import 'package:flame/components.dart';
|
||||
|
||||
class Foundation extends PositionComponent {
|
||||
@override
|
||||
bool get debugMode => true;
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
import 'package:flame/components.dart';
|
||||
|
||||
class Pile extends PositionComponent {
|
||||
@override
|
||||
bool get debugMode => true;
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
import 'package:flame/components.dart';
|
||||
|
||||
class Stock extends PositionComponent {
|
||||
@override
|
||||
bool get debugMode => true;
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
import 'package:flame/components.dart';
|
||||
|
||||
class Waste extends PositionComponent {
|
||||
@override
|
||||
bool get debugMode => true;
|
||||
}
|
||||
56
doc/tutorials/klondike/app/lib/step2/klondike_game.dart
Normal file
56
doc/tutorials/klondike/app/lib/step2/klondike_game.dart
Normal file
@ -0,0 +1,56 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/experimental.dart';
|
||||
import 'package:flame/game.dart';
|
||||
|
||||
import 'components/foundation.dart';
|
||||
import 'components/pile.dart';
|
||||
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;
|
||||
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await images.load('klondike-sprites.png');
|
||||
|
||||
final stock = Stock()
|
||||
..size = Vector2(cardWidth, cardHeight)
|
||||
..position = Vector2(cardGap, cardGap);
|
||||
final waste = Waste()
|
||||
..size = Vector2(cardWidth * 1.5, cardHeight)
|
||||
..position = Vector2(cardWidth + 2 * cardGap, cardGap);
|
||||
final foundations = List.generate(
|
||||
4,
|
||||
(i) => Foundation()
|
||||
..size = Vector2(cardWidth, cardHeight)
|
||||
..position =
|
||||
Vector2((i + 3) * (cardWidth + cardGap) + cardGap, cardGap),
|
||||
);
|
||||
final piles = List.generate(
|
||||
7,
|
||||
(i) => Pile()
|
||||
..size = Vector2(cardWidth, cardHeight)
|
||||
..position = Vector2(
|
||||
cardGap + i * (cardWidth + cardGap),
|
||||
cardHeight + 2 * cardGap,
|
||||
),
|
||||
);
|
||||
|
||||
final world = World()
|
||||
..add(stock)
|
||||
..add(waste)
|
||||
..addAll(foundations)
|
||||
..addAll(piles);
|
||||
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);
|
||||
}
|
||||
}
|
||||
9
doc/tutorials/klondike/app/lib/step2/main.dart
Normal file
9
doc/tutorials/klondike/app/lib/step2/main.dart
Normal 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));
|
||||
}
|
||||
17
doc/tutorials/klondike/app/pubspec.yaml
Normal file
17
doc/tutorials/klondike/app/pubspec.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
name: klondike
|
||||
description: Klondike tutorial
|
||||
version: 1.0.0
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ^2.15.0
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flame:
|
||||
path: ../../../../packages/flame
|
||||
|
||||
flutter:
|
||||
assets:
|
||||
- assets/images/
|
||||
26
doc/tutorials/klondike/app/web/index.html
Normal file
26
doc/tutorials/klondike/app/web/index.html
Normal file
@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="Klondike tutorial">
|
||||
<base href="$FLUTTER_BASE_HREF">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="Klondike">
|
||||
|
||||
<title>Klondike</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('flutter_service_worker.js');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script src="main.dart.js" type="application/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
18
doc/tutorials/klondike/klondike.md
Normal file
18
doc/tutorials/klondike/klondike.md
Normal file
@ -0,0 +1,18 @@
|
||||
# Klondike game tutorial
|
||||
|
||||
[Klondike] is a popular solitaire card game. In this tutorial we will follow the step-by-step
|
||||
process for coding this game using the Flame engine.
|
||||
|
||||
This tutorial assumes that you have at least some familiarity with common programming concepts, and
|
||||
with the [Dart] programming language.
|
||||
|
||||
|
||||
[Dart]: https://dart.dev/overview
|
||||
[Klondike]: https://en.wikipedia.org/wiki/Klondike_(solitaire)
|
||||
|
||||
```{toctree}
|
||||
:hidden:
|
||||
|
||||
1. Preparation <step1.md>
|
||||
2. Scaffolding <step2.md>
|
||||
```
|
||||
108
doc/tutorials/klondike/step1.md
Normal file
108
doc/tutorials/klondike/step1.md
Normal file
@ -0,0 +1,108 @@
|
||||
# 1. Preparation
|
||||
|
||||
Before you begin any kind of game project, you need to give it a **name**. For
|
||||
this tutorial the name will be simply `klondike`.
|
||||
|
||||
Having this name in mind, please head over to the [](../bare_flame_game.md)
|
||||
tutorial and complete the necessary set up steps. When you come back, you should
|
||||
already have the `main.dart` file with the following content:
|
||||
|
||||
```dart
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
void main() {
|
||||
final game = FlameGame();
|
||||
runApp(GameWidget(game: game));
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Planning
|
||||
|
||||
The start of any project usually feels overwhelming. Where even to begin?
|
||||
I always find it useful to create a rough sketch of what I am about to code,
|
||||
so that it can serve as a reference point. My sketch for the Klondike game is
|
||||
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.
|
||||
|
||||
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
|
||||
`Waste` class, a `Tableau` containing seven `Pile`s, and 4 `Foundation`s. There
|
||||
may also be a `Deck`. All of these components will be tied together via the
|
||||
`KlondikeGame` derived from the `FlameGame`.
|
||||
|
||||
|
||||
## Assets
|
||||
|
||||
Another important aspect in any game development is the game's assets. These
|
||||
includes images, sprites, animations, sounds, textures, data files, and so on.
|
||||
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.
|
||||
|
||||
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):
|
||||
|
||||

|
||||
|
||||
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
|
||||
(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
|
||||
```
|
||||
|
||||
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
|
||||
extracted from a single source image can be faster too, since Flutter will
|
||||
optimize multiple such drawing commands into a single `drawAtlas` command.
|
||||
|
||||
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
|
||||
these, but images are much easier to position precisely.
|
||||
- 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
|
||||
tired drawing these.
|
||||
|
||||
Also, you need to tell Flutter about this image (just having it inside the
|
||||
`assets` folder is not enough). In order to do this, let's add the following
|
||||
lines into the `pubspec.yaml` file:
|
||||
|
||||
```yaml
|
||||
flutter:
|
||||
assets:
|
||||
- assets/images/
|
||||
```
|
||||
|
||||
Alright, enough with preparing -- onward to coding!
|
||||
|
||||
|
||||
[standard terminology]: https://en.wikipedia.org/wiki/Solitaire_terminology
|
||||
265
doc/tutorials/klondike/step2.md
Normal file
265
doc/tutorials/klondike/step2.md
Normal file
@ -0,0 +1,265 @@
|
||||
# 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.
|
||||
|
||||
|
||||
## 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
|
||||
comprise the game (the component tree), and usually also serves as the central
|
||||
repository for the game's state.
|
||||
|
||||
So, create a new file called `klondike_game.dart` inside the `lib/` folder, and
|
||||
declare the `KlondikeGame` class inside:
|
||||
|
||||
```dart
|
||||
import 'package:flame/game.dart';
|
||||
|
||||
class KlondikeGame extends FlameGame {
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
await Images.load('klondike-sprites.png');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For now we only declared the `onLoad` method, which is a special handler that
|
||||
is called when the game instance is attached to the Flutter widget tree for the
|
||||
first time. You can think of it as a delayed asynchronous constructor.
|
||||
Currently, the only thing that `onLoad` does is that it loads the sprites image
|
||||
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.
|
||||
|
||||
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.
|
||||
After all is done, the file should look like this:
|
||||
|
||||
```dart
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'klondike_game.dart';
|
||||
|
||||
void main() {
|
||||
final game = KlondikeGame();
|
||||
runApp(GameWidget(game: game));
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Other classes
|
||||
|
||||
So far we have the main `KlondikeGame` class, and now we need to create objects
|
||||
that we will add to the game. In Flame these objects are called _components_,
|
||||
and when added to the game they form a "game component tree". All entities that
|
||||
exist in the game must be components.
|
||||
|
||||
As we already mentioned in the previous chapter, our game mainly consists of
|
||||
`Card` components. However, since drawing the cards will take some effort, we
|
||||
will defer implementation of that class to the next chapter.
|
||||
|
||||
For now, let's create the container classes, as shown on the sketch. These are:
|
||||
`Stock`, `Waste`, `Pile` and `Foundation`. In your project directory create a
|
||||
sub-directory `components`, and then the file `components/stock.dart`. In that
|
||||
file write
|
||||
|
||||
```dart
|
||||
import 'package:flame/components.dart';
|
||||
|
||||
class Stock extends PositionComponent {
|
||||
bool get debugMode => true;
|
||||
}
|
||||
```
|
||||
|
||||
Here we declare the `Stock` class as a `PositionComponent` (which is a component
|
||||
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.
|
||||
|
||||
|
||||
## Game structure
|
||||
|
||||
Once we have some basic components, they need to be added to the game. It is
|
||||
time to make a decision about the high-level structure of the game.
|
||||
|
||||
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
|
||||
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
|
||||
is up to us to decide what will be the size of the world, and what is the unit
|
||||
of measurement for that size. The important part is that the world exists
|
||||
independently from the device, and its dimensions likewise do not depend on the
|
||||
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.
|
||||
|
||||
Thus, the overall structure of the component tree will look approximately like
|
||||
this:
|
||||
```text
|
||||
KlondikeGame
|
||||
├─ World
|
||||
│ ├─ Stock
|
||||
│ ├─ Waste
|
||||
│ ├─ Foundation (×4)
|
||||
│ └─ Pile (×7)
|
||||
└─ CameraComponent
|
||||
```
|
||||
|
||||
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
|
||||
layout is the inter-card distance. It seems like it should be somewhere between
|
||||
150 to 200 units (relative to the card width), so we will declare it as a
|
||||
variable `cardGap` that can be adjusted later if needed. For simplicity, both
|
||||
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:
|
||||
|
||||
```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');
|
||||
|
||||
final stock = Stock()
|
||||
..size = Vector2(cardWidth, cardHeight)
|
||||
..position = Vector2(cardGap, cardGap);
|
||||
final waste = Waste()
|
||||
..size = Vector2(cardWidth * 1.5, cardHeight)
|
||||
..position = Vector2(cardWidth + 2 * cardGap, cardGap);
|
||||
final foundations = List.generate(
|
||||
4,
|
||||
(i) => Foundation()
|
||||
..size = Vector2(cardWidth, cardHeight)
|
||||
..position =
|
||||
Vector2((i + 3) * (cardWidth + cardGap) + cardGap, cardGap),
|
||||
);
|
||||
final piles = List.generate(
|
||||
7,
|
||||
(i) => Pile()
|
||||
..size = Vector2(cardWidth, cardHeight)
|
||||
..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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
[World]: ../../flame/camera_component.md#world
|
||||
[Camera]: ../../flame/camera_component.md#cameracomponent
|
||||
@ -6,8 +6,12 @@ This chapter contains only one tutorial for now, but we'll be adding more soon!
|
||||
for making a new Flame game. This "initial state" is assumed as a starting
|
||||
point for all other tutorials.
|
||||
|
||||
- [](klondike/klondike.md) -- in this tutorial we will build the Klondike
|
||||
solitaire card game.
|
||||
|
||||
```{toctree}
|
||||
:hidden:
|
||||
|
||||
Bare Flame game <bare_flame_game.md>
|
||||
Klondike <klondike/klondike.md>
|
||||
```
|
||||
|
||||
@ -5,6 +5,7 @@ packages:
|
||||
- packages/**
|
||||
- examples/**
|
||||
- tutorials/**
|
||||
- doc/**
|
||||
|
||||
command:
|
||||
version:
|
||||
|
||||
@ -102,7 +102,7 @@ class Viewfinder extends Component implements PositionProvider {
|
||||
|
||||
/// Set [zoom] level based on the [_visibleGameSize].
|
||||
void _initZoom() {
|
||||
if (isMounted && _visibleGameSize != null) {
|
||||
if (parent != null && _visibleGameSize != null) {
|
||||
final viewportSize = camera.viewport.size;
|
||||
final zoomX = viewportSize.x / _visibleGameSize!.x;
|
||||
final zoomY = viewportSize.y / _visibleGameSize!.y;
|
||||
|
||||
Reference in New Issue
Block a user