Unity GameObjects arrays

Started by
9 comments, last by AthosVG 5 years, 11 months ago

I did think about posting this in beginners as I am a beginner in Unity lol (well since today!).

I'm trying to create a bunch of cloned prefabs in a 2d array for the Tower defence game. It worked fine using Instantiate to make a bunch of clones, however a problem has arisen as I need to store a pointer (or reference whatever this magical c# thing is) so I can later do things to these cloned gameobjects. Anyway enough .. here is the code that refuses to work:


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MaLoc
{
	public int contents;
	public GameObject gobj;
}

public class Main : MonoBehaviour {

	public static int m_ciRows = 8;
	public static int m_ciColumns = 8;

	public MaLoc[,] m_Map = new MaLoc[m_ciRows, m_ciColumns];
		
	public GameObject prefabTest;

	// Use this for initialization
	void Start ()
	{
		GameObject blah = (GameObject) Instantiate (prefabTest);
		m_Map[0,0].gobj = (GameObject) Instantiate (prefabTest);
	}
}

Now the first line of Start() ... GameObject blah works, but the second line m_Map[0,0].gobj = does not work with:


NullReferenceException: Object reference not set to an instance of an object

I'm guessing this is something I'm missing about how to use these C# references, which hasn't been explained in the tutorials. How should I be doing this?

Advertisement

MaLoc[,] m_Map is an array that holds MaLoc classes, but the entries are all null by default, as C# classes are handled by reference, much like a C++ pointer. This is obviously different from C++ where an array contains the objects themselves.

You will need to allocate m_Map[0,0] as a new MaLoc(), and then you can safely assign to its gobj member. You probably want to create new instances for each slot in the array.

 

3 minutes ago, Kylotan said:

MaLoc[,] m_Map is an array that holds MaLoc classes, but the entries are all null by default, as C# classes are handled by reference, much like a C++ pointer. This is obviously different from C++ where an array contains the objects themselves.

You will need to allocate m_Map[0,0] as a new MaLoc(), and then you can safely assign to its gobj member. You probably want to create new instances for each slot in the array.

 

Oof, painful! I'll try it, thanks Kylotan! :)

Alternatively, you could change MaLoc to a struct, in which case your existing code would work more like C++ where the array really does contain the MaLocs directly.  There are other caveats of doing that, though:  Passing MaLoc between functions would pass copies rather than pointers (unless you pass using the 'ref' keyword).  This could be either good or bad depending on what you need to do with them.

Thanks Nypyren, I have read that using a 'struct' may be the answer to creating what would be considered normal programming code, I haven't tried it yet.

I just spent 3 hours debugging yet another issue caused by these damn references. I have an array of a small class, and I wanted to copy one element to another, naturally I used a[x] = a[x+1], or similar code.

This DOESN'T copy the element, as you would expect, it seems to copy the reference. WTF happens to the object that was there in a[x] I don't know, it possibly disappears into one of stephen hawkings' black holes, or into a secret missing sock stash. I spent ages trying to figure out if it was some bizarre multithreading bug, but no, C# doesn't actually seem to do basic language expressions.

Instead I had to write a copy function into the class, in order to copy the object members (that weren't references) one by one, just to copy the F***ing data. Is there some kind of equivalent of memcopy that it's possible to use to copy data from an object to another?

This isn't helped by the fact that my monodevelop install doesn't seem to want to debug with unity, so I can't see what's going on in the memory, and am relying on hundreds of verbose Debug.Logging statements.

This language is hands down the worst I've ever used in 35 years. :(

51 minutes ago, lawnjelly said:

Thanks Nypyren, I have read that using a 'struct' may be the answer to creating what would be considered normal programming code, I haven't tried it yet.

I just spent 3 hours debugging yet another issue caused by these damn references. I have an array of a small class, and I wanted to copy one element to another, naturally I used a[x] = a[x+1], or similar code.

This DOESN'T copy the element, as you would expect, it seems to copy the reference. WTF happens to the object that was there in a[x] I don't know, it possibly disappears into one of stephen hawkings' black holes, or into a secret missing sock stash. I spent ages trying to figure out if it was some bizarre multithreading bug, but no, C# doesn't actually seem to do basic language expressions.

Instead I had to write a copy function into the class, in order to copy the object members (that weren't references) one by one, just to copy the F***ing data. Is there some kind of equivalent of memcopy that it's possible to use to copy data from an object to another?

This isn't helped by the fact that my monodevelop install doesn't seem to want to debug with unity, so I can't see what's going on in the memory, and am relying on hundreds of verbose Debug.Logging statements.

This language is hands down the worst I've ever used in 35 years.

I am still getting used to C# having come from initial roots in Rockwell's 6500 assembler, C and the C++, and like yourself, have found this problem you have come across. What happens with C# structs, I haven't yet experimented with, but unlike in C/C++ where you can do simple structure copies, I don't believe this happens with C# structs. However in C++, classes need to have a copy/Clone method. One can perform a shallow copy which IIRC just makes a simple member-wise copy. But for a true clone. a deep copy is required. In which case a function/ sub routine needs to be written as the compiler does not know how to make a copy of a complex structure. It would make sense to me that C# performs in the same way.

I know its not what you wanted to hear, but there is a reason for it. You might find this a good read: https://www.c-sharpcorner.com/uploadfile/56fb14/shallow-copy-and-deep-copy-of-instance-using-c-sharp/

 

 

3 hours ago, lawnjelly said:

Thanks Nypyren, I have read that using a 'struct' may be the answer to creating what would be considered normal programming code, I haven't tried it yet.

I just spent 3 hours debugging yet another issue caused by these damn references. I have an array of a small class, and I wanted to copy one element to another, naturally I used a[x] = a[x+1], or similar code.

This DOESN'T copy the element, as you would expect, it seems to copy the reference. WTF happens to the object that was there in a[x] I don't know, it possibly disappears into one of stephen hawkings' black holes, or into a secret missing sock stash. I spent ages trying to figure out if it was some bizarre multithreading bug, but no, C# doesn't actually seem to do basic language expressions.

Instead I had to write a copy function into the class, in order to copy the object members (that weren't references) one by one, just to copy the F***ing data. Is there some kind of equivalent of memcopy that it's possible to use to copy data from an object to another?

This isn't helped by the fact that my monodevelop install doesn't seem to want to debug with unity, so I can't see what's going on in the memory, and am relying on hundreds of verbose Debug.Logging statements.

This language is hands down the worst I've ever used in 35 years.

If you find yourself wanting to copy, that's when you should use structs.  The assignment operator for structs copies every single field just like you'd get with a memcpy.  HOWEVER, if a member of a struct is a class type, that field will have just the pointer copied.

On the other hand, every single "class" uses reference semantics:  copying JUST the pointer, not the object being pointed at.  It's just like every variable which is a 'class' type also has a C++ * type qualifier on its declaration.  The only ways to copy the contents are to manually copy them, or use a struct instead.

Sometimes you don't want to copy a struct.  In those cases you can pass "pointers" to structs in two main ways:  Make the struct the member of a class and pass the class, or use the 'ref' keyword.  The ref keyword is basically the & type qualifier from C++, but can only be used in function argument lists.

One of the major things you should beware of with structs is combining them with properties.  A property works like a method, so a property for a struct will return a COPY of a struct.

Collections (arrays, lists, etc) are classes which contain whatever type you declare them with, and the elements use their type's semantics - structs in an array will use copy semantics (assigning a[x] = a[x+1] will copy the whole struct, NOT just a reference).  Any class types as an element of an array will make a pointer copy as you've discovered.

Arrays themselves are classes, so you cannot copy the contents of an array to another array using the = operator.  You have to either loop-and-copy-elements or use Array.Copy functions.  Arrays are allocated on the heap by default.  There is a way to allocate arrays on the stack with the stackalloc keyword, but you almost never want to do that.

In your case of overwriting a[x] with a pointer to a different object, the former object will probably get garbage collected.  Garbage collection works by periodically traversing allocated data structures, starting at the stack, static variables, thread locals, etc, and following every pointer.  Any object that is traversed is "live" and is not deleted.  Any objects that it can't reach by following all of the possible pointers are considered "unreachable" and are automatically deleted.  If you had any other reachable variables pointing to the former contents of a[x], that would keep the object alive.  There are some surprising cases where you can keep a reference to an object stored for longer than you intended (usually as delegates) and can be considered a "memory leak".

There is another alternative to just 'use structs or classes to control copying behavior' - using immutable classes.  The common problem with accidentally copying a class is that when you modify it via one variable, you affect anything else referring to it.  If you make every field 'readonly', it results in something that behaves kind of like strings do:  strings in C# are classes, but you can't modify their contents.  So it's safe to copy them freely because you know you can't introduce mysterious bugs because you can't modify their contents.  You are forced to create new strings instead.  Copies are cheap to perform (because you're only copying a pointer address), but you get the safety you would from the uniqueness of a struct.

If you're using Unity and monodevelop can't debug, that will happen if you've switched to the .Net 4.6 runtime.  Monodevelop is being phased out.  The recommendations are (on OSX) to use Visual Studio for Mac or JetBrains Rider.  On Windows, use Visual Studio 2017 (either Community or Professional, depending on whether you're doing commercial work or not).  All of these IDEs are vastly superior to MonoDevelop, can debug Unity, and will provide a much better overall experience working with C#.

Thanks Nypyren. That's a good explanation. It is beginning to be become clearer.

I'm using Unity on Linux, it's possible I botched the install as I had to install monodevelop separately, I might uninstall both and try and do at the same time once I've finished this tower defence game.

So they have basically reversed the c++ convention for passing by value / passing by reference in C#. This begs the question, why would you do that (is this a carryover from java, I have not really used java)? And given it is possible to pass a struct by reference with the ref keyword, is it possible to pass a class by value with another keyword?

What bugs me is not just the reversal of the convention that I'm used to, it is that when using a class / struct, you have to know which it is in order to understand what the code is doing. The result of a reference operation is completely different to an absolute operation, and it is not explicit in the code. Alright in c++ for example, you have to type one extra character (&, or *) in order to use 'reference' like access, but it makes it explicit in the code. You can (at least in theory) instantly understand what a line of code is doing, without having to refer to documentation.

I think I get it now, but I'm sure it will bite me a few times before I get used to it!

Last time I used Java, it didn't have custom value types (structs) at all.  There were primitive data types (a more limited selection than C#) and classes.  I think they're working on adding value types, but I haven't kept up to date on Java since I never use it anymore.  Most of the non-C++ languages I've used are closer to how C# works than how C++ works - passing references by default and using garbage collection.

As far as I know there's no way to treat a class like a struct in C#.  You usually just have to get used to working with references.  Once you're used to it, it's not that big of a deal.

I believe the reason why C# uses reference types primarily is due to the memory management model: garbage collection.  Most of the complex headaches using C++ stem from memory management.  Some language designers looked at C++ and decided the amount of control you get wasn't worth the added language complexity and programmer headaches.  There are trade-offs.  If you need absolute control over every aspect of memory management, you need C++.  It's easy to be a 'slob' in garbage collecting languages - your program will work but if you just allocate objects foolishly without any thought behind it, you'll have memory and performance issues.  You should be intelligent with allocations in C# just like in C++ but you won't have the same degree of control.

As far as avoiding struct/class confusion in day-to-day work, what I do is configure the IDE to color-code all value types red and all reference types blue.  That way I can instantly know what a particular variable is going to do when I use the = operator.

11 hours ago, lawnjelly said:

I think I get it now, but I'm sure it will bite me a few times before I get used to it!

You wouldn't be a first, but from my experience, the inverse tends to bite beginners more due to passing by reference being so common to them. I've had to explain the following situations a few times in the past:


struct Data
{
	int x = 0;
};

class Foo
{
 	Data GetData(); 
}

...
  
Foo foo;
foo.GetData().x = 1;
Console.WriteLine(foo.GetData().x);

And the output is of course, 0. I'm honestly not a fan of it either, but in practice people tend to use the class keyword so much that it doesn't pose that much of a problem from what it seems. At that point, the default of passing it by reference ('pointer') tends to have them shoot themselves less in the foot I guess. 

12 hours ago, lawnjelly said:

is it possible to pass a class by value with another keyword?

No, there is not. The idea behind the struct keyword is that they be used for small data, while classes would be larger with explicit behaviour, making it preferable to have these passed by reference all the time. Microsoft even has guidelines on this, but ironically, they tend to not follow them consistently either.

This topic is closed to new replies.

Advertisement