2

I'm working on my first program in Unity3d, testing the waters with instantiating objects at runtime and manipulating their properties.

So far my testing ground creates a deck of cards spread out vertically, with RigidBody component that do not use gravity. It then attempts (and succeeds!) to make the RigidBody component of the highest card (which is also the last created in the array) use gravity.

All of this works according to plan just fine, the cards appear in a vertical floating stack, and the highest card slowly pushes them all down to the surface below. The part that bothers me however is that assigning gravity to that top card always throws an IndexOutOfRangeException.

public class Deck : MonoBehaviour {

    private GameController mController;
    public GameObject cardObject;
    public GameObject[] get; 
    public Vector3 position;
    public float rotation;

    void Start () {
        get = new GameObject[52];

        for (int i = 0; i < get.Length; i++) {
            get [i] = Instantiate (cardObject, this.transform) as GameObject;
            GameObject go = get [i];
            Card card = go.GetComponent<Card> ();
            card.SetGameControllerReference (mController);
            card.newCard (i);
            //  public void newCard(int i) {
            //      suit = i / 13;
            //      rank = i % 13;
            //      name = RankString [rank] + " of " + SuitString [suit];
            //  }
            Rigidbody rb = go.GetComponent<Rigidbody> ();
            rb.isKinematic = true;
            MeshRenderer mMeshRenderer = go.GetComponent<MeshRenderer> ();
            mMeshRenderer.enabled = false;
        }
        Materialize (position, rotation);
    }

    public void reset() {
        for (int i = 0; i < get.Length; i++) {
            Destroy (get [i]);
        }
        Start ();
    }

    void SetGameControllerReference (GameController controller) {
        mController = controller;
    }

    public void Materialize (Vector3 pos, float rot) {
        transform.Translate (pos);
        transform.Rotate (0, rot, 0);
        for (int i = 0; i < get.Length; i++) {
            GameObject go = get [i];
            Card card = go.GetComponent<Card> ();
            Material mMaterial = go.GetComponent<MeshRenderer> ().material;
            string path = card.tmString();
            mMaterial.SetTexture ("_MainTex", Resources.Load (path) as Texture);
            Rigidbody rb = go.GetComponent<Rigidbody> ();

            float height = 0.05f + (i * (card.cardWidth));

            Quaternion newQuat = new Quaternion ();
            Vector3 newPos = new Vector3(0, (10 * (i + 1)), 0);
            newQuat.SetLookRotation (newPos, Vector3.up);
            newPos.Set (0, (i + height), 0);

            rb.transform.SetPositionAndRotation (newPos, newQuat);
            rb.useGravity = false;
            rb.isKinematic = false;
            go.GetComponent<MeshRenderer> ().enabled = true;
        }
        // This line ALWAYS throws an IndexOutOfRange Exception.
        get [get.Length - 1].GetComponent<Rigidbody> ().useGravity = true;
    }

    void Update () {}
}

The Deck is instantiated here:

public class GameController : MonoBehaviour {

    public Deck deckObject;
    public Deck deck;


    public string state;

    void Start () {
        state = "Entry";
        deck = Instantiate (deckObject);
        deck.Materialize(new Vector3(10, 10, 0), 45);
    }

    public void reset() {
        deck.reset ();
        state = "Entry";
    }

    public void explode() {
        state = "Explode";
    }

    public void gravityGun() {
        state = "GravityGun";
    }

    void Update () {

    }
}

What perplexes me even more is that the line ALWAYS throws the exception. I know that the deck is 52 cards, I hardcoded it to be length 52 (bad practice, I know, but it makes card data instantiation a breeze), and so I tried the following:

get[10].getComponent<RigidBody>().useGravity = true;

Which still threw an IndexOutOfRange exception, even though the eleventh card in the stack still fell down to the ground upon running... Can somebody explain why it's getting thrown?

vimuth
  • 5,064
  • 33
  • 79
  • 116
  • array.Length - 1 -- will throw Out of Range exception when Array is empty. As: array.Length =0 and you are trying to access 0 -1 => -1th Index – Prateek Shrivastava Apr 16 '18 at 09:34
  • @PrateekShrivastava the array is _hard coded_ to be length 52, as I mentioned. The very first line of Start() is get = new GameObject[52]; – Dr_StrangeKill Apr 16 '18 at 09:39
  • No it isn't. It's initialized in the start method. Its length is otherwise indeterminate – Mardoxx Apr 16 '18 at 09:40
  • I've set unity to pause on errors, so it always pauses on the first frame (because the line in question is in the Start() function) but even by then, I can see that the array reads length 52. I can even check the card the line edits, and it's already changed the rigidbody component event. – Dr_StrangeKill Apr 16 '18 at 09:42
  • @Mardoxx could you elaborate? This question is intended to help me understand object initialization in Unity better, and particularly how and where to do so in code. I suspected I was doing this wrong, and your comment semi-confirms my suspicions, but does nothing to help me – Dr_StrangeKill Apr 16 '18 at 09:47
  • Is the Destroy() method your custom? Can you share the code? Moreover - What is the Array.Length on the line where exception happens - Breakpoint/Add Watch?? – Prateek Shrivastava Apr 16 '18 at 09:49
  • 1
    If you're initializing the array at `Start()` (how sure are you, this method is called first/at all?) anyway, why not declare it that way, so it is guaranteed to run at instatiation of the class. I.e.: `public GameObject[] get = new GameObject[52];`. Even better, make it public **readonly** GameObject[], so it can't be reasigned from the outside. (Each individual index/item can still be assigned.) – Corak Apr 16 '18 at 09:50
  • @PrateekShrivastava, the Destroy() method is, I believe, native to unity. – Dr_StrangeKill Apr 16 '18 at 09:51
  • ... insta**n**tiation of *an object* of the class... – Corak Apr 16 '18 at 09:57
  • @Corak I'm fairly sure that fixed it. how do I make your answer my official answer? – Dr_StrangeKill Apr 16 '18 at 09:59
  • @Dr_StrangeKill - the question is, why the `Start()` method ("obviously") isn't being called. *If* it is called (*first*), then everything *after* it should work, as you'd expect it. I'm not familiar with unity, maybe `Start()` is a thing there, I don't know. *Usually* I'd expect such initializations to happen in the [Constructor](https://learn.microsoft.com/dotnet/csharp/programming-guide/classes-and-structs/constructors). – Corak Apr 16 '18 at 10:04
  • @Corak the Start() function IS a unity function. I'm only now beginning to understand slightly more about how Unity handles these things, but I believe it is used much like a constructor. – Dr_StrangeKill Apr 16 '18 at 10:13
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/169064/discussion-between-dr-strangekill-and-corak). – Dr_StrangeKill Apr 16 '18 at 10:13

1 Answers1

2

While I believed my array was hard coded to be length 52:

void Start () {
    get = new GameObject[52];

I was in fact hard coding an instance of that array, hence the new keyword. By instead declaring the array to be length 52...

public class Deck : MonoBehaviour {

private GameController mController;
public GameObject cardObject;
public readonly GameObject[] get = new GameObject[52];

I can ensure that it will always initialize to that size when the deck itself initializes. Simple rookie mistake, but I really appreciate the help guys!

EDIT :

More importantly, I was misusing the Unity Start() function, even after declaring the array correctly. The instantiation of the cards should instead be done in the Awake() function. making the code look like:

public class Deck : MonoBehaviour {

private GameController mController;
public GameObject cardObject;
public readonly GameObject[] get = new GameObject[52];
public Vector3 position;
public float rotation;

void Start () {

}

void Awake() {
    for (int i = 0; i < get.Length; i++) {
        get [i] = Instantiate (cardObject, this.transform) as GameObject;
        GameObject go = get [i];
        ...

Instantiation of the Deck can then be performed by the GameController in it's Start() function, as seen in the original code.