Search Unity

How do you randomly spawn an object on the unused places of a procedurally generated tilemap?

Discussion in '2D' started by Deleted User, Dec 7, 2019.

  1. Deleted User

    Deleted User

    Guest

    Does anybody know how to randomly spawn the player (or anything else) on a procedurally generated platformer tilemap so that they don't spawn inside the walls? I really need help about that. Sigh.

    My idea is getting the unused places on the tilemap after the platforms have been generated, put them in a new list and then use that list to spawn the player but I don't know how to do that. I'm not too knowledgeable at coding yet.

    To make things a little more complicated, the code I use to generate the tilemaps involves using integers arrays and instantiating objects on a map, whatever the map, involves using Vector3.

    Here is the code I use to generate the maps (thanks to Ethan Bruins on the Unity Blog and his articles about randomly generating tilemaps); several maps are generated on the top of each other. I collapsed the switch block because it's just a list to choose from.

    Code (CSharp):
    1.     public void GenerateMap()
    2.     {
    3.         levelMapsList = new List<int[,]>();
    4.  
    5.         //Work through the List of mapSettings
    6.         for(int i = 0; i < mapSettings.Count; i++)
    7.         {
    8.             int[,] map = new int[width, height];
    9.             float seed = mapSettings[i].randomSeed ? Time.time.GetHashCode() : mapSettings[i].seed.GetHashCode();
    10.  
    11.             //Generate the map depending on the algorithm selected
    12.             switch(mapSettings[i].algorithm) ...
    13.  
    14.             //Add the map to the list
    15.             levelMapsList.Add(map);
    16.         }
    17.  
    18.         //Allows for all of the maps to be on the same tilemap without overlaying
    19.         Vector2Int offset = new Vector2Int(-width / 2, (-height / 2) - 1);
    20.  
    21.         //Work through the list to generate all maps
    22.         foreach(int[,] item in levelMapsList)
    23.         {
    24.             Algorithms.RenderMapWithOffset(item, tilemap, ruleTile, offset);
    25.             offset.y += -height + 1;
    26.         }
    27.     }
    28.  
    29.     /// <summary>
    30.     /// Renders a map using an offset provided, Useful for having multiple maps on one tilemap
    31.     /// </summary>
    32.     /// <param name="map">The map to draw</param>
    33.     /// <param name="tilemap">The tilemap to draw on</param>
    34.     /// <param name="tile">The tile to draw with</param>
    35.     /// <param name="offset">The offset to apply</param>
    36.     public static void RenderMapWithOffset(int[,] map, Tilemap tilemap, TileBase tile, Vector2Int offset)
    37.     {
    38.         for(int x = 0; x < map.GetUpperBound(0); x++)
    39.         {
    40.             for(int y = 0; y < map.GetUpperBound(1); y++)
    41.             {
    42.                 if(map[x, y] == 1)
    43.                 {
    44.                     tilemap.SetTile(new Vector3Int(x + offset.x, y + offset.y, 0), tile);
    45.                 }
    46.             }
    47.         }
    48.     }

    Code (CSharp):
    1.     private void InstantiatePlayer()
    2.     {
    3.         int[,] map = new int[width, height];
    4.         for(int x = 0; x < width; x++)
    5.         {
    6.             for(int y = 0; y < height; y++)
    7.             {
    8.                 if(map[x, y] == 0)
    9.                 {
    10.                     int spawnLocationX = x;
    11.                     int spawnLocationY = y;
    12.                     playerClone = Instantiate(playerPrefab, new Vector3(spawnLocationX, spawnLocationY, 0f), Quaternion.identity);
    13.                     playerFollowCamera.m_Follow = playerClone.transform;
    14.                     Debug.Log("instantiating player");
    15.                     return;
    16.                 }
    17.             }
    18.         }
    19.     }

    Both work fine but the player spawns inside the walls of the tilemap on a regular basis. I need to replace:
    Code (CSharp):
    1.         int[,] map = new int[width, height];
    by an array that contains only the unused places on the tilemap that was procedurally generated so that the player doesn't spawn inside the walls.

    I could just use a way for the player to regenerate the maps in game but I need a reliable code that can also generate objects on a map, obstacles, traps, and so on.

    Thank you for your help if you can help me. :)

    Sans-titre-2.jpg

    Sans-titre-3.jpg
     
    Last edited by a moderator: Dec 7, 2019
  2. eses

    eses

    Joined:
    Feb 26, 2013
    Posts:
    2,637
    @APSchmidt

    I didn't yet read your other code block, but your player placement wouldn't work as it is, as it doesn't make any sense. And you didn't address that either. See this, what is currently happening:

    Code (CSharp):
    1. void InstantiatePlayer()
    2. {
    3.     // Why empty array 2d here?
    4.     int[,] map = new int[width, height];
    5.  
    6.     for(int x = 0; x < width; x++)
    7.     {
    8.         for(int y = 0; y < height; y++)
    9.         {
    10.             // this will be zero always, from first array item
    11.             if(map[x, y] == 0)
    12.             {
    13.                 // place player
    14.             }
    15.         }
    16.     }
    17. }
     
  3. Deleted User

    Deleted User

    Guest

    Like I said, it does work, except that the player spawns inside the walls from time to time.

    I don't see the difference between the code I posted and yours.
     
  4. eses

    eses

    Joined:
    Feb 26, 2013
    Posts:
    2,637
    @APSchmidt

    Well there isn't I just made it obvious that you are creating an empty array where each cell will be 0. When you start your for loop, the first tile at 0,0 will be 0, hence it will be selected.
     
  5. Deleted User

    Deleted User

    Guest

    Yeah, I'm aware of that, it's the whole object of this thread. I need a way to generate an array that would contain only the unused places of the generated map. I can edit my first post to make it clearer, thank you for pointing this out. :)
     
  6. eses

    eses

    Joined:
    Feb 26, 2013
    Posts:
    2,637
    @APSchmidt

    But to answer to your question...

    "I need a way to generate an array that would contain only the unused places of the generated map"


    To get tiles that are valid, you could do something like this;

    Get the bounds for certain area or the whole tilemap.

    Then iterate those bounds for each position. Note - you might have to iterate slightly more than bounds area, if you want to capture topmost positions for example.

    Then you can have a pretty "blind" heuristic that decides if location is valid - check if position is empty and if there is floor below. You can extend / replace this rule by making it a delegate to be more specific to place some specific things.

    If location fills the requirements of previous step, store it to list.

    Do the same for all tiles and you have a list of your locations.

    I've done something similar and it worked for me.
     
  7. Deleted User

    Deleted User

    Guest

    Well, thank you for that! I need a crash course in how to do that now. :)
     
  8. eses

    eses

    Joined:
    Feb 26, 2013
    Posts:
    2,637
    Deleted User likes this.
  9. Deleted User

    Deleted User

    Guest

    Thank you! :)
     
  10. MisterSkitz

    MisterSkitz

    Joined:
    Sep 2, 2015
    Posts:
    833
    Why not create a Transform[] array and use FindObjectsOfType<Type>() to locate all of the areas the player can't spawn. Then spawn the player to an area that != the transform.position of the elements within the array?
     
  11. Deleted User

    Deleted User

    Guest

    I still need to try this but I'd like to ask advice to @ChuanXin about that. More brains are always useful. ;)
     
  12. ChuanXin

    ChuanXin

    Unity Technologies

    Joined:
    Apr 7, 2015
    Posts:
    1,068


    Each
    item
    should contain the information on whether a Tile exists (
    item[x, y] == 1
    ) on the map or not (
    item[x, y] == 0
    ).

    For the
    InstantiatePlayer
    , perhaps the spawnLocation should have the same
    offset
    as applied in the map generation to get the right position?

    -

    The solutions in this would work as well, where you would check if the TileBase at the location is
    null
    . Using
    InstantiatePlayer
    as above would work.

    -

    As an extension of this, you could check if the position above an existing Tile is empty or not. This would give you the position of the top of an empty platform where you can place your Player or other obstacles.

    I have a brute force example here (https://github.com/ChuanXin-Unity/ProceduralPatterns2D) where I place flowers at the top of empty platforms and mushrooms at the bottom of empty platforms. I suppose you could substitute them with other objects randomly? The main scene would be Assets/Foilage.unity and scripts in Assets/Other Assets/Scripts.

    -

    If you do not want to loop through and locate cells which are empty, and the Tilemap has a TilemapCollider2D, you could try randomly choosing positions in the Tilemap and running
    Physics2D.OverlapBox
    (https://docs.unity3d.com/ScriptReference/Physics2D.OverlapBox.html) and place player where no collision is detected? This is probably useful if you are only trying to spawn the player, and not multiple objects.
     
    Deleted User and eses like this.
  13. eses

    eses

    Joined:
    Feb 26, 2013
    Posts:
    2,637
    @ChuanXin

    "As an extension of this, you could check if the position above an existing Tile is empty or not. This would give you the position of the top of an empty platform where you can place your Player or other obstacles."

    That is exactly what I already wrote and have done earlier. By scanning optionally several tiles around, it is more expensive but possible to get rule tile like way to place items.

    "If you do not want to loop through and locate cells which are empty"

    BTW - doesn't allPositionsWithin do exactly this? It AFAIK it only gets the tiles from the bounds area, I might be wrong - edit - no it doesn't.

    The code I have works something like this. One tile gap is not a valid position.The sprite is just some random CC0 sprite.

    player_placement4.gif
     
    Last edited: Dec 9, 2019
    MisterSkitz likes this.
  14. Deleted User

    Deleted User

    Guest

    @ChuanXin @eses Thanks you very much for your contributions; I'll take a look at all of them and see what I can do. :)
     
  15. Deleted User

    Deleted User

    Guest

    Hi everyone!

    I wish you a good new year! I cannot believe almost a month has passed since the last time I posted here! I had to fix some problems in my game but now I can come back to this. :)

    Thank you! This looks very promissing; I just downloaded your project, I'll take a close look and see what I can do with it. :)
     
  16. Deleted User

    Deleted User

    Guest

    @ChuanXin Nicely done! I have found the scripts and the rule tile with the mushroom and flower tiles with the offset set to y = -1 so that the mushrooms are upside down.

    Sans-titre-1.jpg

    Now, I have to make it so that the rule tile draws prefabs. The player is already spawned when I generate the map; they'll just have to be move to a position free of tile on the map and on a surface.

    I also have objects I'll have to spawn, like lights, that are also prefabs and I'd like these lights to be added in a place that would be at least 4 units high (two above the ground and one under the bottom of the upper platform.

    Adding a game object to "Default game object" in the rule tile works somehow but the object is not created each time the platforms are created.
     
    Last edited by a moderator: Jan 3, 2020
  17. Deleted User

    Deleted User

    Guest

    I've rewritten your FoilageAddTileLayer and PaintOnLayerRuleTile in order to understand how they work together and I'm using them in my game; I hope doing this is not a problem, even if one day I decide to sell this game?

    My game starts on a scene where you have to choose between two players (for now, maybe more in the future).

    So far, I've managed making it so that the Default Game Object in the Rule Tile is set to the selected player by code:

    Capture.JPG

    Code (CSharp):
    1. public class LevelGenerator : MonoBehaviour
    2. {
    3.     public RuleTile ruleTile;
    4.  
    5.     public void GenerateMap()
    6.     {
    7.         GameObject player = GameObject.FindGameObjectWithTag("Player");
    8.         ruleTile.m_DefaultGameObject = player;
    9.     }
    and it works, the instantiated game object is the desired player. Disabling the supernumerary player wouldn't be a problem but I'd rather moving the existing player instead of instantiating a new one.

    That's one thing, but then there is this recurring problem where the level is generated but the player is just not there; I have to regenerate the level several times so that it appears and most times there are several of them... I need to find a way so that the player is generated each time the level is and in only one instance.

    I've noticed that the instantiated game object automatically comes with a tile under its feet:

    Capture.JPG

    In my game, this makes my player take three units vertically and one horizontally. My guess is that the player is not instantiated if certain conditions are not met (too less room for example?); I'll have to investigate this.

    I'll have good use for the plaint tiles list in the future but I'll have to make it so that the tile are not painted everywhere on the scene as it is for now.

    Thanks anyway for sharing your hard work @ChuanXin! :)
     
    Last edited by a moderator: Jan 5, 2020
  18. Deleted User

    Deleted User

    Guest

    I've tried the first code on that page; it works, mostly, except that the player is generated far outside the bounds of the generated map. It's a progress but I need to find a way to confine the player inside strict borders on the map, and make it spawn on a tile, not in the air...

    I'm not done yet but thanks to you I'm going to make it! :)
     
  19. eses

    eses

    Joined:
    Feb 26, 2013
    Posts:
    2,637
    @APSchmidt

    If you get your Tilemap bounds, it might contain empty area. Once you paint a tile, and then erase it, the bounds are already extended to cover this coordinate... so maybe this is the reason for your character spawning in empty area?

    It might be inside your bounds, but there are no tiles anymore. You can try compressing Tilemap bounds from inspector (also from code), by right clicking Tilemap components menu icon, then select compress bounds.

    Anyway, when you get the bounds like this:
    Code (CSharp):
    1. BoundsInt bounds = tilemap.cellBounds;
    You can then simply check that your are inside bounds, just like in that code snippet. Coordinate is inside bounds if its position is greater than bounds.min, and smaller than bounds.max values in both x and y axis. Then it is a matter of finding a cell inside this area, where you don't have tile / have some specific tile.
     
  20. Deleted User

    Deleted User

    Guest

    Hi! :)

    It seems that the player is inside the bounds; I tried three times in a row and the player's y position is always within the size of the map. Same thing, after compressing the map bounds.
     
  21. Deleted User

    Deleted User

    Guest

    Unfortunately, it's not; the player spawns outside the map bounds. Since I'm tired of this and I cannot find how to fix it, I'm doing something else.

    I'm going to:
    1. create an array of possible spawning positions for the player,
    2. once a position has been chosen, all the positions around it will be cleaned of all the tiles already placed and tiles will be placed under the player's feet, if there are none,
    3. if, although tiles have been delete around it, the player is still imprisoned inside walls, they will be able to dig their way into the walls, using an animation I've already created.
    This should fix my problem. At last. :)