はじめに
概要
Flutterで遊んでみよう期間が続いています。
今回は「RPG風のマップ」上に存在する🐈を、ただグリグリ動かせるコードを書きました。
記事の内容
前回のブロック崩しに引き続きFlameを使用しました。
Flameの概念や簡単な使い方は上記の前回記事で触れているため割愛します。
- アニメーションするキャラクターの作成
- マップの生成
- キャラクターを追尾するカメラ
- ジョイスティックによるキャラクター操作
- 障害物との当たり判定
等々のRPGっぽい2Dゲームを作る必要最小限の実装について以降では説明します。
ちなみに本気で(Flutter使って)RPGを作りたい場合はFlameをもとにしたBonfireがあるので、そちらを使用する方が開発コスト的に良いかと思います。
環境
Flutter 2.8.0
Flame 1.0.0
実装編
アニメーションするキャラクターの作成
一般的なRPGのように立ち絵がアニメーションするキャラクターを作るには、スプライトシートを利用するのが一般的です。
「スプライトシート」というのは,アニメーションのひとコマひとコマをひとつにまとめた画像ファイルだ。ファイルをひとつにすると,読込みが1回で済むため効率がいい。画像からそれぞれのコマのグラフィックイメージを切り出して,アニメーションとして再生する。パフォーマンスを重く見るゲームコンテンツでも使われる仕組みだ。
ひとつのファイル内に複数コマをまとめた画像のことをスプライトシートと呼びます。
今回の🐈の場合は下記のようなcat.png
を準備しました(画像はぴぽや倉庫さんにお借りしました)。
1行目が🐈が下に進んでいるときのアニメーション、2行目が左、3行目が右、4行目が上です。
SpliteSheet
に各コマのサイズ(今回の場合各コマは32×32ピクセル)と画像ファイルを渡して読み込みます。
後はcreateAnimation()
で「どの行を読み込むか」「各行の長さ」等々を指定してあげるだけでOKです。
Future<void> _loadAnimations() async {
final spriteSheet = SpriteSheet(
image: await gameRef.images.load('cat.png'),
srcSize: Vector2(32.0, 32.0),
);
_runDownAnimation =
spriteSheet.createAnimation(row: 0, stepTime: _animationSpeed, to: 3);
_runLeftAnimation =
spriteSheet.createAnimation(row: 1, stepTime: _animationSpeed, to: 3);
_runRightAnimation =
spriteSheet.createAnimation(row: 2, stepTime: _animationSpeed, to: 3);
_runUpAnimation =
spriteSheet.createAnimation(row: 3, stepTime: _animationSpeed, to: 3);
}
読み込んだアニメーションは、SpriteAnimationComponent
等が持つanimation
プロパティに代入するだけでキャラクターがアニメーションを始めます。
上を向いたときには上向きのアニメーションが、下を向いたときには下向きのアニメーションが...といったように、キャラクターの方向と対応するアニメーションが読み込まれるようにしています。
SpriteAnimation _updateAnimation(direction d) {
if (d == direction.up) {
return _runUpAnimation;
} else if (d == direction.right) {
return _runRightAnimation;
} else if (d == direction.down) {
return _runDownAnimation;
} else {
return _runLeftAnimation;
}
}
@override
void update(double dt) {
...
animation = _updateAnimation(currentDirection);
...
}
マップの生成
背景であるマップ画像は、一枚絵の画像ファイル(png等)を読み込む方法もありますが、大きなマップになると画像の重さがキツくなってきます。
今回はタイルセット(マップチップ)を使ってマップを作成することにしました。
タイルセットとは、マップのもととなる要素(木、芝生、花...)が敷き詰められた画像のことです(こちらもぴぽや倉庫さんのものを利用しています)。
タイルセットをTiled(Tiled Map Editor)等のマップエディタに読み込めば任意のマップを作成することができます。
Bonfireの公式も推奨しているので、今回はTiledを利用しました。
Tiledの詳細な使い方は下記の記事等が詳しいです。
簡単な操作感としては、タイルセットの画像から任意の要素をドラッグ&ドロップしながらマップを作っていく感覚です。
レイヤの構造の定義等もできます。最終的には下記のようなマップを作成しました。
衝突するための障害物と芝生だけを敷き詰めた汚いマップです。
Tiledで作成したマップは.tmx
と.tsx
拡張子のファイルで出力されます。
ざっくり言えば.tmx
にはマップの情報が、.tsx
にはタイルセットの情報が載っています。
Flutterで使う上では、まず上記の2ファイルと、タイルセットの画像ファイルをFlutterのassets
ディレクトリに格納しpubspec.yaml
で指定します。
assets:
...
- assets/tilesets.png
- assets/tilesets.tsx
- assets/grassland.tmx
FlameのTiledComponent
に.tmx
ファイルを読み込むとマップが表示されます(.tmx
ファイル内で.tsx
やタイルセットの画像ファイルを参照しています)。
@override
Future<void>? onLoad() async {
...
final tiledMap = await TiledComponent.load(
'grassland.tmx', Vector2(32, 32));
add(WorldMap(tiledMap, Vector2(0, 0), Vector2(canvasSize.x, canvasSize.y)));
...
}
キャラクターを追尾するカメラ
RPGを行う上ではキャラクターが画面の中央に表示されているのが好ましいです。
Flameでは追尾したいコンポーネントをcamera.followComponent()
に入れるだけで実現できます。
@override
Future<void>? onLoad() async {
...
camera.followComponent(cat);
...
}
ジョイスティックによるキャラクター操作
キャラクターをグリグリ動かすジョイスティックはJoystickComponent
で実現できます。
class JoystickController extends JoystickComponent {
JoystickController(Paint knobPaint, Paint backgroundPaint)
: super(
background: CircleComponent(radius: 100, paint: backgroundPaint),
knob: CircleComponent(radius: 30, paint: knobPaint),
margin: const EdgeInsets.only(left: 40, bottom: 40));
}
ジョイスティックがどれくらい動いているかの情報を別のコンポーネントに渡したい場合は、コンストラクタで渡してあげます。
今回の場合、🐈をグリグリ動かしたいので🐈のコンストラクタにジョイスティックを渡します。
@override
Future<void>? onLoad() async {
...
final joystick = JoystickController(knobPaint, backgroundPaint);
await images.load('cat.png');
final cat = Cat(joystick);
...
}
参照渡しになっているので、ジョイスティックの情報が都度渡る形になります。
障害物との当たり判定
当たり判定はかなりマッチョな実装になってしまったのでコード自体は割愛します。
ざっくり言うと、マップは32×32ピクセルのタイルから生成されているので、その内障害物が存在するタイルの座標に32×32ピクセルのHitbox
を配置する実装です1。
本来なら、Tiledではマップに衝突判定等の情報も付与できます。
もしこの情報が読み込めるのならもっとエレガントに書けそうですが、FlameのTiledComponent
では読み込めないっぽいので諦めました(調査不足の可能性アリ)。
最後に
昔から2DのRPGをどう作るのか興味があったためRPGのプロトタイプ的コードを書いてみた、というのが本記事のモチベーションでした。
必要最小限の理解はできたかなと思います。
それにしても初心者でも簡単に触れるFlutter(Flame)スゴイ。
余談
当たり判定の実装は超適当なので、壁に衝突したまま他の壁に衝突するとセカイの外に出て戻って来れなくなります。
参考
こちらの記事を大いに参考にさせていただきました。
-
実際にはもっと綺麗に書ける。障害物が存在するタイルを1、それ以外を0にしたJSONを読み込む、等。めんどいのでやってないが ↩