Handling a 2D procedural world

Started by
16 comments, last by Mekuri 11 years, 6 months ago

Naah, the easiest way should be to load the whole chunk at once. Just make sure you don't use too large chunks and load them in the background to not freeze the game, then it should be fine smile.png Streaming sometimes has a tendency to become a bit complicated if you're not careful though, so if you're just starting out you might want to keep it simple and, if possible, use areas instead, showing a loading screen as the player walks from one area to the next.

Good luck!

Thanks a lot. Tomorrow I will finally have time to dive into all this so I am looking forward to it. About using areas, I don't consider that an option, that will not fit into the concept, in my opinion. But thanks a lot for all the input, I really appreciate it :D

Check out the game I am making here - http://www.youtube.com/user/NasarethMekuri

Advertisement
So I've finally had some time to play around with this, and I've managed to get it to work... But only on very small worlds/file sizes. Now I've tried two ways of doing it, with exactly the same results. The first way is the simple way I described earlier, I did however mark a few objects as nonserializable to save additional space.
The other solution I've tried is the one posted here: http://www.codeproje...ization-using-C.
I got the exact same result with this one when deserializing- The game stays frozen for 5-10 minutes, and then throws an out of memory exception. I then tried to reduce the world to 1024*512. This actually fixed it. I still had a load time on about 1.5 minutes and a save time of maybe 15 second, but it seems to work flawlessly after that.
Just prior to posting this I then figured that it might work by doing it the "simple" way. So I commented out all the code related to the CodeProject article, and tried again, and exactly the same result.
Just now I tried with a world size of 128*128. Here the save time is like .3 seconds, and the load time is maybe around 1 second. This is close enough to be acceptable, and it might probably work if I chunk it down to 128*128 chunk pieces, but it feels wrong doing it like this, since I got the feeling I am doing something wrong.
I did get a suggestion to try and do a custom serialization, but as far as I understand, that's exactly what I am doing if I follow the CodeProject article?
I do have a few Tile classes that inherrits from the base Tile class, to handle objects that changes state, and objects that take up more than 1 tile in the world grid, but I did remember to follow these rules(when using the CodeProject article) - http://msdn.microsof...326(VS.80).aspx so I don't think that is the cause.
Below is the code I use for the CodeProject solution:


/// <summary>
/// Constructer for deserialization
/// </summary>
public Tile(SerializationInfo info, StreamingContext ctxt)
{
Type = (TileType)info.GetValue("TileType", typeof(TileType));
BackgroundType = (TileType)info.GetValue("BackgroundType", typeof(TileType));
Orientation = (TileOrientation)info.GetValue("Orientation", typeof(TileOrientation));
_variant = (byte)info.GetValue("Variant", typeof(byte));
_lightCalculated = false;
if (Type == TileType.None)
{
if (BackgroundType == TileType.None)
_ambientLightLevel = Light.AmbientLight;
else
_ambientLightLevel = 0;
_directLightLevel = 0;
_flickerTimer = 0;
}
else if (Type == TileType.Torch)
{
_directLightLevel = 255;
_ambientLightLevel = 0;
_flickerTimer = 1;
}
else
{
_directLightLevel = 0;
_ambientLightLevel = 0;
_flickerTimer = 0;
}
}
public virtual void GetObjectData(SerializationInfo info, StreamingContext ctxt)
{
info.AddValue("TileType", Type);
info.AddValue("BackgroundType", BackgroundType);
info.AddValue("Orientation", Orientation);
info.AddValue("Variant", _variant);
}


Notice that GetObjectData is virtual. The child classes overrides it, add their own values in the same manner, and then calls the base method. I also remember to make the Tile class inherrit from ISerializable. I then serialize exactly the same way as before, using the methods I posted earlier.

So to sum it up:

As I understand it, when a class inherrits from ISerializable and has the GetObjectData method and the deserialization constructer, both should automatically be called when you are both de- and serializing. This seems very odd to me, but is this true?
And secondly is this what is meant by making custom serialization, or am I completely off?

I'm off to read up on all this, and I'll post here if I happen to answer some of my own questions.

Thanks for reading smile.png

Check out the game I am making here - http://www.youtube.com/user/NasarethMekuri

I did happen to answer a few of my own questions. I now know that my code is true in regards to the Serialization while inherriting from ISerializable. So I sat down and started to think out how I would handle dividing my world up into chunks, and I figured I'd check out how Terraria does this.
I tried to make a world of the largest terraria size, which, to my suprise "only" is around 8400*2400, so I figured I'd begin to test out doing that size. I also realized that Terraria increases drastically in memory usage when you load a large map. It was just above 900MBs (over 400 prior to loading). Which tells me it keeps the world in memory. When saving the game the world file is a bit over 90MBs. In my game currently, my memory usage is only about 200-250MB prior to loading, and just below 900MB when the world is loaded. This is all well and good, My game (as it is right now) consumes slightly less, but my world consumes about 200MB more. I can live with that for now. The problem is then when I try to save the file via selective serialization (I only chose TileType,BackgroundType,Orientation and variant, all of these should be the same as a byte each.) it takes several minutes... It even gave me an out of memory exception on my first try. So instead of serializing the entire world in one go, I made 2 for loops to handle it, one tile at a time. I end up with a world file on no less than 7.4GB, I do understand that using the out of the box serialization that .NET brings, is very ineffective, but this is just rediculous.
If I can dramatically decrease save/load times and file size, I am close to solving this problem- Can anyone help me out here? And maybe even explain why I experience this huge file?

I read something about that a lot of extra data was added to the stream this way. if this happens for every single tile... that's bad tongue.png

Check out the game I am making here - http://www.youtube.com/user/NasarethMekuri

I might have missed if someone else mentioned this, but you might want to take a look at streaming (only load what you need now plus a little more, then let a background thread load more when needed) and/or compression (if you cannot minimize the tile data further), considering the amount of data you are handling. Since you only load one file sequentially, you do not need to worry about (mechanical head) seeking.
Perhaps, depending on the Type/Background/Orientation variables (whether or not they differ a lot for each tile), you can make these objects flyweights, thereby only having one to load 1 object for each different type/background/orientation and in the file reference them by some identifier (ID/GUID)?
Other than that, use as small data types as possible to describe what you need, which I see you already are in Tile, using bytes/shorts. For instance, an int X can often be reduced to short/byte X if any two X does not differ by more than 2^15/2^7 (or unsigned 2^16/2^8). For instance, if your X's are all in the range [545;668], the difference is at most 668-545=123 (making it a perfect fit for a byte), so serialized is byte bX = (int iX - 545) and deserialized is int iX = (byte bX + 545). Warning to readers, in case I just created arithmetic overflow of converted data types (do not know C# well enough to catch these).

I might have missed if someone else mentioned this, but you might want to take a look at streaming (only load what you need now plus a little more, then let a background thread load more when needed) and/or compression (if you cannot minimize the tile data further), considering the amount of data you are handling. Since you only load one file sequentially, you do not need to worry about (mechanical head) seeking.
Perhaps, depending on the Type/Background/Orientation variables (whether or not they differ a lot for each tile), you can make these objects flyweights, thereby only having one to load 1 object for each different type/background/orientation and in the file reference them by some identifier (ID/GUID)?
Other than that, use as small data types as possible to describe what you need, which I see you already are in Tile, using bytes/shorts. For instance, an int X can often be reduced to short/byte X if any two X does not differ by more than 2^15/2^7 (or unsigned 2^16/2^8). For instance, if your X's are all in the range [545;668], the difference is at most 668-545=123 (making it a perfect fit for a byte), so serialized is byte bX = (int iX - 545) and deserialized is int iX = (byte bX + 545). Warning to readers, in case I just created arithmetic overflow of converted data types (do not know C# well enough to catch these).


Those are good ideas, I'll add them to my list of possible optimizations, thanks a lot :) I'll go read up on streaming and see if it is usable in my situation.

Check out the game I am making here - http://www.youtube.com/user/NasarethMekuri

Finally I suceeded.
I was too focused on using the serialization that was build into the .net framework that I misunderstood some of the advice I got earlier.
So I spend some time away from the computer, and when I came back I reread the posts here and decided to try to do it manually by using BinaryWriter and BinaryReader. 30 minutes later (and on my first test I might add) I successfully saved my world (8400*2400 tiles). With a save time on less than 2 seconds, and a load time on about the same. The file size is also down to less than 78MB.
Basically what I do is I make two for loops that starts out at 0, 0 (x, y) and take all the values that aren't recalculated at runtime every frame, from each tile, and write them to a file stream. When I load them back in, I use two for loops again and make sure I load them in the exact same order I wrote them.

To make sure that everything worked as intended, I decided to build a small.. monument for the occasion of being able to save and load my world, I took a small screenshot. This screenshot was taken after I've sucessfully saved and then reloaded the world:

2a9xz6b.png
Although I still need to pollish the save/load algorithm, I consider this step successful. As I posted earlier I considered this step to be one of the harder ones, so I am very grateful for all the help I got from you guys- So thanks a lot :-)

Check out the game I am making here - http://www.youtube.com/user/NasarethMekuri

You could implement some very simple RLE to get rid of all that air in the file. It wouldnt be that hard to add, instead of just copying the stuff to the file one by one, you count how many sequential blocks of the same type there are and then save it as BlockCount,BlockType, and when you read it you write the next BlockCount cells to contain a block of BlockType type.

o3o


You could implement some very simple RLE to get rid of all that air in the file. It wouldnt be that hard to add, instead of just copying the stuff to the file one by one, you count how many sequential blocks of the same type there are and then save it as BlockCount,BlockType, and when you read it you write the next BlockCount cells to contain a block of BlockType type.

That is actually a good and simple idea. I'll keep it in mind when I start optimizing further :)

Check out the game I am making here - http://www.youtube.com/user/NasarethMekuri

This topic is closed to new replies.

Advertisement