8
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ただ🐈が歩き回るだけのゲーム作った

Posted at

はじめに

概要

Flutterで遊んでみよう期間が続いています。
今回は「RPG風のマップ」上に存在する🐈を、ただグリグリ動かせるコードを書きました。

cat.gif

記事の内容

前回のブロック崩しに引き続きFlameを使用しました。

Flameの概念や簡単な使い方は上記の前回記事で触れているため割愛します。

  • アニメーションするキャラクターの作成
  • マップの生成
  • キャラクターを追尾するカメラ
  • ジョイスティックによるキャラクター操作
  • 障害物との当たり判定

等々のRPGっぽい2Dゲームを作る必要最小限の実装について以降では説明します。

ちなみに本気で(Flutter使って)RPGを作りたい場合はFlameをもとにしたBonfireがあるので、そちらを使用する方が開発コスト的に良いかと思います。

環境

Flutter 2.8.0
Flame 1.0.0

実装編

アニメーションするキャラクターの作成

一般的なRPGのように立ち絵がアニメーションするキャラクターを作るには、スプライトシートを利用するのが一般的です。

「⁠スプライトシート」というのは,アニメーションのひとコマひとコマをひとつにまとめた画像ファイルだ。ファイルをひとつにすると,読込みが1回で済むため効率がいい。画像からそれぞれのコマのグラフィックイメージを切り出して,アニメーションとして再生する。パフォーマンスを重く見るゲームコンテンツでも使われる仕組みだ。

第12回 スプライトシートでアニメーションをつくる

ひとつのファイル内に複数コマをまとめた画像のことをスプライトシートと呼びます。
今回の🐈の場合は下記のようなcat.pngを準備しました(画像はぴぽや倉庫さんにお借りしました)。

image.png

1行目が🐈が下に進んでいるときのアニメーション、2行目が左、3行目が右、4行目が上です。

SpliteSheetに各コマのサイズ(今回の場合各コマは32×32ピクセル)と画像ファイルを渡して読み込みます。
後はcreateAnimation()で「どの行を読み込むか」「各行の長さ」等々を指定してあげるだけでOKです。

cat.dart
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プロパティに代入するだけでキャラクターがアニメーションを始めます。
上を向いたときには上向きのアニメーションが、下を向いたときには下向きのアニメーションが...といったように、キャラクターの方向と対応するアニメーションが読み込まれるようにしています。

cat.dart
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等)を読み込む方法もありますが、大きなマップになると画像の重さがキツくなってきます。
今回はタイルセット(マップチップ)を使ってマップを作成することにしました。
タイルセットとは、マップのもととなる要素(木、芝生、花...)が敷き詰められた画像のことです(こちらもぴぽや倉庫さんのものを利用しています)。

image.png

タイルセットをTiled(Tiled Map Editor)等のマップエディタに読み込めば任意のマップを作成することができます。

Bonfireの公式も推奨しているので、今回はTiledを利用しました。
Tiledの詳細な使い方は下記の記事等が詳しいです。

簡単な操作感としては、タイルセットの画像から任意の要素をドラッグ&ドロップしながらマップを作っていく感覚です。
レイヤの構造の定義等もできます。最終的には下記のようなマップを作成しました。
衝突するための障害物と芝生だけを敷き詰めた汚いマップです。

image.png

Tiledで作成したマップは.tmx.tsx拡張子のファイルで出力されます。
ざっくり言えば.tmxにはマップの情報が、.tsxにはタイルセットの情報が載っています。

Flutterで使う上では、まず上記の2ファイルと、タイルセットの画像ファイルをFlutterのassetsディレクトリに格納しpubspec.yamlで指定します。

pubspec.yaml
assets:
    ...
    - assets/tilesets.png
    - assets/tilesets.tsx
    - assets/grassland.tmx

FlameのTiledComponent.tmxファイルを読み込むとマップが表示されます(.tmxファイル内で.tsxやタイルセットの画像ファイルを参照しています)。

game.dart
@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()に入れるだけで実現できます。

game.dart
@override
  Future<void>? onLoad() async {
    ...
    camera.followComponent(cat);
    ...
  }

ジョイスティックによるキャラクター操作

キャラクターをグリグリ動かすジョイスティックはJoystickComponentで実現できます。

joystick.dart
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));
}

ジョイスティックがどれくらい動いているかの情報を別のコンポーネントに渡したい場合は、コンストラクタで渡してあげます。
今回の場合、🐈をグリグリ動かしたいので🐈のコンストラクタにジョイスティックを渡します。

game.dart
@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)スゴイ。

余談

当たり判定の実装は超適当なので、壁に衝突したまま他の壁に衝突するとセカイの外に出て戻って来れなくなります。

cat2.gif

参考

こちらの記事を大いに参考にさせていただきました。

  1. 実際にはもっと綺麗に書ける。障害物が存在するタイルを1、それ以外を0にしたJSONを読み込む、等。めんどいのでやってないが

8
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?