-
Notifications
You must be signed in to change notification settings - Fork 30
Simple Dialogue Manager Part1
In this tutorial I'm going to show you how to write simple dialogue system. To do that let's write simple design document At the end of this part you will get an editor window and first dialogue configurations which would looks like this
- Player spawned at closed room with box NPC and one door. Each elements is a built in unity 3d objects (cubes, spheres, capsules.)
- At the left side we have a box. If user come to this box he will get a message "this box is closed. do you have a key?"
- on the ride side we have a door, and one more character let's say... Bender.
- Character has following commands and dialogue sequences
-
- (D) - dialogue phrase
-
- (c) - command
-
- (P) [ok] [no] - player response with one-two buttons "ok" "no" in this case could be long one like [see you later, bye]
- (D) you shall not pass
- (P) [but i want to pee]
- (D) I wan't to tell you something... "Luke i'm your father"
- (P) [Hey just let me in]
- (D) Ok Jerzy, but you must bring me a persimmon
- (P) [Where i can find a persimmon?]
- (D) In that green box. here is a key
- (C) give key to player (store key at player inventory)
- (P) good (C) exit from dialogue
- (P) [could you repeat please] (c) repeat sequence from beginning
- (D) Did you find the persimmon?
- (P) [Not yet]
- (D) And what are you doing here?
- (P) "I'll be right back"
- (C) exit from dialogue
- (C) [yes, here you are(give key to bender)]
- (D) Let's see.. iftaḥ yā simsim... тыщ пыщ
- (c) get persimmon from player inventory
- (c) open door (physical)
- (c) exit from dialogue
In case if player don't have a key
- (D) You don't have a key
- (P) Ok
- (c) exit from dialogue
In case have a key
- (P) [open box]
- (D) you've got a persimmon
- (c) put persimmon to player inventory
- (P) exit
- (c) exit from dialogue
Let's define our inventory and how we want to work with it. In this project I'll create a simple enum which will contain all game items.
public enum ItemType{
Key,
Persimmon,
_SpokeWithBender
}
I've used a small hack here. All service items start with underscore sign like _SpokeWithBender
and I'll use this as variable holder. (so we will able to mark that players finished some dialogue flow). In a normal system a scriptable languages like Lua could be used, but we don't won't to overcomplicate this project.
A component which we will attach to actors (box, Bender and player). Also let's define some methods which we will use later like check whether we have item, increment or decrement item count. We want to store item and number so i'll create one more class which can store this info.
[Serializable]
public class Slot{
public ItemType Item;
public int Count;
}
public class Inventory : MonoBehaviour {
public List<Slot> Slots;
//Could accept negative value
public void AddItem(ItemType itemType, int count){
if (count > 0){
IncreaseItemNumberOn(itemType, count);
} else {
DecreaseItemNumberOn(itemType, -count);
}
}
void IncreaseItemNumberOn(ItemType itemType, int count){
Slot slot = GetSlotForType(itemType);
if (slot == null){
slot = new Slot(){
Item = itemType,
Count = count
};
} else {
slot.Count+= count;
}
}
void DecreaseItemNumberOn(ItemType itemType, int count){
Slot slot = GetSlotForType(itemType);
if (slot == null){
//good we don't have this item, can't remove anything
} else {
slot.Count -= count;
if (slot.Count < 0){
Slots.Remove(slot);
}
}
}
public bool HasItem(ItemType item){
Slot slot = GetSlotForType(item);
return slot != null && slot.Count > 0;
}
// On later game we can return collection because one item could be stored at several slots
Slot GetSlotForType(ItemType item){
return Slots.First((arg) => arg.Item == item);
}
}
After that we need to add this component to our box, player and Bender. We can then setup items as needed
This is how the box inventory might look like.
p.s. Don't forget to add an empty inventory to the Player
Here we need to setup our configuration for dialogue flow and so on. We want to configure the following things:
- Show phrases (this is what NPC will tell us)
- Player response 1 button (give player options to select several buttons)
- Player response 2 button (right now we don't support arrays or nesting for nodes this is why we have several configs)
- Trigger animation (open door)
- Increment/decrement items (give/take persimmon or key)
- Check condition (do we have item or not to trigger different dialogue branches)
- Close Dialogue
Later we will add triggers to our interactive objects and NPC. When the player reaches the zone, NPC or object we will set up 2 parameters, the GameObjects for the player and the opponent, and start playing sequences. Inside the config if we need to check some parameters we will just use the standard GetComponent<>
method to check the inventory of the player or the opponent.
So let's start and make this configurable scriptable objects
First we need to create some kind of interface that will accept messages with actor and opponent info. This class must derive from ScriptableObjectNode
, a base class that contains the required information for the Editor UI rect to position our node inside the graph. As you can see it is marked as [OneWay]
because we want to link our whole object to properties.
[OneWay]
public abstract class DialogueNode : ScriptableObjectNode {
public abstract void Execute(GameObject actor, GameObject opponent);
}
And this is our dialogue holder that will keep all config nodes.
[CreateAssetMenu(menuName = "NodeInspectorDemo/Dialogue")]
public class DialogueSequence : ScriptableObject {
[Graph("StartItem")]
public List<DialogueNode> Items;
public DialogueNode StartItem;
}
The [Graph]
attribute sets the name of the property that contains the first node that we should use on dialogue start.
To show a phrase we need to use some kind of controller which will show the text on the screen. We didn't implement it yet, so it's just outlined here. We can also define who will say this phrase with the Owner
property.
public enum PhraseOwner{
Actor,
Opponent
}
public class ShowPhrase : DialogueNode {
public PhraseOwner Owner;
public string Text;
[OneWay]
public DialogueNode NextStep;
public override void Execute(GameObject actor, GameObject opponent)
{
throw new System.NotImplementedException();
}
}
As you can see we use the [OneWay]
attribute to identify which node we should call next. I think we will wait for the user to press a button to go to the next node.
###PlayerResponse.cs
public class PlayerResponse : DialogueNode {
public string Caption;
[OneWay]
public DialogueNode NextStep;
public override void Execute(GameObject actor, GameObject opponent)
{
throw new System.NotImplementedException();
}
}
public class PlayerResponse2Button : DialogueNode {
public string Caption1;
[OneWay]
public DialogueNode NextStepOn1;
public string Caption2;
[OneWay]
public DialogueNode NextStepOn2;
public override void Execute(GameObject actor, GameObject opponent)
{
throw new System.NotImplementedException();
}
}
###PlayAnimation.cs
As can be seen here we can reference any external assets like we would in the normal inspector.
public class PlayAnimation : DialogueNode {
public AnimationClip Animation;
[OneWay]
public DialogueNode NextStep;
public override void Execute(GameObject actor, GameObject opponent)
{
throw new System.NotImplementedException();
}
}
###AddItem.cs
Here we can use the inventory methods we created earlier.
public class AddItem : DialogueNode {
public Owner Owner;
public ItemType ItemType;
public int ItemNumber;
[OneWay]
public DialogueNode NextStep;
public override void Execute(GameObject actor, GameObject opponent)
{
GameObject actualOwner = Owner == Owner.Actor ? actor : opponent;
Inventory inventory = actualOwner.GetComponent<Inventory>();
if (inventory == null){
Debug.LogErrorFormat("can't find inventory on {0} gameObject ", actualOwner.name, actualOwner);
} else {
inventory.AddItem(ItemType, ItemNumber);
}
NextStep.Execute(actor, opponent);
}
}
###HasItem.cs
In the full game it would probably be reasonable to create more complex conditions. We could check how many items we have, if we have a specific count, and so on. For now we will just check if the player has the item or not.
We copy paste some code from the previous class, but if you ned to use it one more time in another node, don't forget to extract it as a method and use that instead to avoid code duplication.
public class HasItem : DialogueNode {
public Owner Owner;
public ItemType Item;
[OneWay]
public DialogueNode Yes;
[OneWay]
public DialogueNode No;
public override void Execute(GameObject actor, GameObject opponent)
{
GameObject actualOwner = Owner == Owner.Actor ? actor : opponent;
Inventory inventory = actualOwner.GetComponent<Inventory>();
if (inventory == null){
Debug.LogErrorFormat("can't find inventory on {0} gameObject ", actualOwner.name, actualOwner);
} else {
if (inventory.HasItem(Item)){
Yes.Execute(actor, opponent);
} else {
No.Execute(actor, opponent);
}
}
}
}
I want to make sure that we closed all popup windows here. Not sure if that is needed, maybe we can just delete it.
public class CloseDialogue : DialogueNode {
public override void Execute(GameObject actor, GameObject opponent)
{
throw new System.NotImplementedException();
}
}
p.s. One more thing that you can create is a node which has Dialogue
(our main graph) as a parameter. With this you could split one complex dialogue into sub dialogue branches.
Now we have all required nodes, so let's create our first dialogue. Create a separate folder for it and go to context menu (right mouse button) and click Create/NodeInspectorDemo/Dialogue or whatever menu entry you provided above for your dialogue.
Then you need to open NodeInspector. It is located at the top unity menu bar under Window/NodeInspector. This inspector shows whatever node graph you have selected in your project tab, so if you don't select your dialogue it will show nothing. In the top left corner of the inspector bar you will find the option Create. Use it to create new nodes.
So let's create some instances and create our first dialogue.
This is what i've got for our Bender NPC.