This question is all about file size reduction in an infinite world 2D mining game.
I finally added code to my game to save and load distant chunks instead of keeping all of them in memory. Up to this point, I had only used the default NSCoding Apple library to save and load files from disk. In my initial testing, each Chunk
consisted of 900 Blocks, and when saved to disk the file was 160kb
. This would mean that worlds with just a few million blocks could be bigger than a gigabyte.
I tried a number of different methods to decrease the size of the files. I found some code on Stack Overflow to implement some gzip compression, and that reduced the size down to about 74kb
. This was good, but not nearly good enough. The next thing I tried was to reduce the amount of information stored in each block. I removed all the NSStrings
, and derived those from other values instead of saving them. I converted all the CGPoints
to ints
, and saved those instead. This got the file size down to about 52kb
. This was almost small enough to work, but I knew I could do better.
The Blocks
themselves only had a very small amount of information stored in them. I decided to manually compose the bytes needed to initialize a block, and then manually parse those bytes when the file was loaded.
I succeeded in writing some code to do this, and it got the file size down to 4kb
. Each Block
is 28 bytes
, so this is much closer to a reasonable number. I should note that when I tried without the gzip compression, the file size rose to 25kb
. I decided to use the Apple libraries to do this, so I am reading and writing to NSData
. I believe there are better ways to achieve this, and I would love to hear about them.
DWBlock.m
#pragma mark - Custom encode and decode
-(instancetype) initWithData:(NSData *)data {
self = [super init];
if (self) {
int theBytesRecovered[7];
[data getBytes:theBytesRecovered];
_type = (int)theBytesRecovered[0];
_damageLevel = (int)theBytesRecovered[1];
_chunkSize = (int)theBytesRecovered[2];
int posX = (int)theBytesRecovered[3];
int posY = (int)theBytesRecovered[4];
int chunkPosX = (int)theBytesRecovered[5];
int chunkPosY = (int)theBytesRecovered[6];
_position = CGPointMake(posX, posY);
_chunkPosition = CGPointMake(chunkPosX, chunkPosY);
_blockName = NSStringFromCGPoint(_position);
_chunkName = NSStringFromCGPoint(_chunkPosition);
_calculatedPosition = CGPointMake((_chunkPosition.x * _chunkSize * kBlockSpriteWidth) + (_position.x * kBlockSpriteWidth), (_chunkPosition.y * _chunkSize * kBlockSpriteWidth) + (_position.y * kBlockSpriteWidth));
}
return self;
}
-(NSData *) customEncode {
int type = _type;
int damageLevel = _damageLevel;
int chunkSize = _chunkSize;
int posX = _position.x;
int posY = _position.y;
int chunkPosX = _chunkPosition.x;
int chunkPosY = _chunkPosition.y;
int theBytes[] = {type, damageLevel, chunkSize, posX, posY, chunkPosX, chunkPosY};
return [NSData dataWithBytes:&theBytes length:sizeof(theBytes)];
}
DWChunk.m
#pragma mark - Custom encode and decode
-(instancetype) initWithData:(NSData *)data {
self = [super init];
if (self) {
_memConstants = [[DWMemoryConstants alloc]init];
//Iterate through the chunk data
//Grab the correct number of bytes for each block
//Compose a new NSData out of those bytes, and add them to an array
NSMutableArray *blockDatas = [[NSMutableArray alloc]init];
uint8_t *bytePtr = (uint8_t *)[data bytes];
NSInteger totalData = [data length] / sizeof(uint8_t);
for (int i = 0; i < totalData; i+=kChunkSizeInBytes) {
NSMutableData *blockData = [[NSMutableData alloc]init];
for (int j = 0; j < kChunkSizeInBytes; j++) {
[blockData appendBytes:&bytePtr[i + j] length:1];
}
[blockDatas addObject:blockData];
}
//Create the Block objects
_blocks = [[NSMutableDictionary alloc]init];
BOOL firstRun = YES;
for (NSData *data in blockDatas) {
DWBlock *block = [[DWBlock alloc]initWithData:data];
//Get some data about this Chunk from the first block
if (firstRun) {
_position = CGPointFromString(block.chunkName);
firstRun = NO;
}
[_blocks setObject:block forKey:[NSValue valueWithCGPoint:CGPointFromString(block.blockName)]];
}
_chunkName = NSStringFromCGPoint(_position);
_bounds = CGRectMake(_position.x * _memConstants.chunkSize * kBlockSpriteWidth,
_position.y * _memConstants.chunkSize * kBlockSpriteWidth,
_memConstants.chunkSize * kBlockSpriteWidth,
_memConstants.chunkSize * kBlockSpriteWidth);
}
return self;
}
-(NSData *) customEncode {
NSMutableData *data = [[NSMutableData alloc]init];
for (DWBlock *block in _blocks.allValues) {
//It is very important to get the bytes of the NSData and not just the pointer
[data appendBytes:[[block customEncode]bytes] length:[[block customEncode]length]];
}
return data;
}
Chunks are saved like this:
for (DWChunk *chunk in chunks) {
NSString *path = [[self applicationDocumentsPath] stringByAppendingPathComponent:[NSString stringWithFormat:@"worldtest%@", NSStringFromCGPoint(chunk.position)]];
NSData *data = [chunk customEncode];
data = [DWDataZipper gzipDeflate:data];
[data writeToFile:path atomically:YES];
}
And loaded like this:
NSString *path = [[self applicationDocumentsPath] stringByAppendingPathComponent:[NSString stringWithFormat:@"worldtest%@", NSStringFromCGPoint(position.CGPointValue)]];
NSData *chunkData = [NSData dataWithContentsOfFile:path];
if (chunkData) {
chunkData = [DWDataZipper gzipInflate:chunkData];
DWChunk *chunk = [[DWChunk alloc]initWithData:chunkData];
}