2025-07-08 10:46:31 +00:00
// Copyright (c) Pixel Crushers. All rights reserved.
using UnityEngine ;
using System.Collections ;
using System.Collections.Generic ;
namespace PixelCrushers.DialogueSystem
{
/// <summary>
/// This component implements IDialogueUI using Unity UI. It's based on
/// CanvasDialogueUI and compiles the Unity UI versions of the controls defined in
/// UnityUISubtitleControls, UnityUIResponseMenuControls, UnityUIAlertControls, etc.
///
/// To use this component, build a UI layout (or drag a pre-built one in the Prefabs folder
/// into your scene) and assign the UI control properties. You must assign a scene instance
/// to the DialogueManager; you can't use prefabs with Unity UI dialogue UIs.
///
/// The required controls are:
/// - NPC subtitle line
/// - PC subtitle line
/// - Response menu buttons
///
/// The other control properties are optional. This component will activate and deactivate
/// controls as they are needed in the conversation.
/// </summary>
[AddComponentMenu("")] // Use wrapper.
public class UnityUIDialogueUI : CanvasDialogueUI
{
/// <summary>
/// The UI root.
/// </summary>
[HideInInspector]
public UnityUIRoot unityUIRoot ;
/// <summary>
/// The dialogue controls used in conversations.
/// </summary>
public UnityUIDialogueControls dialogue ;
/// <summary>
/// QTE (Quick Time Event) indicators.
/// </summary>
public UnityEngine . UI . Graphic [ ] qteIndicators ;
/// <summary>
/// The alert message controls.
/// </summary>
public UnityUIAlertControls alert ;
/// <summary>
/// Set <c>true</c> to always keep a control focused; useful for gamepads.
/// </summary>
[Tooltip("Always keep a control focused; useful for gamepads and keyboard.")]
public bool autoFocus = false ;
/// <summary>
/// Allow the dialogue UI to steal focus if a non-dialogue UI panel has it.
/// </summary>
[Tooltip("Allow the dialogue UI to steal focus if a non-dialogue UI panel has it.")]
public bool allowStealFocus = false ;
/// <summary>
/// If auto focusing, check on this frequency in seconds that the control is focused.
/// </summary>
[Tooltip("If auto focusing, check on this frequency in seconds that the control is focused.")]
public float autoFocusCheckFrequency = 0.5f ;
/// <summary>
/// Set <c>true</c> to look for OverrideUnityUIDialogueControls on actors.
/// </summary>
[Tooltip("Look for OverrideUnityUIDialogueControls on actors.")]
public bool findActorOverrides = true ;
/// <summary>
/// Set <c>true</c> to add an EventSystem if one isn't in the scene.
/// </summary>
[Tooltip("Add an EventSystem if one isn't in the scene.")]
public bool addEventSystemIfNeeded = true ;
private UnityUIQTEControls m_qteControls ;
private float m_nextAutoFocusCheckTime = 0 ;
private GameObject m_lastSelection = null ;
public override AbstractUIRoot uiRootControls
{
get { return unityUIRoot ; }
}
public override AbstractDialogueUIControls dialogueControls
{
get { return dialogue ; }
}
public override AbstractUIQTEControls qteControls
{
get { return m_qteControls ; }
}
public override AbstractUIAlertControls alertControls
{
get { return alert ; }
}
private class QueuedAlert
{
public string message ;
public float duration ;
public QueuedAlert ( string message , float duration )
{
this . message = message ;
this . duration = duration ;
}
}
private Queue < QueuedAlert > alertQueue = new Queue < QueuedAlert > ( ) ;
// References to the original controls in case an actor temporarily overrides them:
protected UnityUISubtitleControls originalNPCSubtitle ;
protected UnityUISubtitleControls originalPCSubtitle ;
protected UnityUIResponseMenuControls originalResponseMenu ;
// Caches overrides by actor so we only need to search an actor once:
private Dictionary < Transform , OverrideUnityUIDialogueControls > overrideCache = new Dictionary < Transform , OverrideUnityUIDialogueControls > ( ) ;
private bool isShowingNpcSubtitle = false ;
private bool isShowingPcSubtitle = false ;
private bool isShowingResponses = false ;
#region Initialization
/// <summary>
/// Sets up the component.
/// </summary>
public override void Awake ( )
{
base . Awake ( ) ;
FindControls ( ) ;
alert . DeactivateUIElements ( ) ;
dialogue . DeactivateUIElements ( ) ;
Tools . DeprecationWarning ( this , "Use StandardDialogueUI instead." ) ;
}
#if ! ( UNITY_4_3 | | UNITY_4_5 | | UNITY_4_6 | | UNITY_4_7 | | UNITY_5_0 | | UNITY_5_1 | | UNITY_5_2 | | UNITY_5_3 )
public virtual void OnEnable ( )
{
UnityEngine . SceneManagement . SceneManager . sceneLoaded - = OnSceneLoaded ;
UnityEngine . SceneManagement . SceneManager . sceneLoaded + = OnSceneLoaded ;
}
public virtual void OnDisable ( )
{
UnityEngine . SceneManagement . SceneManager . sceneLoaded - = OnSceneLoaded ;
}
#endif
/// <summary>
/// Logs warnings if any critical controls are unassigned.
/// </summary>
private void FindControls ( )
{
if ( addEventSystemIfNeeded ) UITools . RequireEventSystem ( ) ;
m_qteControls = new UnityUIQTEControls ( qteIndicators ) ;
if ( DialogueDebug . logErrors )
{
if ( DialogueDebug . logWarnings )
{
if ( dialogue . npcSubtitle . line = = null ) Debug . LogWarning ( string . Format ( "{0}: UnityUIDialogueUI NPC Subtitle Line needs to be assigned." , DialogueDebug . Prefix ) ) ;
if ( dialogue . pcSubtitle . line = = null ) Debug . LogWarning ( string . Format ( "{0}: UnityUIDialogueUI PC Subtitle Line needs to be assigned." , DialogueDebug . Prefix ) ) ;
if ( dialogue . responseMenu . buttons . Length = = 0 & & dialogue . responseMenu . buttonTemplate = = null ) Debug . LogWarning ( string . Format ( "{0}: UnityUIDialogueUI Response buttons need to be assigned." , DialogueDebug . Prefix ) ) ;
if ( alert . line = = null ) Debug . LogWarning ( string . Format ( "{0}: UnityUIDialogueUI Alert Line needs to be assigned." , DialogueDebug . Prefix ) ) ;
}
}
originalNPCSubtitle = dialogue . npcSubtitle ;
originalPCSubtitle = dialogue . pcSubtitle ;
originalResponseMenu = dialogue . responseMenu ;
}
public OverrideUnityUIDialogueControls FindActorOverride ( Transform actor )
{
if ( actor = = null ) return null ;
if ( ! overrideCache . ContainsKey ( actor ) )
{
overrideCache . Add ( actor , ( actor ! = null ) ? actor . GetComponentInChildren < OverrideUnityUIDialogueControls > ( ) : null ) ;
}
return overrideCache [ actor ] ;
}
#if UNITY_4_3 | | UNITY_4_5 | | UNITY_4_6 | | UNITY_4_7 | | UNITY_5_0 | | UNITY_5_1 | | UNITY_5_2 | | UNITY_5_3
public void OnLevelWasLoaded ( int level )
{
if ( addEventSystemIfNeeded ) UITools . RequireEventSystem ( ) ;
}
#else
public void OnSceneLoaded ( UnityEngine . SceneManagement . Scene scene , UnityEngine . SceneManagement . LoadSceneMode mode )
{
if ( addEventSystemIfNeeded ) UITools . RequireEventSystem ( ) ;
}
#endif
public override void Open ( )
{
overrideCache . Clear ( ) ;
base . Open ( ) ;
dialogue . npcSubtitle . CheckSubtitlePortrait ( CharacterType . NPC ) ;
dialogue . pcSubtitle . CheckSubtitlePortrait ( CharacterType . PC ) ;
}
#endregion
#region Alerts
public override void ShowAlert ( string message , float duration )
{
if ( alert . queueAlerts )
{
alertQueue . Enqueue ( new QueuedAlert ( message , duration ) ) ;
}
else
{
StartShowingAlert ( message , duration ) ;
}
}
private void ShowNextQueuedAlert ( )
{
if ( alertQueue . Count > 0 )
{
var queuedAlert = alertQueue . Dequeue ( ) ;
StartShowingAlert ( queuedAlert . message , queuedAlert . duration ) ;
}
}
private void StartShowingAlert ( string message , float duration )
{
base . ShowAlert ( message , duration ) ;
if ( autoFocus ) alert . AutoFocus ( ) ;
}
#endregion
#region Subtitles
public override void ShowSubtitle ( Subtitle subtitle )
{
SetIsShowingSubtitle ( subtitle , true ) ;
if ( findActorOverrides & & subtitle ! = null )
{
var overrideControls = ( subtitle . speakerInfo ! = null ) ? FindActorOverride ( subtitle . speakerInfo . transform ) : null ;
if ( overrideControls ! = null ) overrideControls . ApplyToDialogueUI ( this ) ;
if ( subtitle . speakerInfo = = null | | subtitle . speakerInfo . characterType = = CharacterType . NPC )
{
dialogue . npcSubtitle = ( overrideControls ! = null ) ? overrideControls . subtitle : originalNPCSubtitle ;
}
else
{
dialogue . pcSubtitle = ( overrideControls ! = null ) ? overrideControls . subtitle : originalPCSubtitle ;
}
}
HideResponses ( ) ;
CheckForSupercededSubtitle ( subtitle . speakerInfo . characterType ) ;
base . ShowSubtitle ( subtitle ) ; // Calls UnityUISubtitleControls.ShowSubtitle().
ClearSelection ( ) ;
CheckSubtitleAutoFocus ( subtitle ) ;
}
protected void CheckForSupercededSubtitle ( CharacterType characterType )
{
var otherSubtitle = ( characterType = = CharacterType . NPC ) ? dialogue . pcSubtitle : dialogue . npcSubtitle ;
if ( UITools . CanBeSuperceded ( otherSubtitle . uiVisibility ) & & otherSubtitle . isVisible )
{
otherSubtitle . ForceHide ( ) ;
}
}
public void CheckSubtitleAutoFocus ( Subtitle subtitle )
{
if ( autoFocus )
{
if ( subtitle . speakerInfo . isPlayer )
{
dialogue . pcSubtitle . AutoFocus ( allowStealFocus ) ;
}
else
{
dialogue . npcSubtitle . AutoFocus ( allowStealFocus ) ;
}
}
}
protected void SetIsShowingSubtitle ( Subtitle subtitle , bool value )
{
if ( subtitle = = null ) return ;
if ( subtitle . speakerInfo . isNPC )
{
isShowingNpcSubtitle = value ;
}
else
{
isShowingPcSubtitle = value ;
}
}
public override void HideSubtitle ( Subtitle subtitle )
{
SetIsShowingSubtitle ( subtitle , false ) ;
base . HideSubtitle ( subtitle ) ;
}
#endregion
#region Responses
public override void ShowResponses ( Subtitle subtitle , Response [ ] responses , float timeout )
{
isShowingResponses = true ;
if ( findActorOverrides )
{
// Use speaker's (NPC's) world space canvas for subtitle reminder, and for menu if set:
var overrideControls = ( subtitle ! = null & & subtitle . speakerInfo ! = null ) ? FindActorOverride ( subtitle . speakerInfo . transform ) : null ;
var subtitleReminder = ( overrideControls ! = null ) ? overrideControls . subtitleReminder : originalResponseMenu . subtitleReminder ;
if ( overrideControls ! = null & & overrideControls . responseMenu . panel ! = null )
{
dialogue . responseMenu = ( overrideControls ! = null & & overrideControls . responseMenu . panel ! = null ) ? overrideControls . responseMenu : originalResponseMenu ;
}
else
{
// Otherwise use PC's world space canvas for menu if set:
overrideControls = ( subtitle ! = null & & subtitle . listenerInfo ! = null ) ? FindActorOverride ( subtitle . listenerInfo . transform ) : null ;
dialogue . responseMenu = ( overrideControls ! = null & & overrideControls . responseMenu . panel ! = null ) ? overrideControls . responseMenu : originalResponseMenu ;
}
// Either way, use speaker's (NPC's) subtitle reminder:
dialogue . responseMenu . subtitleReminder = subtitleReminder ;
}
if ( dialogue . responseMenu . showHideController . state = = UIShowHideController . State . Hiding )
{
StartCoroutine ( ShowResponsesAfterHidden ( subtitle , responses , timeout ) ) ;
}
else
{
base . ShowResponses ( subtitle , responses , timeout ) ;
ClearSelection ( ) ;
CheckResponseMenuAutoFocus ( ) ;
}
}
private IEnumerator ShowResponsesAfterHidden ( Subtitle subtitle , Response [ ] responses , float timeout )
{
var safeguardTime = Time . realtimeSinceStartup + 5f ;
while ( dialogue . responseMenu . showHideController . state = = UIShowHideController . State . Hiding & & Time . realtimeSinceStartup < safeguardTime )
{
yield return null ;
}
base . ShowResponses ( subtitle , responses , timeout ) ;
ClearSelection ( ) ;
CheckResponseMenuAutoFocus ( ) ;
}
public void CheckResponseMenuAutoFocus ( )
{
if ( autoFocus ) dialogue . responseMenu . AutoFocus ( m_lastSelection , allowStealFocus ) ;
}
public override void HideResponses ( )
{
isShowingResponses = false ;
dialogue . responseMenu . DestroyInstantiatedButtons ( ) ;
base . HideResponses ( ) ;
if ( isShowingNpcSubtitle & & dialogue . responseMenu . subtitleReminder . panel = = dialogue . npcSubtitle . panel )
{
dialogue . npcSubtitle . ForceShow ( ) ; // We hid the NPC subtitle that's supposed to stay visible. Show it.
}
}
public void ClearSelection ( )
{
if ( autoFocus )
{
UnityEngine . EventSystems . EventSystem . current . SetSelectedGameObject ( null ) ;
m_lastSelection = null ;
}
}
#endregion
#region Update
protected int alertQueueCount = 0 ;
protected bool alertIsVisible ;
protected bool alertIsHiding ;
public override void Update ( ) // Handle alert queue and auto-focus.
{
base . Update ( ) ;
alertQueueCount = alertQueue . Count ;
alertIsVisible = alert . IsVisible ;
alertIsHiding = alert . IsHiding ;
// Check alert queue:
if ( alertQueue . Count > 0 & & alert . queueAlerts & & ! alert . IsVisible & & ! ( alert . waitForHideAnimation & & alert . IsHiding ) )
{
ShowNextQueuedAlert ( ) ;
}
// Auto focus dialogue:
if ( autoFocus & & isOpen )
{
if ( UnityEngine . EventSystems . EventSystem . current . currentSelectedGameObject ! = null )
{
m_lastSelection = UnityEngine . EventSystems . EventSystem . current . currentSelectedGameObject ;
}
if ( autoFocusCheckFrequency > 0.001f & & Time . realtimeSinceStartup > m_nextAutoFocusCheckTime )
{
m_nextAutoFocusCheckTime = Time . realtimeSinceStartup + autoFocusCheckFrequency ;
if ( isShowingResponses )
{
dialogue . responseMenu . AutoFocus ( m_lastSelection , allowStealFocus ) ;
}
else if ( isShowingPcSubtitle )
{
dialogue . pcSubtitle . AutoFocus ( allowStealFocus ) ;
}
else if ( isShowingNpcSubtitle )
{
dialogue . npcSubtitle . AutoFocus ( allowStealFocus ) ;
}
}
}
}
#endregion
}
}