Clarifications Requests for Better Understanding

I've been reading through a lot information in the last couple weeks, but I'm not 100% on certain specific points, so these might be 'duh'-level questions.

1) Can you use a class like a giant array? In the sense of dumping level data into a "public class levels", and then access it through dot notation?
To be more specific, if I load level maps and such into my game, can they go into that class and be accessed (assuming initialized values) like "levels.lvl[3].enemies" or "levels.lvl[5].rows"?

2) Follow-up question, can you nest arrays (or perhaps I'm saying it wrong, it could just be objects), such as a string: "levels.lvl[5].dialogue[1]"?

3) Regarding pulling information from JSON files, must you have something like JsonUtility or miniJson to bring in the data? If I build the JSON format, can't I just parse it myself? Or do I need something to pull in the data for me?

Thanks in advance, I've found a lot of information so far, but on a few points it just misses the mark of a clear-cut answer, at least for my understanding.

Yes.

[quote]
2) Follow-up question, can you nest arrays (or perhaps I'm saying it wrong, it could just be objects), such as a string: "levels.lvl[5].dialogue[1]"?
[/quote]

Yes, you can nest arrays and objects in whatever way makes sense to you.

[quote]
3) Regarding pulling information from JSON files, must you have something like JsonUtility or miniJson to bring in the data? If I build the JSON format, can't I just parse it myself? Or do I need something to pull in the data for me?
[/quote]

Of course you can write your own JSON parser, if you're a good enough programmer. But it's not easy, and when there is one built in that works well, I don't quite see why you would (except maybe as an exercise).

2 Likes

@JoeStrout has given you the first level answer. Consider my comments more advanced extras.

C# actually allows you to define a variety of indexers, just like a normal function. So you can actually set up indexers based on int, string, type, or any other type you are interested in. Most of the time its worth just using an array or list, but their are plenty of other ways to do it.

There are two additional ways to 'nest' arrays. A jagged array is an array of arrays. Something like levels[5][3]. The other alternative is a multi dimensional array. Something like levels[5,5].

Unity has access to the .NET framework. So you can open files in the normal manner, read out the bytes, and parse as you like.

However, as a game developer you really should focus your effort on stuff that makes your game unique. The parser you use makes no difference to the player experience. So using one someone else built is advised.

1 Like

I think I have a better understanding of how classes work now, and the indexers. The dialogue is a not an immediate concern, and might end up handled independently of level data. And I'll agree that using a parser is going to be far better than spending time on what already exists.

Thanks to you both!

1 Like

Reviving this rather than starting anew, it seems I'm having a thick-headed moment and just not quite understanding something when using multiple scripts and variables contained therein.

My design plan is to operate from a primary PlayGame script, call up a PlayLevel script for each level, 'close it out' so to speak when the level is done, return to the PlayGame script, and await the next instruction. (Granted, that's all speaking from an old, linear POV vs. OOP style programming, I'm still adapting here, heh).

So my understanding is that PlayGame will need to GetComponent to be able to 'read' the script and reference it. Does this also mean PlayLevel needs to GetComponent to see the public variables it needs to design the level?

Part of my confusion is that some Tutorials like this one: https://unity3d.com/learn/tutorials/topics/scripting/classes?playlist=17117 seem to indicate you can simply make a public class on the same GameObject and the class will be visible by other scripts on the same object. However when I try to reference PlayGame.currentLevel in the PlayLevel script (which is defined and initialized in PlayGame), Unity says "An object reference is required to access non-static member 'PlayGame.currentLevel'

Making it static removed the error, but I'm unclear on what effect that has, or why it works that way. [Edit: re-reading the tutorial on statics helped, but now I lose on-the-fly functionality of setting values in the Inspector.]

EDIT #2: Weird, I have a third script for loading all the levels into a class (preparing to fetch from JSON later). In it, an array is defined as "new LevelClass[PlayGame.totalLevels]" (which is a public static) but this gives the error "A constant value is expected." Argh.

It feels like I'm || this close to having the light bulb go on, but I'm missing something...

NOTE: Please, please, don't tell me "there's many ways to do that" because that's not helpful. I need one, maybe two actual methods rather than general statements, as I'm aware of the great openness of Unity, but that's actually making things harder for me rather than easier. :)

Yeah... that's just not how things work. Unity scripts (and most other modern programming environments, for that matter) are event-driven. Something happens — a new frame begins, your object bumps into some other object, whatever — and you are given an opportunity to do some quick processing in response to that event. That's it.

OK, that's not really it; there are work-arounds and ways to sort of work in a more linear fashion, but you said you wanted to keep it simple, so for now at least, embrace this concept. Make it the basis for every problem you tackle. (It's a powerful paradigm; it can handle it!)

[quote]
So my understanding is that PlayGame will need to GetComponent to be able to 'read' the script and reference it. Does this also mean PlayLevel needs to GetComponent to see the public variables it needs to design the level?
[/quote]

There's no "reading" the script; but yes, it needs to get a reference to the script in order to, erm, reference it (or anything it contains). Keep in mind that the script itself is just a class, and that class could be used by dozens of instances of that class. You may intent that there's only one PlayLevel instance around at a time, but how would the computer know that? Assume there could be dozens. So when you think about accessing some public variable of a PlayLevel instance, you must first answer: which PlayLevel instance are we talking about?

And if the answer is, "well the one on the same GameObject as this PlayGame instance," then yeah, you use GetComponent() to get a reference to that instance. And then you can do stuff with it. Conversely, if PlayLevel needs to talk to some PlayGame that's on the same game object as itself, it will get a reference to that via GetComponent().

[quote]
Part of my confusion is that some Tutorials like this one: https://unity3d.com/learn/tutorials/topics/scripting/classes?playlist=17117 seem to indicate you can simply make a public class on the same GameObject and the class will be visible by other scripts on the same object.
[/quote]

Nope. Not true. Something got misunderstood there.

[quote]
However when I try to reference PlayGame.currentLevel in the PlayLevel script (which is defined and initialized in PlayGame), Unity says "An object reference is required to access non-static member 'PlayGame.currentLevel'
[/quote]

Quite right. This error is basically saying: which PlayGame instance's currentLevel do you mean here? You need a reference to specify which. (You don't need a reference for static variables, because they don't live on instances; there is only one value for each static variable in the entire program, because that's just what "static" means.)

[quote]
EDIT #2: Weird, I have a third script for loading all the levels into a class (preparing to fetch from JSON later). In it, an array is defined as "new LevelClass[PlayGame.totalLevels]" (which is a public static) but this gives the error "A constant value is expected." Argh.
[/quote]

Not so weird. Just a language limitation. This is probably in a field declaration, and as the error says, you have to use a constant value there — PlayGame.totalLevels is not a constant, it's a variable.

Best,
- Joe

1 Like

Wow, thanks. Working on the paradigm shift now, still missing the clarity of linear structure, but it is what it is. Some follow up questions, then:

1) One of the reasons I wanted to create each level and dissolve it when done was to dump all the variables (including arrays) and start fresh on a new level. I'd thought an instance of the level could be created, which creates the structure from the parameters for that level (level[3].rows and such). Generally speaking, couldn't that instance of the level also contain the structure for handling input, managing score for that level, etc. and then nuke itself once the level is failed or defeated?

2) The concept on loading JSON data was to have a set number of levels ahead of time that are read in, but fornow there's 10 levels worth of data written out in the LevelLoader script. This is what I have so far to load the data, but I had expected to have a variable rather than manually changing the value each time. Is that not possible?

public static class GetAllLevels
{
static int Main(string[ ] args)
{
var Levels = new LevelClass[10]

EDIT: I guess this could just be a flaw in C#/Unity or something, it doesn't like this one either:
int[ ] enemyTally = new int [enemyTypes];
"Object reference not set to an instance of an object." This array worked fine before,the only change I made was to replace a fixed number with the variable stating how many different types of enemies were there. That's going to be a major hassle to work around, sigh...

Again, thanks for all the help!

Hmm, I'm not quite getting what you're trying to do here. And why do you have a "Main" function?!?

The simplest way of handling levels is usually to make each one its own scene. Then you just call SceneManager.LoadScene to switch to the next level. At that point all game objects in the current scene (with any exceptions you've specified) are destroyed, and the objects for the new scene are loaded (running their Awake and Start code, etc.).

The Main was in the example I found, didn't know why it needed to be there either, but I threw it in anyway.

Kind of hitting a wall, guess it's time to take a break. Despite GetComponent on Awake, references aren't working, and I'm getting errors that values are defined but not used when they ARE in use in that script.

It sounds to me like you're trying to do too much at once.

One of the advantages of the event-driven paradigm is that it lends itself very well to building up your program in very small steps.

I don't know what you've done so far, so it's hard for me to provide specific advice. But it seems like you're still struggling with some very fundamental concepts here, so I would suggest putting the current project down for a while, and going back to square one.

Square one is a cube in a scene. No scripts at all. Run it. Make sure it appears in the game view. If it does not, move the square and/or camera around until it does. Also make sure it is well lit. Run, and admire your creation. This is a success.

Now, create a new script; let's call it "CubeMover". Don't touch the code for this script yet. But attach it to the cube. Run again. It doesn't do anything, but if it still runs without errors, this too is a success.

Now put some code in the Update event to, say, rotate or translate the cube a small amount on every frame. Run. See the cube move. Success.

Now extend that script to only move/rotate based on keyboard inputs (which you check via the Input class). Run. Drive your cube around. Big success! It's starting to look like a game now.

Now, if ready for a challenge, see if you can get your cube to "shoot" little pellets (spheres) out its front end when you press the spacebar. This step is too big, so you break it into substeps. First add a pellet (sphere) in the scene. (Run.) Then attach a new script to this pellet that makes it move forward at a constant speed. (Run.) Then turn your pellet into a prefab (Run), and verify that while the game is running, you can drag the prefab out into the scene and create a new moving pellet whenever you want. Then give your cube script a reference to the pellet prefab (Run), and then add code to instantiate this prefab when a key is pressed (run again).

Then you'll start to think about cleaning up all these pellets cluttering up your hierarchy. You could at this point create another script that destroys an object after it's been alive for some number of seconds, or after it has gone outside of some bounds you define. You would build up this script in several steps, running and testing after each one.

At no point should you ever have a big, multi-script pile of code that is entirely untested, that you expect to somehow all work together. Build a little, test a little, that's the way!

So far what I have is a script for a match-3 swap archetype that:

  • Generates a grid of x/y size, formerly assigning random values, but currently pulling from an array of manually assigned values for testing.
  • Instantiates objects (sprites) at the x/y locations on the screen, based on value in a grid array (yes, this is a duplication from the numerical to the visual, but I can manage things better 'under the hood' for now)
  • On a GetKeyUp, it checks tiles for matches, updates the numerical array, and destroys specific objects.
  • Remaining objects are manually moved (animation to come later) down the grid.
  • New objects are created at the vacant locations.
  • Process checks for either a 'score goal' and displays a message, or an inability to make more matches.

However, now that the simplest core mechanic is working, I want to set up the overall game management script, then possibly instantiate a level script to play # level, and destroy the level when it's done, reporting back a final score to the game management script.

I managed to use GetComponent to refer to PlayGame, and identify the current level number. Unfortunately, trying to copy arrays over didn't go so well. (Also, had the issue noted above with trying to initialize an array size based on a variable, so I'll have to find another way to handle that.)

I agree on the big changes, it's definitely been the best approach so far. Measured, identifiable steps with trackable progress (even just displaying values in Debug.Log if needed).

Yep. I see you're already much further along than I had supposed.

Well, it sounds like your main sticking points at the moment may be pure C# — how to create and reference arrays, etc. Try reading over this documentation, this tutorial, and maybe this, this, and this too. :) The more different ways you see it, the more likely whatever misconception is tripping you up will suddenly become clear.

Thanks! I've actually read a bunch of that already, heh, but always good to have new resources. I'll add those to my collection.

The array issue I had was in trying to get the array of tiles stored as a GameObject[ ] in the PlayGame script over to the PlayLevel script. (To see how it worked for the future, for some ideas down the road.) It seemed like the GetComponent wasn't working at all, but I found an indication it was. So it was something with the statement itself, most likely. It was something like:

GameObject[ ] tiles = PlayGame.mainTiles;

But any variations I used weren't working, and my first few attempts to find an example or answer came up empty.

Right, well this relates to the whole which-PlayGame-instance-are-you-talking-about issue I explained above. It would only be correct if mainTiles were a static field (which is probably not a good idea). If it were a regular public property, and on the same object as wherever this code was attached, then the correct version would be:

GameObject[] tiles = GetComponent<PlayGame>().mainTiles;

This says, find the first PlayGame attached to the same GameObject as this script, and then gimme its mainTiles property.

Oh! I didn't realize I needed to do that each time. I was under the impression that a GetComponent would make things available automatically.
(I had "playGame = GetComponent ();" in Awake)

I really can't thank you enough! I'll give that a try tomorrow.

You don't have to get the reference each time if you store the reference in a variable, and then subsequently use that variable. This is no different from any other value. So if "playGame" were a field of your class, and you did playGame = GetComponent() in your Awake method, then later you could use it for things like "GameObject[ ] tiles = playGame.mainTiles.

Notice that "playGame" is very much not the same thing as "PlayGame". One is a variable you just made up, that could as well have been called fuzzyBananas. The other is the name of a class. It's just a convention, for the convenience of us humans, to use the lowercased version of a class name a variable for the one and only reference to the instance of that class that we happen to care about.

So putting it all together, the standard pattern looks something like this.

using UnityEngine;

public class PlayLevel : MonoBehaviour {

    PlayGame playGame;    // handy reference to the PlayGame instance on this object

    void Awake() {
        // Find our PlayGame instance (and make sure we have one).
        playGame = GetComponent<PlayGame>();
        Debug.Assert(playGame != null);
    }

    void Start() {
        // ...do whatever we want to do with it.
        GameObject[] tiles = playGame.mainTiles;
        Debug.Log("I found " + tiles.Length + " tiles.  Hooray!");
    }
}
1 Like

That's rather close to what I had (minus the Debugs, of course) but the syntax was probably off on passing the array over. Also,

Hmm, new problem though... the Debug shows all 7 tiles were found, and checking the value about to be assigned shows 2 (well within range), but at the line where it grabs the chosen tile from the array: GameObject toInstantiate = tiles ;[/I]
...it's throwing the error: Object reference not set to an instance of an object
Saving the script also generated the warning: Field PlayLevel.tiles' is never assigned to, and will always have its default valuenull' (This wasn't happening before the change to getting the tiles array passed over.)
I thought perhaps changing 'tiles' in PlayLevel from private to public might make a difference, but now tiles4 throws the error "Array index is out of range."
Would it help to have a foreach loop to pass the values individually, rather than trying to copy the array as a single variable? I'm just not clear on what the problem IS, so I'm not sure how to solve it.

You've got a lot of errors. But without seeing the script, we can't help much.

Please check out how to use code tags, then use that to share the script if you'd like some help.

Oh, that's all you have to do to get code tags? Bah, that's easy, I'll do that going forward.

I left out a lot previously because it didn't directly relate to the tiles array, and as I mentioned, it worked prior to the change. The 'tiles' array was handled locally as a public array of prefabs manually loaded into the Inspector. I've set up the same for 'mainTiles' in the PlayGame script, which looks like this:

using UnityEngine;
using System;
using System.Collections;

[Serializable]
public class PlayGame : MonoBehaviour {

    public static bool allowInput = false;
    public static int currentLevel = 1;
    public static int totalLevels = 10;
    public static int[] scoreValues = { 0, 0, 0, 100, 250, 500, 1000, 2500, 5000 };
    public GameObject[] mainTiles;
}

(There's no functions currently in the PlayGame script, just empty Awake and Update.)

using UnityEngine;
using UnityEngine.UI;
using System;
using System.Collections;
using System.Collections.Generic;
using Random = UnityEngine.Random;

[Serializable]
public class PlayLevel : MonoBehaviour {

    public int[,] gridMap;
    public int[,] tempMap;
    public int cols;
    public int rows;
    public int tileStiles;
    public int lvlScore = 0;
    public int[] scores = { 0, 0, 0, 100, 250, 500, 1000, 2500, 5000 };
    public GameObject[] tiles;    //This should get the prefabs for tile types

    private bool allowInput = false;
    private int matchesFound;
    private Transform tileHolder;
    private PlayGame playGame;

    void Awake() {
        playGame = GetComponent<PlayGame> ();
        Debug.Assert(playGame != null);
    }

    void Start()  {
        GameObject[] tiles = playGame.mainTiles;
        Debug.Log("I found " + tiles.Length + " tiles.  Hooray!");

        StartLevel ();
    }

    void Update() {
    }

    void StartLevel()    //This will set each new level
    {
        allowInput = false;   //Lock out input while setting up the level
        LoadLevelOne ();   //This manually fills the array with specific values for testing
        DisplayMap ();
    }

    void DisplayMap ()  {    //Loop to get tile from the coordinates in gridMap and display
        float cpos = (cols/2);     //Temporary positioning to keep grid centered
        float rpos = (rows/2);
        tileHolder = new GameObject ("Board").transform;
        for (int x = 0; x < cols; x++)  {
            for (int y = 0; y < rows; y++)  {
                int tr = (int)gridMap [x, y] / 10;    //The 10 factor is for future use, storing bonus tile information.
                Debug.Log("Value of tr: " + tr);    //This is showing a legitimate value, 2, 4, etc. well under 7.
                GameObject toInstantiate = tiles [tr];    //ERROR ON THIS LINE: can't seem to find tiles[] value
                GameObject instance = Instantiate (toInstantiate, new Vector3 (x - cpos, y - rpos, 0f), Quaternion.identity) as GameObject;
                instance.transform.SetParent (tileHolder);
                string newname = x + "-" + y;   //Create new name for tile from coordinates
                instance.name = newname;
            }
        }
    }
    void LoadLevelOne()   {   //Included in case, but 'tiles' isn't referenced here
        rows = 9;
        cols = 9;
        tileStiles = 4;
        gridMap = new int[,] { {0, 0, 0, 0, 9, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
            {9, 0, 0, 0, 9, 0, 0, 0, 9}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
            {0, 0, 0, 0, 9, 0, 0, 0, 0} };
        tempMap = new int[cols,rows];
        for (int x = 0; x < cols; x++) {
            for (int y = 0; y < rows; y++) {
                if (gridMap [x, y] == 0)
                    gridMap [x, y] = ((Random.Range (0, tileStiles)) + 1) * 10;
            }
        }
    }

OK, you're still struggling a bit with exactly how variables and fields work.

On line 19 of the above script, you declare a field called "tiles". That's great. But then on line 32, you completely ignore that already-declared tiles field, and declare a new, local variable, which also happens to be called "tiles". As a local variable, it exists only inside the method where it is declared (the Start method in this case). You assign a value to this, and then a couple lines later, the Start method exits, and this "tiles" local variable goes poof.

In the meantime, you call StartLevel. It doesn't declare a "tiles" local variable, so when this code says "tiles", it's referring to the field of that name. The one you declared on line 19, but never assigned a value to (despite assigning to a local variable with the same name, which is a very confusing thing to do). So, yeah, when you try to actually use that field on line 55... it's null. It was null when you declared it, and you never gave it a value, so it's still null.

Now I can clearly see that your intent was to assign to the field in the Start method. But that's not what you actually did; line 32 begins with a type (GameObject[ ]), so it is declaring a new, local variable.

This blog post seems like a reasonable thing to reinforce the difference between local variables and fields (even though it goes into a bit more technical detail than you probably need).

1 Like