私はいつもビデオゲームを作りたいと思っていました。私が最初の仕事を得るのに役立った最初の Android アプリは、Android ビューで作成された単純なゲームでした。その後、ゲームエンジンを使ってより精巧なゲームを作ろうという試みが何度も行われましたが、時間の不足やフレームワークの複雑さなどから、いずれも失敗に終わりました。しかし、Flutter ベースの Flame エンジンについて初めて聞いたとき、そのシンプルさとクロスプラットフォームのサポートにすぐに惹かれ、それを使ってゲームを構築してみることにしました。
エンジンの感触をつかむために、シンプルでありながらやりがいのあるものから始めたかったのです。この一連の記事は、Flame (および Flutter) を学び、基本的なプラットフォーマー ゲームを構築する私の旅です。私はそれを非常に詳細にしようとするので、一般的に、Flame やゲーム開発に足を踏み入れている人には役立つはずです.
4 回の記事で、以下を含む 2D 横スクロール ゲームを作成します。
走ったりジャンプしたりできるキャラクター
プレイヤーを追うカメラ
地面とプラットフォームを含むスクロール レベル マップ
視差の背景
プレイヤーが収集できるコインとコインの数を表示するHUD
勝利画面
最初のパートでは、新しい Flame プロジェクトを作成し、すべてのアセットを読み込み、プレイヤー キャラクターを追加して、彼に走り方を教えます。
まず、新しいプロジェクトを作成しましょう。 Bare Flame の公式ゲームチュートリアルでは、そのためのすべての手順が説明されているので、それに従ってください。
追加することの 1 つ: pubspec.yaml
ファイルを設定するときに、ライブラリのバージョンを利用可能な最新のものに更新するか、そのままにしておくことができます。これは、バージョンの前にあるキャレット記号 (^) により、アプリが最新の非-壊れたバージョン。 (キャレット構文)
すべての手順に従った場合、 main.dart
ファイルは次のようになります。
import 'package:flame/game.dart'; import 'package:flutter/widgets.dart'; void main() { final game = FlameGame(); runApp(GameWidget(game: game)); }
続行する前に、ゲームに使用するアセットを準備する必要があります。アセットとは、画像、アニメーション、サウンドなどです。このシリーズでは、ゲーム開発でスプライトとも呼ばれる画像のみを使用します。
プラットフォーマー レベルを構築する最も簡単な方法は、タイル マップとタイル スプライトを使用することです。これは、レベルが基本的にグリッドであり、各セルがそれが表すオブジェクト/地面/プラットフォームを示すことを意味します。その後、ゲームの実行中に、各セルからの情報が対応するタイル スプライトにマップされます。
この手法を使用して構築されたゲームのグラフィックは、非常に精巧な場合も非常に単純な場合もあります。たとえば、スーパー マリオ ブラザーズでは、多くの要素が繰り返されていることがわかります。これは、ゲーム グリッド内の各グラウンド タイルに対して、それを表すグラウンド イメージが 1 つしかないためです。同じアプローチに従い、静的オブジェクトごとに 1 つの画像を準備します。
また、プレイヤー キャラクターやコインなどの一部のオブジェクトをアニメーション化することも必要です。アニメーションは通常、一連の静止画像として保存され、それぞれが 1 つのフレームを表します。アニメーションの再生中は、フレームが次から次へと進み、オブジェクトが動いているように見えます。
ここで最も重要な問題は、アセットをどこで取得するかです。もちろん、自分で描いてもいいですし、アーティストに依頼して描いても構いません。また、ゲーム アセットをオープンソースに貢献した素晴らしいアーティストがたくさんいます。 GrafxKidのArcade Platformer Assets パックを使用します。
通常、画像アセットには、スプライト シートと単一のスプライトの 2 つの形式があります。前者は、すべてのゲーム アセットを 1 つに含む大きな画像です。次に、ゲーム開発者が必要なスプライトの正確な位置を指定すると、ゲーム エンジンがスプライトをシートから切り取ります。このゲームでは、スプライト シートで提供されるすべてのアセットを必要としないため、単一のスプライトを使用します (アニメーションを除き、1 つの画像として保持する方が簡単です)。
スプライトを自分で作成する場合でも、アーティストから入手する場合でも、スプライトをスライスして、ゲーム エンジンにより適したものにする必要がある場合があります。その目的のために特別に作成されたツール (テクスチャ パッカーなど)または任意のグラフィカル エディターを使用できます。 Adobe Photoshop を使用しました。このスプライト シートでは、スプライト間のスペースが不均等であり、自動ツールで画像を抽出するのが難しく、手動で行う必要がありました。
アセットのサイズを大きくしたい場合もありますが、ベクター画像でない場合、結果のスプライトがぼやけてしまう可能性があります。ピクセルアートに最適な回避策の1つは、PhotoshopでNearest Neighbour (hard edges)
のサイズ変更方法を使用することです(またはGimpで補間をNoneに設定します)。ただし、アセットがより詳細な場合は、おそらく機能しません。
説明はさておき、私が用意したアセットをダウンロードするか、自分で用意してプロジェクトのassets/images
フォルダに追加してください。
新しいアセットを追加するたびに、次のようにpubspec.yaml
ファイルに登録する必要があります。
flutter: assets: - assets/images/
今後のヒント: 既に登録されているアセットを更新する場合は、ゲームを再起動して変更を確認する必要があります。
それでは実際にアセットをゲームにロードしてみましょう。すべてのアセット名を 1 か所にまとめるのが好きです。これは、すべてを追跡し、必要に応じて変更するのが簡単になるため、小規模なゲームには最適です。それでは、 lib
ディレクトリに新しいファイルを作成しましょう: assets.dart
const String THE_BOY = "theboy.png"; const String GROUND = "ground.png"; const String PLATFORM = "platform.png"; const String MIST = "mist.png"; const String CLOUDS = "clouds.png"; const String HILLS = "hills.png"; const String COIN = "coin.png"; const String HUD = "hud.png"; const List<String> SPRITES = [THE_BOY, GROUND, PLATFORM, MIST, CLOUDS, HILLS, COIN, HUD];
そして、将来すべてのゲームロジックを含む別のファイルを作成します: game.dart
import 'package:flame/game.dart'; import 'assets.dart' as Assets; class PlatformerGame extends FlameGame { @override Future<void> onLoad() async { await images.loadAll(Assets.SPRITES); } }
PlatformerGame
はゲームを表すメイン クラスであり、Flame エンジンで使用される基本ゲーム クラスであるFlameGame
を拡張します。これにより、Flame の基本的な構成要素であるComponent
拡張されます。画像、インターフェイス、エフェクトなど、ゲーム内のすべてがコンポーネントです。各Component
は、コンポーネントの初期化時に呼び出される非同期メソッドonLoad
あります。通常、すべてのコンポーネント セットアップ ロジックはそこに配置されます。
最後に、前に作成したassets.dart
ファイルをインポートし、 as Assets
追加して、assets 定数の取得元を明示的に宣言します。そして、メソッドimages.loadAll
を使用して、 SPRITES
リストにリストされているすべてのアセットをゲーム イメージ キャッシュにロードしました。
次に、 main.dart
から新しいPlatformerGame
を作成する必要があります。次のようにファイルを変更します。
import 'package:flame/game.dart'; import 'package:flutter/widgets.dart'; import 'game.dart'; void main() { runApp( const GameWidget<PlatformerGame>.controlled( gameFactory: PlatformerGame.new, ), ); }
すべての準備が完了し、楽しい部分が始まります。
新しいフォルダーlib/actors/
とその中に新しいファイルtheboy.dart
を作成します。これは、プレイヤー キャラクターである The Boy を表すコンポーネントになります。
import '../game.dart'; import '../assets.dart' as Assets; import 'package:flame/components.dart'; class TheBoy extends SpriteAnimationComponent with HasGameRef<PlatformerGame> { TheBoy({ required super.position, // Position on the screen }) : super( size: Vector2.all(48), // Size of the component anchor: Anchor.bottomCenter // ); @override Future<void> onLoad() async { animation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.THE_BOY), SpriteAnimationData.sequenced( amount: 1, // For now we only need idle animation, so we load only 1 frame textureSize: Vector2.all(20), // Size of a single sprite in the sprite sheet stepTime: 0.12, // Time between frames, since it's a single frame not that important ), ); } }
このクラスは、アニメーション化されたスプライトに使用されるコンポーネントであるSpriteAnimationComponent
を拡張し、ゲーム オブジェクトを参照してゲーム キャッシュから画像をロードしたり、後でグローバル変数を取得したりできるようにする mixin HasGameRef
を持っています。
onLoad
メソッドで、 assets.dart
ファイルで宣言したTHE_BOY
スプライト シートから新しいSpriteAnimation
を作成します。
それでは、プレーヤーをゲームに追加しましょう! game.dart
ファイルに戻り、 onLoad
メソッドの最後に以下を追加します。
final theBoy = TheBoy(position: Vector2(size.x / 2, size.y / 2)); add(theBoy);
今すぐゲームを実行すると、ザ・ボーイに会えるはずです!
まず、キーボードから The Boy を制御する機能を追加する必要があります。 HasKeyboardHandlerComponents
mixin をgame.dart
ファイルに追加しましょう。
class PlatformerGame extends FlameGame with HasKeyboardHandlerComponents
次に、 theboy.dart
とKeyboardHandler
mixin に戻りましょう。
class TheBoy extends SpriteAnimationComponent with KeyboardHandler, HasGameRef<PlatformerGame>
次に、いくつかの新しいクラス変数をTheBoy
コンポーネントに追加します。
final double _moveSpeed = 300; // Max player's move speed int _horizontalDirection = 0; // Current direction the player is facing final Vector2 _velocity = Vector2.zero(); // Current player's speed
最後に、キーボード入力をリッスンできるonKeyEvent
メソッドをオーバーライドしましょう。
@override bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) { _horizontalDirection = 0; _horizontalDirection += (keysPressed.contains(LogicalKeyboardKey.keyA) || keysPressed.contains(LogicalKeyboardKey.arrowLeft)) ? -1 : 0; _horizontalDirection += (keysPressed.contains(LogicalKeyboardKey.keyD) || keysPressed.contains(LogicalKeyboardKey.arrowRight)) ? 1 : 0; return true; }
_horizontalDirection
は、プレイヤーが右に移動すると 1、プレイヤーが左に移動すると -1、プレイヤーが移動しない場合は 0 になります。ただし、プレーヤーの位置はまだ変更されていないため、画面にはまだ表示されません。 update
メソッドを追加して修正しましょう。
ここで、ゲーム ループとは何かを説明する必要があります。基本的に、これはゲームが無限ループで実行されていることを意味します。各反復で、現在の状態がComponent's
メソッドrender
でレンダリングされ、次にメソッドupdate
で新しい状態が計算されます。メソッドの署名のdt
パラメータは、状態が最後に更新されてからの時間 (ミリ秒) です。それを念頭に置いて、以下をtheboy.dart
に追加します。
@override void update(double dt) { super.update(dt); _velocity.x = _horizontalDirection * _moveSpeed; position += _velocity * dt; }
ゲーム ループ サイクルごとに、現在の方向と最大速度を使用して、水平方向の速度を更新します。次に、更新された値にdt
を掛けてスプライトの位置を変更します。
なぜ最後の部分が必要なのですか?速度だけで位置を更新すると、スプライトは宇宙に飛び去ります。しかし、小さい方の速度値を使用することはできますか?できますが、プレーヤーの動き方は、1 秒あたりのフレーム数 (FPS) レートが異なると異なります。 1 秒あたりのフレーム (またはゲーム ループ) の数は、ゲームのパフォーマンスと実行されているハードウェアによって異なります。デバイスのパフォーマンスが高いほど、FPS が高くなり、プレイヤーの動きが速くなります。それを避けるために、最後のフレームからの経過時間に速度を依存させます。そうすれば、スプライトはどの FPS でも同じように動きます。
さて、ここでゲームを実行すると、次のように表示されます。
すごい、今度は男の子が左に行くときに振り向くようにしましょう。これをupdate
メソッドの最後に追加します。
if ((_horizontalDirection < 0 && scale.x > 0) || (_horizontalDirection > 0 && scale.x < 0)) { flipHorizontally(); }
かなり簡単なロジック: 現在の方向 (ユーザーが押している矢印) がスプライトの方向と異なるかどうかを確認し、水平軸に沿ってスプライトを反転します。
次に、実行中のアニメーションも追加しましょう。まず、2 つの新しいクラス変数を定義します。
late final SpriteAnimation _runAnimation; late final SpriteAnimation _idleAnimation;
次に、 onLoad
次のように更新します。
@override Future<void> onLoad() async { _idleAnimation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.THE_BOY), SpriteAnimationData.sequenced( amount: 1, textureSize: Vector2.all(20), stepTime: 0.12, ), ); _runAnimation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.THE_BOY), SpriteAnimationData.sequenced( amount: 4, textureSize: Vector2.all(20), stepTime: 0.12, ), ); animation = _idleAnimation; }
ここでは、以前に追加したアイドル アニメーションをクラス変数に抽出し、新しい実行アニメーション変数を定義しました。
次に、新しいupdateAnimation
メソッドを追加しましょう。
void updateAnimation() { if (_horizontalDirection == 0) { animation = _idleAnimation; } else { animation = _runAnimation; } }
そして最後にupdate
メソッドの一番下でこのメソッドを呼び出してゲームを実行します。
前半は以上です。 Flame ゲームをセットアップする方法、アセットを見つける場所、ゲームにロードする方法、素晴らしいアニメーション キャラクターを作成してキーボード入力に基づいて動かす方法を学びました。この部分のコードは、私の github にあります。
次の記事では、Tiled を使用してゲーム レベルを作成する方法、Flame カメラを制御する方法、視差背景を追加する方法について説明します。乞うご期待!
各パートの最後に、私が学んだすばらしいクリエイターとリソースのリストを追加します。