はじめに
- Flutterでマルチプラットフォーム対応 (Web, モバイル, PC) のRPGゲームを作成します
- ソースコードはこちら (アセットなど一部差し替えています)
- この記事は5つのPartに分かれていて、今回はそのPart 2です。
- Part 1:マップとUIの実装
- Part 2:プレイヤーと当たり判定の実装
- Part 3:マップ移動の実装
- Part 4:アイテムと取得イベントの実装
- Part 5:NPCと会話の実装
- Bonfireという2Dゲームに特化したフレームワーク(Flutterパッケージ)を利用しています
- マップ制作にはTiledという無料のGUIツールを利用しています
Tiledは必須ではありませんが、使い方も簡単で制作作業がとても捗ります - アセットはitch.ioでダウンロードできるものを利用しています
- トップの画像は利用しているアセットのサンプル画像です
開発環境
OS: macOS 12.6
Flutter: 3.3.5
Bonfire: 2.10.10
Tiled: 1.9.2
Xcode: 14.0.1
テスト環境: Chrome, iOS Simulator
今回のパートの大まかな流れ
- プレイヤー用アニメーションスプライトの作成
- プレイヤークラスの作成
- マップ用タイルに当たり判定を設定 (Tiled利用)
- 奥行きを表現する
1. プレイヤー用アニメーションスプライトの作成
1.1 アセットの準備
-
こちらから画像をダウンロード
Download Now
からNo thanks, just take me to the downloads
を選択で無料ダウンロードできます - ダウンロードした画像を
simple_bonfire/assets/images/charactors
に配置
(simple_bonfire
はFlutterプロジェクト名です) -
pubspec.yaml
に追加したアセットのディレクトリを記載
pubspec.yaml
flutter:
uses-material-design: true
assets:
- assets/images/
- assets/images/maps/
- assets/images/charactors/ # ←追加
1.2 追加したアセットからSpriteSheetを作成
- ゲームで使用できるSpriteSheet生成するためのクラスを実装
-
lib
ディレクトリ下にplayer/player_sprite.dart
を作成します - 今回は複数のキャラクターが1枚になったアセットを利用します
- 画像内のそれぞれのコマは、左から右の順でコマ送りになっています
-
simple_bonfire/lib/player/player_sprite.dart
import 'package:bonfire/bonfire.dart';
class PlayerSpriteSheet {
static late SpriteSheet all;
// ゲーム起動時に実行するメソッド
static Future<void> load() async {
// 追加したアセットのパスからSpriteSheetを作成
all = await _create('charactors/franukas_charactors_16x24.png');
}
// 画像からSpriteSheetを生成する処理本体
static Future<SpriteSheet> _create(String path) async {
// ファイルパスから画像を取得して
final image = await Flame.images.load(path);
// 1枚ずつ分割する (横40x縦8)
return SpriteSheet.fromColumnsAndRows(image: image, columns: 40, rows: 8);
}
}
- 起動後にSpriteSheetを作成するよう
main()
に追加
simple_bonfire/lib/main.dart
void main() async {
// ...省略...
// アセットからSpriteSheetを生成
await PlayerSpriteSheet.load(); // ←追加
runApp(const MyApp());
}
2. プレイヤークラスの作成
- 作成したSpriteSheetを実際に表示して動かすプレイヤークラスを作成します
2.1 プレイヤークラスの作成
-
lib/player
ディレクトリ下にplayer_beared_dude.dart
を作成します - プレイヤーは
Bonfire
のSimplePlayer
クラスを継承します-
ObjectCollision Mixin
を継承し当たり判定を持たせます
-
- ここではコンストラクタ内で以下の4つを設定します
-
idle
状態とrun
状態のアニメーション - ゲーム内での表示サイズ
-
run
状態時の移動スピード - 当たり判定
-
-
idle
とrun
にはそれぞれ上下左右があるため、合計8種類分のアニメを設定します
simple_bonfire/lib/player/player_beared_dude.dart
import 'package:bonfire/bonfire.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class PlayerBeardedDude extends SimplePlayer with ObjectCollision {
PlayerBeardedDude(
position, {
required this.spriteSheet,
Direction initDirection = Direction.down,
}) : super(
// SpriteSheetからアニメーションを設定
animation: SimpleDirectionAnimation(
idleDown: spriteSheet.createAnimation(row: 0, stepTime: 0.4, from: 1, to: 3).asFuture(),
idleLeft: spriteSheet.createAnimation(row: 1, stepTime: 0.4, from: 1, to: 3).asFuture(),
idleRight:
spriteSheet.createAnimation(row: 2, stepTime: 0.4, from: 1, to: 3).asFuture(),
idleUp: spriteSheet.createAnimation(row: 3, stepTime: 0.4, from: 1, to: 3).asFuture(),
runDown: spriteSheet.createAnimation(row: 0, stepTime: 0.1, from: 4, to: 8).asFuture(),
runLeft: spriteSheet.createAnimation(row: 1, stepTime: 0.1, from: 4, to: 8).asFuture(),
runRight: spriteSheet.createAnimation(row: 2, stepTime: 0.1, from: 4, to: 8).asFuture(),
runUp: spriteSheet.createAnimation(row: 3, stepTime: 0.1, from: 4, to: 8).asFuture(),
),
// 画面上の表示サイズ
size: Vector2(16, 24) * 3,
// 移動速度
speed: 80 * 3,
position: position,
initDirection: initDirection,
) {
// 当たり判定の設定
setupCollision(
CollisionConfig(
collisions: [
CollisionArea.rectangle(
// 首から足下までの矩形を指定
size: Vector2(size.x * 12 / 16, size.y * 7 / 24),
align: Vector2(size.x * 2 / 16, size.y * 16 / 24),
),
],
),
);
}
final SpriteSheet spriteSheet;
}
-
createAnimation()
メソッドについて
アセットからコマを切り出して、コマ送りアニメーションを生成します
引数の数値の設定方法にクセがあるので要注意です
2.2 マップに配置する
-
Part 1で作成したマップにプレイヤーを追加
- 初期位置と初期方向
initDirection
を適当に決める -
spriteSheet
は先に作成したPlayerSpriteSheet
からall
を取得
- 初期位置と初期方向
- カメラの追従設定を好みで追加
-
moveOnlyMapArea
:true
でタイルの無い部分へのカメラ移動を制限 -
sizeMovementWindow
: プレイヤーを収めるカメラ内の範囲
(広くするほどカメラの追従が甘くなる) -
smoothCameraEnabled
:true
でカメラの動きが滑らかになる -
smoothCameraSpeed
:smoothCameraEnabled: true
時の、カメラの軽快さ
(小さくするとカメラが緩やかに動き始め、緩やかに止まる)
-
simple_bonfire/lib/maps/halloween_map_01.dart
// ...省略...
class _HalloweenMap01State extends State<HalloweenMap01> {
final tileSize = 48.0; // タイルのサイズ定義
@override
Widget build(BuildContext context) {
// 画面
return BonfireWidget(
showCollisionArea: true, // 当たり判定の可視化
// マップ用jsonファイル読み込み
map: WorldMapByTiled(
'maps/halloween_map_01.json',
forceTileSize: Vector2(tileSize, tileSize),
),
// ---------- ここから追加 ----------
// プレイヤーキャラクター
player: PlayerBeardedDude(
Vector2(tileSize * 7.5, tileSize * 4),
spriteSheet: PlayerSpriteSheet.all,
initDirection: Direction.down,
),
// カメラ設定
cameraConfig: CameraConfig(
moveOnlyMapArea: true,
sizeMovementWindow: Vector2.zero(),
smoothCameraEnabled: true,
smoothCameraSpeed: 10,
),
// ---------- ここまで追加 ----------
// ...以下略...
);
}
}
-
BonfireWidget
内にshowCollisionArea: true,
を追加して当たり判定を設定
showCollisionArea
でより当たり判定を可視化することができます
この時点でジョイスティックやWASDキーでキャラクターが操作できる様になっているので、アニメーションや移動の速度、カメラの追従が想定通りに設定できているかチェックしてください
3. マップ用タイルに当たり判定を設定
- プレイヤー以外のオブジェクトに当たり判定を設定します
- Part 1で作成したタイルセットのjsonファイルをTiledを利用して編集します
3.1 柵にシンプルな当たり判定を追加
-
Open file or Project
を選択、Part 1で作成したsimple_bonfire.tiled-project
を開く
左のカラムにTiledプロジェクトのディレクトリと、Part 1で作成したjsonファイルが表示されます
- 左のカラムから
tileset.json
を選択して、Part 1で追加したアセットがタイル状に表示されるのを確認
ここでは上部の柵に当たり判定をつけるため、タイル一覧最上段のID 17
(左から18番目) のタイルを選択します
- メニューから
タイルセット
>タイルの当たり判定エディター
を選択 - 選択したタイルが拡大表示されたら
四角形を追加
を選び、画像の様に一つ四角形を追加する
右側の画面で視覚を追加した後、真ん中のプロパティから値を変更することもできます
- 保存をすると
franuka_halloween_tileset.json
にtiles
内の配列にid: 17
の要素が追加されている
franuka_halloween_tileset.json
{
"columns": 32,
"image": "franuka_halloween_tileset.png",
"imageheight": 512,
"imagewidth": 512,
"margin": 0,
"name": "franuka_halloween_tileset",
"spacing": 0,
"tilecount": 1024,
"tiledversion": "1.9.2",
"tileheight": 16,
"tiles": [
{
"id": 17,
"objectgroup": {
"draworder": "index",
"id": 2,
"name": "",
"objects": [
{
"class": "",
"height": 12,
"id": 4,
"name": "",
"rotation": 0,
"visible": true,
"width": 16,
"x": 0,
"y": 1
}
],
"opacity": 1,
"type": "objectgroup",
"visible": true,
"x": 0,
"y": 0
}
}
],
"tilewidth": 16,
"type": "tileset",
"version": "1.9"
}
3.2 複雑な形状の当たり判定の追加
4. 奥行きを表現する
- 木などの高さのあるオブジェクトは、当たり判定だけではうまく表現できません
- オブジェクトの後ろにキャラクターが回り込める様にして、高さと奥行きを表現します
- 画像は回り込みを実装する前なので、キャラクターが木の上に描画されてしまっています
- 木の根元に当たり判定をつける
柵と同じ要領で、木の根元のタイルに当たり判定をつけます。
今回は左右のタイルに分かれているので、両方に適当な形を追加します。
- 木の上部に
above
属性を付与
木の上部は高さがありキャラクターと衝突しない想定のため、当たり判定はつけません
その代わりに、プロパティ内のClass
にabove
を入力して、キャラクターより手前に描画される様にします
- 保存後
franuka_halloween_tileset.json
内のtiles
にclass: above
のタイルが登録されているのを確認する
franuka_halloween_tileset.json
{
"columns": 32,
"image": "franuka_halloween_tileset.png",
"長いので": "中略",
"tiles": [
{"長いので": "省略"}
{
"class": "above"
"id": 205,
}
{"長いので": "省略"}
],
"tilewidth": 16,
"type": "tileset",
"version": "1.9"
}
- ゲーム画面で当たり判定と、回り込みが再現できているか確認
画像の様に、木のまわりでの衝突と描画が上手く再現できたら完成です。
Bonfireにおけるタイルのタイプは他にもdynamicAbove
があります。
詳しくはこちらをご覧ください。
おわりに
マップとキャラクターの作成ができました。
Bonfire (と裏で動いているFlameエンジン) のおかげで、とてもシンプルな手順でここまでゲーム開発ができます。
次回のパート以降ではマップの移動、アイテムやNPCなどのRPGゲームらしい要素を実装します。
いいね、質問やコメントお待ちしております!