Custom Condition Control and Timeline

Announcements, support questions, and discussion for the Dialogue System.
Post Reply
Loremu
Posts: 20
Joined: Wed Dec 29, 2021 11:06 am

Custom Condition Control and Timeline

Post by Loremu »

Dear Pixel Crushers Team,

I hope you can help me with an issue I faced using the StartConversationClip in Unity's Timeline in combination with custom conditions that access C# scripts.

There are 2 Characters in my scene: Harald and Silvia.
You (the player) can talk to Harald, and Harald uses the Timeline Trigger Component to trigger a cutscene OnConversationEnd. The playableDirector sits on a different GameObject.
The cutscene is quite short:
Harald walks over to Silvia and Silvia says something to Harald.

In the timeline I use simple animation and a StartConversationClip for this that starts Silvia's conversation.

Sivlia has two different options of what to say to Harald. What Sivlia says during runtime is determined by a custom condition that depends on a boolean inside a C# script to which the dialogue database has access:
true-> Sivlia says Option 1 ;;; false -> Silvia says Option 2.

This works fine except that it calls the boolean from the wrong instance of the script - it should call it from Sivlia but it actually calls it from Harald who has the script attached, too.
I have no idea how to tell the dialogue manager to call the boolean function on Silvia.

Do you have any advice for me and/or a general explanation on how the dialogue database calls functions?
Also (out of curiosity) what does the Conversant mean/do that appears in the inspector when you select the StartConversationClip in the timeline? I assigned all kinds of stuff to it but I couldn't observe any effects...

Any help would be appreciated! Thank you in advance!
User avatar
Tony Li
Posts: 22154
Joined: Thu Jul 18, 2013 1:27 pm

Re: Custom Condition Control and Timeline

Post by Tony Li »

Hi,

When you write a conversation, you can specify the conversation's primary Actor and Conversant. (In the Dialogue Editor, inspect a conversation and select Menu > Conversation Properties to see the primary Actor and Conversant dropdowns.)

When you set up a Dialogue System Trigger to start a conversation, you can specify the Conversation Actor and Conversation Conversant GameObjects. If these are assigned, then it will use these GameObjects for the active conversation's primary Actor and Conversant, even if those GameObject represent different actors than in the original conversation. More info: Character GameObject Assignments.

In Timeline, a Start Conversation clip will use the Start Conversation track's GameObject as the Conversation Actor. You can use the clip's Conversant field to specify the Conversation Conversant. If you leave it unassigned, the Start Conversation clip will look for the GameObject that represents the conversation's primary Conversant.

---

Are you registering your C# method with Lua so you can access it in dialogue entries' Conditions? If so, bear in mind that these Lua functions are global. If you register the C# method with Lua on Harald, then you can't also register the same method on Silvia. Since you can only register one instance of the C# method with Lua, you may need to pass the character name to the C# method. If your method is "IsBoolTrue()", then you may need to change it to "IsBoolTrue(string characterName)" so you can check IsBoolTrue("Harald") or IsBoolTrue("Silvia").
Loremu
Posts: 20
Joined: Wed Dec 29, 2021 11:06 am

Re: Custom Condition Control and Timeline

Post by Loremu »

Hi,

thanks for your help! A happy new year for you and your team! :D

Ah okay, I wasn't aware that if you register a function with Lua you could only have one instance of the respective C# script in the scene, so that's probably where the problem is.

My programming skills are somewhere between beginner and intermediate, so maybe you can give me some advice on how to handle this?
I have multiple characters in the scene. Depending on different variables they should say different things. At the moment there are only two variables: their energy level and their mood. There might be more variables in the future that affect conversation options. So far I have only hooked up the mood-variable with the dialogue system - this is the boolean I'm having trouble with.

I use the Lua functions to gain access to the character's variables and I originally planned to have the same C# script attached to every character in the scene (with different individual values). However, since you can only have one instance of the script in the scene, the only possibility I see is to somehow access all needed variables from a single script that either holds the variables of all characters or has access to all characters. Either way it would need to iterate through arrays or lists to find the right character. This shouldn't be an issue, since there are probably max 30 characters in the scene, however, this method seems quite clumsy to me.

I guess I'm not the first one to make characters say different things depending on their energy or health or whatsoever. Do you have a better idea how to do this or some "best practice" advice?

Thanks a lot!
User avatar
Tony Li
Posts: 22154
Joined: Thu Jul 18, 2013 1:27 pm

Re: Custom Condition Control and Timeline

Post by Tony Li »

Hi,

There are several ways you could approach this. Here's a suggestion that's fairly straightforward and flexible.

Let's assume each of your character GameObjects has a script like this:

Code: Select all

public class CharacterAttributes : MonoBehaviour
{
    public int energyLevel;
    public int mood;
}
Add a Dialogue Actor component to each character GameObject, and set the Actor field to the character's actor in your dialogue database. The Dialogue Actor component will register the character's GameObject with the Dialogue System, which makes it easy to access the GameObject at runtime without having to search the scene.

Add a script like the one below to the Dialogue Manager GameObject. This script will register Lua functions for each character attribute, where you can provide a character name and it will return that character's value.

Code: Select all

public class CharacterAttributesLua : MonoBehaviour
{
    private void Awake()
    {
        Lua.RegisterFunction("GetEnergyLevel", this, SymbolExtensions.GetMethodInfo(() => GetEnergyLevel(string.Empty)));
        Lua.RegisterFunction("GetMood", this, SymbolExtensions.GetMethodInfo(() => GetMood(string.Empty)));
    }
    
    private double GetEnergyLevel(string actorName)
    {
        CharacterAttributes characterAttributes = GetCharacterAttributes(actorName);
        return (characterAttributes != null) ? characterAttributes.energyLevel : 0;
    }
    
    private double GetMood(string actorName)
    {
        CharacterAttributes characterAttributes = GetCharacterAttributes(actorName);
        return (characterAttributes != null) ?  characterAttributes.mood : 0;
    }
    
    private CharacterAttributes GetCharacterAttributes(string actorName)
    {
        Transform actor = GetRegisteredActorTransform(characterName);
        if (actor == null) 
        {
            Debug.LogWarning($"Can't find CharacterAttributes on {actorName}");
            return null;
        }
        else 
        {
            return actor.GetComponent<CharacterAttributes>();
        }
    }
}
For brevity, I omitted the 'using' statements at the top.

If you don't want to write a separate function for each attribute, you could write a single function that accepts two parameters: the character's name and the attribute to look up. It's up to whatever you prefer.

In your conversation nodes' Conditions fields, you can check attributes such as:

Code: Select all

GetEnergyLevel("Harald") > 5;
or

Code: Select all

GetMood("Silvia") == 42;
To avoid having to type "GetEnergyLevel(..." in your conversations' Conditions fields, you can create a Custom Lua Function Info asset as described in Registering Lua Functions. This way you can select your functions from the "..." dropdown menu.
Loremu
Posts: 20
Joined: Wed Dec 29, 2021 11:06 am

Re: Custom Condition Control and Timeline

Post by Loremu »

Hi,

wonderful, this is exactly what I'm looking for :D

Thank you!
User avatar
Tony Li
Posts: 22154
Joined: Thu Jul 18, 2013 1:27 pm

Re: Custom Condition Control and Timeline

Post by Tony Li »

Happy to help!
Loremu
Posts: 20
Joined: Wed Dec 29, 2021 11:06 am

Re: Custom Conditions - C# and Lua

Post by Loremu »

Hi,
it's me again, I keep running into issues with Custom Conditions that call C# functions.
Quick summary: I have a condition that calls a boolean from a C# script but it chooses the "condition bool == true" path even though the condition is false.
(Warning: wall of text ahead....)
EDIT: I found the error before I even sent the message. I'm sending it anyways in case someone else has a similar issue. Yet, dear support person: If you have any ideas on how I could achieve my goal in a more elegant way- I appreciate any suggestions :)
Also: Do you have an answer for question 2 at the end of the text? It works just fine, I'm rather asking out of curiosity :)

What I want to have:
In the game some characters hesistate to speak to others due to various reasons. You (the player) can give them energy to overcome those impediments. If you start a conversation you get to read the thoughts of the character. Depending on how much energy the character has they either say the impediment text (e.g. "I'm afraid!") or its counterpart (e.g. "I'm afraid - but I should tell them anyways!"). In my example there are 3 impediments + counterparts.
(dialogue entries are structured as follows: start -> introduction -> imp 1 OR counterpart 1 -> imp 2 OR counterpart 2 -> imp 3 OR counterpart 3)

Each impediment has an individual energy-cost. The costs add up for each impediment and you have to pay the total cost to gain access to the respective counterparts.
Example:
imp 1 cost: 20 energy --> you need 20 energy in total
imp 2 cost: 50 energy --> you need 70 energy in total
imp 3 cost: 40 energy --> you need 110 energy in total

So far I used the following conditions which worked fine:
imp 1: GetEnergy("Harald") < 20
counterpart 1: GetEnergy("Harald") >= 20
imp 2: GetEnergy("Harald") < 70
counterpart 1: GetEnergy("Harald") >= 70
imp 3: GetEnergy("Harald") < 110
counterpart 3: GetEnergy("Harald") >= 110

However, I want to control those costs outside the dialogue database so if I want to change values later I don't have to go into each conversation and change (and calculate) them manually for each entry (there will be a LOT of dialogue).

What I did:
There's another script (Opportunity) that sits on a GameObject in the scene. The NPC GameObjects that are involved in the opportunity are children objects of the Opportunity-GameObject. Opportunity counts the dialogue entries that I marked as impediments using a custom field and creates an int[] of the respective length. I made this function available in editor: you press a button in the inspector, it calls initializeImpedimentArray, and you can assign the values for each impediment. This works fine.

Code: Select all

public int impedimentsCount()
    {
        int sum = 0;
        //conversation = DialogueManager.MasterDatabase.GetConversation(conversationName); //copy pasted from Dialogue System Trigger -- might not work in editor, since master database is created during runtime?

        conversation = dialogueDatabase.GetConversation(conversationName);

        foreach (DialogueEntry entry in conversation.dialogueEntries)
        {

            if (Field.LookupValue(entry.fields, "IsImpediment") == "True")
            {
                sum += 1;
            }
        }
        return sum;
    }
    public void initializeImpedimentArray() //Forum entry: https://www.pixelcrushers.com/phpbb/viewtopic.php?f=3&t=3737&p=20811&hilit=access+custom+field+by+code#p20811
    {
        
        impedimentCosts = new float[impedimentsCount()];
    }
My NPC has access to this array. A boolean (HasEnoughEnergy) compares the NPC's current energy to the energy cost values.
The dialogue manager has access to specific instances of the NPC script using the approach you suggested me earlier: It calls a function on an NPCLua script that sits on the Dialogue Manager GameObject in the scen and the NPCLua calls the NPC function with the given actor.
This is the code in NPCLua:

Code: Select all

public bool GetHasEnoughEnergy(string actorName, double impedimentIndex)
    {
        NPC npc = FindNPC(actorName);
        if (npc != null)
        {
            //Debug.Log(this + " npc.HasEnoughEnergy(impedimentIndex): " + npc.HasEnoughEnergy(impedimentIndex));
            return npc.HasEnoughEnergy(impedimentIndex);
        }
        else
        {
            Debug.Log(this + "npc = null, actorName: " + actorName);
            return false;
        }
    }
This is the code in my NPC script:

Code: Select all

public bool HasEnoughEnergy(double impedimentIndex)
    {
        //int index = (int)dialogueEntryIndex;
        int index = Mathf.RoundToInt((float)impedimentIndex); //convert double to int
        //Debug.Log("dialogueEntryIndex: " + impedimentIndex + " index: " + index);

        double cost = 0;
        if (opportunity != null) //check if there is an Opportunity script attached to a parent 
        {
            if (opportunity.impedimentCosts.Length > 0) //check if array has any values
            {
                for (int i = 0; i < index; i++) //energy cost ist checked in each dialogue entry and you need the complete amount of energy to make the character talk so all values up to the current dialogue entry are added together
                {
                    cost += opportunity.impedimentCosts[i];
                    Debug.Log(this + " dialogueEntryIndex (input): " + impedimentIndex + " index: " + index + " i: " + i + " dialogueEntryIndex (input): " + impedimentIndex + " cost: " + cost + " energy: " + energy);
                    //Debug.Log(this + " energy: " + energy);
                }
            }
        }
        if(energy >= cost)
        {
            Debug.Log(this + " energy: " + energy + " cost: " + cost + " returning true");
            return true;
        }
        else
        {
            Debug.Log(this + " energy: " + energy + " cost: " + cost + " returning false");
            return false;
        }
        
    }
These are the conditions (in my example there are 3 impediments):
imp 1: GetHasEnoughEnergy("Harald", 0) == false
counterpart 1: GetHasEnoughEnergy("Harald", 0) == true
imp 2: GetHasEnoughEnergy("Harald", 1) == false
counterpart2: GetHasEnoughEnergy("Harald", 1) == true
imp 3: GetHasEnoughEnergy("Harald", 2) == false
counterpart 3: GetHasEnoughEnergy("Harald", 2) == true



The issue
The character starts with 0 energy. Still, when I start the conversation, it directly goes to counterpart 1, then continues with imp 2 and imp 3. In this example this should be the case when character.energy >= 20

The first dialogue entry (ID 1 - right after the 'Start' entry) has no conditions. It is an introduction to what the character is thinking about, and only then come the impediments.
Starting the conversation: It shows the introduction.
Console Output:
Harald (NPC) energy: 0 cost: 0 returning true
Harald (NPC) energy: 0 cost: 0 returning true
(--> it does not enter the for-loop in HasEnoughEnergy in NPC; --> is that why cost is 0? Also why does it call the function at all even though this dialoge entry does not have any conditions? edit after I found my error: I think I understand it now, apparently the conditions are already called in the previous entry)

Clicking "continue" first time: It shows counterpart 1 (should be impediment 1).
Console Output:
Harald (NPC) dialogueEntryIndex (input): 1 index: 1 i: 0 dialogueEntryIndex (input): 1 cost: 20 energy: 0
Harald (NPC) energy: 0 cost: 20 returning false
Harald (NPC) dialogueEntryIndex (input): 1 index: 1 i: 0 dialogueEntryIndex (input): 1 cost: 20 energy: 0
Harald (NPC) energy: 0 cost: 20 returning false
(--> dialogueEntryIndex (input) shouldn't it be 0? Also why does it enter the "condition = true" entry even though the function returns false? edit after I found my error: I think I understand it now, apparently the conditions are already called in the previous entry, so 1 is correct)

Clicking "continue" second time: It shows impediment 2.
Console Output:
Harald (NPC) dialogueEntryIndex (input): 2 index: 2 i: 0 dialogueEntryIndex (input): 2 cost: 20 energy: 0
Harald (NPC) dialogueEntryIndex (input): 2 index: 2 i: 1 dialogueEntryIndex (input): 2 cost: 70 energy: 0
Harald (NPC) energy: 0 cost: 70 returning false
(--> dialogueEntryIndex (input) shouldn't it be 1? edit after I found my error: I think I understand it now, apparently the conditions are already called in the previous entry, so 2 is correct)

Clicking "continue" third time: It shows impediment 3.
Console Output:
(--> shouldn't there be any output? edit after I found my error: I think I understand it now, apparently the conditions are already called in the previous entry, so the call for this condition already happened)


My questions are:
1: Where does it get the energy cost value "0" from? (values are not assigned at runtime - none of the values in the array is 0)
2: When entering imp2/counterpart2: Why does it only call the function once (There are 2 possible options - shouldn't it check all of them like it did before)?
3 (main question): It seems to me like there is an offset of 1 in the function calls (so conditions are already called on entering the previous dialogue entry). This worked fine with the conditions I used before (mentioned further up in the text). I think the error has to do with accessing the ImpedimentCosts Array. For some reason it returns a 0 value (and thus returns HasEnoughEnergy == true) which should not happen bc. there IS no 0 value.
Any ideas / suggestions / solutions?

I apologize for the massive amount of text that I threw at you. I appreciate any kind of help with this issue, any insight on the execution order of the condition calls or any suggestions of how I could handle the "energy costs" somewhere outside of the database in an easier, more elegant way (like I mentioned before - I'm not a total beginner to scripting but still quite a beginner ^^")

Thanks in advance!

lol, I found the error whilst writing down the issue: It's in the for loop in HasEnoughEnergy in my NPC script:
I wrote:

Code: Select all

or (int i = 0; i < index; i++)
--> when it is called for the first time index = 0 so it doesn't enter the for loop and returns energy = 0.
It should be:

Code: Select all

or (int i = 0; i <= index; i++)
User avatar
Tony Li
Posts: 22154
Joined: Thu Jul 18, 2013 1:27 pm

Re: Custom Condition Control and Timeline

Post by Tony Li »

Hi,

I think this all boils down to: Conversations Evaluate Conditions One Extra Level Ahead

You'll need to structure your conversations with this in mind.
Loremu
Posts: 20
Joined: Wed Dec 29, 2021 11:06 am

Re: Custom Condition Control and Timeline

Post by Loremu »

Hi,
thanks for the documentation link, this is really helpful!
User avatar
Tony Li
Posts: 22154
Joined: Thu Jul 18, 2013 1:27 pm

Re: Custom Condition Control and Timeline

Post by Tony Li »

Glad to help!
Post Reply