Tilebased boardgame dicenumber restriction (Incremental algorithms) - c#

Developing a 2D tile-based "boardgame" I'm struggling with the restriction I have to make, when a player rolls the dice(Move 5 tiles if you land a 5 etc.)
I'm trying to use the following logic:
Start on starting point
Check the position to the sides, above and below
Check if the neighbour tiles are walkable, if they are, change them to reachable
Go to neighbour tile, repeat
I've been looking on A* and D* pathing, but it's a new subject to me and they seem more focused on getting from point A to B, not "reach" which is what I need.
How do I do this through code?
I created a 2D array from an array which was holding my tile(I need a normal array of the tilemap for another purpose):
for(int i = 0; i < 27; i++)
{
for(int j = 0; j < 33; j++)
{
tileMap[i, j] = goTile[i * 33 + j];
}
}
I now use the tileMap as my positiong factor, e.g. my players current position is tileMap[2,4].
I then tried to develop a function:
void pathFinding(Vector2 playerPosition, int diceNumber)
{
GameObject currentPos = tileMap[(int)playerPosition.x, (int)playerPosition.y];
for (int i = 0; i < diceNumber; i++) {
if (tileMap[(int)playerPosition.x + 1, (int)playerPosition.y].tag == "walkableGrid")
{
tileMap[(int)playerPosition.x + 1, (int)playerPosition.y].gameObject.tag = "reachable";
playerPosition.x++;
}
if (tileMap[(int)playerPosition.x - 1, (int)playerPosition.y].tag == "walkableGrid")
{
playerPosition.x--;
}
if (tileMap[(int)playerPosition.x, (int)playerPosition.y + 1].tag == "walkableGrid")
{
playerPosition.y++;
}
if (tileMap[(int)playerPosition.x, (int)playerPosition.y - 1].tag == "walkableGrid")
{
playerPosition.y--;
}
}
}
But as finishing this (if it even would work), would require MANY lines of code, and I believe there's a swifter method using a nested for loop maybe?

//I have now edited the code to better reflect your real data
public void ShowMoves(Vector2 playerPosition, int diceNumber, bool[] blocks)
{
int x = (int)playerPosition.x;
int y = (int)playerPosition.y;
if(tileMap.GetUpperBound(0) < x + 1)
{
if(tileMap[x + 1, y].tag == "walkableGrid" && blocks[0])
{
/*Light up the tile*/
if(diceNumber > 0)
ShowMoves(new Vector2(x + 1, y), diceNumber - 1, new bool[] { x != tileMap.GetUpperBound(0), false, y != tileMap.GetUpperBound(1), y != 0 });
}
}
if(x - 1 >= 0)
{
if(tileMap[x - 1, y].tag == "walkableGrid" && blocks[1])
{
/*Light up the tile*/
if(diceNumber > 0)
ShowMoves(new Vector2(x - 1, y), diceNumber - 1, new bool[] { false, x != 0, y != tileMap.GetUpperBound(1), y != 0 });
}
}
if(tileMap.GetUpperBound(1) < y + 1)
{
if(tileMap[x, y + 1].tag == "walkableGrid" && blocks[2])
{
/*Light up the tile*/
if(diceNumber > 0)
ShowMoves(new Vector2(x, y + 1), diceNumber - 1, new bool[] { x != tileMap.GetUpperBound(0), x != 0, y != tileMap.GetUpperBound(1), false });
}
}
if(y - 1 >= 0)
{
if(tileMap[x, y - 1].tag == "walkableGrid" && blocks[3])
{
/*Light up the tile*/
if(diceNumber > 0)
ShowMoves(new Vector2(x, y - 1), diceNumber - 1, new bool[] { x != tileMap.GetUpperBound(0), x != 0, false, y != 0 });
}
}
}
This code might not compile, but it's an example to help you along - it will loop until there are no available moves, and exhaust every option. It will also not go back on itself due to the blocks boolean array. The input format would be the position they're at two ints, one for x and one for y, the tiles available, the number of moves left in their roll, and the block available from the beginning (always new bool[] {true, true, true, true}
Be careful, there may be errors in my code, I wrote it in SO and have no clue how well it runs, if it runs at all or anything. Even if it does not, it should be a good starting point for you to create your logic and code it all
EDIT: Code has been changed so that it better fits how your code looks and the data types it uses
To avoid always calling the method by inputting a blocks variable of new bool[] {true, true, true, true}; you can make it an optional operator by making this the method parameters
public void ShowMoves(Vector2 playerPosition, int diceNumber, bool[] blocks = new bool[] {true, true, true, true})

Related

Check adjacent indices in a multidimensional array having into account the borders

I did this so I can know how many asterisks appear in the adjacent squares.
private int CheckAdjacents(Coordinate cord)
{
List<Coordinate> coordinates = new List<Coordinate>()
{
new Coordinate(cord.X - 1, cord.Y - 1),
new Coordinate(cord.X, cord.Y-1),
new Coordinate(cord.X + 1, cord.Y -1),
new Coordinate(cord.X + 1, cord.Y),
new Coordinate(cord.X + 1, cord.Y + 1),
new Coordinate(cord.X, cord.Y + 1),
new Coordinate(cord.X - 1, cord.Y + 1),
new Coordinate(cord.X - 1, cord.Y)
};
return coordinates.Count(x => _matrix.At(x).Value == '*');
}
The thing here is that obviously it returns an exception because is checking indexes that wouldn't be checked. What'd be the best way to skip those kind of indexes? Using a try/catch could be kinda tricky? Thanks!
EDIT:
Matrix class
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace MineSweeper
{
public record Coordinate (int X, int Y);
public record Size(int M, int N);
public class Matrix
{
private readonly Size _size;
private readonly Cell[,] _matrix;
private const char InitValue = '.';
public Matrix(Size size)
{
_size = size;
_matrix = new Cell[size.M, size.N];
Initialize();
}
private void Initialize()
{
for (int m = 0; m < _size.M; m++)
for (int n = 0; n < _size.N; n++)
_matrix[m, n] = new Cell(InitValue);
}
public Size GetSize()
=> _size;
public Cell At(Coordinate coordinate)
=> _matrix[coordinate.X, coordinate.Y];
public void SetMine(Coordinate coordinate)
=> _matrix[coordinate.X, coordinate.Y] = new Cell('*');
public void ChangeValue(Coordinate coordinate, char value)
=> _matrix[coordinate.X, coordinate.Y] = new Cell(value);
public Cell Open(Coordinate coordinate)
=> _matrix[coordinate.X, coordinate.Y];
public IEnumerable ToList()
=> _matrix.Cast<Cell>().ToList();
private string CellsAsString()
=> string.Concat(_matrix.OfType<Cell>().Select(c => c.Value));
public override bool Equals(object other)
=> this.CellsAsString().Equals((other as Matrix)?.CellsAsString());
public override int GetHashCode()
=> this.CellsAsString().GetHashCode();
}
}
EDIT(2):
PrintMatrix and Open methods from the main class.
public void Open(Coordinate coordinate)
{
if (_matrix.At(coordinate).Value == '*')
HasLose = true;
int numOfMines = _matrix.NeighborsOf(coordinate).Count(cell => cell.Value == '*');
_showedMatrix.ChangeValue(coordinate, char.Parse(numOfMines.ToString()));
HasWin = PlayerHasWin();
}
public String PrintMatrix()
{
string temp = "";
for (int x = 0; x < _size.M; x++)
{
for (int y = 0; y < _size.N; y++)
{
temp += _showedMatrix.At(new Coordinate(x, y)).Value;
}
temp += '\n';
}
return temp;
}
Notice that I'm using a showedMatrix which is another matrix with cells and its value for each one is a simple .. I'm using this new matrix so I can change its value and printing it.
These are the two tests that fail.
[Fact]
public void CellIsOpenWithoutAMineButWithOneMineAdjacent()
{
string printExpected = "1...\n....\n....\n....\n";
Matrix matrix = new Matrix(new(4, 4));
matrix.SetMine(new(0,1));
MineSweeper mineSweeper = new(matrix, 2);
mineSweeper.Open(new(0,0));
mineSweeper.PrintMatrix().Should().Be(printExpected);
}
[Fact]
public void CellIsOpenWithoutAMineButWithTwoMineAdjacent()
{
string printExpected = "2...\n....\n....\n....\n";
Matrix matrix = new Matrix(new(4, 4));
matrix.SetMine(new(0,1));
matrix.SetMine(new(1,0));
MineSweeper mineSweeper = new(matrix, 2);
mineSweeper.Open(new(0,0));
mineSweeper.PrintMatrix().Should().Be(printExpected);
}
Since I'm aware that my main class for these tests is putting 2 random mines plus the mines I'm putting by myself with the SetMine() method, I executed these tests several times to make sure that it was failing. The conclusion was that "2...\n....\n....\n....\n"; for some reason is always a 0 instead of a 2, or a 1.
I'd suggest an iterator method on the matrix itself, that took care of checking the bounds before returning the cells, something like this:
IEnumerable<Cell> NeighborsOf(Coordinate coord)
{
// if coord is not in bounds of the matrix, throw an argument exception
for (int x = Math.Max(coord.X - 1, 0); x <= Math.Min(coord.X + 1, _size.M - 1); x++)
{
for (int y = Math.Max(coord.Y - 1, 0); y <= Math.Min(coord.Y + 1, _size.N - 1); y++)
{
if ((x,y) == (coord.X, coord.Y)) continue;
yield return At(new Coordinate(x, y));
}
}
}
The for loops define the "3x3 window" of coordinates (central cell +/- 1) describing a maximum block of 9 cells, that has its edges limited by the Math.Min and Math.Max calls. Then, the central cell itself is skipped by the if check, resulting only on adjacent cells being returned.
Then, to count the asterisks, you just leverage this method:
_matrix.NeighborsOf(coordinate).Count(cell => cell.Value == '*');
Relying on exceptions to ignore the out-of-bounds coordinates would be considered a bad practice (as it would be an use of the "exception as control flow" anti-pattern) and also fairly poor performance-wise.
The neighbor idea also allows you to parameterize the "size" of the neighbor region, if that's useful to you.
You could add a method to the Matrix class, that returns true if both x and y are within their allowed ranges (0 <= x < M, 0 <= y < N) for a given Coordinate:
public bool IsValid(Coordinate coordinate) =>
0 <= coordinate.X && coordinate.X < _size.M &&
0 <= coordinate.Y && coordinate.Y < _size.N;
Then you can count only the valid cells:
return coordinates.Count(x => _matrix.IsValid(x) && _matrix.At(x).Value == '*');
Additionally, it makes sense to use this method for validation purpose in all other public methods of the Matrix class accepting Coordinate as an argument.

Zero padding a 2D array in C#

I currently have an issue with zero padding my 2d Array. I want to transfer my current data in my array to a new array, which is the exact same array but with a border of 0's around it.
Example:
|1 2 3|
|4 5 6|
|7 8 9|
Should become
|0 0 0 0 0|
|0 1 2 3 0|
|0 4 5 6 0|
|0 7 8 9 0|
|0 0 0 0 0|
int[,] Array = new int[,] { { 1, 2, 3 }, { 3, 4, 5 }, { 6, 7, 8 } };
int[,] ArrayZeroPad = new int[Array.GetLength(0) + 2, Array.GetLength(1) + 2];
for (int y = 0; y < Array.GetLength(1); y++)
{
for (int x = 0; x < ArrayZeroPad.GetLength(0); x++)
{
if (y == 0)
{ ArrayZeroPad[y, x] = 0; }
else if (y == ArrayZeroPad.GetLength(1))
{ ArrayZeroPad[y, x] = 0; }
else if (x == 0)
{
ArrayZeroPad[y, x] = 0;
}
else if (x == ArrayZeroPad.GetLength(0))
{ ArrayZeroPad[y, x] = 0; }
else ArrayZeroPad[y, x] = Array[y, x];
}
}
for (int y = 0; y < ArrayZeroPad.GetLength(1); y++)
{
Console.WriteLine();
for (int x = 0; x < ArrayZeroPad.GetLength(0); x++)
{ Console.Write(ArrayZeroPad[y, x]); }
Console.ReadLine();
}
}
This is what I have come to thus far, but I keep getting stuck on out of bounds errors, is there anyone who could work this out for me with some explanation?
Kind regards,
D.
This is not quite what you are asking (I thought a completely different alternative would be interesting).
Here is a No-Copy version that works for any type of array, of any size. It's appropriate if the original array is quite large (since it doesn't require a copy).
It uses a 2-dimensional indexer that either returns the default value of T (zero or null) for items on the edge, and uses the original array (with the indexes offset) for non-edge values:
public class ZeroPadArray <T>
{
private readonly T[,] _initArray;
public ZeroPadArray(T[,] arrayToPad)
{
_initArray = arrayToPad;
}
public T this[int i, int j]
{
get
{
if (i < 0 || i > _initArray.GetLength(0) + 1)
{
throw new ArgumentOutOfRangeException(nameof(i),
$#"Index {nameof(i)} must be between 0 and the width of the padded array");
}
if (j < 0 || j > _initArray.GetLength(1) + 1)
{
throw new ArgumentOutOfRangeException(nameof(j),
$#"Index {nameof(j)} must be between 0 and the width of the padded array");
}
if (i == 0 || j == 0)
{
return default(T);
}
if (i == _initArray.GetLength(0) + 1)
{
return default(T);
}
if (j == _initArray.GetLength(1) + 1)
{
return default(T);
}
//otherwise, just offset into the original array
return _initArray[i - 1, j - 1];
}
}
}
I just tested it with some Debug.Assert calls. The test coverage is weak, but it was good enough to say "this probably works":
int[,] array = new int[,] { { 1, 2, 3 }, { 11, 12, 13 }, { 21, 22, 23 } };
var paddedArray = new ZeroPadArray<int>(array);
Debug.Assert(paddedArray[0, 0] == 0);
Debug.Assert(paddedArray[4,4] == 0);
Debug.Assert(paddedArray[2,3] == 13);
And, finally, for fun, I added a nice little hack to make creating these things require less typing. When you call a method, the compiler is often able to deduce the generic type of the object from the method parameters. This doesn't work for constructors. That's why you need to specify new ZeroPadArray<int>(array) even though array is obviously an array of int.
The way to get around this is to create a second, non-generic class that you use as a static factory for creating things. Something like:
public static class ZeroPadArray
{
public static ZeroPadArray<T> Create<T>(T[,] arrayToPad)
{
return new ZeroPadArray<T>(arrayToPad);
}
}
Now, instead of typing:
var paddedArray = new ZeroPadArray<int>(array);
you can type:
var paddedArray = ZeroPadArray.Create(array);
Saving you two characters of typing (but, you need to admit that typing the <int> is frustrating).
int[,] Array = new int[,] { { 1, 2, 3 }, { 3, 4, 5 }, { 6, 7, 8 } };
int[,] ArrayZeroPad = new int[Array.GetLength(0) + 2, Array.GetLength(1) + 2];
for (int x = 0; x < ArrayZeroPad.GetLength(0); x++)
{
for (int y = 0; y < ArrayZeroPad.GetLength(0); y++)
{
//First row and last row
if (x == 0 || x == ArrayZeroPad.GetLength(0) - 1)
ArrayZeroPad[x, y] = 0;
else
{
//Fist column and last column
if (y == 0 || y == ArrayZeroPad.GetLength(0) - 1)
ArrayZeroPad[x, y] = 0;
else
{
//Content
ArrayZeroPad[x, y] = Array[x-1, y-1];
}
}
}
}
It seems that you are confusing dimensions - Array.GetLength(0) is for the first one in access Array[i, j] and Array.GetLength(1) is for the second. Also you can simplify copy by just scanning through Array elements and adjusting destination indexes by one, you don't need to explicitly set others to 0 cause it would be done for you (unless you are using stackalloc and skipping local init but I highly doubt that this is the case):
var length0 = Array.GetLength(0);
var length1 = Array.GetLength(1);
for (int i = 0; i < length0; i++)
{
for (int j = 0; j < length1; j++)
{
ArrayZeroPad[i + 1, j + 1] = Array[i, j];
}
}
And in the "print" method too - y should be the first dimension and x - second:
var length = ArrayZeroPad.GetLength(0);
for (int y = 0; y < length; y++)
{
Console.WriteLine();
var i = ArrayZeroPad.GetLength(1);
for (int x = 0; x < i; x++)
{
Console.Write(ArrayZeroPad[y, x]);
}
Console.ReadLine();
}
You can also solve this using Array.Copy(). If you require highest performance and the arrays are big enough, then this might be faster than explicitly copying each element:
public static int[,] Pad(int[,] input)
{
int h = input.GetLength(0);
int w = input.GetLength(1);
var output = new int[h+2, w+2];
for (int r = 0; r < h; ++r)
{
Array.Copy(input, r*w, output, (r+1)*(w+2)+1, w);
}
return output;
}
The (r+1)*(w+2)+1 requires some explanation. Array.Copy() treats a 2D array as a linear 1D array, and you must specify the destination offset for the copy as an offset from the start of the 1D array (in row-major order).
Since w is the width of the input array and r is the current row of the input array, the destination for the copy of the current input row will be the output row number, (r+1) times the output row width (w+2), plus 1 for to account for the left-hand column of 0 in the output array.
It's possible that using Buffer.BlockCopy() (which operates on bytes) could be even faster:
public static int[,] Pad(int[,] input)
{
int h = input.GetLength(0);
int w = input.GetLength(1);
var output = new int[h+2, w+2];
for (int r = 0; r < h; ++r)
{
Buffer.BlockCopy(input, r*w*sizeof(int), output, ((r+1)*(w+2)+1)*sizeof(int), w*sizeof(int));
}
return output;
}
As always, this is only worth worrying about if performance is critical, and even then only after you've benchmarked the code to verify that it actually is faster.

Is my method for Recursive division for my maze generator correct?

Im trying to make a maze generator using Recursive division explained here: https://en.wikipedia.org/wiki/Maze_generation_algorithm
The first iteration works for me. But when I do it multiple times It doesnt do it properly.
Question is: Is my recursve method corrext? The if and else statements followed up with either a return or recusion.
private void DevideRecursive(int pMinX, int pMaxX, int pMinY, int pMaxY)
{
Debug.Log("minx: " + pMinX);
Debug.Log("maxx: " + pMaxX);
Debug.Log("miny: " + pMinY);
Debug.Log("maxy: " + pMaxY);
int randomX = Random.Range(pMinX +1, pMaxX);
int randomY = Random.Range(pMinY +1, pMaxY);
int randomWall = Random.Range(0, 4);
List<GameObject> WalllistX1 = new List<GameObject>();
List<GameObject> WalllistX2 = new List<GameObject>();
List<GameObject> WalllistY1 = new List<GameObject>();
List<GameObject> WalllistY2 = new List<GameObject>();
List<List<GameObject>> MainWallList = new List<List<GameObject>>();
MainWallList.Add(WalllistX1);
MainWallList.Add(WalllistX2);
MainWallList.Add(WalllistY1);
MainWallList.Add(WalllistY2);
//// add a wall on a random x coordinate
for (int x = pMinX; x < pMaxX; x++)
{
GameObject wall = Instantiate(WallHor);
wall.transform.position = new Vector2(tilesize * x + tilesize / 2, tilesize * randomY);
if (x < randomX)
{
WalllistX1.Add(wall);
}
else
{
WalllistX2.Add(wall);
}
}
//// add a wall on a random y coordinate
for (int y = pMinY; y < pMaxY ; y++)
{
GameObject wall = Instantiate(WallVer);
wall.transform.position = new Vector2(tilesize * randomX, tilesize * y + tilesize / 2);
if (y < randomY)
{
WalllistY1.Add(wall);
}
else
{
WalllistY2.Add(wall);
}
}
//make a hole in 3 out of tht 4 walls randomly
for (int i = 0; i < MainWallList.Count; i++)
{
Debug.Log("list" + MainWallList.Count);
if (randomWall != i)
{
Debug.Log("1: " + WalllistX1.Count);
Debug.Log("2: " + WalllistX2.Count);
Debug.Log("3: " + WalllistY1.Count);
Debug.Log("4: " + WalllistY2.Count);
RemoveWall(MainWallList[i]);
}
}
////
////
//// If either of the walls have a cell with only 1 grid stop the recursion
if (randomX - pMinX <= 1 || pMaxY - randomY <= 1)
{
return;
}
else
{
DevideRecursive(pMinX, randomY, randomX, pMaxY);
}
if (pMaxX - randomX <= 1 || pMaxY - randomY <= 1)
{
return;
}
else
{
DevideRecursive(randomY, pMaxX, randomX, pMaxY);
}
if (randomX - pMinX <= 1 || randomY - pMinY <=1)
{
return;
}
else
{
DevideRecursive(pMinX, randomY, pMinY, randomX);
}
if (pMaxX - randomX <= 1 || randomY - pMinY <= 1)
{
return;
}
else
{
DevideRecursive(randomY, pMaxX, pMinY, randomX);
}
}
what my code does(or is supposed to do). in a rectangular maze, build at random points two walls that are perpendicular to each other. These two walls divide the large chamber into four smaller chambers separated by four walls. Choose three of the four walls at random, and open a one cell-wide hole at a random point in each of the three. Continue in this manner recursively, until every chamber has a width of one cell in either of the two directions.
here is quick illustration of the frist iteration https://gyazo.com/08e1cb2483fcf841e4d854d001c51647
for example the next iteration is going to be in the upper left room. then minX stays the same. MaxX becomes the RandomX. the same for the Y values. then it should do the same again(creating 2 walls )

How can I run code when the contents of an Array change?

Basically, I am creating a voxel-based game. I have an object Chunk with a 3 dimensional array of the object Voxel. In the array of Voxels, the Voxel currently has an isAir bool on it that signifies no voxel being there.
I am trying to add a property to Chunk which returns a 2 dimensional array of Voxel's that are on the surface of the chunk. Currently, this array will always have Voxel.isAir == false in the lower Y values and Voxel.isAir == true in the higher Y values, there is never a clear run on the "Y" axis of the array that is either all "air"s or all Voxel's, and there is never a "air" below a Voxel in the array.
To get only the "Surface" Voxel's, I have added this code to the Chunk:
public Voxel[,,] Voxels { get; set; }
private Voxel[,] _surfaceVoxels = null;
public Voxel[,] SurfaceVoxels
{
get
{
if (_surfaceVoxels == null)
{
_surfaceVoxels = new Voxel[this.Voxels.GetLength(0), this.Voxels.GetLength(2)];
for (var x = 0; x < this.Voxels.GetLength(0); x++)
{
for (var z = 0; z < this.Voxels.GetLength(2); z++)
{
for (var y = 0; y < this.Voxels.GetLength(1); y++)
{
Voxel v = this.Voxels[x, y, z];
var solidAbove = false;
var solidBelow = false;
if (y - 1 >= 0)
{
Voxel vBelow = this.Voxels[x, y - 1, z];
solidBelow = !vBelow.isAir;
}
if (y + 1 < this.Voxels.GetLength(1))
{
Voxel vAbove = this.Voxels[x, y + 1, z];
solidAbove = !vAbove.isAir;
}
if (!v.isAir && !solidAbove && solidBelow)
{
_surfaceVoxels[x, z] = v;
}
}
}
}
}
return _surfaceVoxels;
}
}
Calculating the surface voxels this way is computationally expensive, and i cannot see a faster way of doing this. Because of this I effectively "cache" the surface array, which is fine until the underlying 3 dimensional array is changed, as then the surface voxels will obviously need to be recalculated.
Is there some way on the get of public Voxel[,,] it only returns a clone of the array and not the array itself, and on the set set the entire array and set _surfaceVoxels to NULL, or even better is there some inexpensive way to execute code when values in an array are changed?
Is there some a more efficient way to accomplish what I have explained here that I have overlooked?

Randomly generate blocks on a flat map

I'm trying to randomly generate blocks on a flat map and make it so that they don't overlap each other.
I have made a matrix (c# array) of the size of the map (500x500), the blocks have a scale between 1 and 5.
The code works but if a generated block overlaps another one, it is destroyed and not regenerated somewhere else.
Only around 80 of the 1000 blocks I try to generate don't overlap another block.
Here is a picture of the map with around 80 blocks generated, the green squares are blocks
void generateElement(int ratio, int minScale, int maxScale, GameObject g) {
bool elementFound = false;
for (int i = 0; i < ratio * generationDefault; i++) {
GameObject el;
// Randomly generate block size and position
int size = Random.Range(minScale, maxScale + 1);
int x = Random.Range(0, mapSizex + 1 - size);
int y = Random.Range(0, mapSizey + 1 - size);
// Check if there is already an element
for (int j = x; j < x + size; j++)
for (int k = y; k < y + size; k++)
if (map[j][k] != null)
elementFound = true;
if (elementFound)
continue;
else {
el = (GameObject)Instantiate(g, new Vector3(x + (float)size / 2, (float)size / 2, y + (float)size / 2), Quaternion.Euler(0, 0, 0));
el.transform.localScale *= size;
}
// Create element on map array
for (int j = x; j < x + size; j++)
for (int k = y; k < y + size; k++)
if (map[j][k] == null) {
map[j][k] = el.GetComponent<ObjectInterface>();
}
}
}
I thought of 3 possible fixes
I should set the size of the block depending of the place it has.
I should use another randomization algorithm.
I'm not doing this right.
What do you think is the best idea ?
UPDATE
I got the code working much better. I now try to instantiate the blocks multiple times if needed (maximum 5 for the moment) and I fixed the bugs. If there are already many elements on the map, they will not always be instantiated and that's what I wanted, I just have to find the right amount of times it will try to instantiate the block.
I tried instantiating 1280 elements on a 500x500 map. It takes only about 1.5 second and it instantiated 1278/1280 blocks (99.843%).
void generateElement(int ratio, int minScale, int maxScale, GameObject g) {
bool elementFound = false;
int cnt = 0;
// Generate every block
for (int i = 0; i < ratio * generationDefault; i++) {
GameObject el = null;
// Randomly generate block size and position
int size, x, y, tryCnt = 0;
// Try maximum 5 times to generate the block
do {
elementFound = false;
// Randomly set block size and position
size = Random.Range(minScale, maxScale + 1);
x = Random.Range(0, mapSizex + 1 - size);
y = Random.Range(0, mapSizey + 1 - size);
// Check if there is already an element
for (int j = x; j < x + size; j++)
for (int k = y; k < y + size; k++)
if (map[j][k] != null)
elementFound = true;
tryCnt++;
} while (elementFound && tryCnt < 5);
if (tryCnt >= 5 && elementFound) continue;
// Instantiate the block
el = (GameObject)Instantiate(g, new Vector3(x + (float)size / 2, (float)size / 2, y + (float)size / 2), Quaternion.Euler(0, 0, 0));
el.transform.localScale *= size;
// Create element on map array
for (int j = x; j < x + size; j++)
for (int k = y; k < y + size; k++)
if (map[j][k] == null) {
map[j][k] = el.GetComponent<ObjectInterface>();
}
cnt++;
}
print("Instantiated " + cnt + "/" + ratio * generationDefault);
}
This is incredibly difficult to do well.
Here's a quick solution you'll maybe like ... depending on your scene.
actualWidth = 500 //or whatever. assume here is square
// your blocks are up to 5 size
chunkWidth = actualWidth / 5
// it goes without saying, everything here is an int
kChunks = chunkWidth*chunkWidth
List<int> shuf = Enumerable.Range(1,kChunks).OrderBy(r=>Random.value).ToList();
howManyWanted = 1000
shuf = shuf.Take(howManyWanted)
foreach( i in shuf )
x = i % actualWidth
y = i / actualWidth
make block at x y
put block in list allBlocks
HOWEVER ............
...... you'll see that this looks kind of "regular", so do this:
Just randomly perturb all the blocks. Remember, video game programming is about clever tricks!
Ideally, you have to start from the middle and work your way out; in any event you can't just do them in a line. Shuffling is OK. So, do this ..
harmonic = 3 //for example. TRY DIFFERENT VALUES
function rh = Random.Range(1,harmonic) (that's 1 not 0)
function rhPosNeg
n = rh
n = either +n or -n
return n
function onePerturbation
{
allBlocks = allBlocks.OrderBy(r => Random.value) //essential
foreach b in allBlocks
newPotentialPosition = Vector2(rhPosNeg,rhPosNeg)
possible = your function to check if it is possible
to have a block at newPotentialPosition,
however be careful not to check "yourself"
if possible, move block to newPotentialPosition
}
The simplest approach is just run onePerturbation, say, three times. Have a look at it between each run. Also try different values of the harmonic tuning factor.
There are many ways to perturb fields of differently-sized blocks, above is a KISS solution that hopefully looks good for your situation.
Coding note...
How to get sets of unique random numbers.
Just to explain this line of code...
List<int> shuf = Enumerable.Range(1,kChunks).OrderBy(r=>Random.value).ToList();
If you are new to coding: say you want to do this: "get a hundred random numbers, from 1 to million, but with no repeats".
Fortunately, this is a very well known problem with a very simple solution.
The way you get numbers with no repeats, is simply shuffle all the numbers, and then take how many you want off the top.
For example, say you need a random couple of numbers from 1-10 but with no repeats.
So, here's the numbers 1-10 shuffled: 3,8,6,1,2,7,10,9,4,5
Simply take what you need off the front: so, 3, 8, 6 etc.
So to make an example let's say you want twelve numbers, no repeats, from 1 through 75. So the first problem is, you want a List with all the numbers up to 75, but shuffled. In fact you do that like this ..
List<int> shuf = Enumerable.Range(1,75).OrderBy(r=>Random.value).ToList();
So that list is 75 items long. You can check it by saying foreach(int r in shuf) Debug.Log(r);. Next in the example you only want 12 of those numbers. Fortunately there's a List call that does this:
shuf = shuf.Take(12)
So, that's it - you now have 12 numbers, no repeats, all random between 1 and 75. Again you can check with foreach(int r in shuf) Debug.Log(r);
In short, when you want "n" numbers, no repeats, between 1 and Max, all you have to so is this:
List<int> shuf = Enumerable.Range(1,Max).OrderBy(r=>Random.value).ToList();
shuf = shuf.Take(n);
et voilĂ , you can check the result with foreach(int r in shuf) Debug.Log(r);
I just explain this at length because the question is often asked "how to get random numbers that are unique". This is an "age-old" programming trick and the answer is simply that you shuffle an array of all the integers involved.
Interestingly, if you google this question ("how to get random numbers that are unique") it's one of those rare occasions where google is not much help, because: whenever this question is asked, you get a plethora of keen new programmers (who have not heard the simple trick to do it properly!!) writing out huge long complicated ideas, leading to further confusion and complication.
So that's how you make random numbers with no repeats, fortunately it is trivial.
if (elementFound) continue; will skip out this current loop iteration. You need to wrap the int x=Random..; int y=Random()..; part in a while loop with the condition being while(/* position x/y already occupued*/) { /* generate new valid point */} like this for example:
void generateElement(int ratio, int minScale, int maxScale, GameObject g) {
for (int i = 0; i < ratio * generationDefault; i++) {
GameObject el;
// Randomly generate block size and position
bool elementFound = false;
int size, x, y;
do
{
elementFound = false;
size = Random.Range(minScale, maxScale + 1);
x = Random.Range(0, mapSizex + 1 - size);
y = Random.Range(0, mapSizey + 1 - size);
// Check if there is already an element
for (int j = x; j < x + size; j++)
for (int k = y; k < y + size; k++)
if (map[j][k] != null)
elementFound = true;
} while(elementFound);
el = (GameObject)Instantiate(g, new Vector3(x + (float)size / 2, (float)size / 2, y + (float)size / 2), Quaternion.Euler(0, 0, 0));
el.transform.localScale *= size;
// Create element on map array
for (int j = x; j < x + size; j++)
for (int k = y; k < y + size; k++)
if (map[j][k] == null) {
map[j][k] = el.GetComponent<ObjectInterface>();
}
}
}
You shouldn't be getting that many collisions.
Assuming your blocks were ALL 5 units wide and you're trying to fit them into a grid of 500,500 you would have 100*100 spaces for them at minimum, which gives 10,000 spaces into which to fit 1,000 blocks.
Try playing around with this code:
using System;
using System.Collections.Generic;
namespace ConsoleApplication1
{
class Program
{
static void Main()
{
var result = PlaceNonOverlappingBlocks(1000, 5, 500, 500);
}
static List<Block> PlaceNonOverlappingBlocks(int count, int maxBlockSize, int mapX, int mapY)
{
var map = new bool[mapY, mapX];
var rng = new Random();
var result = new List<Block>(count);
int collisions = 0;
while (count > 0)
{
int size = rng.Next(1, maxBlockSize + 1);
int x = rng.Next(0, mapX - size);
int y = rng.Next(0, mapY - size);
if (fits(map, x, y, size))
{
result.Add(new Block(x, y, size));
addToMap(map, x, y, size);
--count;
}
else
{
if (++collisions> 100000)
throw new InvalidOperationException("Hell has frozen over");
}
}
// This is just for diagnostics, and can be removed.
Console.WriteLine($"There were {collisions} collisions.");
return result;
}
static void addToMap(bool[,] map, int px, int py, int size)
{
for (int x = px; x < px+size; ++x)
for (int y = py; y < py + size; ++y)
map[y, x] = true;
}
static bool fits(bool[,] map, int px, int py, int size)
{
for (int x = px; x < px + size; ++x)
for (int y = py; y < py + size; ++y)
if (map[y, x])
return false;
return true;
}
internal class Block
{
public int X { get; }
public int Y { get; }
public int Size { get; }
public Block(int x, int y, int size)
{
X = x;
Y = y;
Size = size;
}
}
}
}

Categories

Resources