数年ぶりにUnityを触ったら気持ちが盛り上がってしまい、簡単なアドベンチャーゲームを作るための仕組みを作ってみました。
制作過程のスクリーンショットは以下のモーメントにまとめていたのですが、この記事でどんな流れ・どんな仕組みでアドベンチャーゲームツクールを作成していったのかまとめようと思います。
前提として、今回作成するのは以下のようなアドベンチャーゲームを作る仕組みです。
- 2Dトップビューのグリッドマップ
- プレイヤーはタップ(クリック)した箇所に自動的に移動
- タップした箇所にイベントが設定されていたらイベントを再生
- イベントの内容はスクリプト言語Luaで記述できるようにする
イベントの内容は以下のようなLua言語で記述できるようになっています。
技術的な部分を全て解説しようとすると長くなりすぎてしまうので、「こんな技術要素を使ってでこんな仕組みを作ったよ」程度にとどめて説明します。

みどー
イメージとしてはドラクエの仕組みから戦闘システムを引き算したようなゲームが作れる仕組みです。細かい部分わかりづらければXのDMなどで質問いただければ(その時もし暇なら)答えることもできます。
Table of Contents
Step1: マップを表示する
まずは、以下のツイートの画像にあるような簡単なマップを画面上に表示できるようにします。
これはUnityのTilemapという仕組みを使うことで簡単に実現できます。タイルマップの使い方は以下のマニュアルを参考にしてください。
Step2:プレイヤーの移動
次にプレイヤーの移動を実装します。
A*アルゴリズムを使ってクリックした位置移動するための経路を導出しています。上記のツイートのGIF画像では花を障害物に見立てており、花が存在するセルは通行不可としています。
A*アルゴリズムの解説記事は以下がわかりやすくて参考になりました。
Step3:プレイヤーの向きを考慮する
プレイヤーが1セル動くごとに移動方向を計算して、プレイヤーの向きを変更します。
ここはそんなに難しくないので詰まらないはず。
Step4:メッセージウィンドウを実装
今後、会話イベントなどを作る際にメッセージを表示する仕組みが必要になるので先に実装してしまいます。
以下のツイートではメッセージウィンドウを作ったあと、動作確認としてプレイヤーの移動位置をメッセージウィンドウに表示するようにしています(あくまで動作確認なのでこの実装は不要)
ちなみに文字が少しずつ表示されるように制御するプログラムはUniTaskを利用して以下のように実装しています。
public class Message : MonoBehaviour
{
/** メッセージウィンドウのテキスト */
[SerializeField] private TextMeshProUGUI _messageText = default;
/** 1文字表示にかかる時間 */
private int _messageDurationMillSec = 25;
public async UniTask ShowMessage(string message)
{
for (int index = 0; index <= message.Length; index++)
{
_messageText.SetText(message.Substring(0, index));
await UniTask.Delay(_messageDurationMillSec);
}
}
}
Step5:Luaスクリプトから画面を操作できるようにする
ここが今回の肝となる部分でLuaスクリプトから「メッセージの表示」「キャラクターの移動」などを行えるように実装しました。
LuaスクリプトをC#から実装するために、MoonSharpというライブラリを利用しています。
Luaスクリプトを実行しているクラスは以下のような実装になっています。
public class LuaInterpreter
{
private DynValue _c = null;
private Script _interpreter;
private Dictionary<string, LuaInterpreterHandlerBase> _handlers = new Dictionary<string, LuaInterpreterHandlerBase>();
public LuaInterpreter(string script)
{
_interpreter = new Script();
UserData.RegisterAssembly();
_interpreter.DoString(script);
}
public void AddHandler(string key, LuaInterpreterHandlerBase handler)
{
_interpreter.Globals[key] = handler;
_handlers.Add(key, handler);
}
public bool HasNextScript()
{
return _c == null || _c.Coroutine.State != CoroutineState.Dead;
}
public void Resume()
{
if (_c == null)
{
DynValue func = _interpreter.Globals.Get("main");
_c = _interpreter.CreateCoroutine(func);
}
_c.Coroutine.Resume();
}
public async UniTask WaitUntilFinishCurrentTask()
{
foreach (var handler in _handlers.Values)
{
await handler.WaitUntilFinishCurrentTask();
}
}
}
上記のクラスの利用イメージは以下の通り。
スクリプトを文字列で渡して初期化、その後Luaスクリプトから呼び出される処理(=LuaInterpreterHandlerBaseを継承したクラス)をAddHandlerで登録し、Resumeを繰り返し呼ぶことでスクリプトが進行します。
Resumeを1回呼ぶと、Luaスクリプト内でcorutine.yield()メソッドが呼ばれる箇所までスクリプトが進みます。その後、WaitUntilFinishCurrentTaskメソッドでハンドラの処理が終わるのを非同期で待ち受けています。
public async UniTask ExecuteEvent(string script)
{
var interpreter = new LuaInterpreter(script); // スクリプトを渡して初期化
interpreter.AddHandler("message", messageHandler); // メッセージ制御のハンドラを登録
interpreter.AddHandler("player", playerHandler); // プレイヤー制御のハンドラを登録
while (interpreter.HasNextScript())
{
interpreter.Resume();
await interpreter.WaitUntilFinishCurrentTask();
}
}
ハンドラのベースクラス「LuaInterpreterHandlerBase」は以下のようになっています。非同期で待ち受けたい処理(例:メッセージが表示し終わるまで待機する)がある場合、継承先のクラスでcurrentTaskに待ち受けたいUniTaskを登録します。
public abstract class LuaInterpreterHandlerBase : MonoBehaviour
{
protected UniTask currentTask = UniTask.FromResult((DynValue)null);
[MoonSharpHidden]
public async UniTask WaitUntilFinishCurrentTask()
{
await currentTask;
}
}
ハンドラの実装例としてメッセージを表示して、メッセージの表示が終わるまで待ち受けるためのハンドラを以下に示します。ShowMessageメソッドが非同期処理で、メッセージの表示が終わるまで待ち受ける必要があるのでcurrentTaskとして登録しています。
[MoonSharpUserData]
public class MessageEventHandler : LuaInterpreterHandlerBase
{
[SerializedField]
private Message _messageComponent; // インスペクタでメッセージコンポーネントを設定しておく
public void Show(string message)
{
messageComponent.SetMessageVisibility(true); // メッセージウィンドウを表示
currentTask = _messageComponent.ShowMessage(message); // メッセージを表示して待ち受け
}
public void Close()
{
_messageComponent.SetMessageVisibility(false);
}
}
例として「プレイヤーが2歩左に動き、その後一言しゃべる」というイベントをLuaスクリプトで実装すると以下のようになります。
function main()
// 左に2マスプレイヤーを動かし、移動終了を待ち受ける
player.lookToward('left')
player.moveToward('left', 2)
coroutine.yield()
// メッセージを表示して、メッセージの終了を待ち受ける
message.showMessage("Hello, world!")
coroutine.yield()
end
Step6:画面上にイベントを配置し、イベントに触れたらLuaスクリプトが実行されるようにする
実装内容は見出しのまんまです。
プレイヤーの移動先の目的地にイベントオブジェクトがある場合、イベントオブジェクトに設定されているLuaスクリプトを実行するように実装を追加すれば上記のGIF画像のような動きが実現できます。
Step7:フラグ管理により処理を分岐
次にLuaスクリプトからフラグ(例:宝箱を開けたかどうかフラグなど)を管理できるようにし、処理や画面の表示を分岐させることができるよう実装します。
上記のツイートのような挙動を実現するのに必要なのは2つ。
- フラグをLuaスクリプトからON/OFFできるようにする
- フラグに応じてイベントオブジェクト(宝箱など)の表示をON/OFFできるようにする
フラグ管理には以下のような簡易クラスを作りました。true/falseで持たせるか数値で持たせるかはお好みで。私は数値にしているので、フラグ管理というよりは変数管理といった方が適切かも…。
public class FlagManager
{
[SerializeField]
private Dictionary<string, int> _flags = new Dictionary<string, int>();
public void SetFlag(string key, int value)
{
_flags[key] = value;
}
public int GetFlag(string key)
{
return _flags.ContainsKey(key)
? _flags[key]
: 0;
}
}
そしてフラグ管理をするためのLuaスクリプトのハンドラを以下のように作り、LuaIntepreterに登録するようにします。
[MoonSharpUserData]
public class FlagEventHandler : LuaInterpreterHandlerBase
{
[SerializeField]
private FlagManager _flagManager = new FlagManager();
public void Set(string key, int value)
{
_flagManager.SetFlag(key, value);
}
public int Get(string key)
{
_flagManager.GetFlag(key);
}
}
あとはフラグによって実行するLuaスクリプトを切り替えたり、イベントの見た目(表示・非表示の切り替え)をできるように実装すればOK!
Step8:自分好みのハンドラを量産する
ここまでくれば、あとはLuaイベントから呼びたい処理を実装したハンドラを量産していくだけです!
例として私は以下のようなハンドラを追加しました。
- 画面に選択肢を表示して分岐するハンドラ
- 複数のマップ間を移動できるハンドラ
- フェードイン・フェードアウトできるハンドラ
- アイテムの取得・使用
おわりに
この記事では、Unityでアドベンチャーゲームを作るための仕組みとして、Luaスクリプトを利用してイベントを構築する方法について解説しました。
技術的な詳細についてはさらっと触れた程度ですが、どんな順で何を実装していくのか、MoonSharpをどう利用しているのかといったあたりが、私と同じようにアドベンチャーゲーム作成をする人の参考になれば幸いです。