前回のあらすじ
前回は次のエリアへの移動中、いきなり戦闘開始になる処理を実装しました。
何を作っているか詳しく知りたい人は1の記事をご覧ください。
トーク処理
次は④のトーク処理を実装していきます。トーク処理自体はすでに実装済みで、選択・分岐機能が備わっています。今回はそれを移動中の処理に組み込むための修正を含めて行います。
とある問題
トーク処理は、参考元のミートピアにはない機能です(キャラのつぶやきのようなものはあります)。そのため、いざ実装しようと設計を考えていると、とある問題を想定できました。それは、「会話途中で戦闘、または移動が終了してしまう問題」です。まずはこの問題を解決するためには何をするべきなのかを考えます。
とにかく案を出す
今回は自分が考えた案をいくつか出し、それを自己レビューしていきます。そのあとはAIさんにも意見をもらいます。
①乗り物を実際には動かさず、乗り物と背景の動きで動いているように見せる。
②一定の会話が終了するまではチェックポイントに到達→停止する。
③会話を自動送りにする。
④goal 到達後も会話が終わるまで待つ
このような4つの案が出ました。
レビュー
自己レビューをしたうえでAIさんにも意見をもらった結果、以下のような評価になりました。
①★★★☆☆(悪くない)
備考:本当に移動している感が弱くなる可能性高、背景の作りこみが必要。
②★★★★☆(工夫次第で自然にできる)
備考:プレイヤーが「何で止まった?」と感じる可能性が高いが、演出次第でどうにかなる。
③★★☆☆☆(補助的にはアリ)
備考:プレイヤーが会話に置いてけぼりになる可能性が高い→ログ機能が必須になる。
④★☆☆☆☆(ほぼナシ)
備考:途中の戦闘は止められない。
この結果、今回は一番評価が高かった②の処理を実装します。
チェックポイント処理
まず、自分が考えた実装方法はチェックポイントのオブジェクトとbool型の配列を用意して、会話のテキストデータに[GO]という特殊文字を定義します。そして[GO]のときに配列の1列目からboolをtrueにして進めるようにする仕組みです。しかし、これはboolの管理が大変なのと、会話とチェックポイントの紐付けが弱いためバグが起きやすくなります。
そのため、この方法を断念してチェックポイント自体に会話のデータを持たせて会話が進んでいるのかを判定させるような仕組みにすることにしました。
トーク処理改造
まずTalkDataは完全に共通のInterfaceを持ったNPCとの会話向けに作られている設計なので、チェックポイントから呼び出すための関数を追加します。
public void StartTalk(List<TalkSentence> sentences)
{
//バグ防止フラグ
if (isTalking) return;
//チェックポイントからの会話データを利用する
sentenceList = sentences;
//テキストボックス表示
textBox.alpha = 1f;
//ジャンプ停止
CharCtrl.blockJumpInput = true;
//テキスト処理
StartCoroutine(TalkFlow());
}
ちなみに、停止しているときの状態はIdleにしてしまうと操作キャラが追従対象になってしまうのでSlowという新しい状態を定義します。
public enum VehicleState
{
Idle,
Moving,
Slow//移動中にチェックポイントにとどまる
}
更新によって追従処理が変わらないように修正します。
void SetCameraTarget(VehicleState newState)
{
if (newState == VehicleState.Moving || newState == VehicleState.Slow)
{
//デバッグ用(本来はエリアごとに別の乗り物を設定)
CameraCtrl.target = debugVehicle.transform;
}
else
{
CameraCtrl.target = Character.Current.tf;
}
}
トークチェックポイント
次にトークチェックポイント側の処理を実装します。ここでは会話データを持たせ、OnTriggerEneterで会話が進んでいるか判定します。
using UnityEngine;
using System.Collections.Generic;
public class TalkPoint : MonoBehaviour
{
public List<TalkSentence> talkSentences;
private bool isDone = false;
private void OnTriggerEnter(Collider other)
{
if (!other.CompareTag("Vehicle")) return;
if (isDone) return;
isDone = true;
// 会話中はチェックポイントにとどまる
TravelCtrl.Instance.State = VehicleState.Slow;
AreaManager.Instance.canBattle = false;
// 会話開始
TalkCtrl.Instance.StartTalk(talkSentences);
}
}
これで完成です。
しかし...
会話中は戦闘をできないようにするというやり方が引っかかりました。自分のイメージでは会話の流れの中のちょうどいいタイミングで戦闘に入るのが理想的です。なので、このやり方は根本から自分のイメージとは異なる実装だと気付きました。
スポーンポイントとの統合
理想的な実装をするために、今回実装したトークポイントのclassと前回実装したスポーンポイントの処理を融合させました。以下が完成形です。
using System.Collections.Generic;
using UnityEngine;
public class CheckPoint : MonoBehaviour
{
public List<TalkSentence> talkSentences;
public bool hasBattle;
public EnemyTable enemyTable;
private bool triggered = false;
private bool waitingForTalkEnd = false;
private void OnTriggerEnter(Collider other)
{
if (!other.CompareTag("Vehicle"))
return;
if (triggered)
return;
//★ 会話中かどうかで分岐 ★
if (TalkCtrl.Instance.IsTalking)
{
// 会話が終わるまで停止
TravelCtrl.Instance.State = VehicleState.Slow;
waitingForTalkEnd = true;
return;
}
//誤作動防止用通過済みフラグ
triggered = true;
TalkCtrl.Instance.StartTalk(talkSentences, OnTalkEnd);
}
void Update()
{
// ★ 会話が終わった瞬間にイベント実行 ★
if (waitingForTalkEnd && !TalkCtrl.Instance.IsTalking)
{
waitingForTalkEnd = false;
triggered = true; // このチェックポイントはもう使わない
OnTalkEnd();
}
}
void OnTalkEnd()
{
//戦闘があるか判定
if (hasBattle)
{
//戦闘モード遷移
StartBattle();
}
else
{
//戦闘をしない場合は
TravelCtrl.Instance.State = VehicleState.Moving;
}
}
void StartBattle()
{
//戦闘用のフラグ
AreaManager.Instance.battleStarted = true;
BattleTransition.Instance.isTransition = true;
//モンスター抽選
GameObject enemy = EnemySelector.GetRandomEnemy(enemyTable);
//戦闘モードに遷移
BattleTransition.Instance.StartBattle(enemy);
}
}
流れとしては...
チェックポイント到達
↓
会話開始
↓
移動しながら会話
↓
次のチェックポイント到達
↓
会話が終わるまで停止
↓
戦闘 or 移動再開(次の会話へ)
このような流れです。CheckPointにはTriggerで自身の状態から会話・戦闘の開始を管理する役割を持たせました。ちなみに、ほかの影響する処理も変えなければいけません。
//追加処理
public bool IsTalking => isTalking;
private Action onEnd;
public void StartTalk(List<TalkSentence> sentences, Action onEnd)
{
if (isTalking) return;
//チェックポイントからの会話データを利用する
sentenceList = sentences;
//テキストボックス表示
textBox.alpha = 1f;
//ジャンプ停止
CharCtrl.blockJumpInput = true;
// 会話終了時に呼ぶコールバック
this.onEnd = onEnd;
//テキスト処理
StartCoroutine(TalkFlow());
}
//EndTalk()
onEnd?.Invoke(); // 会話終了を通知
onEnd = null;
実装はひとまず完了しました。ちなみに、今回実装したTalkPoint.csと前回実装したスポーンポイントのclassはもう使いません。
デバッグ
デバッグしていきます。準備は以前用意したスポーンポイントのオブジェクトにCheckPoint.csを付け替えるだけです。
ただ、今回はいくつかのテストケースを行っていきます。
テストケース
①会話&戦闘なし。(何も起こらない)
②会話のみ。(会話の後にチェックポイントで次の会話が行われる)
③戦闘のみ。(チェックポイントで戦闘モードへ遷移)
④会話&戦闘あり。(会話の後にチェックポイントで戦闘モードへ遷移)
これらのケースからデバッグをします。
バグ発生!
バグが発生しました
チェックポイント到達
↓
会話開始
↓
移動しながら会話
↓
次のチェックポイント到達 → ここまではOK
↓
会話が終わるまで停止 → 次のテキストを表示するボタンを押すと会話が終わっていなくても勝手に移動再開してしまう!
↓
戦闘 or 移動再開(次の会話へ)
原因を探る
void Update()
{
if (waitingForTalkEnd && !TalkCtrl.Instance.IsTalking)
{
waitingForTalkEnd = false;
triggered = true
OnTalkEnd();
}
}
void OnTalkEnd()
{
if (hasBattle)
{
StartBattle();
}
else
{
//ここが原因?
TravelCtrl.Instance.State = VehicleState.Moving;
}
}
ここが最有力候補です。なぜなら今回実装した中でMovingに関する処理はこの部分のみだからです。そもそもここが原因なのかを確かめるためにコメントアウトをして動作が変わるのかを確かめます。動作が変わった場合は間違えなくUpdate()の条件であるIsTalkingの状態が勝手に偽になったことが原因だとわかります。
しかし、実際にコメントアウトした状態で試してみると動作は変わりませんでした。つまり、IsTalkingは常に正常の状態です。そうなると、残る原因は一つしかありません。それは乗り物の移動を制御しているスクリプトにあります。
void Update()
{
//この条件が原因!!
if (TravelCtrl.Instance.State != VehicleState.Moving) return;
transform.position = Vector3.MoveTowards(
transform.position,
goal.position,
moveSpeed * Time.deltaTime
);
if (Vector3.Distance(transform.position, goal.position) < 0.1f)
{
TravelCtrl.Instance.State = VehicleState.Idle;
}
}
だと思っていました。しかし、条件を変えて潰してもダメでした。なのでここは原因ではなかったです。Inspecter確認したところ、勝手にMovingにされていたのは確かでした。そのため、どのMovingが誤作動しているかを特定します。
あ...
using UnityEngine;
public class MovingChangeCtrl : MonoBehaviour
{
//デバッグ用
void Update()
{
if (Input.GetKeyDown(KeyCode.Joystick1Button2))
{
//移動開始
TravelCtrl.Instance.State = VehicleState.Moving;
}
}
}
デバッグ用の移動開始キーコードと会話送りの操作のキーコードが同じものを割り当てていました。
見なかったことにしてください。
仕上げ
バグを直したところで、まだ次のトークを開始する処理は実装していなかったのでやっていきます。しかし、この処理は戦闘に入った場合を考慮しなければいけなかったので、かなり実装に時間がかかりました。
public CheckPoint talkAfterBattle;
このような変数を定義します。空かどうかを判定するのでフラグのようなものです。nextCheckポイントは地図上のポイントであり次の会話ポイントとは別として考える必要性がありました。
public void OnTalkEnd()
{
//戦闘があるか判定
if (hasBattle)
{
//戦闘後にこの CheckPoint の続きをやる
TravelCtrl.Instance.talkAfterBattle = this;
//戦闘モード遷移
StartBattle();
}
else
{
//戦闘をしない場合は
//次の会話があるなら開始
if (nextCheckPoint != null)
{
TalkCtrl.Instance.StartTalk(nextCheckPoint.talkSentences, nextCheckPoint.OnTalkEnd);
}
//移動再開
TravelCtrl.Instance.State = VehicleState.Moving;
}
}
TravelCtrl.csで定義した変数に次のチェックポイントのインスタンスを格納します。
if (TravelCtrl.Instance.talkAfterBattle != null)
{
var cp = TravelCtrl.Instance.talkAfterBattle;
TravelCtrl.Instance.talkAfterBattle = null;
// A の「次の会話」を開始する
if (cp.nextCheckPoint != null)
{
var next = cp.nextCheckPoint;
next.triggered = true;
TalkCtrl.Instance.StartTalk(next.talkSentences, next.OnTalkEnd);
}
TravelCtrl.Instance.State = VehicleState.Moving;
}
そしてこのような処理を戦闘終了時に追加して会話中をしていたか判定します。
これでデバッグすると、すべてのテストケースにおいてバッチリ正しい挙動が確認できました!
感想
記事を書きながらダラダラと設計やコーディングをしていたので、想定よりもかなり時間がかかりました。ただ、このやり方のほうが圧倒的に理解が深まるのでこれからはもっと時間を短縮できるように集中力を高めたり効率を上げる工夫をしたいです。今のところは実装が完了したといっても、最低限決めた動作のみバグなしでできるようにすることを目標に開発をしていたので、正直まだ望ましく無い挙動をしてしまうことがあります。これからはそれを修正する作業もしながら学びを深めていきたいです。適当すぎるがあまりにキーコードが被るといったアクシデントがありましたが、このようなミスをもっと減らせるようにしたいです。今回の実装では、「イベント再開ポイントを明確にすることの重要性」を身をもって実感しました。これがこれからのシステム設計に活きることを願っています。
次回予告
次回は今まで飛ばしていた②のキャラ管理処理にようやく入ります。新学期が始まってこれから忙しくなりますが、記事はゲーム開発以外のことでも少しずつ書いていくつもりです。Thank You For Reading!
今までの記事
本シリーズの別パートの記事もぜひご覧ください。
Unityで制作中のRPGにミートピアのようなステージ制の仕組みを実装したい1
Unityで制作中のRPGにミートピアのようなステージ制の仕組みを実装したい2
Unityで制作中のRPGにミートピアのようなステージ制の仕組みを実装したい3