GameObjects sharing a script are also sharing private variables

Originally posted here but with 3,000 views I don’t have an answer and this is preventing me from proceeding further.

Suppose I have two nearly identical game objects, RedObject and BlueObject, they share the same script because their behaviors are similar and to save on creating scripts and maintanance I’d rather have one script defining an arbitrary number of ‘behaviours’ and then have the object switch between them or be set to a behavour when instantiated.

So two objects, and I have two scripts ObjectStateController:

   private float elapsedTime = 0.0f;
    //private float elapsedTime2 = 0.0f;
    private float timer = 0.25f;
    public bool ticker = false;
    private bool myTicker = false;

    public enum objectStates
    {
        idle = 0,
        move2next, // easier debugging omfg
        roll,
        firing,
        kill,
        //patterns
        pattern0,
        //homingpatterns
        homing0,
        _stateCount
    }

    public delegate void FairyStateHandler(FairyStateController.objectStates newState);
    public static event FairyStateHandler onStateChange;
    // Use this for initialization
    void Start()
    {
        elapsedTime = Time.time + timer;
        onStateChange(FairyStateController.objectStates.idle);
        myTicker = ticker;
        Debug.Log("myTicker: " + ticker);
    }

    public FairyStateController()
    {
    }

    // Update is called once per frame
    void LateUpdate()
    {
        //Debug.Log("Init Pattern: " + ticker);
        if (myTicker)
        {
                onStateChange(FairyStateController.objectStates.homing0);
                Debug.Log("True");
            if ( elapsedTime < Time.time)
            {

                //elapsedTime = Time.time + timer;
            }
            else
            {
                //onStateChange(FairyStateController.objectStates.idle);
            }
        }
        else
        {
                onStateChange(FairyStateController.objectStates.firing);
                Debug.Log("False");
            if (elapsedTime < Time.time)
            {

                //elapsedTime = Time.time + timer;
            }
            else
            {
                //onStateChange(FairyStateController.objectStates.idle);
            }

        }

Which is my AI state file, it switches between which ‘pattern’/‘behavour’ the object should be executing, and then signals ObjectStateListener as to what to do, I’m 99% sure the problem is either related to the Controller script for some limitation with Unity but I’ll post anyways:

    public float objectTurnSpeed = 1.05f;
    public float objectMoveSpeed = 5.05f;

    public GameObject dispenserObjectBulletPrefab = null;
    public Transform turret = null;
    public Transform bulletSpawnTransform;

    protected Transform currTarPos;
    private Transform nextPoint;

    public GameObject target;
    private List<GameObject> myList;
    private int counter = 0;

    public FairyStateController.objectStates currentState = FairyStateController.objectStates.idle;
    public TakeDamageFromPlayerBullet bulletColliderListener = null;
    private Animator fairyAnimator = null;

    // Use this for initialization
    void Start()
    {
        fairyAnimator = GetComponent<Animator>();
    }

    void initListPoints()
    {
        var array = GameObject.FindGameObjectsWithTag("NavPoint");
        myList = new List<GameObject>();

        for (int i = 0; i < array.Length; i++)
        {
            myList.Add(array[i]);
        }
        //Debug.Log("ListSize: " + myList.Count);
    }

    void OnEnable()
    {
        //target = GameObject.FindGameObjectWithTag("MarisaHitBox");
        FairyStateController.onStateChange += onStateChange;
        bulletColliderListener.hitByBullet += hitByPlayerBullet;

        //currTarPos = target.transform;
        //initListPoints();
        //Debug.Log(counter);
        //nextPoint = myList[counter].transform;
        //counter++;

    }

    void OnDisable()
    {
        FairyStateController.onStateChange -= onStateChange;
        bulletColliderListener.hitByBullet -= hitByPlayerBullet;

    }

    // onStateChange is called whenever we make a change to the player's state
    // from anywhere within the game's code.
    public void onStateChange(FairyStateController.objectStates newState)
    {
        if (newState == currentState)
        {
            return;
        }

        // Check if the current state is allowed to transition into this state. If it's not, abort.
        if (!checkForValidStatePair(newState))
            return;
        switch (newState)
        {
            case FairyStateController.objectStates.idle:
                fairyAnimator.SetBool("attacking", false);
                //moveThroughPoints();
                //onStateChange(FairyStateController.objectStates.move2next);
                break;
            case FairyStateController.objectStates.firing:
                fairyAnimator.SetBool("attacking", true);
                for (int i = 0; i < 24; i++)
                {
                    //fireBullet((i * 15.0f));
                }

                break;
            case FairyStateController.objectStates.pattern0:
                fairyAnimator.SetBool("attacking", true);
                //pattern0();
                break;
            case FairyStateController.objectStates.homing0:
                fairyAnimator.SetBool("attacking", true);
                //Debug.Log("Debug");
                //homingBullet();
                break;
        }
        // And finally, assign the new state to the player object
        currentState = newState;
    }

    bool checkForValidStatePair(FairyStateController.objectStates newState)
    {
        bool returnVal = false;

        // Compare the current against the new desired state.
        switch (currentState)
        {
            case FairyStateController.objectStates.homing0:
                // Any state can take over from idle.
                returnVal = true;
                break;
            case FairyStateController.objectStates.idle:
                // Any state can take over from idle.
                returnVal = true;
                break;
            case FairyStateController.objectStates.pattern0:
                returnVal = true;
                break;
            case FairyStateController.objectStates.move2next:
                returnVal = true;
                break;
            case FairyStateController.objectStates.kill:
            // The only state that can take over from kill is resurrect
            case FairyStateController.objectStates.firing:
                returnVal = true;
                break;
        }
        return returnVal;

    }

    void onStateCycle()
    {
        switch (currentState)
        {
            case FairyStateController.objectStates.move2next:
                //moveToNextNavPoint2(transform.position, nextPoint.position);
                break;
            case FairyStateController.objectStates.idle:
                {
                    //moveThroughPoints();
                    //onStateChange(FairyStateController.objectStates.move2next);
                    break;
                }
            case FairyStateController.objectStates.pattern0:
                transform.Translate(Vector3.up * objectMoveSpeed * Time.deltaTime);
                break;
            case FairyStateController.objectStates.homing0:
                transform.Translate(Vector3.up * objectMoveSpeed * Time.deltaTime);
                break;
            case FairyStateController.objectStates.kill:
                //onStateChange(FairyStateController.playerStates.resurrect);
                break;
            case FairyStateController.objectStates.firing:
                transform.Translate(-Vector3.up * objectMoveSpeed * Time.deltaTime);
                break;

            default:
                break;
        }

    }

    void homingBullet()
    {
        //Debug.Log("Am I here");
        //Debug.Log("Bullet firing...");
        GameObject newBullet = (GameObject)Instantiate(dispenserObjectBulletPrefab);

        //bulletSpawnTransform.transform.rotation = Quaternion.LookRotation(currTarPos.position - newBullet.transform.position, Vector3.up);
        bulletSpawnTransform.transform.LookAt(currTarPos);
        //turret.transform.LookAt(currTarPos);


        newBullet.transform.position = bulletSpawnTransform.position;
        newBullet.transform.rotation = bulletSpawnTransform.rotation;

        //newBullet.transform.rotation = Quaternion.Slerp(newBullet.transform.rotation, Quaternion.LookRotation(currTarPos.position - newBullet.transform.position), 100.0f*Time.deltaTime);
        newBullet.transform.rotation = Quaternion.LookRotation(currTarPos.position - newBullet.transform.position);

        BulletController bullCon = newBullet.GetComponent<BulletController>();
        bullCon.enemyObject = GameObject.FindGameObjectWithTag("CrystalOne");
        bullCon.launchBullet();
        onStateChange(currentState);
    }

    void fireBullet()
    {
        GameObject newBullet = (GameObject)Instantiate(dispenserObjectBulletPrefab);
        newBullet.transform.position = bulletSpawnTransform.position;
        newBullet.transform.rotation = bulletSpawnTransform.rotation;
        BulletController bullCon = newBullet.GetComponent<BulletController>();
        bullCon.enemyObject = GameObject.FindGameObjectWithTag("TestDispenser");
        bullCon.launchBullet();
        onStateChange(currentState);
    }

    void fireBullet(float offsetRot)
    {
        GameObject newBullet = (GameObject)Instantiate(dispenserObjectBulletPrefab);
        newBullet.transform.position = bulletSpawnTransform.position;
        newBullet.transform.rotation = bulletSpawnTransform.rotation;

        Vector3 newRotation = newBullet.transform.rotation.eulerAngles;
        newRotation.y += offsetRot;
        newBullet.transform.rotation = Quaternion.Euler(newRotation);

        BulletController bullCon = newBullet.GetComponent<BulletController>();
        bullCon.enemyObject = GameObject.FindGameObjectWithTag("TestDispenser");
        bullCon.launchBullet();
        onStateChange(currentState);
    }

    void pattern0()
    {
        Transform myRot = bulletSpawnTransform;
        myRot.LookAt(currTarPos);

        Vector3 newRotation0 = myRot.transform.rotation.eulerAngles;
        newRotation0.y -= 45;
        myRot.rotation = Quaternion.Euler(newRotation0);

        for (int i = 0; i < 18; i++)
        {
            GameObject newBullet = (GameObject)Instantiate(dispenserObjectBulletPrefab);
            newBullet.transform.position = myRot.position;
            newBullet.transform.rotation = myRot.rotation;

            Vector3 newRotation = newBullet.transform.rotation.eulerAngles;
            newRotation.y += (5 * i);
            newBullet.transform.rotation = Quaternion.Euler(newRotation);

            BulletController bullCon = newBullet.GetComponent<BulletController>();
            bullCon.enemyObject = GameObject.FindGameObjectWithTag("CrystalOne");
            bullCon.launchBullet();
            onStateChange(currentState);

        }

    }

    void moveToNextNavPoint(Vector3 start, Vector3 end)
    {
        /*
* Lerp Dashing Left/Right
*/
        //Transform myPosition = transform;
        //Vector3 startPos = myPosition.position;
        //transform.Translate(new Vector3((playerDashSpeed * 1.0f) * Time.deltaTime, 0.0f, 0.0f));
        //myPosition.Translate(new Vector3(0.0f, 0.0f, objectMoveSpeed));

        //Vector3 endPos = myPosition.position;

        float t = 0.0f;
        float time = 1.0f;
        while (t < 0.00001f)
        {
            t += Time.deltaTime / time;
            transform.position = Vector3.Lerp(start, end, t);
        }
    }


    void moveToNextNavPoint2(Vector3 start, Vector3 end)
    {
        if (myList.Count < 7)
        {
            //Debug.Log(myList.Count);
        }

        transform.LookAt(end);

        if (Vector3.Distance(transform.position, end) >= 0.5f)
        {
            transform.position += transform.forward * objectMoveSpeed * Time.deltaTime;

            if (Vector3.Distance(transform.position, end) <= 0.5)
            {
                onStateChange(FairyStateController.objectStates.idle);
            }
        }

    }

    void moveThroughPoints()
    {
        //Debug.Break();
        if (counter < myList.Count)
        {
            //Debug.Log("Movin' to next point: "+counter);
            nextPoint = myList[counter].transform;
            counter++;
        }
        else
        {
            counter = 0;
            nextPoint = myList[counter].transform;
            counter++;
        }

    }
    // Update is called once per frame
    void Update()
    {
        currTarPos = target.transform;
        onStateCycle();

    }

    public void hitByPlayerBullet()
    {
        Destroy(gameObject, 0.1f);
    }

Pretty standard.

The problem is that, if I set one object to be true in the inspector (trigger is checked) and the other is false, they both insist on doing the same action, whichever is the “last” is becomes the action both objects are to execute, the debug output will alternate true/false but both execute true or false depending in which order they are set to true/false.

Even if I use random numbers set to private, they both do the same action.

So how do I get it so that if I have two objects sharing the same two scripts they will execute two different actions at the same time?

Why not use interfaces in two different scripts?

What if I have 10 objects? Or 100? The two objects is just what I’m using for testing, I want there to be the possibility of more.

Edit: Is it possible that using Delegates/Events is limited? Whatever event is called is automatically that event for all listeners?

simple: you make an interface, and apply that interface to each enemy type. When referencing an enemy, no matter what type, you refer to it with the interface name and not with the class name. This semplify all the system.

The two scripts on the open post not makes difference of enemy type, thats why if one is enabled and another not, the two objects will move.

First lets step back a second here, are you saying to do this instead of using event listeners/delegates? Because I am increasingly convinced the problem lies there, that whichever event is triggered last becomes the event for all listening objects.

And I’d like to determine what the problem actually is first before defining solutions.

im saying: all the enemyes have similar funtions, then, include each of this function in an interface and differtiate each enemy.

This currently doesn’t solve my problem, because if I call them using my state controller they will all still do the same action regardless of differentiation. Suppose all objects have a moveUp and moveDown; this is NOT going to be different between objects, but I call one object to moveUp and then the other to moveDown, they will BOTH moveDown.

I’m trying to confirm if the delegates are the problem, because if they are then using an interface won’t work if they’re still invoked through my state controller.

I never used delegates in my projects, i barely know how they works. So i think i can’t help you…
Just readed the MSDN documentation over delegates, and yes, they are the problem. Calling a delegate can cause the script being executed on the disabled object.

I understand you’re trying to help, so lets assume then that delegates are the problem; both listeners will do the same action of any ONE broadcaster so I need a different solution.

I have two objects, I would like to use just one AI file to define their behavour, I looked at a tutorial and understand interfaces are useful for defining functionality common to all objects (such as is damageable), but what specifically to behavour is interfaces you would feel is useful here?

I want something like this:

Checks If Player Is Near > Shoot Enemy if true.
Move in a circle around the level.
Stop moving if enemy is near.
If my colour is red shoot a wave.
If my colour is blue shoot a line.

Are interfaces useful for AI scripting?

what about simply use a boolean or an enum to define the type of weapon armed, because the code not change you not have to code anything (and you can change weapons at runtime), when you will have more than 2 type of weapons use a case to shoot the right bullet. No need to use delegates. Rember the code is executed per objects. Checking the distance to the player in two different objcets with same script will return a different value. Because they are two different instances of 1 class. This is the basic of the OOP programming. If used well interfaces can be used everywhere.

I was using delegates because I am using a state machine to decouple agent AI behavour and increase reuseability.

Why is your event static? You’ll also typically send 2 arguments to the delegate handling the event - the sender and an EventArgs instance. Lastly, your naming convention is a little confusing. The event should just be called StateChanged. If you wanted something like OnStateChanged then it should be a method that calls StateChanged. Something like:

public delegate StateChangeHandler(object sender, EventArgs e);
public event StateChangeHandler StateChanged;
void OnStateChanged(EventArgs e)
{
    if (StateChanged != null)
        StateChanged(this, e);
}

// hook event somewhere else
var controller = someObject.GetComponent<ObjectStateController>();
controller.StateChanged += DoStuff;

void DoStuff(object sender, EventArgs e)
{
    string senderName = ((ObjectStateController)sender).gameObject.name;
    Debug.Log("event triggered from " + senderName);
}
1 Like

Static events can cause some weird behavior. My guess is that’s the root of the code issues you’re seeing. See here for more info.

I was following the 2D platformer tutorial from the Unity book Unity 2D Development which was intended for a PlayerStateContoller - PlayerStateListener dynamic which I hoped to repurpose for n many AI’s, since I felt the concept was transferable.

Would making the event non-static make it so it only effects the currently attached object?

e: Sadly I am at work so I can’t test it out until later tonight. :frowning:

That’s certainly where I would start. It sounds like you don’t want it to be static anyway.

Yeah I was just following the book, I’m still a little new to C# and using listeners I only fiddled with in C++ with a class I had to make from scratch.

Any chance can you post the C# version of that code? I tried it myself and I’m getting errors trying to get controller to be instantiated:

  public FairyStateController controller;
  
  controller = gameObject.GetComponent<FairyStateController>();

Can’t get controller to be recognized by intellisense.

“// hook event somewhere else” is what I put in my listener script right? So far what’s problematic is that all event tutorials seem to use static.

edit: “public delegate StateChangeHandler(object sender, EventArgs e);” doesn’t work either, are you missing void?

I figured it out, I had to put

controller = gameObject.GetComponent<FairyStateController>();

in OnEnable, because just because C3 isn’t C++ doesn’t mean classes work differently!

And then change

FairyStateController.onStateChange += onStateChange;

to:

controller.onStateChange += onStateChange;

Now I realize EventArgs just refers to whatever we’d think to pass to the event handler but I don’t really see any utility for that at this time that outweighs the effort and time in the code rewriting I’d need to sort through; deadlines sadly short circuit a lot of optimizations I’d otherwise consider.