Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice

Resolved World Generation Script Error - "The Given Key was not Present in the Dictionary"

Discussion in 'Scripting' started by EnderSkelly34, Aug 13, 2021.

  1. EnderSkelly34

    EnderSkelly34

    Joined:
    Jun 13, 2020
    Posts:
    27
    Hello, I am making a 2d game. I decided to use perlin noise to generate my world. (I am relatively new, and this is the first time I am using dictionaries, as opposed to lists or arrays). Here is my script:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.Tilemaps;
    5. using UnityEditor;
    6.  
    7. public class WorldGeneration : MonoBehaviour
    8. {
    9.  
    10.     Dictionary<int, GameObject> tileSet;
    11.     Dictionary<int, GameObject> tileGroups;
    12.     public GameObject[] tiles;
    13.  
    14.     public int width;
    15.     public int height;
    16.  
    17.     List<List<int>> noiseGrid = new List<List<int>>();
    18.     List<List<GameObject>> tileGrid = new List<List<GameObject>>();
    19.  
    20.     public float magnification;
    21.     public int xOffset;
    22.     public int yOffset;
    23.  
    24.     private void Start()
    25.     {
    26.  
    27.         createTileSet();
    28.         createTileGroups();
    29.         generateWorld();
    30.  
    31.     }
    32.  
    33.     void createTileGroups()
    34.     {
    35.  
    36.         tileGroups = new Dictionary<int, GameObject>();
    37.         foreach (KeyValuePair<int, GameObject> tile in tileSet)
    38.         {
    39.  
    40.             GameObject tileGroup = new GameObject(tile.Value.name);
    41.             tileGroup.transform.parent = gameObject.transform;
    42.             tileGroup.transform.localPosition = new Vector3(0, 0, 0);
    43.             tileGroups.Add(tile.Key, tileGroup);
    44.  
    45.         }
    46.  
    47.     }
    48.  
    49.     void createTileSet()
    50.     {
    51.  
    52.         tileSet = new Dictionary<int, GameObject>();
    53.         for (int i = 0; i < tiles.Length; i++)
    54.         {
    55.  
    56.             tileSet.Add(i, tiles[i]);
    57.  
    58.         }
    59.  
    60.     }
    61.  
    62.     void generateWorld()
    63.     {
    64.  
    65.         for (int x = 0; x < width; x++)
    66.         {
    67.  
    68.             noiseGrid.Add(new List<int>());
    69.             tileGrid.Add(new List<GameObject>());
    70.  
    71.             for (int y = 0; y < height; y++)
    72.             {
    73.  
    74.                 int tileId = getIdUsingPerlin(x, y);
    75.                 noiseGrid[x].Add(tileId);
    76.                 createTile(tileId, x, y);
    77.  
    78.             }
    79.  
    80.         }
    81.  
    82.     }
    83.  
    84.     void createTile(int tileId, int x, int y)
    85.     {
    86.  
    87.         GameObject tilePrefab = tileSet[tileId];
    88.         GameObject tileGroup = tileGroups[tileId];
    89.         GameObject tile = Instantiate(tilePrefab, tileGroup.transform);
    90.  
    91.         tile.name = string.Format("tile_x[0]_y[1]", x, y);
    92.         tile.transform.localPosition = new Vector3(x, y, 0);
    93.         tileGrid[x].Add(tile);
    94.  
    95.     }
    96.  
    97.     int getIdUsingPerlin(int x, int y)
    98.     {
    99.  
    100.         float rawPerlin = Mathf.PerlinNoise((x - xOffset) / magnification, y - yOffset / magnification);
    101.         float clampPerlin = Mathf.Clamp(rawPerlin, 0.0f, 1.0f);
    102.         float scalePerlin = clampPerlin * tileSet.Count;
    103.         if(scalePerlin > tileSet.Count - 1)
    104.         {
    105.  
    106.             scalePerlin = tileSet.Count - 1;
    107.  
    108.         }
    109.  
    110.         return Mathf.FloorToInt(scalePerlin);
    111.  
    112.     }
    113.  
    114. }
    115.  
    Here is my Full Error:

    KeyNotFoundException: The given key was not present in the dictionary.
    System.Collections.Generic.Dictionary`2[TKey,TValue].get_Item (TKey key) (at <9577ac7a62ef43179789031239ba8798>:0)
    WorldGeneration.createTile (System.Int32 tileId, System.Int32 x, System.Int32 y) (at Assets/Scripts/World/WorldGeneration.cs:87)
    WorldGeneration.generateWorld () (at Assets/Scripts/World/WorldGeneration.cs:76)
    WorldGeneration.Start () (at Assets/Scripts/World/WorldGeneration.cs:29)


    Any help would be appreciated, thanks!
     
  2. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,589
    Isn't the "tileSet" dictionary set up with a key that ranges from "0 to tiles.Length"? You're passing in a key created by "int tileId = getIdUsingPerlin(x, y)" unless I'm tired and I'm not following.
     
  3. Yoreki

    Yoreki

    Joined:
    Apr 10, 2019
    Posts:
    2,606
    It's telling you that the dictionary tileSet has no such key, refering to line 87. The key there is 'tileId', which as MelvMay stated above, you calculate via the getIdUsingPerlin function. Inside that function the actual perlin noise is clamped to the range of 0 to 1, which you then multiply with tileSet.Count. And that's where the mistake lies.
    Imagine you have 10 tiles. What is tileSet.Count? 10 of course. But what's the last index in an array containing those? We start with 0, so the last indice is 9, and thus that's also the last key you set ;)
    The problem now is that you can run into the case of actually returning 10, which would normally result in an IndexOutOfBoundsException, but in this case, the dictionary does not find the key 10.
    So instead you want to scale with 9 there, or tileSet.Count-1 in other words.

    Why do you have to clamp Perlin anyways? Most coherend noise functions i used so far return values in a range of 0 to 1 by default. In which case you would only very rarely run into this problem, basically only for perlin return values of exactly 1. But if your function returns a different range, clamping doesnt really solve that problem and would result in 1 being returned a lot more often. Check if that's the case. If not, you can remove the clamp there. If it is, you need to scale the return value of your perlin function with its actual range, ie if it can return values to 10, you need to divide the result by 10, and similarly if it can return negative numbers.

    Edit: Didnt see that you basically intend to get the value back in range with scalePerlin. Add some Debug.Log statements to see what is being returned by getIdUsingPerlin, and figure out why that number is not in the set, ie still likely out of the range of indices your array had when you populated the set.
     
    Last edited: Aug 13, 2021
  4. EnderSkelly34

    EnderSkelly34

    Joined:
    Jun 13, 2020
    Posts:
    27
    What is the proposed fix?
     
  5. Yoreki

    Yoreki

    Joined:
    Apr 10, 2019
    Posts:
    2,606
    I missed that you intend to bring the value back to range with scalePerlin. Added an edit to my post but i guess that came too late haha. Try generating values with getIdUsingPerlin and print the output using Debug.Log. Some of these values will not be contained in your dictionary as keys. Your code is a bit hard to reason about, so knowing which value triggers the exception will make it easier. How long is your tiles list?

    Code (CSharp):
    1.  
    2. void createTile(int tileId, int x, int y)
    3.     {
    4.        try
    5.         {
    6.             GameObject tilePrefab = tileSet[tileId];
    7.             GameObject tileGroup = tileGroups[tileId];
    8.             GameObject tile = Instantiate(tilePrefab, tileGroup.transform);
    9.  
    10.             tile.name = string.Format("tile_x[0]_y[1]", x, y);
    11.             tile.transform.localPosition = new Vector3(x, y, 0);
    12.             tileGrid[x].Add(tile);
    13.         }
    14.         catch (Exception e)
    15.         {
    16.             Debug.Log("This key causes the Exception: " + tileId);
    17.         }
    18.     }
    May i ask, why are you using a dictionary for your tiles? Since you use the indices as keys anyways, what do you gain compared to using a list?
     
  6. EnderSkelly34

    EnderSkelly34

    Joined:
    Jun 13, 2020
    Posts:
    27

    Dictionary is 3 long, is is easier to use a list?
     
  7. Yoreki

    Yoreki

    Joined:
    Apr 10, 2019
    Posts:
    2,606
    And what does the Debug.Log say for the key that causes the exception?
    Depends. A little Dictionary vs List excursion:
    Maps / Dictionaries are fantastic because they allow looking up arbitrary value pairs in constant time, meaning it's usually a huge performance improvement or can make it a lot easier to work with data. Imagine if you had a literal dictionary to represent in software. Say, a million words plus their definitions. A dictionary would be able to find the corresponding definition to a word in O(1), meaning constant time (a bit simplified, but on average true). Imagine doing the same thing with a list of pairs, or two lists. Youd now have to iterate over all one million items sequentially, check if it's the one with the name you are looking for, and only then you will get the correct defintion in return. This is called linear time, or O(n), meaning the more elements there are the slower it gets in a linear fashion.

    So dictionaries are pretty great, right? They work with words as keys, objects as keys, .. yes they are definitely a nice tool to have in your toolbelt. However, in your case you dont have complicated pairs. Your key is literally the index at which you would find it in a list. So you dont really gain anything, and looking something up at a known location in a list is also O(1) (and technically faster due to no overhead calculations, but i'm just mentioning this for completions sake. You should absolutely not worry about performance, especially not on things like these).

    If eventually you need to lookup things with a different key, say a string, then stick with dictionaries. But if your key is going to stay the index number, you may as well use a list. Which you use is in the end up to you, they both will work done properly.