Grid Question (Part 3)

OK, I may have researched myself into complete confusion!

Thanks to those who have already reached out trying to help guide me in the right direction so far. Let me please lay out what I’m trying to do and what I’ve tried so far. Some of the things I was asking for in the past, I think I’ve eliminated them based on evolved thinking.

Picture a set of 3D models (all 1x1) laid out in a 5x10 pattern, all aligned up properly along the XYZ axis. Let’s call the lower left tile the Starting Point and the most upper right tile the End Point. In my mind, I’m thinking the Starting Point is 0,0 and the End Point 5,10. Now this thinking might be completely wrong. I don’t know.

Through the course of the game, the character’s movement will be decided by a random number ( later to be a dice roll). The movement will not be like in the game Chute and Ladders where they follow along the path of 0,1 0, 2 0,3, etc… The layout will have predefined points to move to. For example the Starting Point might be 0,0 and subsequent moves might take it to 1,3 or 2,4 or 3,4 or 4,5

At first my mind was wrapped around having all these 3d Tiles placed by an array automatically, and that may still be the answer. Or I could predesign the layout since there will be approximately 15 levels of new designs of the same tiles. I would then call the levels by way of a Game Manager. If I lay out the design myself and place the objects into an array, (Tile 0,0 in the 1st element of the array, Tile 0,1 in the 2nd element of the array and so forth), do I check on which tile the character is on by seeing which element in the array it is on? For example if the “Lava” Tile is placed in the layout in position 3,5…that would end up placing it in the 15th element in the array. If I write a function to check if the character has collided with the Lava Tile, would I have to see if the character is on the 15th element of the array? And if that’s the case, would I have to change the function attached to the Lava Tile for each level to match where it’s located in each array? What if the Lava Tile is placed in position 1,2 next level?

Next I started thinking maybe I don’t need an array, I’ll just place them all in a Grid using the UI panel and the Grid Layout Group script. But that only works with Sprites, not 3D models.

I could lay down a Plane and lay the objects on that.

Honestly, I’ve researched myself to complete confusion. Now the answer might be very easy for someone with more experience, but I’m baffled. I don’t want anyone to write the script for me, I just want a good swift kick in the exact direction I should be looking. I just want to be able to define where the Tiles are so that when a character moves, we know what it’s landing on.

Sorry for making this one so long, I was just trying to be specific.

Thanks again for even reading this far
Doc

The answer: is all of the above!

You can implement things in an almost limitless number of ways and you’ll usually find one or two implementations have a few pros and a few cons, there’s usually not a perfect solution for every case, but generally there’s one that suits your needs best compared to the rest.

From the sounds of your brief description of player movement not following the grid but instead moving to predefined locations on the grid, I’m leaning towards you not needing a grid at all. Instead it sounds like what you want is a list of next possible player locations and each of those locations is assigned to the numbers 1-6 (dice roll). Then, on the result of the roll you simple Vector3.Lerp the player to that arbitrary location and apply whatever effects are assigned to that location, like lava for example.

That new location also specifies what the next 6 possible locations will be and so on…

1 Like

@DocJ , your question was very clear. Thanks for taking the time to make it so.

I’ve done games like this a few times (see Chesster for example), and here’s how I would think about it.

You will certainly want code that can go both ways: from a GameObject in the scene to a grid position, and from a grid position to a GameObject in the scene. The first one is easy; the latter one is slightly harder, and leads to the first major decision you need to make: will you

  • Have some GameManager code that creates all the GameObjects on the fly when the level begins, based on data read from somewhere else? Or,
  • Create the levels by manually arranging GameObjects in a scene, and then having them inform some GameManager of what & where they are?

You can see that in both cases, there will be a GameManager — it’s what is responsible for telling you what GameObject is in a given grid position. It will probably also contain most of the logic for your game. Once you get this thing up and working, your logjam will be broken and your project will begin to flow.

But on that major decision… which to choose? I’m generally more of a data-driven guy, so I would pick #1, but I recommended that as a beginner you should probably do approach #2, and I stick by that. So, here’s how that would work.

You need a couple of scripts that work together here. One script goes on each of your GameObjects. Let’s call it Tile. The Tile script’s job is to keep information about what sort of tile it is, and where it is in the grid. These can be simply public properties — int row, int column for where it is in the grid, for example. This, by the way, is what provides the easy answer to the very first need we identified, converting from a GameObject to a grid position. It’s trivial; you just get a reference to the Tile script, and check its row and column properties.

(How do these properties get set? You can either set them manually when you arrange these things in your scene, or you can have Tile’s Awake method set them based on transform.position.)

OK, then the second script you need is GameManager. It’s responsible for going the other way: converting from a grid position to a Tile (or GameObject). So it will have a 2D array, where each entry in the array is either a Tile reference, or null. So any code in the GameManager can use this array to find out what’s where. Any code anywhere else, can get a reference to the GameManager, and ask it the same thing.

So how do these array entries get set? Each Tile will (in its Start method) inform the GameManager of what it is, and where it is. The GameManager will simply store this info in its array.

To summarize:

  • Create a Tile script; place this on each tile GameObject, so each object knows its own grid position.
  • Create a GameManager script. Make sure this is easily accessible from any other code (e.g. using the Singleton pattern).
  • Have Tiles tell the GameManager where they are.
  • GameManager can then remember and report back what tiles are where.
1 Like

Hi Doc, saw you struggling a bit in your last post and was rooting for you to figure this out on your own. Your stuck in analysis paralysis. There are parts you understand and know what to do and other parts that are vague and veiled in confusion. You could do it, if you knew how to do it, Boy, have I been there and @JoeStrout has always been there for some help and a lot of encouragement. Others too, but Joe stood out.

I slammed this together in 10 minutes and is as basic as I can make it. I made 9 cubes and textured each with a color, and made a prefab from each. Cubes are 1 unit. Remove original cubes from hierarchy. Add an empty game object to your project and call it GameManager. Add the script below to your GameManager object. Add your cube prefabs to the script via the editor.

Here’s a cool trick; select the GameManager object in the hierarchy and click the small lock icon at the top of the inspector. This locks the inspector and keeps the GameManager object selected. Then find and select ALL of your prefabs, drag all prefabs to the field label on the GameManager script. This will add all prefabs in a singe step. Be sure to unlock the inspector.

Now play. A grid will be created initially for the Start function and a new random grid is created each time you press the spacebar. Grid size and grid spacing can be adjusted in the inspector. The array tileRef is a way to reference each tile instantiated.

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

public class GameManager : MonoBehaviour {

        public GameObject[] PFtile; // tile prefabs 9 tiles (0 - 8)
        public float tileSize = 1.0f; // Size height & width
        public int gridWidth;
        public int gridHeight;
        private int[,] tileMap;
        private GameObject[] tileRef; // Keep a reference to each tile

        void Start () {
        BuildTileLayout ();
        BuildGrid ();
        }
       
        void Update () {

        if (Input.GetKeyDown (KeyCode.Space)) {
            for (int i = 0; i < tileRef.Length; i++) {
                Destroy (tileRef [i]);
            }
            BuildTileLayout ();
            BuildGrid ();
        }
        }

        public void BuildTileLayout() {
            tileMap = new int[gridWidth, gridHeight];
            for (int y = 0; y < gridHeight; y++) {
                for (int x = 0; x < gridWidth; x++) {
                    tileMap [x, y] = Random.Range (0, 9);
                }
            }
        }

        public void BuildGrid() { // Draw Grid At Origin
            tileRef = new GameObject[gridHeight * gridWidth];
            float vOffset = (gridHeight * tileSize) * 0.5f - (tileSize * 0.5f);
            float hOffset = (gridWidth * tileSize) * 0.5f - (tileSize * 0.5f);

            for (int y = 0; y < gridHeight; y++) {
                for (int x = 0; x < gridWidth; x++) {
                tileRef[x + (gridWidth * y)] = Instantiate(PFtile[tileMap[x,y]], new Vector3(-hOffset + (x * tileSize), 0.0f, -vOffset + (y * tileSize)), Quaternion.identity);
                }
            }
        }
    }
2 Likes

Good stuff. And to be clear, Bill is showing you how to do the other approach, where GameManager creates all the tiles. And so in that case, there is no need for the tiles to tell GameManager where they are — it already knows, since it created them. And that’s a perfectly valid way to go, too.

But @Bill_Martini , why are you storing the tileRefs as a 1-D array, doing the x + gridWidth * y math, instead of using another 2D array just like tileMap?

1 Like

It was my attempt to illustrate that it can be done in a 1-D array also using that simple formula. Maybe that was confusing but the tileMap array could also be 1-D. And the formula is reversible where an array index can be converted into an x and y value. If it makes it more clear Doc, use a 2-D array.

2 Likes

You know, I put that together very quickly for my reply and had a reason for the 1-D array, and promptly forgot why, when I replied last. It was to be used for the player position. The player starts off at tile zero, a random number is selected and the player moves to that index in the tileRef array. Using the reverse formula provides the players actual X and Y tile location. It just seems easer to traverse a 1-D array and do the math for me. Doc, take what you can use from that example and modify as you need. At least you have something to tinker with and be able to see what the results look like.

1 Like

@JoeStrout and @Bill_Martini you guys are awesome!!

I appreciate the work and the comments you guys have given me. Although I didn’t want anyone to feel I wanted a game scripted for me, I got to say, that script helped a lot as far as understanding the concept of generating the grid. I am impressed that you were able to whip that together in 10 minutes, but I also see know after typing all of it in, how after a while and lots of experience, that kind of ease might come to me as well.

It’s one thing to do the tutorials, but when you’re typing code that has to do with what you personally are trying to accomplish, it really makes much more sense. I did have a couple of comments and questions on the code, then I’ll go further to where my learning curve is.

I had a question as to why there was a Quaternion in there if the tiles were being lined up by the grid. We weren’t rotating them in any way. I noticed that when I typed in the word ‘identity’ it said it was Read Only. Was that just so we could get a reference for its rotation?

At first I was confused in the GameManager field of the editor. I did not know what the Size field was for. And it didn’t seem I was able to drag the prefabs to any of the fields in the editor. Then I realized I could drop them in the gray space near the fields and it changed the Size field to how many prefabs I had in there, as well as created Elements of the prefabs I had dragged over. By the way, great tip about locking the editor because when I selected the GameObject, and then tried to select my prefabs, I lost my GameObject editor window.

I don’t know if this next one was intended or unintentional, but the generated grid was different sizes. When I input the grid width and grid height in their respective fields (i.e 4x9) a grid formed multiple different ways. What I mean is that grid dimensions were random with each press of the space bar. Instead of randomly generating the set size grid in different ways, with the tiles in randomly selected places, the grid was sometimes a 1x1 or 1x6 or 2x5 or even weirder was a 2x4 grid where some of the spaces were left empty ( 1,2 and 2,2 were left empty but 3,2 and 4,2 were filled in).

All in all, great code for me to learn from. I can’t thank you enough. I think that analysis by paralysis phrase fits perfectly for a lot of us newbies. Again, it’s one thing to do the tutorials or research code, but when it’s directly related to what you personally are trying to do, it makes so much more sense. That way the next time I have to do something similar, it’ll be much easier because I’ve done it before for what I specifically have in mind. I hope that makes sense.

Even though I really appreciate the random generator for my grid, I think I will be better suited laying them all out in a pre-defined manner. This way I get them exactly how I want them and can even add more decorative touches to them. I will have my GameManager call upon them as different scenes at the appropriate time in the game.

I understand the concept of letting the tile tell GameManager what kind of tile it is and whether there are any functions that need to occur based on being on that tile.

I have to see about wrapping my head around the concept of both the tile and the GameManager communicating the location in the grid. I have to see where in the script the tile tells the GameManger its location in the grid if it’s not going to be generated randomly.

And quick question about the first response to this question where it was recommended a List be used. What are your thoughts on that?

Again, I can’t thank you guys enough for the guidance so far. My secondary goal is to someday be able to pay this forward to another newbie.

Quaternion.identity is a static method that returns you the “identity” quaternion — that is, the quaternion which, when multiplied by another quaternion, gives you back exactly that other quaternion. In other words, it does no rotation at all.

It’s called “identity” in the mathematics sense; 1 is the multiplicative identity for real numbers, because x * 1 = x. The additive identity is 0, because x + 0 = x. So this is the quaternion identity, q * i = q.

It just so happens that if you pass a position into Instantiate, you have to also pass a rotation. Don’t want any rotation? Pass Quaternion.identity.

Yep. Or you can set the Size to however many entries you want, and then drag the prefabs in one by one. Experiment and learn to work with the array editor in the Inspector — you’ll use it a lot. :slight_smile:

Yep, that’s a good trick! You’re starting to stray out of Beginner waters already.

OK, so in this case you can either follow the approach I outlined, where you place the tiles in the scene editor and they just tell the GameManager where they are; or you can create some other way to define your grid as plain old data, and then have it create the tiles that correspond to that data.

The tile would do this in its Start method. Something like this:

void Start() {
    GameObject.FindObjectOfType<GameManager>().NoteTile(this, row, column);
}

You’ll need to have a NoteTile method on your GameManager script, of course. And using FindObjectOfType like this is not ideal, but I’m keeping it simple — you can research the Singleton pattern later. For now always focus on the simplest solution that works.

Honestly, I recommend you ignore that one. I don’t think he fully grasped what you were trying to do.

Best,

  • Joe
2 Likes

I think @JoeStrout answered your questions very well, but if you still have more questions, ask.

I’d like to point out my example was a very quick and dirty bit of code. I made several coding no-no’s and should be looked at as a concept rather than code you want to emulate. First and foremost there was no error checking and probably allowed the odd behavior you encountered. Secondly, commenting was minimal, always comment what you do. Code you write today may make perfect sense now, but come back to it in a year and you’ll be saying ‘what the hell did I do here?’.

So I added a few improvements, more commenting, but still no error checking to a new script. I also added a player to the mix. You’ll need to add a player object to the scene. I used a sphere scaled to 1. Set it at origin and add it to the new script. Next go back to your tile prefabs and give each one a unique tag. Since I colored the tiles, each tile tag is the color of the tile. (Red tile tag is 'Red")

Here is why I’m leaning to the random design method. First, it’s used a lot in games to prevent a player from ‘learning the moves’ to get through a level or circumvent intended game play. Saves you a lot of time building custom layouts and provides a maximum number of permutations. But purely random layouts don’t work either as every game has rules and randomizing simply does not take into account those rules. But you can accommodate for the game rules in the tileMap creation and still have some randomness.

First we need actual game rules, and should have been established before starting a project. Since were ‘winging it’ here I’m now creating some rules for my example. Rule #1 - land on a green tile and move one tile forward. Rule #2 - land on a red tile and move one tile back. To keep this simple that’s all the rules for the game.

Randomly creating any ol’ tile is going to break the rules. First, the start tile should not be red or green, and the last tile shouldn’t be either. But worse is the green/red combo. If a random sequence produces a green tile followed by a red tile, you create an endless loop paradox. Land on a green tile and move one space forward and it’s red then move back one space to green and so on. So a condition needs to be tested when generating the layout to prevent a green followed by a red. The reverse is is acceptable and is not a problem. So a few checks while creating a random grid give you a nice way to mix it up with minimal work and maintain the integrity of the game rules at the same time.

I know you are still wobbly on this, but I added some embellishments, which I hope add some clarity to the big picture. I’m hoping it doesn’t muddy the waters any more than it is. Note that I’ve force the layout to have a specific tile for the start and end but all others are random. I did not add testing for the ‘endless loop’ scenario as I thought it would be a good example for you to figure out. There is no player movement, but player is placed at tile zero initially. You may also have questions about my changes, so ask away.

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

public class GameManager : MonoBehaviour {

        public GameObject[] PFtile; // tile prefabs 9 tiles (0 - 8)
        public GameObject player; // player Object

        public float tileSize = 1.0f; // Size height & width
        public int gridWidth = 9;
        public int gridHeight = 4;

        private int[,] tileMap;
        private GameObject[] tileRef; // Keep a reference to each tile
        private int tilePlayerIsOn;
        private Vector3 playerHeight = new Vector3(0,1.0f,0); // PlayerHeight
        private Material plMat; // Player Material

        void Start () {
        tilePlayerIsOn = 0; // Starting Tile Position
        plMat = player.GetComponent<MeshRenderer> ().material; // Get A Reference To Player Material
        BuildTileLayout ();
        BuildGrid ();
        }
       
        void Update () {

        if (Input.GetKeyDown (KeyCode.Space)) {
            for (int i = 0; i < tileRef.Length; i++) { // loop through all previous tiles and remove them
                Destroy (tileRef [i]);
            }
            BuildTileLayout (); // make a new random tile layout
            BuildGrid (); // make the new layout
        }
        }

        public void BuildTileLayout() {
            tileMap = new int[gridWidth, gridHeight];
            for (int y = 0; y < gridHeight; y++) {
                for (int x = 0; x < gridWidth; x++) {
                tileMap [x, y] = Random.Range (0, PFtile.Length);
                }
            }
            tileMap [0, 0] = 0; // Force Starting tile to prefab 0
            tileMap [gridWidth - 1, gridHeight - 1] = PFtile.Length - 1; // Force Ending tile to last prefab in array
        }

        public void BuildGrid() { // Draw Grid At Origin And Set Player On Tile #0
            tileRef = new GameObject[gridHeight * gridWidth];
            float vOffset = (gridHeight * tileSize) * 0.5f - (tileSize * 0.5f);
            float hOffset = (gridWidth * tileSize) * 0.5f - (tileSize * 0.5f);

            for (int y = 0; y < gridHeight; y++) {
                for (int x = 0; x < gridWidth; x++) {
                tileRef[x + (gridWidth * y)] = Instantiate(PFtile[tileMap[x,y]], new Vector3(-hOffset + (x * tileSize), 0.0f, -vOffset + (y * tileSize)), Quaternion.identity);
                }
            }
            player.transform.position = tileRef [tilePlayerIsOn].transform.position + playerHeight; // Set player position from tile position
            Color c = tileRef [tilePlayerIsOn].GetComponent<MeshRenderer>().material.color; // Get Tile Color
            // plMat.color = c; // Make Player Color Same As Tile Color
            plMat.color = new Color(1.0f - c.r, 1.0f - c.g, 1.0f-c.b, c.a); // Make Player Color Inverse Of Tile Color
            string tileColor = tileRef [tilePlayerIsOn].tag; // Get Tile Tag
            Debug.Log ("I Am On Tile " + tilePlayerIsOn.ToString () + " And It's Color Is " + tileColor); // Revaeal Player Info
        }
    }
2 Likes

@Bill_Martini is clearly intent on repaying whatever karma he borrowed as a newbie! Great stuff, Bill. :slight_smile:

2 Likes

@JoeStrout , I’ve got a long way to go before I can fill your shoes.

Hey Doc, If your hell bent on manually placing tiles…

Replace the code in the ‘BuildTileLayout’ function with this;

tileMap = new int[,] {{1,2,3,4,5,1,2,3,4}, {8,7,6,5,0,3,4,5,6}, {1,8,2,7,3,6,4,5,0} {2,4,6,8,1,3,5,7,0}}; // Level 1 Tile Layout
            // tileMap = new int[,] {{2,3,5,4,5,1,6,0,8}, {0,5,3,5,0,2,4,3,1}, {7,8,4,1,3,6,0,5,0} {1,2,3,1,3,2,4,8,5}}; // Level 2 Tile Layout

Each digit in the array is an index into the PFTile array the same as before but you fill in the data. The example above assumes a 9x4 grid. Each sequence within brackets are a row and each bracketed sequence is a column. You are now responsible for all game rules.

1 Like

@Bill_Martini @JoeStrout
Lady Karma is definitely smiling down upon you both :smile:. I will spend this weekend going over everything you guys have been very gracious about doing for this newb.

Bill, if I can eventually create a golf game that not only myself, but others might enjoy too, then you’ll have done more for this golf nut than you can possibly know. That will go a tremendous way to filling up that old pair of shoes Joe left you to fill;)

Here is a 2D golf project that is currently being promoted in the asset store.

While I don’t recommend someone starting out to deconstruct a project for learning purposes, it’s $15 and maybe it might help you. Keep in mind that it’s most likely will be a waste of $15 and some time. You’re just getting into the fray, until you’ve finished a couple of your own projects it’s difficult to learn that way.

1 Like

Thanks Bill. I haven’t seen that one yet, but I have seen some others and was considering buying them to see if I could learn from breaking them down. But then I had the same thought as you, I would probably waste time and money. Being too new to understand what they did and why would more than likely being a waste of time.

Again, I have to say learning from you and Joe has been invaluable because of the conversation back and forth, as well as the comments on the code. And I’ve even started to see where I can apply what I’ve learned from you guys so far in other games I have in mind.

I have my work cut out for me this weekend to read over these last few messages and code and really get it to sink in.

Thanks again. Have a great weekend.

My advice to you is to go back and do several more tutorials. Not view but do! Each one should be a complete working version of the tutorial. Debugging is a large part of programming and this can only be learned by running into code issues, identifying the cause, and correcting the condition. As a beginner it’s hard to find spelling errors (case matters), syntactical errors, missing object or variable references, and missing frameworks. Completing the tutorials on your own gives you ample opportunity to make, find, and fix problems.

If your game idea is so great, it will still be great in six months and with a little hard work you could be well equipped to make that game, and without asking for assistance in that time.

At this point if you have questions, you should proved specific questions, provide code you tried, and provide complete error messages if any. I think it abundantly clear there is some always here to assist.

So I’m ready to put this to bed but I’d like to add two more things…

I have two more installments of the script I’ve been sharing. The next one is texturing the tiles with a little more than just a color. It will require your art skills in your favorite paint / image editing app. And the last installment is moving the player object around the grid. And of course a surprise or two. If anyone is interested I’ll post, so speak up.

Lastly the script I’ve provided so far is just a few dozen lines of code in a single script. While it may not be immediately apparent, it demonstrated a wide range of real world every day things that as a developer you will use in most every project. Here is a list of thing you should walk away with from this simple example. You learned how to;

• Interact with Unity’s inspector panel.
• Add a script to a gameobject.
• Add gameobject references from both in scene and prefabs into a script. Giving the script the ability to interact with those objects.
• Get a reference to a component on an object.
• Set a value or value set on a component on an object.
• Create a randomly generated set of data and use in conjunction with gameobjects.
• Override specific data points in random data.
• Procedurally instantiate gameobjects and set their position and orientation.
• Keep a reference to all instantiated objects and demonstrate interaction with the gameobject.
• Provide runtime feedback of object state via Debug.Log.
• Destroy instances of gameobjects.
• Move a gameobject to a specific position.
• Align a gameobject to another gameobject.
• Provides a starter script template than can be modified and or added upon. It illustrates that a few lines of code can produce a ‘proof of concept’ idea quickly.

The list would be bigger if those last two installments were included. Go back and look for the trees in that forest, they’re there.

2 Likes

Bill, it’s ironic you said that because after work yesterday I spent my free time going back to the Tic Tac Toe tutorial because of the aspects of the Grid and Game Manager. It’s amazing how differently the tutorial is to me now that I’ve had guidance and help from you and @JoeStrout . A perfect example is the locking of the inspector to be able to drag objects into the editor. It seems I had already learned that, but since everything is so new and you’re trying to absorb a lot, it’s very easy to forget things you’ve learned. More so though, I think having had guidance, things you go back to make a LOT more sense.

And you’re right, these things you’ve already helped with I can see me using over and over again. Some things are a lot more clear too. For example, it’s nice now that I’ve already gotten to recognize code for a Loop just off the first few variables in the line and don’t have to look back at the tutorials or your examples to finish it. Of course, I always welcome whatever work you’ve put together for me to learn from, I also want to try my best on this and experiment/fail. Sometimes the hardest part of the journey is the first step. Thank you so much!! I’ll keep you posted.

2 Likes