会話文字送り完了時演出(および入力ブロック制御)

絶対調味ロジック

セリフが終わった瞬間演出(効果音やBGMストップ、画像表示など)を行えるように
セリフシステムに機能を追加をしました。

もともとセリフが終わって進行クリックした後の演出用は実装していましたが、

キャラクター「そんなことが!!(ピロリーン)」▼待機アイコン表示中

という演出ができないと思いたち、別途用意したというところです。
今作で使用するかは微妙でしたが、ADV基本機能として追加しておきました。

背景と構築の経緯

① 当初の目的(activateOnCompletedObjects の構築理由)

従来の仕様(activateOnFinishedObjects)では、プレイヤーが進行クリック(ボタン押し/画面タップ)を押して次のメッセージに進む瞬間にしかオブジェクトをアクティブ化(演出発火)できませんでした。

しかし、以下のような演出を実現するためには「進行クリックを待つ前=文字送りが完了した瞬間」にオブジェクトを起動する仕組みが必要となりました。

  • 文字が読み終わった瞬間に「ピコン!」とSEを鳴らす
  • テキスト表示完了に合わせてカットインやエフェクトを即座に表示する
  • ユーザーがクリックする前の「待ち時間(ウェイトアイコン表示中)」に背景や差分を動かす

これに対応するため、
Sys_Messenger に activateOnCompletedObjects(文字送り完了時にアクティブ化するリスト) を導入しました。

② 発生した問題(連打問題とディレイ制御の衝突)

activateOnCompletedObjects を導入した際、以下の2つの仕様要求が同時に発生し、制御の衝突が起きました。

  • 連打防止(入力ブロック)の要求:
    文字送りが完了した瞬間にプレイヤーがクリックを連打すると、演出(SE等)を見る前に次の会話へ進んでしまうため、完了と同時に Sys_InputBlockerMessenger を起動して一時的に入力を遮断したい。
  • 演出側の待機(ディレイ)の要求:
    「文字完了から 0.5 秒遅れて SE を鳴らしたい」など、演出用オブジェクトには delay(遅延秒数)を設定したい。

【従来の誤った挙動】
演出用SEに delay = 0.5 を設定した際、
Sys_Messenger 内のリスト一括処理によって Sys_InputBlockerMessenger まで0.5秒遅れて起動してしまい、
「文字完了直後の0.5秒間は入力ブロックが効かず、連打で飛ばせてしまう」 という不具合が発生しました。

③ 最終的な設計解決(役割の完全分離)

コード内で例外処理(If文でスクリプトの種類を判定してディレイを消す等)を行うのではなく、
「単一責任の原則」に基づき、オブジェクトごとの役割と設定を独立させる設計を採用しました。

  • Sys_Messenger:
    文字送り完了の「瞬間」に、登録されたオブジェクトをそれぞれの設定に従って SetActive(true) するだけ(演出用もブロッカー用も差別化せず素直に扱う)。
  • Sys_InputBlockerMessenger:
    自身が SetActive(true) された瞬間(OnEnable)に、自身の blockerDelay(デフォルト0秒)に従って即時で入力ブロックを発火させる。

最終実装

Sys_Messenger.cs★改修・追加

[System.Serializable]
    public class FinishActivateEntry
    {
        public GameObject target;
        [Tooltip("アクティブにするまでの遅延秒数")]
        public float delay = 0f;
    }

    [Header("会話終了後にアクティブにするオブジェクト(進行クリック後)")]
    public List<FinishActivateEntry> activateOnFinishedObjects;

    [Header("文字送り完了時にアクティブにするオブジェクト(進行クリック前・演出用)")]
    public List<FinishActivateEntry> activateOnCompletedObjects;

    // --- DisplayMessage コルーチン内 ---
    
    // 文字送りが完了した「瞬間」(クリック待ち前)に演出・ブロッカーグループを発火
    if (currentConversationIndex == conversations.Count - 1)
    {
        TriggerActivateEntries(activateOnCompletedObjects);
    }

    // --- リスト処理ロジック ---

    /// <summary>
    /// 指定されたエントリーリストのオブジェクトをアクティブ化する(遅延対応)
    /// </summary>
    private void TriggerActivateEntries(List<FinishActivateEntry> entries)
    {
        if (entries == null) return;

        foreach (var entry in entries)
        {
            if (entry.target == null) continue;
            if (entry.delay <= 0f)
                entry.target.SetActive(true);
            else
                StartCoroutine(ActivateAfterDelay(entry.target, entry.delay));
        }
    }

    IEnumerator ActivateAfterDelay(GameObject target, float delay)
    {
        yield return new WaitForSeconds(delay);
        if (target != null)
            target.SetActive(true);
    }

Sys_InputBlockerMessenger.cs

既存の連打防止機能(duration、ロード/スキップ中のガード、OnDisable での安全解除)を100%保持しつつ、
自身側の発火ディレイ blockerDelay を統合したコードです。

using UnityEngine;
using System.Collections;

/// <summary>
/// 会話完了時の演出中(SE再生や画面演出中)にプレイヤーの連打(進行入力)を防止するスクリプト。
/// OnEnableでSys_InputManagerをブロックし、指定時間(duration)経過後に自動で解除します。
/// </summary>
public class Sys_InputBlockerMessenger : MonoBehaviour
{
    [Header("ブロック開始ディレイ設定")]
    [Tooltip("アクティブ化されてから実際にBlock()を発火するまでの遅延秒数。\n" +
             "連打防止目的の場合は 0 のまま(即時ブロック)にしてください。")]
    [SerializeField] private float blockerDelay = 0f;

    [Header("ブロック解除時間設定")]
    [Tooltip("入力をブロックする時間(秒)。演出やSEの長さに合わせて設定してください。")]
    [SerializeField] private float duration = 0.5f;

    private bool isBlocking = false;

    private void OnEnable()
    {
        // コルーチンの二重起動を防止
        StopAllCoroutines();

        // ロード中または既読スキップ中は連打防止を行わない(スムーズな進行のため)
        if (Sys_SaveManager.IsLoading) return;
        if (Sys_SkipManager.Instance != null && Sys_SkipManager.Instance.IsSkipping) return;

        StartCoroutine(StartBlockRoutine());
    }

    private IEnumerator StartBlockRoutine()
    {
        // 1. 発火用ディレイ(必要であれば待機)
        if (blockerDelay > 0f)
        {
            yield return new WaitForSeconds(blockerDelay);
        }

        // 2. ブロック実行
        if (Sys_InputManager.Instance != null)
        {
            Sys_InputManager.Instance.Block();
            isBlocking = true;
        }

        // 3. 指定時間(duration)待機して解除
        yield return new WaitForSeconds(duration);
        ReleaseBlock();
    }

    private void ReleaseBlock()
    {
        if (isBlocking)
        {
            if (Sys_InputManager.Instance != null)
            {
                Sys_InputManager.Instance.Unblock();
            }
            isBlocking = false;
        }
    }

    private void OnDisable()
    {
        // シーン遷移やCut終了等で途中非アクティブ化された場合の安全対策(ロック残存防止)
        StopAllCoroutines();
        ReleaseBlock();
    }
}

Inspectorでの設定手順と運用ルール

文字送り完了時に「即時入力ブロック」と「遅延演出SE」を同時に扱う場合の設定例です。

■ Sys_Messenger の設定

Activate On Completed Objects の要素を別々に(分ける形で)2つ登録します。

登録オブジェクト (Target)Messenger側の Delay動作
Sys_InputBlockerMessenger0文字送り完了の瞬間に SetActive(true) され、
即時で入力ブロックが掛かる。
演出用SE / エフェクトObj0.5 など任意文字送り完了から 0.5 秒後に SetActive(true) され、
SEが再生される。

演出用オブジェクトに「Sys_InputBlockerMessenger」をアタッチし、
オブジェクト内に演出オブジェクト「EffectObject」を非アクティブで配置します。

InputBlockerMessengerSys_InputBlockerMessengerをアタッチ。非アクティブ
  └── EffectObject ← 演出オブジェクトを配置する入れ物。非アクティブ
       ├── Sys_AudioTrigger ← 効果音設定。アクティブ
       ├── Cmd_PlayBGM ← BGM設定。アクティブ
       └── 画像やモデルなど ← 表示したいオブジェクト。アクティブ

InputBlockerMessenger」と「EffectObject」をそれぞれ登録するということになります。

■ Sys_InputBlockerMessenger の設定

  • Blocker Delay:0 (文字送り完了と同時に即座に入力遮断を行うため)
  • Duration:0.5 (連打を防止したいガード時間)

保守上のガードレール(注意点)

「進行クリック後」か「文字完了時」かの用途区別:
・会話が完全に終わって次のカット/会話へ移る進行クリックのタイミングで発火させたい場合は、
進行クリック後(activateOnFinishedObjects)を使用してください。
・会話文が読み終わった表示完了のタイミング(クリック待ち中)に発火させたい場合は、
文字完了時(activateOnCompletedObjects)を使用してください。

スキップ・ロード処理の自動保護:
・既読スキップ時やロード時は Sys_InputBlockerMessenger 内で自動バイパスされるため、
連打防止機能が原因で進行が止まることはありません。

コメント