Orchestrating the initial game moment without a GameManager

I finished this course not too long ago and I’m now trying to set up a nice opening scene for a new game. I start by triggering a dialogue (code below) with a MonoBehaviour I call ConditionallyExecuteTriggerOnce. It uses the strategy pattern and conditions. If the condition is true then it calls a TriggeringStrategy (scriptable object strategy pattern) which can execute arbitrary code. All well and good. I also use TriggeringStrategy’s in the DialogueNode’s onExit/onEnterAction to trigger arbitrary code at different points in the dialogue (i.e. do a cut scene, grant the initial quest, change music, etc)

But here’s an example of the kids of things that trip me up.

After the first quest is done, I need to change the way music works. Outside of the opening sequence, I have a monobehavior (MusicPlayer) on the player gameobject with an update loop that checks what scene we’re in, if there are monsters, etc and picks the appropriate track. But that update loop shouldn’t run unless the first quest is complete because it a specific set of musical tracks triggered by either starting the game or by the initial dialogue. So now the music player also needs a condition (e.g. if first quest not completed, don’t do the normal update loop).

I’m realizing I might need to do a lot of these conditions in other parts of the code now because this opening sequence changes how the normal course of the game is played. Here are other examples:

  • Disable the portal in first scene unless the initial quest is complete
  • Add tutorial arrows (e.g. “click here”, “notice this UI element here”, disable certain elements of the UI) the first time a certain piece of the UI is used.

The pattern seems to be that some objects have “a first time through” state and there is some arbitrary condition that breaks that object out of that state.

EDIT (adding the question): It feels like it can get out of hand quickly so is adding condition(s) for each class that has some sort of initial behavior a good way to do it?

ConditionallyExecuteTriggerOnce.cs
using GameDevTV.Saving;
using GameDevTV.Utils;
using Newtonsoft.Json.Linq;
using UnityEngine;

namespace RPG.Triggers
{
    public class ConditionallyExecuteTriggerOnce : MonoBehaviour, IJsonSavable
    {
        [SerializeField] private TriggeringStrategy[] triggerArray;
        [SerializeField] private Condition condition;
        
        // State
        private bool previouslyTriggered;
        
        // cached references
        private GameObject player;
        private IPredicateEvaluator[] evaluators;
        
        private void Awake()
        {
            player = GameObject.FindWithTag("Player");
        }
        
        private void Start()
        {
            evaluators = player.GetComponents<IPredicateEvaluator>();
        }

        private void Update()
        {
            if (condition.Check(evaluators))
            {
                foreach (var trigger in triggerArray)
                {
                    trigger.Trigger();                    
                }
                enabled = false;
                previouslyTriggered = true;
            }
        }
        
        public JToken CaptureAsJToken()
        {
            return JToken.FromObject(previouslyTriggered);
        }

        public void RestoreFromJToken(JToken state)
        {
            previouslyTriggered = state.ToObject<bool>();
            if (previouslyTriggered) enabled = false;
        }
    }
}
TriggeringStrategy.cs
using RPG.Core;
using UnityEngine;

namespace RPG.Triggers
{
    public abstract class TriggeringStrategy : ScriptableObject
    {
        public abstract void Trigger();
    }
}

I came up with something (a MonoBehaviour on the Player called MusicPlayer). MusicPlayer almost feels like a state machine for the whole game because it has to consider the following

  • what scene are we in
  • are we coming from a restored game or is this a brand new started game
  • have we had some external event trigger an override as to what music to play (that’s the triggering strategy above which)
  • has the override been canceled
  • are there any enemies present (there’s an external class for this but there’s still a dependency on RPG.Combat)

It honestly feels like a mess and was a pain to debug. Are there are quick pointers here? (don’t need code)

I’m not sure if that’s the way I would approach it, but that doesn’t mean it isn’t a good way to do it.

Not necessarily. You could structure the player to abort the Update loop unless a boolean is set, and then using a DialogueTrigger (when you complete the quest), you can set the that boolean to allow the Update to continue.

Begin with the portal disabled, use a DialogueTrigger to enable it when the quest has been completed. You’ll probably also need to have an SaveableEntity on the portal that can manage it’s active state. In this case, the SaveableEntity and a bridge component would stay enabled on the portal, and the Portal script would be disabled until activated by the bridge component.

That’s probably a whole course in itself.

OK - so oddly enough before I posted the last message that’s what I did. Almost. Part of the “problem” is that the quest completes via CompleteObjectivesByPredicate so I had to use a gameobject with ConditionallyExecuteTriggerOnce on it break out of the override. But instead I could add a townperson and a dialogue easily enough. But this brings me back to the first problem where I don’t want the townsperson to appear after I exit that scene.

So I’m back to using ConditionallyExecuteTriggerOnce. This at least has the nice feature that my monobehaviours are not cluttered with conditions. Is there a better solution?
EDIT: I think I know what will be most scalable. Just like our DialogueNode have an optional OnExitAction I will add an OnCompleteAction to Quests. I will then I used the TriggeringStrategy to trigger whatever action I need. I think I’m going to go with this because it’s useful in other parts of my game where I need to trigger something to happen right after a quest completes.

What’s a bridge component in this context?
EDIT: I assume you mean a monobehaviour that can receive a DialogueTrigger. e.g. ChangeStateOnTrigger which keeps the referenced object or component disabled (or enabled) by default and then toggles its state based on the trigger.

OK fair enough. :slight_smile: I think I’ll just have the relevant UI components check if they are in a tutorial state (via their non-UI counterpart scripts) when they open.

OK. Thanks for talking me through it. I think I have the solution and I think this will be useful for other students. I think what I landed on will be way simpler and has much more reusability.

The solution below is combined with advice you gave me earlier on having an SO being able to reference objects in the scene. These are some of the key pieces. I left out some of the obvious parts.

A few gotchas… if you are subscribing to events in Start (vs OnEnable) you might get some NullReferenceExceptions with some of the custom triggers

InitializeGame.cs
using GameDevTV.Saving;
using GameDevTV.Utils;
using Newtonsoft.Json.Linq;
using UnityEngine;

namespace RPG.Triggers
{
    public class InitializeGame : MonoBehaviour, IJsonSavable
    {
        [SerializeField] private Condition gameNotStartedCondition;
        [SerializeField] private TriggeringStrategy[] triggerArray;
        
        // State
        private bool successfullyTriggered;
        
        // cached references
        private GameObject player;
        private IPredicateEvaluator[] evaluators;
        
        private void Awake()
        {
            player = GameObject.FindWithTag("Player");
        }
        
        private void Start()
        {
            evaluators = player.GetComponents<IPredicateEvaluator>();
            if (gameNotStartedCondition.Check(evaluators))
            {
                Debug.Log("Game has not started yet");
                foreach (var trigger in triggerArray)
                {
                    trigger.Trigger();                    
                }
                // Return is necessary here just in case the player didn't initialize the game correctly.
                // For example you may check if the player has the initial quest and if not you
                // automatically start a dialogue which grants the quest. If the player doesn't save the
                // game or exits the dialogue prematurely this will trigger it again.
                return;
            }
            Debug.Log("Game has started so we can disable");
            enabled = false;
            successfullyTriggered = true;
        }
        
        public JToken CaptureAsJToken()
        {
            return JToken.FromObject(successfullyTriggered);
        }

        public void RestoreFromJToken(JToken state)
        {
            successfullyTriggered = state.ToObject<bool>();
            if (successfullyTriggered) enabled = false;
        }
    }
}
ChildrenStateController.cs
using GameDevTV.Saving;
using Newtonsoft.Json.Linq;
using UnityEngine;

namespace RPG.Core
{
    public class ChildrenStateController : MonoBehaviour, IJsonSavable
    {
        [SerializeField] private bool defaultStateOfChildren;
        
        private bool isEnabledOnNextLoad;

        private void Awake()
        {
            isEnabledOnNextLoad = defaultStateOfChildren;
        }

        private void Start()
        {
            foreach (Transform child in transform)
            {
                child.gameObject.SetActive(isEnabledOnNextLoad);
            }
        }

        public void StateOnNextLoad(bool enableOnNextLoad)
        {
            isEnabledOnNextLoad = enableOnNextLoad;
        }

        public JToken CaptureAsJToken()
        {
            return JToken.FromObject(isEnabledOnNextLoad);
        }

        public void RestoreFromJToken(JToken state)
        {
            isEnabledOnNextLoad = (bool)state;
        }
    }
}
PassthroughTriggerReceiver.cs
using UnityEngine;
using UnityEngine.Events;

namespace RPG.Triggers
{
    public class PassthroughTriggerReceiver : MonoBehaviour
    {
        [SerializeField] private PassthroughTrigger trigger;

        public UnityEvent onTrigger;
        
        void OnEnable()
        {
            if (trigger != null)
            {
                trigger.TriggerActivated += Trigger;
            }
        }

        private void OnDisable()
        {
            if (trigger != null)
            {
                trigger.TriggerActivated -= Trigger;
            }
        }

        private void Trigger()
        {
            onTrigger?.Invoke();
        }
    }
}
PassthroughTrigger.cs
using UnityEngine;

namespace RPG.Triggers
{
    [CreateAssetMenu(menuName = "Triggers/Passthrough Trigger")]
    public class PassthroughTrigger : TriggeringStrategy
    {
        public event System.Action TriggerActivated;

        public override void Trigger()
        {
            TriggerActivated?.Invoke();
        }
    }
}
Portal.cs
        private void OnTriggerEnter(Collider other)
        {
            if (!enabled) return;
            if (other.gameObject.CompareTag("Player"))
            {
                StartCoroutine(Transition());
            }
        }

        public JToken CaptureAsJToken()
        {
            return JToken.FromObject(enabled);
        }

        public void RestoreFromJToken(JToken state)
        {
            enabled = (bool)state;

        }
TriggeringStrategy
using UnityEngine;

namespace RPG.Triggers
{
    public abstract class TriggeringStrategy : ScriptableObject
    {
        public abstract void Trigger();
    }
}

DialogueNode.cs
    public class DialogueNode : ScriptableObject
    {
        [SerializeField] TriggeringStrategy onEnterAction;
        [SerializeField] TriggeringStrategy onExitAction;
    }
Quest.cs
public class Quest : RetrievableScriptableObject, IJsonSavable
{
        [SerializeField] private TriggeringStrategy onQuestComplete;
}

1 Like

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

Privacy & Terms