Reputation: 85
I'm currently learning how to code games and have designed a biom generation algorithm.
As long as I run that algorithm below syncron, it generates the same output every time and works perfectly fine.
Now I tried to speed it up and make it multithreaded, but every time I call the method, it results in a different result.
As far as I know, I used Threadsave Collections, whenever necessary, but it still doesn't work.
Also, I tried to lock the collection, but this didn't work either.
So I'm completely clueless as to why this doesn't work.
If you see anything that I could make better or how I could fix that problem, please let me know.
This code is working:
private Biome[,] Generate(string worldSeed, Vector2Int targetChunk, List<(Biome, float)> multiplier, float centroidsPerChunk)
{
//Calculate the NeighboursToGenerate depeding on the cendroids per Chunk value
int chunkNeighboursToGenerate = (int)Math.Ceiling(Math.Sqrt(1f / centroidsPerChunk * 12.5f));
int chunkSize = 8;
//Create List that contains all centroids of the chunk
List<(Vector2Int, Biome)> centroids = new();
//Create Centdroids for every chunk of the generated region around the targetchunk
for (int chunkX = targetChunk.x - chunkNeighboursToGenerate; chunkX < targetChunk.x + chunkNeighboursToGenerate + 1; chunkX++)
{
for (int chunkZ = targetChunk.y - chunkNeighboursToGenerate; chunkZ < targetChunk.y + chunkNeighboursToGenerate + 1; chunkZ++)
{
List<(Vector2Int, Biome)> generatedCentdroids = GetCentdroidsByChunk(worldSeed, new(chunkX, chunkZ), centroidsPerChunk, chunkSize, multiplier, targetChunk, chunkNeighboursToGenerate);
foreach ((Vector2Int, Biome) generatedCentdroid in generatedCentdroids)
{
centroids.Add(generatedCentdroid);
}
}
}
Biome[,] biomeMap = new Biome[chunkSize, chunkSize];
//---Generate biomeMap of the target Chunk---
for (int tx = 0; tx < chunkSize; tx++)
{
for (int tz = 0; tz < chunkSize; tz++)
{
int x = chunkSize * chunkNeighboursToGenerate + tx;
int z = chunkSize * chunkNeighboursToGenerate + tz;
biomeMap[tz, tx] = GetClosestCentroidBiome(new(x, z), centroids.ToArray());
};
};
//Return the biome map of the target chunk
return biomeMap;
}
private static List<(Vector2Int, Biome)> GetCentdroidsByChunk(string worldSeed, Vector2Int chunkToGenerate, float centroidsPerChunk, int chunkSize, List<(Biome, float)> multiplier, Vector2Int targetChunk, int chunkNeighboursToGenerate)
{
List<(Vector2Int, Biome)> centroids = new();
//---Generate Cendroids of a single chunk---
float centroidsInThisChunk = centroidsPerChunk;
//Init randomizer
System.Random randomInstance = new(Randomizer.GetSeed(worldSeed, chunkToGenerate.x, chunkToGenerate.y));
while (centroidsInThisChunk > 0.0f)
{
//if at least one more centroid is to generate do it
//if not randomize by the given probability if another one should be generated
if (centroidsInThisChunk >= 1 || (float)randomInstance.NextDouble() * (1 - 0) + 0 <= centroidsInThisChunk)
{
//Generate random point for a new centroid
Vector2Int pos = new(randomInstance.Next(0, chunkSize + 1), randomInstance.Next(0, chunkSize + 1));
//map the point to a zerobased coordinatesystem
int mappedX = (((chunkToGenerate.x - targetChunk.x) + chunkNeighboursToGenerate) * chunkSize) + pos.x;
int mappedZ = (((chunkToGenerate.y - targetChunk.y) + chunkNeighboursToGenerate) * chunkSize) + pos.y;
Vector2Int mappedPos = new Vector2Int(mappedX, mappedZ);
//Select the biom randomized
Biome biome = Randomizer.GetRandomBiom(randomInstance, multiplier);
centroids.Add(new(mappedPos, biome));
centroidsInThisChunk -= 1.0f;
}
//if no centroid is left to generate, end the loop
else
{
break;
}
}
return centroids;
}
//Calculates the closest Centroid to the given possition
Biome GetClosestCentroidBiome(Vector2Int pixelPos, IEnumerable<(Vector2Int, Biome)> centroids)
{
//Warp the possition so the biom borders won't be straight
//Vector2 warpedPos = pixelPos + Get2DTurbulence(pixelPos);
Vector2 warpedPos = pixelPos;
float smallestDst = float.MaxValue;
Biome closestBiome = Biome.Empty;
foreach ((Vector2Int, Biome) centroid in centroids)
{
float distance = Vector2.Distance(warpedPos, centroid.Item1);
if (distance < smallestDst)
{
smallestDst = distance;
closestBiome = centroid.Item2;
}
}
return closestBiome;
}
public static class Randomizer
{
//Generates a random integerseed by combining an hashing the inputvalues
public static int GetSeed(string worldSeed, int chunkx, int chunkz)
{
var stringSeed = worldSeed + ":" + chunkx + ";" + chunkz;
MD5 md5Hasher = MD5.Create();
byte[] hashed = md5Hasher.ComputeHash(Encoding.UTF8.GetBytes(stringSeed));
return BitConverter.ToInt32(hashed, 0);
}
//Returns a random biome based on the given properbilities/multiplier
//multiplier = 2 for example means the biom is generated twice as often as usually
public static Biome GetRandomBiom(System.Random rndm, List<(Biome, float)> multiplier)
{
float multmax = 0.0f;
multiplier.ForEach(x => multmax += x.Item2);
//Generate a random value that is in the range of all multiplieres added
float biome = (float)rndm.NextDouble() * (multmax + 0.01f);
//Map the biome to the multipliers and return the biome
float multcalc = 0.0f;
for (int r = 0; r < multiplier.Count; r++)
{
multcalc += multiplier[r].Item2;
if (multcalc >= biome)
{
return multiplier[r].Item1;
}
}
//Return Biome.Empty if something did't worked correct
return Biome.Empty;
}
}
This doesn't work:
private Biome[,] Generate(string worldSeed, Vector2Int targetChunk, List<(Biome, float)> multiplier, float centroidsPerChunk)
{
//Calculate the NeighboursToGenerate depeding on the cendroids per Chunk value
int chunkNeighboursToGenerate = (int)Math.Ceiling(Math.Sqrt(1f / centroidsPerChunk * 12.5f));
int chunkSize = 8;
//Create List that contains all centroids of the chunk
ConcurrentBag<(Vector2Int, Biome)> centroids = new();
ConcurrentQueue<Task> tasks = new();
//Create Centdroids for every chunk of the generated region around the targetchunk
for (int chunkX = targetChunk.x - chunkNeighboursToGenerate; chunkX < targetChunk.x + chunkNeighboursToGenerate + 1; chunkX++)
{
for (int chunkZ = targetChunk.y - chunkNeighboursToGenerate; chunkZ < targetChunk.y + chunkNeighboursToGenerate + 1; chunkZ++)
{
tasks.Enqueue(Task.Run(() =>
{
List<(Vector2Int, Biome)> generatedCentdroids = GetCentdroidsByChunk(worldSeed, new(chunkX, chunkZ), centroidsPerChunk, chunkSize, multiplier, targetChunk, chunkNeighboursToGenerate);
foreach ((Vector2Int, Biome) generatedCentdroid in generatedCentdroids)
{
centroids.Add(generatedCentdroid);
}
}));
}
}
Biome[,] biomeMap = new Biome[chunkSize, chunkSize];
Task.WaitAll(tasks.ToArray());
//---Generate biomeMap of the target Chunk---
for (int tx = 0; tx < chunkSize; tx++)
{
for (int tz = 0; tz < chunkSize; tz++)
{
int x = chunkSize * chunkNeighboursToGenerate + tx;
int z = chunkSize * chunkNeighboursToGenerate + tz;
biomeMap[tz, tx] = GetClosestCentroidBiome(new(x, z), centroids.ToArray());
};
};
//Return the biome map of the target chunk
return biomeMap;
}
Upvotes: 1
Views: 153
Reputation: 78
If you're starting to get into programming and you want to learn multi-threading, converting a large piece of complex code like this is not where you want to start. I highly recommend you pick up a book or tutorial on threading/async in C#/.NET before starting something like this. Unity also has its own multi-threading library with its Job System, which is built for the Unity workflow: https://docs.unity3d.com/Manual/JobSystemMultithreading.html
I don't think most people could find what's causing the problem in these two code snippets alone. But I have a couple of suggestions
List<T>
, tasks
is only ever accessed on one thread so there's no need to use ConcurrentQueue<T>
Biome
a class? Cause if so it's technically fine but modifying data structures from multiple threads gets hairy fast. And while I can't see that you're modifying data from these snippets, without the full code I can't say for sure. Turn Biome
into a struct or make a struct equivalent for threading purposes.centroids.ToArray()
in your loop, as doing so will actually copy the original array over and over and over again. Call it once outside of your loop and that alone should be a pretty huge performance bump.List<T>
inside your tasks that you're new to threading. Understanding what code is ran on another thread and the repercussions from that (race conditions, and so on) is huge.Upvotes: 1