Tactics RPG Conversations

This week we will implement the UI components from the previous lesson in a Conversation UI element which would appear as part of a cut-scene before and/or after battles. The panels will hold a little bit of text along with a sprite showing who is speaking. A bouncing arrow will indicate when there is more to read and using the input “Fire” event will cause the conversation to move on to the next message or the next character who will speak, etc. These panels can appear in any corner of the screen (which could indicate the direction of a speaking character relative to the player), and will animate in and out of the screen as needed.

Speaker Data

One of the first things we will need is a data model. Create a new class named SpeakerData in the Scripts/Model folder. I didn’t specify a class to inherit from, and I marked the class Serializable so that we can see and configure it using the Editor’s inspector. Model classes can often be quite simple – like this one. I have merely declared a few fields:

  • A list of string called messages will contain all of the “pages” of text that any one character will speak.
  • A reference to a sprite will be used to show who is speaking.
  • A TextAnchor enum is used to determine which corner of the screen the panel will display in.
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

[System.Serializable]
public class SpeakerData 
{
	public List<string> messages;
	public Sprite speaker;
	public TextAnchor anchor;
}

Conversation Data

Since this lesson isn’t about creating a monologue, let’s create another script to hold a sequence (list) of Speaker Data instances. This way, we can have multiple different people speaking and interacting with each other. Create a script named ConversationData and place it in the Scripts/Model folder.

This time our class will inherit from something. Even though you can see Serializeable classes in the editor, you can only see them when they are attached to an asset. It just so happens that you can make project assets out of ScriptableObject so we will specify it here.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class ConversationData : ScriptableObject 
{
	public List<SpeakerData> list;
}

How to Create Conversation Assets

Prototype Approach

Initially, you are unlikely to have a full grasp of all of the features your project will need. You may want to just create a few simple assets to test with and see if you like your architecture decisions. In this sort of scenario, it would be really handy to just be able to create an instance of a scriptable object using a menu action, just like when you want to create any other kind of GameObject.

The Unity Community has provided a handy little script for that called ScriptableObjectUtility, which you should download and add to the project. Make sure to stick it in an Editor folder, because it uses the UnityEditor namespace.

This script can’t do anything by itself- we must add another script which actually triggers it and tells it what kind of asset we want to make. So, create another script called AssetCreator and place it alongside the utility script in the Editor folder.

using UnityEngine;
using UnityEditor;

public class YourClassAsset
{
	[MenuItem("Assets/Create/Conversation Data")]
	public static void CreateConversationData ()
	{
		ScriptableObjectUtility.CreateAsset<ConversationData> ();
	}
}

Now that we have these two scripts, we can create a ConversationData asset by using the file menu (Assets->Create->Conversation Data). Go ahead and create an instance of our Conversation Data and place it in the Resources/Conversations folder (Create the Conversations subfolder). Name the asset IntroScene and give it some sort of implementation:

Note that in this example, I duplicated the Avatar sprite which already existed in the project and then recolored it and saved it as EvilAvatar. This way I could test out the dynamic speaker image feature of my panel.

Production Approach

In a fully realized project, I would actually want to be creating my conversation data in some external form – this could be anything you want, but ideally it should allow you to easily see and edit all of your text, have a spell-checker, etc. The inspector in Unity doesn’t have these sorts of features. Imagine how tedious it could be to try to track down a spelling mistake if you had hundreds of Conversation assets, each having multiple speakers with multiple pages. Or suppose you changed a commonly used character or location name and needed to propogate the change across the entire game’s script. If all of the text existed in an external source, like a .csv for example, then it would be as easy as running a Find and Replace command.

If you are interested in the full production-level approach, I have written a separate post called Bestiary Management and Scriptable Objects which should be able to help you get a good head start on the process.

Conversation Panel

Now that we have a data model to store our conversation, we need a view to display it to a user. Add a new script named ConversationPanel to the Scripts/View Model Component folder. The implementation follows:

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class ConversationPanel : MonoBehaviour 
{
	public Text message;
	public Image speaker;
	public GameObject arrow;
	public Panel panel;

	void Start ()
	{
		Vector3 pos = arrow.transform.localPosition;
		arrow.transform.localPosition = new Vector3(pos.x, pos.y + 5, pos.z);
		Tweener t = arrow.transform.MoveToLocal(new Vector3(pos.x, pos.y - 5, pos.z), 0.5f, EasingEquations.EaseInQuad);
		t.easingControl.loopType = EasingControl.LoopType.PingPong;
		t.easingControl.loopCount = -1;
	}

	public IEnumerator Display (SpeakerData sd)
	{
		speaker.sprite = sd.speaker;
		speaker.SetNativeSize();

		for (int i = 0; i < sd.messages.Count; ++i)
		{
			message.text = sd.messages[i];
			arrow.SetActive( i + 1 < sd.messages.Count );
			yield return null;
		}
	}
}

This is also a pretty simple script. We need a reference to the Text component so we can update it with dynamic messages from our conversation. We need a reference to the speaker so we can show who is speaking. We will need a reference to the arrow so we can turn it on or off based on whether or not more “pages” of text exist in the current speaker’s dialog. Finally we need a reference to the Panel component so we can make the view tween into or out of the screen.

In the Start method we get the current local position of the arrow. Using that as a base point, we move it up by a fixed amount and then tell it to tween down just as far – this way the animation is centered around the original point. After completing the tween the animation will play in reverse so that you see it animate back up to where it started (due to the PingPong setting). I set the tween to loop infinitely by setting the loopCount to -1. This little bit of animation should help indicate to the user that there is more to read.

I created an IEnumerator method called Display. This will not be the target of a MonoBehaviour’s StartCoroutine. Instead, we will manually move through the method based on “Fire” input events. This process may be new for many of you, but it is fairly simple to use, and can be a very powerful feature.

Conversation Controller

The real meat of this lesson is found in the Conversation Controller. Its job is to handle the process of a conversation including making sure that an appropriate panel is used and positioned correctly, tweening the panel into view, showing the speaker and the speaker’s messages one at a time based on user input, tweening the panel out when the speaker is finished speaking, and then tweening in another panel (if necessary) for a new speaker, etc. The complex part of this is deciding how to maintain state between all of the different speakers and pages of a speaker’s dialogue.

Begin by creating a script named ConversationController in the Scripts/Controller folder.

using UnityEngine;
using System;
using System.Collections;

public class ConversationController : MonoBehaviour 
{
	
}

I usually begin something complex like this by defining my fields and properties. I like to know what is going to be needed first and then implement the details later. First, I have decided to create two separate Conversation Panels – one for display on the left side of the screen, and one for display on the right side of the screen. It would have been possible to use a single panel, and simply modify assets as necessary to show it on the different sides, but this is simpler.

[SerializeField] ConversationPanel leftPanel;
[SerializeField] ConversationPanel rightPanel;

I will also want the ability to turn on and off the canvas based on whether or not a conversation is actually taking place. Disabling the canvas when it is not used should allow the rendering speed to run at peak performance.

Canvas canvas;

I will maintain a reference to an IEnumerator which steps through all of the speakers and their messages in a conversation. This will make it easy to maintain state without adding a bunch of other properties.

IEnumerator conversation;

Finally I will maintain a reference to a Tweener, which is used to animate the current panel into or out of the screen. I maintain this reference, because while the transitions are active, I dont want the user to be able to advance the conversations position.

Tweener transition;

If you remember from the previous lesson, you are able to specify panel positions using string names. These names will be set in the inspector, but in code, it generally isn’t a good idea to use strings (due to typo errors etc). One common practice is to define constants at the top of your script so that you know you will be using the same text anywhere the constant itself is used. This practice can help to alleviate potentially confusing bugs.

const string ShowTop = "Show Top";
const string ShowBottom = "Show Bottom";
const string HideTop = "Hide Top";
const string HideBottom = "Hide Bottom";

In the start method, I will connect some references, set the default off screen position for both panels, and then disable the canvas (until it is needed).

void Start ()
{
	canvas = GetComponentInChildren<Canvas>();
	if (leftPanel.panel.CurrentPosition == null)
		leftPanel.panel.SetPosition(HideBottom, false);
	if (rightPanel.panel.CurrentPosition == null)
		rightPanel.panel.SetPosition(HideBottom, false);
	canvas.gameObject.SetActive(false);
}

The public interface for this script will have two main options. Initially you will tell it to Show and will pass along an instance of ConversationData as a parameter. This method will set everything up and bring out the first panel and display the initial message of the first speaker. From then on, new messages and speakers must be manually triggered using a Next method call. This process will continue until the final message has been dismissed. When the panel has completed its animation offscreen an event will be posted.

I could have allowed the conversation panel itself to receive input and manage the entire sequence itself, but in the end I decided it would make the most sense to keep all of the input routed through a single source – the game state, which displays the conversation in the first place.

Add the following event for when the conversation controller has finished:

public static event EventHandler completeEvent;

Here are the public methods mentioned above:

public void Show (ConversationData data)
{
	canvas.gameObject.SetActive(true);
	conversation = Sequence (data);
	conversation.MoveNext();
}

public void Next ()
{
	if (conversation == null || transition != null)
		return;
	
	conversation.MoveNext();
}

The Sequence call is to a method with an IEnumerator return type. This is a bit of a lengthy method, but essentially, it loops over all of the speakers in a conversation, and in a nested loop, iterates over each of the speaker’s messages via another IEnumerator from the current panel.

IEnumerator Sequence (ConversationData data)
{
	for (int i = 0; i < data.list.Count; ++i)
	{
		SpeakerData sd = data.list[i];

		ConversationPanel currentPanel = (sd.anchor == TextAnchor.UpperLeft || sd.anchor == TextAnchor.MiddleLeft || sd.anchor == TextAnchor.LowerLeft) ? leftPanel : rightPanel;
		IEnumerator presenter = currentPanel.Display(sd);
		presenter.MoveNext();

		string show, hide;
		if (sd.anchor == TextAnchor.UpperLeft || sd.anchor == TextAnchor.UpperCenter || sd.anchor == TextAnchor.UpperRight)
		{
			show = ShowTop;
			hide = HideTop;
		}
		else
		{
			show = ShowBottom;
			hide = HideBottom;
		}

		currentPanel.panel.SetPosition(hide, false);
		MovePanel(currentPanel, show);

		yield return null;
		while (presenter.MoveNext())
			yield return null;

		MovePanel(currentPanel, hide);
		transition.easingControl.completedEvent += delegate(object sender, EventArgs e) {
			conversation.MoveNext();
		};

		yield return null;
	}

	canvas.gameObject.SetActive(false);
	if (completeEvent != null)
		completeEvent(this, EventArgs.Empty);
}

For each speaker, I determine what entry and exit point to use for the panel based on the SpeakerData’s anchor setting. I store those in a temporary local variable so that later I can just tell the panel to show or hide.

Before I animate a panel on screen, I first snap it offscreen (no animation). This way if it had previously been in a different vertical location, you wont see it tween on the Y axis (it would enter along a diagonal animation path).

Once I have animated a panel onto the screen, I create a pause in the sequence using a yield statement. All of the state will be preserved at this spot until I tell it to continue moving via the MoveNext function (called by the public Next method I exposed earlier).

As the user provides input, I will loop through the messages of the speaker in a while loop. When the presenter has no further yield statements the while loop will terminate. At this point I will move the current panel off screen. I use an anonymous delegate to automatically continue the conversation once it completes. The yield statement immediately after it is the one it will skip.

Once all of the speakers have completed their dialogue, the canvas is disabled, and the event is fired.

The MovePanel method is below:

void MovePanel (ConversationPanel obj, string pos)
{
	transition = obj.panel.SetPosition(pos, true);
	transition.easingControl.duration = 0.5f;
	transition.easingControl.equation = EasingEquations.EaseOutQuad;
}

Cut Scene State

In order to see our conversation as a part of the game, we will add another game state called CutSceneState in the Scripts/Controller/Battle States folder. This script loads the sample conversation asset we created earlier and displays it. When the animation is complete it moves on to the next state of the game. Of course, a more complete implementation would not hard-code the conversation to load. There would probably be additional data, perhaps in some sort of Mission data model which contained all sorts of information, like what conversations to play, what enemies to load, what rewards you get for winning, etc.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class CutSceneState : BattleState 
{
	ConversationController conversationController;
	ConversationData data;

	protected override void Awake ()
	{
		base.Awake ();
		conversationController = owner.GetComponentInChildren<ConversationController>();
		data = Resources.Load<ConversationData>("Conversations/IntroScene");
	}

	protected override void OnDestroy ()
	{
		base.OnDestroy ();
		if (data)
			Resources.UnloadAsset(data);
	}

	public override void Enter ()
	{
		base.Enter ();
		conversationController.Show(data);
	}

	protected override void AddListeners ()
	{
		base.AddListeners ();
		ConversationController.completeEvent += OnCompleteConversation;
	}

	protected override void RemoveListeners ()
	{
		base.RemoveListeners ();
		ConversationController.completeEvent -= OnCompleteConversation;
	}

	protected override void OnFire (object sender, InfoEventArgs<int> e)
	{
		base.OnFire (sender, e);
		conversationController.Next();
	}

	void OnCompleteConversation (object sender, System.EventArgs e)
	{
		owner.ChangeState<SelectUnitState>();
	}
}

Init Battle State

To finish plugging in our Cut Scene, we need to make it the target of the InitBattleState. Change the last line of the Init method to the following:

owner.ChangeState<CutSceneState>();

Scene Setup

Open the game’s Battle scene. Create an empty GameObject named Conversation Controller. Make this object a child of Battle Controller. This object will be made into a special prefab to hold and manage our conversation panels. An example of the final result appears below:

This prefab has several gameobjects arranged in a particular hierarchy as shown in the image below:

Add our ConversationController script to the root Conversation Controller object and make sure you dont forget to set its dependencies when you finish creating them in a bit:

Create and add a Canvas object as indicated by the hierachy example above. Note that I customized the Canvas Scaler component so that my UI elements can scale with different screen sizes. Even with different sizes and aspect ratios, our setup will always work as expected.

Create an empty GameObject named Right Edge Conversation Panel. Add the Panel component to this object and it will automatically make the Transform a RectTransform and add the Layout Anchor for us. Make this object a child of the Canvas. Next, add the ConversationPanel script. Make sure you dont forget to set its dependencies when you finish creating them. The image below, as well as all of the next series of images show the parallel settings (one for the left panel and one for the right panel). I would recommend creating just the right side first, and then duplicating it and then just make the few changes necessary.

Create an image object for the Background:

Create another image object for the Speaker:

Create one final image object for the More Arrow:

Finally create a Text object for the Message:

Make sure to make a prefab out of your Conversation Controller. If you were working on a large project with other people, this allows you to make changes to the prefab itself without needing to modify the scene. Since you added the prefab to the scene for the first time though, you will need to save the scene this time. Also save the project to make sure the prefab is properly saved.

At this point you should be able to run the scene and test everything out. The conversation will automatically appear and wait for your input to skip from message to message. When the conversation completes, you should be able to select and move a unit on the board as you were able to do before.

Summary

In this lesson we implemented the UI components from the previous lesson in a Conversation UI element which appears as part of a cut-scene before and/or after battles. We made use of Scriptable objects to store the conversation messages and avatar sprites. We used a free library to easily create project assets out of our Scriptable Object which we could configure in the inspector. We also used an IEnumerator natively (not through a StartCoroutine call) to see how easy it can be to drive the conversation updates through events.

Don’t forget that the project repository is available online here. If you ever have any trouble getting something to compile, or need an asset, feel free to use this resource.

84 thoughts on “Tactics RPG Conversations

  1. Excellent !!!! Great stay !!!! Thank you so much for these tutorials !!! I am creating a game based on your tutorials Tactical RPG. There is no legal problem with that right? A query, now we are seeing the management UI, you know how it could be implemented inventory? Sorry if there are typos, I am using Google translator to write faster (I speak Spanish and my English is not very good). Regards !!!

    1. Hey Gustavo, I am happy to hear you are enjoying this series so much! There is no problem using my material however you wish, as it is released under the open source MIT license. As a personal request, if you complete your project I would love for you to share a link so I can see how it turned out!

      Although I have created inventory systems and menus in other games, I’m afraid that feature won’t appear in this series for awhile. Since I am working solo, I am trying to do things which are easy (read – little to no art or design required). I should have some decent architecture code-wise to support items and inventory but my initial focus on this project will be what happens within a battle, and managing your inventory is something I would expect to take place outside of battle.

      In the meantime, there are plenty of other authors out there who have shared inventory examples. This one might help,

  2. Hey excuse me, I forgot to respond to you. Of course I will show you my game when I finished. For now I’m involved with inventory. I found a very complete code and I’m studying to use as I want. Do you know the game Tactics Ogre for GBA? Well, it’s similar to FFTA in many ways, but includes some features that set it apart, and a more mature story. Features including two that I include in my games, one is the use of items and other medals system. If you do not know what I recommend you do not you will regret.

    1. Sounds good Gustavo! I’ve never played Tactics Ogre myself but it sounds like a good game and I have heard a bunch of people commenting on it. I’ll have to add it to my continually growing list of games I need to try 😉

      Im curious about the code you found. Is it a public (free) link? Perhaps it would benefit other readers here as well if you wanted to share?

  3. “NullReferenceException: Object reference not set to an instance of an object
    ConversationController.MovePanel (.ConversationPanel obj, System.String pos) (at Assets/_Scripts/Controller/ConversationController.cs:66)
    ConversationController+c__Iterator3.MoveNext () (at Assets/_Scripts/Controller/ConversationController.cs:95)
    ConversationController+c__Iterator3.m__0 (System.Object sender, System.EventArgs e) (at Assets/_Scripts/Controller/ConversationController.cs:102)
    EasingControl.Tick (Single time) (at Assets/_Scripts/Common/EasingControl.cs:202)
    EasingControl+c__Iterator0.MoveNext () (at Assets/_Scripts/Common/EasingControl.cs:151”

    When I run the game I get an empty conversation box on the upper right hand side followed by that error and nothing appear. It just goes to the gameboard with my unit, of which I cannot move now. Thanks for any help 🙂

    1. Assuming you copied the code exactly, it is probably a missing reference in the scene (like forgetting to assign a reference to one of the scripts), or perhaps the ConversationData asset was not able to be loaded. When I am hunting down issues like these I usually scatter a few calls such as “Debug.Log(sd == null);” and do one such line for each object which might be null. If it’s a code error don’t forget you can download my code from the repository and then you can compare from there.

    1. Also, I meant to ask – can you explain why you use the ++i version of the increment in your for loops? I understand the difference and have used it in things such as Debug.Logs and arguments, but what is the benefit of using it in a for loop? Thanks again for the awesome series!

  4. I followed the code exactly but I still get this when the second message is supposed to appear:

    NullReferenceException: Object reference not set to an instance of an object
    RectTransformAnimationExtensions.AnchorTo (UnityEngine.RectTransform t, Vector3 position, Single duration, System.Func`4 equation) (at Assets/Scripts/Common/Animation/RectTransformAnimationExtensions.cs:18)
    RectTransformAnimationExtensions.AnchorTo (UnityEngine.RectTransform t, Vector3 position, Single duration) (at Assets/Scripts/Common/Animation/RectTransformAnimationExtensions.cs:11)
    RectTransformAnimationExtensions.AnchorTo (UnityEngine.RectTransform t, Vector3 position) (at Assets/Scripts/Common/Animation/RectTransformAnimationExtensions.cs:7)
    LayoutAnchor.MoveToAnchorPosition (TextAnchor myAnchor, TextAnchor parentAnchor, Vector2 offset) (at Assets/Scripts/Common/UI/LayoutAnchor.cs:65)
    Panel.SetPosition (.Position p, Boolean animated) (at Assets/Scripts/Common/UI/Panel.cs:53)
    Panel.SetPosition (System.String positionName, Boolean animated) (at Assets/Scripts/Common/UI/Panel.cs:41)
    ConversationController.MovePanel (.ConversationPanel obj, System.String pos) (at Assets/Scripts/Controller/ConversationController.cs:74)
    ConversationController+c__Iterator4.MoveNext () (at Assets/Scripts/Controller/ConversationController.cs:55)
    ConversationController+c__Iterator4.m__0 (System.Object sender, System.EventArgs e) (at Assets/Scripts/Controller/ConversationController.cs:63)
    EasingControl.Tick (Single time) (at Assets/Scripts/Common/Utility/EasingControl.cs:202)
    EasingControl+c__Iterator1.MoveNext () (at Assets/Scripts/Common/Utility/EasingControl.cs:151)

    I will let you know if I discover the issue.

    1. Looking only at the output all I can tell you is that it expected to find an object reference and didn’t, but I can’t tell you “why” that happened. Some of the most frequent issues people encounter are usually related to forgetting to connect a reference in the Editor’s inspector pane, or using a wrong reference such as a prefab from the project rather than an instance of a prefab in the scene.

      If you checkout the project from the repository at commit “1c8787bf2e87f07cc66fabce733b5055d5a4e8e4” then you can see how the code and project looked at the time I wrote the post, and will perhaps be able to figure out the problem by comparing this against your own.

      It is also possible that you encountered a bug similar to one I found in the animation libraries. I refactored it due to some null references I was getting in the upcoming lesson on Magic. Hope that helps!

      1. I had connected all the prefabs and it works until the second message tries to appear, then it errors. And I tried to continue on anyway and now the Ability Menu won’t show up either. I’ll check out the commits and see if anything is different from your code at all.

        1. If one of the conversation messages appears, then it would be worth verifying your Conversation Data is properly configured. Sometimes if you forget to save the project then your settings can be lost.

      2. No errors anymore but still not showing the second message but the ability menu shows up now and it works to some degree (outside the menu options cloning themselves right now). I’ll continue on with your tutorials and compare with your repository as I go. Thanks for the help!

      3. The conversation itself is fine, or should be. I’ll be going back and seeing if I missed anything though.

  5. After writing the ConversationPanel script, ran into an error, maybe it has to do with some new unity update? I’m still a novice so it could definitely be user error. Anyway the error messages are as follows:

    Assets/Assets/Scripts/View Model Components/ConversationPanel.cs(17,19): error CS1061: Type `Tweener’ does not contain a definition for `easingControl’ and no extension method `easingControl’ of type `Tweener’ could be found (are you missing a using directive or an assembly reference?)

    Assets/Assets/Scripts/View Model Components/ConversationPanel.cs(18,19): error CS1061: Type `Tweener’ does not contain a definition for `easingControl’ and no extension method `easingControl’ of type `Tweener’ could be found (are you missing a using directive or an assembly reference?)

    I’ve looked at all the animation code I have compared to the scripts in your repo and it looks like everything is in line. Has anyone run into something similar or have any advice on further troubleshooting?

    1. I’m wondering if you happen to be using my updated animation code. I refactored it toward the end of this project by combining the Tweener component and the animation timer (which I had called easingControl) into just one component called Tweener. If so, you can probably just delete the easingControl reference but leave everything else and it will work. Otherwise, I would try checking out the commit for this step of the project and compare your code against it.

  6. hello, and thank you for this one of a kind on the inernet tutorial series of awesomeness. I am among the legions of readers who intend to use this lesson as a combat engine framework for a coming game! Now i havent looked ahead yet, and maybe you left it for us to put in, or its fixed after i read the next one, but the scripts above as written never trigger the event to move to the next state?

  7. I know this might be a stupid question, but when you set up the scene i don’t get where the Left(and right) Edge Conversation Panel Script come form or the other things, i’m pretty confused :/

    1. Hey Michael, if you look at the Hierarchy screen grab of the “Conversation Controller” prefab there will be two panels beneath its canvas. One is named “Right Edge Conversation Panel” and the other is named “Left Edge Conversation Panel”. Both objects are configured using the same kinds of components such as a “ConversationPanel” component, but are specifically configured via the inspector to look correct on the indicated side of the screen. These objects should be dragged into the relevant slots of the “Conversation Controller” root object.

      1. Hi thanks for the help but another problem arrived :/ i now get this error when i play the scene : “NullReferenceException: Object reference not set to an instance of an object” any help?

        1. If you copied and pasted the code examples, then the most likely culprit is that you forgot to connect a reference in the inspector window. There are lots of references, so make sure that that you closely follow and match all the screen grabs I included.

  8. The IntroScene conversation seems to be deleting itself whenever I test out the scripts in Unity (The conversation runs as planned, but after the program itself is run IntroScene’s size is set back to 0).

    Great tutorials, by the way! It’s helped me learn a lot about programming my own FFT-like, although I do plan on changing some systems along the way to make sure i’m actually learning 🙂

    1. Make sure that you didn’t type out the conversation while in play mode, because exiting play mode will reset everything back to the way it was before you started. Also, after creating the conversation make sure you save the project. You should be able to close Unity and open it again and see the conversation data saved. If so then playing and stopping the scene shouldn’t be able to erase your data either.

  9. I’m using the most recent code for the animation scripts, and it looks like my arrows in the conversation panel no longer animate. I wonder what could be going on here?

    1. Good catch, I never noticed it had stopped working. I think we just need to tell it to “Play” – could be as simple as adding that to the “Start” method on the Tweener script, unless you want more control than that.

    2. It seems, once the gameobject Arrow is deactived, it lost his pingpong loop.

      I tried using Play() during start but it didn’t fixed the problem so i tried other way, i disable the image. The animation is running in background but you will not able to see the arrow.

    1. Sure, it should be relatively easy to skip a conversation. If I were to add that feature I would probably start with the “CutSceneState” by adding a listener for whichever button press I wanted to use for skipping. Much like we have an “OnFire” method for causing the conversation to continue to the next panel, I might use a “OnSkip” method to tell the conversationController to skip to the end. This will allow it a chance to dismiss any active panels before calling its own complete method, which then triggers the “OnCompleteConversation” in our “CutSceneState” which allows the game to continue.

  10. Hey Men

    Thank you for the great Tutorial. My friend and i try to do our best to follow your steps.
    we Downloaded the whole Projekt and created also a new one. The wohle Project is just to compare.
    We Work on the new One and did all your Steps. Now we cant find our mistake in the project. Maybe you can help us to solve it.

    NullReferenceException: Object reference not set to an instance of an object
    ConversationController+c__Iterator0.MoveNext () (at Assets/Skripts/Controller/ConversationController.cs:50)
    ConversationController.Show (.ConversationData data) (at Assets/Skripts/Controller/ConversationController.cs:37)
    CutSceneState.Enter () (at Assets/Skripts/Controller/Battle States/CutSceneState.cs:30)
    StateMachine.Transition (.State value) (at Assets/Skripts/Common/State Machine/StateMachine.cs:40)
    StateMachine.set_CurrentState (.State value) (at Assets/Skripts/Common/State Machine/StateMachine.cs:9)
    StateMachine.ChangeState[CutSceneState] () (at Assets/Skripts/Common/State Machine/StateMachine.cs:24)
    InitBattleState+c__Iterator0.MoveNext () (at Assets/Skripts/Controller/Battle States/InitBattleState.cs:22)
    UnityEngine.SetupCoroutine.InvokeMoveNext (IEnumerator enumerator, IntPtr returnValueAddress) (at C:/buildslave/unity/build/Runtime/Export/Coroutines.cs:17)

    Thank you and best regards a Team from Switzerland

  11. OK, I’ve been trying to follow the tutorial and got to this part. The conversations open up and generally work fine, but… the placeholder text (“Here is a lot of text to read. Use the arrow button for more”) shows up on the conversations, and the last line for the first conversation message (“You’re scared aren’t you?”) gets repeated at the third conversation as the first line (so it goes “You’re scared aren’t you?” then “Oh… well… hm…”). Strangely during this last bit the arrow disappeared. Any ideas why?

    1. Problems could occur in a variety of places, such as in the way you prepared the conversation project assets, missing a line of code here or there, etc. I recommend copy-pasting the code snippets to see if anything changes, and/or downloading the project from the repository, checking out the appropriate “commit” via source control and then comparing it against your own version.

      The arrow is intended to disappear when there is no additional text to read. Good luck!

  12. Hi Jon! Great job on the tutorial! I’m using it as a basis for my end of degree thesis (I intend to focus on the AI part) and it’s awesome, but I have a bit of an issue right now.

    At the end, when playing the scene, I’m getting a NullReferenceException on the Enter method, pointing that “data” is not assigned to anything. I tried calling Debug.Log to see if data is really null and they turn positive (as in, data is not null), so I don’t know what could be the problem :/

    Any ideas?

    1. This is the full error log, by the way:

      NullReferenceException: Object reference not set to an instance of an object
      CutSceneState.Enter () (at Assets/Scripts/Controller/Battle States/CutSceneState.cs:23)
      StateMachine.Transition (.State value) (at Assets/Scripts/Common/State Machine/StateMachine.cs:39)
      StateMachine.set_CurrentState (.State value) (at Assets/Scripts/Common/State Machine/StateMachine.cs:9)
      StateMachine.ChangeState[CutSceneState] () (at Assets/Scripts/Common/State Machine/StateMachine.cs:25)
      InitBattleState+c__Iterator0.MoveNext () (at Assets/Scripts/Controller/Battle States/InitBattleState.cs:17)
      UnityEngine.SetupCoroutine.InvokeMoveNext (IEnumerator enumerator, IntPtr returnValueAddress) (at C:/buildslave/unity/build/Runtime/Export/Coroutines.cs:17)

      I’ve seen others have had a similar problem but didn’t post the solution 🙁

      1. O-kay, found the issue. In case anyone has the same issue: My ConversationController was not a child of BattleController.

  13. Hmm, as far as I know, I followed the Conversation Controller instructions correctly, but it’s not starting at the beginning of the battle. Or at all.

    Sorry if I’m bothering you, but I do need more help. I’m not getting any more errors, so I don’t know what I did wrong.

    1. No worries, I’m always happy to help, but as you’ve probably noticed, if there are no errors then it means it could be just about anything. Usually if there are no errors, it indicates that the problem is with the scene or asset setup rather than with code, so at least it’s narrowed down a little bit. Your best bet is to reread the lesson paying extra careful attention to any bit that describes creating objects, attaching scripts, updating prefabs, etc. Verify that the conversation object are created successfully and that they are saved (even when quitting Unity and re-opening). Alternatively you can download the completed project from my repository and compare it against your own. If you use version control software you can look at the project exactly as it would have been on the lesson you are following along with. Good luck!

    2. Had the same problem as you. The Conversation was not initializing. The scene was jumping right to the SelectUnitState.
      Cause i’ve forgot to check the InitBattleState.cs and change the line “owner.ChangeState();” to “owner.ChangeState();”.
      Now everything is working fine.
      Damn this tutorial is too good to be true.

  14. Did I miss something? I’m getting this error, but I don’t think it has anything to with the conversation state.

    Which, by the way, isn’t starting either. It just cuts straight to the hero prefab and the tile selection indicator slowly sliding to the left and I can’t input anything.

    Before I got this error, the TSI was working like a charm:

    NullReferenceException: Object reference not set to an instance of an object
    InitBattleState.SpawnTestUnits () (at Assets/Scripts/Controller/Battlestates/InitBattleState.cs:33)
    InitBattleState+c__Iterator0.MoveNext () (at Assets/Scripts/Controller/Battlestates/InitBattleState.cs:21)
    UnityEngine.SetupCoroutine.InvokeMoveNext (IEnumerator enumerator, IntPtr returnValueAddress) (at C:/buildslave/unity/build/Runtime/Export/Coroutines.cs:17)
    UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
    InitBattleState:Enter() (at Assets/Scripts/Controller/Battlestates/InitBattleState.cs:12)
    StateMachine:Transition(State) (at Assets/Scripts/View Model Component/StateMachine/StateMachine.cs:36)
    StateMachine:set_CurrentState(State) (at Assets/Scripts/View Model Component/StateMachine/StateMachine.cs:8)
    StateMachine:ChangeState() (at Assets/Scripts/View Model Component/StateMachine/StateMachine.cs:22)
    BattleController:Start() (at Assets/Scripts/Controller/BattleController.cs:17)

    1. It is likely that you have missed a resource setup step somewhere. You may need to compare your version of the project against the repository. Since the error is a null reference triggered at line 33 of the “SpawnTestUnits” method, my guess is that the hero prefab is not setup correctly, such as by missing a Unit component.

      1. Ok, I added a unit component, and now three Hero units appear instead of one, however I’m still getting that weird sliding issue with the units and the TSI.

        The cutscene is almost working, but the conversation text boxes are off center. They are animated though.

        Here’s the new error I have:

        NullReferenceException: Object reference not set to an instance of an object
        ConversationController+c__Iterator0.MoveNext () (at Assets/Scripts/Controller/ConversationController.cs:45)
        ConversationController.Show (.ConversationData data) (at Assets/Scripts/Controller/ConversationController.cs:34)
        CutSceneState.Enter () (at Assets/Scripts/Controller/Battlestates/CutSceneState.cs:23)
        StateMachine.Transition (.State value) (at Assets/Scripts/View Model Component/StateMachine/StateMachine.cs:36)
        StateMachine.set_CurrentState (.State value) (at Assets/Scripts/View Model Component/StateMachine/StateMachine.cs:8)
        StateMachine.ChangeState[CutSceneState] () (at Assets/Scripts/View Model Component/StateMachine/StateMachine.cs:22)
        InitBattleState+c__Iterator0.MoveNext () (at Assets/Scripts/Controller/Battlestates/InitBattleState.cs:23)
        UnityEngine.SetupCoroutine.InvokeMoveNext (IEnumerator enumerator, IntPtr returnValueAddress) (at C:/buildslave/unity/build/Runtime/Export/Coroutines.cs:17)

        1. As a tip, try reading the error output. It may look like a bunch of gibberish as a beginner, but it actually has a lot of useful information. The bit that you need is almost always right at the beginning. In this case, your problem appears to be with the ConversationController script at line 45. So take a look at the script at that line – whatever appears there could be the problem.

          The line numbers of your code don’t quite line up with the line numbers of the repository code, so I can only guess at your current exception. It could be that the canvas component was not found in the conversation controller’s hierarchy, or that the “data” you are passing in is null.

          I don’t have any good ideas why you might see the TSI sliding, but if you follow the setup and code exactly from the site it shouldn’t be a problem. The conversation boxes being off center are probably related to your setup of the prefabs. I would recommend re-reading each of the sections where they were configured and very carefully check everything. I also have a repository where you can download the entire project. Using source control you can view the project at each lesson so you can compare against your own version. Good luck!

          1. Ok, so I paid extra attention to the console, and I think the error has something to do with this portion of the IEnumerator:

            for (int i = 0; i < data.list.Count; ++i)

            At least, this is where the console leads me when I double click it. I've copied this exactly from both the website and the repository; its exactly the same.

            I've made a backup, so I'm going to try and complete the tutorial without the conversation portion for now, and return to it when the implementation is completed.

            And thanks for continuing to respond to these after so long, I know this tut is relatively old, so it might not be convenient to keep responding to these.

          2. Glad you gave it a shot, I think it will help you a lot. Since you identified the line of the problem, it helps already. The only potential line crashers are if the data is null or its list is null, because you can’t call properties on null objects. This would lead me to verifying that your data asset was saved to the project correctly, and that it is also loaded properly. You can add simple debug log checks to determine just before the problematic line such as:
            `Debug.Log(data != null)` and `Debug.Log(data.list != null)`
            Good luck!

  15. Not sure if I did something wrong or if it is meant to be, but I found that the tweeter duration was causing the panels to sometimes be slightly off, making it longer at 1.0f fixed this in the movepanel function. Not exactly sure why this was happening. I created a panel tester with ongui for all of the screen positions. They all worked perfectly every time for the positioning. When the scene played the conversations the positions were not correct. Very strange, any idea what may be happening?

  16. Having reached the Victory Conditions segment of this tutorial, I went back to extend the dialogue system laid out here.

    What I’ve got in mind is that the game would store a number of profiles for characters, independent from their units or combat stats. These would hold a name, a portrait (and/or a dictionary of portraits keyed to various reactions), a short descriptive blurb, and a bio. They would also optionally hold a list of Relationships – an int for a character’s social development with another character, with series of conversations and stat bonuses unlocked as it increases.

    The idea would be to simply drop one of these profiles into a Dialogue controller as the speaker, and get back a name and portrait. Units on the field would also hold a reference to a profile: if one is connected, they derive their name, portrait, and bonuses from it.

    This brings me to my question: from what I’ve learned following along with this excellent series, there are two obvious ways to create and store profiles: as a Persona component attached to a prefab GameObject – perhaps with Relationships as further components attached to the same root object; and as ScriptableObjects which exist on their own and can be easily created and saved out.

    Am I on the right track with this approach? Before I develop further is there anything I’m missing, that you’d optimize, or one of these two methods you’d recommend?

    1. Great questions. I think a ScriptableObject could be a great fit to hold collections of static data to help configure your dialogue panels. This object could hold the unit name, as well as sprite references to show based on the mood of the text.

      For elements which are dynamic, such as social development with other characters, I tend to prefer other architectures for persisting that data. It could be something as complex as a database, or as simple as writing to player prefs, but I prefer working with standard C# classes or structs in these cases. I have an “Unofficial Pokemon Board Game” project that shows how to work with databases as well as a simple tutorial on saving data if you need help: http://theliquidfire.com/2015/03/10/saving-data/

      Where you end up putting all of this is a deeper architectural question than I can really cover here. Try experimenting and see what you like. You could add new script components to each unit, or you could create a system that can load what it needs on demand. The trick is to figure out how to make it easily accessible and reusable from wherever you need it. Good luck!

  17. Hi John, amazing work you’ve done here, it’s been a week since I started following this tutorial and it’s by far the best tutorial in terms of code quality and displaying what’s a real game project is about.

    Just a touch that you might already know, but may be helpful to someone else: In more recent versions of Unity there’s no need to use the ScriptableObjectUtility lib you showed on the post, one can simply put the [CreateAssetMenu] annotation above the class name to create a Menu just like you did.

    1. Haha good catch, no particular reason – probably just an oversight. I’ll leave it as is because it matches what is in the repository and it works, but feel free to fix it in your own version.

  18. First of all, a great tutorial.
    I have completed this part of the tutorial, but when I want to test the conversations and the first text box appears, when I try to press any key it does not continue with the rest of the dialog.
    I have no errors in the console and I have checked that all the references are well placed.
    If you can help me with this bug I would appreciate it very much.
    I am using UNITY 2018.3.7f1, if it helps

    Greetings

    1. It’s not easy to help when there are no errors in the console, because it could be any number of things. One tip I can give to help trouble shoot is to add “Debug.Log” statements all throughout your code. This will let you know whether code you think is running is actually running (by seeing messages printed to the console) and whether or not your assumptions about current values are correct or not. For example, you could add one inside of the CutSceneState’s OnFire method to make sure that you are correctly receiving input from the keyboard and directing it to the correct game state. If you aren’t you have an idea what needs to be fixed, and if you aren’t, then start putting logs somewhere else such as in the ConversationController Sequence. Hope that helps!

  19. Now that async await is in unity, would this be implementable with it instead of yield? If so could you give me a rough outline on how would you approach it?

      1. Gotta admit I was mostly asking cause I’ve been meaning to learn unreal, and I use a system roughly based on this one as the core of my game. So I’m at a loss on how to implement it on c++ without IEnumerator and yield. Do you have any suggested reads or ideas on how to aproach this?

        1. I am not very experienced with Unreal and C++, so I am probably not the best person to ask, but here’s my advice. IEnumerators allow for some very simple implementations of some complex problems, but at the core it is really just another way of managing state. For example, if you are presenting a conversation, you could iterate over each of the messages by maintaining your own state of the index of the message. Or perhaps you want to assign some sort of id to each message, so that things like branching conversations are easier to manage without losing track of where you are.

  20. Not sure what I am doing wrong here, but I am seeing some strange behavior. Everything works fine, plays, conversation goes, all smooth. If I have a next arrow however for two lines of text, the arrow to show there is more starts slow then speeds up slowly until my FPS goes to a crawl. Any ideas? Also, if I have three lines of text, my second line is sometimes skipped and the third triggers directly after the first.

    1. My best guess is that there is an error in the code somewhere. This will be a little tricker because the compiler didn’t help point to the problem. Most likely it will have something to do with a loop, so I would look there first. If you have trouble finding it, you may want to try copy / pasting the code from the tutorial, or download the demo repository and copy from there.

      1. It is strange, I have figured out that it is just the ping pong effect, if I change it to repeat there is no issue, strange.
        Have not fixed the skipping issue yet though. Might be that I am calling this to activate and such from unity timeline instead.

  21. Also got all the conversations into an csv that auto updates and keeps all references that I assign in the inspector that are generated from a factory class like jobs! Learning so much here.

  22. Brilliant series, just a heads up, your link to
    ScriptableObjectUtility
    Now appears to be dead. Is there somewhere else I can get this?

    1. Great question, I am not sure where it has gone, but here is what you are missing:

      using UnityEngine;
      using UnityEditor;
      using System.IO;

      public static class ScriptableObjectUtility
      {
      /// <summary>
      // This makes it easy to create, name and place unique new ScriptableObject asset files.
      /// </summary>
      public static void CreateAsset<T> () where T : ScriptableObject
      {
      T asset = ScriptableObject.CreateInstance<T> ();

      string path = AssetDatabase.GetAssetPath (Selection.activeObject);
      if (path == "")
      {
      path = "Assets";
      }
      else if (Path.GetExtension (path) != "")
      {
      path = path.Replace (Path.GetFileName (AssetDatabase.GetAssetPath (Selection.activeObject)), "");
      }

      string assetPathAndName = AssetDatabase.GenerateUniqueAssetPath (path + "/New " + typeof(T).ToString() + ".asset");

      AssetDatabase.CreateAsset (asset, assetPathAndName);

      AssetDatabase.SaveAssets ();
      AssetDatabase.Refresh();
      EditorUtility.FocusProjectWindow ();
      Selection.activeObject = asset;
      }
      }

  23. Hi, I’ve been trying to follow along and understand every bit that’s being used (so I actually feel like I’m learning) so this isn’t the first snag I’ve hit but managed to figure out. However, I’m genuinely stuck here.
    I copied out the ScriptableObjectUtility class you have posted in the comments (because the link doesn’t work) but my AssetCreator still can’t seem to see it.

    using UnityEditor;
    using UnityEngine;

    public class AssetCreator
    {
    [MenuItem(“Assets/Create/Conversation Data”)]
    public static void CreateConversationData()
    {
    ScriptableObjectUtility.CreateAsset();
    }
    }

    (I’ve changed it to both AssetCreator and YourClassAsset for the class name and neither works.)
    I keep getting the “The name ‘ScriptableObjectUtility’ does not exist in the current context” error.
    I do plan on changing a lot of things but I figured it’d be a good idea to follow everything to a T first before attempting to change anything.

    Any help would be great, thank you, and thank you so much for this tutorial, years later and I think is the best out there (I also like that it’s text based and not a video)

    1. oh it’s supposed to have the brackets and ConversationData in there but the < brackets probably get removed like html code haha I have it exact, I promise!

        1. Thank you so much for the quick response!

          I’m not very good at all this but from what I understand, I don’t need the AssetCreator anymore and I just use the CreateAssetMenu attribute on ConversationData instead?

          I hope I got it right!

  24. Hi! Love the series!

    My issue: the Right Side Conversation Panel fails to load its Avatar sprite during the scene (the “Evil Avatar”, though the bug is reproduced with the original sprite as well). Any idea where I went wrong?

    It’s probably not the script if it’s working for the left side, but I made the right side by duplicating the left side.

    1. Hi, glad you are enjoying it!

      As you say, if the left side works, then the script is probably fine. The most likely issue is that something is wrong with the setup of the panel. Check the references on the scripts on the panel and make sure they each point to the appropriate things. If you can’t find the issue, perhaps try re-creating the panel from scratch. If you happen to see any errors printed to the console it might help point to the specific problem.

      1. Hmm, still didn’t fix itself. I have now:

        +Triple-checked my script references
        +Remade the panel from scratch
        +Used the “Conversation Controller.prefab” from your project files and added my scripts
        +Used the “ConversationPanel.cs” and “ConversationController.cs” from the project files, just to verify my scripts weren’t at fault

        Also, no errors have shown on console.

        1. Without specific errors it is hard to say for sure what the issue could be. Some things I would try on my own to track down the problem might include:
          1. Try configuring the panel manually – like with the inspector, make it show the evil avatar and make sure it can appear. Does it look wrong? Can the code then swap from the evil avatar to the good one?
          2. Try printing debug log statements and or use breakpoints to step through every line of code and make sure everything looks correct at each step of the way.
          3. Try putting just the right side panel in a scene all by itself, and make a custom script that triggers showing the avatar by a keypress or something like that. The basic idea is to make sure no other scripts are able to “undo” or conflict with your code and to reduce the possibility of bad references.

          Good luck!

Leave a Reply

Your email address will not be published. Required fields are marked *