Hello and welcome to the articy:draft X Importer for Unity tutorial series, lesson 4.

In this lesson we will start updating our current dialogue solution to incorporate branching dialogue.

Branching Test Dialogue in articy:draft X

The Branching Test Dialogue is a simple dialogue, branching out to two and then to three player choices, which will help us evaluate the button layout in this and upcoming lessons.

Branching Test Dialogue

Branching dialogue in current solution

Let’s add Branching Test Dialogue as the target of the ArticyReference (1) of NPC 2 (2), then go to Play Mode to see how this dialogue plays out with our current solution.

Assigning Articy ref to NPC2

The dialogue plays, but we have no way of choosing any option. If we compare it to how the dialogue is set up in articy, we can see that the topmost option of a branching point gets selected automatically each time.

Please accept marketing cookies to watch this video.

Setting up branching functionality

To set up branching functionality, we first have to break some of what we have created so far. The current solution works fine for linear dialogue, but to be able to react to a changing number of dialogue options, we need to update our code.

In DialogueManager.cs we need to remove everything that has to do with dialogueButton and endDialogueButton. We also have no further use for the ContinueDialogue method, it needs to go, too.

[DialogueManager.cs]

[...]

[SerializeField]
private Button dialogueButton;
[SerializeField]
private Button endDialogueButton;

[...]

private void Start()
{
    flowPlayer = GetComponent<ArticyFlowPlayer>();
    dialogueButton.onClick.AddListener(ContinueDialogue);
    endDialogueButton.onClick.AddListener(EndDialogue);
}

public void StartDialogue(InteractPrompt aPrompt, ArticyObject aObject)
{            
    [...]

    dialogueButton.gameObject.SetActive(true);
}

 public void ContinueDialogue()
 {
     flowPlayer.Play();
 }

public void EndDialogue()
{
    [...]

    endDialogueButton.gameObject.SetActive(false);
    
    [...]    
}

[...]

public void OnBranchesUpdated(IList<Branch> aBranches)
{
    [...]
    
    if (isDialogueFinished)
    {           
        dialogueButton.gameObject.SetActive(false);
        endDialogueButton.gameObject.SetActive(true);
    }
}

Now, we declare two new private fields in DialogueManager.cs, which we both open to the editor via SerializeField: One RectTransform branchLayoutPanel and a GameObject branchPrefab.

[DialogueManager.cs]

[...]

[SerializeField]
private RectTransform branchLayoutPanel;
[SerializeField]
private GameObject branchPrefab;

[...]

In the Unity editor set the dialogueBranch (3) and closeDialogueBranch (4) canvas objects back to active (5).

Setting buttons to active

Now we prefab them. This is a crucial step, because we are going to base our instantiation of new buttons on these prefabs.

Click and drag the dialogueBranch and the closeDialogueBranch objects from the scene hierarchy to the Prefabs folder. If they appear in blue in the hierarchy afterwards, then everything is fine and we can continue.

Please accept marketing cookies to watch this video.

Select the DialogueManager game object (6) and drag the dialogueBranch prefab from the Prefabs folder to assign the Branch Prefab reference. Again, very important that we use the actual prefab here!

Using prefab to assign reference

The Branch Layout Panel reference field is assigned the Branches game object. We briefly touched upon this object in the last video when looking at the Dialogue Widget: It is responsible for the button layout and positioning, so that the buttons we are going to instantiate know where they have to appear.

Assigning Branch Layout Panel reference

For your project, you do not have to design your UI in the same way as in this demo project. You can display your dialogue options below or next to each other, or even in a more creative way. For the approach we are aiming for in this series, it is just important that we have two button prefabs – for a Continue/Content and for an End button – and have a layout component, to position the instantiated buttons in the desired way.

It is time for some coding in the DialogueManager.cs script. In OnBranchesUpdated we change the second if-statement to now check if the dialogue is not finished, by just adding an exclamation mark in front of the variable name.

Inside this if-clause we create another foreach loop to go through all branch objects of the aBranches list, similar to the first foreach loop in this method.

With this loop we are going to create a number of buttons matching the amount of available branch options.

We declare a variable btn of type GameObject and assign the return value of Instantiate with branchPrefab to it. With the Instantiate method we can also set the parent with a handy overload, which here is going to be branchLayoutPanel.

[DialogueManager.cs]

[...]

public void OnBranchesUpdated(IList<Branch> aBranches)
{
    [...]

    if (!isDialogueFinished)
    {
        foreach (var branch in aBranches)
        {
            GameObject btn = Instantiate(branchPrefab, branchLayoutPanel);
        }
    }
}

There are a few small steps left before we test whether the button creation works. We create a new private void method and call it ClearAllBranches.

In ClearAllBranches we go through all child objects of branchLayoutPanel with a foreach loop and destroy them.

We call ClearAllBranches inside OnBranchesUpdated, at the very top, so that we remove old branch buttons before we instantiate new ones.

[DialogueManager.cs]

[...]

public void OnBranchesUpdated(IList<Branch> aBranches)
{
    ClearAllBranches();

    [...]
}

private void ClearAllBranches()
{
    foreach (Transform child in branchLayoutPanel)
    {
        Destroy(child.gameObject);  
    }
}

Now it is time for a quick test. The buttons are again without any functionality at the moment. However the important thing right now is how many buttons we see when interacting with our NPCs.

NPC 1 with the linear dialogue has one button in the dialogue UI.

Dialogue in articy and Unity - Linear

NPC 2, with the branching test dialogue, which offers the player two choices right at the start, we have two buttons properly instantiated and positioned in the dialogue UI. We are getting there!

Dialogue in articy and Unity - Branching

Adding button text

In our Scripts folder, we create a new C# script with the name BranchChoice (7) and attach it to the dialogueBranch prefab (8) as a component (9).

Create and attach new script

Then we open the BranchChoice script in our IDE. Start and Update can be removed, we do not need them for this class. We will need a couple of additional namespaces. If your IDE adds them automatically when needed, you can skip this, but it also does not hurt to add them manually. We are going to need Articy.Unity, Articy.Unity.Interfaces, and TMPro.

We create two methods. The first is public void AssignBranch, a method that will be called each time a button is created to represent a single branch in the flow. The second method’s name is going to be public void OnBranchSelected. With this method we will control what happens when a button is clicked.

Furthermore, we need some fields, which can all be private: one of type Branch, called branch, one of type ArticyFlowPlayer, called flowPlayer, and lastly one of type TMP_Text, called buttonText, which we open to the editor via the SerializeField attribute.

[BranchChoice.cs]

using Articy.Unity;
using Articy.Unity.Interfaces;
using TMPro;
using UnityEngine;

public class BranchChoice : MonoBehaviour
{
    [SerializeField]
    private TMP_Text buttonText;

    private Branch branch;
    private ArticyFlowPlayer flowPlayer;
    
    public void AssignBranch()
    {

    }

    public void OnBranchSelected()
    {

    }

    private void Start()
    {
        
    }

    private void Update()
    {
        
    }
}

Open the dialogueBranch prefab (10) in the Unity editor and select the Text child object of dialogueBranch as the target for the buttonText reference we just created. Then we can get back to the BranchChoice script.

Assigning buttonText reference

We add two parameters to AssignBranch: ArticyFlowPlayer aFlowPlayer and Branch aBranch. Inside the method we assign branch the value of aBranch and flowPlayer the value of aFlowPlayer. Then we assign buttonText.text the value of an empty string.

[BranchChoice.cs]

[...]

public void AssignBranch(ArticyFlowPlayer aFlowPlayer, Branch aBranch)
{
    branch = aBranch;
    flowPlayer = aFlowPlayer;
    buttonText.text = string.Empty;
}

[...]

If we look at the branching test dialogue in articy, we see that for situations where the player can choose between different options, a so-called player choice, the menu text field was used in addition to the dialogue line.

Usage of menu text in articy

This menu text is supposed to be displayed as the button text in Unity. You could of course display the entire dialogue line, but usually games go for a different text. For one, the whole dialogue line might easily be too long for a button and lead to truncation or overlap, and secondly already displaying the line here and then again after selecting feels kind of redundant.

In the end it is a design decision, but for this project we are going with menu text for player choice situations and for linear ‘click to continue’ conversations, we are going to display an arrow or similar instead of the text.

That means we are going to check for menu text in the AssignBranch method. First, we declare a variable var objectWithMenuText and initialize it to aBranch.Target cast to IObjectWithLocalizableMenuText. Next, we check whether a menu text property exists by null-checking objectWithMenuText. If it isn’t null, we assign buttonText.text the value of objectWithMenuText.MenuText.

[BranchChoice.cs]

[...]

public void AssignBranch(ArticyFlowPlayer aFlowPlayer, Branch aBranch)
{
    [...]

    var objectWithMenuText = aBranch.Target as IObjectWithLocalizableMenuText;
    if (objectWithMenuText != null)
    {
        buttonText.text = objectWithMenuText.MenuText;
    }
}

[...]

If there is menu text, we display it as button text. Now, we need to cover the case if there is no menu text, usually when following a linear sequence of dialogue.

For that we start with another if-statement checking if the buttonText.text string currently is empty or null. If this is the case, we simply assign buttonText.text the value of the string we want to display, something like ‘Continue’ or some kind of arrow symbol.

[BranchChoice.cs]

[...]

public void AssignBranch(ArticyFlowPlayer aFlowPlayer, Branch aBranch)
{
    [...]
    
    if (string.IsNullOrEmpty(buttonText.text))
    {
        buttonText.text = ">>>";
    } 
}

[...]

What we still need to do, is to define what is supposed to happen on a button click.

It is similar to our solution we had in place for the linear dialogue. There we just told the Flow Player to get going again. This we are going to do here as well, in the OnBranchSelected method, only with one added detail.

For the linear path, we could just use flowPlayer.Play, without any additional argument. Without an argument the Flow Player always chooses the first branch, which for a linear flow is absolutely fine. However, now we can have one or multiple paths to follow, so we need to be specific, therefore we pass the argument branch.

[BranchChoice.cs]

[...]

public void OnBranchSelected()
{
    flowPlayer.Play(branch);
}

Just two more small steps, then we are ready for a test. First, we switch over to the DialogueManager.cs script.

Here we already create the buttons for all current branches, what we still need to do is provide them with the functionality, we just added in the BranchChoice.cs script.

Go to the OnBranchesUpdated method. We add one line of code to the second foreach loop: We get the component BranchChoice for the btn object and call the AssignBranch method on it. As arguments, we pass the flowPlayer and the current branch.

[DialogueManager.cs]

[...]

public void OnBranchesUpdated(IList<Branch> aBranches)
{
    [...]

    if (!isDialogueFinished)
    {
        foreach (var branch in aBranches)
        {
            GameObject btn = Instantiate(branchPrefab, branchLayoutPanel);
            btn.GetComponent<BranchChoice>().AssignBranch(flowPlayer, branch);
        }
    }
}

[...]

Step two happens in the Unity editor. Open the dialogueBranch prefab (11). Here, we go to On Click (12) and select dialogueBranch as the referenced object (13). In the function drop down select BranchChoice and then choose OnBranchSelected (14). Now we have enough functionality up and running that we can do another test.

Set OnClick functionality

Let’s see what the red NPC with the branching dialogue has to say.

Please accept marketing cookies to watch this video.

Buttons properly display menu text for choices and lead to the correct path when clicked, and to move the dialogue along, we see the continue symbol we opted for.

Missing menu text

Just in case, you encounter the situation pictured below, where you cannot get your menu text to display, although the code is absolutely correct, you might miss a simple checkmark in articy:draft.

Missing menu text localization property

Open the Settings from the Navigator (15) and select the Localization tab (16).

Make sure that the MenuText property is checked for localization (17). It is an easy one to miss. Do a fresh export after setting the checkmark and everything should be fine now.

Set menu text property for localization

Alternative approach: Display dialogue line

Say you want to display the entire dialogue line and not just an arrow symbol.

Displaying entire dialogue line as button text

This would mean some small changes in the AssignBranch method in the BranchChoice.cs script. Instead of just assigning a string value to buttonText.text, we would declare a variable var objectWithText and initialize it with aBranch.Target cast to IObjectWithLocalizableText. Next comes our usual safety check to make sure the object is not null. If it is not null, so if there is any text property, we assign buttonText.text the value of objectwithText.Text.

[BranchChoice.cs]

[...]

public void AssignBranch(ArticyFlowPlayer aFlowPlayer, Branch aBranch)
{
    branch = aBranch;
    flowPlayer = aFlowPlayer;
    buttonText.text = string.Empty;

    var objectWithMenuText = aBranch.Target as IObjectWithLocalizableMenuText;
    if (objectWithMenuText != null)
    {
        buttonText.text = objectWithMenuText.MenuText;
    }

    if (string.IsNullOrEmpty(buttonText.text))
    {
        buttonText.text = ">>>";

        var objectWithText = aBranch.Target as IObjectWithLocalizableText;
        if (objectWithText != null)
        {
            buttonText.text = objectWithText.Text;
        }
    }
}

[...]

I will however keep our initial solution.

End dialogue button

The only thing that is still missing is the button to conclude the dialogue and that we will take care of right away.

In DialogueManager.cs we add a private field GameObject closePrefab and open it to the editor via SerializeField.

[DialogueManager.cs]

[...]

[SerializeField]
private GameObject closePrefab;

[...]

In the Unity editor, select the DialogueManager game object (18) and assign the closeDialogueBranch prefab to the ClosePrefab reference. Again, it is important that the prefab is used, it will not work with an instantiated game object from the prefab.

Assigning prefab to reference

Back to DialogueManager.cs and the OnBranchesUpdated method. Here, we are basically doing the same thing as with the navigational button, only with a different listener.

We add an else clause to the if-statement checking if the dialogue is not yet finished. Now we cover the case for the dialogue actually being over.

We declare a GameObject btn and initialize it with the return value of the Instantiate method with closePrefab and branchLayoutPanel as arguments. This creates the button, now we give it a listener to add functionality. We declare a variable Button btnComp and initialize it to getting the Button component of btn. Then we add a listener to a click for btnComp and call EndDialogue.

[DialogueManager.cs]

[...]

public void OnBranchesUpdated(IList<Branch> aBranches)
{
    [...]

    if (!isDialogueFinished)
    {
        foreach (var branch in aBranches)
        {
            GameObject btn = Instantiate(branchPrefab, branchLayoutPanel);
            btn.GetComponent<BranchChoice>().AssignBranch(flowPlayer, branch);
        }
    }
    else
    {
        GameObject btn = Instantiate(closePrefab, branchLayoutPanel);
        Button btnComp = btn.GetComponent<Button>();
        btnComp.onClick.AddListener(EndDialogue);        
    }
}

[...]

Time for another test. Both linear and branching test dialogues behave as expected. We can traverse through the dialogues, make choices where applicable, and conclude the dialogue by closing the dialogue UI.

Please accept marketing cookies to watch this video.

Next lesson

We have almost arrived at the end of this tutorial series. In the final lesson, we will take a look what is needed to handle more complex dialogues, which use some scripting logic in the form of global variables.

Current state of C# scripts

BranchChoice.cs

using Articy.Unity;
using Articy.Unity.Interfaces;
using TMPro;
using UnityEngine;

public class BranchChoice : MonoBehaviour
{
    [SerializeField]
    private TMP_Text buttonText;

    private Branch branch;
    private ArticyFlowPlayer flowPlayer;

    public void AssignBranch(ArticyFlowPlayer aFlowPlayer, Branch aBranch)
    {
        branch = aBranch;
        flowPlayer = aFlowPlayer;
        buttonText.text = string.Empty;

        var objectWithMenuText = aBranch.Target as IObjectWithLocalizableMenuText;
        if (objectWithMenuText != null)
        {
            buttonText.text = objectWithMenuText.MenuText;
        }

        if (string.IsNullOrEmpty(buttonText.text))
        {
            buttonText.text = “>>>”;
        }
    }

    public void OnBranchSelected()
    {
        flowPlayer.Play(branch);
    }
}
DialogueManager.cs

using Articy.Adx_unitytutorial;
using Articy.Unity;
using Articy.Unity.Interfaces;
using System.Collections.Generic;
using TMPro;
using Unity.Cinemachine;
using UnityEngine;
using UnityEngine.UI;

public class DialogueManager : MonoBehaviour, IArticyFlowPlayerCallbacks
{
    [Header(“UI”)]
    // Reference to Dialogue UI
    [SerializeField]
    private GameObject dialogueWidget;
    // Reference to Dialogue text
    [SerializeField]
    private TMP_Text dialogueText;
    // Reference to speaker
    [SerializeField]
    private TMP_Text dialogueSpeaker;
    [SerializeField]
    private RectTransform branchLayoutPanel;
    [SerializeField]
    private GameObject branchPrefab;
    [SerializeField]
    private GameObject closePrefab;

    [Header(“Cameras”)]
    [SerializeField]
    private CinemachineCamera movementCam;
    [SerializeField]
    private CinemachineCamera dialogueCam;

    private InteractPrompt prompt;
    private ArticyFlowPlayer flowPlayer;

    // To check if we are currently showing the Dialogue UI interface
    public bool DialogueActive { get; set; }

    private void Awake()
    {
        //Default camera setting
        movementCam.enabled = true;
        dialogueCam.enabled = false;
    }

    private void Start()
    {
        flowPlayer = GetComponent<ArticyFlowPlayer>();
    }

    public void StartDialogue(InteractPrompt aPrompt, ArticyObject aObject)
    {
        DialogueActive = true;
        dialogueWidget.SetActive(DialogueActive);

        movementCam.enabled = false;
        dialogueCam.enabled = true;

        prompt = aPrompt;
        prompt.ShowInteractPrompt(false);

        flowPlayer.StartOn = aObject;
    }

    public void EndDialogue()
    {
        DialogueActive = false;
        dialogueWidget.SetActive(DialogueActive);

        movementCam.enabled = true;
        dialogueCam.enabled = false;

        flowPlayer.FinishCurrentPausedObject();

        if (prompt != null)
        {
            prompt.ShowInteractPrompt(true);
        }
    }

    public void OnFlowPlayerPaused(IFlowObject aObject)
    {
        dialogueText.text = string.Empty;
        dialogueSpeaker.text = string.Empty;   

        var objectWithText = aObject as IObjectWithLocalizableText;
        if (objectWithText != null)
        {
            dialogueText.text = objectWithText.Text;
        }

        var objectWithSpeaker = aObject as IObjectWithSpeaker;
        if (objectWithSpeaker != null)
        {
            var speakerEntity = objectWithSpeaker.Speaker as Entity;
            if (speakerEntity != null)
            {
                dialogueSpeaker.text = speakerEntity.DisplayName;
            }
        }
    }

    public void OnBranchesUpdated(IList<Branch> aBranches)
    {
        ClearAllBranches();
        bool isDialogueFinished = true;

        foreach (var branch in aBranches)
        {
            if (branch.Target is IDialogueFragment)
            {
                isDialogueFinished = false;
                break;
            }
        }

        if (!isDialogueFinished)
        {
            foreach (var branch in aBranches)
            {
                GameObject btn = Instantiate(branchPrefab, branchLayoutPanel);
                btn.GetComponent<BranchChoice>().AssignBranch(flowPlayer, branch);
            }
        }
        else
        {
            GameObject btn = Instantiate(closePrefab, branchLayoutPanel);
            Button btnComp = btn.GetComponent<Button>();
            btnComp.onClick.AddListener(EndDialogue);
        }
    }

    private void ClearAllBranches()
    {
        foreach (Transform child in branchLayoutPanel)
        {
            Destroy(child.gameObject);
        }
    }
}
PlayerController.cs

using Articy.Unity;
using UnityEngine;
using UnityEngine.SceneManagement;

public class PlayerController : MonoBehaviour
{
    [SerializeField]
    private int speed;
    [SerializeField]
    private Animator animator;
    [SerializeField]
    private SpriteRenderer playerSprite;

    private PlayerControls playerControls;
    private Rigidbody rb;
    private DialogueManager dialogueManager;

    private Vector3 movement;
    private InteractPrompt interactPrompt;
    private ArticyRef availableDialogue;    

    //Animation Control
    private const string IS_WALKING_PARAM = “IsWalking”;
    private const string IS_DIALOGUE_PARAM = “IsDialogue”;

    //Tags
    private const string IS_NPC_TAG = “NPC”;

    private void Awake()
    {
        playerControls = new PlayerControls();
    }

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

    private void Start()
    {
        rb = GetComponent<Rigidbody>();
        dialogueManager = FindFirstObjectByType<DialogueManager>();
    }

    private void Update()
    {
        GetPlayerPosition();
        AnimationControl();
        PlayerInteractions();
    }

    private void FixedUpdate()
    {
        MovePlayer();
    }

    private void GetPlayerPosition()
    {
        float x = playerControls.Player.Move.ReadValue<Vector2>().x;
        float z = playerControls.Player.Move.ReadValue<Vector2>().y;

        movement = new Vector3(x, 0, z).normalized;

        SpriteDirection(x);
    }

    private void MovePlayer()
    {
        // Remove movement control from player while in dialogue
        if (dialogueManager.DialogueActive)
            return;

        rb.MovePosition(transform.position + movement * speed * Time.fixedDeltaTime);
    }

    private void PlayerInteractions()
    {
        if (playerControls.Player.Interact.WasPerformedThisFrame() && availableDialogue?.GetObject() != null)
        {
            dialogueManager.StartDialogue(interactPrompt, availableDialogue.GetObject() );
        }

        // Debug functionality to easy get out of a dialogue and restart the scene
        if (playerControls.Player.Quit.WasPerformedThisFrame() && dialogueManager.DialogueActive)
        {
            dialogueManager.EndDialogue();
        }

        if (playerControls.Player.Restart.WasPerformedThisFrame())
        {
            RestartScene();
        }
    }

    private void AnimationControl()
    {
        //Idle to Walking
        animator.SetBool(IS_WALKING_PARAM, movement != Vector3.zero);
        //Idle to still when in dialogue
        animator.SetBool(IS_DIALOGUE_PARAM, dialogueManager.DialogueActive);
    }

    private void SpriteDirection(float x)
    {
        if (x != 0 && x < 0)
        {
            playerSprite.flipX = true;
        }

        if (x != 0 && x > 0)
        {
            playerSprite.flipX = false;
        }

    }

    private void OnTriggerEnter(Collider aOther)
    {
        if (aOther.CompareTag(IS_NPC_TAG))
        {
            var articyReferenceComp = aOther.GetComponent<ArticyReference>();
            if (articyReferenceComp != null)
            {
                availableDialogue = articyReferenceComp.reference;
            }

            interactPrompt = aOther.GetComponent<InteractPrompt>();
            if (interactPrompt != null && availableDialogue?.GetObject() != null)
            {
                interactPrompt.ShowInteractPrompt(true);
            }
        }
    }

    private void OnTriggerExit(Collider aOther)
    {
        if (aOther.CompareTag(IS_NPC_TAG))
        {
            if (aOther.GetComponent<InteractPrompt>() != null)
            {
                interactPrompt.ShowInteractPrompt(false);
            }
        }
    }

    private void RestartScene()
    {
        playerControls.Player.Disable();
        SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }
}

GO TO LESSON 5

Don’t have articy:draft X yet? Get the free version now!
Get articy:draft X FREE
*No Payment information required

Follow us on Twitter, Facebook and LinkedIn to keep yourself up to date and informed. To exchange ideas and interact with other articy:draft users, join our community on discord.