I could use a bit of guidance on the predicate system

Hello,

Im trying to use the IPredicateEvaluator to add a “shop” dialogue during certain times, using a TimeManager script I wrote outside of this course.

Before diving into how the time manager works, I just wanted to share the predicate I set up, just to check if I understand this correctly.

Parameters in editor:

Evaluate function in TimeManager:

public bool? Evaluate(string predicate, string[] parameters)
    {
        if (predicate == "ShopTime")
        {
            if (Hour >= int.Parse(parameters[0]) && Hour <= int.Parse(parameters[1]))
            {
                return true;
            }
            return false;
        }
        return null;
    }

Per my understanding, the way this should work is if the “hour” is greater than 8 but less than 10, the dialogue option to open the shopUI should appear. Currently, this dialogue option appears no matter what the in-game time is.

Ive honestly always struggled with this system, and have never really been able to make it work consistently. What am I doing wrong?

for reference, here is the entire timemanager script, but Im not sure if the problem is coming from there:

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

public class TimeManager : MonoBehaviour, IPredicateEvaluator
{
    //Time
    public static Action OnMinuteChanged;
    public static Action OnHourChanged;
    public static Action OnSleep;

    public static int Minute { get; private set; }
    public static int Hour { get; private set; }
    public static int SeasonDate { get; private set; }

    public static string CurrentSeason { get; private set; }
    public static string CurrentDay { get; private set; }

    [SerializeField] float minuteToRealTime = 0.5f;
    [SerializeField] int startHour = 6;
    private float timer;

    //Day and Season
    public Season season = 0;
    public Day day = 0;

    void Start()
    {
        Minute = 0;
        Hour = startHour;
        timer = minuteToRealTime;

        //day count
        SeasonDate = 0;
        UpdateDate();
        SetDate();
    }

    void Update()
    {
        timer -= Time.deltaTime;

        if (timer <= 0)
        {
            Minute++;
            OnMinuteChanged?.Invoke();
            if(Minute >= 60)
            {
                Hour++;
                Minute = 0;
                if (Hour >= 24)
                {
                    Hour = 0;
                    UpdateDate();
                    SetDate();
                }
                OnHourChanged?.Invoke();
            }

            timer = minuteToRealTime;
        }
    }

    public void UpdateDate()
    {
        if (SeasonDate < 30)
        {
            SeasonDate++;
        }
        else
        {
            season++;
            if (season > Season.Winter)
            {
                season = Season.Spring;
            }
            SeasonDate = 1;
        }
        if (day > Day.Saturday)
        {
            day = Day.Sunday;
        }
        else
        {
            day++;
        }
    }

    public void SetDate()
    {
        CurrentDay = day.ToString();
        CurrentSeason = season.ToString();
    }

    public void Sleep()
    {
        UIFade.Instance.FadeToBlack();
        if(Hour  >=4 && Hour <= 5)
        {
            Hour += 6; //not a full night sleep
            Minute = 0;
        }
        else
        {
            Hour = 6;
            Minute = 0;
            UpdateDate();
            SetDate();
            PlayerController.Instance.GetComponent<PlayerHealth>().FullHealPlayer();
            GetComponentInParent<ManagerResetter>().DailyReset();
        }
        OnSleep?.Invoke();
        UIFade.Instance.FadeToClear();
    }

    public bool? Evaluate(string predicate, string[] parameters)
    {
        if (predicate == "ShopTime")
        {
            if (Hour >= int.Parse(parameters[0]) && Hour <= int.Parse(parameters[1]))
            {
                return true;
            }
            return false;
        }
        return null;
    }
}

This looks like it should work. Let’s add a little Debug to make sure all the eyes are crossed and tees dotted.

    public bool? Evaluate(string predicate, string[] parameters)
    {
        if (predicate == "ShopTime")
        {
            Debug.Log($"TimeManager.Evaluate(ShopTime({parameters[0]}, {parameters[1]}), Hour = {Hour}");
            if (Hour >= int.Parse(parameters[0]) && Hour <= int.Parse(parameters[1]))
            {
                return true;
            }
            return false;
        }
        return null;
    }

I added the log, and it’s not getting called. Looks like something is missing.
Are there any other components I should share? Maybe I forgot to attach something to the time manager gameobject?

Here is the condition script:

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

[System.Serializable]
public class Condition
{
    [SerializeField] Disjunction[] and;
    public bool Check(IEnumerable<IPredicateEvaluator> evaluators)
    {
        foreach (Disjunction dis in and)
        {
            if (!dis.Check(evaluators))
            {
                return false;
            }
        }
        return true;
    }

    [System.Serializable]
    class Disjunction
    {
        [SerializeField] Predicate[] or;

        public bool Check(IEnumerable<IPredicateEvaluator> evaluators)
        {
            foreach (Predicate pred in or)
            {
                if (pred.Check(evaluators))
                {
                    return true;
                }
            }
            return false;
        }
    }

    [System.Serializable]
    class Predicate
    {
        [SerializeField] string predicate;
        [SerializeField] string[] parameters;
        [SerializeField] bool negate = false;

        public bool Check(IEnumerable<IPredicateEvaluator> evaluators)
        {
            foreach (var evaluator in evaluators)
            {
                bool? result = evaluator.Evaluate(predicate, parameters);
                if (result == null)
                {
                    continue;
                }

                if (result == negate) return false;
            }
            return true;
        }
    }
}

Is the TimeManager attached to the Player? If it’s not, it won’t get picked up by the Condition system, as it expressly looks for conditions on the Player…
You could write a passthrough class for this if TimeManager is on another GameObject:

public class TimePredicateChecker, MonoBehaviour, IPredicateEvaluator
{
         public bool? Evaluate(string predicate, string[] parameters)
    {
        if (predicate == "ShopTime")
        {
            if (TimeManager.Hour >= int.Parse(parameters[0]) && TimeManager.Hour <= int.Parse(parameters[1]))
            {
                return true;
            }
            return false;
        }
        return null;
    }
}

Place this on your player, and it should check against the TimeManager.

Ok, now I see.

I added IPredicateEvaluator directly to the player controller, and now it is functioning as expected.

The only issue is that I know get an “index out of bounds” error when I enter that dialogue node while the condition is not met. I think I will re-watch some of the dialogue videos, as I vaguely recall a similar bug getting addressed in the lectures.

The error is getting called in Next() on the player conversant, when setting currentNode = children[randomIndex].

Summary

{

public class PlayerConversant : MonoBehaviour
{
    [SerializeField] string playerName;

    Dialogue currentDialogue;
    DialogueNode currentNode = null;
    AIConversant currentConversant = null;
    bool isChoosing = false;

    public event Action onConversationUpdated;
    public event Action onConversationEnded;

    public void StartDialogue(AIConversant newConversant, Dialogue newDialogue)
    {
        currentConversant = newConversant;
        currentDialogue = newDialogue;
        currentNode = currentDialogue.GetRootNode();
        TriggerEnterAction();
        onConversationUpdated();
    }

    public bool IsChoosing()
    {
        return isChoosing;
    }

    public string GetText()
    {
        if (currentNode == null)
        {
            return "";
        }

        return currentNode.GetText();
    }

    public IEnumerable<DialogueNode> GetChoices()
    {
        return FilterOnCondition(currentDialogue.GetPlayerChildren(currentNode));
    }

    public void SelectChoice(DialogueNode chosenNode)
    {
        currentNode = chosenNode;
        TriggerEnterAction();
        isChoosing = false;
        Next();
    }

    public string GetCurrentConversantName()
    {
        if (isChoosing)
        {
            return playerName;
        }
        else
        {
            return currentConversant.conversantName;
        }
    }

    public void Next()
    {
        int numPlayerResponses = FilterOnCondition(currentDialogue.GetPlayerChildren(currentNode)).Count();
        if (numPlayerResponses > 0)
        {
            isChoosing = true;
            TriggerExitAction();
            onConversationUpdated();
            return;
        }

        DialogueNode[] children = FilterOnCondition(currentDialogue.GetAIChildren(currentNode)).ToArray();
        int randomIndex = UnityEngine.Random.Range(0, children.Count());
        TriggerExitAction();
        currentNode = children[randomIndex];
        TriggerEnterAction();
        onConversationUpdated();
    }

    public bool HasNext()
    {
        return currentDialogue.GetAllChildren(currentNode).Count() > 0; //true if there is another node after current
    }

    private IEnumerable<DialogueNode> FilterOnCondition(IEnumerable<DialogueNode> inputNode)
    {
        foreach (var node in inputNode)
        {
            if (node.CheckCondition(GetEvaluators()))
            {
                yield return node;
            }
        }
    }

    private IEnumerable<IPredicateEvaluator> GetEvaluators()
    {
        return GetComponents<IPredicateEvaluator>();
    }

    private void TriggerEnterAction()
    {
        if (currentNode != null)
        {
            TriggerAction(currentNode.GetOnEnterAction());
        }
    }

    private void TriggerExitAction()
    {
        if (currentNode != null)
        {
            TriggerAction(currentNode.GetOnExitAction());
        }
    }

    private void TriggerAction(string action)
    {
        if (action == "") return;

        foreach (DialogueTrigger trigger in currentConversant.GetComponents<DialogueTrigger>())
        {
            trigger.Trigger(action);
        }
    }

    public void Quit()
    {
        currentDialogue = null;
        TriggerExitAction();
        currentNode = null;
        isChoosing = false;
        currentConversant = null;
        onConversationUpdated();
        onConversationEnded();
    }

    public bool IsActive()
    {
        return currentDialogue != null; 
    }
}

}

Thank you for your help!

You need a node with the opposite condition (same condition but with negate) that says something like “Hey, buddy, I’m not open for business right now!” (or something similar). You have to ensure that any group of dialogues will always have a node that doesn’t fail.

thank you Brian, I think I get it now.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms