Ivy Walobwaさんが、Flutter Flameを使ったゲームの作り方をご紹介いたします。
Flutterでは、単一のコードベースからAndroid、iOS、デスクトップ、ウェブなどのプラットフォーム向けのアプリケーションを開発することが可能です。マルチプラットフォームのUIツールキットとして、Flutterチームは、あらゆる開発者が迅速にアプリケーションを構築し、リリースできるようにすることに専念しています。例えば、ゲーム開発者は、パフォーマンスやロード時間、アプリサイズを気にすることなく、美しいゲームアプリを作ることができるようになりました。
本チュートリアルでは、Flutter Flameゲームエンジンの入門編をご提供いたします。Flutter Flameのゲームの設定と構築、スプライトの読み込み、アニメーションの追加をご紹介いたします。
本チュートリアルは、DartとFlutterの知識をお持ちであることを前提にしております。
Flameエンジン
Flameは、Flutter上で動作する2Dゲーム開発フレームワークです。Flameエンジンでは、ゲームループをはじめ、アニメーション、衝突・跳ね返り検出、視差スクロールなど、必要な機能を簡単に実装できます。
Flameはモジュール化されており、以下のような機能拡張に利用できる独立したパッケージが提供されています:
- flame_audio: オーディオ機能を提供。
- flame_forge2d: 物理機能を提供。
- flame_tiled: タイル操作機能を提供。
Flutter Flameの設定
Flameを使い始めるには、パッケージのインストールが必要です。pubspec.yaml
ファイルに、以下のように依存関係を追加します:
dependencies:
flame: ^1.1.1
ゲームをレンダリングするには、GameWidget
を使用します。以下のコードスニペットを main.dart
ファイルに追加すると、現在黒い画面であるFlameゲームがレンダリングされます。
void main() {
final game = FlameGame();
runApp(
GameWidget(
game: game,
),
);
}
これで、ゲームにグラフィックを追加する準備が整いました。
スプライトの読み込み
静的な画像をレンダリングするには、SpriteComponent
クラスを利用する必要があります。ゲームグラフィックを assets/images
フォルダに追加し、アセットを読み込むためにpubspec.yaml
ファイルを更新します。本チュートリアルでは、プレイヤー画像と背景画像が読み込まれます。
以下の3つのファイルを lib
フォルダに作成・更新します:
dino_player.dart
はプレイヤーの読み込みと配置を行います:
import 'package:flame/components.dart';
class DinoPlayer extends SpriteComponent with HasGameRef {
DinoPlayer() : super(size: Vector2.all(100.0));
@override
Future<void> onLoad() async {
super.onLoad();
sprite = await gameRef.loadSprite('idle.png');
position = gameRef.size / 2;
}
}
dino_world.dart
は、ゲームの背景を読み込みます:
import 'package:flame/components.dart';
class DinoWorld extends SpriteComponent with HasGameRef {
@override
Future<void> onLoad() async {
super.onLoad();
sprite = await gameRef.loadSprite('background.png');
size = sprite!.originalSize;
}
}
dino_game.dart
は、すべてのゲームコンポーネントを管理します。ゲームプレイヤーと背景を追加し、それらを配置します:
import 'dart:ui';
import 'package:flame/game.dart';
import 'dino_player.dart';
import 'dino_world.dart';
class DinoGame extends FlameGame{
DinoPlayer _dinoPlayer = DinoPlayer();
DinoWorld _dinoWorld = DinoWorld();
@override
Future<void> onLoad() async {
super.onLoad();
await add(_dinoWorld);
await add(_dinoPlayer);
_dinoPlayer.position = _dinoWorld.size / 1.5;
camera.followComponent(_dinoPlayer,
worldBounds: Rect.fromLTRB(0, 0, _dinoWorld.size.x, _dinoWorld.size.y));
}
}
camera.followComponent
関数は、ゲームビューポートをプレイヤーに追従するように設定します。この関数は、プレイヤーに動きを追加するために必要です。
以下のように main.dart
ファイルを更新し、DinoGame
を読み込みます:
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'dino_game.dart';
void main() {
final game = DinoGame();
runApp(
GameWidget(game: game),
);
}
アプリケーションを実行すると、プレイヤーと背景が表示されるはずです。
スプライトの動き
プレイヤーを動かすには、選択した方向を検知し、それに対応する必要があります。本チュートリアルでは、ゲームの矢印キーを使って、プレイヤーに動きを追加していきます。
まず、以下のファイルを含むhelpers
フォルダを作成し、以下のように更新してください:
directions.dart
には、方向列挙型が含まれています:
enum Direction { up, down, left, right, none }
navigation_keys.dart
には、ナビゲーションキーのUIとロジックが含まれています:
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'directions.dart';
class NavigationKeys extends StatefulWidget {
final ValueChanged<Direction>? onDirectionChanged;
const NavigationKeys({Key? key, required this.onDirectionChanged})
: super(key: key);
@override
State<NavigationKeys> createState() => _NavigationKeysState();
}
class _NavigationKeysState extends State<NavigationKeys> {
Direction direction = Direction.none;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 200,
width: 120,
child: Column(
children: [
ArrowKey(
icons: Icons.keyboard_arrow_up,
onTapDown: (det) {
updateDirection(Direction.up);
},
onTapUp: (dets) {
updateDirection(Direction.none);
},
onLongPressDown: () {
updateDirection(Direction.up);
},
onLongPressEnd: (dets) {
updateDirection(Direction.none);
},
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ArrowKey(
icons: Icons.keyboard_arrow_left,
onTapDown: (det) {
updateDirection(Direction.left);
},
onTapUp: (dets) {
updateDirection(Direction.none);
},
onLongPressDown: () {
updateDirection(Direction.left);
},
onLongPressEnd: (dets) {
updateDirection(Direction.none);
},
),
ArrowKey(
icons: Icons.keyboard_arrow_right,
onTapDown: (det) {
updateDirection(Direction.right);
},
onTapUp: (dets) {
updateDirection(Direction.none);
},
onLongPressDown: () {
updateDirection(Direction.right);
},
onLongPressEnd: (dets) {
updateDirection(Direction.none);
},
),
],
),
ArrowKey(
icons: Icons.keyboard_arrow_down,
onTapDown: (det) {
updateDirection(Direction.down);
},
onTapUp: (dets) {
updateDirection(Direction.none);
},
onLongPressDown: () {
updateDirection(Direction.down);
},
onLongPressEnd: (dets) {
updateDirection(Direction.none);
},
),
],
),
);
}
void updateDirection(Direction newDirection) {
direction = newDirection;
widget.onDirectionChanged!(direction);
}
}
class ArrowKey extends StatelessWidget {
const ArrowKey({
Key? key,
required this.icons,
required this.onTapDown,
required this.onTapUp,
required this.onLongPressDown,
required this.onLongPressEnd,
}) : super(key: key);
final IconData icons;
final Function(TapDownDetails) onTapDown;
final Function(TapUpDetails) onTapUp;
final Function() onLongPressDown;
final Function(LongPressEndDetails) onLongPressEnd;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: onTapDown,
onTapUp: onTapUp,
onLongPress: onLongPressDown,
onLongPressEnd: onLongPressEnd,
child: Container(
margin: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0x88ffffff),
borderRadius: BorderRadius.circular(60),
),
child: Icon(
icons,
size: 42,
),
),
);
}
}
次に、main.dart
ファイルを更新して、以下のようにゲームとキーを表示します:
void main() {
final game = DinoGame();
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Stack(
children: [
GameWidget(
game: game,
),
Align(
alignment: Alignment.bottomRight,
child: NavigationKeys(onDirectionChanged: game.onArrowKeyChanged,),
),
],
),
),
),
);
}
以下の関数をdino_game.dart
ファイルに追加して、プレイヤーの動きを実行します:
onArrowKeyChanged(Direction direction){
_dinoPlayer.direction = direction;
}
最後に、dino_player.dart
ファイルを更新して、以下のコードスニペットを含めてプレイヤーの位置を更新します:
Direction direction = Direction.none;
@override
void update(double dt) {
super.update(dt);
updatePosition(dt);
}
updatePosition(double dt) {
switch (direction) {
case Direction.up:
position.y --;
break;
case Direction.down:
position.y ++;
break;
case Direction.left:
position.x --;
break;
case Direction.right:
position.x ++;
break;
case Direction.none:
break;
}
}
アプリケーションを起動し、矢印キーのいずれかを押すと、プレイヤーの位置が更新されるはずです。
スプライトアニメーション
さて、プレイヤーは期待通りに動きますが、その動きはまだ自然に見えるようなアニメーションではありません。プレイヤーをアニメーションさせるには、スプライトシートを活用する必要があります。
スプライトシートは、行と列に並べられたスプライトの集合体です。個々のスプライトに比べ、読み込みが早いのが特長です。Flameエンジンは、スプライトシートの一部分のみを読み込んでレンダリングできます。下の画像は、ディノプレイヤーのスプライトシートを表示したものです。
スプライトシートには、右や左に歩くなどの動作を表現するためにアニメーションさせることができる、さまざまなプレイヤーフレームが含まれています。スプライトシートはassets/images
フォルダに追加されます。
プレイヤーをアニメーションさせるには、dino_player.dart
ファイルで以下のようにします:
SpriteComponent
の代わりにSpriteAnimationComponent
を拡張します。- アニメーションとアニメーションスピードを初期化します。このチュートリアルでは、左右に歩くアニメーションに焦点をあてて説明します。
late final SpriteAnimation _walkingRightAnimation;
late final SpriteAnimation _walkingLeftAnimation;
late final SpriteAnimation _idleAnimation;
final double _animationSpeed = .15;
- スプライトシートからスプライトを読み込みます。スプライトは、シート上の位置に応じて読み込まれます。スプライトの読み込みは、各スプライトの幅と列を指定する方法と、行と列の位置から各スプライトを選択する方法とがあります。
Future<void> _loadAnimations() async {
final spriteSheet = SpriteSheet.fromColumnsAndRows(
image: await gameRef.images.load('spritesheet.png'),
columns: 30,
rows: 1);
_idleAnimation = spriteSheet.createAnimation(
row: 0, stepTime: _animationSpeed, from: 0, to: 9);
_walkingRightAnimation = spriteSheet.createAnimation(
row: 0, stepTime: _animationSpeed, from: 10, to: 19);
_walkingLeftAnimation = spriteSheet.createAnimation(row: 0, stepTime: _animationSpeed, from: 20, to: 29);
}
spriteSheet.createAnimation
関数は、row
、from
、to
プロパティで定義された一連のスプライトを選択し、アニメーションを作成します。
- プレイヤーを更新して選択されたアニメーションを読み込みます。
まず、onLoad
関数をオーバーライドして、_idleAnimation
を読み込みます。
@override
Future<void> onLoad() async {
super.onLoad();
await _loadAnimations().then((_) => {animation = _idleAnimation});
}
次に updatePosition
関数を更新して、プレイヤーの向いている方向によって異なるアニメーションを読み込むようにします。このチュートリアルでは、アイドル状態、右移動、左移動のスプライトを用意しています。
updatePosition(double dt) {
switch (direction) {
case Direction.up:
position.y --;
break;
case Direction.down:
position.y ++;
break;
case Direction.left:
animation = _walkingLeftAnimation;
position.x --;
break;
case Direction.right:
animation = _walkingRightAnimation;
position.x ++;
break;
case Direction.none:
animation = _idleAnimation;
break;
}
}
アプリを起動して左右に動かすと、プレイヤーの動きが更新され、よりリアルに見えるようになりました。
おめでとうございます! Flameを使って初めて簡単なゲームを作りました!
flame_tiledパッケージを使用すると、衝突レイヤーを追加したカスタムマップやタイルをアプリに読み込んで、ゲームを向上させることができます。マップやタイルをデザインするには、Tiledを使って作成する方法を知っておく必要があります。
また、flame_audioパッケージを使用して、ゲームに音声を追加することもできます。
Codemagicでアプリの成果物を構築して共有する
さて、Flameエンジンを使ってゲームを作成したわけですが、簡単にビルドしてアプリの成果物を共有するにはどうしたらいいでしょうか? 解決策としては、CodemagicのようなCI/CDツールを使って、プロジェクトのビルド、テスト、リリースのすべてを自動的に処理することが挙げられます。
Codemagicでアプリの成果物をビルドして共有するには、まずFlutterアプリをお気に入りのGitプロバイダーにホストしておく必要があります。アプリをリリースするための準備として、以下が必要です:
- アプリランチャーアイコンを設定する
- アプリ名を設定する
- 一意なアプリIDを割り当てる
Flutter公式ドキュメントにあるガイドに従って、アプリをリリースするための準備ができます。
その後、CI/CDツールを使用するためにCodemagicのアカウントが必要になります。まだお持ちでない方は、GitプロバイダーでCodemagicにサインアップできます。以下の手順でCodemagicをセットアップしてください:
-
アプリケーションを作成し、Gitプロバイダーからリポジトリを接続します。
-
プロジェクトのリポジトリとタイプを選択します。この場合、プロジェクトタイプは「Flutter App (via WorkFlow Editor)」となります。
アプリの準備ができましたので、アプリの構築方法を決定するための設定をいくつか追加します。
アプリをビルドするには、ビルド設定をアプリに合わせてカスタマイズする必要があります:
- 初めてのアプリケーションの場合は、「ビルド設定の完了」をクリックします。既存のアプリの場合は、設定アイコンをクリックします。
- ワークフローエディタページで、ビルドプラットフォームとして「Android」を選択します。
- 「ビルドトリガー」セクションを展開し、希望のビルドトリガーを選択します。監視対象のブランチとタグを設定することもできます。これらの設定は、発生するたびにアプリのビルドをトリガーします。
- 「ビルド」セクションを展開し、アプリのビルド形式とモードを選択します。
- 変更を保存して、新しいビルドを開始します。Codemagicは、アプリが正常にビルドされると、アプリ名の横に緑色のチェックを追加します。また、ダウンロード可能なAndroidの成果物も追加されます。
おめでとうございます! Codemagicで最初のビルドを行い、アプリの成果物をダウンロードして共有できるようになりました!
結論
Flameは、Flutterをベースに開発された軽量なゲームエンジンで、開発者は2Dゲームを迅速に作成できます。
本チュートリアルでは、Flameのインストールとセットアップの方法をご紹介しました。また、Flutter Flameのゲームサンプルに取り組むことで、スプライトの読み込みとスプライトの動きやアニメーションを追加する方法もご説明しました。ゲームを充実させるために使える、さまざまな独立したパッケージを取り上げました。最後に、Codemagicでアプリの成果物をビルドして共有する方法をご紹介いたしました。
本チュートリアルで使用したアプリケーションは、GitHubに掲載されております。このチュートリアルをお楽しみいただけたら幸いです!
本記事は、Flutterの一開発者でありテクニカルオーサーのIvy Walobwaさんが執筆いたしました。Ivyさんは、コミュニティに情熱を持ち、技術分野で学生たちの学習を促進することに常に意欲的です。コミュニティを作ったり、コンテンツを作ったりしていないときは、おそらくどこかでハイキングをされているかもしれません。彼女のプロフィールをご覧ください。