0

I am currently working with C# using the Unity3D engine and have come upon the following problem:

I created a class that has two private references to instances of another class which it has to access. Once I create multiple instances of the class and set the references I found out that all instances were using the same variable. I realized this as I was destroying an instance and just before that set the two variables holding the references to null. Immediately after doing that all other instances were throwing NullReferenceExceptions because they were still trying to access the references. The referenced objects are fine, other scripts can still access them.

Here is some pseudo code illustrating the structure:

public class Character
{
    // Character data
}

public class StatusEffect
{
    private Character target;
    private Character originator;

    public void Init(Character _Target, Character _Originator)
    {
        target = _Target;
        originator = _Originator;
    }

    public void Destroy()
    {
        target = null;
        originator = null;
    }
}

In the program it would be called like this:

StatusEffect effect = new StatusEffect();
effect.Init(player1, player2);

// Time goes by

effect.Destroy();

After calling Destroy() every StatusEffect's two references will be null.

This is not only an issue when destroying StatusEffects, but also when creating new ones. As soon as I touch the references from within a new instance all StatusEffects will reference the two Characters specified by the new StatusEffect.

I do not understand why or how I can fix this issue. Can someone enlighten me on this matter?

Cheers, Valtaroth

EDIT:

Here is the real code as requested: I have a container class holding several StatusEffects. As soon as it starts, it initializes all of them.

public class CElementTag
{
    // ..Other data..

    public float f_Duration; // Set in the editor

    private CGladiator gl_target;
    private CGladiator gl_originator;
    private float f_currentDuration;

    public CStatusEffect[] ar_statusEffects;

    // Starts the effect of the element tag
    public void StartEffect(CGladiator _Originator, CGladiator _Target)
    {
        gl_originator = _Originator;
        gl_target = _Target;
        f_currentDuration = f_Duration;

        for(int i = 0; i < ar_statusEffects.Length; i++)
            ar_statusEffects[i].Initialize(gl_originator, gl_target);
    }

    // Ends the effect of the element tag
    public void EndEffect()
    {
        for(int i = 0; i < ar_statusEffects.Length; i++)
        {
            if(ar_statusEffects[i] != null)
                ar_statusEffects[i].Destroy();
        }
    }

    // Called every update, returns true if the tag can be destroyed
    public bool ActivateEffect()
    {
        f_currentDuration -= Time.deltaTime;

        if(f_currentDuration <= 0.0f)
        {
            EndEffect();
            return true;
        }

        for(int i = 0; i < ar_statusEffects.Length; i++)
        {
            if(ar_statusEffects[i] != null && ar_statusEffects[i].Update())
                RemoveStatusEffect(i);
        }

        return false;
    }

    // Removes expired status effects
    private void RemoveStatusEffect(int _Index)
    {
        // Call destroy method
        ar_statusEffects[_Index].Destroy();

        // Remove effect from array
        for(int i = _Index; i < ar_statusEffects.Length - 1; i++)
            ar_statusEffects[i] = ar_statusEffects[i+1];

        ar_statusEffects[ar_statusEffects.Length - 1] = null;
}
}

The actual StatusEffect class is holding the two references as well as some other data it needs to work. It has virtual methods because there are some classes inheriting from it.

public class CStatusEffect
{
    // ..Necessary data..

    // References
    protected CGladiator gl_target;
    protected CGladiator gl_originator;

    virtual public void Initialize(CGladiator _Target, CGladiator _Originator)
    {
        gl_target = _Target;
        gl_originator = _Originator;

        // ..Initialize other necessary stuff..
    }

    virtual public void Destroy()
    {
        gl_target = null;
        gl_originator = null;

        // ..Tidy up other data..
    }

    virtual public bool Update()
    {
        // ..Modifying data of gl_target and gl_originator..
        // Returns true as soon as the effect is supposed to end.
    }
}

That should be all the relevant code concerning this problem.

EDIT2

@KeithPayne I have a static array of ElementTags defined in the editor and saved to xml. At the beginning of the program the static array is loading the xml and stores all element tags. When creating a new element tag to use I utilize this constructor:

// Receives a static tag as parameter
public CElementTag(CElementTag _Tag)
{
    i_ID = _Tag.i_ID;
    str_Name = _Tag.str_Name;
    enum_Type = _Tag.enum_Type;
    f_Duration = _Tag.f_Duration;

    ar_statusEffects = new CStatusEffect[_Tag.ar_statusEffects.Length];
    Array.Copy(_Tag.ar_statusEffects, ar_statusEffects, _Tag.ar_statusEffects.Length);
}

Do I have to use a different method to copy the array to the new tag? I thought Array.Copy would make a deep copy of the source array and stored it in the destination array. If it is in fact making a shallow copy, I understand where the problem is coming from now.

Valtaroth
  • 65
  • 6
  • Can you post some code to demonstrate the problem? From your description and the code sample, the problem that you see is not possible. Also, how are you determining that the private members of your objects are all being affected. – Keith Payne Sep 26 '13 at 13:29
  • @KeithPayne I have every StatusEffect output his target. As soon as I create a second effect with a new target, the first effect changes his target to the new one as well. – Valtaroth Sep 26 '13 at 14:33
  • The one thing that could still be shown is how the elements of `ar_statusEffect[]` are set. I suspect that each array element is referencing the same `CStatusEffect` object. – Keith Payne Sep 26 '13 at 15:11
  • @KeithPayne I edited my post to include that as well. It might actually have uncovered the source of my problem. – Valtaroth Sep 27 '13 at 06:08

4 Answers4

1

Because you're passing in references to the objects via your Init() method, you're not actually "copying" the objects, just maintaining a reference to the same underlying objects in memory.

If you have multiple players with the same references to the same underlying objects, then changes made by player 1 will effect the objects being used by player 2.

Having said all that, you're not actually disposing the objects in your Destory method. Just setting the local instance references to Null which shouldn't affect any other instances of StatusEffects. Are you sure something else isn't disposing the objects, or that you haven't properly init'd your other instances.

If you do want to take a full copy of the passed in objects, take a look at the ICloneable interface. It looks like you want to pass in a copy of the objects into each Player.

public class Character : ICloneable
{
    // Character data

    //Implement Clone Method
}

public class StatusEffect
{
    private Character target;
    private Character originator;

    public void Init(Character _Target, Character _Originator)
    {
        target = _Target.Clone()
        originator = _Originator.Clone();
    }
Eoin Campbell
  • 43,500
  • 17
  • 101
  • 157
  • I do not want to actually clone the Character objects, I simply want references to the correct characters because the effect needs to access some of their variables and methods. The problem is that they are all using the same references although the variables themselves are not static and should not be shared across instances. – Valtaroth Sep 26 '13 at 13:36
  • in that case, what you are doing should work fine, because you're only setting to null the local variables, which wouldn't affect the underlying objects. so there's seomthing else going on. – Eoin Campbell Sep 26 '13 at 13:37
  • 1
    @Liam I was thinking of references as somewhat like pointers in C++ where setting a pointer to null would only affect the pointer itself, not the actual data it was pointing at. Is it not possible to assign different references to the member variables of each instance? As far as I know, they should not affect each other the way they do. – Valtaroth Sep 26 '13 at 14:21
  • @Liam - I'm pretty sure that's not the case. What you're setting to null is the reference. Essentially breaking the link to that object in memory. see here for a simple linqpad test - http://pastebin.com/EFgsGrS3 – Eoin Campbell Sep 26 '13 at 14:32
  • Exactly. Which is why I do not understand how the variables can affect each other. – Valtaroth Sep 26 '13 at 14:47
1

From Array.Copy Method (Array, Array, Int32):

If sourceArray and destinationArray are both reference-type arrays or are both arrays of type Object, a shallow copy is performed. A shallow copy of an Array is a new Array containing references to the same elements as the original Array. The elements themselves or anything referenced by the elements are not copied. In contrast, a deep copy of an Array copies the elements and everything directly or indirectly referenced by the elements.

Consider this fluent version of the StatusEffect class and its usage below:

public class StatusEffect
{
    public Character Target { get; private set; }
    public Character Originator { get; private set; }

    public StatusEffect Init(Character target, Character originator)
    {
        Target = target.Clone()
        Originator = originator.Clone();
        return this;
    }
//...
}    

public CElementTag(CElementTag _Tag)
    {
        i_ID = _Tag.i_ID;
        str_Name = _Tag.str_Name;
        enum_Type = _Tag.enum_Type;
        f_Duration = _Tag.f_Duration;

        ar_statusEffects = _Tag.ar_statusEffects.Select(eff => 
            new StatusEffect().Init(eff.Target, eff.Originator)).ToArray();

        // ar_statusEffects = new CStatusEffect[_Tag.ar_statusEffects.Length];
        // Array.Copy(_Tag.ar_statusEffects, ar_statusEffects, _Tag.ar_statusEffects.Length);


    }
Keith Payne
  • 3,002
  • 16
  • 30
0

The fields aren't shared(static) among other instances. So calling target = null; in Destroy() won't affect other instances.

StatusEffect effect1 = new StatusEffect();
effect1.Init(player1, player2);

StatusEffect effect2 = new StatusEffect();
effect2.Init(player1, player2);

// Time goes by

effect2.Destroy();

// Some more time goes by

// accessing effect1.target won't give a `NullReferenceException` here unless player1 was null before passed to the init.

effect1.Destroy();

I think you did forget the Init(..) on the other instances. Every time you create an instance of StatusEffect, you need to call Init(...).


Update:

This line will clear the reference to the effect, but you never recreate it:

ar_statusEffects[ar_statusEffects.Length - 1] = null;

so the next time you call ar_statusEffects[x].Update() or Initialize() etc it will throw a NullReferenceException

If you want to clear out effects within you array, you could create an Enable bool in the effect, this way you only have to set/reset it.

    for(int i = 0; i < ar_statusEffects.Length; i++)
        if(ar_statusEffects[i].IsEnabled)
            ar_statusEffects[i].Update();

Why don't you use a List instead? Arrays will be faster as long you don't have to shuffle in it. (like circulair buffers etc)

Jeroen van Langen
  • 21,446
  • 3
  • 42
  • 57
  • I did not forget to call Init(..) on other instances, as it changes the variables used by every instance if I do so. Lets say I create an effect with target "player1" and after that I create one with target "player2", both instances will have the target "player2". – Valtaroth Sep 26 '13 at 13:34
  • In normal conditions, this isn't possible, I think the code you posted isn't representing the problem. Can you post the original code here? – Jeroen van Langen Sep 26 '13 at 13:37
  • Before calling any methods of a StatusEffect I check whether it does equal null or not. The null reference exceptions do not occur in these lines of the code. – Valtaroth Sep 26 '13 at 14:25
  • I don't see a check here: `for(int i = 0; i < ar_statusEffects.Length; i++) ar_statusEffects[i].Initialize(gl_originator, gl_target);` – Jeroen van Langen Sep 26 '13 at 14:27
  • You are right, sorry. But I can be certain that no status effect is null at this point in time, because I load the effects out of a file shortly before that and check whether they are null there. But I can add an additional check, just to be sure. I have not considered using a List for this, as I do not need to add elements at runtime and considering the size (under 5 elements) and lifetime (few seconds to a minute) of the array, the reserved memory should not be a problem. – Valtaroth Sep 26 '13 at 14:41
0

Thanks to Keith Payne I figured out where the problem was. I was creating a deep copy of CElementTag, but not of my ar_statusEffects array. I wrongly assumed Array.Copy was creating a deep copy of an array when it actually was not.

I implemented the IClonable interface for my CStatusEffect and use the Clone() method to create a true deep copy for each member of the static array and add it to the new tags ar_statusEffects array. This way I have seperate instances of the effects instead of references to the same static effect.

Thanks to everyone, especially Keith Payne, for their help and support!

Valtaroth
  • 65
  • 6