Thoughts about how to organize a simulation

Hi All,
I’m making a simulation about a school. there is no PC, only NPCs. The Agents of the simulation are students, teachers and maybe staff. they should go about their business as usual, like being in a class, and while in class go to the board, and back to their desk, maybe take a bathroom break, and maybe on the way they could meet another student from other class so they stop and chat a little…etc. so in short, students will have different events.
I was thinking how to organize the project, since I have made a couple attempts and the project still seems disorganized to me.

what I have is a settings file that have all the major parameters of the simulation like the frequency of events, and stuff like that.
I have an agent controller that control the movement of the agent, I plan to make subclasses of the agent as students/teachers.
I have a file that controls the general events, and here is what I feel a bit lost, right now, all I have scripted is the students going to the bathroom, yet the file is big, and has a lot of instructions, I roll a dice for each student to see if he can go to the bathroom every couple of seconds in real time, then if true, he would go to the bathroom, when he/she goes to the bathroom, the script assigns an unoccupied toilet for him, he stays there a random amount of time , then goes back to his class. And when I tried to add the event of stopping to chat with other students, things pretty much exploded.

tl;dr: I am making a simulation about a school with students that has several events, how do I go about organizing the components and so?

I hope I’m making sense, and sorry for the long post. please let me know your thoughts about this, and /or if you need any information about the setup.

It seems reasonable to me to have a float you could call “needToPee” or something and simply increment it by some amount every frame. And perhaps getting a drink could increase that.

In general the way I’m handling this sort of thing is like this:

public enum NumpkinAction
{
    Idling = 0,
    Moving = 1,
    Dancing = 2,
    Talking = 3,
    Running = 4,
    Flying = 5,
    Swimming = 6,
    Training = 7,
    Playing = 8,
    Procreating = 9,
}

And then at another point:

int action_num = UnityEngine.Random.Range(1, action_count);
            action = (NumpkinAction)action_num;
            switch (action)
            {
                case NumpkinAction.Moving:
                    ActionMove();
                    break;
                case NumpkinAction.Dancing:
                    ActionDance();
                    break;
                case NumpkinAction.Talking:
                    ActionTalk();
                    break;
                case NumpkinAction.Running:
                    ActionRun();
                    break;
                case NumpkinAction.Flying:
                    ActionFly();
                    break;
                case NumpkinAction.Swimming:
                    //ActionSwim();
                    break;
                case NumpkinAction.Training:
                    ActionTrain();
                    break;
                case NumpkinAction.Playing:
                    //ActionPlay();
                    break;
                case NumpkinAction.Procreating:
                    //ActionWooWoo();
                    break;
            }

You could have weights on different actions, based on stats like needToPee.

I think we’d need to see how exactly what you’re doing has “exploded” to give any specific advice for handling the complexity.

Thank you for your reply.
what I mean by exploded is not necessarily not working, but i think it becomes convoluted and I am not sure that this is how things should be, for instance here is my navigator class, this script should oversee the movement of the pupils every where, this is only the part that handles bathroom break.

public class Navigator : MonoBehaviour
{
    float timer = 0;
    Settings settings;
    int simulationStep;
    Pupil[] pupils;
    int bathroomBreakChance;
    List<Pupil> pupilsInBathroom = new List<Pupil>();



    // Start is called before the first frame update
    void Start()
    {
        settings = FindObjectOfType<Settings>();
        simulationStep = settings.GetSimulationStep();
        pupils = settings.GetAllPupils();
        bathroomBreakChance = settings.GetBathroomBreakChacne();
      
    }

    // Update is called once per frame
    void Update()
    {
      
        timer += Time.deltaTime;
        if (timer > simulationStep)
        {
            foreach (Pupil pupil in pupils)
            {
                var agentController = pupil.GetComponent<AgentController>();
                if (agentController.GetInClassroom())
                {
                    var chance = Random.Range(0, 100);
                    if (chance < bathroomBreakChance)
                    {
                        Space closestBathroom = FindNearestBathroom(pupil);
                        agentController.SetDestination(closestBathroom.transform.position);
                    }
                }

                //TODO check if pupil in Space (bathroom) , then distribute to a subspace(toilet), then go back to class
              
            }
            timer -= simulationStep;
        }
      
        foreach (Pupil pupil in pupils)
        {
            var agentController = pupil.GetComponent<AgentController>();
            if (agentController.GetInBathroom() && !pupilsInBathroom.Contains(pupil))
            {
                Space closestBathroom = FindNearestBathroom(pupil);
                agentController.SetDestination(closestBathroom.GetAvailableSubspace().position);
                pupilsInBathroom.Add(pupil);
            }
            if (agentController.GetStandingCounter() > 3 && agentController.GetInBathroom() && !agentController.GetInToilet())
            {
                agentController.GoBack();
            }

            if (agentController.GetStandingCounter() > 10 && agentController.GetInToilet())
            {
                agentController.GoBack();
            }


        }
    }

    private Space FindNearestBathroom(Pupil pupil)
    {
        var bathroomLocations = GetComponent<Locations>().GetBathroomLocations();
        var closestBathroom = bathroomLocations[0];
        var prevBathroom = bathroomLocations[0];
        foreach (Space bathroom in bathroomLocations)
        {

            if (Vector3.Distance(pupil.transform.position, bathroom.transform.position) <
                Vector3.Distance(pupil.transform.position, prevBathroom.transform.position))
            {
                closestBathroom = bathroom;
            }
        }

        return closestBathroom;
    }

Yeah I think you should break more stuff out into methods.

Here’s ActionFly from my code, which I reference in the bit I included above:

    void ActionFly()
    {
        agent.enabled = false;
        flightBase = this.transform.position.y; // have full calculation beforehand?
        flightHeight = this.transform.position.y + (10 + (5f*numpkin.Genetics.Stats[NumpkinStats.Flight]/100f));
        flightStartTime = Time.time;
        StartCoroutine(FlyUp());
    }

    IEnumerator FlyUp()
    {
        while(this.transform.position.y <= flightHeight)
        {
            yield return new WaitForEndOfFrame();
            this.transform.position += new Vector3(0, Time.fixedDeltaTime * 10 * flightCurve.Evaluate(Time.time - flightStartTime), 0);
        }
        yield return new WaitForSecondsRealtime(1f);
        StartCoroutine(FlyDown());
    }

    IEnumerator FlyDown()
    {
        while(this.transform.position.y >= flightBase)
        {
            yield return new WaitForEndOfFrame();
            var r = 5;
            var offset_x = r * Mathf.Cos(flightAngle);
            var offset_y = r * Mathf.Sin(flightAngle);
            flightAngle += Time.fixedDeltaTime;
            if (flightAngle >= (Math.PI * 2))
            {
                flightAngle = 0;
            }
            var shift_x = offset_x - r * Mathf.Cos(flightAngle);
            var shift_z = offset_y - r * Mathf.Sin(flightAngle);
            this.transform.position -= new Vector3(shift_x, ((5 - (5f * numpkin.Genetics.Stats[NumpkinStats.Flight] / 100f)) + 1) * Time.fixedDeltaTime, shift_z);
        }
        agent.enabled = true;
        EndAction();
    }

You can see that all I do from Update is just call the method for the “action” and all of the action’s complexity is in that action’s (and other subsequent) method(s).

I don’t think you’re going to be able to get rid of the general complexity, but if you better segment it it will be easier to understand and debug.

And ideally you should be able to get rid of Update altogether, but that’s another topic.

This is the perfect case for a Behavior tree in combination with some utility AI

First, don’t make subclasses, use composition. Have Teacher/Student as a component, and “Agent” as a component they control.

This is basically a state machine, and I would consider implementing each “state” (idling, going to class, studying, going to bathroom) as a separate class, and have THAT control agent. The class could be derived from Component or from ScriptableObject.

A student could store a stack of “states”, and current state is the one on the top of the stack. Current state can request to replace itself with another state, or push another state onto stack, which will then take control. Or it can return, in which case previously active state will become active again.

For example, you have [Idle] state which just checks when it is time to go to school. When it is the time, it pushes [School] state onto stack. Then it goes to class, then goes to bathroom, then handles encounter. And when it is active it handles entirety of school activity. When school is over, it “returns”, and student returns to being “Idle”.

Same deal with bathroom breaks encounters, and so on.

[Idle] <-- top
Becomes
[School] <-- top, currently active
[Idle]
Becomes
[Class] <--- Top, currently active
[School]
[Idle]
Becomes
[BathroomBreak]
[Class]
[School]
[Idle]

Becomes
[Chat]
[BathroomBreak]
[Class]
[School]
[Idle]

Also, this is the situation where you might want to consider using AI frameworks that implement decision trees. As certain situation that your NPC is handling should be represented by a “Node”, but logic of switching between those Nodes can get convoluted quickly.

Coroutines also are a possible option for this, although they do not really serialize.

Thank you all for your answers, that should greatly help. I never knew about behavior trees before but it sounds like it’s what I am after.

Any recommendation on where to begin on this, and is it too advanced? I’ve only just started with unity but it seems like a very nice option for me?

We use node canvas which we are pretty happy with.

https://assetstore.unity.com/packages/tools/visual-scripting/nodecanvas-14914

If you only just started with unity I would ignore the suggestions about behaviour trees and utility AI. Get to grips with the core engine first before trying to learn these topics because you will end up wasting a lot of time trying to learn deep topics and getting nothing actually done.

Utility AI in particular is not an easy thing for a beginner to implement, even with a node / visual editor to visualise things.

If you’ve just started, you’re likely aiming too high and need to get used to the engine first.

If you cant manage that then a School Tycoon game is probably out of scope anyway

Not necessarily, it depends on how in-depth OP wants it to be and how realistic. It could be very cartoonish and therefore get away with a lot of things being representations rather than simulations of many aspects. I think it could be a good beginner project, if designed and tackled the right way. There are enough tutorials and articles out there related to aspects of tycoon games that OP could create a decent enough prototype and learn a lot along the way.

I would like to thank you all for your comments, they were very helpful. I am currently keeping things very simple, I will start investigating your suggestions and see how far I can go.