Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question Organization through generating a random 2D world through scripting.

Discussion in 'Scripting' started by Guilhaume, May 24, 2023.

  1. Guilhaume

    Guilhaume

    Joined:
    Sep 14, 2016
    Posts:
    30
    Hello!
    First of all, this is not exclusively about scripting, but it seems like the best place to ask :).

    Here is some informations to contextualize :
    I am making a script that generates a 2D Map. Everything is done with a script by choice(for random generation purposes).
    My tiles(64x64) are made with aseprite to fit in a rectangular grid.
    Now that i have written some code and got some results in my generation, i would like to find a way to access my tiles at different moments :
    -While creating the map, i need to check if that position on the grid is taken to avoid overlapping.
    -Later on i will probably need to access it to manage some things(walking speed/spawning of tress or whatever ^^) etc..

    Here is basically how i do it, i'll try to keep it simple :
    -I have a Blocks.cs class that contains my tiles informations(position, position in the grid, type etc..)
    -Then in my WorldGenerator.cs i declare a list of Blocks, and through different methods and loops, i add blocks elements to this list(RecordGround(), RecordLakes(), RecordOcean() and so on..).
    -Finally, i have a GenerateBlocks() method that will loop through the blocks list and instantiate a GameObject for each element in it. It will take from a GroundPrefab, LakePrefab, OceanPrefab.., and instantiate the GameObject at the blocks.position (basically).

    Now that i need to check for overlapping, and that i relalize that my list is disorganized(not like an array for example), and that i can't access it easily, it smells like issues for the future of my project.. and after searching a lot online, it seemed like the best way to not mess up was explaining how i did it and asking what would be the best(or a better) way to go about this.

    Should i try to instantiate Tiles instead of GameObjects ? Couldn't really understand how to do it online, since it's exclusively through script on my end.
    Would that be better to check if a position is already taken ? to check the type of adjacent positions ? Theses ares things i'm struggling with my current setup.

    Just for Clarity, it works for now, but i am mainly curious about an experienced view on this topic and about how to organize and access my "data" better for the future of my project.
    I hope it is clear enough(english is not my native language)

    FYI, some of the the code below if it helps.(whole code attached).
    (Thank you! :) )

    Code (CSharp):
    1.     private void RecordGround(){//This will record all the Ground tiles for generation later.
    2.         for(int x=0; x<worldSizeX; x++){
    3.             for(int y=0; y<worldSizeY; y++){
    4.                 Vector3 pos = new Vector3(x*gridOffset,y*gridOffset,1);
    5.                 Blocks temp = new Blocks(pos, x, y, 0);
    6.                 blocks.Add(temp);
    7.             }
    8.         }
    9.     }
    Code (CSharp):
    1.     //This goes through all the blocks recorded earlier and generates a GameObject for each one
    2.     //It also sets the parent depending on the tile Type and Renames the GameObject with its position on the grid.
    3.     private void GenerateBlocks(){
    4.         for (int i=0; i<blocks.Count; i++){
    5.             GameObject toGenerate = Instantiate(Ground[blocks[i].GetVar()], blocks[i].GetPos(), Quaternion.identity);
    6.             if(CheckIfPresent(blocks[i])){continue;}//This doesn't work since i don't know(yet) how to check properly if a position is already taken
    7.             if(blocks[i].GetVar()==0){//Ground tile
    8.                 toGenerate.transform.SetParent(groundParent.transform);
    9.                 toGenerate.name = ("Ground: "+blocks[i].GetX()+" - "+blocks[i].GetY());
    10.             }
    11.             if(blocks[i].GetVar()==1){//Water tile
    12.                 toGenerate.transform.SetParent(lakeParent.transform);
    13.                 toGenerate.name = ("Lake: "+(blocks[i].GetX())+" - "+(blocks[i].GetY()));
    14.             }
    15.             if(blocks[i].GetVar()==2){//Ocean tile
    16.                 toGenerate.transform.SetParent(oceanParent.transform);
    17.                 toGenerate.name = ("Ocean: "+(blocks[i].GetX())+" - "+(blocks[i].GetY()));
    18.             }
    19.         }
    20.     }
     

    Attached Files:

  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,563
  3. Guilhaume

    Guilhaume

    Joined:
    Sep 14, 2016
    Posts:
    30
    Well I spawn GameObjects that i set child to an empty on a grid element. but my script only generates theses objects and put them in there, i still don't really know how to check if an element is in the grid to be honest.
    What's confusing me is that i am barely using the editor or any tile settings, i just instantiate from the script, so that's why if feels like it is not really "connected" to the grid/tiles system :/.

    edit: I would probably have to add the grid to my script as a public GameObject through the inspector i guess ?.. i'll look into that.
    Thank you for all the info, i'll look into that too ;).
     
  4. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    Regarding the data structure, the best and fastest way to organize a tilemap is to make a single-dimension array and turn it into a collection-like object.

    Here's a small example of this.

    So for example you have this struct
    Code (csharp):
    1. public struct Tile {
    2.  
    3.   public int id;
    4.  
    5.   public Tile(int id) {
    6.     this.id = id;
    7.   }
    8.  
    9. }
    Obviously in reality it's supposed to have more than one field in it, but you start from somewhere and leave room to expand later.

    Code (csharp):
    1. public class TileMap {
    2.  
    3.   Tile[] _map;
    4.  
    5.   public TileMap(Vector2Int size) {
    6.     _map = new Tile[size.x * size.y];
    7.   }
    8.  
    9. }
    To address the indice properly, that's pretty easy in 2D. Your columns are columns, but you treat your rows like multiples of columns. And so on a map (10, 10) instead of having something at (5, 3) you do 3x10+5 = 35 which means that a tile at (5, 3) would be labelled 35 if you would count them one by one, columns first, starting from 0.

    Consider this image

    upload_2023-5-24_17-55-34.png

    This formula generalizes to y * width + x but you can also get the original (columns, row) information back. How?

    Well you take the index of 35 and integer-divide it by 10 (the width), you get 35/10=3
    Now you have two options to get back to 5:
    - 35 - 35/10 * 10 = 35 - 3 * 10 = 5, or
    - 35 % 10 = 5 (now you know the formula for the integer remainder operator, it's N-N/M*M)

    So lets add these conversions (though you don't need the 2nd one in this example)
    Code (csharp):
    1. static int toIndex(Vector2Int pos, int width) => pos.y * width + pos.x;
    2. static Vector2Int fromIndex(int index, int width) => new Vector2Int(index / width, index % width);
    Now you can introduce a proper indexer to your map
    Code (csharp):
    1. public Tile this[Vector2Int pos] {
    2.   get => _map[toIndex(pos)];
    3.   set => _map[toIndex(pos)] = value;
    4. }
    But let's not stop there. Currently this would crash if you were to read something that's outside of the map area. Let's make this more tolerant, so that it doesn't crash but instead return a default tile that has an id of -1.

    We can introduce a Contains test to make this easier.

    Code (csharp):
    1. using UnityEngine;
    2.  
    3. public class TileMap {
    4.  
    5.   Tile[] _map;
    6.  
    7.   readonly Tile _defaultTile = new Tile(id: -1);
    8.  
    9.   public Vector2Int Size { get; private set; }
    10.  
    11.   public TileMap(Vector2Int size) {
    12.     if(size.x < 1 || size.y < 1) throw new ArgumentException("Invalid size.");
    13.     _map = new Tile[size.x * size.y];
    14.     Size = size;
    15.   }
    16.  
    17.   public TileMap(int width, int height) : this(new Vector2nt(width, height)) {}
    18.  
    19.   public bool Contains(Vector2Int pos)
    20.     => pos.x >= 0 && pos.y >= 0 && pos.x < Size.x && pos.y < Size.y;
    21.  
    22.   public Tile this[Vector2Int pos] {
    23.     get => _map[toIndex(pos)];
    24.     set => _map[toIndex(pos)] = value;
    25.   }
    26.  
    27.   public Tile this[int x, int y] {
    28.     get => _map[toIndex(new Vector2Int(x, y))];
    29.     set => _map[toIndex(new Vector2Int(x, y))] = value;
    30.   }
    31.  
    32.   public Tile SafeRead(Vector2Int pos)
    33.     => Contains(pos)? this[pos] : _defaultTile;
    34.  
    35.   public Tile SafeRead(int x, int y) => SafeRead(new Vector2Int(x, y));
    36.  
    37.   int toIndex(Vector2Int pos) => pos.y * Size.x + pos.x;
    38.   // Vector2Int fromIndex(int index) => new Vector2Int(index / Size.x, index % Size.x);
    39.  
    40. }
    Now you can implement a better read method, one that includes an offset

    Code (csharp):
    1. public Tile OffsetRead(Vector2Int pos, Vector2Int offset) {
    2.   pos += offset;
    3.   return Contains(pos)? this[pos] : _defaultTile;
    4. }
    This now allows you to scan for adjacencies, and whoever is calling this, won't have to make sure about the out-of-bounds positions, it will simply work!

    And so on and so on, I could make an entire engine out of this.

    Regarding the other part, where you match everything with
    if
    s, that's smelly. We never do things like that. What you're doing there is called "mapping" when you match one set with another. There are many many ways to do the mapping, but if you're doing the
    if
    s on site, you're doing it wrong. Even
    switch
    is wrong, because it's not that these instructions are bad by themselves, they're just not supposed to be used in this case.

    Your ID should be sufficient to map to an index to some data structure that is initialized to hold references to proper information. Maybe you've supplied this through the inspector, or ScriptableObject, or some data file, or even an array inside the code, it doesn't matter, but when you do it like that, there's no need to
    if
    each and every case.

    ---
    Edit: added some x,y overloads for coding convenience.

    I forgot to show how to use this
    Code (csharp):
    1. var map = new TileMap(10, 10);
    2. map[5, 3] = new Tile(8);
    3. Debug.Log(map[5, 3].id); // 8
    4. Debug.Log(map[0, 0].id); // 0
    5. Debug.Log(map.SafeRead(-1, 3).id); // -1
    Edit2: fixed toIndex method so that it accepts Size.x instead of taking width as an argument
     
    Last edited: May 25, 2023
    AngryProgrammer and mopthrow like this.
  5. Guilhaume

    Guilhaume

    Joined:
    Sep 14, 2016
    Posts:
    30
    Wow! Thank you so much :).
    That's a lot of information(for me :p) and i'm still trying to understand it all. I haven't coded in years and got back to it 2 days ago ><.
    Clearly this is a better way to do it, and it is also way more responsive for later use and more data, which is what has been bothering me.
    I have some questions though.. and mainly it's about how to implement this. I'm sorry if thoses sound basic, but it is usually where i'm struggling.

    1-The public struct Tile is basically my Block class right ? except it's a struct. So i just add it to my scripts and voila ?(i read a bit about it and struct are definitely better in this case indeed).

    2-The public class TileMap i'm unsure about, i understand how it works, and even the index system thanks to your very good explanation. But i am not sure where to implement it.
    Is it just a script that will not be attached to anything(since it doesn't derive from MB ?..) and contains all my tiles data ?
    If that is the case, how do i go about adding tiles to it from my WorldGeneration script ? I would have to make a link to it at some point if it contains the _map right ?

    3-If my point above is correct, then i can call that constructor
    Code (CSharp):
    1.   public TileMap(Vector2Int size) {
    2.     _map = new Tile[size.x * size.y];
    3.   }
    from my world generation script each time i want to add a tile to my map by just sending it a Vector2Int.
    That will add a Tile element to the _map and then i can edit the parameters of that specific tile.

    4Lastly :p, concerning the indexer and below it, i am still trying to understand it fully(to be honest i am not used to this kind of syntax just yet : => Contains(pos)? this[pos] : _defaultTile) but i'll get to it. There is already a lot to go on for me and that is exactly what i needed :).
    Mostly for thoses last ones i am not sure how to use them. If i'm in my worldGeneration script i would have to have a link to this script(assuming point 2 above is right) and do for example TileMap.Contains() to check if a position is already used, or TileMap.toIndex() to have a conversion from a pos to an index ?

    Sorry for all the questions :p That is a lot of information for me and i want to try and understand it the best i can :).
    Thanks again :).
     
    Last edited: May 24, 2023
  6. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    1. Yes. You should learn the difference between a struct and a class.

    When instantiated, class objects live in a so-called heap-memory, and are considered as reference types, i.e. they are accessed by references. Heap is slower than any other memory used by CPU because it resides in your RAM, and so it has to go through many system hoops but also has to synchronize with the main clock before it can be accessed.

    When you use the keyword
    new
    in an object-oriented language, this means that an object will get allocated in memory. Normally we programmers shudder when we see
    new
    used in a frequent manner because that means a lot of objects have to be written to RAM. This potentially produces garbage, which is data material that is abandoned but now has to be cleaned. Cleaning is automatic in C# and will only waste more CPU time (it's called 'garbage collection').

    When you're doing memory-intensive things, yet you need to use objects, preferably small-ish ones, you really want a faster memory access. A CPU program is evaluated on a so-called stack, which is a very fast but tiny memory by modern standards, but perfectly sufficient for the program to execute in its entirety as long as the majority of its actual data is on the heap. However, small stuff like numbers and other transient data, like the ones you store in a function variable do live on the stack.

    These types are called value types, because you don't need a reference to access them and so they're readily available. Struct allows you to build a custom value type. However, that doesn't mean structs exclusively live on the stack, but most of the time they will, especially in examples such as this. When
    new
    keyword is used with structs, program go brrr, and programmers don't shudder any more.

    Because tiles are supposed to be small-ish and locally manipulated in various ways, they are perfect candidates to be the stack-loving value objects.

    2. (and 3.)
    You see, with object-oriented programming, your code is either the service, or the user. Most likely it's the both at the same time, but never in the same domain. When you're providing a service, you implement some internal details that aren't useful to the outside world, and when you're using some service API, you just want the results, and it should take care of the details.

    So if you understand how to use variables and other Unity methods, that's exactly how you're supposed to use this. Like a ready-made tool. You just use it. Do not mix the internals of an object with how it's used on the outside. You don't care how
    toGenerate.transform.SetParent
    works, from your example. I don't see why would you care here either. You can see usage example at the end of the post.

    Yes, this is a so-called pure C# class, to distinguish it from Unity scripts.

    You simply instantiate an object by calling the constructor with the
    new
    keyword. As I explained before, this will allocate the object on the heap, and you'll get a reference back, as with any other reference type. You then keep this reference, so that you can refer to this instance over and over. This is your data model essentially. A place where all the structs live.

    Code (csharp):
    1. TileMap map = new TileMap(10, 10); // keep this reference
    2. var move = new Vector2(-1, 0);
    3.  
    4. if(map.OffsetRead(player.pos, move).isPassable))
    5.   player.MoveTo(player.pos + move); // do something with it
    6.  
    7. // note: this isn't really covered by the original example,
    8. //      but it's something you could ultimately do with it
    And now we can talk some more about the Tiles and where they're stored. You see, they internally belong to an array. And this array isn't a value type, it's a reference type. And so this object lives entirely on the heap.

    Which is okay, you don't want your map to completely blot the tiny stack. Which means that these structs do live on the heap. At least when you're reading/writing individual tiles in it.

    But notice this, you never have to allocate anything, which is a relatively slow process, nor do you ever introduce garbage if you decide to make a new Tile. The memory was allocated once, as a single block, when you called the constructor. And now you're free to read or write to it, for the rest of your application's lifetime.

    So what's the point of this struct then if everything is on the heap? Well, when you're passing the Tile object around, as an argument, from function to function, value types are simply copied right there on the stack, so once you get it from the model, it's in a super fast mode from that point on. In other words, all its properties are right there with you, and you can always produce a new copy of it on the fly. This is what you get when you're using Vector2 and 3 as well, these are light numeric objects that simply do not allocate on the heap.

    However, when you finish with it and decide to store it, this struct will be saved to the model somewhere in your RAM. You can use this knowledge to your advantage, because you can make super big maps if you know how, RAM is pretty huge nowadays, but you need to make systems that are aware of the performance bottlenecks, to leverage all this.

    There is important note to be made here. Nothing gets added any more. Adding means "making room for". The room has already been made. You just use this as a platter for your data. It's just there, ready to go. But to access it you have to keep the reference to the original instance of it lying around. As soon as you lose it, garbage collector kicks in and wipes this allocation from memory. Which is why you don't need to care about manually clearing memory in C#.

    4. Sure. Here's a crash course.

    => is a special notation that means "this method is so light, it doesn't even have a method body"
    A method body is when you make a method like this
    Code (csharp):
    1. void SomeMethod(int someArgument) {
    2.   // method body
    3. }
    Some methods can be so light that they are in fact simple expressions. Like cells in Excel. No statements at all!
    And you definitely need to understand a difference between a statement and an expression.

    C# supports this in form of "expression-statements" because it's more of a hybrid really, but it's predominantly expression-like. Let's say you're computing a ratio between two numbers expressed as percentage.
    Code (csharp):
    1. float computePercentage(float a, float b) {
    2.   return a / b * 100f;
    3. }
    Well this is so trivial it can be written like this
    Code (csharp):
    1. float computePercentage(float a, float b) => a / b * 100f;
    Nothing too special about it, right? However, it has some advantages. First of all, you can immediately see it's supposed to be light. Second, it takes much less room for something that isn't supposed to grow in the future anyway. Third, it is much more likely that this will be inlined by the compiler, and that's like a hint that you want this method to disappear in the actual code that will run. You see, method calls aren't really for free. They are actually produced on the stack, and some things need to happen between calls, things get written, things get copied around, a new frame opens up yadda yadda. With method inlining, you get to work around this, and expression-statements help with making your final code run in a more streamlined fashion.

    Regarding the ?: operator, it's called a ternary operator. Some people hate it, some people love it. It is especially useful in expression-statements, but also when you need to assign a decision-based value.

    Here I'm making a decision on the spot and return the value based on some criteria. I could've written this differently
    Code (csharp):
    1. public Tile SafeRead(Vector2Int pos) {
    2.   if(Contains(pos)) {
    3.     return this[pos];
    4.   } else {
    5.     return _defaultTile;
    6.   }
    7. }
    But this is poor form. This is better
    Code (csharp):
    1. public Tile SafeRead(Vector2Int pos) {
    2.   if(Contains(pos)) return this[pos];
    3.   return _defaultTile;
    4. }
    Not only it takes less space, it has no indentation, it is easier to read, and has a clear default path. This means there is one default behavior and one override. In today's programming we actually tend to avoid
    else
    whenever possible. Simply put,
    else
    is more often than not a poor practice in programming. But let's not generalize,
    else
    has its uses still!!

    Finally, I decided not to do this like that, because I can see through this, and I see it's so simple it can be compressed into a single expression. This is because ternary operation behaves exactly like an
    if
    but is instead an operator. Why it's called ternary? Because it has three parts (for example + is a binary, because it has two).
    Code (csharp):
    1. public Tile SafeRead(Vector2Int pos) {
    2.   return Contains(pos)? this[pos] : _defaultTile;
    3. }
    However, this is poor form as well, because we're just playing smart if we leave it like that. This looks wrong and is more difficult to debug, since you'd have to rearrange everything into separate lines to introduce a Debug.Log, for example.

    If I was to use a method body, I would always go for the above variant. But instead, I want it to be simple, to be fast, and to never grow beyond this. I also have to make sure that this is not a point of failure in my solution. And I do. I will learn my lesson if this doesn't work, so I think twice about how complex I want this to become.

    Therefore
    Code (csharp):
    1. public Tile SafeRead(Vector2Int pos)
    2.   => Contains(pos)? this[pos] : _defaultTile;
    Is it complex now that you can read it?
    Hopefully this explains the reasoning behind it.

    No, you wouldn't normally use Contains unless in some special cases, which is why I left it as public. It is mainly used for the SafeRead method, but could also be used in other areas when and if a need would arise. Contains really just returns true/false based on whether the tile you query about belongs to the pre-allocated area or not. It is really useful to have this simple query written once, and you can always reliably use this without having to repeat yourself all over your project. Simple things like this a) make your code more readable, b) make your code more compact, c) help you with keeping bugs at bay either because you didn't repeat yourself, or because you have a single point of failure, so it's now very easy to verify this for correctness and a fix would propagate throughout the project.

    toIndex, on the other hand, is not public which is why I personally start such methods with lowercase letters. You cannot use toIndex, and you don't need to, the service as it is, takes care of it for you. This is what is called an implementation detail, it is a part of the internal mechanism of this object, that you're not supposed to know anything about, from a vantage point of its user.

    There you go, hopefully you've learned something new from this. Good luck!
     
    Last edited: May 24, 2023
    mopthrow likes this.
  7. Guilhaume

    Guilhaume

    Joined:
    Sep 14, 2016
    Posts:
    30
    Hello!
    Thank you so much, this has been tremendously helpful !
    You have a great way of explaining things, and even if english is not my mother tongue, it felt easy to understand.
    Now it'll take a few days to really assimilate all this and learn to integrate this with my future project, but this is exactly what i needed right now :).
    Thanks again! i wish you all the best.
    Guilhaume
     
    orionsyndrome likes this.
  8. Guilhaume

    Guilhaume

    Joined:
    Sep 14, 2016
    Posts:
    30
    Okay, i'm sorry to re-open this :( but there is a last thing that i don't get in your initial code.
    the indexer here :
    Code (CSharp):
    1.  public Tile this[Vector2Int pos] {
    2.     get => _map[toIndex(pos)];
    3.     set => _map[toIndex(pos)] = value;
    4.   }
    and here :
    Code (CSharp):
    1.   public Tile this[int x, int y] {
    2.     get => _map[toIndex(new Vector2Int(x, y))];
    3.     set => _map[toIndex(new Vector2Int(x, y))] = value;
    4.   }
    I cant' seem to really grasp if this is a method or a variable or when it's called.
    What does "this" refer to here ? from my understanding this = the TileMap class, but it's an array here ? :/
    (couldn't find examples of this used this way online).

    Moreover, it feels like this won't work, since toIndex would need the width of the map too as an argument?..And since width is only taken once as an argument in the constructor, it would no longer be accessible here.
    The indexer is called at creation of the map and "indexes" all of the tiles properly in the array, but is never called again. Actually, only SafeRead and OffsetRead are to be used later on if i'm not mitaken?

    Thanks again if anyone could shed light on this last part.
     
    orionsyndrome likes this.
  9. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    Sure thing. The indexer is a special kind of property.
    And properties are special kind of methods.

    Normally when you make some encapsulated behavior (that means something goes on inside of an object, some intimate detail that is responsible for it functioning properly, but it isn't necessarily something a user cares about), you want to expose some control parameters. For example you make an object that behaves like a box and looks like a box, and you expose its color and size, for user customization.

    Exposing this would normally look like this
    Code (csharp):
    1. public class Box {
    2.  
    3.   public Color color;
    4.   public float size;
    5.  
    6. }
    It's public so it's accessible from the outside. But now you have a problem. The user has too much control over the internal state of the object. You're letting him tamper with the internal logic, because you're passively allowing for some invalid states, namely he can set a negative size, or set an invisible color (by passing a color with an alpha of 0).

    So to sort this out, you decide to make a barrier. How? Well, you stop exposing the fields themselves, and make dedicated methods.
    Code (csharp):
    1. public class Box {
    2.  
    3.   private Color color;
    4.   private float size;
    5.  
    6.   public Color GetColor() {
    7.     return color;
    8.   }
    9.  
    10.   public float GetSize() {
    11.     return size;
    12.   }
    13.  
    14. }
    But now you can only read, not write. Okay.
    Code (csharp):
    1. public class Box {
    2.  
    3.   private Color color;
    4.   private float size;
    5.  
    6.   public Color GetColor() {
    7.     return color;
    8.   }
    9.  
    10.   public void SetColor(Color color) {
    11.     this.color = color;
    12.   }
    13.  
    14.   public float GetSize() {
    15.     return size;
    16.   }
    17.  
    18.   public void SetSize(float size) {
    19.     this.size = size;
    20.   }
    21.  
    22. }
    Notice how we're having a collision with the names of our arguments and our internal fields, and this is why I use
    this
    here. It's a special keyword that refers to this object's instance, from the inside. Thanks to
    this
    , we can specifically target its class fields, otherwise this assignment wouldn't work.

    Notice also how in the original code, I prepend class fields with an underscore '_'. This is a convention by which I'm telling everyone "this is a class variable". This also makes it possible to do
    _color = color;
    and circumvent the name collision.

    Anyway, we can now fix the sanitation issues. Sanitation is making sure that values are good and healthy on input. And we can also apply the expression-statements I talked about earlier.
    Code (csharp):
    1. public class Box {
    2.  
    3.   private Color _color;
    4.   private float _size;
    5.  
    6.   public Color GetColor() => _color;
    7.   public void SetColor(Color color) => _color = new Color(color.r, color.g, color.b, alpha: 1f);
    8.   public float GetSize() => _size;
    9.   public void SetSize(float size) => _size = MathF.Max(0f, size);
    10.  
    11. }
    Well, that's done.
    And this is why this format is called an expression-statement btw, because it also allows some statements, like assignments, which are half-expression, half-statement.

    But can you see how little code we had in the beginning, and look at this thing! Even though we've compressed it slightly, just look at how much noise we got there.

    See how everything comes in pairs? Get Set Get Set. Now we have 4 methods, just to read/write two class fields. It's too much code -- too much boilerplate (a jargon for the stuff that needs to be written but doesn't do much on its own).

    To use this you would have to access each method individually
    Code (csharp):
    1. var myBox = new Box();
    2. myBox.SetSize(16); // this one to write
    3. Debug.Log(myBox.GetSize()); // this one to read
    To fix this C# has a feature called properties. Which is essentially just a syntactic sugar to make coding more easier and code itself easier to read and maintain.

    There are two flavors of properties, but I'll focus on the ordinary, manual properties, so you can understand better the whole idea.
    Code (csharp):
    1. public class Box {
    2.  
    3.   private Color _color;
    4.   private float _size;
    5.  
    6.   public Color Color {
    7.     get {
    8.       return _color;
    9.     }
    10.     set {
    11.       _color = new Color(value.r, value.g, value.b, alpha: 1f)
    12.     }
    13.   }
    14.  
    15.   public float Size {
    16.     get {
    17.       return _size;
    18.     }
    19.     set {
    20.       _size = MathF.Max(0f, value);
    21.     }
    22.   }
    23.  
    24. }
    The thing with properties is that they can reuse the same name for both reading and writing. So they behave like fields to the outside world. And yet they internally allow you to sanitize the incoming value. Here the value comes in through a special contextual keyword
    value
    which is of the same type as the property itself.

    Now you can do this
    Code (csharp):
    1. var myBox = new Box();
    2. myBox.Size = 16; // write 16
    3. Debug.Log(myBox.Size); // read 16
    4. myBox.Size = -2; // write -2
    5. Debug.Log(myBox.Size); // read 0 because we softly disallow negative values
    These
    get
    and
    set
    blocks are called getters and setters, respectively. Additionally, a property can be made into a read-only by excluding a setter or making it private. And we can also apply expression-statement thing to shorten this already simple code further
    Code (csharp):
    1. public class Box {
    2.  
    3.   private Color _color;
    4.   private float _size;
    5.  
    6.   public Color Color {
    7.     get => _color;
    8.     set => _color = new Color(value.r, value.g, value.b, alpha: 1f)
    9.   }
    10.  
    11.   public float Size {
    12.     get => _size;
    13.     set => _size = MathF.Max(0f, value);
    14.   }
    15.  
    16. }
    This here
    Code (csharp):
    1. public Vector2Int Size { get; private set; }
    That's called an auto-property. It lets you just declare the getter and the setter, and it will automatically apply an appropriate getter and setter. It is in every way equivalent to
    Code (csharp):
    1. private float _sizeVar;
    2. public float Size {
    3.   get { return _sizeVar; }
    4.   private set { _sizeVar = value; }
    5. }
    but saves you from having to create a backing field (
    _sizeVar
    ), and from typing the rest of the boilerplate.

    Ok, does this look familiar? So what does indexer do?
    Indexer is a special kind of a property.

    You know like when you're using an array, you want to extract a value at some index, and so you have to use square brackets
    [
    and
    ]
    with some expression inside (typically just an integer). That whole syntax is called indexer. It let's you to "refer to things by their index".

    So for example, here's how you would use an array
    Code (csharp):
    1. int[] myArray; // declare an array variable
    2. myArray = new int[6]; // allocate a new array of 6 elements and assign it to variable
    3. myArray[0] = 18; // assign a value of 18 to the first element
    4. myArray[3] = 5; // assign a value of 5 to the 4th element
    5. Debug.Log(myArray[0]); // prints 18
    C# lets you introduce your own indexer in your objects. You can do whatever you want with how they work internally, or what they represent. But they are used the same from the outside, the syntax is the same. For example
    Code (csharp):
    1. Garage myGarage;
    2. myGarage = new Garage();
    3. myGarage[ColorName.Red] = "ferrari";
    4. myGarage[ColorName.White] = "porsche";
    Now to write your own indexer, you need to follow some rules. First of all they're very similar to properties in that they have a getter and setter. The other thing, well they can't have a custom name, and there is only one way to declare them properly, and that's by using
    this[...]
    . It's just something designers of C# decided how it's gonna be.
    Code (csharp):
    1. using System.Collections.Generic;
    2.  
    3. public class Garage {
    4.  
    5.   Dictionary<ColorName, string> _dictionary = new Dictionary<ColorName, string>();
    6.  
    7.   public string this[ColorName colorName] {
    8.     get => _dictionary[colorName];
    9.     set => _dictionary[colorName] = value;
    10.   }
    11.  
    12. }
    13.  
    14. public enum ColorName {
    15.   Red,
    16.   Green,
    17.   Blue,
    18.   White
    19. }
    Look how that dictionary also uses an indexer for its own thing. Each object below hides its internal mechanisms from the user above. However, the syntax is always the same, this is what marries all this code with a clear standard of what we mean by all these symbols.

    And so finally, what does this mean?
    Code (csharp):
    1. public Tile this[Vector2Int pos] {
    2.   get => _map[toIndex(pos)];
    3.   set => _map[toIndex(pos)] = value;
    4. }
    Well it's an indexer that lets you pass in a Vector2Int value, which in itself consist of two integers represented by x and y. This is used to represent a particular coordinate in your tile map. We're now hiding the fact that these values are yet to be converted to a true index by toIndex. But that's something we do internally, remember? You don't care about this fact from the level above.

    And this is all fine if you want to use this with a variable, here are a couple of examples
    Code (csharp):
    1. var pos = new Vector2Int(3, 5);
    2. map[pos].id = 8;
    3. Debug.Log(map[pos].id); // prints 8
    Code (csharp):
    1. for(int x = 0; x < 5; x++) {
    2.   for(int y = 2; y < 4; y++) {
    3.     var pos = new Vector2Int(x, y);
    4.     map[pos].id = 2;
    5.   }
    6. }
    But it's a little cumbersome if you just want to try a quick demo because you constantly need to type
    new Vector2Int(...)
    bla bla, so why not introduce an indexer overload (learn what that is) which simply takes two integers and does that for you. Computers are all about automation, so let's automate. That's what this is for
    Code (csharp):
    1. public Tile this[int x, int y] {
    2.   get => _map[toIndex(new Vector2Int(x, y))];
    3.   set => _map[toIndex(new Vector2Int(x, y))] = value;
    4. }
    Now you get the same effect (internally), but you don't have to write
    new Vector2(...)
    by yourself, if you don't want to. The previous examples will still work, as will this
    Code (csharp):
    1. map[3, 5].id = 8;
    2. Debug.Log(map[3, 5].id); // prints 8
    As I said originally, this is for coding convenience.
     
    Last edited: May 25, 2023
  10. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    And let me address this last bit, because there are some good points!

    Try here

    You are absolutely right! That's because I forgot to include it! Good catch. Can you add it yourself? The width is supposed to be Size.x, it is already accessible and ready to use.

    hint: you can modify the toIndex method itself, here's how
    Code (csharp):
    1. int toIndex(Vector2Int pos) => pos.y * Size.x + pos.x;
    it can take the width information by itself, but now it can't be
    static
    anymore, because that's information associated with the object's instance.

    You can use any of the three. I'm not claiming this particular assortment to be the best, you can customize this however you see fit. Maybe you don't need SafeRead, and you make indexer to behave like SafeRead. Or you can cram it all into just one Read method (with an optional offset), no indexer at all. These are all just examples of what can be accomplished. Feel free to mix and match.

    The idea behind this wasn't to make you blindly follow the prescription, but to show you what you can do with a smart data structure that is infinitely extendable. That you can rely upon from wherever in your code. You were wondering about discovering adjacencies and similar topics that arise once you get past the basics, and this here would be the path forward in that regard.

    The idea is that you work a little bit more today, sure there is some work in order to encapsulate some advanced behavior and make provisions for complex queries, but this will surely diminish your work tomorrow, you will have everything ready to use, and letting you move up in the realm of higher abstraction, which is where our games and dreams live.
     
    Last edited: May 25, 2023
  11. Guilhaume

    Guilhaume

    Joined:
    Sep 14, 2016
    Posts:
    30
    Okay, after a whole day of coding(instead of playing for once :D), i have re-read this whole post many times and implemented it with my project until it was fully understood, playing around and modifying it to test things out. A few hours ago i was confortable in saying that i understood it all :). I then deleted everything and started a new project to implement it from scratch, and it is going very well! And if i ever get lost on this topic again, i could always come back to this post and re-read it(it is my first bookmark now :D).
    I could never thank you enough @orionsyndrome !This has been of incredible help, i learned so much :) I wish you all the best for your projects, and beyond.
    Have a great day!
    Guilhaume
     
    orionsyndrome likes this.
  12. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    That's great news, kudos for staying committed! Honestly, that's half the job.

    It also means my time wasn't wasted, which is always a welcome thing.
    I encourage you to check my other tutorials as well.

    You can try this one, and in the end of this admittedly horrendously long post, there is a short list of all kinds of different topics I've covered before. Some of it is perhaps too advanced, but there is also simpler stuff, I'm sure you'll learn a thing or two.
     
    Guilhaume likes this.
  13. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,563
    The mark of a True Learner(tm). Great idea... glad you're getting some traction.

    Procedural generation is something you will always get better at. It's like golf. You can play a lifetime and think you have reached a level of unparalleled mastery, then you suddenly see someone else do something head-and-shoulders above what you even dreamed was possible, and off you go again.

    Welcome.
     
    Guilhaume and orionsyndrome like this.
  14. Guilhaume

    Guilhaume

    Joined:
    Sep 14, 2016
    Posts:
    30
    Already checked it!(the post) and it was way out of my range :D but i had not seen the links at the end to your other tutorials! Interesting stuff, i'll definitely check them out :).

    That's the feeling exactly, there's always more ways to do it, every part is like a riddle that has many answers, it is truly a fascinating topic :). I just hope i'll be satisfied at one point and start working on the rest of the game :D (but the map has always felt like the first thing to deal with for me ;) )

    Thank you to you both! I may at some point post in the WIP section if i feel confortable in doing so, i'll be sure to tag/credit you :D, it feels like i wouldn't be there without you help :).
     
    Last edited: May 28, 2023