くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

FlameのGameLoopはどう実現しているのか

前回、FlutterのゲームエンジンFlame
に入門してみたけど、GameLoopってどうしてるのか気になり、
いろいろ調べてみたときの備忘録(*´ω`*)

Tickerで実現してるっぽい

ソースを見るとすごくシンプル

import 'package:flutter/scheduler.dart';

class GameLoop {
  GameLoop(this.callback) {
    _ticker = Ticker(_tick);
  }
  
  void Function(double dt) callback;
  Duration _previous = Duration.zero;
  late final Ticker _ticker;

  /// This method is periodically invoked by the [_ticker].
  void _tick(Duration timestamp) {
    final durationDelta = timestamp - _previous;
    final dt = durationDelta.inMicroseconds / Duration.microsecondsPerSecond;
    _previous = timestamp;
    callback(dt);
  }
  
  void start() {
    if (!_ticker.isActive)  _ticker.start();
  }

  void stop() {
    _ticker.stop();
    _previous = Duration.zero;
  }

  void step(double stepTime) {
    if (!_ticker.isActive) callback(stepTime);
  }
  
  void dispose() {
    _ticker.dispose();
  }
}

FlutterのTickerを使っていて、
Tickerはアニメーションフレームごとに関数を呼び出してくれるやつ

GameLoopはRenderGameWidgetでつくられる

FlameはRenderObjectWidgetとRenderBoxを継承した、
RenderGameWidgetとGameRenderBoxで描画しているっぽい。

GameLoopはGameRenderBoxがattachされたときに生成&スタートしているよう。

/// A [RenderObjectWidget] that renders the [GameRenderBox].
///
/// This is the widget that is used by the [GameWidget] to ACTUALLY
/// render the game.
class RenderGameWidget extends LeafRenderObjectWidget {
  // ...略
  @override
  RenderBox createRenderObject(BuildContext context) {
    return GameRenderBox(game, context, isRepaintBoundary: addRepaintBoundary);
  }
  // ...略
}

class GameRenderBox extends RenderBox with WidgetsBindingObserver {
  // ...略
  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
    _attachGame(owner);
  }
  
  void _attachGame(PipelineOwner owner) {
    game.attach(owner, this);

    final gameLoop = this.gameLoop = GameLoop(gameLoopCallback);

    if (!game.paused) {
      gameLoop.start();
    }

    _bindLifecycleListener();
  }
  // ...略
  void gameLoopCallback(double dt) {
    assert(attached);
    if (!attached) {
      return;
    }
    game.update(dt);
    markNeedsPaint();
  }
  // ...略
}

RenderGameWidgetは、GameWidgetで生成/組込され、
GameWidgetはStatefulWidgetを継承してる。

class GameWidget<T extends Game> extends StatefulWidget {
  // ...略
}

class GameWidgetState<T extends Game> extends State<GameWidget<T>> {
  // ...略
  @override
  Widget build(BuildContext context) {
    return _protectedBuild(() {
      Widget? internalGameWidget = RenderGameWidget(
        game: currentGame,
        addRepaintBoundary: widget.addRepaintBoundary,
      );
      // ... 略
    );
  }
}

GameLoopの一時停止/再開はGameで

GameLoopの操作はGameから行っている感じ
GameRenderBoxがattachされたとき(GameRenderBoxのattach())に、
GameRenderBoxがGameをattachする(game.attach())っぽい。

abstract mixin class Game {
  // ...略
  /// Marks game as attached to any Flutter widget tree.
  ///
  /// Should not be called manually.
  void attach(PipelineOwner owner, GameRenderBox gameRenderBox) {
    // ...略
    _gameRenderBox = gameRenderBox;
    // ...略
  }
  
  // ...略
  /// Pauses or resume the engine
  set paused(bool value) {
    _paused = value;

    final gameLoop = _gameRenderBox?.gameLoop;
    if (gameLoop != null) {
      if (value) {
        gameLoop.stop();
      } else {
        gameLoop.start();
      }
    }
  }
  
  /// Pauses the engine game loop execution.
  void pauseEngine() {
    _paused = true;
    _gameRenderBox?.gameLoop?.stop();
  }

  /// Resumes the engine game loop execution.
  void resumeEngine() {
    _paused = false;
    _gameRenderBox?.gameLoop?.start();
  }

  /// Steps the engine game loop by one frame. Works only if the engine is in
  /// paused state. By default step time is assumed to be 1/60th of a second.
  void stepEngine({double stepTime = 1 / 60}) {
    if (_paused) {
      _paused = false;
      _gameRenderBox?.gameLoop?.step(stepTime);
      _paused = true;
    }
  }
  // ...略
}

まとめ

Flameでは、GameがGameLoopをもつっぽい感じだと思ったけど、

  • GameWidget extends StatefulWidget
  • RenderGameWidget extends LeafRenderObjectWidget
  • GameRenderBox extends RenderBox with WidgetsBindingObserver
    • ここでGameLoopを生成
    • &gameのupdate()を呼び出す

という感じっぽい。

Game/GameWidget/RenderGameWidget/GameRenderBox/GameLoopは、
役割ごとに分かれているけど、どれも1:1で、共通的に管理されているっぽい。

なるほど。。(*´ω`*)