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! ) ```csharp
private void RecordGround(){//This will record all the Ground tiles for generation later.
for(int x=0; x<worldSizeX; x++){
for(int y=0; y<worldSizeY; y++){
Vector3 pos = new Vector3(xgridOffset,ygridOffset,1);
Blocks temp = new Blocks(pos, x, y, 0);
blocks.Add(temp);
}
}
}* * *csharp
//This goes through all the blocks recorded earlier and generates a GameObject for each one
//It also sets the parent depending on the tile Type and Renames the GameObject with its position on the grid.
private void GenerateBlocks(){
for (int i=0; i<blocks.Count; i++){
GameObject toGenerate = Instantiate(Ground[blocks[i].GetVar()], blocks[i].GetPos(), Quaternion.identity);
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
if(blocks[i].GetVar()==0){//Ground tile
toGenerate.transform.SetParent(groundParent.transform);
toGenerate.name = ("Ground: â+blocks[i].GetX()+â - "+blocks[i].GetY());
}
if(blocks[i].GetVar()==1){//Water tile
toGenerate.transform.SetParent(lakeParent.transform);
toGenerate.name = ("Lake: â+(blocks[i].GetX())+â - "+(blocks[i].GetY()));
}
if(blocks[i].GetVar()==2){//Ocean tile
toGenerate.transform.SetParent(oceanParent.transform);
toGenerate.name = ("Ocean: â+(blocks[i].GetX())+â - "+(blocks[i].GetY()));
}
}
}* ```
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 ;).
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
public struct Tile {
public int id;
public Tile(int id) {
this.id = id;
}
}
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.
public class TileMap {
Tile[] _map;
public TileMap(Vector2Int size) {
_map = new Tile[size.x * size.y];
}
}
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
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)
static int toIndex(Vector2Int pos, int width) => pos.y * width + pos.x;
static Vector2Int fromIndex(int index, int width) => new Vector2Int(index / width, index % width);
Now you can introduce a proper indexer to your map
public Tile this[Vector2Int pos] {
get => _map[toIndex(pos)];
set => _map[toIndex(pos)] = value;
}
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.
using UnityEngine;
public class TileMap {
Tile[] _map;
readonly Tile _defaultTile = new Tile(id: -1);
public Vector2Int Size { get; private set; }
public TileMap(Vector2Int size) {
if(size.x < 1 || size.y < 1) throw new ArgumentException("Invalid size.");
_map = new Tile[size.x * size.y];
Size = size;
}
public TileMap(int width, int height) : this(new Vector2nt(width, height)) {}
public bool Contains(Vector2Int pos)
=> pos.x >= 0 && pos.y >= 0 && pos.x < Size.x && pos.y < Size.y;
public Tile this[Vector2Int pos] {
get => _map[toIndex(pos)];
set => _map[toIndex(pos)] = value;
}
public Tile this[int x, int y] {
get => _map[toIndex(new Vector2Int(x, y))];
set => _map[toIndex(new Vector2Int(x, y))] = value;
}
public Tile SafeRead(Vector2Int pos)
=> Contains(pos)? this[pos] : _defaultTile;
public Tile SafeRead(int x, int y) => SafeRead(new Vector2Int(x, y));
int toIndex(Vector2Int pos) => pos.y * Size.x + pos.x;
// Vector2Int fromIndex(int index) => new Vector2Int(index / Size.x, index % Size.x);
}
Now you can implement a better read method, one that includes an offset
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 ifs, 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 ifs 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
var map = new TileMap(10, 10);
map[5, 3] = new Tile(8);
Debug.Log(map[5, 3].id); // 8
Debug.Log(map[0, 0].id); // 0
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
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
public TileMap(Vector2Int size) {
_map = new Tile[size.x * size.y];
}
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 That is a lot of information for me and i want to try and understand it the best i can :).
Thanks again :).
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.
(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.
TileMap map = new TileMap(10, 10); // keep this reference
var move = new Vector2(-1, 0);
if(map.OffsetRead(player.pos, move).isPassable))
player.MoveTo(player.pos + move); // do something with it
// note: this isn't really covered by the original example,
// 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#.
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
void SomeMethod(int someArgument) {
// method body
}
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.
float computePercentage(float a, float b) {
return a / b * 100f;
}
Well this is so trivial it can be written like this
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
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).
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
public Tile SafeRead(Vector2Int pos)
=> 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!
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
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 :
public Tile this[Vector2Int pos] {
get => _map[toIndex(pos)];
set => _map[toIndex(pos)] = value;
}
and here :
public Tile this[int x, int y] {
get => _map[toIndex(new Vector2Int(x, y))];
set => _map[toIndex(new Vector2Int(x, y))] = value;
}
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.
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
public class Box {
public Color color;
public float size;
}
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.
public class Box {
private Color color;
private float size;
public Color GetColor() {
return color;
}
public float GetSize() {
return size;
}
}
But now you can only read, not write. Okay.
public class Box {
private Color color;
private float size;
public Color GetColor() {
return color;
}
public void SetColor(Color color) {
this.color = color;
}
public float GetSize() {
return size;
}
public void SetSize(float size) {
this.size = size;
}
}
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.
public class Box {
private Color _color;
private float _size;
public Color GetColor() => _color;
public void SetColor(Color color) => _color = new Color(color.r, color.g, color.b, alpha: 1f);
public float GetSize() => _size;
public void SetSize(float size) => _size = MathF.Max(0f, size);
}
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
var myBox = new Box();
myBox.SetSize(16); // this one to write
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.
public class Box {
private Color _color;
private float _size;
public Color Color {
get {
return _color;
}
set {
_color = new Color(value.r, value.g, value.b, alpha: 1f)
}
}
public float Size {
get {
return _size;
}
set {
_size = MathF.Max(0f, value);
}
}
}
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
var myBox = new Box();
myBox.Size = 16; // write 16
Debug.Log(myBox.Size); // read 16
myBox.Size = -2; // write -2
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
public class Box {
private Color _color;
private float _size;
public Color Color {
get => _color;
set => _color = new Color(value.r, value.g, value.b, alpha: 1f)
}
public float Size {
get => _size;
set => _size = MathF.Max(0f, value);
}
}
Auto-property
This here
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
private float _sizeVar;
public float Size {
get { return _sizeVar; }
private set { _sizeVar = value; }
}
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
int[] myArray; // declare an array variable
myArray = new int[6]; // allocate a new array of 6 elements and assign it to variable
myArray[0] = 18; // assign a value of 18 to the first element
myArray[3] = 5; // assign a value of 5 to the 4th element
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
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.
using System.Collections.Generic;
public class Garage {
Dictionary<ColorName, string> _dictionary = new Dictionary<ColorName, string>();
public string this[ColorName colorName] {
get => _dictionary[colorName];
set => _dictionary[colorName] = value;
}
}
public enum ColorName {
Red,
Green,
Blue,
White
}
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?
public Tile this[Vector2Int pos] {
get => _map[toIndex(pos)];
set => _map[toIndex(pos)] = value;
}
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
var pos = new Vector2Int(3, 5);
map[pos].id = 8;
Debug.Log(map[pos].id); // prints 8
for(int x = 0; x < 5; x++) {
for(int y = 2; y < 4; y++) {
var pos = new Vector2Int(x, y);
map[pos].id = 2;
}
}
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
public Tile this[int x, int y] {
get => _map[toIndex(new Vector2Int(x, y))];
set => _map[toIndex(new Vector2Int(x, y))] = value;
}
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
map[3, 5].id = 8;
Debug.Log(map[3, 5].id); // prints 8
As I said originally, this is for coding convenience.
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.
edit
hint: you can modify the toIndex method itself, hereâs how
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.
Okay, after a whole day of coding(instead of playing for once ), 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 ).
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
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.
The mark of a True Learnerâ˘. 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.
Already checked it!(the post) and it was way out of my range 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 (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 , it feels like i wouldnât be there without you help :).