Search Unity

How to get surrounding tiles or game objects?

Discussion in 'Scripting' started by Autoface, Apr 28, 2017.

  1. Autoface

    Autoface

    Joined:
    Sep 23, 2013
    Posts:
    112
    Hi, I'm just working on an auto tiling system. I have most of the logic figured in my head in regards to calculating what sprite changes in to what. I just can't figure out how I would go about getting a game object based on the position in relation to the object that this script is running on.

    A picture might help me explain:

    - All of the squares in the image are instances of terrain prefabs e.g. grass, sand, water. They all run the same script.
    - The green square in the centre represents the game object that the script is running on. It is just a sprite with a script.
    - The blue squares surrounding the green square represent game objects and the position of them in relation to the green square.

    The code below is the loop which cycles through the array in which the game objects are stored. It then runs the autotile script attached to the game objects one by one.
    The array size is also determined by the size of the map because that's how many tiles there are.
    Code (CSharp):
    1.  
    2. terrainIndex = 0;
    3.         for (terrainIndex = 0; terrainIndex < mapWidth * mapHeight; terrainIndex++)
    4.         {
    5.             terrainObject[terrainIndex].GetComponent<AutoTile>().StartAutoTile ();
    6.         }
    7.  

    So now what I'm trying to figure out is how to get the green square to to check each position and compare the game object that was found with its self.

    How does one find a game object base on its position?

    I've read about using Physics.OverlapSphere but i would rather not have any collisions on the prefabs.

    Does any one have any ideas?

    **Additional info**

    I have achieved this before using GameMaker but I feel C# and unity is the way forward with this project.
    This is the code in GML I used to get the sprites surrounding the centre sprite.
    Code (CSharp):
    1.  
    2. iw                 = sprite_width;
    3. w_left           = place_meeting(x-iw,y,object_index);
    4. w_right          = place_meeting(x+iw,y,object_index);
    5. w_up             = place_meeting(x,y-iw,object_index);
    6. w_down        = place_meeting(x,y+iw,object_index);
    7. w_upleft        = place_meeting(x-iw,y-iw,object_index);
    8. w_downleft   = place_meeting(x-iw,y+iw,object_index);
    9. w_upright      = place_meeting(x+iw,y-iw,object_index);
    10. w_downright  = place_meeting(x+iw,y+iw,object_index);
    - place_meeting basically checks the for an object at the location provided but I think it does use collision.



    Thanks!
     
    Last edited: Apr 28, 2017
  2. WarmedxMints

    WarmedxMints

    Joined:
    Feb 6, 2017
    Posts:
    1,035
    There are a few ways of doing it. You could raycast in the 8 directions and check for hits within a set distance.
     
  3. Autoface

    Autoface

    Joined:
    Sep 23, 2013
    Posts:
    112
    I was thinking about using raycast but I'm trying to avoid using collisions. If my maps size was 512 x 512, then the amount of sprites in my scene would be 512*512 which equals 262144. That would mean there would be 262144 collisions in my scene.

    I have been thinking of disabling everything that's out of view but I would like other stuff to be going on even if its not on screen.

    Unless of course if unity automatically disables off screen collisions or something?

    **Edit**

    In fact I think my terrain objects are going to need collisions if I'm going to be able to interact with them so a trace might be the way forward. I would think running a single raycast line would have less impact on performance than a sphere collision would?

    Thanks
     
    Last edited: Apr 28, 2017
  4. laxbrookes

    laxbrookes

    Joined:
    Jan 9, 2015
    Posts:
    235
    So, I set up a quick mock scene to demonstrate a way of achieving this without having to rely on maths and would also accommodate unconventional tile placement.

    I set up tiles in my scene as follows...



    Each one a gameobject with a sprite renderer and a CircleCollider2D.



    The Tile component is as follows...

    Code (csharp):
    1.  
    2. public class Tile : MonoBehaviour {
    3.     public List<GameObject> neighbours = new List<GameObject> ();
    4.     public CircleCollider2D col2d;
    5.     // Use this for initialization
    6.     void Start () {
    7.         FindNeighbours ();
    8.     }
    9.    
    10.     // Update is called once per frame
    11.     void Update () {
    12.        
    13.     }
    14.  
    15.     void FindNeighbours(){
    16.         Tile[] tiles = FindObjectsOfType<Tile> ();
    17.  
    18.         foreach (Tile tile in tiles) {
    19.             if (tile.gameObject.GetInstanceID() != gameObject.GetInstanceID()) {
    20.                 if (col2d.bounds.Intersects (tile.gameObject.GetComponent<Collider2D> ().bounds)) {
    21.                     Debug.Log ("[" + gameObject.name + "] found a neighbour: " + tile.gameObject.name);
    22.                     neighbours.Add (tile.gameObject);
    23.                 }
    24.             }
    25.         }
    26.  
    27.     }
    28. }
    29.  
    ...and produces the following result...



     
  5. Autoface

    Autoface

    Joined:
    Sep 23, 2013
    Posts:
    112
    Wow thanks. That looks like exactly what I need.
    I started to go about this:
    Code (CSharp):
    1. RaycastHit2D w_left = Physics2D.Raycast(transform.position, Vector2.left);
    2.         RaycastHit2D w_right = Physics2D.Raycast(transform.position, Vector2.right);
    3.         RaycastHit2D w_up = Physics2D.Raycast(transform.position, Vector2.up);
    4.         RaycastHit2D w_down = Physics2D.Raycast(transform.position, Vector2.down);
    5.         RaycastHit2D w_upleft = Physics2D.Raycast(transform.position, Vector2.left + Vector2.up);
    6.         RaycastHit2D w_downleft = Physics2D.Raycast(transform.position, Vector2.left + Vector2.down);
    7.         RaycastHit2D w_upright = Physics2D.Raycast(transform.position, Vector2.up + Vector2.right);
    8.         RaycastHit2D w_downright = Physics2D.Raycast(transform.position, Vector2.down + Vector2.right);
    I'll give your method a go! thanks!
     
    laxbrookes likes this.
  6. Autoface

    Autoface

    Joined:
    Sep 23, 2013
    Posts:
    112
    Ok, so I've managed to get it up and running. It may take a while to generate the map but once it's generated it runs very nice on windows and mobile devices.

    I ended up not using the method posted above and used raycasts instead. Just because I feel a bit more comfortable with it.

    Here are some shots:



    It takes a little while to load on mobile devices but it definitely runs smoother on them. But I think that has something to do with unity having the editor window and the game window running at the same time.

    But either way it's a success!

    Thanks.


    **Edit** just fyi using maximise on play does increase the performance. I guess it was down to having the editor window and the game windows being open.
     
    Last edited: Apr 28, 2017
  7. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    That's because this is a horribly inefficient way to find neighbors, in a lot of ways.

    Raycasts are not fast, especially when you have a lot of colliders... and you have a lot of colliders. The raycast function effectively has to loop through all of these. This is giving your initialization an efficiency of O(N^2), which is pretty awful - the bigger your map gets, the worse your startup time will get. Double the number of tiles, startup will be 4x slower. And since a 2D map is effectively sized at O(N^2) already in terms of number of tiles, it's more useful to think of your efficiency as O(N^4), which is super bad - doubling the apparent size (e.g. doubling on both dimensions) will result in an eight times longer load time.

    By my estimate it looks like you've got at least 9,000 tiles just in what's visible in the screenshot. You're well into "this needs to be as efficient as possible" territory with those numbers.

    Sometimes those bad efficiencies are unavoidable, but this is not one of those times.

    The correct way for tiles to find their neighbors - one that avoids all this extra work - is to store references to them in the right data structure upon their creation. That'd be a 2D array. You can access a neighbor in a 2D array instantly - no raycast required at all.

    Something like:
    Code (csharp):
    1. // imagine this is in TileGenerator.cs
    2. public static Tile[,] tileGrid;
    3. public static int width = 100;
    4. public static int height = 100; //these are static so that Tile can easily access them
    5.  
    6. void Awake() {
    7. tileGrid = new Tile[width,height];
    8. for (int x=0;x<width;x++) {
    9. for (int y=0; y<height;y++) {
    10. GameObject newTileGO = Instantiate(tilePrefab);
    11. Tile newTile = newTileGO.GetComponent<Tile>();
    12. tileGrid[x,y] = newTile;
    13. newTile.x = x;
    14. newTile.y = y;
    15. }
    16. }
    17. // in Tile
    18. public int x, y;
    19. public List<Tile> neighbors;
    20. void Start() {
    21. //will run after all tiles have been generated
    22. neighbors = new List<Tile>;
    23. if (x > 0) neighbors.Add(TileGenerator.tileGrid[x - 1, y]);
    24. if (x < TileGenerator.width - 1) neighbors.Add(TileGenerator.tileGrid[x + 1, y]);
    25. //and so on for all other neighbors
    26. }
     
  8. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    Additionally, if your tiles can convert conveniently from worldspace units into int's you can easily retrieve a tile at [x,y] and you'll never have to raycast for a tile. Super fast, super reliable.
     
  9. Autoface

    Autoface

    Joined:
    Sep 23, 2013
    Posts:
    112
    This was what I was asking for advise on in the first post.

    My sprites are 16 pixels by 16 pixels. And I've set them up in editor as 16 pixels per unit. This is utilised as x and y when I'm generating the basic map before the autotiling process as shown below.

    Code (CSharp):
    1. void CalcMap()
    2.     {
    3.         float y = 0.0F;
    4.         while (y < mapHeight)
    5.         {
    6.             float x = 0.0F;
    7.             while (x < mapWidth)
    8.             {
    9.                 //Perlin noise and gradient stuff
    10.                 *********************************
    11.                 *********************************
    12.                 *********************************
    13.                 float sample = samplePerlin;
    14.  
    15.                 if (sample <= 0.2)
    16.                 {
    17.                     terrainObject[terrainIndex] = Instantiate(waterDeep, new Vector3(x, y), Quaternion.identity);
    18.                 }
    19.                 else if (sample <= 0.3)
    20.                 {
    21.                     terrainObject[terrainIndex] = Instantiate(waterShallow, new Vector3(x, y), Quaternion.identity);
    22.                 }
    23.                 else if (sample <= 0.4)
    24.                 {
    25.                     terrainObject[terrainIndex] = Instantiate(sand, new Vector3(x, y), Quaternion.identity);
    26.                 }
    27.                 else if (sample <= 0.45)
    28.                 {
    29.                     terrainObject[terrainIndex] = Instantiate(dirt, new Vector3(x, y), Quaternion.identity);
    30.                 }
    31.                 else if (sample <= 0.6)
    32.                 {
    33.                     terrainObject[terrainIndex] = Instantiate(grassLight, new Vector3(x, y), Quaternion.identity);
    34.                 }
    35.                 else if (sample <= 0.7)
    36.                 {
    37.                     terrainObject[terrainIndex] = Instantiate(grassDark, new Vector3(x, y), Quaternion.identity);
    38.                 }
    39.                 else if (sample <= 0.8)
    40.                 {
    41.                     terrainObject[terrainIndex] = Instantiate(rockDark, new Vector3(x, y), Quaternion.identity);
    42.                 }
    43.                 else if (sample <= 1)
    44.                 {
    45.                     terrainObject[terrainIndex] = Instantiate(rockLight, new Vector3(x, y), Quaternion.identity);
    46.                 }
    47.  
    48.                 terrainIndex = terrainIndex + 1;
    49.                 x++;
    50.             }
    51.             y++;
    52.         }
    53.         terrainIndex = 0;
    54.         for (terrainIndex = 0; terrainIndex < mapWidth * mapHeight; terrainIndex++)
    55.         {
    56.             terrainObject[terrainIndex].GetComponent<AutoTile>().StartAutoTile ();
    57.         }
    58. }
    59.  
    As shown above the x and y values that I use to count the loop are also used for spawn positions. so the first sprites position.x is 0 and the second sprites position.x is 1 and so on.
    So given that I already know the locations of the sprites. How can I get them and compare them.

    for example when I want to check the left object I would say something like:
    Code (CSharp):
    1. leftSprite = otherObject.name at transform.position.x - 1, transform.position.y ;
    But I don't know how to actually write it in c#.

    I just want to get the name of the object that's at the position I specify.

    is it actually possible? Finding an object based on it's position alone?
     
    Last edited: Apr 28, 2017
  10. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    Efficiently, using the raw position? No.

    With ints, using a 2D array (as the code sample in my comment does)? Yes, absolutely. You have to put them into a 2D array upon instantiation, though, for it to be workable.

    If you just change your terrainObject array into a 2D array (see my code for the syntax), it should be pretty easy to write.
     
  11. Autoface

    Autoface

    Joined:
    Sep 23, 2013
    Posts:
    112
    ok so I've got it set up similar to a way you have suggested but it takes longer than using the raycast method which is just about 20 seconds on windows and 2 minutes on mobile. The 2d array method is taking about 20 seconds on windows and about 5 minutes on mobile. I've implemented this 2d array method In an attempt to recreate yours, though I'm having a bit of trouble doing it exactly which is why it's turned out like this.

    This is what happens in the map gen script:
    Code (CSharp):
    1.    
    2.  
    3. void CalcMap()
    4.     {
    5.         for (y = 0; y < mapHeight; y++)
    6.         {
    7.  
    8.             for (x = 0; x < mapWidth; x++)
    9.             {
    10.                 //Perlin noise stuff
    11.                 **************
    12.                 float sample = samplePerlin;
    13.  
    14.                 if (sample <= 0.2)
    15.               {
    16.                 //--- And so on....
    17.             }
    18.         }
    19.        
    20.         AutoTile ();
    21.     }
    22.  
    23.     void AutoTile()
    24.     {
    25.         y = 0.0F;
    26.         for (y = 0; y < mapHeight; y++)
    27.         {
    28.             x = 0.0F;
    29.             for (x = 0; x < mapWidth; x++)
    30.             {
    31.                 terrainObject[(int)x, (int)y].GetComponent<AutoTile>().StartAutoTile ();
    32.             }
    33.         }
    34.     }
    This is the auto tile script:
    Code (CSharp):
    1. GameObject[,] terrainArray;
    2.     int length;
    3.  
    4.     public void StartAutoTile ()
    5.     {
    6.         length = GameObject.Find("GenerateMap").GetComponent<GenPerlin> ().mapWidth;
    7.         terrainArray = GameObject.Find("GenerateMap").GetComponent<GenPerlin> ().terrainObject;
    8.  
    9.         bool w_left = !(x - 1 < 0) && gameObject.name == terrainArray[x - 1, y].gameObject.name;
    10.         bool w_right = !(x + 1 == length) && gameObject.name == terrainArray[x + 1, y].gameObject.name;
    11.         bool w_up = !(y + 1 == length) && gameObject.name == terrainArray[x, y +
    12. //--- And so on....
    13.  
    14. if (!w_left && !w_right && !w_up && !w_down)
    15.         {
    16.             spr = spr44;
    17.         }
    18.  
    19.         if (w_up) {
    20.             spr = spr0;
    21.             if (w_right) {
    22.                 spr = spr4;
    23.                 if (w_down) {
    24. //--- And so on....
    25.  
    26. gameObject.GetComponent<SpriteRenderer> ().sprite = spr;
    27.  
    28.  
    Any ideas? :S
     
    Last edited: Apr 29, 2017
  12. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    Yep, it's GameObject.Find, no question. Kill it! Kill it with fire! I recommend the singleton solution in this case.

    Also, typecasting from float to int, as you do in lines 7-8 of autotile - can be risky - it's possible (though unlikely in this particular case I think) that the position might be something like 3.999999999, and typecasting to int would turn that into 3. In this case, you can use Mathf.RoundToInt to be a little safer.
     
  13. Autoface

    Autoface

    Joined:
    Sep 23, 2013
    Posts:
    112
    You know I did think it could be that. As its happening like thousands of times. I'll see what I can figure out when I get home after work.

    Thanks again!
     
  14. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    I'm just waking up so forgive me if I miss something very obvious.. but as your loop is going up by 1s all the time, I can't understand why you'd make them floats to begin with (then cast 'em back to ints).
     
  15. Autoface

    Autoface

    Joined:
    Sep 23, 2013
    Posts:
    112
    Because x and y is also the position which is a float, and when I'm using it as an index it needs to be an int.

    Though saying that. The fact you don't understand probably means I have less of an understanding of it seeing as I haven't been coding for that long.
     
    Last edited: Apr 29, 2017
  16. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    Well, you've got some good stuff done & good advice from other posters here.

    The position can be set with ints ; ints can be implicitly converted to floats :)
    https://msdn.microsoft.com/en-us/library/y5b434w4.aspx

    Can't say that's going to make it all perfect, but doesn't hurt to fix it if it's not interfering with anything, too.

    How's your project going in general with the updates from this thread + your own? :)
     
  17. Autoface

    Autoface

    Joined:
    Sep 23, 2013
    Posts:
    112
    Just a quick update. It now loads in less than 10 seconds!

    Thanks a million guys! I'll probably be back in a couple of days with another issue but for now. peace!
     
    StarManta likes this.
  18. Autoface

    Autoface

    Joined:
    Sep 23, 2013
    Posts:
    112
    Ahh I see what you mean. Ill deffo take a look in to it if it could cause trouble later on down the line. I'll tell you what though when I was developing this project in gml and game maker the load times would be horrific. I'm not sure whether that was due my knowledge of coding at the time or the application its self. But either way I'm really enjoying learning c#.
     
    Last edited: Apr 29, 2017
  19. methos5k

    methos5k

    Joined:
    Aug 3, 2015
    Posts:
    8,712
    lol that's great. Enjoy :)