Gameplay.DialogueController - robblofield/TomeboundDocs GitHub Wiki

Gameplay.DialogueController

Author: Shahrzad Beevis
Last Updated: 14/05/2024

Overview

A script controlling the sequencing and use of dialogue.

Dependencies

N/A

Contents

  • Use case
  • Breakdown of code
    • Using Statements
    • Class Declaration
    • Variables
    • Method/Awake()
    • Method/OnEnable()
    • Method/OnDisable()
    • Method/HandleDialogueInteraction(InputAction.CallbackContext context)
    • Method/IEnumerator LerpFocalLengthAndContrast(float targetFocalValue, float targetContrastValue)
    • Method/NextSentence()
    • Method/IEnumerator EndDialogueCoroutine()
    • Method/EndDialogue()
    • Method/IEnumerator DeactivateBackgroundWithDelay()
    • Method/IEnumerator WriteSentence()
    • Method/DisplayFullSentence()
    • Method/SetSpeakerBackground(int speaker)
    • Method/UpdateSpeakerText(int speaker)
    • Method/IEnumerator StartDialogueAfterDelay(float delay)
    • Method/DeactivateGameObject()
  • Future Expansion
  • Full Code Reference

Use case

The script can be used whenever dialogue cutscenes are required in game, and can be adapted to a varying number of speakers.

Breakdown of Code

Using Statements

using UnityEngine;
using System.Collections;
using TMPro;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering;
using UnityEngine.InputSystem;

Class Declaration

public class DialogueController : MonoBehaviour

Variables

 public TextMeshProUGUI Speaker1Text;
 public TextMeshProUGUI Speaker2Text;
 public GameObject Speaker1Background;
 public GameObject Speaker2Background;
 public Animator Speaker1Animator;
 public Animator Speaker2Animator;

 [TextArea(3, 10)]
 public string[] Sentences;
 public int[] Speakers;
 private int Index = 0;
 public float DialogueSpeed;
 public float EnterExitDelay = 1f;
 public bool dialogueCalled = false;
 private bool dialogueStarted = false;
 public bool dialogueEnded = false;

 private Coroutine typingCoroutine;

 public Volume volume;
 private DepthOfField depthOfField;
 private ColorAdjustments colorAdjustments;

 private GridBasedMovement1 playerMovement;

 public float startingFocalLength = 167f;
 public float targetFocalLength = 300f;

 public float startingContrast = 0f;
 public float targetContrast = 60f;
 public float lerpSpeed = 1f;

 private PlayerInputActions playerActions;

public TextMeshProUGUI Speaker1Text & public TextMeshProUGUI Speaker2Text:

These variables hold references to the TextMeshPro components responsible for displaying text attributed to Speaker 1 and Speaker 2.

public Animator Speaker1Animator & public Animator Speaker2Animator:

Represents the background GameObjects associated with Speaker 1 and Speaker 2's dialogues.

public GameObject Speaker1Background & public GameObject Speaker2Background:

These variable reference the Animator components controlling animations for Speaker 1 and Speaker 2's dialogues.

[TextArea(3, 10)] public string[] Sentences:

An array holding the sentences of the dialogue. The TextArea attribute specifies the minimum and maximum lines for the text area when viewed in the Unity editor.

public int[] Speakers:

An array indicating which speaker (0 for Speaker 1, 1 for Speaker 2) delivers each sentence.

private int Index = 0:

Tracks the index of the current sentence being displayed in the dialogue.

public float DialogueSpeed:

Determines the speed at which characters are displayed during the dialogue.

public float EnterExitDelay = 1f:

Specifies the delay (in seconds) between entering and exiting animations for each sentence.

public float EnterExitDelay = 1f:

Specifies the delay (in seconds) between entering and exiting animations for each sentence.

public bool dialogueCalled = false:

Indicates whether the dialogue has been initiated externally.

private bool dialogueStarted = false:

Tracks whether the dialogue display process has started.

public bool dialogueEnded = false:

Indicates whether the dialogue has reached its conclusion.

private Coroutine typingCoroutine:

Holds a reference to the coroutine responsible for typing out the current sentence.

public Volume volume:

Reference to the Volume component used to apply post-processing effects.

private DepthOfField depthOfField:

Reference to the DepthOfField post-processing effect within the Volume component.

Awake()

private void Awake()
{
    playerActions = new PlayerInputActions();
    playerActions.Player.ObjectInteraction.performed += ctx => HandleDialogueInteraction(ctx);
}

This method is called when the script is initialised. It creates a new instance of PlayerInputActions and assigns it to the playerActions variable. Then, it subscribes to the 'ObjectInteraction' action performed by the player input actions, linking it to the HandleDialogueInteraction(ctx) method. This ensures that the script can respond to player interactions.

OnEnable()

private void OnEnable()
{
    playerActions.Player.Enable();
}

This method is invoked when the script's GameObject becomes enabled and active. It enables the player input actions, allowing the player to interact with the dialogue system.

OnDisable()

private void OnDisable()
{
    playerActions.Player.Disable();
}

This method is called when the script's GameObject becomes disabled or inactive. It disables the player input actions, preventing further interaction with the dialogue system while it's inactive.

Start()

 private void Start()
 {
     volume.profile.TryGet(out depthOfField);
     volume.profile.TryGet(out colorAdjustments);
     playerMovement = FindObjectOfType<GridBasedMovement1>();
 }

This method is executed before the first frame update. It attempts to retrieve references to the DepthOfField and ColorAdjustments components from the assigned Volume component. These components are used to control post-processing effects. Additionally, it finds and assigns the GridBasedMovement1 component to the playerMovement variable, enabling interaction with the player's movement.

HandleDialogueInteraction(InputAction.CallbackContext context)

  private void HandleDialogueInteraction(InputAction.CallbackContext context)
  {
      if (dialogueStarted && dialogueCalled)
      {
          if (typingCoroutine != null)
          {
              StopCoroutine(typingCoroutine);
              DisplayFullSentence();
          }
          else
          {
              NextSentence();
          }
      }
  }

This method is triggered when the player interacts with the dialogue system. It checks if the dialogue has started and been called. If both conditions are met, it checks if a typing coroutine is currently running. If so, it stops the coroutine and displays the full sentence instantly. If not, it proceeds to display the next sentence.

StartDialogueFromScript()

public void StartDialogueFromScript()
{
    Debug.Log("StartDialogueFromScript() called.");
    dialogueCalled = true;

    if (typingCoroutine != null)
    {
        StopCoroutine(typingCoroutine);
        DisplayFullSentence();
    }
    else
    {
        NextSentence();
    }

    dialogueStarted = true;

    playerMovement.xLocked = true;
    playerMovement.yLocked = true;

This method is intended to start the dialogue from an external script. It sets dialogueCalled to true, indicating that the dialogue has been initiated externally. It then checks if a typing coroutine is currently running. If so, it stops the coroutine and displays the full sentence instantly. If not, it proceeds to display the next sentence. Additionally, it sets dialogueStarted to true and locks the player's movement on the x and y axes.

IEnumerator LerpFocalLengthAndContrast(float targetFocalValue, float targetContrastValue)

private IEnumerator LerpFocalLengthAndContrast(float targetFocalValue, float targetContrastValue)
{
    float startFocalValue = depthOfField.focalLength.value;
    float startContrastValue = colorAdjustments.contrast.value;
    float elapsedTime = 0f;

    while (elapsedTime < lerpSpeed)
    {
        float t = elapsedTime / lerpSpeed;
        depthOfField.focalLength.value = Mathf.Lerp(startFocalValue, targetFocalValue, t);
        colorAdjustments.contrast.value = Mathf.Lerp(startContrastValue, targetContrastValue, t);

        elapsedTime += Time.deltaTime;
        yield return null;
    }

    depthOfField.focalLength.value = targetFocalValue;
    colorAdjustments.contrast.value = targetContrastValue;
}

This coroutine smoothly transitions the camera's focal length and contrast to the specified target values over time. It calculates the interpolation between the initial and target values based on the specified lerp speed. This function is used to create dynamic visual effects during dialogue sequences.

NextSentence()

   private void NextSentence()
   {
       if (Index < Sentences.Length)
       {
           UpdateSpeakerText(Speakers[Index]);
           typingCoroutine = StartCoroutine(WriteSentence());
       }
       else
       {
           EndDialogue();
           StartCoroutine(EndDialogueCoroutine());
       }
   }

This method displays the next sentence in the dialogue sequence. If there are more sentences remaining, it updates the speaker's text and starts a coroutine to gradually display the sentence characters. If it's the last sentence, it triggers the end of the dialogue sequence.

IEnumerator EndDialogueCoroutine()

private IEnumerator EndDialogueCoroutine()
{
    StartCoroutine(LerpFocalLengthAndContrast(startingFocalLength, startingContrast));
    yield return new WaitForSeconds(lerpSpeed);
    DeactivateGameObject();
}

This coroutine manages the end of the dialogue sequence. It smoothly transitions post-processing effects back to their initial values over time, creating a visually pleasing transition. After the transition is complete, it deactivates the dialogue GameObject.

EndDialogue()

private void EndDialogue()
{
    if (Index < Sentences.Length)
    {
        StartCoroutine(DeactivateBackgroundWithDelay());
    }
    else
    {
        dialogueEnded = true;
        Speaker1Background.SetActive(false);
        Speaker2Background.SetActive(false);
        Speaker1Text.text = "";
        Speaker2Text.text = "";

    }
}

This method handles the conclusion of the dialogue sequence. If there are more sentences remaining, it starts a coroutine to deactivate speaker backgrounds with a delay. If it's the last sentence, it sets dialogueEnded to true and deactivates speaker backgrounds, clearing the speaker text.

IEnumerator DeactivateBackgroundWithDelay()

private IEnumerator DeactivateBackgroundWithDelay()
{
    yield return new WaitForSeconds(EnterExitDelay);
    if (Index < Sentences.Length)
    {
        int currentSpeaker = Speakers[Index];
        if (currentSpeaker == 0)
            Speaker1Animator.SetTrigger("Exit");
        else
            Speaker2Animator.SetTrigger("Exit");
        Speaker1Background.SetActive(false);
        Speaker2Background.SetActive(false);
        Index++;
    }
}

This coroutine deactivates speaker backgrounds after a specified delay. It triggers the exit animation for the current speaker's background and deactivates both speaker backgrounds. This delay creates a smooth transition between sentences.

IEnumerator WriteSentence()

private IEnumerator WriteSentence()
{
    int currentSpeaker = Speakers[Index];
    SetSpeakerBackground(currentSpeaker);
    if (currentSpeaker == 0)
        Speaker1Animator.SetTrigger("Enter");
    else
        Speaker2Animator.SetTrigger("Enter");
    yield return new WaitForSeconds(EnterExitDelay);
    TextMeshProUGUI currentText = currentSpeaker == 0 ? Speaker1Text : Speaker2Text;

    currentText.text = "";

    string sentence = Sentences[Index];
    foreach (char c in sentence)
    {
        currentText.text += c;
        yield return new WaitForSeconds(DialogueSpeed);
    }
    DisplayFullSentence();
}

This coroutine simulates a typing animation by gradually displaying each character of the current sentence. It sets the speaker's background, triggers the enter animation, and then iterates through the characters of the sentence, displaying them with a delay determined by the dialogue speed.

DisplayFullSentence()

 private void DisplayFullSentence()
 {
     int currentSpeaker = Speakers[Index];
     TextMeshProUGUI currentText = currentSpeaker == 0 ? Speaker1Text : Speaker2Text;
     currentText.text = Sentences[Index];
     Index++;
     typingCoroutine = null;
 }

This method instantly displays the full sentence without a typing animation. It updates the speaker's text and moves to the next sentence in the dialogue sequence.

SetSpeakerBackground(int speaker)

private void SetSpeakerBackground(int speaker)
{
    if (speaker == 0)
    {
        Speaker1Background.SetActive(true);
        Speaker2Background.SetActive(false);
    }
    else
    {
        Speaker1Background.SetActive(false);
        Speaker2Background.SetActive(true);
    }
}

This method sets the background for the current speaker based on the provided speaker index. It activates the background for the appropriate speaker and deactivates the background for the other speaker, ensuring that only the relevant background is displayed.

UpdateSpeakerText(int speaker)

  private void UpdateSpeakerText(int speaker)
  {
      if (speaker == 0)
      {
          Speaker2Text.text = "";
          Speaker2Background.SetActive(false);
      }
      else
      {
          Speaker1Text.text = "";
          Speaker1Background.SetActive(false);
      }
  }

This method updates the text display for the specified speaker. It clears the text for the inactive speaker, ensuring that only the text for the current speaker is visible.

IEnumerator StartDialogueAfterDelay(float delay)

private IEnumerator StartDialogueAfterDelay(float delay)
{
    yield return new WaitForSeconds(delay);
    NextSentence();
}

This coroutine introduces a delay before starting the dialogue sequence. After the specified delay, it proceeds to display the next sentence in the dialogue sequence.

DeactivateGameObject()

private void DeactivateGameObject()
{
    gameObject.SetActive(false);
}

This method deactivates the GameObject to which the script is attached, effectively hiding the dialogue interface from the player. This is used to conclude the dialogue sequence or hide the dialogue interface when it's no longer needed.

Future Expansion

N/A

Full Code Reference

using UnityEngine;
using System.Collections;
using TMPro;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering;
using UnityEngine.InputSystem;

public class DialogueController : MonoBehaviour
{
    public TextMeshProUGUI Speaker1Text;
    public TextMeshProUGUI Speaker2Text;
    public GameObject Speaker1Background;
    public GameObject Speaker2Background;
    public Animator Speaker1Animator;
    public Animator Speaker2Animator;

    [TextArea(3, 10)]
    public string[] Sentences;
    public int[] Speakers;
    private int Index = 0;
    public float DialogueSpeed;
    public float EnterExitDelay = 1f;
    public bool dialogueCalled = false;
    private bool dialogueStarted = false;
    public bool dialogueEnded = false;

    private Coroutine typingCoroutine;

    public Volume volume;
    private DepthOfField depthOfField;
    private ColorAdjustments colorAdjustments;

    private GridBasedMovement1 playerMovement;

    public float startingFocalLength = 167f;
    public float targetFocalLength = 300f;

    public float startingContrast = 0f;
    public float targetContrast = 60f;
    public float lerpSpeed = 1f;

    private PlayerInputActions playerActions;

    private void Awake()
    {
        playerActions = new PlayerInputActions();
        playerActions.Player.ObjectInteraction.performed += ctx => HandleDialogueInteraction(ctx);
    }

    private void OnEnable()
    {
        playerActions.Player.Enable();
    }

    private void OnDisable()
    {
        playerActions.Player.Disable();
    }

    private void Start()
    {
        volume.profile.TryGet(out depthOfField);
        volume.profile.TryGet(out colorAdjustments);
        playerMovement = FindObjectOfType<GridBasedMovement1>();
    }

    private void HandleDialogueInteraction(InputAction.CallbackContext context)
    {
        if (dialogueStarted && dialogueCalled)
        {
            if (typingCoroutine != null)
            {
                StopCoroutine(typingCoroutine);
                DisplayFullSentence();
            }
            else
            {
                NextSentence();
            }
        }
    }

    public void StartDialogueFromScript()
    {
        Debug.Log("StartDialogueFromScript() called.");
        dialogueCalled = true;

        if (typingCoroutine != null)
        {
            StopCoroutine(typingCoroutine);
            DisplayFullSentence();
        }
        else
        {
            NextSentence();
        }

        dialogueStarted = true;

        playerMovement.xLocked = true;
        playerMovement.yLocked = true;

        StartCoroutine(LerpFocalLengthAndContrast(targetFocalLength, targetContrast));
    }

    private IEnumerator LerpFocalLengthAndContrast(float targetFocalValue, float targetContrastValue)
    {
        float startFocalValue = depthOfField.focalLength.value;
        float startContrastValue = colorAdjustments.contrast.value;
        float elapsedTime = 0f;

        while (elapsedTime < lerpSpeed)
        {
            float t = elapsedTime / lerpSpeed;
            depthOfField.focalLength.value = Mathf.Lerp(startFocalValue, targetFocalValue, t);
            colorAdjustments.contrast.value = Mathf.Lerp(startContrastValue, targetContrastValue, t);

            elapsedTime += Time.deltaTime;
            yield return null;
        }

        depthOfField.focalLength.value = targetFocalValue;
        colorAdjustments.contrast.value = targetContrastValue;
    }

    private void NextSentence()
    {
        if (Index < Sentences.Length)
        {
            UpdateSpeakerText(Speakers[Index]);
            typingCoroutine = StartCoroutine(WriteSentence());
        }
        else
        {
            EndDialogue();
            StartCoroutine(EndDialogueCoroutine());
        }
    }

    private IEnumerator EndDialogueCoroutine()
    {
        StartCoroutine(LerpFocalLengthAndContrast(startingFocalLength, startingContrast));
        yield return new WaitForSeconds(lerpSpeed);
        DeactivateGameObject();
    }

    private void EndDialogue()
    {
        if (Index < Sentences.Length)
        {
            StartCoroutine(DeactivateBackgroundWithDelay());
        }
        else
        {
            dialogueEnded = true;
            Speaker1Background.SetActive(false);
            Speaker2Background.SetActive(false);
            Speaker1Text.text = "";
            Speaker2Text.text = "";

        }
    }

    private IEnumerator DeactivateBackgroundWithDelay()
    {
        yield return new WaitForSeconds(EnterExitDelay);
        if (Index < Sentences.Length)
        {
            int currentSpeaker = Speakers[Index];
            if (currentSpeaker == 0)
                Speaker1Animator.SetTrigger("Exit");
            else
                Speaker2Animator.SetTrigger("Exit");
            Speaker1Background.SetActive(false);
            Speaker2Background.SetActive(false);
            Index++;
        }
    }

    private IEnumerator WriteSentence()
    {
        int currentSpeaker = Speakers[Index];
        SetSpeakerBackground(currentSpeaker);
        if (currentSpeaker == 0)
            Speaker1Animator.SetTrigger("Enter");
        else
            Speaker2Animator.SetTrigger("Enter");
        yield return new WaitForSeconds(EnterExitDelay);
        TextMeshProUGUI currentText = currentSpeaker == 0 ? Speaker1Text : Speaker2Text;

        currentText.text = "";

        string sentence = Sentences[Index];
        foreach (char c in sentence)
        {
            currentText.text += c;
            yield return new WaitForSeconds(DialogueSpeed);
        }
        DisplayFullSentence();
    }

    private void DisplayFullSentence()
    {
        int currentSpeaker = Speakers[Index];
        TextMeshProUGUI currentText = currentSpeaker == 0 ? Speaker1Text : Speaker2Text;
        currentText.text = Sentences[Index];
        Index++;
        typingCoroutine = null;
    }

    private void SetSpeakerBackground(int speaker)
    {
        if (speaker == 0)
        {
            Speaker1Background.SetActive(true);
            Speaker2Background.SetActive(false);
        }
        else
        {
            Speaker1Background.SetActive(false);
            Speaker2Background.SetActive(true);
        }
    }

    private void UpdateSpeakerText(int speaker)
    {
        if (speaker == 0)
        {
            Speaker2Text.text = "";
            Speaker2Background.SetActive(false);
        }
        else
        {
            Speaker1Text.text = "";
            Speaker1Background.SetActive(false);
        }
    }

    private IEnumerator StartDialogueAfterDelay(float delay)
    {
        yield return new WaitForSeconds(delay);
        NextSentence();
    }

    private void DeactivateGameObject()
    {
        gameObject.SetActive(false);
    }
}