Hi Brian, here is the PlayerConversant.cs Script:
using GameDevTV.Saving;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using System;
using RPG.Core; // for the Evaluator of the Dialogues (line 133)
using RPG.Movement;
namespace RPG.Dialogue {
public class PlayerConversant : MonoBehaviour, IAction
{
[SerializeField] string playerName;
Dialogue currentDialogue;
DialogueNode currentNode = null;
AIConversant currentConversant = null; // the conversation being occured between the Player and the AI (to trigger events during the conversation for instance)
bool isChoosing = false;
// OUT OF COURSE CONTENT ------------------------------------------------------------------------------------------------
private Dialogue targetDialogue; // the Dialogue our Player is aiming to go to, for conversation purposes
private AIConversant targetConversant; // the Target Conversant of our Quest Giver = Player Conversant
public float acceptanceRadius; // the minimum distance our Player has to be from the NPC before they can talk
// ----------------------------------------------------------------------------------------------------------------------
public event Action onConversationUpdated;
public void StartDialogue(AIConversant newConversant, Dialogue newDialogue) {
currentConversant = newConversant;
currentDialogue = newDialogue;
currentNode = currentDialogue.GetRootNode();
TriggerEnterAction();
onConversationUpdated(); // this line subscribes our Dialogue to the event Action we created above, so that it follows along when something new to that event occurs
}
public void Quit() {
// When the conversation is over, or the 'x' button is clicked, this function is called
currentDialogue = null; // no node available
TriggerExitAction();
currentNode = null; // no Nodes available to process through
isChoosing = false; // no choices of conversations available
currentConversant = null; // quitting the AI Conversant Dialogue
onConversationUpdated(); // makes the dialogue hide itself when the chat is over
}
public bool IsActive() {
// This function (a 'getter' function) returns whether we have a current Dialogue to refer to (after the 2 seconds of the IEnumerator)
return currentDialogue != null;
}
public bool IsChoosing() {
// getter
return isChoosing;
}
public string GetText() {
// getter
if (currentDialogue == null) {
return "";
}
return currentNode.GetText();
}
public IEnumerable<DialogueNode> GetChoices() {
return FilterOnCondition(currentDialogue.GetPlayerChildren(currentNode));
}
public void SelectChoice(DialogueNode chosenNode) {
currentNode = chosenNode;
TriggerEnterAction();
isChoosing = false;
// OPTIONAL: Implement this line only if you don't want the next conversation to display what your button just had written on it:
Next();
// if we didnt call 'Next()', which calls 'onConversationUpdated()' event subscription, we could've called it here instead
}
public void Next() {
int numPlayerResponses = FilterOnCondition(currentDialogue.GetPlayerChildren(currentNode)).Count();
if (numPlayerResponses > 0) {
isChoosing = true;
TriggerExitAction();
onConversationUpdated();
return;
}
DialogueNode[] children = FilterOnCondition(currentDialogue.GetAIChildren(currentNode)).ToArray(); // filters our Player <-> Quest Giver responses based on the process of our Quests
int randomIndex = UnityEngine.Random.Range(0, children.Count()); // UnityEngine is mentioned here because Random.Range comes from both UnityEngine and System (which we need for event Action), hence we need to specify which function we are calling
TriggerExitAction();
currentNode = children[randomIndex];
TriggerEnterAction();
onConversationUpdated();
}
public bool HasNext() {
return FilterOnCondition(currentDialogue.GetAllChildren(currentNode)).Count() > 0;
}
private IEnumerable<DialogueNode> FilterOnCondition(IEnumerable<DialogueNode> inputNode) {
// This function ensures we can play different Nodes on our dialogues, based on the Progress Status of our Quests (Start, Pending, Complete, etc)
foreach (var node in inputNode) {
if (node.CheckCondition(GetEvaluators())) {
yield return node; // if a condition (E.g: A quest has been done) is met, we include it in our filter, otherwise it's excluded from the Filter
}
}
}
private IEnumerable<IPredicateEvaluator> GetEvaluators() {
return GetComponents<IPredicateEvaluator>();
}
private void TriggerEnterAction() {
if (currentNode != null) {
TriggerAction(currentNode.GetOnEnterAction());
}
}
private void TriggerExitAction() {
if (currentNode != null)
{
TriggerAction(currentNode.GetOnExitAction());
}
}
private void TriggerAction(string action) {
if (action == "") return;
foreach(DialogueTrigger trigger in currentConversant.GetComponents<DialogueTrigger>()) {
trigger.Trigger(action);
}
}
public string GetCurrentConversantName()
{
if (isChoosing) {
return playerName;
}
else {
return currentConversant.GetName();
}
}
// MORE OUT OF COURSE CONTENT -----------------------------------------------------------------------------
public void Cancel()
{
targetConversant = null;
}
public void StartConversation(AIConversant conversant, Dialogue dialogue) {
GetComponent<ActionSchedular>().StartAction(this);
targetConversant = conversant;
targetDialogue = dialogue;
}
void Update() {
if (!targetConversant) return;
if (Vector3.Distance(transform.position, targetConversant.transform.position) > acceptanceRadius) {
transform.LookAt(targetConversant.transform);
GetComponent<Mover>().MoveTo(targetConversant.transform.position, 1.0f);
}
else {
GetComponent<Mover>().Cancel();
StartDialogue(targetConversant, targetDialogue);
targetConversant = null;
}
}
// -----------------------------------------------------------------------------------------------------------
}
}
and here is my DialogueUI.cs Script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using RPG.Dialogue;
using TMPro;
namespace RPG.UI {
public class DialogueUI : MonoBehaviour
{
PlayerConversant playerConversant;
[SerializeField] TextMeshProUGUI AIText;
[SerializeField] Button nextButton;
[SerializeField] GameObject AIResponse;
[SerializeField] Transform choiceRoot;
[SerializeField] GameObject choicePrefab;
[SerializeField] Button quitButton; // quit button, to quit the dialogue midway through
[SerializeField] TextMeshProUGUI conversantName; // name of our conversant in a dialogue conversation
// Start is called before the first frame update
void Start()
{
playerConversant = GameObject.FindGameObjectWithTag("Player").GetComponent<PlayerConversant>();
playerConversant.onConversationUpdated += UpdateUI; // subscribing our UI Upate to our Conversation UI Updates (so our UI Updates with our Dialogue)
nextButton.onClick.AddListener(() => playerConversant.Next()); // calls the 'Next' function (yes, the function not a variable) Subscriber (to the onClick event) when our player hits 'Next' in the dialogue, using the Lambda "() => {}" function
quitButton.onClick.AddListener(() => playerConversant.Quit()); // Lambda Function, "() => {}", is used here to add a listener for our quit button. When clicked the script is closed
UpdateUI();
}
// Update is called once per frame
void UpdateUI()
{
// The following line ensures the dialogue is invisible until the return time
// of the IEnumerator in 'playerConversant.cs' time counter is over
gameObject.SetActive(playerConversant.IsActive());
if (!playerConversant.IsActive()) {
return;
}
conversantName.text = playerConversant.GetCurrentConversantName();
AIResponse.SetActive(!playerConversant.IsChoosing());
choiceRoot.gameObject.SetActive(playerConversant.IsChoosing());
if (playerConversant.IsChoosing())
{
BuildChoiceList();
}
else {
AIText.text = playerConversant.GetText();
nextButton.gameObject.SetActive(playerConversant.HasNext());
}
}
private void BuildChoiceList()
{
foreach (Transform item in choiceRoot) {
// avoids Dangling Nodes in the Hierarchy, which will eventually slow our entire game down
Destroy(item.gameObject);
}
foreach (DialogueNode choice in playerConversant.GetChoices())
{
GameObject choiceInstance = Instantiate(choicePrefab, choiceRoot);
var textComp = choiceInstance.GetComponentInChildren<TextMeshProUGUI>();
textComp.text = choice.GetText();
Button button = choiceInstance.GetComponentInChildren<Button>();
button.onClick.AddListener(() => { // "() => {}" is a 'lambda' function ("()" is the argument, "{}" is the internals of the function), which only works when a button is clicked (do some research about this)
playerConversant.SelectChoice(choice);
});
}
}
}
}
Just to be extremely clear with my issue: Everytime, at the end of the quest, I hit the ‘X’ button, I get the reward but the dialogue never closes, and then you can pretty much keep clicking the button forever and it’ll never close, it’ll just keep rewarding you forever (as fun as that sounds to have, I’d like to eliminate this bug from my game )