Open7

Flame でゲーム作ってみる

Masatoshi TsushimaMasatoshi Tsushima

タイトル画面とゲーム画面を分けてみたくなったので Router を導入する。

https://docs.flame-engine.org/latest/flame/router.html

  late final RouterComponent router;

  
  void onLoad() {
    add(
      router = RouterComponent(
        initialRoute: 'title',
        routes: {
          'title': Route(TitleScreen.new),
        },
      ),
    );
  }

FlameGame の延長みたいな遷移先を作りたい場合は HasGameReference で作ると良いみたい。

class TitleScreen extends Component with HasGameReference<MyGame> {
  static final _titleTextPaint = TextPaint(
    style: const TextStyle(
      fontFamily: 'Press Start 2P',
      fontSize: 48,
      color: Colors.white,
    ),
  );

  Vector2 get size => game.size;

  
  void onLoad() {
    super.onLoad();
    final scale = size.scaleFromStandard();
    add(
      TextComponent(
        text: 'Hello, Flame',
        anchor: Anchor.center,
        textRenderer: _titleTextPaint,
        position: Vector2(size.x / 2, size.y / 3),
        scale: scale,
      ),
    );
  }
}

final standardScreenSize = Vector2(960, 720);

extension on Vector2 {
  Vector2 scaleFromStandard() =>
      Vector2(x / standardScreenSize.x, y / standardScreenSize.y);
}

Scale を計算して画面サイズが変わっても大丈夫なようにしている。

Google Font からそれっぽいフォント探して埋め込んでみた。

https://docs.flame-engine.org/latest/flame/rendering/text_rendering.html#textpaint
https://docs.flutter.dev/cookbook/design/fonts

Masatoshi TsushimaMasatoshi Tsushima

画面サイズを固定したい。 Linux マシンで作成しているのでとりあえず Linux と Web で。

Linux は set_resizable を呼び出す。

https://docs.gtk.org/gtk4/method.Window.set_resizable.html

diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc
index bd13a93..17906ba 100644
--- a/linux/runner/my_application.cc
+++ b/linux/runner/my_application.cc
@@ -27,7 +27,7 @@ static void my_application_activate(GApplication* application) {
   // in case the window manager does more exotic layout, e.g. tiling.
   // If running on Wayland assume the header bar will work (may need changing
   // if future cases occur).
-  gboolean use_header_bar = TRUE;
+  gboolean use_header_bar = FALSE;
 #ifdef GDK_WINDOWING_X11
   GdkScreen* screen = gtk_window_get_screen(window);
   if (GDK_IS_X11_SCREEN(screen)) {
@@ -47,7 +47,8 @@ static void my_application_activate(GApplication* application) {
     gtk_window_set_title(window, "flame_platformer");
   }
 
-  gtk_window_set_default_size(window, 1280, 720);
+  gtk_window_set_default_size(window, 960, 720);
+  gtk_window_set_resizable(window, FALSE);
   gtk_widget_show(GTK_WIDGET(window));
 
   g_autoptr(FlDartProject) project = fl_dart_project_new();

Web はホストする要素を作ってサイズを固定する。

diff --git a/web/index.html b/web/index.html
index 347c6dd..b08bdfa 100644
--- a/web/index.html
+++ b/web/index.html
@@ -33,6 +33,14 @@
   <link rel="manifest" href="manifest.json">
 </head>
 <body>
+  <div style="width: 960px; margin: 0 auto;">
+    <header>
+      <h1>My Game</h1>
+    </header>
+    <main>
+      <div id="flutter_host" style="width: 960px; height: 720px;"></div>
+    </main>
+  </div>
   <script src="flutter_bootstrap.js" async></script>
 </body>

flutter_bootstrap.js を作成してホストする Element を渡すようにする。

{{flutter_js}}
{{flutter_build_config}}
_flutter.loader.load({
    config: {
        hostElement: document.getElementById('flutter_host'),
    }
});
Masatoshi TsushimaMasatoshi Tsushima

タイトルメニューを作る。

extends FlameGame with HasKeyboardHandlerComponents で Mixin を追加してキーボードイベントを受けられるようにしておく。

enum _MenuItem {
  gameStart('Game Start'),
  exit('Exit'),
  ;

  final String text;

  const _MenuItem(this.text);
}
  static final _itemTextPaint = TextPaint(
    style: const TextStyle(
      fontFamily: 'Press Start 2P',
      fontSize: 16,
      color: Colors.white,
    ),
  );

  static final _selectedItemTextPaint = TextPaint(
    style: const TextStyle(
      fontFamily: 'Press Start 2P',
      fontSize: 16,
      color: Colors.red,
    ),
  );

  late final List<TextComponent> _menuItems;
  var _selected = _MenuItem.gameStart;
  var _overlay = false;

  
  void onLoad() {
    // 略
    addAll(
      _menuItems = _MenuItem.values
          .mapIndexed((index, item) => TextComponent(
                text: item.text,
                anchor: Anchor.center,
                textRenderer: _itemTextPaint,
                position: Vector2(size.x / 2, size.y * 2 / 3 + index * 32),
                scale: scale,
              ))
          .toList(),
    );
    _onUpdateIndex();
    add(
      KeyboardListenerComponent(
        keyDown: {
          LogicalKeyboardKey.arrowUp: (pressed) {
            if (_overlay) return false;
            _updateIndex((current) => current - 1);
            return true;
          },
          LogicalKeyboardKey.arrowDown: (pressed) {
            if (_overlay) return false;
            _updateIndex((current) => current + 1);
            return true;
          },
      ),
    );
  }

  void _onUpdateIndex() {
    for (final (index, item) in _menuItems.indexed) {
      item.textRenderer =
          index == _selected.index ? _selectedItemTextPaint : _itemTextPaint;
    }
  }

  void _updateIndex(int Function(int current) fn) {
    final nextIndex = fn(_selected.index) % _MenuItem.values.length;
    _selected = _MenuItem.values[nextIndex];
    _onUpdateIndex();
  }
}
Masatoshi TsushimaMasatoshi Tsushima

google_fonts を見てて LicenseRegistry を使う方法が書いてあったのでそれに習ってライセンスを表示したい。
https://pub.dev/packages/google_fonts

void main() async {
  LicenseRegistry.addLicense(() async* {
    yield LicenseEntryWithLineBreaks(
      ['google_fonts'],
      await rootBundle.loadString('assets/fonts/OFL.txt'),
    );
  });
  runApp(GameWidget(
    game: MyGame(),
    overlayBuilderMap: {
      'licenses': (context, game) => LicenseOverlay(),
    },
  ));
}

LicenseOverlay は普通の Widget で LicenseRegistry.licenses.toList()FutueBuilder で表示するだけのもの。

Masatoshi TsushimaMasatoshi Tsushima

メニュー項目をついかしてライセンスの Overlay を表示させる。表示中は他の操作が効かないようにする。

          LogicalKeyboardKey.space: (pressed) {
            switch (_selected) {
              case _MenuItem.licenses:
                _overlay = true;
                game.overlays.add('licenses');
                break;
              case _MenuItem.exit:
                SystemNavigator.pop();
                break;
            }
            return true;
          },
          LogicalKeyboardKey.escape: (pressed) {
            game.overlays.clear();
            _overlay = false;
            return true;
          },