making seamless repeating backgrounds using photoshop and cocos2d iPhone

May 8, 2009

I received a lot of inquiries about how the seamless repeating backgrounds were made for my Ninja Jumper iPhone game. So I figured it would be easier to share it here instead of typing a whole bunch of emails…

Ninja Jumper is built on the ever so awesome cocos2d iPhone game engine. My goal was to create beautiful painterly scrolling backgrounds that are very fast to draw. I had to add a new texture drawing function to achieve that, but let’s first look at how to create the art.

Creating repeating backgrounds (or any repeating texture) is very easy in Photoshop. The trick is to use the offset filter for getting rid of the seams.

To demonstrate the technique, I made a very simple background, 512 pixels wide, 256 pixels tall. When creating the background, make sure that the lines on the right edge somewhat match the lines on the left (where the red arrows point.) Of course you don’t need to make it perfect, we’ll fix that in the next step:

initial background sketch

initial background sketch

Now go to filter > other > offset:

filters > other > offset

filter > other > offset

The offset filter works on layers. If you have multiple layers, you should either merge them or offset all of them. Make sure that the wrap around option is active, and offset the image horizontally by half its width (in this case the image is 512 wide, so offset it 256 pixels):

offset half width

offset half width

The filter “scrolls” the image so that what used to be on the edges is now in the center. You just need to fix the seams in the middle (turquoise circle) and make sure that you don’t modify anything around the edges now (red areas):

fix seams in middle

fix seams in middle

Here’s the image with the seams fixed. The rubber stamp tool will be one of your best friends to do this:

fixed seams

fixed seams

Now offset the layer by half its width again to get the original image back:

original image, seams fixed

original image, seams fixed

Of course fixing the seams on a more complicated image will take a bit longer. Here’s an actual background from the Ninja Jumper game:

Ninja Jumper background

Ninja Jumper background

The background in Ninja Jumper is made up of several layers. The layers scroll at different speeds to give the illusion of depth. Each layer was created using the process described above and saved as separate images.

I found that PVR images draw a lot faster and are much more memory efficient, so all the textures are converted to 4 bit per pixel PVRs. (2 bits per pixel images are completely useless because of the compression artifacts.)

Let’s look at the drawing code now. This was the first draw method that I tried in Texture2D, but was slow as a 200 lbs chihuahua:

fat chihuahua

fat chihuahua

The slow method is:

- (void) drawInRect:(CGRect)rect texOffset:(CGPoint)texOffset stretch:(bool)stretch {
	float xOffset = texOffset.x / rect.size.width;
	float yOffset = texOffset.y / rect.size.height;
	float maxT = _maxT;
	float maxS = _maxS;

	if (!stretch) {
		maxT = rect.size.height / _size.height;
		maxS = rect.size.width / _size.width;
	}

	GLfloat  coordinates[] = {
		0.0f + xOffset,         maxT + yOffset,
		maxS + xOffset,         maxT + yOffset,
		0.0f + xOffset,         0.0f + yOffset,
		maxS + xOffset,         0.0f + yOffset
	};

	GLfloat	vertices[] = {	rect.origin.x,							rect.origin.y,
							rect.origin.x + rect.size.width,		rect.origin.y,
							rect.origin.x,							rect.origin.y + rect.size.height,
							rect.origin.x + rect.size.width,		rect.origin.y + rect.size.height};

	glBindTexture(GL_TEXTURE_2D, _name);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
	glVertexPointer(2, GL_FLOAT, 0, vertices);
	glTexCoordPointer(2, GL_FLOAT, 0, coordinates);
	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

}

It’s also unnecessarily complicated for our purpose, we really want to draw at a point and don’t care about stretching. Here’s the method I created for drawing a scrolling texture at a point:

- (void) drawAtPoint:(CGPoint)point texOffset:(CGPoint)texOffset
{   
    float xOffset = texOffset.x / _size.width;
    float yOffset = texOffset.y / _size.height;

    GLfloat  coordinates[] = {
        0.0f + xOffset,         _maxT + yOffset,
        _maxS + xOffset,         _maxT + yOffset,
        0.0f + xOffset,         0.0f + yOffset,
        _maxS + xOffset,         0.0f + yOffset
    };

    GLfloat    vertices[] = {    point.x,                point.y,                           
                            point.x + _size.width,    point.y,                           
                            point.x,                point.y + _size.height,       
                            point.x + _size.width,    point.y + _size.height};

    glBindTexture(GL_TEXTURE_2D, _name);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glVertexPointer(2, GL_FLOAT, 0, vertices);
    glTexCoordPointer(2, GL_FLOAT, 0, coordinates);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}

(Wouldn’t syntax highlighting be nice? Oh yeah. I tried several plugins but none seemed to work correctly. So sorry. I’ll try again some day.)

This method is super fast. Call it with the point you want to draw the texture at and the offset (the offset is also a point to allow vertical scrolling as well.)

For example to draw a texture at the middle of the screen, offset horizonally:

Texture2D *scrollingTexture;
float horizontalOffset;
[scrollingTexture drawAtPoint:CGPointZero texOffset:CGPointMake(horizontalOffset, 0)];

In Ninja Jumper I subclass Sprite and use this drawing method:

- (void) draw {

	glEnableClientState( GL_VERTEX_ARRAY);
	glEnableClientState( GL_TEXTURE_COORD_ARRAY );

	glEnable( GL_TEXTURE_2D);

	glColor4ub( r, g, b, opacity);

	[texture drawAtPoint:CGPointZero texOffset:CGPointMake(texOffset.x, texOffset.y)];

	glColor4ub( 255, 255, 255, 255);

	glDisable( GL_TEXTURE_2D);
	glDisableClientState(GL_VERTEX_ARRAY );
	glDisableClientState( GL_TEXTURE_COORD_ARRAY );

}

Here’s the method that does the scrolling calculation:

-(void)scroll:(float)offset {
	texOffset = ccpAdd(texOffset, ccpMult(ccpForAngle(CC_DEGREES_TO_RADIAN(scrollAngle)), offset));    // scrollAngle is the scrolling direction in degrees
	CGSize contentSize = texture_.contentSize;
	if (texOffset.x > contentSize.width) texOffset.x -= contentSize.width;
	if (texOffset.y > contentSize.height) texOffset.y -= contentSize.height;
	if (texOffset.x < 0) texOffset.x += contentSize.width;
	if (texOffset.y < 0) texOffset.y += contentSize.height;
}

Where drawSize = texture.contentSize and scollAngle is the angle of movement in degrees.

Happy scrolling! Please check out Ninja Jumper

19 Responses to “making seamless repeating backgrounds using photoshop and cocos2d iPhone”

  1. Where and how do you implement this code? Do you replace the current methods within Texture2D? if so, in both the .m and .h files?

  2. @Exxx: I added the extra drawing method to the Texture2D class. It was the easiest solution, otherwise you have to subclass a whole bunch of classes. (The method is added to .m and the signature to .h.)

    I created a subclass of Sprite and overrode the draw method to call - (void) drawAtPoint:(CGPoint)point texOffset:(CGPoint)texOffset instead of - (void) drawAtPoint:(CGPoint)point.

  3. [...] Making Seamless Repeating Backgrounds is a helpful tutorial. [...]

  4. What template are you using to do this. Open GL ES or making a cocos2d one? Also where are you putting all this code?

  5. @Austin: This goes into Texture2D.m. I think there has been some improvements to the Texture2D class so you could actually subclass it instead of modifying it directly, but that way you won’t be able to use TextureMgr.

  6. You know how we end up doing it?

    So that there’s no blinking gap between your background

    Sprites you could make one end with about 20px of gradient to transparency and put one over another a little bit

    It solved it for us - we use simply 3 sprites and move the one that gos off screen to the top and again…

  7. Hi,
    I’ve literally spent days trying to get this code to work, but I’m completely failing. Where do you initialize the texture? and what exactly is the texture offset?

    Thanks!
    Ryan

  8. @Ryan: I subclass the Sprite class and use the spriteWithFile: method to initialize it. That method loads the texture.

  9. Thanks for the reply lajos. I’m just having serious trouble piecing everything togther. Were is the scroll:offset method called?

    Thanks for the help in advance :) Brilliant blog you have here by the way. Renamed my mac using your other post ;)

  10. I call the scroll method from the scheduled selector of the game (most likely called step: if you are using cocos2d examples).

  11. Is there a way to eliminate the gaps in the background? There is always a small gap scrolling horizontally, and a larger one scrolling vertically.

  12. @rpeck: maybe it’s caused by the texture filtering setting?

  13. I tried this code in cocos2d .99RC and I have a very strange behaviour: The background overlaps any other normal sprites in the screen. It will hide everything else that is in the screen. I tried to play a litle bit with the z: of both the sprites and the background but nothing. Only the scrolling background appears. Any ideas on how to overcome this?

  14. @MMG: z order should fix your problems. Are you using AtlasSprites? If you are, make sure you set the z order of the manager, not the individual sprites.

  15. @lajos - Thanks for the tutorial, its been very helpful!

    @MMG - I had the same problem with the rest of the sprites not showing up. I created a separate .h and .m file for the sprite(s) with a similar draw method used for the background sprite. That worked for me anyway :)

    For anyone else that is starting out like I am. Cocos2D .99RC switched the class names from Texture2D to CCTexture2d, etc. The release notes are helpful:

    http://www.cocos2d-iphone.org/wiki/doku.php/release_notes:0_99_0_rc

  16. @lajos - All my sprites work fine, and the background scrolls seamlessly. I am trying to add text on the screen for the score. As soon as I added the OpenGL stuff, the text no longer appears (even the framerate stopped appearing). Any hints are greatly appreciated.

    Sweet app btw ;)

  17. @finklebottom: Not sure why the text would disappear. I would check the z-orders, make sure the background id not drawing on top.

  18. I’m also getting the same thing. When I add the OpenGL stuff CCLabels won’t show, even though all the functionality still works.

  19. @jikky

    I think its got to be something with the 0.99 update. The GL states changed for CCSprite or CCTexture. I still haven’t figured it out, but have posted a question on the forum:
    http://www.cocos2d-iphone.org/forum/topic/4916#post-29338

    I’ll post here when I find an answer :)

Leave a Reply