✅ World generation optimization

Hey guys, i'm having issues with world generation for my game, do you guys know how i could optimize it? it takes 15s for the window to load rn
61 Replies
Mąż Zuzanny Harmider Szczęście
public Tile(MainWindow window, double x, double y, TileType type) : this(window)
{
X = x;
Y = y;
Type = type;
List<Tile> tiles = _MainWindow.Display.Children.OfType<Tile>().ToList();
tiles = tiles.Where(r => r.Type == Type && r.Source != null).ToList();
if (tiles.Count>0)
{
Tile tile = tiles.First();
Source=tile.Source;
}
else
{
ImageBrush brush = new ImageBrush();
try
{

brush.ImageSource = new BitmapImage(new Uri(GetImagePath(type), UriKind.RelativeOrAbsolute));
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}

Source = brush.ImageSource;

}
MouseLeftButtonDown += TileClicked;
}
public Tile(MainWindow window, double x, double y, TileType type) : this(window)
{
X = x;
Y = y;
Type = type;
List<Tile> tiles = _MainWindow.Display.Children.OfType<Tile>().ToList();
tiles = tiles.Where(r => r.Type == Type && r.Source != null).ToList();
if (tiles.Count>0)
{
Tile tile = tiles.First();
Source=tile.Source;
}
else
{
ImageBrush brush = new ImageBrush();
try
{

brush.ImageSource = new BitmapImage(new Uri(GetImagePath(type), UriKind.RelativeOrAbsolute));
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}

Source = brush.ImageSource;

}
MouseLeftButtonDown += TileClicked;
}
tried to shorten the time of each individual image to load, but it changed nothing anyone? please i need help
FusedQyou
FusedQyou2mo ago
I have a really hard time understanding why half this code is the way it is Dispatchers? 2 second timers? Why the hell do these things exist in a world generator? I would expect an asynchronous method that builds the world in the background. You'd then delegate the result to the UI thread using a Dispatcher which does STRICTLY the visualization What is this anyway? Please tell me it's not Winforms?
Mąż Zuzanny Harmider Szczęście
Dispatcher is for it to be async ig, and the timer is for tick wpf
Mąż Zuzanny Harmider Szczęście
this is the game
No description
FusedQyou
FusedQyou2mo ago
Dispatchers and timers are not async A Dispatcher just delegates stuff to the UI thread
Mąż Zuzanny Harmider Szczęście
oh, ok how should i do it then
FusedQyou
FusedQyou2mo ago
Make it async, use an async Task
Mąż Zuzanny Harmider Szczęście
thats why it takes so much for the window to load...
FusedQyou
FusedQyou2mo ago
Just make sure it doesn't involve UI modification because WPF doesn't like that
Mąż Zuzanny Harmider Szczęście
okey, could u send me a example/tutorial to Task?
FusedQyou
FusedQyou2mo ago
You have to use Dispatcher when you want to delegate calls to the UI thread for modifying the UI after the task ran
Mąż Zuzanny Harmider Szczęście
whats the async alternative to timer?
FusedQyou
FusedQyou2mo ago
There's an async timer variant
Mąż Zuzanny Harmider Szczęście
dont understand
FusedQyou
FusedQyou2mo ago
You don't really need to modify the timer though
FusedQyou
FusedQyou2mo ago
Asynchronous programming - C#
An overview of the C# language support for asynchronous programming using async, await, Task, and Task
FusedQyou
FusedQyou2mo ago
You can also use a thread, which kind of works the same but is more lower level. I'd use async Task though
Mąż Zuzanny Harmider Szczęście
i dont understand ;-; async in js for example is so much simpler
FusedQyou
FusedQyou2mo ago
It's not hard Idk what you want me to say Even in javascript you have to explicitly define an async function and await it That's also the case here, but you can work with a return type called Task which basically defines the internal workings on asynchronous programming That's nothing you have to worry about, you just have to understand that Task means asychronous. This means you can switch between asynchronous and synchronous in C#
Mąż Zuzanny Harmider Szczęście
like i get the await part, but what if i just want to make it not wait to generate the world?
FusedQyou
FusedQyou2mo ago
Then call the method but don't await it Note the method executed regardless if you wait for it A good idea is to wrap it in Task.Run, due to the way asynchronous works with threads It doesn't always run asynchronous because it efficiently determines if it has to Task.Run ensures it guarantees that it's run in the background
Mąż Zuzanny Harmider Szczęście
public MainWindow()
{
var generate = GenerateWorld()
}

private async Task GenerateWorld()
{
//code here
}
public MainWindow()
{
var generate = GenerateWorld()
}

private async Task GenerateWorld()
{
//code here
}
like this?
FusedQyou
FusedQyou2mo ago
Just GenerateWorld(). You don't need the return type But it does absolutely nothing because GenerateWorld is not asynchronous You have to understand you can't just wrap this in an asynchronous method and get an improvement. Your code is not set up in a way where it will work properly for it A big ass Dispatcher method prevents it from reaching half the code because it's all moved to the UI thread You have to write code that generates the world asynchronously in the background in full, and when it's done it has to tell the UI to update Alternatively, even better, is to generate parts and move each resulting part to the UI thread for it to be stored and rendered I can't/won't help you with it because I literally can't wrap my head around half this code and what you try to accomplish You should really look into asynchronous programming tho
Mąż Zuzanny Harmider Szczęście
ok, so like push all the tiles to a list and move the list to a dispatcher? @Foosed am i doing it right?:
private async void GenerateWorld()
{
List<Tile> tiles = new List<Tile>();
for(int i=0;i<WorldHeight;i++)
{
for(int j=0;j<WorldWidth;j++)
{
//populate list
tiles.Add(new Tile(i,j))
}
}
await Dispatcher.InvokeAsync(()=>{
foreach(Tile tile in tiles)Display.Children.Add(tile)
})
}
private async void GenerateWorld()
{
List<Tile> tiles = new List<Tile>();
for(int i=0;i<WorldHeight;i++)
{
for(int j=0;j<WorldWidth;j++)
{
//populate list
tiles.Add(new Tile(i,j))
}
}
await Dispatcher.InvokeAsync(()=>{
foreach(Tile tile in tiles)Display.Children.Add(tile)
})
}
FusedQyou
FusedQyou2mo ago
No, you should really learn asynchronous programming using a more basic example But maybe take a step back. Figure out what specifically takes an ass load of time to happen in your code. Maybe it's simpler to fix when it comes to speeding things up
Mąż Zuzanny Harmider Szczęście
i tried, but i just dont see how it translates to my project
Danny May
Danny May2mo ago
I dont think the answer would be to make it async here, atleast not at first. I think theres some issues with the loops in the first place, depending on the size of WorldHeight and WorldWidth. In the first loop of GenerateWorld you loop over every tile (WorldHeight * WorldWidth iterations) and create a new tile. Inside the Tile constructor, you then loop over the Display.Children twice to find a tile with the same type and null source. It would be much faster to initialize your image sources first, and then look them up in a dictionary.
class MainWindow
{
public Dictionary<TileType, BitmapImage> TileImages { get; private set; }

public MainWindow()
{
InitializeComponent();
TileImages = Enum.GetValues<TileType>()
.ToDictionary(type => type, type => new BitmapImage(new Uri(GetImagePath(type), UriKind.RelativeOrAbsolute)));
...
}
}

class Tile
{
public Tile(MainWindow window, double x, double y, TileType type) : this(window)
{
X = x;
Y = y;
Type = type;
Source = window.TileImages[type];
MouseLeftButtonDown += TileClicked;
}
}
class MainWindow
{
public Dictionary<TileType, BitmapImage> TileImages { get; private set; }

public MainWindow()
{
InitializeComponent();
TileImages = Enum.GetValues<TileType>()
.ToDictionary(type => type, type => new BitmapImage(new Uri(GetImagePath(type), UriKind.RelativeOrAbsolute)));
...
}
}

class Tile
{
public Tile(MainWindow window, double x, double y, TileType type) : this(window)
{
X = x;
Y = y;
Type = type;
Source = window.TileImages[type];
MouseLeftButtonDown += TileClicked;
}
}
Then in the MaxTrees and MaxMushrooms loops of GenerateWorld you iterate and over all the tiles on every iteration, but never change any property on the tile in either of the loops. Pull these lines out of the for loops:
Tile[] tiles = Display.Children.OfType<Tile>().Where(r => r.Type == TileType.GrassA).ToArray();
for(int i = 0; i < MaxTrees; i++)
{
...
}
tiles = tiles.Where(tile =>!tiles.Any(otherTile => tile != otherTile && tile.OverlapsWith(otherTile))).ToArray();
for(int i=0; i < MaxMushrooms; i++)
{
...
}
Tile[] tiles = Display.Children.OfType<Tile>().Where(r => r.Type == TileType.GrassA).ToArray();
for(int i = 0; i < MaxTrees; i++)
{
...
}
tiles = tiles.Where(tile =>!tiles.Any(otherTile => tile != otherTile && tile.OverlapsWith(otherTile))).ToArray();
for(int i=0; i < MaxMushrooms; i++)
{
...
}
Also in the MaxTrees and MaxMushrooms loops, you perform a check on the type of the bgTile every loop, but the type can never be DestructableTile because you havent added any of type GrassA at any point before that. I think you can remove that check but double check before committing to that.
Mąż Zuzanny Harmider Szczęście
thanks man, when it comes to the tile images ill try it, but with the other stuff i already coded a solution and got the world to generate in about 3s
Mąż Zuzanny Harmider Szczęście
this is my new GenerateWorld() function
FusedQyou
FusedQyou2mo ago
Eh, you know the whole async thing was something I mentioned because it does generally improve the application by avoiding blocking calls if done right But if world generationt akes a long time that's not something asynchronous programming just fixes 3 seconds is still much, but it sounds like a reasonable number when crunching through data to generate it
FusedQyou
FusedQyou2mo ago
Can you share the code through https://scriptbin.xyz/ ?
Scriptbin - Share Your Code Easily
Use Scriptbin to share your code with others quickly and easily.
Mąż Zuzanny Harmider Szczęście
Scriptbin - Share Your Code Easily
Use Scriptbin to share your code with others quickly and easily.
Mąż Zuzanny Harmider Szczęście
Took 1312ms to generate world damn added a Stopwatch to measure started it in mainwindow() and stopped at end of worlgen
Danny May
Danny May2mo ago
Considering there are only 2160 tiles (I think) 3s is a lot of time for generating the world. I think the change to the time images will help, but probably not solve the speed issue. AddTilesToUIThread wont really help much I dont think because GenerateWorld is already running on the UI thread, so they will be dispatched instantly. A quick and dirty way to get GenerateWorld to run in the background would be to change
Loaded +=(s,e)=>GenerateWorld();
Loaded +=(s,e)=>GenerateWorld();
to
Loaded +=(s,e)=> Task.Run(GenerateWorld);
Loaded +=(s,e)=> Task.Run(GenerateWorld);
Id recommend you split GenerateWorld up into multiple methods too, theres a lot going on in there.
Mąż Zuzanny Harmider Szczęście
. without AddTilesToUIThread it took a lot more time for the game to load this is good advice, already thinking of how to do so
Danny May
Danny May2mo ago
Looking over the code I cant see anywhere that jumps out as a performance bottleneck, not at 2k items in the collection. Worst case this line will have to loop through all 2k tiles for every tile you attempt to place a mushroom or tree at
var existingTile = Display.Children.OfType<Tile>().FirstOrDefault(t => t.X == col * Tile.Size && t.Y == row * Tile.Size);
var existingTile = Display.Children.OfType<Tile>().FirstOrDefault(t => t.X == col * Tile.Size && t.Y == row * Tile.Size);
If you instead looped over a shuffled list of tiles instead of their positions, you wouldnt have to do a lookup at all
var availableTiles = Display.Children.OfType<Tile>().OrderBy(x => random.Next()).ToList();
foreach (var existingTile in availableTiles)
{
...
}
var availableTiles = Display.Children.OfType<Tile>().OrderBy(x => random.Next()).ToList();
foreach (var existingTile in availableTiles)
{
...
}
Not what I expected but sure. Try the Task.Run thing anyway, the window should pop up almost immediately while GenerateWorld runs in the background
Mąż Zuzanny Harmider Szczęście
the window didnt load and returned errors
Danny May
Danny May2mo ago
Youll have to change all your Display.Children.Add to call AddTilesToUIThread though lines 139, 140 & 147 in the scriptbin you sent
Mąż Zuzanny Harmider Szczęście
right i changed:
- var existingTile = Display.Children.OfType<Tile>().FirstOrDefault(t => t.X == col * Tile.Size && t.Y == row * Tile.Size);
+ if (Display.Children.OfType<DestructableTile>().Any(r => r.OverlapsWith(bgTile))) return;
- var existingTile = Display.Children.OfType<Tile>().FirstOrDefault(t => t.X == col * Tile.Size && t.Y == row * Tile.Size);
+ if (Display.Children.OfType<DestructableTile>().Any(r => r.OverlapsWith(bgTile))) return;
Danny May
Danny May2mo ago
The .Any will still loop over the children until it matches, so you have a loop within a loop of the same collection making it O(n^2) rather than O(n) but again, at 2k elements total the issue is likely somewhere else
Mąż Zuzanny Harmider Szczęście
so how do i optimize it
FusedQyou
FusedQyou2mo ago
Can you share the results and also where specifically bottlenecks are?
Mąż Zuzanny Harmider Szczęście
no bottlenecks rn i think, current ms is Took 1122ms to generate world
Mąż Zuzanny Harmider Szczęście
current version of code after some changes: https://scriptbin.xyz/zuqubejewe.cs
Scriptbin - Share Your Code Easily
Use Scriptbin to share your code with others quickly and easily.
Danny May
Danny May2mo ago
1kms to generate 2k tiles feels quite high to me. If youre ok with that though then is there still an issue you need help with?
Mąż Zuzanny Harmider Szczęście
maybe the bottleneck is image generation?
Danny May
Danny May2mo ago
Wont know without benchmarking.
Mąż Zuzanny Harmider Szczęście
how to make it faster?
FusedQyou
FusedQyou2mo ago
Try putting stopwatch intervals in the code to see when it jumps quite high The total is less relevant here
Danny May
Danny May2mo ago
Like I said, loop over the tiles directly rather than their positions
var availableTiles = Display.Children.OfType<Tile>().OrderBy(x => random.Next()).ToList();
foreach (var existingTile in availableTiles)
{
...
}
var availableTiles = Display.Children.OfType<Tile>().OrderBy(x => random.Next()).ToList();
foreach (var existingTile in availableTiles)
{
...
}
But you need to use the stopwatch to work out what is taking the longest, I dont see anything off the top of my head that jumps out as an issue best way to solve performance issues is to measure, measure, measure
Mąż Zuzanny Harmider Szczęście
WorldHeight: 34, WorldWidth: 60 Checkpoint: 922ms Checkpoint: 1221ms Checkpoint: 1228ms Grass tiles generation complete. Checkpoint: 1230ms Checkpoint: 1231ms Checkpoint: 1396ms Tree and mushroom generation complete. Took 1397ms to generate world Looks like it takes a whole 922ms for the function to even get called sure
Danny May
Danny May2mo ago
Put a checkpoint before and after the TileImages= line Might be just how long it takes to load the images
Mąż Zuzanny Harmider Szczęście
Main Checkpoint: 70ms
Main Checkpoint: 71ms
Main Checkpoint: 72ms
GrassA
GrassB
GrassC
MushroomA
TreeBottomA
TreeTopA
BushA
Main Checkpoint: 86ms
Main Checkpoint: 70ms
Main Checkpoint: 71ms
Main Checkpoint: 72ms
GrassA
GrassB
GrassC
MushroomA
TreeBottomA
TreeTopA
BushA
Main Checkpoint: 86ms
public MainWindow()
{
Stopwatch = new Stopwatch();
Stopwatch.Start();
Player = new Player();
InitializeComponent();
Seed = new Random().Next(1,1000);
Debug.WriteLine($"Main Checkpoint: {Stopwatch.ElapsedMilliseconds}ms");
Loaded +=(s,e)=>GenerateWorld();
Debug.WriteLine($"Main Checkpoint: {Stopwatch.ElapsedMilliseconds}ms");
timer = new System.Timers.Timer();
timer.Interval = 2000;
timer.Elapsed += Tick;
timer.Enabled = true;
DataContext = this;
Config = new Config()
{
MaxFlowerCount = 250,
MaxMushroomCount = 50,
MaxTreeCount = 50,
TransformLimit = 30
};
Debug.WriteLine($"Main Checkpoint: {Stopwatch.ElapsedMilliseconds}ms");
TileImages =Enum.GetValues(typeof(TileType)).Cast<TileType>().ToDictionary(type=>type,type=>new BitmapImage(new Uri(Tile.GetImagePath(type),UriKind.RelativeOrAbsolute)));
Debug.WriteLine($"Main Checkpoint: {Stopwatch.ElapsedMilliseconds}ms");
}
public MainWindow()
{
Stopwatch = new Stopwatch();
Stopwatch.Start();
Player = new Player();
InitializeComponent();
Seed = new Random().Next(1,1000);
Debug.WriteLine($"Main Checkpoint: {Stopwatch.ElapsedMilliseconds}ms");
Loaded +=(s,e)=>GenerateWorld();
Debug.WriteLine($"Main Checkpoint: {Stopwatch.ElapsedMilliseconds}ms");
timer = new System.Timers.Timer();
timer.Interval = 2000;
timer.Elapsed += Tick;
timer.Enabled = true;
DataContext = this;
Config = new Config()
{
MaxFlowerCount = 250,
MaxMushroomCount = 50,
MaxTreeCount = 50,
TransformLimit = 30
};
Debug.WriteLine($"Main Checkpoint: {Stopwatch.ElapsedMilliseconds}ms");
TileImages =Enum.GetValues(typeof(TileType)).Cast<TileType>().ToDictionary(type=>type,type=>new BitmapImage(new Uri(Tile.GetImagePath(type),UriKind.RelativeOrAbsolute)));
Debug.WriteLine($"Main Checkpoint: {Stopwatch.ElapsedMilliseconds}ms");
}
Danny May
Danny May2mo ago
Looks like theres a ~1s delay between the component being constructed and the Loaded event being triggered. Im not sure what would be causing that, but it might just be how long it takes for the framework to initialize
Mąż Zuzanny Harmider Szczęście
ig, is there a way to shorten that time?
Danny May
Danny May2mo ago
No clue off the top of my head, but again would be a case of adding logs and timestamps to other areas of your application to see if theres anything else which is getting called during that period. Pretty sure that Loaded is one of the last events to get called, so the framework will have been doing a lot in the meantime. Also seeing as this is called MainWindow, im guessing this is the window that shows up as the first thing after you started the application? Probably not surprising that it takes a second or two to warm up
Mąż Zuzanny Harmider Szczęście
where should i move it then?
Mąż Zuzanny Harmider Szczęście
Started building tiles: 89ms
... (irrelevant data)
Ended building tiles: 1053ms
Took 975ms to generate base
Started building tiles: 89ms
... (irrelevant data)
Ended building tiles: 1053ms
Took 975ms to generate base
Mąż Zuzanny Harmider Szczęście
public MainWindow()
{
Stopwatch = new Stopwatch();
Stopwatch.Start();
Player = new Player();
InitializeComponent();
Seed = new Random().Next(1,1000);
Dispatcher.InvokeAsync(GenerateWorld);
timer = new System.Timers.Timer();
timer.Interval = 2000;
timer.Elapsed += Tick;
timer.Enabled = true;
DataContext = this;
Config = new Config()
{
MaxFlowerCount = 250,
MaxMushroomCount = 50,
MaxTreeCount = 50,
TransformLimit = 30
};
TileImages=Enum.GetValues(typeof(TileType)).Cast<TileType>().ToDictionary(type=>type,type=>new BitmapImage(new Uri(Tile.GetImagePath(type),UriKind.RelativeOrAbsolute)));
}
public MainWindow()
{
Stopwatch = new Stopwatch();
Stopwatch.Start();
Player = new Player();
InitializeComponent();
Seed = new Random().Next(1,1000);
Dispatcher.InvokeAsync(GenerateWorld);
timer = new System.Timers.Timer();
timer.Interval = 2000;
timer.Elapsed += Tick;
timer.Enabled = true;
DataContext = this;
Config = new Config()
{
MaxFlowerCount = 250,
MaxMushroomCount = 50,
MaxTreeCount = 50,
TransformLimit = 30
};
TileImages=Enum.GetValues(typeof(TileType)).Cast<TileType>().ToDictionary(type=>type,type=>new BitmapImage(new Uri(Tile.GetImagePath(type),UriKind.RelativeOrAbsolute)));
}
this is the current way i call the function the function gets called in 70-150ms
Mąż Zuzanny Harmider Szczęście
Hey guys, new errors popping up: https://paste.mod.gg/ljcdcarukqbf/1 sorry if im a burden
BlazeBin - ljcdcarukqbf
A tool for sharing your source code with the world!

Did you find this page helpful?