What is the best way of setting States for a single game object? (Enum, static instances, etc.)

I have a game object with 4 states: Divisible, Mergeable, Selectable, and Attackable. I am about to put them together to form a StateManager sort of MonoBehaviour script, so the game object will be able to switch to one of the 4 states given when certain conditions and player conditions / actions were met.

What is the best way of designing a state management system for a single game object that is flexible and understandable to use?

Flexible, as in similar to C++/Java switch-case and enum checking.
Understandable, as in, once Update() detects a state has been changed, immediately transition to that state and do state logic accordingly.

However, because Unity is built with Components in mind, I am not familiar with implementing a Component-based state manager, especially for player input and networking. Therefore, I am asking for the best method, recommended by experts in Unity game development.

Thanks in advance.

What I would do:
Starting with the State class:

// To be attached on each game object that can have state change
public class StateChangeable: MonoBehaviour
{
    public enum StateType { Divisible, Mergeable, Selectable, Attackable };

    public event System.Action<StateChangeable, StateType> EventChangeState;

    private StateType state;
    public virtual StateType State
    {
        get
        {
            return state;
        }

        set
        {
            if (state == value)
                return;

            state = value;

            if (EventChangeState != null)
                EventChangeState(this, state);
        }   
    }
}

So the idea is to attach this behaviour in objects that can have a state.
Now this class is prepared to be used either by inheritance or as component.
Let’s use as example that there is a Person game object using states.
By inheritance, for example (in this case you just need to attach this one below in each game object):

public class Person: StateChangeable
{
    public override StateType State
    {
        get
        {
            return base.State;
        }
        set
        {
            base.State = value;

            if (base.State == StateType.Attackable)
                Debug.Log("Now I can attack!");
        }
    }
}

And now following the component pattern (you have to attach both StateChangeable and Person at same game object):

public class Person : MonoBehaviour
{
    StateChangeable StateChangeable;

    void Awake()
    {
        StateChangeable = GetComponent<StateChangeable>();
        StateChangeable.EventChangeState += StateChangeable_EventChangeState;
    }

    void OnDisable()
    {
        StateChangeable.EventChangeState -= StateChangeable_EventChangeState;
    }

    void StateChangeable_EventChangeState(StateChangeable stateChangeable, StateChangeable.StateType state)
    {
        if (state == StateType.Attackable)
            Debug.Log("Now I can attack!");
    }
}

Both methods are ok to use. A nice way to chose the better approach is to answer the following question for each entity relationship: Is a or Has a. At least in my projects, more common is to have “has a” so I tend to use more components (but also inheritance some times).

You may want to read this composition over inheritance discussion.

Using the example above, the way I designed my “Person” game object is it has 4 states. That means, I need to have 1 StateManager component, plus the number of States that the game object will have, right?

There’s no need for static instances? As in, the StateManager can be unique per instances of the same game object throughout the duration of the game? I am having trouble trying to mash (or visualize) multicasting delegates with networking concepts, so I can send messages from client to server and get them to change states. I thought the game might need a global single instance of StateManager, that handles multiple instances of the game object, “Person”.

Or I am making it more complicated than it sounds?

Maybe you could post a brief example like how you would do in your C++ / Java code… Then I can try to point you what could be done inside C#/Unity3D

I think you’re making it out to be more complicated than it sounds.

  1. you can still use enums in C#/Unity. Enums exist!

  2. Have you never done a state machine in Java or C++ that used objects to represent the states? That’s how a component based state machine would work, just that the objects are also components.

  3. what is the “best”? What is defined as the “best”? If you want to have a discussion about what programming practices are better than others, you’re going about it a bit odd of a manner.

Here’s a sample code from one of my projects (I just copy/paste, so you may skip a few of them.):

/**
* ALL WORKS COPYRIGHTED TO The Pokémon Company and Nintendo. I REPEAT, THIS IS A CLONE.
*
* YOU MAY NOT SELL COMMERCIALLY, OR YOU WILL BE PROSECUTED BY The Pokémon Company AND Nintendo.
*
* THE CREATOR IS NOT LIABLE FOR ANY DAMAGES DONE. FOLLOW LOCAL LAWS, BE RESPECTFUL, AND HAVE A GOOD DAY!
* */

package level;

import resources.Art;
import screen.BaseBitmap;

public class PixelData {
    // This class contains all of the area's pixel color, pixel's properties, pixel flags to check, etc.
    // This object will be loaded along with other PixelData objects when loading an area.
 
    /*
     * Pixel data types: (Including alpha values)
     *
     * 0xFF00FF00: Flat grass (Can be walked, no Pokémon) 0xFF0000DD: Ledges Horizontal 0xFF0000AA: Small tree
     *
     * Anything else: Flat grass.
     */
 
    // This is also the ID number for the pixel.
    private int color;
    // If false, it's an obstacle.
    private boolean[] facingsBlocked = new boolean[4];
    public int xPosition;
    public int yPosition;
    // This represents the art resource this PixelData object is representing.
    // This can also give flexibility when it comes to puzzle-themed areas.
    // Now adding animations.
    public BaseBitmap[] bitmap;
    public BaseBitmap[] biomeBitmap;
    public int bitmapTick;
    public int biomeBitmapTick;
 
    // This can also be "isWayPoint".
    private boolean isWarpZone;
    private int targetArea;
    private int targetSector;
    private int groundHeight;
 
    private boolean triggerFlag;
    private int targetMovementScriptID;
 
    public PixelData(int pixel, int x, int y) {
        this.xPosition = x;
        this.yPosition = y;
        this.color = pixel;
        this.targetArea = -1;
        this.targetSector = -1;
        this.groundHeight = 0; // Default
        this.bitmapTick = 0;
        this.biomeBitmapTick = 0;
        this.triggerFlag = false;
        this.targetMovementScriptID = 0;
     
        int alpha = (pixel >> 24) & 0xFF;
        int red = (pixel >> 16) & 0xFF;
        int green = (pixel >> 8) & 0xFF;
        int blue = pixel & 0xFF;
     
        setProperties(alpha, red, green, blue);
        prepareBitmap(alpha, red, green, blue);
    }
 
    public void enableTrigger() {
        this.triggerFlag = true;
    }
 
    public void disableTrigger() {
        this.triggerFlag = false;
    }
 
    public boolean hasTriggerEvent() {
        return this.triggerFlag;
    }
 
    public int getTargetScriptID() {
        return this.targetMovementScriptID;
    }
 
    public void disableWarpZone() {
        this.isWarpZone = false;
    }
 
    public void enableWarpZone() {
        this.isWarpZone = true;
    }
 
    public boolean isWarpZoneEnabled() {
        return this.isWarpZone;
    }
 
    /**
     * Sets the bitmap tile the pixel data is representing.
     *
     * <p>
     * When setting the bitmap, first it must set the bitmap to something other than null. Since the bitmap variable holds an array, it takes in at least 1 bitmap pre-loaded by the Art class. Then, once the bitmap is set, it must break all the way outside of the nested switch conditions, otherwise,
     * setting bitmaps will overwrite correct data with incorrect data.
     *
     * <p>
     * If the bitmap stays null, the bitmap will then be set to "NO PNG" error bitmap, which when loaded into the game, the game will not crash, and the developers/players can tell where the bitmap loading has gone wrong.
     *
     * <p>
     * If the bitmap is an animated bitmap, the Art class will load the animated bitmap into an array. The next step would be to just pass the array to this bitmap.
     *
     * @param alpha
     *            The alpha value of the pixel data's color.
     * @param red
     *            The red value of the pixel data's color.
     * @param green
     *            The green value of the pixel data's color.
     * @param blue
     *            The blue value of the pixel data's color.
     * @return Nothing.
     * */
    public void prepareBitmap(int alpha, int red, int green, int blue) {
        switch (alpha) {
            case 0x01: // Path
                this.bitmap = new BaseBitmap[1];
                // Tile Type
                switch (red) {
                    case 0x00: // Grass Path
                        this.bitmap[0] = Art.grass;
                        break;
                    case 0x01: // Mountain Ground Path
                        this.bitmap[0] = Art.mt_ground;
                        break;
                    case 0x02: // Road Path
                        this.bitmap[0] = Art.path;
                        break;
                    case 0x03: // Hardwood Floor (Indoors)
                        this.bitmap[0] = Art.hardwood_indoors;
                        break;
                    case 0x04: // Tatami Floor Type 1 (Indoors)
                        this.bitmap[0] = Art.tatami_1_indoors;
                        break;
                    case 0x05: // Tatami Floor Type 2 (Indoors)
                        this.bitmap[0] = Art.tatami_2_indoors;
                        break;
                    default:
                        break;
                }
                switch (green) { // Area Type
                    case 0x00:
                        this.biomeBitmap = new BaseBitmap[1];
                        this.biomeBitmap[0] = Art.grass; // Forest
                        break;
                    case 0x01:
                        //TODO: Change this biome bitmap to something that represents the city even more.
                        this.biomeBitmap = new BaseBitmap[1];
                        this.biomeBitmap[0] = Art.path; //City
                    case 0x02:
                        this.biomeBitmap = new BaseBitmap[1];
                        this.biomeBitmap[0] = Art.mt_ground; // Mountain
                        break;
                    case 0x03:
                        this.biomeBitmap = new BaseBitmap[1];
                        this.biomeBitmap[0] = Art.water[0];
                        //TODO: Add more area type biome bitmaps here to the Path. (Refer to documentation.)
                    default:
                        break;
                }
                break;
            case 0x02: // Ledge
            {
                //TODO: Add biome bitmaps to ledge.
                this.bitmap = new BaseBitmap[1];
                switch (red) {
                    case 0x00: // Bottom
                        this.bitmap[0] = Art.ledge_bottom;
                        break;
                    case 0x01: // Bottom left
                        this.bitmap[0] = Art.ledge_bottom_left;
                        break;
                    case 0x02: // Right
                        this.bitmap[0] = Art.ledge_left;
                        break;
                    case 0x03: // Top Left
                        this.bitmap[0] = Art.ledge_top_left;
                        break;
                    case 0x04: // Top
                        this.bitmap[0] = Art.ledge_top;
                        break;
                    case 0x05: // Top Right
                        this.bitmap[0] = Art.ledge_top_right;
                        break;
                    case 0x06: // Left
                        this.bitmap[0] = Art.ledge_right;
                        break;
                    case 0x07: // Bottom Right
                        this.bitmap[0] = Art.ledge_bottom_right;
                        break;
                    // ---------------------------------------------------------
                    case 0x08:
                        this.bitmap[0] = Art.ledge_mt_bottom;
                        break;
                    case 0x09:
                        this.bitmap[0] = Art.ledge_mt_bottom_left;
                        break;
                    case 0x0A:
                        this.bitmap[0] = Art.ledge_mt_left;
                        break;
                    case 0x0B:
                        this.bitmap[0] = Art.ledge_mt_top_left;
                        break;
                    case 0x0C:
                        this.bitmap[0] = Art.ledge_mt_top;
                        break;
                    case 0x0D:
                        this.bitmap[0] = Art.ledge_mt_top_right;
                        break;
                    case 0x0E:
                        this.bitmap[0] = Art.ledge_mt_right;
                        break;
                    case 0x0F:
                        this.bitmap[0] = Art.ledge_mt_bottom_right;
                        break;
                    // ---------------------------------------------------------                     
                    case 0x10:
                        this.bitmap[0] = Art.ledge_inner_bottom;
                        break;
                    case 0x11:
                        this.bitmap[0] = Art.ledge_inner_bottom_left;
                        break;
                    case 0x12:
                        this.bitmap[0] = Art.ledge_inner_left;
                        break;
                    case 0x13:
                        this.bitmap[0] = Art.ledge_inner_top_left;
                        break;
                    case 0x14:
                        this.bitmap[0] = Art.ledge_inner_top;
                        break;
                    case 0x15:
                        this.bitmap[0] = Art.ledge_inner_top_right;
                        break;
                    case 0x16:
                        this.bitmap[0] = Art.ledge_inner_right;
                        break;
                    case 0x17:
                        this.bitmap[0] = Art.ledge_inner_bottom_right;
                        break;
                }
                break;
            }
            case 0x03: // Obstacles
                this.bitmap = new BaseBitmap[1];
                switch (red) {
                    case 0x00: // Small Tree
                        this.bitmap[0] = Art.smallTree;
                        break;
                    case 0x01: // Logs
                        this.bitmap[0] = Art.logs;
                        break;
                    case 0x02: // Planks
                        this.bitmap[0] = Art.planks;
                        break;
                    case 0x03: // Scaffolding (Left)
                        this.bitmap[0] = Art.scaffolding_left;
                        break;
                    case 0x04: // Scaffolding (Right)
                        this.bitmap[0] = Art.scaffolding_right;
                        break;
                    case 0x05: // Sign
                        this.bitmap[0] = Art.sign;
                        break;
                    case 0x06: // Workbench Left
                        this.bitmap[0] = Art.workbench_left;
                        break;
                    case 0x07: // Workbench Right
                        this.bitmap[0] = Art.workbench_right;
                        break;
                    case 0x08: // Dead small tree
                        this.bitmap[0] = Art.deadSmallTree;
                        break;
                }
                break;
            case 0x04: // Warp point (Refer to documentation for flaws.)
                this.bitmap = new BaseBitmap[1];
                this.bitmap[0] = Art.forestEntrance;
                break;
            case 0x05: // Area Sector Point (Refer to documentation.)
                this.bitmap = new BaseBitmap[1];
                // TODO: Add new bitmaps for connection points to make them blend in with the surroundings.
                // TODO: Create more biome bitmaps.
                this.bitmap[0] = Art.grass;
                break;
            case 0x06: // Stairs
                this.bitmap = new BaseBitmap[1];
                switch (red) {
                    case 0x00:
                        this.bitmap[0] = Art.stairs_bottom;
                        break;
                    case 0x01:
                        this.bitmap[0] = Art.stairs_left;
                        break;
                    case 0x02:
                        this.bitmap[0] = Art.stairs_top;
                        break;
                    case 0x03:
                        this.bitmap[0] = Art.stairs_right;
                        break;
                    case 0x04:
                        this.bitmap[0] = Art.stairs_mt_bottom;
                        break;
                    case 0x05:
                        this.bitmap[0] = Art.stairs_mt_left;
                        break;
                    case 0x06:
                        this.bitmap[0] = Art.stairs_mt_top;
                        break;
                    case 0x07:
                        this.bitmap[0] = Art.stairs_mt_right;
                        break;
                }
                break;
            case 0x07: { // Water
                // Always start with the first frame of any animation.
                // TODO: Add more water tiles with borders.
                // FIXME: Make the borders a bit more thicker.
                switch (red) {
                    case 0x00: // Pure water, no border.
                        this.bitmap = Art.water;
                        break;
                    case 0x01: // Left Border
                        this.bitmap = Art.water_left;
                        break;
                    case 0x02: // Top Left Border
                        this.bitmap = Art.water_top_left;
                        break;
                    case 0x03: // Top Border
                        this.bitmap = Art.water_top;
                        break;
                    case 0x04: // Top Right Border
                        this.bitmap = Art.water_top_right;
                        break;
                    case 0x05: // Right Border
                        this.bitmap = Art.water_right;
                        break;
                }
                break;
            }
            case 0x08: // House
                this.bitmap = new BaseBitmap[1];
                switch (red) { // House related tiles. Way too many to list them orderly.
                    case 0x00: // Bottom wall
                        this.bitmap[0] = Art.house_bottom;
                        break;
                    case 0x01: // Bottom left wall
                        this.bitmap[0] = Art.house_bottom_left;
                        break;
                    case 0x02: // Bottom right wall
                        this.bitmap[0] = Art.house_bottom_right;
                        break;
                    case 0x03: //Center wall
                        this.bitmap[0] = Art.house_center;
                        break;
                    case 0x04: //Center wall with windows in center
                        this.bitmap[0] = Art.house_center_windows_center;
                        break;
                    case 0x05: //Center wall with windows on left
                        this.bitmap[0] = Art.house_center_windows_left;
                        break;
                    case 0x06: //Center wall with windows on right
                        this.bitmap[0] = Art.house_center_windows_right;
                        break;
                    case 0x07: //Left wall
                        this.bitmap[0] = Art.house_left;
                        break;
                    case 0x08: //Left wall with windows on right
                        this.bitmap[0] = Art.house_left_windows_right;
                        break;
                    case 0x09: //Right wall
                        this.bitmap[0] = Art.house_right; 
                        break;
                    case 0x0A: //Right wall with windows on left
                        this.bitmap[0] = Art.house_right_windows_left;
                        break;
                    case 0x0B: // Single Roof left
                        this.bitmap[0] = Art.changeColors(Art.house_roof_left, WorldConstants.convertToAreaColor(green));
                        // this.bitmap[0] = Art.house_roof_left;
                        break;
                    case 0x0C: // Single Roof middle
                        this.bitmap[0] = Art.changeColors(Art.house_roof_middle, WorldConstants.convertToAreaColor(green));
                        break;
                    case 0x0D: // Single Roof right
                        this.bitmap[0] = Art.changeColors(Art.house_roof_right, WorldConstants.convertToAreaColor(green));
                        break;
                }
                break;
            case 0x09: // House Door
                this.bitmap = new BaseBitmap[1];
                this.bitmap[0] = Art.house_door;
                break;
            case 0x0A: // Item
                this.bitmap = new BaseBitmap[1];
                this.bitmap[0] = Art.item;
                break;
            case 0x0B: // Carpet Floor (Indoors)
                this.bitmap = new BaseBitmap[1];
                this.bitmap[0] = Art.carpet_indoors;
                this.biomeBitmap = Art.exit_arrow;
                break;
            case 0x0C: // Carpet Floors (Outdoors)
                this.bitmap = new BaseBitmap[1];
                this.bitmap[0] = Art.carpet_outdoors;
                this.biomeBitmap = Art.exit_arrow;
                break;
            case 0x0D: //Starting position when game has initialized;
                this.bitmap = new BaseBitmap[1];
                switch (red){
                    case 0x01:
                    default:
                        this.bitmap[0] = Art.grass;
                        break;
                }
                break;
            default: // Any other type of tiles.
                break;
        }
        if (this.bitmap == null) {
            this.bitmap = new BaseBitmap[1];
            this.bitmap[0] = Art.error;
        }
        if (this.biomeBitmap == null) {
            this.biomeBitmap = new BaseBitmap[1];
            this.biomeBitmap[0] = Art.grass; // By default, biome bitmap should be grass.
        }
    }
 
    /**
     * Prepares the bitmap if the color has been pre-determined.
     *
     * <p>
     * Will cause undefined behaviors if the colors have not been set yet.
     *
     * @return Nothing.
     * */
    public void prepareBitmap() {
        int alpha = (this.color >> 24) & 0xFF;
        int red = (this.color >> 16) & 0xFF;
        int green = (this.color >> 8) & 0xFF;
        int blue = this.color & 0xFF;
        this.prepareBitmap(alpha, red, green, blue);
    }
 
    /**
     * Sets the properties of a given pixel data. This is where the game gets the area's information on what the player should do and don't.
     *
     * <p>
     * Some of the features are currently unused. Especially collision detection.
     *
     * <p>
     * Only the ones that set target areas, warp zone areas, etc. are the ones being used.
     *
     * @param alpha
     *            The alpha value of the pixel data's color.
     * @param red
     *            The red value of the pixel data's color.
     * @param green
     *            The green value of the pixel data's color.
     * @param blue
     *            The blue value of the pixel data's color.
     * @return Nothing.
     * */
    public void setProperties(int alpha, int red, int green, int blue) {
        // TODO: Refactor the code to make it more readable and more modular than if...elses.
        this.targetArea = 0;
        this.isWarpZone = false;
        this.groundHeight = 0;
        switch (alpha) {
            case 0x01: // Grass
                this.groundHeight = blue;
                break;
            case 0x02: // Ledges
                break;
            case 0x03: // Obstacles
                this.facingsBlocked[0] = this.facingsBlocked[1] = this.facingsBlocked[2] = this.facingsBlocked[3] = false;
                break;
            case 0x04: // Warp Point
                // this.targetArea = red;
                // FIXME: Level Editor must set target area ID to at least 1 or above.
                this.targetArea = WorldConstants.isModsEnabled.booleanValue() ? red + 1001 : red;
                this.isWarpZone = true;
                // this.facingsBlocked[0] = this.facingsBlocked[1] = this.facingsBlocked[2] = this.facingsBlocked[3] = true;
                break;
            case 0x05: // ACP (Refer to documentation.)
                // this.targetArea = red;
                // FIXME: Level Editor must set target area ID to at least 1 or above.
                this.targetArea = WorldConstants.isModsEnabled.booleanValue() ? red + 1001 : red;
                this.targetSector = green;
                this.isWarpZone = false;
                // this.facingsBlocked[0] = this.facingsBlocked[1] = this.facingsBlocked[2] = this.facingsBlocked[3] = true;
                break;
            case 0x06: // Stairs
                break;
            case 0x07: // Water
                // TODO: Needs to do something with this. It must not block the player, however, without special boolean value, it will always block player from advancing.
                // this.facingsBlocked[0] = this.facingsBlocked[1] = this.facingsBlocked[2] = this.facingsBlocked[3] = true;
            case 0x08: // House
                // this.facingsBlocked[0] = this.facingsBlocked[1] = this.facingsBlocked[2] = this.facingsBlocked[3] = false;
                break;
            case 0x09: // House Door
                // this.targetArea = red;
                // FIXME: Level Editor must set target area ID to at least 1 or above.
                this.targetArea = WorldConstants.isModsEnabled.booleanValue() ? red + 1001 : red;
                this.isWarpZone = true;
                break;
            case 0x0A: // Item
                break;
            case 0x0B: // Carpets
            case 0x0C: // Carpets
                // FIXME: Level Editor must set target area ID to at least 1 or above.
                this.targetArea = WorldConstants.isModsEnabled.booleanValue() ? red + 1001 : red;
                break;
            case 0x0D: //Default Starting Position
                break;
            default:
                this.facingsBlocked[0] = this.facingsBlocked[1] = this.facingsBlocked[2] = this.facingsBlocked[3] = false;
                break;
        }
    }
 
    public int getColor() {
        return this.color;
    }
 
    // public int getParentAreaID() {
    // return parentArea;
    // }
 
    public int getTargetAreaID() {
        return targetArea;
    }
 
    public boolean[] isWalkThroughable() {
        return this.facingsBlocked;
    }
 
    public int getTargetSectorID() {
        return this.targetSector;
    }
 
    public void tick() {
        this.bitmapTick++;
        if (this.bitmapTick >= this.bitmap.length)
            this.bitmapTick = 0;
        this.biomeBitmapTick++;
        if (this.biomeBitmapTick >= this.biomeBitmap.length)
            this.biomeBitmapTick = 0;
    }
 
    public BaseBitmap getBitmap() {
        return this.bitmap[this.bitmapTick];
    }
 
    public int getGroundHeight() {
        return this.groundHeight;
    }
 
    public BaseBitmap getBiomeBitmap() {
        return this.biomeBitmap[this.biomeBitmapTick];
    }
}

Yes, I have. I am asking if there are better ways of implementing states that are recommended and used in Unity, and how it is used, henceforth.

In the code above, I used static instances rather than objects used to represent states. I have trouble picturing how multiple state managers on a single component work together. My game involves using 1 game object for everything (well, state managers if this is considered), and it is by design, hence it is tough to visualize.

Again, what is “better”???

Anyways, I use component based simple state machines. This is my small library of bases classes I use for it:

And then how I’d implement them for a specific case:

public class ExampleStateMotor : SPComponent
{

    #region Fields
  
    [System.NonSerialized()]
    private ComponentStateMachine<IExampleState> _states;
  
    #endregion

    #region CONSTRUCTOR

    protected override void Awake()
    {
        base.Awake();

        _states = new ComponentStateMachine<IExampleState>(this.gameObject);
        _states.StateChanged += this.OnStateChanged;
    }

    #endregion

    #regoin Properties
  
    public ComponentStateMachine<ISPAnimatorState> States { get { return _states; } }

    #endregion
  
    #region Methods
  
    private void OnStateChanged(object sender, StateChangedEventArgs<IExampleState> e)
    {
        if (e.FromState != null) e.FromState.Deactivate();
        if (e.ToState != null) e.ToState.Activate();
    }

    protected virtual void Update()
    {
        if (_states.Current != null) _states.Current.UpdateState();
    }

    #endregion
  
}

public interface IExampleState : IComponent
{

    void Activate();
    void Deactivate();
    void UpdateState();

}

public class SomeState : SPComponent, IExampleState
{

    public void Activate()
    {
        //do stuff when this state enters
    }
  
    public void Deactivate()
    {
        //do stuff when this state exits
    }
  
    public void UpdateState()
    {
        //do stuff on update
    }

}

All states should implement IExampleState, and you can get a reference to the motor to change states:

var motor = go.GetComponent<ExampleStateMotor>();
motor.States.ChangeState<SomeState>();

You have a misspelling in #region. It was spelled, “#regoin Properties”

I will see what I can do with the component-based states. If I have any other questions regarding component-based state machines, I will post in a new thread.

Alright, maybe that is way too subjective for me to define. “Commonly used/recommended” is a better way to put it?.

oh no, I mispelled region when typing code into the browser…

it’s just generic example code dude…

Note, a component based state machine isn’t necessarily the right answer. Go with what works for your situation and style!

In this code example, I couldn’t wrap my head around on the part where I need to trigger it using OnGUI() and update it using Update().

  • Do I just add new event delegates?
  • Do I just use the OnGUI() and Update() methods normally, just like how I usually do it with MonoBehaviour?
  • Do I just put the StateChangeable_EventChangeState() delegate method inside the Update() in this class?

Thanks in advance.

Sorry if it sounds rude, but I guess that you need a firmer grasp on component system inside Unity, the Monobehaviour worflow and associated events. It’s obvious that you have good experience in general programming but I recommend a step back and check manuals, tutorials from the start on using Unity.

I already watched everything. I already done multiple practice projects, as well as finished Unity game prototypes. I am just new to this Event Manager programming design, particularly. :confused: I am really close to getting it to work, is just that I need a bit more examples. That is all.

Sorry to give you such a bad impression. I didn’t really mean it.

I see. Let me explain in more details the example code that you replied with doubts.

The idea is to have the state class as a limited behaviour to be attached in each object. So, for example, a GameObject (in scene or as a prefab) named Person would have 3 attached behaviours: Transform (default), StateChangeable and Person. The person get the stateChangeable reference at the awake, and store it for later use:

public class Person : MonoBehaviour
{
    // A pointer to the state changeable component attached to this same game object
    StateChangeable StateChangeable;

    void Awake()
    {
        // Get the instance on the same object
        StateChangeable = GetComponent<StateChangeable>();
       
        // Register as observer on state change event
        StateChangeable.EventChangeState += StateChangeable_EventChangeState;
    }

    void OnDisable()    
    {
        // Unregister from state change event
        StateChangeable.EventChangeState -= StateChangeable_EventChangeState;
    }

    // This is the handler for state change event
    void StateChangeable_EventChangeState(StateChangeable stateChangeable, StateChangeable.StateType state)
    {
        // Just a example on the action to this new state
        if (state == StateType.Attackable)
            Debug.Log("Now I can attack!");
    }
}

This is all. Any change on the state event on this object will be captured by this person script using it in component way.
For example, you could have another script attached that changes the state on the StateChangeable in this object with information from a remote source in a multiplayer environment.

Sounds like I don’t need to implement it along with Update() or OnGUI() in the EventManager class. But my main doubts are still not answered, which is how you would trigger the events. Does the event needs to be triggered one time for that state, or only when the state changes? The more I write this event handling code, the more I felt I am reinventing the wheel, since all of these can be triggered with just Input.GetMouse/Down/Up() methods.

    //Events
    //Events must be void and have 0 or 1 parameter.
    public delegate void OnLeftClickEvent(GameObject go);
    public event OnLeftClickEvent OnLeftClickUp;
    public event OnLeftClickEvent OnLeftClickHold;
    public event OnLeftClickEvent OnLeftClickDown;

    public void Update() {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, 100f)) {
            if (Input.GetMouseButtonUp(0)){
                if (OnLeftClickUp != null) {
                    OnLeftClickUp(hit.transform.gameObject);
                }
            }
            else if (Input.GetMouseButtonDown(0)){
                if (OnLeftClickDown != null) {
                    OnLeftClickDown(hit.transform.gameObject);
                }
            }
            else if (Input.GetMouseButton(0)) {
                if (OnLeftClickHold != null) {
                    OnLeftClickHold(hit.transform.gameObject);
                }
            }
        }
    }
using UnityEngine;
using System.Collections;

public class SelectState : MonoBehaviour {
    public void Awake() {
        EventManager.Instance.OnLeftClickUp += SelectState_OnLeftClickUp;
        EventManager.Instance.OnLeftClickDown += SelectState_OnLeftClickDown;
        EventManager.Instance.OnLeftClickHold += SelectState_OnLeftClickHold;
    }

    public void OnDisable() {
        EventManager.Instance.OnLeftClickUp -= SelectState_OnLeftClickUp;
        EventManager.Instance.OnLeftClickDown -= SelectState_OnLeftClickDown;
        EventManager.Instance.OnLeftClickHold -= SelectState_OnLeftClickHold;
    }

    public void SelectState_OnLeftClickUp(GameObject go) {
        if (go != null && go.Equals(this.gameObject)) {
            //Do action after releasing mouse button.
        }
    }

    public void SelectState_OnLeftClickDown(GameObject go) {
        if (go != null && go.Equals(this.gameObject)) {
            //Do action after clicking mouse button.
        }
    }

    public void SelectState_OnLeftClickHold(GameObject go) {
        if (go != null && go.Equals(this.gameObject)) {
            //Do action after holding mouse button.
        }
    }
}