mirror of
				https://github.com/flame-engine/flame.git
				synced 2025-11-01 01:18:38 +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 { | #flutter-app-overlay.active iframe { | ||||||
|   border: none; |   border: none; | ||||||
|   box-shadow: 0px 0px 9px 3px black; |   box-shadow: 0px 0px 30px 0px #ffffff26; | ||||||
|   display: none; |   display: none; | ||||||
|  |   height: 80vh; | ||||||
|   left: 50%; |   left: 50%; | ||||||
|   position: absolute; |   position: absolute; | ||||||
|   top: 50%; |   top: 50%; | ||||||
|   transform: translate(-50%, -50%); |   transform: translate(-50%, -50%); | ||||||
|  |   width: 80vw; | ||||||
| } | } | ||||||
|  |  | ||||||
| #flutter-app-overlay.active iframe.active { | #flutter-app-overlay.active iframe.active { | ||||||
| @ -77,6 +79,7 @@ button.flutter-app-button:after { | |||||||
|   text-align: center; |   text-align: center; | ||||||
|   top: 10%; |   top: 10%; | ||||||
|   width: 30px; |   width: 30px; | ||||||
|  |   z-index: 1; | ||||||
| } | } | ||||||
|  |  | ||||||
| #flutter-app-close-button:hover { | #flutter-app-close-button:hover { | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| #!/usr/bin/env python | #!/usr/bin/env python | ||||||
| import os | import os | ||||||
|  | import re | ||||||
| import shutil | import shutil | ||||||
| import subprocess | import subprocess | ||||||
| from docutils import nodes | from docutils import nodes | ||||||
| @ -73,14 +74,14 @@ class FlutterAppDirective(SphinxDirective): | |||||||
|         self._process_show_option() |         self._process_show_option() | ||||||
|         self._process_sources_option() |         self._process_sources_option() | ||||||
|         self.source_build_dir = os.path.join(self.source_dir, 'build', 'web') |         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.html_dir = '_static/apps/' + self.app_name | ||||||
|         self.target_dir = os.path.abspath( |         self.target_dir = os.path.abspath( | ||||||
|             os.path.join('..', '_build', 'html', self.html_dir)) |             os.path.join('..', '_build', 'html', self.html_dir)) | ||||||
|         self._ensure_compiled() |         self._ensure_compiled() | ||||||
|  |  | ||||||
|         page = self.options.get('page', '') |         page = self.options.get('page', '') | ||||||
|         iframe_url = self.html_dir + '/index.html?' + page |         iframe_url = '/' + self.html_dir + '/index.html?' + page | ||||||
|         result = [] |         result = [] | ||||||
|         if 'popup' in self.modes: |         if 'popup' in self.modes: | ||||||
|             result.append(Button( |             result.append(Button( | ||||||
| @ -117,6 +118,10 @@ class FlutterAppDirective(SphinxDirective): | |||||||
|         assert not abspath.endswith('/') |         assert not abspath.endswith('/') | ||||||
|         self.source_dir = abspath |         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): |     def _ensure_compiled(self): | ||||||
|         need_compiling = ( |         need_compiling = ( | ||||||
|             ('popup' in self.modes or 'widget' in self.modes) and |             ('popup' in self.modes or 'widget' in self.modes) and | ||||||
| @ -143,8 +148,8 @@ class FlutterAppDirective(SphinxDirective): | |||||||
|                 check=True, |                 check=True, | ||||||
|             ) |             ) | ||||||
|         except subprocess.CalledProcessError as e: |         except subprocess.CalledProcessError as e: | ||||||
|             cmd = e.cmd.join(' ') |             cmd = ' '.join(e.cmd) | ||||||
|             raise self.severe( |             raise self.error( | ||||||
|                 f'Command `{cmd}` returned with exit status {e.returncode}\n' + |                 f'Command `{cmd}` returned with exit status {e.returncode}\n' + | ||||||
|                 e.output.decode('utf-8'), |                 e.output.decode('utf-8'), | ||||||
|             ) |             ) | ||||||
|  | |||||||
| @ -71,6 +71,14 @@ div.expander { | |||||||
|   flex: 1; |   flex: 1; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | p + ul { | ||||||
|  |   margin-top: -0.5em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ul ul.simple p { | ||||||
|  |   margin-bottom: 1em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
| /*----------------------------------------------------------------------------* | /*----------------------------------------------------------------------------* | ||||||
|  * Top navigation bar |  * Top navigation bar | ||||||
| @ -587,6 +595,7 @@ pre { | |||||||
|   border-radius: 0.4em; |   border-radius: 0.4em; | ||||||
|   box-shadow: 0px 0px 3px black; |   box-shadow: 0px 0px 3px black; | ||||||
|   color: #888; |   color: #888; | ||||||
|  |   font-family: var(--font-mono); | ||||||
|   font-size: 85%; |   font-size: 85%; | ||||||
|   line-height: 125%; |   line-height: 125%; | ||||||
|   margin: 0; |   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 |   for making a new Flame game. This "initial state" is assumed as a starting | ||||||
|   point for all other tutorials. |   point for all other tutorials. | ||||||
|  |  | ||||||
|  | - [](klondike/klondike.md) -- in this tutorial we will build the Klondike | ||||||
|  |   solitaire card game. | ||||||
|  |  | ||||||
| ```{toctree} | ```{toctree} | ||||||
| :hidden: | :hidden: | ||||||
|  |  | ||||||
| Bare Flame game   <bare_flame_game.md> | Bare Flame game   <bare_flame_game.md> | ||||||
|  | Klondike          <klondike/klondike.md> | ||||||
| ``` | ``` | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ packages: | |||||||
|   - packages/** |   - packages/** | ||||||
|   - examples/** |   - examples/** | ||||||
|   - tutorials/** |   - tutorials/** | ||||||
|  |   - doc/** | ||||||
|  |  | ||||||
| command: | command: | ||||||
|   version: |   version: | ||||||
|  | |||||||
| @ -102,7 +102,7 @@ class Viewfinder extends Component implements PositionProvider { | |||||||
|  |  | ||||||
|   /// Set [zoom] level based on the [_visibleGameSize]. |   /// Set [zoom] level based on the [_visibleGameSize]. | ||||||
|   void _initZoom() { |   void _initZoom() { | ||||||
|     if (isMounted && _visibleGameSize != null) { |     if (parent != null && _visibleGameSize != null) { | ||||||
|       final viewportSize = camera.viewport.size; |       final viewportSize = camera.viewport.size; | ||||||
|       final zoomX = viewportSize.x / _visibleGameSize!.x; |       final zoomX = viewportSize.x / _visibleGameSize!.x; | ||||||
|       final zoomY = viewportSize.y / _visibleGameSize!.y; |       final zoomY = viewportSize.y / _visibleGameSize!.y; | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Pasha Stetsenko
					Pasha Stetsenko