3D array is referenced by two separate game objects

I have two gameobjects, lets call them 0 and 1. 0 and 1 attack each other in an attack, counterattack pattern, once per second. I use 3D arrays to keep track of how many healthy units they have in each army, since there are many types and tiers of units. The one that is important here is numberOfHealthUnits. I am using a MapUnit class to hold this 3d array. The gameObject unit is instantiated and there are two of them on the map. Both contain the MapUnit class.

The problem, is when I have them attack, both in the attack (which 0 → 1 means 1 takes unit damage), then counterattack (1 → 0 means 0 takes damage), the units that are killed are both subtracted from 1 3D array. In the Debug below, you can see how it goes from 1000 → 816 → 678 when it should be (1000 → 816) && (1000 → 862).

My first thought is that the 3D array must be treated as global for some reason, but it is not static. I also can’t find much unity docs on 3D arrays anyways. If this is true, I could make it a 4D array with an index for each unit, or instead manually variable out each class, which would be a big pain.

Any thoughts? Thanks.

Code:

public class MapUnit : MonoBehaviour


    public int[,,] numberOfHealthUnits = new int[4,5,1] {
        //Number of Units
        {{1000},{0},{0},{0},{0}}, //Infantry, {T1 - T5}
        {{0},{0},{0},{0},{0}}, //Artillery, {T1 - T5}
        {{0},{0},{0},{0},{0}}, //Armored Vehicles, {T1 - T5}
        {{0},{0},{0},{0},{0}} //Transport, {T1 - T5}
    };

private void AttackTakeDamage(float avgEnemyAtt, float totalDMGTook) {
            Debug.Log("Target: " + this.gameObject.name);
            var combatEnvironment = 0; //0 = field, 1 = city

            var totalHealthyUnits = 0;
            for(int a = 0; a < numberOfHealthUnits.GetLength(0); a++) {
                for(int b = 0; b < numberOfHealthUnits.GetLength(1); b++) {
                    totalHealthyUnits += numberOfHealthUnits[a, b, 0];
                }
            }

            var ratioUnitsToTotal = 0f;
            var ratioOfDefenseToEnemyAttack = 0f;

            var unitsSurvived = false;
            for(int a = 0; a < numberOfHealthUnits.GetLength(0); a++) {
                for(int b = 0; b < numberOfHealthUnits.GetLength(1); b++) {
                    if(numberOfHealthUnits[a, b, 0] > 0) {
                        ratioUnitsToTotal = numberOfHealthUnits[a, b, 0] / totalHealthyUnits;
                        ratioOfDefenseToEnemyAttack = avgEnemyAtt / AM.GetUnitDefense(a, b, 1);
                        Debug.Log("Hit: DMG Took: " + totalDMGTook + " R Units: " + ratioUnitsToTotal + " R Def: " + ratioOfDefenseToEnemyAttack + " UHealth: " + AM.GetUnitHealth(a, b, 1));
                        var totalHitUnits = Mathf.Ceil((totalDMGTook * ratioUnitsToTotal * ratioOfDefenseToEnemyAttack) / AM.GetUnitHealth(a, b, 1));
                        Debug.Log("Hit: Type: " + a + " Tier: " + b + " Num: " + totalHitUnits);
                        Debug.Log("B4: " + numberOfHealthUnits[a, b, 0]);

                        numberOfHealthUnits[a, b, 0] = numberOfHealthUnits[a, b, 0] - (int)totalHitUnits;
                        Debug.Log("After: " + numberOfHealthUnits[a, b, 0]);
                        if(combatEnvironment == 0) {
                            numberOfSlightlyWoundedUnits[a, b, 0] = numberOfSlightlyWoundedUnits[a, b, 0] + (int)totalHitUnits;
                        }

                        if(numberOfHealthUnits[a, b, 0] > 0)
                            unitsSurvived = true;
                    }
                }
            }

            if(!unitsSurvived) {
                DefeatUnit();
            }
        }

Debug:

Attack!
Friendly Units:
Healthy:
Type: 0Tier: 0Num: 1000

Target: Unit Preset(Clone)0
Hit: DMG Took: 23000 R Units: 1 R Def: 0.9583333 UHealth: 120
Hit: Type: 0 Tier: 0 Num: 184
B4: 1000
After: 816

Target: Unit Preset(Clone)1
Hit: DMG Took: 17250 R Units: 1 R Def: 0.9583333 UHealth: 120
Hit: Type: 0 Tier: 0 Num: 138
B4: 816
After: 678

As you can see from the debug, 0 and 1 both call AttackTakeDamage() when damage was done to them. The call is local to that gameObject, as by the printed name, however the damage seems to be both to the same 3D array (numberOfHealthUnits). I want to reference the specific array for that specific gameObject.

Anytime I have a data structure that is 2D in shape (or higher) I wrap it up in an API and access it exclusively through that API, which lets me centralize the ugly details of its storage, eg., treat the x,y,z,w as true N-dimensional indices, or alternately as a key into a sparse dictionary, rather than splattering [z][y][x]-style dereferences all over my codebase.

That said, what you have above feels like it shouldn’t be a 3D array but rather a meaningful data structure tailored to what data you are representing. The choice of how to structure this is a fairly well-traveled road as far as gamedev goes, with things like ScriptableObjects used for pre-authored data, and other structures used for instance-as-you-play data.

Here are some potentially-relevant resources:

ScriptableObject usage in RPGs:

Usage as a shared common data container:

As for combat specifically, here’s some more discussions:

1 Like

Thanks. I’ll take a look at this when I get home.

Yes, this makes more sense. I’ve used scriptable objects before, but just thought of them as a more ‘expandable’ solution. This makes sense though. I do, as you said, have additional data in other 3D arrays that expands on what is here, but it would be easier to reference as a set of objects. I’ll think about how to incorporate that properly.

Also, thanks for including an example for combat. I’ve thought about it myself, but this will add to my thoughts.

1 Like

Also, while you’re doing data structure decisions, make sure you consider if this game will one day want its data be serialized and saved / loaded.

Humorously I have yet another video by the same guy, along with all kinds of handy info about that topic at the bottom here!!

Load/Save steps:

An excellent discussion of loading/saving in Unity3D by Xarbrough:

And another excellent set of notes and considerations by karliss_coldwild:

Loading/Saving ScriptableObjects by a proxy identifier such as name:

When loading, you can never re-create a MonoBehaviour or ScriptableObject instance directly from JSON. Save data needs to be all entirely plain C# data with no Unity objects in it.

The reason is they are hybrid C# and native engine objects, and when the JSON package calls new to make one, it cannot make the native engine portion of the object, so you end up with a defective “dead” Unity object.

Instead you must first create the MonoBehaviour using AddComponent() on a GameObject instance, or use ScriptableObject.CreateInstance() to make your SO, then use the appropriate JSON “populate object” call to fill in its public fields.

If you want to use PlayerPrefs to save your game, it’s always better to use a JSON-based wrapper such as this one I forked from a fellow named Brett M Johnson on github:

Do not use the binary formatter/serializer: it is insecure, it cannot be made secure, and it makes debugging very difficult, plus it actually will NOT prevent people from modifying your save data on their computers.

A good summary / survey of the problem space: