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

In this lesson we will see how our solution handles more complex dialogues and make some small final adjustments.

Displaying final dialogues

The test dialogues work fine, but how will it look if the dialogues get bigger and have some story logic added?

Let’s set up the final dialogues in Unity. All we need to do, is to select the NPC game objects and change the reference target in the Articy Reference component. Dialogue 1 for NPC 1 (1) and Dialogue 2 for NPC 2 (2). Then we can run a test to see what happens.

Setting new dialogue references

Select the Articy tab (3) and click on ‘Show global variables’ (4). In Play mode (5), we now can monitor how variable values change while playing.

Monitor Global Variables changes during Play mode

Monitor Global Variables changes during Play mode

In case you do not have the articy tab, you can activate it via Tools -> articy:draft importer -> Show database panel.

Activate articy database panel

Dialogue 1 begins with a reaction point with unseen and fallback scripting options. This automatically will pick a branch, depending on whether it is the first or subsequent time the NPC is addressed. If we talk to the NPC for the first time, we experience his entire dialogue, if we interact with him again, we only get a short reply.

unseen (6) is a bool that is true the first time we start the dialogue, meaning that this dialogue option will be displayed. The bottom branch has a fallback condition in its input pin (7). fallback evaluates to true if no other branch at the current branching point is valid. Which is not the case the first time we go through this dialogue, therefore the bottom branch is hidden at this point.

Check out the scripting article in the Help Center to learn more about how to use these scripts in more detail.

If we select the ‘Hunting’ branch when talking with Jogolf, isHunting (8) is set to true.

Scripting logic in Dialogue 1

When we interact with him a second time, the conversation now automatically follows the bottom branch. The first option is now hidden because unseen returns false, and the second option is now displayed, because the fallback condition now returns true, as this is the only valid path here.

Please accept marketing cookies to watch this video.

When interacting with Lady Ruby for the first time, her dialogue automatically follows the top path as well, as we also have an unseen (9) / fallback (10) branching point.

Later in the conversation, we encounter a Questions Hub object (11), leading to a player choice point. The Questions Hub leads to another Hub, called Inner loop (12), and to a bottom branch, which leads out of the question loop (13).

The Inner loop choices loop back to the Questions Hub via a Jump object (14), allowing the player to ask multiple questions. Here, we also make use of unseen for the top branch (15). This means that this option can only be selected once, after that it is hidden from the player. After the player receives water, they are no longer thirsty, so it does not make sense to display this option again.

The bottom branch leads out of the question loop (13). However, we would like the player to ask at least one question first, therefore we do not want to display this option to them right away. To achieve that, we use a query for the getSeenCounter of the Inner loop Hub, using its technical name. If it was visited at least once, the query evaluates to true and the bottom branch gets displayed as well, allowing the player to stop asking questions and continue the conversation.

Scripting logic in Dialogue 2

When we follow the bottom branch, we get to a Condition node (16), where we check for the IsHunting variable. Depending on which choice the player made in Dialogue 1 the path in Dialogue 2 will take a different route.

Condition node in Dialogue 2

Please accept marketing cookies to watch this video.

If you do not want to have to go through everything, when dealing with variables that are set in one and used in another dialogue, you can change their default value.

When displaying articy’s global variables, you can change their default values. If you change the value while in Play Mode, the change is only temporary, following the usual Unity behavior.

Monitor Global Variables changes during Play mode

Please accept marketing cookies to watch this video.

Besides changing the reference for which dialogue to start on, we haven’t made a single change since last lesson. The Flow Player takes care of all those new elements for us in the background, right out of the box.

articy:draft improves workflow

We have set up a workflow to properly display dialogue we imported from articy:draft X in the custom dialogue UI within our Unity project. Now writers and designers can make changes in articy and are able to check these changes in the engine quickly.

For example updating some text, adding another branch to a dialogue, or renaming an Entity. Do a fresh export from articy and you can test and verify the data within Unity.

Please accept marketing cookies to watch this video.

End of dialogue clarification

When we started to set up button functionality we implemented a check whether the dialogue ends or continues. This we accomplished by looking if the target of the current branch is going to be a Dialogue Fragment.

[DialogueManager.cs]

[...]

public void OnBranchesUpdated(IList<Branch> aBranches)
{
    [...]
    
    foreach (var branch in aBranches)
    {
        if (branch.Target is IDialogueFragment)
        {
            isDialogueFinished = false;
            break;   
        }
    }

    [...]
}

[...]

However, if we take a look at our final Dialogue 2, we have elements in there that are clearly not Dialogue Fragments, like two Hubs (17), a Jump (18), or a Condition (19) node. Still the dialogue does not end when getting to these points. Why not?

Dialogue structure in articy:draft

What was stated at the time: “We know that the dialogue is over when no Dialogue Fragment can be found as the following branch target.”

However, this target does not necessarily have to be the very next element, like we see it in the Flow. It depends on the On Pause settings we’ve done in the ArticyFlowPlayer component. We have only selected Dialogue Fragments for the Flow Player to pause on, that means in this dialogue only Dialogue Fragments will appear as valid stop targets.

Pause On settings

Simply put, the Flow Player looks ahead, to determine which branches are valid. This process is called forecasting.

Say we are at this position in the Flow of Dialogue 2, pausing on the Dialogue Fragment before the Hub (20). To determine which branches to display as options, the Flow Player continues to the next valid pause targets, while also evaluating any scripts on the way. Because of this, coming from this Dialogue Fragment (20), we would see branches (21), (22), and (23).

Flow Player forecasting

When selecting one of these options, we give a Play command to the Flow Player, which would traverse the respective branch, stopping on the next Dialogue Fragment. Then it would run through forecasting again to be able to display the next valid options. This continues until we arrive at the end of the Dialogue node, which is not connected to any other node, therefore making it impossible to find a Dialogue Fragment as a target, thus ending the dialogue.

Generally speaking, it makes sense to have the Flow Player stop only on elements that have data we need to fetch, or that we want to display.

That can vary greatly depending on your project. If you know where you need to pause, you can properly set up your checks. For example, if we would add Flow Fragments as an additional element to pause on, we would need to update our check for the end of a dialogue accordingly.

Resetting variable values

One small thing left that I would like to add to the code. Currently the values of variables from articy are not reset when we reset the scene with the ‘R’ key. However, we can fix this with one additional line of code in the RestartScene method in the PlayerController.cs script.

[PlayerController.cs]

using Articy.Adx_unitytutorial.GlobalVariables;

[...]

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

In case your IDE does not automatically add the necessary namespace, please add it manually from the code example above. This again is a namespace that is derived from the technical name of the project, so if your project is named differently, please adjust accordingly.

Final 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.Adx_unitytutorial.GlobalVariables;
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);
        ArticyGlobalVariables.Default.ResetVariables();
    }
}

Additional resources

This was just a short introduction into what you can do with articy:draft in combination with the Unity engine.

If you want to dive deeper into what is possible, check out our extensive documentation for the articy Importer for Unity.

For a more hands-on experience you can take a look at the Maniac Manfred demo project. It consists of projects for both articy and Unity and shows more advanced use cases for the Importer, including a simple localization solution.

If you have specific questions, you are welcome to join our community Discord server.

All the best and stay creative!

Useful links:

Scripting in articy:draft X
articy:draft X Importer for Unity documentation
Maniac Manfred demo project
Articy community Discord

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.