Search Unity

Clarifications Requests for Better Understanding

Discussion in 'Getting Started' started by CreedGameStudios, Sep 9, 2016.

  1. CreedGameStudios

    CreedGameStudios

    Joined:
    Aug 26, 2016
    Posts:
    44
    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.
     
  2. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Yes.

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

    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).
     
    Kiwasi and CreedGameStudios like this.
  3. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    @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.
     
    CreedGameStudios likes this.
  4. CreedGameStudios

    CreedGameStudios

    Joined:
    Aug 26, 2016
    Posts:
    44
    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!
     
    JoeStrout likes this.
  5. CreedGameStudios

    CreedGameStudios

    Joined:
    Aug 26, 2016
    Posts:
    44
    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<PlayLevel> to be able to 'read' the script and reference it. Does this also mean PlayLevel needs to GetComponent<PlayGame> 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. :)
     
    Last edited: Sep 17, 2016
  6. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    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!)

    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<PlayLevel>() 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<PlayGame>().

    Nope. Not true. Something got misunderstood there.

    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.)

    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
     
    CreedGameStudios likes this.
  7. CreedGameStudios

    CreedGameStudios

    Joined:
    Aug 26, 2016
    Posts:
    44
    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!
     
    Last edited: Sep 17, 2016
  8. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    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.).
     
  9. CreedGameStudios

    CreedGameStudios

    Joined:
    Aug 26, 2016
    Posts:
    44
    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<PlayGame> 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.
     
  10. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    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!
     
  11. CreedGameStudios

    CreedGameStudios

    Joined:
    Aug 26, 2016
    Posts:
    44
    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).
     
  12. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    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.
     
  13. CreedGameStudios

    CreedGameStudios

    Joined:
    Aug 26, 2016
    Posts:
    44
    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.
     
  14. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    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:

    Code (csharp):
    1. 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.
     
  15. CreedGameStudios

    CreedGameStudios

    Joined:
    Aug 26, 2016
    Posts:
    44
    Oh! I didn't realize I needed to do that each time. I was under the impression that a GetComponent<theotherscriptname> would make things available automatically.
    (I had "playGame = GetComponent<PlayGame> ();" in Awake)

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

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    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<PlayGame>() 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.

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class PlayLevel : MonoBehaviour {
    4.  
    5.     PlayGame playGame;    // handy reference to the PlayGame instance on this object
    6.    
    7.     void Awake() {
    8.         // Find our PlayGame instance (and make sure we have one).
    9.         playGame = GetComponent<PlayGame>();
    10.         Debug.Assert(playGame != null);
    11.     }
    12.    
    13.     void Start() {
    14.         // ...do whatever we want to do with it.
    15.         GameObject[] tiles = playGame.mainTiles;
    16.         Debug.Log("I found " + tiles.Length + " tiles.  Hooray!");
    17.     }
    18. }
     
    CreedGameStudios likes this.
  17. CreedGameStudios

    CreedGameStudios

    Joined:
    Aug 26, 2016
    Posts:
    44
    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 [tr];
    ...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 value `null' (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 tiles[4] (out of 7) 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.
     
  18. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    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.
     
  19. CreedGameStudios

    CreedGameStudios

    Joined:
    Aug 26, 2016
    Posts:
    44
    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:

    Code (CSharp):
    1. using UnityEngine;
    2. using System;
    3. using System.Collections;
    4.  
    5. [Serializable]
    6. public class PlayGame : MonoBehaviour {
    7.  
    8.     public static bool allowInput = false;
    9.     public static int currentLevel = 1;
    10.     public static int totalLevels = 10;
    11.     public static int[] scoreValues = { 0, 0, 0, 100, 250, 500, 1000, 2500, 5000 };
    12.     public GameObject[] mainTiles;
    13. }
    (There's no functions currently in the PlayGame script, just empty Awake and Update.)

    Code (CSharp):
    1.  
    2. using UnityEngine;
    3. using UnityEngine.UI;
    4. using System;
    5. using System.Collections;
    6. using System.Collections.Generic;
    7. using Random = UnityEngine.Random;
    8.  
    9. [Serializable]
    10. public class PlayLevel : MonoBehaviour {
    11.  
    12.     public int[,] gridMap;
    13.     public int[,] tempMap;
    14.     public int cols;
    15.     public int rows;
    16.     public int tileStiles;
    17.     public int lvlScore = 0;
    18.     public int[] scores = { 0, 0, 0, 100, 250, 500, 1000, 2500, 5000 };
    19.     public GameObject[] tiles;    //This should get the prefabs for tile types
    20.  
    21.     private bool allowInput = false;
    22.     private int matchesFound;
    23.     private Transform tileHolder;
    24.     private PlayGame playGame;
    25.  
    26.     void Awake() {
    27.         playGame = GetComponent<PlayGame> ();
    28.         Debug.Assert(playGame != null);
    29.     }
    30.  
    31.     void Start()  {
    32.         GameObject[] tiles = playGame.mainTiles;
    33.         Debug.Log("I found " + tiles.Length + " tiles.  Hooray!");
    34.  
    35.         StartLevel ();
    36.     }
    37.  
    38.     void Update() {
    39.     }
    40.  
    41.     void StartLevel()    //This will set each new level
    42.     {
    43.         allowInput = false;   //Lock out input while setting up the level
    44.         LoadLevelOne ();   //This manually fills the array with specific values for testing
    45.         DisplayMap ();
    46.     }
    47.  
    48.     void DisplayMap ()  {    //Loop to get tile from the coordinates in gridMap and display
    49.         float cpos = (cols/2);     //Temporary positioning to keep grid centered
    50.         float rpos = (rows/2);
    51.         tileHolder = new GameObject ("Board").transform;
    52.         for (int x = 0; x < cols; x++)  {
    53.             for (int y = 0; y < rows; y++)  {
    54.                 int tr = (int)gridMap [x, y] / 10;    //The 10 factor is for future use, storing bonus tile information.
    55.                 Debug.Log("Value of tr: " + tr);    //This is showing a legitimate value, 2, 4, etc. well under 7.
    56.                 GameObject toInstantiate = tiles [tr];    //ERROR ON THIS LINE: can't seem to find tiles[] value
    57.                 GameObject instance = Instantiate (toInstantiate, new Vector3 (x - cpos, y - rpos, 0f), Quaternion.identity) as GameObject;
    58.                 instance.transform.SetParent (tileHolder);
    59.                 string newname = x + "-" + y;   //Create new name for tile from coordinates
    60.                 instance.name = newname;
    61.             }
    62.         }
    63.     }
    64.     void LoadLevelOne()   {   //Included in case, but 'tiles' isn't referenced here
    65.         rows = 9;
    66.         cols = 9;
    67.         tileStiles = 4;
    68.         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},
    69.             {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},
    70.             {0, 0, 0, 0, 9, 0, 0, 0, 0} };
    71.         tempMap = new int[cols,rows];
    72.         for (int x = 0; x < cols; x++) {
    73.             for (int y = 0; y < rows; y++) {
    74.                 if (gridMap [x, y] == 0)
    75.                     gridMap [x, y] = ((Random.Range (0, tileStiles)) + 1) * 10;
    76.             }
    77.         }
    78.     }
    79.  
     
  20. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    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).
     
    CreedGameStudios likes this.
  21. CreedGameStudios

    CreedGameStudios

    Joined:
    Aug 26, 2016
    Posts:
    44
    EUREKA!

    Removing 'GameObject' from line 32 fixed it. Yes, I need a little more practice on local variables, I'll review the post you linked. But I just tested my game and it functioned correctly. Now I understand better, and can use these concepts going forward.

    Hearty thanks and great appreciation to you, Joe!
     
    JoeStrout likes this.