Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Referencing Children of Classes in C# Scripting

Discussion in 'Scripting' started by JimothyMarmalade, Jul 13, 2021.

  1. JimothyMarmalade

    JimothyMarmalade

    Joined:
    Apr 6, 2019
    Posts:
    10
    I'm working on a dialog system for my game. The system I've created uses a parent class "Dialog" that inherits from ScriptableObject, and child classes that hold unique information such as if the Dialog presents a choice to the player or activates a quest, for example.

    Currently in my DialogManager, I've created multiple near-duplicate methods for displaying dialog that perform the same operations, but with one or two additional actions based on the class of Dialog. This has led to having to juggle several variables and method overloads to keep the scripting operational.

    My question is whether or not it is possible, within a single C# method, to distinguish if an object's class is a child of Dialog and perform the unique actions associated with it. Simply casting the child to Dialog does not seem to work.

    For reference, here's a sample of the code:

    Code (CSharp):
    1. public void StartDialog(Dialog dialog, ExpressionController face)
    2.     {
    3.         //Debug.Log("Starting conversation with " + dialog.speakerName);
    4.         animator.SetBool("IsOpen", true);
    5.  
    6.         DEBUGDialogMenuNametag.SetActive(true);
    7.  
    8.         ShowNextButton();
    9.  
    10.         currentDialog = dialog;
    11.         focusedCharacterFace = face;
    12.  
    13.  
    14.         nameText.text = currentDialog.speakerName;
    15.         string sentence = currentDialog.dialogLine[0];
    16.         //Debug.Log(sentence);
    17.         StopAllCoroutines();
    18.         StartCoroutine(TypeSentence(sentence));
    19.         focusedCharacterFace.ChangeExpression(currentDialog.eyesExpression, currentDialog.mouthExpression);
    20.     }
    21.  
    22. public void StartDialogChoice(DialogChoice d, ExpressionController face)
    23.     {
    24.         //Debug.Log("Starting conversation with " + dialog.speakerName);
    25.         animator.SetBool("IsOpen", true);
    26.  
    27.         DEBUGDialogMenuNametag.SetActive(true);
    28.  
    29.  
    30.         currentDialogChoice = d;
    31.         focusedCharacterFace = face;
    32.         nameText.text = currentDialogChoice.speakerName;
    33.  
    34.         string sentence = currentDialogChoice.dialogLine[0];
    35.  
    36.  
    37.         ShowOptionsButtons();
    38.         Op1Button.GetComponentInChildren<TMP_Text>().text = currentDialogChoice.PlayerReaction1;
    39.  
    40.         Op2Button.GetComponentInChildren<TMP_Text>().text = currentDialogChoice.PlayerReaction2;
    41.  
    42.  
    43.         StopAllCoroutines();
    44.         StartCoroutine(TypeSentence(sentence));
    45.     }
    They're two methods that do essentially the same thing, but rely on the overload to do something slightly different. I'd like this to be optimized to where it's all in a single method for multiple potential classes to be input.
     
    Last edited: Jul 13, 2021
  2. Epsilon_Delta

    Epsilon_Delta

    Joined:
    Mar 14, 2018
    Posts:
    207
  3. gorbit99

    gorbit99

    Joined:
    Jul 14, 2015
    Posts:
    1,350
    Well, first of all, you missed the most important part of creating methods: Creating even more methods for functionality that's shared between your overloads. Your code is very wet and a huge portion of your methods could be separated out into a new method.
    Secondly, a big part of oop is the "don't ask, tell" principle. If your logic depends on the kind of dialog you get, then make the different dialogs inherit some baseclass or interface, then have them show themselves. Create a virtual method, override it to your hearts content, and then have a single method on the parent to call this. Oop will take care of the rwst.
     
  4. JimothyMarmalade

    JimothyMarmalade

    Joined:
    Apr 6, 2019
    Posts:
    10
    What exactly do you mean by creating an interface to "Show Themselves?" Are you referring to an inherited method that returns the type of the class?
     
  5. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,571
    No, you still think backwards. You still have the assumption that the calling code needs to know what exact type it deals with. The point of OOP is the exact opposite. The calling code should not care about what kind of implementation it deals with. The base class essentially establishes an interface and describes what kind of methods every dialog has. What individual child classes do in those methods can be overridden by them. That's the point of polymorphism.

    You haven't really provided a lot of code to look at. How does your base class look like? Does it actually have virtual methods? Do the child classes override any of those methods? If you don't have virtual classes, implementing a base class and child classes is kinda pointless. All you're doing now is just composing new classes out of another one.

    Apart from having a base class with virtual methods, one could also use an interface and have the concrete classes implement that interface. This is very similar to actual inheritance, but interfaces only define methods.

    Apart from the pure oop point of view, it's often better to build more complex logic out of composition of small building blocks. Specifically with scriptable objects and the strategy pattern you can simply "plugin" generic actions into a sequence which would simply hold a list of such generic action objects. So you can simply plug those things together in the editor.

    Though as mentioned you provided almost no code so it's completely unclear where and how you actually use your Dialog class(es) and how they are implemented. To me that sounds a bit like you don't really understand the concept of classes and inheritance. Maybe you should look up some basic tutorials on OOP. Though keep in mind that there is not just a single solution for a certain problem.

    Richard Fine has some examples on how to use ScriptableObjects as "pluggable" logic in his Unite talk.

    If you want more help from our side, you need to share more details on how your system works and what does the base class look like. We had countless generic questions like this and we can't provide generic examples of how OOP works every time :)
     
    mopthrow likes this.
  6. JimothyMarmalade

    JimothyMarmalade

    Joined:
    Apr 6, 2019
    Posts:
    10
    Well, I suppose I'll start there. I guess it's my own fault for attempting to pose a question when I was half-asleep, huh? There is a lot of info missing.

    First up is the base Dialog class that inherits from ScriptableObject. All it does is hold information about one piece of a conversation, enough to fit in a single text box. There's also a reference to the next Dialog in the chain.

    Below is the entirety of the Dialog class:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. [CreateAssetMenu(fileName = "New Dialog", menuName = "Dialog/New Empty Dialog")]
    6. public class Dialog: ScriptableObject
    7. {
    8.     //Used as a casual Identifier for dialog triggers for programmers to keep track of what's what
    9.     public string dialogIdentifier = "";
    10.  
    11.     //Name of the person speaking in this dialog segment
    12.     public string speakerName = "";
    13.  
    14.     //Name of the eyes expression used when saying this dialogue
    15.     public ExpressionController.EyesExpression eyesExpression;
    16.  
    17.     //name of the mouth expression used when saying this dialogue
    18.     public ExpressionController.MouthExpression mouthExpression;
    19.  
    20.     //name of the body animation used when saying this dialogue
    21.     public string bodyAnimation = "";
    22.  
    23.     //The sentences that will be spoken
    24.     [TextArea(3, 10)]
    25.     public string dialogLine;
    26.  
    27.     //Reference to the next dialog in the chain
    28.     public Dialog NextDialog;
    29. }
    But, I don't want each piece of Dialog to be as cut-and-dry as a single sentence or paragraph. I want to implement choices, or for a quest to be assigned. So my current method of implementing such has been to create a new class that inherits from Dialog, such as with DialogChoice:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5.  
    6. [CreateAssetMenu(fileName = "New DialogChoice", menuName = "Dialog/New Empty DialogChoice")]
    7.  
    8. public class DialogChoice : Dialog
    9. {
    10.  
    11.     [Header("Multiple Choice Dialog Paths")]
    12.     public string PlayerReaction1;
    13.     public Dialog DialogChoice1;
    14.     public string PlayerReaction2;
    15.     public Dialog DialogChoice2;
    16. }
    17.  
    ...or DialogQuest:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. [CreateAssetMenu(fileName = "New DialogQuest", menuName = "Dialog/New Empty DialogQuest")]
    6. public class DialogQuest : Dialog
    7. {
    8.     [Header("Assigned Quest")]
    9.     public Quest AssignedQuest;
    10. }
    11.  
    The above code is all that exists for those classes.

    These objects are fed into a DialogManager that displays the information saved to them to my conversation UI. As you can see, I've tried to have the dialogs just be data containers. My DialogManager has a series of Methods that are nearly identical, but with unique overloads to account for the different classes of dialog. Here is the beginning of DialogManager:

    Code (CSharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. using UnityEngine.UI;
    6. using TMPro;
    7.  
    8. public class DialogManager : MonoBehaviour
    9. {
    10.     public static DialogManager Instance { get; set; }
    11.  
    12.     public GameObject DEBUGDialogMenuNametag;
    13.  
    14.     public delegate void DialogEvent();
    15.     public static event DialogEvent OnDialogOver;
    16.  
    17.     [Header("Speaker/Text Data")]
    18.     public TMP_Text nameText;
    19.     public TMP_Text dialogText;
    20.  
    21.     [Header("Option Buttons")]
    22.     public GameObject NextButton;
    23.     public GameObject Op1Button;
    24.     public GameObject Op2Button;
    25.  
    26.     [Header("Animator Reference")]
    27.     public Animator animator;
    28.  
    29.     private Dialog currentDialog;
    30.     private DialogChoice currentDialogChoice;
    31.     private ExpressionController focusedCharacterFace;
    32.  
    33.     public void StartDialog(Dialog dialog, ExpressionController face)
    34.     {
    35.         //Debug.Log("Starting conversation with " + dialog.speakerName);
    36.         animator.SetBool("IsOpen", true);
    37.  
    38.         DEBUGDialogMenuNametag.SetActive(true);
    39.  
    40.         ShowNextButton();
    41.  
    42.         currentDialog = dialog;
    43.         focusedCharacterFace = face;
    44.  
    45.  
    46.         nameText.text = currentDialog.speakerName;
    47.         string sentence = currentDialog.dialogLine;
    48.         //Debug.Log(sentence);
    49.         StopAllCoroutines();
    50.         StartCoroutine(TypeSentence(sentence));
    51.         focusedCharacterFace.ChangeExpression(currentDialog.eyesExpression, currentDialog.mouthExpression);
    52.     }
    53.  
    54.     public void DisplayNextDialogLine()
    55.     {
    56.         if (currentDialog.NextDialog == null)
    57.         {
    58.             EndDialog();
    59.             return;
    60.         }
    61.         else
    62.         {
    63.             currentDialog = currentDialog.NextDialog;
    64.             nameText.text = currentDialog.speakerName;
    65.             ShowNextButton();
    66.  
    67.             string sentence = currentDialog.dialogLine;
    68.             //Debug.Log(sentence);
    69.             StopAllCoroutines();
    70.             StartCoroutine(TypeSentence(sentence));
    71.             if (focusedCharacterFace != null)
    72.             {
    73.                 focusedCharacterFace.ChangeExpression(currentDialog.eyesExpression, currentDialog.mouthExpression);
    74.             }
    75.         }
    76.     }
    77.  
    And, for example, here's the StartDialog() "duplicate" for DialogChoices. There are similar duplicates for DisplayNextDialogLine() and StartDialog() for DialogChoice and DialogQuest.
    Code (CSharp):
    1.  
    2.     public void StartDialogChoice(DialogChoice d, ExpressionController face)
    3.     {
    4.         //Debug.Log("Starting conversation with " + dialog.speakerName);
    5.         animator.SetBool("IsOpen", true);
    6.  
    7.         DEBUGDialogMenuNametag.SetActive(true);
    8.  
    9.  
    10.         currentDialogChoice = d;
    11.         focusedCharacterFace = face;
    12.         nameText.text = currentDialogChoice.speakerName;
    13.  
    14.         string sentence = currentDialogChoice.dialogLine;
    15.  
    16.         StopAllCoroutines();
    17.         StartCoroutine(TypeSentence(sentence));
    18.         focusedCharacterFace.ChangeExpression(currentDialog.eyesExpression, currentDialog.mouthExpression);
    19.  
    20.  
    21.         ShowOptionsButtons();
    22.         Op1Button.GetComponentInChildren<TMP_Text>().text = currentDialogChoice.PlayerReaction1;
    23.  
    24.         Op2Button.GetComponentInChildren<TMP_Text>().text = currentDialogChoice.PlayerReaction2;
    25.     }
    Also, in order to begin each piece of Dialog, NPCs in-game have a DialogTrigger component attached to them. This is the method that's currently used to begin conversations:

    Code (CSharp):
    1. public void TriggerDialog(Dialog d, ExpressionController charFace)
    2.     {
    3.         //Add methods to OndialogOver to restore control after conversation
    4.         DialogManager.OnDialogOver += DialogOver;
    5.  
    6.         //Disable player movement
    7.         PlayerController.Instance.DisableAllMovement();
    8.  
    9.         //Begin dialog
    10.         if (d is DialogChoice)
    11.         {
    12.             DialogManager.Instance.StartDialogChoice((DialogChoice)d, charFace);
    13.         }
    14.         else
    15.         {
    16.             DialogManager.Instance.StartDialog(d, charFace);
    17.         }
    18.  
    19.         //Begin facial animations
    20.         //charFace.ChangeExpression(d.eyesExpression, d.mouthExpression);
    21.  
    22.         //Begin music changes
    23.         AudioManager.Instance.BGMFocusActivity(1.5f);
    24.     }
     
    Last edited: Jul 13, 2021
  7. kdgalla

    kdgalla

    Joined:
    Mar 15, 2013
    Posts:
    4,380
    Yeah, don't do that. Have each of these methods in it's own dialogue class instead.

    This is what @gorbit99 and @Bunny83 are trying to tell you. You don't want your manager object to do something different for each dialog object. That means every time that you add a new dialog object you'll break the dialog manager. You want to design the dialog manager so that it knows nothing about the different types of dialog objects. Have each type handle it's own behavior.
     
  8. JimothyMarmalade

    JimothyMarmalade

    Joined:
    Apr 6, 2019
    Posts:
    10
    After taking everyone's comments into consideration, I've redesigned my code.

    This is the new DialogManager class:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.UI;
    5. using TMPro;
    6.  
    7. public class DialogManager : MonoBehaviour
    8. {
    9.     public static DialogManager Instance { get; set; }
    10.  
    11.     public GameObject DEBUGDialogMenuNametag;
    12.  
    13.     public delegate void DialogEvent();
    14.     public static event DialogEvent OnDialogOver;
    15.  
    16.     [Header("Speaker/Text Data")]
    17.     public TMP_Text nameText;
    18.     public TMP_Text dialogText;
    19.  
    20.     [Header("Option Buttons")]
    21.     public GameObject NextButton;
    22.     public GameObject Op1Button;
    23.     public GameObject Op2Button;
    24.  
    25.     [Header("Animator Reference")]
    26.     public Animator animator;
    27.  
    28.     private Dialog nextDialog;
    29.     private Dialog Choice1;
    30.     private Dialog Choice2;
    31.     private ExpressionController focusedCharacterFace;
    32.  
    33.     private void Start()
    34.     {
    35.         //sentences = new Queue<string>();
    36.         ShowNextButton();
    37.     }
    38.  
    39.     public void DisplayDialogNormal(Dialog d)
    40.     {
    41.         animator.SetBool("IsOpen", true);
    42.         DEBUGDialogMenuNametag.SetActive(true);
    43.  
    44.         ShowNextButton();
    45.  
    46.  
    47.         nameText.text = d.speakerName;
    48.         if (focusedCharacterFace != null)
    49.         {
    50.             focusedCharacterFace.ChangeExpression(d.eyesExpression, d.mouthExpression);
    51.         }
    52.         StopAllCoroutines();
    53.         StartCoroutine(TypeSentence(d.dialogLine));
    54.     }
    55.  
    56.     public void DisplayDialogChoices(Dialog d, string playerChoice1, string playerChoice2)
    57.     {
    58.         animator.SetBool("IsOpen", true);
    59.         DEBUGDialogMenuNametag.SetActive(true);
    60.  
    61.         ShowOptionsButtons();
    62.  
    63.         nameText.text = d.speakerName;
    64.         Op1Button.GetComponentInChildren<TMP_Text>().text = playerChoice1;
    65.  
    66.         Op2Button.GetComponentInChildren<TMP_Text>().text = playerChoice2;
    67.  
    68.         StopAllCoroutines();
    69.         StartCoroutine(TypeSentence(d.dialogLine));
    70.     }
    71.  
    72.     public void SetNextDialog(Dialog d)
    73.     {
    74.         nextDialog = d;
    75.     }
    76.  
    77.     public void SetPlayerChoices(Dialog d1, Dialog d2)
    78.     {
    79.         Choice1 = d1;
    80.         Choice2 = d2;
    81.     }
    82.  
    83.     public void SetCharacterFace(ExpressionController exc)
    84.     {
    85.         focusedCharacterFace = exc;
    86.     }
    87.  
    88.     public void DisplayNextDialogLine()
    89.     {
    90.         if (nextDialog == null)
    91.         {
    92.             EndDialog();
    93.             return;
    94.         }
    95.         else
    96.         {
    97.             nextDialog.DisplayDialog();
    98.         }
    99.     }
    100.     public void MakePlayerDecision(bool choice)
    101.     {
    102.         if (choice)
    103.         {
    104.             Choice1.DisplayDialog();
    105.         }
    106.         //if choice is false, player selected the lower option (option 2)
    107.         else if (!choice)
    108.         {
    109.             Choice2.DisplayDialog();
    110.         }
    111.     }
    112. }
    And, for example, here is my Dialog base class and the new DialogNormal class:

    Code (CSharp):
    1. public class Dialog: ScriptableObject
    2. {
    3.     //Used as a casual Identifier for dialog triggers for programmers to keep track of what's what
    4.     public string dialogIdentifier = "";
    5.  
    6.     //Name of the person speaking in this dialog segment
    7.     public string speakerName = "";
    8.  
    9.     //Name of the eyes expression used when saying this dialogue
    10.     public ExpressionController.EyesExpression eyesExpression;
    11.  
    12.     //name of the mouth expression used when saying this dialogue
    13.     public ExpressionController.MouthExpression mouthExpression;
    14.  
    15.     //name of the body animation used when saying this dialogue
    16.     public string bodyAnimation = "";
    17.  
    18.     //The sentences that will be spoken
    19.     [TextArea(3, 10)]
    20.     public string dialogLine;
    21.  
    22.     public virtual void DisplayDialog()
    23.     {
    24.         //method should be inherited, scripting goes here.
    25.     }
    26. }
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. [CreateAssetMenu(fileName = "New DialogNormal", menuName = "Dialog/New DialogNormal")]
    6. public class DialogNormal : Dialog
    7. {
    8.     [Header("Next Dialog")]
    9.     public Dialog NextDialog;
    10.  
    11.  
    12.     public override void DisplayDialog()
    13.     {
    14.         DialogManager.Instance.DisplayDialogNormal(this);
    15.         DialogManager.Instance.SetNextDialog(NextDialog);
    16.     }
    17. }
    It's all working much better now. Dialog Choices and Quest Assignment is also functional.

    Thank you to @gorbit99, @Bunny83, and @kdgalla for your input.
     
    Bunny83 and kdgalla like this.