This section is another with no new rules – just useful tricks. Now that we have structs, and lists, and string indexes, we can combine them to make big data structures.
Usually these are pretty natural - maybe you have several people who can own several dogs each. With practice, that’s easy to put into the computer and use.
Big data structures are also good practice writing double and triple loops.
Making a list of Cow structs looks like this:
As we know, in the Inspector you have to type a non-zero number for the size of
that list. Popping it open will show lots of Cow’s, which you can also pop
open.
More interesting is how to search around the Barn in code. The trick is the same as for things like c1.spotCol.r for the old cow’s with colored spots. Go left-to-right, using the options for that type.
Here’s code using Barn:
Barn is a list, so we can use Count, or can pick out one item using []. Barn[0] is a Cow, so we can use dot-age or dot-name.
To compare, some errors:
We can also copy entire cows in and out of the barn. This is really the same boring stuff we did with lists of ints, or normal structs:
We can create the entire Barn list ourselves, adding each Cow. This makes six 1-year olds named cowy:
A list of classes works the same, except they also need to be new’d. This makes a list of six Dog’s (Dog is a class):
That’s a little sneaky. new Dog() creates a Dog and returns a pointer to it. Normally we assign that to a variable, but adding it to the list is just as good, since that’s the final place for it.
We could use two steps instead, with a middeman variable:
Here’s a bugged version. See if you can spot the problem. Hint: how many Dog’s are there?:
This creates 6 pointers, all aimed at the same Dog.
A neat Unity trick is making a list of GameObject’s. This lets us drag many Cubes into the Inspector:
We’d change them the usual way. Blocks[0].transform.position=pos; would move the first block. Or this would turn every other block red:
We can also fill that list ourself, by adding instantiated objects. They’ll all be in the same spot, but we can move them later:
This solves a problem from before. We were able to create dozens of balls, but we had no place to save pointers to them all. A list is the perfect place for that.
The shortcut Blocks.Add(Instantiate(ballPrefab)); would also work. I prefer the extra step of declaring gg. We’ll probably want to use it to color or place each fresh ball.
Structs with lists inside them also aren’t special, but look interesting. As usual, the list needs to be new’d. Here’s a Goat struct which has a list of everything the goat eats:
Some sample code playing around with Goat g;, showing how to use eats. This goat will eat tin cans and old shoes:
Then some errors:
We could also create an eat-list first, then assign it all at once. Here BillyFood is like a temp, passing the list of foods along to g2:
new’s can pile up on us. Suppose Goat was a class. We’d need to create and and to create it’s list:
A string can use indexes and has a length. So an list of strings is sort of like a 2-dimensional list. A warm-up review:
Now the new-looking part, using two []’s in a row:
The cool thing is it’s not a new rule. W[0] is "cow", which is a string. So of course
adding [2] after grabs the ’w’ character.
Now we can write this cool nested loop to count the total number of a’s in every word in the list:
As usual, it’s easy to mix up the i’s and j’s and get wrong counts and
out-of-range crashes. W[word][letter] might be better variable names if you don’t
use i and j for every nested loop.
A cute trick with arrays of strings is to use them to make a map. This usually isn’t the best way, but it’s fun and a nice example.
For a 3x4 map, we can make a list with 3 strings, all length 4. We’ll make the rule that o is an open space, and X has a block in it:
We can use a nested loop to make that picture. Cubes go on the X’s. This is a copy of the nested grid loop from last chapter, except we check the map before deciding to make a block:
It doesn’t look as nice as it could since newBlockAt leaves a little space between.
In this case it would be better to have them touching, like one long solid
wall.
To get a real feel for working on a grid, we can count how many walls are next to spot (x,y). We take a pretend step in each of the 4 directions:
It crashes if we’re on an edge (fixed with if’s). Math like this is common for walking around in a grid: M[y][x-1] is the space to the left of us, and so on.
A real 2D list is just a list of lists. The cool thing is there are no new rules - we can
make them out of what we have now.
For example, List<List<string>> is a list of lists of words. The inside part is List<string>, a normal list of strings. Then List< ... > goes around it. So it’s a list, containing lists of strings (if you can figure out the TV show about a serial killer who kills serial killers, you can figure out this).
To get started, suppose we have some lists of words for a Mad Lib:
We eventually want to use these to randomly make 4-part sentences, choosing one from each, like “The cat inspects heavy duty truth”.
We’ll put those four lists into a big list:
Now AllParts has four string lists in it. AllParts[1] is the verb list, and AllParts[1][0] is the first verb, "runs".
A picture would look like:
Playing with it to get a feel, we can do things like this:
We can make a random sentence using a loop to pick from each part:
Notice how the last line uses the usual [][]. Down the side to the sentence slot
we’re on now, then across to the random word for that slot.
There’s one final super-cool thing a list of lists lets us do. Each of the inside lists counts as a normal list, and we can do normal list things with it. For example, we could write a function to pick a random word from a normal list:
This is a totally normal list function, that knows nothing about lists of lists and won’t work with them. But we can still use it in our 2D list sentence maker:
This works since AllParts[i] counts as an ordinary list of strings. getRandWord
is happy to take it as an input and pick out a random work from that one
list.
A 2D integer list of lists feels more griddy, but it’s made mostly the same way. This makes a 4x4 grid:
Probably the most confusing line is G.Add(new List<int>());. The inside of it
creates a fresh int-list. Normally we assign that to a variable, but we’re allowed to
skip the middle-man and jam it straight into the list.
We can think of this as a real grid:
I put G across the bottom, with the lists going up, so we could use G[x][y] in the
more normal way. For example, G[3][0] is the lower right.
These next two single loops put 7’s up the right side, and 8’s across the middle (the last 8 overwrites a 7). Notice which slots have the variables:
We can even use a normal List<int> function on each column, since each column is a list (but not on a row.) This sets everything in an ordinary list to a value:
Now setVal(G[3],9); replaces those 7’s with 9’s. Of course, it only works for columns – each column is one list. There’s no way to send a row to setVal since each row is 1 box from each of the column lists.
We can use these rules to make structs with lists of structs, and larger. It’s not that complicated, since you make them based on the data you already have.
For example, making a list of goats (which have lists of what they eat):
Notice we have to new the goat-list, new each goat in the list, and new each eats list for each goat.
A partial picture:
A few sample lines that do and don’t work:
We can run some interesting loops through this. This nested loop counts how many goats like a certain food:
It checks the obvious way: scan every goat, look through what that goat eats,
count it and skip to the next if you get a food match.
Here’s a triple loop to count the total Z’s in all food items (if five goats like “pizza”, one of them twice, this would count as 12 z’s):
GG[i].eats[j][k] has four total look-ups, but it’s fine since we really need that many: which goat, what it eats, which food, which letter.
Previously we used a nested loop to create a 2D grid of Cubes. But we couldn’t save pointers to them, which meant we couldn’t find and change them later. Now we can, which means we can almost make a game.
Our game will eventually allow you to toggle spaces (selected or un-selected) letting you make crude pixel shapes. First a simple class to hold data for each space:
Then we’ll make a simple 2D list, to hold them, for when we make the cubes, later:
Now Board.Count is 8 (going across the bottom.) Board[0].Count is 6 (going
up.) Board[7][5].selected=true; selects the upper-right corner.
Making the blocks and hooking them up is nothing special. We’ll use Board[x][y].cube=gg; as we create each real Cube, then math to get the proper arrangement:
That gives us something new. We have 6 by 8 Cubes on the screen, and we
now have a way to find and change them all. Board[x][y].cube is that
Cube.
The “game” will be to move around and toggle the current square on/off. We’ll be able to make a crude picture, which is still pretty cool. I’ll break it into using the keys to move, and using space to toggle the color of the cube we’re on. Update will call each function:
selectCheck will toggle the color of the current cube. Without movement, we can never leave square (0,0), but that’s good enough to test. We can tap space and watch the corner cube change color:
The best part of this is the top line with bs=Board[col][row]. It picks out the current spot, based on row and col, exactly how we want in a 2D grid. Once we have that, we use bs.selected and bs.cube to look at the parts.
Without using bs as a shortcut, the last line would start with:
Board[col][row].cube.GetComponent. That’s long, but reading it left-to-right
should look fine.
Movement is the arrow keys. left/right change the column, up/down change the row. I decided to have off-edge wrap around, which requires 4 ugly if’s. The current cube is a tad smaller, to show that we’re on it:
Doing something special to the current item is always a pain. That’s what the second half of the code is doing. We need to check whether we moved, reset to old square’s size (which is why we needed to save it in old row and col), and finally change the new square.
Be sure and look at Board[col][row].cube.transform.localScale. Long, but
pretty.
This type of set-up for a board is common. Each square might need to know the terrain type, buildings in it, who captured it, and so on. A class or struct is good for that. A link to the visible part (cube, here) is just one more field.
This is an old trick, not very common anymore, but it’s a fun exercise. Suppose you want to make a list of some of the things in a list. If you wanted items 1,4 and 6. You’d make a list [1,4,6], which is an array of indexes. Here it is in code:
We’ve never seen anything like Ani[InZoo[i]] before. It’s a nested look-up,
doing the thing above. InZoo[i] goes through 1, 4, 6. So Ani[InZoo[i]] goes
through items 1, 4 and 6: bear, eel, goat.
Here’s a longer version of the same trick. Suppose we want to print the animals in a random order. A cheap way is to shuffle a list with 0-7, then read the animals in that order:
The last line is the same nested look-up as before.
This is really a version of pointer thinking. We can’t have pointers to strings, but we can use the indexes like pointers. It’s not common, but it’s a nice way to practice.
If we have a list of objects, we can pick out some of them with another list of pointers. For example AllDogs is a list of every dog. SledTeam won’t have any new dogs – it will only point to things in AllDogs:
You can’t tell by the definition. but in our minds SledTeam isn’t holding Dogs. It will never have any Dogs of its own. It’s purpose is to pick 4 dogs out of the main Dog list.
This would print the names of all dogs on our team:
Here, checking SledTeam[i]==null; makes perfect sense. null is a perfectly good value – we haven’t picked a Dog for that position yet. We’re also imagining SledDog[i].nameas reaching over into AllDogs:
There’s another version of this trick where a class has pointers to itself inside of it. For example, all of our Rabbits can have another Rabbit best friend:
Clearly, we can’t have an actual rabbit in a rabbit, since that would have another
rabbit inside of it, going on forever. But it’s fine to have bestFriend be an arrow.
We’ll never use new on it. Its purpose is to point out some other pre-made rabbit (or
be null if we have no best friend).
A neat thing is that Unity can’t tell. It assumes gameObject variables pointer to gameObjects it created. But it assumes a rabbit is a rabbit and tries to make one for bestFriend, triggering infinite rabbits. A special safely check kicks in after it makes 7 levels of nested rabbits and gives an error.
[System.NonSerialized] is there to explain to Unity that bestFriend is like a
gameObject pointer – it’s only an arrow. It says not to create it. All of this can get
complicated quickly, but deep down it’s the Real vs. Arrow issue that all reference
types have.
This code makes every pair of rabbits be mutual best friends:
Or we could randomly assign best friends, with some anti-social rabbits whose best friends are null (1 in 6 chance):
Basically, bestFriend could aim at any other rabbit, or null, and can double-up.
That’s not as fair as pairs of best-friend rabbits, or maybe it is, if you’re a
rabbit.
Now that we’ve set best friend arrows, we can use them. This would count how many rabbits have a best friend named Thumper:
Finding the most popular rabbit needs a nested loop. We’ll check every rabbit, counting how many other rabbits like it, remembering the highest total:
It’s a nested loop because each rabbit knows who it likes, but not who likes it. We need another loop for that.