This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
tutorials:tilemaps_with_compiled_sprites [2013/05/30 11:32] – daniel | tutorials:tilemaps_with_compiled_sprites [2013/05/30 11:40] (current) – daniel | ||
---|---|---|---|
Line 1: | Line 1: | ||
+ | ====== Tilemaps with compiled sprites ====== | ||
+ | |||
+ | {{ http:// | ||
+ | |||
+ | Creating large tile maps is a challenging thing to do with opengles, especially with older IOS hardware. | ||
+ | |||
+ | Advantages: | ||
+ | Fast rendering of very large maps. | ||
+ | |||
+ | Disadvantages: | ||
+ | Compilation is slow (this could be helped by modifying the compilation code). | ||
+ | |||
+ | First we need to build a generic map parser that we can use to parse maps and textures. | ||
+ | |||
+ | <note warning> | ||
+ | This tutorial was written for Sparrow 1.x. In Sparrow 2, you have to replace " | ||
+ | </ | ||
+ | |||
+ | Header: | ||
+ | <code objc> | ||
+ | #import < | ||
+ | #import " | ||
+ | |||
+ | @interface SXTilemap : SPSprite{ | ||
+ | NSMutableData *data; | ||
+ | NSMutableArray *tiles; | ||
+ | |||
+ | int tileWidth; | ||
+ | int tileHeight; | ||
+ | int mapWidth; | ||
+ | int mapHeight; | ||
+ | |||
+ | int viewportWidth; | ||
+ | int viewportHeight; | ||
+ | |||
+ | BOOL hasData; | ||
+ | BOOL hasTileImage; | ||
+ | |||
+ | SPPoint *camera; | ||
+ | SPPoint *oldCam; | ||
+ | |||
+ | BOOL flagForRefresh; | ||
+ | |||
+ | BOOL updating; | ||
+ | } | ||
+ | |||
+ | @property (nonatomic, assign) int tileWidth; | ||
+ | @property (nonatomic, assign) int tileHeight; | ||
+ | @property (nonatomic, assign) int mapWidth; | ||
+ | @property (nonatomic, assign) int mapHeight; | ||
+ | @property (nonatomic, retain) NSMutableData *data; | ||
+ | @property (nonatomic, retain) NSMutableArray *tiles; | ||
+ | @property (nonatomic, retain) SPPoint *camera; | ||
+ | |||
+ | - (id)initWithViewportSize: | ||
+ | - (void)createFromCSV: | ||
+ | - (void)createTilesFromImage: | ||
+ | - (void)changeTileId: | ||
+ | |||
+ | - (BOOL)mapReady; | ||
+ | |||
+ | - (void)updatePosition; | ||
+ | - (void)updateData; | ||
+ | - (void)update: | ||
+ | - (void)startUpdates; | ||
+ | - (void)stopUpdates; | ||
+ | |||
+ | @end | ||
+ | </ | ||
+ | And the implementation: | ||
+ | <code objc> | ||
+ | #import " | ||
+ | |||
+ | // internal methods | ||
+ | @interface SXTilemap() | ||
+ | - (void)initVars; | ||
+ | @end | ||
+ | |||
+ | @implementation SXTilemap | ||
+ | |||
+ | @synthesize mapWidth; | ||
+ | @synthesize mapHeight; | ||
+ | @synthesize tileWidth; | ||
+ | @synthesize tileHeight; | ||
+ | @synthesize data; | ||
+ | @synthesize tiles; | ||
+ | @synthesize camera; | ||
+ | |||
+ | - (id)init{ | ||
+ | self = [super init]; | ||
+ | [self initVars]; | ||
+ | return self; | ||
+ | } | ||
+ | |||
+ | - (void)initVars{ | ||
+ | self.data = [[[NSMutableData alloc] init] autorelease]; | ||
+ | self.camera = [[[SPPoint alloc] initWithX:0 y:0] autorelease]; | ||
+ | self.tiles = [[[NSMutableArray alloc] init] autorelease]; | ||
+ | oldCam = [[SPPoint alloc] initWithX:0 y:0]; | ||
+ | tileWidth = 16; | ||
+ | tileHeight = 16; | ||
+ | viewportWidth = 320; | ||
+ | viewportHeight = 480; | ||
+ | } | ||
+ | |||
+ | - (id)initWithViewportSize: | ||
+ | self = [super init]; | ||
+ | [self initVars]; | ||
+ | tileWidth = tileSize.width; | ||
+ | tileHeight = tileSize.height; | ||
+ | viewportWidth = vpSize.width; | ||
+ | viewportHeight = vpSize.height; | ||
+ | [self createFromCSV: | ||
+ | [self createTilesFromImage: | ||
+ | return self; | ||
+ | } | ||
+ | |||
+ | - (void)createFromCSV: | ||
+ | csvData = [csvData stringByTrimmingCharactersInSet: | ||
+ | NSArray *rows = [csvData componentsSeparatedByString: | ||
+ | mapHeight = [rows count]; | ||
+ | mapWidth = [[[rows objectAtIndex: | ||
+ | |||
+ | int amount = ((mapWidth) * (mapHeight)) + mapWidth; | ||
+ | [data setLength: | ||
+ | |||
+ | int *pixels = [data mutableBytes]; | ||
+ | |||
+ | hasData = YES; | ||
+ | |||
+ | NSArray *colData; | ||
+ | int i = 0; | ||
+ | for(NSString *row in rows){ | ||
+ | colData = [row componentsSeparatedByString: | ||
+ | for(NSString *val in colData){ | ||
+ | pixels[i] = [val intValue]; | ||
+ | i++; | ||
+ | } | ||
+ | } | ||
+ | hasData = YES; | ||
+ | if([self mapReady]){ | ||
+ | [self startUpdates]; | ||
+ | flagForRefresh = YES; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | - (void)createTilesFromImage: | ||
+ | int amount = floor(img.size.width/ | ||
+ | | ||
+ | | ||
+ | } | ||
+ | | ||
+ | | ||
+ | | ||
+ | | ||
+ | | ||
+ | | ||
+ | [rect release]; | ||
+ | | ||
+ | | ||
+ | } | ||
+ | | ||
+ | | ||
+ | if([self mapReady]){ | ||
+ | [self startUpdates]; | ||
+ | flagForRefresh = YES; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | - (BOOL)mapReady{ | ||
+ | return hasData && hasTileImage; | ||
+ | } | ||
+ | |||
+ | - (void)startUpdates{ | ||
+ | if(!updating){ | ||
+ | [self addEventListener: | ||
+ | updating = YES; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | - (void)stopUpdates{ | ||
+ | if(updating){ | ||
+ | [self removeEventListener: | ||
+ | updating = NO; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | - (void)updatePosition{ | ||
+ | // override for scrolling | ||
+ | } | ||
+ | |||
+ | - (void)updateData{ | ||
+ | // override for when overalldata has been changed | ||
+ | } | ||
+ | |||
+ | - (void)update: | ||
+ | if(oldCam.x != camera.x || oldCam.y != camera.y){ | ||
+ | if(!flagForRefresh){ | ||
+ | [self updatePosition]; | ||
+ | oldCam.x = camera.x; | ||
+ | oldCam.y = camera.y; | ||
+ | } | ||
+ | } | ||
+ | if(flagForRefresh){ | ||
+ | [self updateData]; | ||
+ | flagForRefresh = NO; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | - (void)changeTileId: | ||
+ | int *pixels = [data mutableBytes]; | ||
+ | int tileIndex = (yPos * mapWidth) + xPos; | ||
+ | pixels[tileIndex] = tileId; | ||
+ | } | ||
+ | |||
+ | - (void)dealloc{ | ||
+ | self.camera = nil; | ||
+ | self.tiles = nil; | ||
+ | self.data = nil; | ||
+ | [oldCam release]; | ||
+ | [self stopUpdates]; | ||
+ | [super dealloc]; | ||
+ | } | ||
+ | |||
+ | @end | ||
+ | </ | ||
+ | |||
+ | |||
+ | This will leave you with a generic system for doing you own rendering code. It is based on using an image strip that contains the tiles in a row. So a map that contains 2 16x16 tiles would have an image with the dimensions 32x16. | ||
+ | |||
+ | {{http:// | ||
+ | |||
+ | The map data is then parse off a csv file and flattened out into a standard c buffer for speed. | ||
+ | < | ||
+ | 1, | ||
+ | 1, | ||
+ | 1, | ||
+ | 1, | ||
+ | 1, | ||
+ | 1, | ||
+ | 1, | ||
+ | 1, | ||
+ | 1, | ||
+ | 1, | ||
+ | 1, | ||
+ | 1, | ||
+ | 1, | ||
+ | 1, | ||
+ | 1, | ||
+ | </ | ||
+ | |||
+ | You access the index of the tile you want like so: | ||
+ | <code objc> | ||
+ | int *pixels = [data mutableBytes]; | ||
+ | int tileIndex = (yPos * mapWidth) + xPos; // here is the position in the flat array | ||
+ | pixels[tileIndex] = tileId; | ||
+ | </ | ||
+ | |||
+ | We keep a camera variable so that we are still able to position the tile map as a normal sparrow sprite on the stage. | ||
+ | |||
+ | We also leave empty methods for updatePosition and updateData. | ||
+ | |||
+ | Now that we have a nice generic setup we will build another class to actually do the rendering. | ||
+ | |||
+ | Header: | ||
+ | <code objc> | ||
+ | #import < | ||
+ | #import " | ||
+ | #import " | ||
+ | #import " | ||
+ | |||
+ | #define SX_COMPILED_TILEMAP_RENDER_COMPLETE_EVENT @" | ||
+ | |||
+ | @interface SXCompiledTileMap : SXTilemap { | ||
+ | int quadrantsX; | ||
+ | int quadrantsY; | ||
+ | int quadrantWidth; | ||
+ | int quadrantHeight; | ||
+ | NSMutableDictionary *quadrants; | ||
+ | NSMutableArray *imagePool; | ||
+ | |||
+ | BOOL hasCompiled; | ||
+ | |||
+ | int indexX; | ||
+ | int indexY; | ||
+ | } | ||
+ | |||
+ | @property(nonatomic, | ||
+ | |||
+ | - (void)drawQuadrantAtX: | ||
+ | - (void)showRelavantQuadrants; | ||
+ | - (void)showQuad: | ||
+ | - (void)prerenderTick: | ||
+ | |||
+ | @end | ||
+ | </ | ||
+ | |||
+ | And the implementation: | ||
+ | <code objc> | ||
+ | #import " | ||
+ | |||
+ | @implementation SXCompiledTileMap | ||
+ | |||
+ | @synthesize hasCompiled; | ||
+ | |||
+ | - (id)initWithViewportSize: | ||
+ | self = [super initWithViewportSize: | ||
+ | hasCompiled = NO; | ||
+ | quadrants = [[NSMutableDictionary alloc] initWithCapacity: | ||
+ | return self; | ||
+ | } | ||
+ | |||
+ | - (void)updateData{ | ||
+ | [super updateData]; | ||
+ | |||
+ | hasCompiled = NO; | ||
+ | |||
+ | quadrantsX = ceil(mapWidth/ | ||
+ | quadrantsY = ceil(mapHeight/ | ||
+ | quadrantWidth = floor(viewportWidth/ | ||
+ | quadrantHeight = floor(viewportHeight/ | ||
+ | |||
+ | if(!imagePool){ | ||
+ | imagePool = [[NSMutableArray alloc] initWithCapacity: | ||
+ | } | ||
+ | |||
+ | int yy, xx; | ||
+ | |||
+ | indexX = 0; | ||
+ | indexY = 0; | ||
+ | |||
+ | SPImage *img; | ||
+ | for(yy=0; yy< | ||
+ | for(xx=0; xx< | ||
+ | img = [[SPImage alloc] initWithWidth: | ||
+ | [imagePool addObject: | ||
+ | } | ||
+ | } | ||
+ | [self addEventListener: | ||
+ | } | ||
+ | |||
+ | - (void)prerenderTick: | ||
+ | [self drawQuadrantAtX: | ||
+ | if(indexX < quadrantsX){ | ||
+ | indexX += 1; | ||
+ | }else{ | ||
+ | indexX = 0; | ||
+ | indexY += 1; | ||
+ | } | ||
+ | if(indexX >= quadrantsX && indexY >= quadrantsY){ | ||
+ | hasCompiled = YES; | ||
+ | [self dispatchEvent: | ||
+ | [self removeEventListener: | ||
+ | [self showRelavantQuadrants]; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | - (void)updatePosition{ | ||
+ | [super updatePosition]; | ||
+ | if(!hasCompiled) return; | ||
+ | [self showRelavantQuadrants]; | ||
+ | } | ||
+ | |||
+ | - (void)showRelavantQuadrants{ | ||
+ | int quadX = floor(camera.x/ | ||
+ | int quadY = floor(camera.y/ | ||
+ | |||
+ | NSString *quadId0 = [NSString stringWithFormat: | ||
+ | NSString *quadId1 = [NSString stringWithFormat: | ||
+ | NSString *quadId2 = [NSString stringWithFormat: | ||
+ | NSString *quadId3 = [NSString stringWithFormat: | ||
+ | |||
+ | SPCompiledSprite *spr; | ||
+ | |||
+ | |||
+ | float xPos; | ||
+ | float yPos; | ||
+ | |||
+ | while([self numChildren] > 0){ | ||
+ | [self removeChildAtIndex: | ||
+ | } | ||
+ | |||
+ | spr = [quadrants objectForKey: | ||
+ | xPos = ((quadX * quadrantWidth) * tileWidth) - camera.x; | ||
+ | yPos = ((quadY * quadrantHeight) * tileHeight) - camera.y; | ||
+ | [self showQuad: | ||
+ | |||
+ | spr = [quadrants objectForKey: | ||
+ | xPos += (quadrantWidth * tileWidth); | ||
+ | [self showQuad: | ||
+ | |||
+ | spr = [quadrants objectForKey: | ||
+ | yPos += (quadrantHeight * tileHeight); | ||
+ | [self showQuad: | ||
+ | |||
+ | spr = [quadrants objectForKey: | ||
+ | xPos -= (quadrantWidth * tileWidth); | ||
+ | [self showQuad: | ||
+ | |||
+ | } | ||
+ | |||
+ | - (void)showQuad: | ||
+ | if(quad){ | ||
+ | quad.x = xPos; | ||
+ | quad.y = yPos; | ||
+ | [self addChild: | ||
+ | } | ||
+ | } | ||
+ | |||
+ | - (void)drawQuadrantAtX: | ||
+ | int startX, startY, endX, endY; | ||
+ | |||
+ | startX = xPos * quadrantWidth; | ||
+ | startY = yPos * quadrantHeight; | ||
+ | endX = startX + quadrantWidth; | ||
+ | endY = startY + quadrantHeight; | ||
+ | |||
+ | if(startX > mapWidth) return; | ||
+ | if(startY > mapHeight) return; | ||
+ | if(endX > mapWidth) endX = mapWidth; | ||
+ | if(endY > mapHeight) endY = mapHeight; | ||
+ | |||
+ | SPCompiledSprite *quad = [[[SPCompiledSprite alloc] init] autorelease]; | ||
+ | NSString *quadId = [NSString stringWithFormat: | ||
+ | [quadrants setObject: | ||
+ | |||
+ | int tileIndex; | ||
+ | int *pixels = [data mutableBytes]; | ||
+ | |||
+ | float xOffset = 0; | ||
+ | float yOffset = 0; | ||
+ | |||
+ | int i = 0; | ||
+ | |||
+ | for(int yy=startY; yy<endY; yy++){ | ||
+ | for(int xx=startX; xx<endX; xx++){ | ||
+ | tileIndex = (yy * mapWidth) + xx; | ||
+ | SPImage *img = [imagePool objectAtIndex: | ||
+ | img.texture = [[tiles objectAtIndex: | ||
+ | img.x = xOffset; | ||
+ | img.y = yOffset; | ||
+ | [quad addChild: | ||
+ | xOffset += tileWidth; | ||
+ | i ++; | ||
+ | } | ||
+ | xOffset = 0; | ||
+ | yOffset += tileHeight; | ||
+ | } | ||
+ | [quad compile]; | ||
+ | |||
+ | while([quad numChildren] > 0){ | ||
+ | [quad removeChildAtIndex: | ||
+ | } | ||
+ | } | ||
+ | |||
+ | - (void)dealloc{ | ||
+ | if(!hasCompiled && [self hasEventListenerForType: | ||
+ | [self removeEventListener: | ||
+ | } | ||
+ | [quadrants removeAllObjects]; | ||
+ | [quadrants release]; | ||
+ | while([self numChildren] > 0) { | ||
+ | [self removeChildAtIndex: | ||
+ | } | ||
+ | [super dealloc]; | ||
+ | } | ||
+ | |||
+ | @end | ||
+ | </ | ||
+ | |||
+ | What this class does is break the map up into distinct quadrants, then converts all those quadrants to compiled sprites. | ||
+ | |||
+ | Hopefully this helps people at least get something into their game to try out. I have put sample files [[https:// | ||
+ | |||
+ | A note on pre-compilation: | ||
+ | Pre-compilation could be greatly sped up. Right now SPCompiledSprite does a lot of malloc/ | ||
+ | |||