619 lines
22 KiB
C#
619 lines
22 KiB
C#
using System;
|
|
using Cysharp.Threading.Tasks;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
using UnityEngine.UI;
|
|
using Quaternion = UnityEngine.Quaternion;
|
|
using Vector3 = UnityEngine.Vector3;
|
|
using TriInspector;
|
|
|
|
namespace R0bbie.VRSubtitles
|
|
{
|
|
/// <summary>
|
|
/// Class to handle dialogue panel UI
|
|
/// </summary>
|
|
[RequireComponent(typeof(AudioSource))]
|
|
public class SubtitlePanel : MonoBehaviour
|
|
{
|
|
// Exposed Variables
|
|
[Title("UI Child Refs")]
|
|
[SerializeField] Image characterAvatar;
|
|
[SerializeField] TMP_Text characterNameText;
|
|
[SerializeField] TMP_Text subtitleText;
|
|
//[SerializeField] TextAnimatorPlayer animatorPlayer;
|
|
[SerializeField] CanvasGroup panelCanvasGroup;
|
|
|
|
[Title("Core Behaviour")]
|
|
[SerializeField] bool waitForInput;
|
|
//[SerializeField] bool animateInText; // TODO: Implement
|
|
|
|
[Title("Follow Behaviour")]
|
|
[SerializeField] float posPanelAtCameraDistance = 0.35f; // how far the subtitle panel will be positioned from the camera
|
|
[SerializeField] float followCameraMovementSpeed = 2f;
|
|
[SerializeField] float followCameraRotationSpeed = 2f;
|
|
|
|
[Title("Direction Arrows")]
|
|
[SerializeField] Image leftArrow;
|
|
[SerializeField] Image rightArrow;
|
|
[SerializeField] Image aboveArrow;
|
|
[SerializeField] Image belowArrow;
|
|
|
|
[Title("Icon Tabs")]
|
|
[SerializeField] CanvasGroup continueIcons;
|
|
|
|
[Title("Panel Size")]
|
|
[SerializeField] float smlTextScale = 0.18f;
|
|
[SerializeField] float medTextScale = 0.25f;
|
|
[SerializeField] float lrgTextScale = 0.35f;
|
|
|
|
[Title("Audio Clips")]
|
|
[SerializeField] AudioClip subPopupSound;
|
|
[SerializeField] AudioClip subContinuedSound;
|
|
|
|
[Title("Fonts")]
|
|
[SerializeField] TMP_FontAsset defaultFont;
|
|
[SerializeField] Material defaultFontMat;
|
|
|
|
|
|
// Private Variables
|
|
|
|
[System.NonSerialized] string activeLine;
|
|
|
|
CharacterSubtitleData activeCharacterData;
|
|
|
|
Transform vrCameraTransform;
|
|
Transform speakingCharTransform;
|
|
|
|
//[System.NonSerialized] public bool textAnimatedToEnd;
|
|
|
|
Vector3 childCanvasOffscreenDefaultPos;
|
|
Vector3 childCanvasOffscreenDefaultRot;
|
|
|
|
public bool panelActive { get; private set; }
|
|
|
|
float timeLastHidden;
|
|
bool snappedToStartTransform;
|
|
|
|
bool dontFollowCharacter;
|
|
|
|
|
|
enum Mode
|
|
{
|
|
CameraFollow,
|
|
FixedToChar
|
|
}
|
|
Mode activeMode;
|
|
|
|
enum CharScreenPosition
|
|
{
|
|
OnScreen,
|
|
OffToLeft,
|
|
OffToRight
|
|
}
|
|
CharScreenPosition activeCharScreenPosition;
|
|
|
|
public enum ArrowDirection
|
|
{
|
|
Off,
|
|
Up,
|
|
Down,
|
|
Left,
|
|
Right
|
|
}
|
|
ArrowDirection activeArrowDir;
|
|
|
|
public enum SubtitleCharPlacement
|
|
{
|
|
Above,
|
|
Below
|
|
}
|
|
SubtitleCharPlacement activeSubsCharPlacement;
|
|
|
|
float followCharacterMinChange;
|
|
|
|
// LeanTween settings
|
|
|
|
float tabFadeInTime = 0.3f;
|
|
LeanTweenType tabFadeTweenType = LeanTweenType.easeOutSine;
|
|
|
|
int activeTabFadeTweenId;
|
|
|
|
Vector3 targetPosition;
|
|
Vector3 childTargetPosition;
|
|
Quaternion targetRotation;
|
|
|
|
AudioSource attachedAudioSource;
|
|
|
|
bool init;
|
|
|
|
// Subscribable events
|
|
public Action onLineDisplayed; // once line has been fully displayed to the player
|
|
public Action onSubtitleEnded; // when line is ready to be continued, either automatically or via player input
|
|
|
|
|
|
public void Init(Camera _vrCamera)
|
|
{
|
|
// Set the player camera position as target position for the parent object
|
|
vrCameraTransform = _vrCamera.transform;
|
|
|
|
// Get attached audio source (for playing some simple effects on interactions)
|
|
attachedAudioSource = GetComponent<AudioSource>();
|
|
|
|
timeLastHidden = Time.timeSinceLevelLoad;
|
|
|
|
UpdateArrow(ArrowDirection.Off);
|
|
|
|
// Get canvas group child starting / default transform values, for when UI is positioned when char off screen
|
|
childCanvasOffscreenDefaultPos = panelCanvasGroup.transform.localPosition;
|
|
childCanvasOffscreenDefaultRot = panelCanvasGroup.transform.localEulerAngles;
|
|
|
|
// Initialise canvas element to face camera
|
|
GetComponentInChildren<FaceCamera>().Init(_vrCamera);
|
|
|
|
// TODO: Initialise text animation here if turned on?
|
|
|
|
// Hide continue icons tab by default
|
|
ContinueIconsOff();
|
|
|
|
// Hide panel until first line requested
|
|
HidePanel();
|
|
|
|
init = true;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Display subtitle with requested data
|
|
/// </summary>
|
|
/// <param name="_text">Text to show (dialogue being said)</param>
|
|
/// <param name="_charTransform">Character transform to place the panel arrow</param>
|
|
/// <param name="_characterDataRef">Speaking character data (name and avatar) to display</param>
|
|
public void PlaySubtitle(string _text, Transform _charTransform, CharacterSubtitleData _characterDataRef, float _audioDuration, bool _dontAttachSubToCharacter = false)
|
|
{
|
|
// Catch not yet initialised
|
|
if (!init)
|
|
{
|
|
Debug.LogError("Tried to show a subtitle without initialising panel first!");
|
|
return;
|
|
}
|
|
|
|
//textAnimatedToEnd = false;
|
|
|
|
// TODO: Ensure font set and size to correct when shown
|
|
//UpdateFont();
|
|
//UpdateSize();
|
|
|
|
// Hide continue icons tab by default on start of new line
|
|
ContinueIconsOff();
|
|
|
|
// Set character transform to tie sub to
|
|
speakingCharTransform = _charTransform;
|
|
|
|
// Update speaking character name and avatar displayed
|
|
UpdateCharacterDisplay(_characterDataRef);
|
|
|
|
// Start subtitle position and rotation where we want it, before showing
|
|
SnapToFollowTarget();
|
|
|
|
// Set new active line to show
|
|
activeLine = _text;
|
|
|
|
// TODO: Animate text in (if text scrolling on), otherwise snap in immediately
|
|
//AnimateText(activeLine);
|
|
UpdateTextImmediate(_text);
|
|
|
|
// If we want to have this line not follow the character position
|
|
dontFollowCharacter = _dontAttachSubToCharacter;
|
|
|
|
// Allow continue after audio finished playing
|
|
Invoke(nameof(LineAudioFinished), _audioDuration);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Clears current subtitle and hides panel
|
|
/// </summary>
|
|
public void EndSubtitle()
|
|
{
|
|
// If active line
|
|
if (string.IsNullOrEmpty(activeLine))
|
|
{
|
|
// Manually ended
|
|
onSubtitleEnded?.Invoke();
|
|
}
|
|
|
|
// Clear active line
|
|
activeLine = null;
|
|
activeCharacterData = null;
|
|
|
|
// Hide panel
|
|
HidePanel();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Hide the UI element designed to show the "continue" input button etc
|
|
/// </summary>
|
|
void ContinueIconsOff()
|
|
{
|
|
// Stop any existing tab fade tween
|
|
LeanTween.cancel(activeTabFadeTweenId);
|
|
|
|
// Hide continue icon tab
|
|
continueIcons.alpha = 0;
|
|
continueIcons.gameObject.SetActive(false);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Unity Update function called each frame - we'll make sure subtitle panel positions are up-to-date here
|
|
/// </summary>
|
|
void Update()
|
|
{
|
|
if (!panelActive || !init || !snappedToStartTransform)
|
|
return;
|
|
|
|
// Get latest target positions for parent and child canvas object
|
|
CalculateTargetPositions();
|
|
|
|
// Lerp both parent and child objects pos to target
|
|
transform.position = Vector3.Lerp(transform.position, targetPosition, Time.fixedDeltaTime * followCameraMovementSpeed);
|
|
|
|
panelCanvasGroup.transform.localPosition = Vector3.Lerp(panelCanvasGroup.transform.localPosition, childTargetPosition, Time.fixedDeltaTime * followCameraMovementSpeed);
|
|
|
|
// If character is off-screen then also lerp rotation to target
|
|
if (activeCharScreenPosition != CharScreenPosition.OnScreen || dontFollowCharacter)
|
|
transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation,Time.fixedDeltaTime * followCameraRotationSpeed);
|
|
|
|
// TODO: Only lerp / update positions if they've actually changed
|
|
// Also add small deadzone at edges
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Get latest target positions for the panel objects, based on character being followed, where they are in relation to the player and camera, etc
|
|
/// </summary>
|
|
void CalculateTargetPositions()
|
|
{
|
|
// Get camera forward vector, projected onto horizontal plane
|
|
Vector3 cameraForwardProjection = Vector3.ProjectOnPlane(vrCameraTransform.forward, Vector3.up);
|
|
|
|
// Get directional vector from camera to currently speaking character, then project onto horizontal plane
|
|
Vector3 directionFromCameraToChar = (speakingCharTransform.position - vrCameraTransform.position).normalized;
|
|
Vector3 cameraToCharProjection = Vector3.ProjectOnPlane(directionFromCameraToChar, Vector3.up);
|
|
|
|
// Calculate angle between two vectors (projected onto a horizontal plane)
|
|
float angleBetweenVectors = Math.SignedAngleBetween(cameraForwardProjection, cameraToCharProjection, Vector3.up);
|
|
|
|
// Determine if character is on screen
|
|
if (angleBetweenVectors is >= -45 and <= 45)
|
|
activeCharScreenPosition = CharScreenPosition.OnScreen;
|
|
else if (angleBetweenVectors is > 45)
|
|
activeCharScreenPosition = CharScreenPosition.OffToRight;
|
|
else if (angleBetweenVectors is < -45)
|
|
activeCharScreenPosition = CharScreenPosition.OffToLeft;
|
|
|
|
// Update active camera follow mode (if needed based on character position on screen)
|
|
// unless dontFollowCharacter is set!
|
|
if (activeCharScreenPosition == CharScreenPosition.OnScreen && !dontFollowCharacter)
|
|
{
|
|
if (activeMode != Mode.FixedToChar)
|
|
UpdateFollowMode(Mode.FixedToChar);
|
|
}
|
|
else
|
|
{
|
|
if (activeMode != Mode.CameraFollow)
|
|
UpdateFollowMode(Mode.CameraFollow);
|
|
}
|
|
|
|
// If character is off-screen then subtitle will follow camera facing direction
|
|
if (activeCharScreenPosition != CharScreenPosition.OnScreen || dontFollowCharacter)
|
|
{
|
|
// Set target position. Will then Lerp parent object to camera position (smooths slightly, with child object at offset in front of camera)
|
|
targetPosition = vrCameraTransform.position;
|
|
|
|
// Get target rotation, following camera in y axis, but not moving in x or z
|
|
Vector3 curEulerAngles = transform.eulerAngles;
|
|
targetRotation = Quaternion.Euler(curEulerAngles.x, vrCameraTransform.eulerAngles.y, curEulerAngles.z);
|
|
|
|
// Reset child pos/rot
|
|
childTargetPosition = childCanvasOffscreenDefaultPos;
|
|
panelCanvasGroup.transform.localEulerAngles = childCanvasOffscreenDefaultRot;
|
|
|
|
// Show left/right arrows
|
|
if (!dontFollowCharacter)
|
|
{
|
|
if (activeCharScreenPosition == CharScreenPosition.OffToLeft)
|
|
UpdateArrow(ArrowDirection.Left);
|
|
else
|
|
UpdateArrow(ArrowDirection.Right);
|
|
}
|
|
else
|
|
{
|
|
UpdateArrow(ArrowDirection.Off);
|
|
}
|
|
}
|
|
else // otherwise we'll position the subtitle UI next to the character, locked in pos
|
|
{
|
|
// Get distance from camera to char (directional vector previously calculated above)
|
|
float distanceFromCameraToChar = Vector3.Distance(vrCameraTransform.position, speakingCharTransform.position);
|
|
|
|
// If distance < a minimum (onscreenCharDistance)
|
|
if (distanceFromCameraToChar < posPanelAtCameraDistance)
|
|
{
|
|
// panel parent at char position
|
|
targetPosition = speakingCharTransform.position;
|
|
}
|
|
else
|
|
{
|
|
// panel parent onscreenCharDistance along vector from camera to char
|
|
targetPosition = vrCameraTransform.position + directionFromCameraToChar * Mathf.Sqrt(posPanelAtCameraDistance);
|
|
}
|
|
|
|
|
|
// Position child object at offset from character pos (above or below)
|
|
childTargetPosition = Vector3.zero;
|
|
|
|
// Set arrow pointing direction
|
|
// If no character offset
|
|
if (activeCharacterData && activeCharacterData.subtitlePositionOffset == 0)
|
|
{
|
|
UpdateArrow(ArrowDirection.Up);
|
|
}
|
|
else // has offset
|
|
{
|
|
childTargetPosition = new Vector3(0, activeCharacterData.subtitlePositionOffset, 0);
|
|
if (activeCharacterData.subtitlePositionOffset <= 0)
|
|
UpdateArrow(ArrowDirection.Up);
|
|
else
|
|
UpdateArrow(ArrowDirection.Down);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Update current subtitle panel follow mode
|
|
/// </summary>
|
|
/// <param name="_newMode">Whether subtitle panel is following camera or locked to character in view</param>
|
|
void UpdateFollowMode(Mode _newMode)
|
|
{
|
|
activeMode = _newMode;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Immediately snap panel position to the target position
|
|
/// </summary>
|
|
public void SnapToFollowTarget()
|
|
{
|
|
CalculateTargetPositions();
|
|
|
|
// Immediately update both parent and child objects pos to target
|
|
transform.position = targetPosition;
|
|
panelCanvasGroup.transform.localPosition = childTargetPosition;
|
|
|
|
// If character is off-screen then also snap rotation to target
|
|
if (activeCharScreenPosition != CharScreenPosition.OnScreen)
|
|
transform.rotation = targetRotation;
|
|
|
|
snappedToStartTransform = true;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Update the arrow to face correct direction (towards speaking character)
|
|
/// </summary>
|
|
/// <param name="_updateTo"></param>
|
|
void UpdateArrow(ArrowDirection _updateTo)
|
|
{
|
|
if (activeArrowDir == _updateTo)
|
|
return;
|
|
|
|
activeArrowDir = _updateTo;
|
|
|
|
// If we don't want to show an arrow (i.e. if no character is being pointed to, maybe an off-screen narrator etc)
|
|
if (activeArrowDir == ArrowDirection.Off)
|
|
{
|
|
belowArrow.gameObject.SetActive(false);
|
|
aboveArrow.gameObject.SetActive(false);
|
|
leftArrow.gameObject.SetActive(false);
|
|
rightArrow.gameObject.SetActive(false);
|
|
return;
|
|
}
|
|
|
|
// Otherwise update active UI element for the desired arrow
|
|
|
|
if (activeArrowDir == ArrowDirection.Down)
|
|
belowArrow.gameObject.SetActive(true);
|
|
else
|
|
belowArrow.gameObject.SetActive(false);
|
|
|
|
if (activeArrowDir == ArrowDirection.Up)
|
|
aboveArrow.gameObject.SetActive(true);
|
|
else
|
|
aboveArrow.gameObject.SetActive(false);
|
|
|
|
if (activeArrowDir == ArrowDirection.Left)
|
|
leftArrow.gameObject.SetActive(true);
|
|
else
|
|
leftArrow.gameObject.SetActive(false);
|
|
|
|
if (activeArrowDir == ArrowDirection.Right)
|
|
rightArrow.gameObject.SetActive(true);
|
|
else
|
|
rightArrow.gameObject.SetActive(false);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Invoked once the audio associated with this line of dialogue has ended, allowing the subtitle to be continued / ended
|
|
/// </summary>
|
|
void LineAudioFinished()
|
|
{
|
|
// Show continue input tab, if turned on
|
|
if (waitForInput)
|
|
{
|
|
ShowContinueInputsTab();
|
|
}
|
|
else
|
|
{
|
|
LineContinued();
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Once conditions have been met to end this subtitle line
|
|
/// </summary>
|
|
void LineContinued()
|
|
{
|
|
// Play continued sound if set
|
|
if (subContinuedSound)
|
|
attachedAudioSource.PlayOneShot(subContinuedSound);
|
|
|
|
onSubtitleEnded?.Invoke();
|
|
|
|
activeLine = null;
|
|
activeCharacterData = null;
|
|
HidePanel();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Activate and show panel
|
|
/// </summary>
|
|
async UniTaskVoid ShowPanel()
|
|
{
|
|
// If panel is being shown, and wasn't already showing
|
|
|
|
// Wait a frame to ensure subtitle UI correctly positioned before showing
|
|
await UniTask.DelayFrame(2);
|
|
|
|
// Add a slight delay for playing sound again, in case toggled quickly off and on again
|
|
if (!panelActive && (Time.timeSinceLevelLoad > (timeLastHidden + 0.5f)))
|
|
{
|
|
// Play popup sound event
|
|
if (subPopupSound)
|
|
attachedAudioSource.PlayOneShot(subPopupSound);
|
|
}
|
|
|
|
// Show panel
|
|
panelCanvasGroup.alpha = 1f;
|
|
panelActive = true;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Hide the panel and set it as inactive for now
|
|
/// </summary>
|
|
void HidePanel()
|
|
{
|
|
panelActive = false;
|
|
snappedToStartTransform = false;
|
|
panelCanvasGroup.alpha = 0f;
|
|
timeLastHidden = Time.timeSinceLevelLoad;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Show the tab UI element which displays the "continue" input for subtitle line
|
|
/// </summary>
|
|
void ShowContinueInputsTab()
|
|
{
|
|
// Stop any existing tab fade tween
|
|
LeanTween.cancel(activeTabFadeTweenId);
|
|
|
|
// TODO: Get correct continue buttons from button mappings / active controller and change sprites accordingly before showing?
|
|
|
|
// Start alpha at 0, then we'll fade in
|
|
continueIcons.alpha = 0;
|
|
continueIcons.gameObject.SetActive(true);
|
|
|
|
// Fade in
|
|
activeTabFadeTweenId = LeanTween.alphaCanvas(continueIcons, 1f, tabFadeInTime).setEase(tabFadeTweenType).uniqueId;
|
|
|
|
// Listen for continue input
|
|
//if (continueInputEvent != null)
|
|
//continueInputEvent.RegisterListener(this);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Update the character avatar and name shown in the dialogue panel
|
|
/// </summary>
|
|
/// <param name="_characterData">Character whose avatar to use</param>
|
|
void UpdateCharacterDisplay(CharacterSubtitleData _characterData)
|
|
{
|
|
if (!_characterData)
|
|
return;
|
|
|
|
activeCharacterData = _characterData;
|
|
|
|
// TODO: If no character provided, use a default / anonymous avatar?
|
|
|
|
characterAvatar.sprite = activeCharacterData.avatar;
|
|
characterNameText.text = activeCharacterData.subtitleDisplayName + ":";
|
|
characterNameText.color = activeCharacterData.textNameColour;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Used internally to update the panel text, and ensure it's displayed
|
|
/// </summary>
|
|
/// <param name="_textToShow">Subtitle text to update to</param>
|
|
void UpdateTextImmediate(string _textToShow)
|
|
{
|
|
// Update text setting
|
|
subtitleText.text = _textToShow;
|
|
|
|
// Show the dialogue panel
|
|
ShowPanel();
|
|
//textAnimatedToEnd = true;
|
|
|
|
// Line now displayed
|
|
onLineDisplayed?.Invoke();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Updates the font used to display subtitles
|
|
/// </summary>
|
|
/// <param name="_newFont">TextMeshPro font to use</param>
|
|
/// <param name="_fontMaterial">Font material to use (if wish to override)</param>
|
|
public void UpdateFont(TMP_FontAsset _newFont, Material _fontMaterial = null)
|
|
{
|
|
characterNameText.font = _newFont;
|
|
subtitleText.font = _newFont;
|
|
|
|
if (_fontMaterial)
|
|
{
|
|
characterNameText.fontMaterial = _fontMaterial;
|
|
subtitleText.fontMaterial = _fontMaterial;
|
|
}
|
|
}
|
|
|
|
|
|
public void UpdateSize(int _newScale)
|
|
{
|
|
// TODO: Handle scaling better here!
|
|
|
|
switch (_newScale)
|
|
{
|
|
case 1:
|
|
gameObject.transform.localScale = new Vector3(smlTextScale, smlTextScale, smlTextScale);
|
|
break;
|
|
case 2:
|
|
gameObject.transform.localScale = new Vector3(medTextScale, medTextScale, medTextScale);
|
|
break;
|
|
case 3:
|
|
gameObject.transform.localScale = new Vector3(lrgTextScale, lrgTextScale, lrgTextScale);
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
}
|