【実践記事・ADV編⑨】キャラクターをクリックして話す——UIクリック検知と話題システムの設計

初心者向け実践開発

対応バージョン:Unity 6.3 LTS(6000.3系)/WebGL出力 この記事はシリーズ全11回の第9回です。第8回ではオープニング会話と話題カードのフラグ連動を実装しました。今回はUIのクリック検知とScriptableObjectの扱い方に焦点を当てて、キャラクター選択の仕組みを掘り下げます。 【PR】本記事にはアフィリエイトリンクを含みます。

目次


はじめに

前回はオープニング会話と話題カードのフラグ連動を実装しました。

今回は「キャラクターをクリックして話題カードを開く」仕組みを作ります。一見シンプルに見えますが、UnityのUIクリック検知には注意点があり、話題の解放管理にも設計上の落とし穴があります。

この記事で解説するのは以下の3点です。

  • なぜOnMouseDownではなくUIクリックには専用の仕組みが必要なのか
  • 話題の解放状態をScriptableObjectで管理すると何が起きるか
  • 話題の状態(あり・尽き・未解放)に応じてセリフを出し分ける方法

1. UIオブジェクトのクリック検知

OnMouseDownが使えない理由

立ち絵はCanvasの子オブジェクト(UI Image)として配置されています。WorldSpaceのオブジェクトに使うOnMouseDown()はUI要素では動作しません。

UnityのUIには別のクリック検知の仕組みがあります。それがIPointerClickHandlerインターフェースです。

IPointerClickHandlerとは

IPointerClickHandlerを実装したコンポーネントをUI要素にアタッチすると、そのUI要素がクリックされたときにOnPointerClick()が自動的に呼ばれます。

仕組みはシンプルです。

プレイヤーがクリック
 ↓
UnityのEventSystemがUI要素のRaycastを検知
 ↓
IPointerClickHandlerを持つコンポーネントのOnPointerClick()を呼ぶ

ただしRaycast TargetがONになっていないとクリックが届きません。立ち絵のImageコンポーネントでRaycast TargetがONになっているか必ず確認してください。

CharacterClickHandler.csの作成

Assets/Scripts/UI/フォルダにCharacterClickHandler.csを新規作成します。

using UnityEngine;
using UnityEngine.EventSystems;

public class CharacterClickHandler : MonoBehaviour, IPointerClickHandler
{
    public CharacterData characterData;

    public void OnPointerClick(PointerEventData eventData)
    {
        if (characterData == null) return;
        if (DialogueManager.Instance.IsDialogueRunning()) return;

        TopicCardManager.Instance.OpenTopicPanel(characterData);
    }
}

会話中はクリックを無効にするため、IsDialogueRunning()を使っています。このメソッドはAssets/Scripts/Dialogue/DialogueManager.csに以下を追加して実装します。

public bool IsDialogueRunning()
{
    return isPlaying;
}

Unityでの設定

各キャラクターオブジェクト(CharacterTanaka・CharacterSaya・CharacterIsobe)に対して:

  1. Add ComponentCharacterClickHandlerを追加
  2. characterDataに対応するCharacterDataをアサイン

2. 話題の解放管理——ScriptableObjectの落とし穴

何が起きるか

前回の実装では、フラグが立ったときにTopicData.isUnlocked = trueと書き換えていました。

これはエディタ上でPlayモードを終了しても変更が残るという問題を引き起こします。ScriptableObjectはアセットファイルそのものなので、Playモード中の変更がアセットに保存されてしまうのです。

Playするたびに話題のチェックが増えていき、毎回手動でリセットしなければならなくなります。

解決策:FlagManagerで管理する

ScriptableObjectを書き換えるのをやめ、FlagManagerに「解放済みTopicIDのセット」を持たせます。

話題の解放状態
 Before:TopicData.isUnlocked(アセットに保存される)
 After :FlagManager.unlockedTopics(実行中のみ保持)

Assets/Scripts/System/FlagManager.cs内の話題解放処理をこう変えます。

private HashSet<string> unlockedTopics = new HashSet<string>();

public bool IsTopicUnlocked(string topicID, string unlockFlagID)
{
    if (string.IsNullOrEmpty(unlockFlagID)) return true; // 最初から解放
    return unlockedTopics.Contains(topicID);
}

void UnlockTopicsByFlag(string flagID)
{
    TopicData[] allTopics = Resources.LoadAll<TopicData>("Topics");
    foreach (TopicData topic in allTopics)
    {
        if (topic.unlockFlagID == flagID)
            unlockedTopics.Add(topic.topicID);
    }
}

Assets/Scripts/Dialogue/TopicData.csからisUnlockedフィールドも削除します。Playを止めてもTopicDataアセットは一切変化しなくなります。


3. 話題の状態に応じたセリフ出し分け

キャラクターをクリックしたとき、3つの状態が考えられます。

状態挙動
話せる話題があるTopicPanelを開く
全話題を聞き終えた「もう話すことはない」と表示
まだ解放されていない「あの人とは、まだ話せない」と表示

「もう話すことはない」「まだ話せない」は通常の会話パネルを使ってモノローグとして表示します。これにより専用UIを作らずに済みます。

CharacterDataに専用フィールドを追加する

Assets/Scripts/Dialogue/CharacterData.csに2つのフィールドを追加します。

public DialogueData noMoreTopicsDialogue;    // 話題が尽きた時
public DialogueData notYetAvailableDialogue; // まだ話せない時

DialogueDataを作成する

以下のCSVをインポートして、2種類のDialogueDataを作成します。

NoMoreTopics.csv

sceneTitle,setFlagOnComplete,speakerName,dialogueText,isMonologue
NoMoreTopics,,凛,(もう話すことはない。),true

NotYetAvailable.csv

sceneTitle,setFlagOnComplete,speakerName,dialogueText,isMonologue
NotYetAvailable,,凛,(あの人とは、まだ話せない。),true

これを全キャラのCharacterDataにアサインします。1つ作れば全キャラで使い回せます。

クリック時の分岐フロー

クリック時の処理の流れは以下の通りです。

キャラクタークリック
 ↓
話題の状態を判定
 ↓
HasTopics      → TopicPanelを開く
NoMoreTopics   → 「もう話すことはない」を再生 → 選択画面に戻る
NotYetAvailable → 「まだ話せない」を再生 → 選択画面に戻る

4. 会話終了後の遷移設計

会話が1つ終わったとき、次にどの画面を出すかの設計も重要です。

基本の流れ

会話終了
 ↓
そのキャラにまだ話題がある → TopicPanelを再表示
そのキャラの話題が尽きた  → キャラクター選択画面に戻る

Assets/Scripts/UI/TopicCardManager.csRefreshTopicButtons()に「表示できる話題がゼロなら選択画面に戻る」処理を追加します。

EndDialogue()のコールバック設計に注意

EndDialogue()内でフラグを立てると、そのフラグに反応して別の会話がすぐに始まることがあります(Chapter4への自動遷移など)。このときonEndedCallbackが上書きされてしまう問題が起きます。

対策はシンプルです。コールバックをローカル変数に退避してnullにしてから、フラグを立てるという順番にします。

コールバックをローカル変数に退避
 ↓
onEndedCallback = null(上書き対策)
 ↓
フラグを立てる(この中で新しい会話が始まっても影響なし)
 ↓
退避したコールバックを呼ぶ

5. 動作確認チェックリスト

  • [ ] キャラクターをクリックするとTopicPanelが開く
  • [ ] 会話中にキャラクターをクリックしても反応しない
  • [ ] 話し終わった話題は話題カードから消える
  • [ ] 全話題を聞き終えたキャラをクリックすると「もう話すことはない」と表示される
  • [ ] まだ解放されていないキャラをクリックすると「まだ話せない」と表示される
  • [ ] Playを止めてもTopicDataのチェックが残らない(ScriptableObject汚染なし)

まとめ

今回のポイントは2つです。

UIクリックにはIPointerClickHandlerを使う。 Canvas上のUI要素はOnMouseDownでは反応しません。EventSystemを通じてクリックを検知する仕組みを理解しておくと、今後のUI実装でも役立ちます。

ScriptableObjectを実行時に書き換えない。 状態管理はMonoBehaviourのフィールドやHashSetで行い、アセットは設定データとして読み取り専用で使うのが基本です。

次回は、キャラクター選択画面の実装と、Chapter3・4へのフラグ連鎖を解説します。


あわせて読みたいおすすめ書籍【PR】

今回実装したUIクリック検知・ScriptableObjectの扱い方についてさらに学びたい方には、以下の書籍もおすすめです。本セクションにはアフィリエイトリンクを含みます。

※このセクションのリンクはアフィリエイトリンクであり、商品が購入された場合、サイト運営者に紹介料が支払われることがあります【PR】。


この記事を作成するにあたって参考にしたサイト

コメント

タイトルとURLをコピーしました