こんにちは、虎の穴ラボFantia開発エンジニアの吉岡です。
今回はFlutterとそのゲームエンジンであるFlameを使って簡単なジャンプゲームを作っていきます。
Flameとは
Flutterのプラグインとして提供されている2Dゲーム向けのゲームエンジンです。
ユーザ入力、物理演算のサポート、サウンド・アニメーションの再生など一通りの機能が備わっており
FlutterやDartを使ったことある人なら簡単にゲームを作ることが出来ます。
またFlutterのプラグインであるためWebやスマホ、デスクトップのマルチプラットフォーム対応も簡単に行えるのが特徴です。
今回作るゲーム
今回は比較的シンプルなゲームとして「メイドちゃんのジャンプゲーム」を作って行きます。
イメージとしてはGoogle Chromeがオフラインの時に表示される恐竜のゲームです。
今回の実行環境はchromeをメインにしています。iOSやAndroidで実行する際は表示位置等調整が必要です。
環境
今回使うFlutterのバージョンになります。
$ flutter --version Flutter 3.24.5 • channel stable • https://github.com/flutter/flutter.git Framework • revision dec2ee5c1f (12 hours ago) • 2024-11-13 11:13:06 -0800 Engine • revision a18df97ca5 Tools • Dart 3.5.4 • DevTools 2.37.3
実装
プロジェクト作成・ライブラリ追加
まずはflutterのプロジェクトを作っていきます。
flutter create jump_game
jump_game
のプロジェクトが出来たら pubspec.yaml
を編集して flameのライブラリを追加します。
~ 略 ~ dependencies: flutter: sdk: flutter flame: ^1.19.0 # 今回追加
追加をしたら保存をして
flutter pub get
を実行してインストール完了です。
ゲーム画面の作成
まずはメイドちゃんと障害物が表示されるゲームの画面を作成していきます。
Flameは様々な要素をComponentとして扱っていきます。
ゲームで使用するComponentの追加、更新などを行うベースとなるComponentとして
lib/games/jump_game.dart
というファイルを用意してその中に、
FlameGame
クラスを継承した JumpGame
というクラスを作成し画面端のあたり判定を追加します。
今後はこの JumpGame
クラスへキャラクターや障害物、ユーザの入力処理などを追加していきます。
class JumpGame extends FlameGame{ // 初回の読み込み時に呼ばれるメソッド // 他のComponentの初期化などを行う @override Future<void> onLoad() async { super.onLoad(); add(ScreenHitbox()); // 画面端のあたり判定を追加 } }
Componentの追加は add
メソッドに引数として渡すことでゲーム内へ追加することが出来ます。
ここで作ったJumpGame
をFlutterで表示させるため Widgetを作成します。
lib/pages/jump_game_page.dart
にJumpGamePage
クラスを作ります。
class JumpGamePage extends StatelessWidget { JumpGamePage({super.key}); // 今回作成したゲームを持つ final game = JumpGame(); @override Widget build(BuildContext context) { // FlutterへFlameのGameWidgetを追加 return GameWidget(game: game); } }
ついでにmain.dart
も修正します。
void main() async { WidgetsFlutterBinding.ensureInitialized(); // web以外の場合は横向きの画面にする if (!kIsWeb) { await Flame.device.setLandscape(); await Flame.device.fullScreen(); } runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: "Jump Games", home: JumpGamePage(), ); } }
キャラクターのComponentの作成
ゲーム内でプレイヤーが操作するキャラクターのComponentを作成していきます。
今回はメイドちゃんがキャラクターのため lib/components/maid_component.dart
のファイルを作成し、
SpriteComponent
を継承した MaidComponent
クラスを作ります。
class MaidComponent extends SpriteComponent{ MaidComponent({ super.position, super.size, super.sprite }):super( anchor: Anchor.center, paint: BasicPalette.gary.paint() ); }
一旦これでゲームにメイドちゃんを表示させるための MaidComponent
が出来ました。
ジャンプやあたり判定などは後ほど実装していきます。
キャラクターを表示させる
先ほど作った MaidComponent
をJumpGame
へ追加してゲーム内に表示せていきます。
まず表示させる画像を用意します。今回は虎の穴ラボのWebサイトを使います。
画像を用意ししたら assets/images/maid01.png
として保存します。
次にFlutter内で画像を使うために pubspec.yaml
にassetsの追加を行います。
~ 略 ~ flutter: ~ 略 ~ assets: - assets/images/
これで画像を使える様になりました。次にゲーム内に表示させるための処理を作っていきます。
lib/games/jump_game.dart
の JumpGame
を編集します。
class JumpGame extends FlameGame{ late MaidComponent _maid; // MaidComponentの用意 @override Future<void> onLoad() async { super.onLoad(); add(ScreenHitbox()); final maidSprite = await Sprite.load('maid01.png'); // メイドちゃんの画像の読み込み _maid = MaidComponent( position: Vector2(100, size.y * 0.8), // 初期の表示位置を指定 size: Vector2.all(size.x * 0.1), // サイズを指定 sprite: maidSprite, // メイドちゃんの画像を読み込んだspriteを指定 ) add(_maid); // ゲーム内へMaidComponentを追加する } }
これで実行するとメイドちゃんが表示されるようになりました! 実際に実行して確認してみます。
flutter run -d chrome
ジャンプさせる
MaidComponent
をジャンプできるように実装していきます。
class MaidComponent extends SpriteComponent{ bool isJump = false; // ジャンプ動作のフラグ bool isJumpUp = false; // ジャンプ方向のフラグ final double maxJump = 450.0; // ジャンプの最高到達点 final double jumpSpeed = 10.0; // ジャンプするスピード final double basePos; // ジャンプ開始・終了の高さ MaidComponent({ super.position, super.size, super.sprite, required this.basePos }):super( anchor: Anchor.center, paint:: BasicPalette.gary.paint() ); // フレームごとに呼ばれるメソッド @override void update(double dt){ super.update(dt); // ジャンプの動作を実装 // 最高到達点へ行ったら下に落ちる if (isJump) { if (isJumpUp) { position.y -= jumpSpeed; if (position.y <= maxJump) { isJumpUp = false; } } else { position.y += jumpSpeed; if (position.y >= basePos) { positionReset(); isJump = false; } } } } // ジャンプした際に呼ばれる void jump(){ isJump = false; isJumpUp = false; } // キャラクター位置をリセット void positionReset(){ position.y = basePos; } }
これでMaidComponent
はジャンプする準備が出来ました。
次はJumpGame
側からキーイベント、タップを検知して実際にジャンプするようにしていきます。
物理演算等を使えば簡単にジャンプの動作も実装出来ますが、他に必要なライブラリを追加したり説明が多くなるので今回は省きます。
気になる方はflame_forge2d等を確認してください。
// タップのイベントを取るための TapCallbacks // キーボードのイベントを取るための KeyboardEvents // をそれぞれmixin class JumpGame extends FlameGame with TapCallbacks, KeyboardEvents { late MaidComponent _maid; // MaidComponentの用意 @override Future<void> onLoad() async { ~ 略 ~ } // キーイベントを取るコールバック @override KeyEventResult onKeyEvent( KeyEvent event, Set<LogicalKeyboardKey> keysPressed) { // スペースキーを押されたらキャラクターがジャンプ if (keysPressed.contains(LogicalKeyboardKey.space)) { _maid.jump(); } return KeyEventResult.ignored; } // タップされたイベントを取るコールバック @override void onTapDown(TapDownEvent event) { // クリックでもキャラクターがジャンプ super.onTapDown(event); _maid.jump(); } }
実行してスペースキーの入力かスマホの場合は画面タップでメイドちゃんがジャンプするようになりました!
障害物の実装
次にメイドちゃんへ向かって飛んでくる障害物、今回は玉を実装していきます。
lib/components/ball_component.dart
ファイルを作成しその中で
CircleComponent
を継承した BallComponent
を作って、
玉が画面の右端から左へ飛んでいき画面端へ当たると消えるように実装します。
class BallComponent extends CircleComponent with CollisionCallbacks { // 当たり判定を取るCollisionCallbacks をmixin BallComponent({super.position}): super(radius: 10); final double ballSpeed = 10.0; @override void update(double dt) { super.update(dt); // 右から左へ移動する position.x -= ballSpeed; } // 玉が何かにあたった時に呼ばれるコールバック @override void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) { super.onCollision(intersectionPoints, other); // 壁に当たった場合の処理 if (other is ScreenHitbox) { removeFromParent(); // 自身をゲームから削除する } } }
玉の準備は出来ました!これをゲームに追加していきます。
玉の出現をランダムにすればゲーム性が出て面白いですが今回はシンプルに5秒ごとに玉が飛んでくるようにします。
5秒おきに実行する処理はDartの標準クラスのTimerではなくFlameに用意されている TimerComponent
を使用します。
TimerComponent
を使用することでゲーム全体をpauseさせた際の挙動などFlameに合わせて設計されています。
// 当たり判定を取るためにCollisionCallbacksをmixin class JumpGame extends FlameGame with TapCallbacks, KeyboardEvents, CollisionCallbacks { late MaidComponent _maid; // MaidComponentの用意 @override Future<void> onLoad() async { ~ 略 ~ // 5秒ごとに球を発射するようにTimerComponentを追加 add(TimerComponent( period: 5, // 間隔を指定 repeat: true, // 繰り返し実行のフラグ onTick: () { add(BallComponent( position: Vector2(size.x - 30, size.y * 0.8))); // 5秒ごとに球をゲームに追加 })); } @override KeyEventResult onKeyEvent( KeyEvent event, Set<LogicalKeyboardKey> keysPressed) { ~ 略 ~ } @override void onTapDown(TapDownEvent event) { ~ 略 ~ } }
これで玉が飛んでくるようになりました! 実行して確認してみましょう。
flutter run -d chrome
メイドちゃんの当たり判定
メイドちゃんが玉に当たった場合の動作を実装していきます。 メイドちゃんがたまに当たった場合は玉の表示を消し画像を「スライムになったメイドちゃん」に切り替えます。
「スライムになったメイドちゃん」の画像をassets/images/slime.png
に保存します。
MaidComponent
に当たり判定を追加します。
class MaidComponent extends SpriteComponent with CollisionCallbacks{ // 当たり判定を取る CollisionCallbacks をmixin bool isJump = false; bool isJumpUp = false; final double maxJump = 450.0; final double jumpSpeed = 10.0; final double basePos; MaidComponent(~略~):super( anchor: Anchor.center, paint:: BasicPalette.gary.paint() ); @override void update(double dt){ ~ 略 ~ } //当たり判定のコールバック @override void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) { super.onCollision(intersectionPoints, other); // 玉に当たったらゲームオーバに if (other is BallComponent) { gameOver(); } } void jump(){ ~ 略 ~ } void positionReset(){ ~ 略 ~ } // ゲームオーバーの処理 void gameOver() async{ // スライムの画像を読み込み // spriteを置き換える final slimeSprite = await Sprite.load('slime.png'); sprite = slimeSprite; } }
メイドちゃん側の当たり判定は出来ました、合わせて玉の当たり判定も追加しておきます。
class BallComponent extends CircleComponent with CollisionCallbacks { // 当たり判定を取るCollisionCallbacks をmixin BallComponent({super.position}): super(radius: 10); final double ballSpeed = 10.0; @override void update(double dt) { ~ 略 ~ } // 玉が何かにあたった時に呼ばれるコールバック @override void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) { super.onCollision(intersectionPoints, other); // 壁に当たった場合の処理 if (other is ScreenHitbox) { removeFromParent(); // 自身をゲームから削除する } //メイドちゃんに当たった場合 if(other is MaidComponent){ removeFromParent(); // 自身をゲームから削除する } } }
これで実行すると、メイドちゃんが玉に当たった際にスライムになるように出来ました!
スタート画面・ゲームオーバー画面の追加
現在のままではゲームを起動したら5秒後に玉が飛んできて、メイドちゃんが玉に当たってもひたすた玉が飛んできます。 もう少しゲーム性を持たせるために、スタート画面とメイドちゃんが玉に当たったあとのゲームオーバー画面を作っていきます。 この2つの画面はFlutterのWidgetを使用して作成し、Flameのオーバーレイ機能を使ってゲーム上に表示させます。
まずはlib/pages/start_page.dart
にスタート画面のStartPage
クラスを作ります。
class StartPage extends StatelessWidget { final JumpGame jumpGame; const StartPage({super.key, required this.jumpGame}); @override Widget build(BuildContext context) { // スタートボタンが押されたらJumpGameのgameStartメソッド(後述)を呼び出す return Center( child: ElevatedButton( onPressed: () => jumpGame.gameStart(), child: const Text("Start")), ); } }
次にlib/pages/game_over_page.dart
にゲームオーバー画面 GameOverPage
の作成をします
class GameOverPage extends StatelessWidget { final JumpGame jumpGame; const GameOverPage({super.key, required this.jumpGame}); @override Widget build(BuildContext context) { return Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Material(child: Text("結果:${jumpGame.jumpCount}")), // 避けれた玉の数を表示 // ボタンを押すとJumpGameのgameRestart(後述)を呼び出す ElevatedButton( onPressed: () => jumpGame.gameRestart(), child: const Text("Re Start")), ]), ); } }
この2つのWidgetでスタート画面とゲームオーバー画面の準備が出来ました。
次にオーバーレイの設定をします。
lib/pages/jump_game_page.dart
の JumpGamePage
を修正します。
class JumpGamePage extends StatelessWidget { JumpGamePage({super.key}); final game = JumpGame(); @override Widget build(BuildContext context) { // FlutterへFlameのGameWidgetを追加 return GameWidget( // FlutterのUI(Widget)を追加する overlayBuilderMap: { // スタート画面のWidget "start": (context, JumpGame jumpGame) => StartPage( jumpGame: jumpGame, ), // ゲームオーバー画面のWidget "gameOver": (context, JumpGame jumpGame) => GameOverPage(jumpGame: jumpGame) }, // ゲーム自体はJumpGameを設定 game: game, initialActiveOverlays: const ['start'], // 起動時はスタート画面を設定 ); } }
この修正でゲームが起動するとまずはスタート画面が表示されます。
ただこのままだとスタート画面やゲームオーバー画面の裏でゲームの処理、今回では玉の発射やキャラクターのジャンプが動き続きます。
そこでスタートボタンが押されるまでとゲームオーバー後はFlameGame
のpaused
を true
にしてゲーム全体を止めます。
gameStart
メソッド, gameRestart
メソッドの実装と合わせてpaused
の追加もやっていきましょう。
class JumpGame extends FlameGame with TapCallbacks, KeyboardEvents, CollisionCallbacks { late MaidComponent _maid; int jumpCount = 0; //ゲームオーバー画面で表示するジャンプの成功数を保持 @override Future<void> onLoad() async { ~ 略 ~ } @override void update(double dt){ // メイドちゃんがゲームオーバーになったらgameOverメソッドを呼び出す if(_maid.isGameOver){ gameOver() } } // onLoadが完了したあとに呼ばれるコールバック @override void onMount(){ super.onMount(); paused = true; // trueを代入することでゲーム全体を停止させる } // スタートボタンを押した際に呼ばれるメソッド void gameStart() async { // オーバーレイのスタート画面を削除する overlays.remove('start'); // ポーズを解除 paused = false; // メイドちゃんをスタート状態にする _maid.gameStart(); } void gameOver(){ // ゲームをポーズ状態にする paused = true; // ゲームオーバー画面をオーバーレイする overlays.add("gameOver"); } // ゲームオーバー画面でリスタートボタンを押された時に呼ばれる void gameRestart(){ // ゲームオーバー画面のオーバーレイを削除して // スタート画面をオーバレイする overlays.remove("gameOver"); overlays.add("start"); } @override KeyEventResult onKeyEvent( KeyEvent event, Set<LogicalKeyboardKey> keysPressed) { ~ 略 ~ } @override void onTapDown(TapDownEvent event) { ~ 略 ~ } }
これでスタート画面とゲームオーバー画面の切り替えゲームの停止ができるようになりました。
次はMaidComponent
でgameStart
メソッド実装とgameOver
メソッドの修正をします。
class MaidComponent extends SpriteComponent with CollisionCallbacks, HasGameRef<JumpGame> { ~ 略 ~ // ゲームスタート、リスタート時に呼ばれる // 画像、フラグのリセット void gameStart() async { final maidSprite = await Sprite.load('maid01.png'); sprite = slimeSprite; isGameOver = false; } // ゲームオーバー // 画像の差し替え、フラグの変更 void gameOver() async { positionReset(); final slimeSprite = await Sprite.load('slime.png'); sprite = slimeSprite; isGameOver = true; } }
最後にBallComponent
内でジャンプの成功数をカウントします。
// HasGameRefをmixinする // JumpGameを指定することでJumpGame側のメソッドや変数にアクセスできるようになる class BallComponent extends CircleComponent with CollisionCallbacks, HasGameRef<JumpGame> { ~ 略 ~ @override void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) { super.onCollision(intersectionPoints, other); if (other is ScreenHitbox) { // 壁に当たったらカウントして球を消す gameRef.jumpCount++; //gameRefを使うことでJumpGameのjumpCountをインクリメントできる removeFromParent(); } if (other is MaidComponent) { // プレイヤーキャラクターに当たったら球を消す removeFromParent(); } } }
これでスタート画面からゲームオーバーまでの一連の流れが出来ました!
また、ジャンプの高さの調整などは必要ですがiPhoneでも実行が可能です!
最終的なlib以下のファイル構成は以下のようになっています。
lib ├── components │ ├── ball_component.dart │ └── maid_component.dart ├── games │ └── jump_game.dart ├── main.dart └── pages ├── game_over_page.dart ├── init_page.dart └── jump_game_page.dart
まとめ
Flameを使うことでFlutter上でも簡単に2Dの実装を行うことが出来ました。
有名なゲームエンジンのような派手なGUIでのゲーム制作では無いですが、普段から使ってるFlutterで簡単にマルチプラットフォーム対応のゲームを作る事が出来ました。
今回は簡単なジャンプゲームだけでしたが、アイデア次第で色々なゲームが作れて楽しそうです。
Fantia開発採用情報
虎の穴ラボでは現在、一緒にFantiaを開発していく仲間を積極募集中です!
多くのユーザーに使っていただけるtoCサービスの開発をやってみたい方は、ぜひ弊社の採用情報をご覧ください。
https://toranoana-lab.co.jp/job/302toranoana-lab.co.jp