Skip to main content
GameDev.net gamedev.net
✓ Solved 🔒 Locked 🎮 Unity

Understanding the manager pattern for an enemy state machine.

Started by Elgrino2244 Aug 6 at 8:33 PM 7 replies 650+ views
Original Post
Elgrino2244
Elgrino2244

So I have an enemy with 3 states, a manager, and a couple motors. So far the manager only controls state changes and runs the state update() of whichever state is selected. The manager defines three enums for each of the three states, then each state is represented by its own script. What I want to know is how I should organize and split data among states and manager. Should manager contain all the data and each state and motor access the manager to retrieve their data? Or should each state script have its own data. I understand that data should flow downward and references should not criss-cross each other to avoid errors. So what is the best way to maintain organized referencing and data flow in this setup?

Accepted Answer

This is roughly the design I would aim for. The core idea is to keep the components purely data, and then to have singular systems (e.g. EnemyManager) that operate on that data across all objects in the scene. This has numerous benefits compared to the standard Unity-style "everything can access everything" script design: centrality, performance, easier inter-object communication, ease of access of dependencies without globals/singletons.

Rather than having a separate script for each state, put it all in the same file (at least until that becomes too large), and use a switch statement to choose the right function for the state. Using polymorphism to avoid a switch statement is probably not the right choice in 99% of cases. Keep it simple. I strongly suggest avoiding the creation of huge numbers of small scripts without a good reason to do so. The Single Responsibility Principle is often applied overzealously. Monoliths are not as bad as you might think.

I repeat for emphasis: the script-facing architecture of Unity is extremely bad. It is a poorly-designed legacy system. That bad design encourages bad developer habits that cause many problems. The fundamental issue is that the "MonoBehavior" can do almost anything, which makes debugging and reasoning about the program state extremely difficult because every engine feature can be accessed from anywhere. Furthermore, Unity encourages putting game logic in scripts attached to individual game objects. This seems natural at first glance, but is the root cause of the bad architecture. With logic in scripts, rather than in centralized Systems, every object must now have a reference to the CollisionSoundManager to play a sound when it recieves a collision, or CollisionSoundManager must be a global/singleton. Components containing game logic eventually either become bloated (without globals) or spagetti (with globals).

class EnemyData
{
    // Only data and functions that operate on that data exclusively.
    enum currentState;
    Motor [] motors;
};

// Only one of these in the scene
class EnemyManager
{
    // Register enemies with manager when added to scene. Unregister when removed.
    SomeDataStructure<EnemyData> enemies;
    // Enemy manager can have references to other dependencies it needs.
    
    void update()
    {
        // for each enemy in the scene, update its current state with updateEnemy()
    }
    
    private void updateEnemy( EnemyData enemy )
    {
        switch ( enemy.currentState )
        {
            case State1: updateState1( enemy ); break;
            case State2: updateState2( enemy ); break;
            case State3: updateState3( enemy ); break;
        }
        // Do some stuff always, regardless of state.
    }
    
    private void updateState1( EnemyData enemy )
    {
        // Do stuff specific to state 1.
        enemy.motors[0].setSpeed( 1.0f );
    }
};



Alberth
Alberth

In a monolithic system design, the single "thing" is responsible for everything. That may get tricky because with every step you have to consider how to keep everything in sync all the same time. Updating itself is however simple because all data is directly available.

On the other end, you split the design into a lot of different parts (you can split responsibilities infinitely often). Each part is simple or even trivial (ie almost empty), but splitting introduces the cost of needing to let all other part know something happened, so they can update themselves. The parts are then foremost busy with communicating with other parts.

The first design is complicated because the responsibilities tend to become inter-twined, making code hard to understand. The second design is very fragmented, making it very hard to understand the behavior as a whole. The sweet spot is usually somewhere in the middle.

Note that OOP is primarily aimed at large scale systems, in general. In that situation, it pays to split the problem because each sub-problem in itself is already highly complicated. Without splitting it may be too complicated to ever get it working. An enemy with 3 states however doesn't sound highly complicated to me.

An exercise you may want to do is to write down the alternatives you consider, and just build each as an exercise. Afterwards you can then compare the various designs.

TooOld2rock-nRoll
TooOld2rock-nRoll

Well.... until recently, I never really committed to use ALL of C++.

My uses for OO were very much like an orderly C, inheritance were used occasionally for things that could have been a void*

Until recently... I never understood the saying "You can shoot your own feet programing in C, but C++ will cost you an arm and a leg!"

So I will agree with @Aressera, keep things in context, balance what is pertinent for the complexity of your end goal.

I decided to implement the command pattern with the state pattern to solve the problem you are facing, this way I could avoid other overlooking modules, each state was self contained and actions could be triggered from events interdependently.

What quickly become a nightmare, were an endless list of small classes that did almost nothing and could have been dealt with a line at a switch.

It should make maintenance much easier at the long run, if you have a bug in the behavior of a character in a specific situation, you just find the state and command that trigger that. Very clean, very precise, but not really.

You just get lost in endless scrolling and intertwined function jumps, because nothing knows nothing about anything else but that ONE THING!

Get too deep at inheritance and any refactoring is a entire week job.

And templates will get you mad, months after working properly, with compilation errors that make no fucking sense in header files too long for their own good.

What I will try to do next time I feel like procrastinating, is try to make the "entities" of the game more like RPG characters and less like efficient computer simulations. The self contained entity will know how to be itself and the world will limit what it tries to do, instead of telling what and how to do it.

Did that make sense for your question?

"No, you're never too old to rock and roll
If you're too young to die"
LorenzoGatti
LorenzoGatti

How do you think your Enemy scripts are going to evolve during development? Make those kinds of change easy and neat, at the expense of others.

  • If states are going to proliferate and each has a separate script, mitigate duplication and inconsistency by factoring behaviour that is common between several states into common functions, called by the state scripts (e.g. choosing a place to retreat), or maybe executed in separate processing steps before or after this state machine (e.g. checking whether the Enemy is dying and its data should be reclaimed).

  • If you are going to tweak numbers (e.g. the range or accuracy of a weapon) to affect behaviour, it's probably better to put such numbers in individual Enemies and in Enemy type definitions, where they can be accessed by scripts and inspected and modified easily, and leave scripts simple and generic (without important constants) and unaltered over the course of many experiments.

Regarding where to put data, it should be clear what should be global (e.g. a map data structure) and what belongs to individual Enemies (e.g. position), what is shared between groupings of Enemies (e.g. patrol waypoints for a squad of soldiers), what is inherited from Enemy types (same for all instances of that type, e.g. what remains the enemy leaves when killed or destroyed). Is the game engine tempting you to do something else?

Omae Wa Mou Shindeiru
Elgrino2244
Elgrino2244

I think my solution is to create 3 scriptable object types, one for each state. I will put all unchanging state specific data in these containers while the state scripts may have a few private data such as a Vector2, that I can expect to change often during the game. The Scriptable object references will be held in the manager and accessed via manager.Statedata.property by states and motors. I will also give each state script its own set of enums so I may include substates later. I will have a lot of behaviors that will be interchangeable to create new situations with the same enemy. Later, I will come across a new problem if I want to do advanced combinations. I wish there was a condition "type" I could put in my scriptable objects to determine the condition needed to switch substates during runtime. Because initially it will be choosing 1 substate for runtime.

Elgrino2244
Elgrino2244

TooOld2rock-nRoll wrote:

Well.... until recently, I never really committed to use ALL of C++.

My uses for OO were very much like an orderly C, inheritance were used occasionally for things that could have been a void*

Until recently... I never understood the saying "You can shoot your own feet programing in C, but C++ will cost you an arm and a leg!"

So I will agree with @Aressera, keep things in context, balance what is pertinent for the complexity of your end goal.

I decided to implement the command pattern with t...


It seems like balance and intuition are the key. If there was an obvious answer more people would be making games.

Aressera
Aressera

Elgrino2244 wrote:

The Scriptable object references will be held in the manager and accessed via manager.Statedata.property by states and motors

This is backwards. In your first post you even correctly acknowledge that data should flow downward. States and motors accessing the manager is the opposite of how it should be. Low-level components should have no knowledge of the higher-level thing that controls them. Instead, the manager should push new data into the lower level components. Doing it the way you suggest will lead to spagetti code troubles. Does a car's motor ask the driver if it should run? No, the driver tells the motor. Actions in this universe always follow a dependency chain from the highest level (consciousness) down to the lowest level (physical reaction). Similarly, software should be built as a hierarchy of layers, each higher level depending on and controlling the layer(s) below it, but not usually the reverse. The reverse direction, when needed, can be implemented using abstractions like dependency injection, delegates, or function pointers that allow low-level systems to communicate with high-level systems without knowing their concrete type or implementation. Even in those cases, the high level systems are still providing the dependencies or delegates or function pointers to the low level system.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.