私はいつもビデオゲームを作りたいと思っていました。私が最初の仕事を得るのに役立った最初の 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で のサイズ変更方法を使用することです(またはGimpで補間をNoneに設定します)。ただし、アセットがより詳細な場合は、おそらく機能しません。 Nearest Neighbour (hard edges) 説明はさておき、 ダウンロードするか、自分で用意してプロジェクトの フォルダに追加してください。 私が用意したアセットを 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); } } はゲームを表すメイン クラスであり、Flame エンジンで使用される基本ゲーム クラスである を拡張します。これにより、Flame の基本的な構成要素である 拡張されます。画像、インターフェイス、エフェクトなど、ゲーム内のすべてがコンポーネントです。各 は、コンポーネントの初期化時に呼び出される非同期メソッド あります。通常、すべてのコンポーネント セットアップ ロジックはそこに配置されます。 PlatformerGame FlameGame Component Component onLoad 最後に、前に作成した ファイルをインポートし、 追加して、assets 定数の取得元を明示的に宣言します。そして、メソッド を使用して、 リストにリストされているすべてのアセットをゲーム イメージ キャッシュにロードしました。 assets.dart as 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, ), ); } すべての準備が完了し、楽しい部分が始まります。 プレイヤーキャラクターの追加 新しいフォルダー とその中に新しいファイル を作成します。これは、プレイヤー キャラクターである The Boy を表すコンポーネントになります。 lib/actors/ theboy.dart 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 ), ); } } このクラスは、アニメーション化されたスプライトに使用されるコンポーネントである を拡張し、ゲーム オブジェクトを参照してゲーム キャッシュから画像をロードしたり、後でグローバル変数を取得したりできるようにする mixin を持っています。 SpriteAnimationComponent 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 を制御する機能を追加する必要があります。 mixin を ファイルに追加しましょう。 HasKeyboardHandlerComponents game.dart class PlatformerGame extends FlameGame with HasKeyboardHandlerComponents 次に、 と mixin に戻りましょう。 theboy.dart KeyboardHandler 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; } は、プレイヤーが右に移動すると 1、プレイヤーが左に移動すると -1、プレイヤーが移動しない場合は 0 になります。ただし、プレーヤーの位置はまだ変更されていないため、画面にはまだ表示されません。 メソッドを追加して修正しましょう。 _horizontalDirection 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 カメラを制御する方法、視差背景を追加する方法について説明します。乞うご期待! 資力 各パートの最後に、私が学んだすばらしいクリエイターとリソースのリストを追加します。 GrafxKid のアーケード プラットフォーマー アセット https://opengameart.org/content/arcade-platformer-assets DevKage Flame ゲーム開発シリーズ: https://youtu.be/mSPalRqZQS8 クレイグ・オダチャンネル https://youtu.be/hwQpBuZoV9s Ember Quest ゲーム チュートリアル https://github.com/flame-engine/flame/blob/main/doc/tutorials/platformer/platformer.md Flame エンジンのドキュメント https://docs.flame-engine.org/1.6.0/flame/flame.html