Cocos2d-x v3.x Multi Resolution Support

Datetime:2016-08-23 04:31:56          Topic: Cocos2d-X           Share

How Cocos2d-x Multi-resolution support works

Introduction

Despite reading Wikis, experimenting and getting lots of help in the forums, my brain just wasn’t ‘getting’ how multi-resolution support works in cocos. I don’t know why – perhaps I am just getting too old. But the best way I have of learning something is to teach someone else – so this is my explanation – maybe it will help someone else; it will certainly help me!

The Problem

If I develop an App using cocos2d-x I might decide to design it for a resolution of 1024 x 768 – which is a common screen resolution on both tablets, phones and desktop computers of yore.   So I design all of my assets – sprites, background images etc. – to suit this resolution.   But what about all of the other devices out there that run at different resolutions?   There are two things I need to do:

  1. Provide assets for other resolutions
  2. Provide scaling

Providing assets for other resolutions is fine –up to a point. I might provide images for 2048 x 1536, 1024 x 768 and 512 x 394 – and that will give me better quality generally than scaling all of my assets at runtime. But that still doesn’t solve the issue if I want to run my app on a device that has a different resolution to any of those for which I have provided assets.   What I need to do (somehow) is to take my assets and scale them (up or down) so that they fit the screen resolution as well as possible.   I find real examples easiest to deal with – so here’s our issue – I’ll talk through the problems and the solution step by step.

Design size 1024 x 768

The design size is the size I am programming for initially. Knowing the design size, I will (more accurately, cocos2d-x will) be able to scale assets correctly to other resolutions.

Screen size 1024 x 768 and 640 x 512

This is the physical size of the screen I am going to get the application running on. I’m looking at two resolutions because it makes sense to first make sure my screen looks correct at exactly my design resolution, then to try it with other resolutions.

Resolution Policy

Cocos2d-X offers different ‘policies’ which determine exactly how it handles scaling your assets to fit. These are described in the code as follows:

// The entire application is visible in the specified area without trying to preserve the original aspect ratio.
// Distortion can occur, and the application may appear stretched or compressed.
	EXACT_FIT,
// The entire application fills the specified area, without distortion but possibly with some cropping,
// while maintaining the original aspect ratio of the application.
	NO_BORDER,
// The entire application is visible in the specified area without distortion while maintaining the original
// aspect ratio of the application. Borders can appear on two sides of the application.
	SHOW_ALL,
// The application takes the height of the design resolution size and modifies the width of the internal
// canvas so that it fits the aspect ratio of the device
// no distortion will occur however you must make sure your application works on different
// aspect ratios
	FIXED_HEIGHT,
// The application takes the width of the design resolution size and modifies the height of the internal
// canvas so that it fits the aspect ratio of the device
// no distortion will occur however you must make sure your application works on different
	// aspect ratios
	FIXED_WIDTH,

I’m going to choose FIXED_HEIGHT as I want there to be no gaps around the screen, but I don’t mind if the left or right of my background are cut off a little bit. What fixed height policy will do is to scale my images so that the height of the image fills the height of the device – so as long as the width of my image, after this scaling, is equal to or greater than the device width, all will look fine.

The Solution

Cocos2d-X will look at our design size (1024 x 768) and at our screen size. (640 x 512) These two rectangles have different aspect ratios.

1024 / 768 = 1.333

640 / 512 = 1.25

So the shape of each is different – but we definitely don’t want to ‘squish’ our graphics by scaling our assets differently in the X and Y direction.

To scale the design size to the screen size we could do one of two things…

a)    scale so that the height fits exactly in the new sizeb)   scale so that the width fits exactly in the new size.

If we do a) then we would need to scale our design size by the ratio of the heights – i.e. 512 / 768 = 0.66667

If we do b) then we would need to scale our design size by the ratio of the widths – i.e. 640 / 1024 = 0.625

So, if we had an asset that was the size of our full screen (1024 x 768)   Scaling using option a) would give (1024 * 0.66667) x (768 * 0.66667) which = 682 x 512.

Scaling using option b) would give (1024 * 0.625) x (768 * 0.625) which = 640 x 480

You can see that option a) will give us an asset that is wider than our screen, and exactly the same height. So, if we used this, we would fill the screen, but lose the sides of our image.

Using option b) would give us an asset that is the same width as our screen, but smaller than its height – which means we would have borders at the top and bottom of the screen – which we don’t want (which is why we asked for the FIXED_HEIGHT policy!)   So cocos2d-x will use option a) because of the FIXED_HEIGHT policy.

Recall, we have a 1024 x 768 image being displayed on a 640 x 512 screen. The image is scaled to 682 x 512.

We will lose 21 pixels on the left and 21 pixels on the right of our image, but there will be no gaps.

We could try this the ‘other way around’. If we had an image 640 x 512 and went to display it on a 1024 x 768 screen, to scale the height we need to multiply 512 by 1.5 and scale the width by the same amount, 640 x 1.5 = 960.l

So our image would be scaled to 960 x 768 – and we’d have black bars to the left and the right!   Depending on what devices you are trying to support, and memory availability etc. you need to make a decision as to what images you will supply to support those resolutions.   For example, you could replace the 640 x 512 image with one at 683 x 512. Why 683? Because 683 * 1.5 = 1024.5 – so providing that image would scale up to fit a 1024 x 768 device. But remember it would also truncate pixels (43 of them) on the sides.

For the purposes of this article, let’s go back to using a design size of 1024 x 768 displaying on a 640 x 512 device

So far we have just been talking about the whole screen – but what happens when we display a sprite?   Think about scaling a large sprite that we are using as the ground in a game. At our design size of 1024 x 768 we will provide an asset that is 1024 pixels wide (so it covers the whole width of the screen) and is, say, 77 pixels high (which happens to be about 1/10 of the height of the design screen height, which makes my maths easier!

To make It sit at the bottom of the screen at 1024 x 768 we would set the anchorpoint to (0.5,0) and set the position to ((screenwidth / 2) , 0)   Cocos will take our 1024 x 77 sprite and scale it by .66667 giving us a sprite 682 x 51.   This will be positioned mid-screen on the x axis, and at the bottom on the y axis – exactly what we want.

Here’s my grass.png.

It is 1024 x 77 pixels. I’ve surrounded and crossed it with red lines so it is easy to see which areas are shown when it is displayed   Here’s the program running at 1024 x 768.

You can see the entire border.

Running at 640 x 512

The image is central, scaled to the same proportion of the height, but the right and left edges of the grass image are truncated.   Here’s the code that allows us to do this:

Appdelegate.cpp

bool AppDelegate::applicationDidFinishLaunching() {
	// initialize director
	auto director = Director::getInstance();
	auto glview = director->getOpenGLView();
	if(!glview) {
		//glview = GLViewImpl::create("My Game");
		// set the size of the screen we're testing on
		glview = GLViewImpl::createWithRect("MultiRes", Rect(0,0,640,512));
		director->setOpenGLView(glview);
	}
	// Tell cocos our design resolution and our resolution policy
	glview->setDesignResolutionSize(1024, 768, ResolutionPolicy::FIXED_HEIGHT);
	// turn on display FPS
	director->setDisplayStats(true);
	// set FPS. the default value is 1.0/60 if you don't call this
	director->setAnimationInterval(1.0 / 60);
	// create a scene. it's an autorelease object
	auto scene = HelloWorld::createScene();
	// run
	director->runWithScene(scene);
	return true;
}

HelloworldScene.cpp

bool HelloWorld::init()
{
	//////////////////////////////
	// 1. super init first
	if ( !Layer::init() )
	{
		return false;
	}
	Size visibleSize = Director::getInstance()->getVisibleSize();
	Vec2 origin = Director::getInstance()->getVisibleOrigin();
	CCLOG("visibleSize is (%.2f, %.2f)", visibleSize.width, visibleSize.height);
	CCLOG("origin is (%.2f, %.2f)", origin.x, origin.y);
	// add our terrain sprite
	auto sprite = Sprite::create("grass.png");
	sprite->setAnchorPoint(Vec2(0.5,0));
	// position the sprite on the center of the screen
	sprite->setPosition(Vec2(visibleSize.width/2 + origin.x, 0 + origin.y));
	// add the sprite as a child to this layer
	this->addChild(sprite, 0);
	return true;
}

In AppDelegate.cpp we make the changes in bold – this sets up the window on the Mac (or on Windows presumably) to have a content size of 640 x 512. This is a great way of testing different resolutions without running emulators or buying lots of devices – just set the window size to the resolution of the device you want to test for.

In HelloWorldScene.cpp I’ve replaced the init() method to make it simple. After getting the visibleSize and the origin, I CCLOG them – it’s interesting to look at the output and see if it’s what you expect.

When I run with a design resolution of 1024 x 768 and a screen resolution of 1024 x 768 then I expect the visibleSize to be 1024, 768 and the origin to be 0,0

When I run with a design resolution of 1024 x 768 and a screen resolution of 640 x 512 my visible size is (960 x 768) and my origin (32,0)

This puzzled me at first – what is that 960? Didn’t we calculate the scaled width to be 682 pixels?

The answer is that visible size and offset refer to the design size and not the visible size. So 960 pixels from my original image will be visible in 682 pixels of screen.   I’ll repeat that.   visibleSize is telling us that 960 pixels of our 1024 pixel design width will be visible – scaled to fit our physical screen – but we don’t care about that scaling, as cocos2d-x is going to take care of it for us.

The full 768 pixels height of our design will be visible. Origin is telling us that 32 pixels left and right are chopped off from our design (i.e. before scaling) – and no pixels are chopped off top and bottom.

For our grass, we used the offset to adjust the position of our sprite – and we need to remember to do this for any sprite where we want it to appear at the same relative location on the screen at different resolutions.

For example, if we had a player sprite positioned at (0,100), using the figures from above (design resolution 1024 x 768, screen resolution 640 x 512) the sprite would actually appear 32 pixels to the left of the screen – so it might be invisible! So we need to position it to (0 + origin.x, 100 + origin.y) and instead of (0,100) they now are positioned at (32 , 100).   The y coordinate is the same – cocos will scale the 100 appropriately to fit the screen size. The x coordinate has the effect of moving the sprite 32 pixels to the right at the design resolution before cocos scales it to be in the right position on the screen.

Assets

Now we know how to position stuff so it looks the same in different resolutions, we come to the scaling problem.   If I supply a sprite and allow it to be scaled to any resolution the game is run at, it can look pretty bad. An extreme example would be a sprite has single-pixel width vertical lines; during scaling down, these lines might disappear completely. During scaling up to a larger size, some lines may end up being 2 physical pixels wide, while others are a single pixel wide. (This honestly depends on the algorithm used by the software for scaling, and I haven’t investigated cocos2d-x scaling – but the premise is the same; scaling can make sprites look bad!)

The answer, of course, is to provide different sprite assets for different resolutions. Back in the day, the resolutions generally covered were standard iphone, ipad and hd iphone (480 x 320, 1024 x 768 and 960 x 640) and we could provide assets for all three. Cross platform resolutions just used the closest resolution assets available. But now there are 2048 x 1536 ipads, 1136 x 640 iphones and the iPhone 6 comes in 1334 x 750 and 1920 x 1080 flavours!   Providing assets for any or all of these resolutions is pretty much up to you as a developer – you may want to provide assets for all resolutions if the look of your sprites is really important – or fewer if you don’t mind losing some resolution buy scaling while saving the overall size of your application.

Cocos2d-x v3 has a great way of providing you with all the help you need to provide the assets for as many or as few resolutions as you want to support.   First of all we need to create the assets and add them as resources to our application; but add different resolution resources in different folders. (I’ve called my folders HD, SD and UHD for 1024 x 768, 480 x 320 and 1920 x 1080 based graphics – but you can call them whatever makes sense to you – and provide them at whatever resolutions you want.   Now, to tell cocos2d-x how to find the right resources   In Appdelegate.cpp I add the following…

// Information about resources
typedef struct tagResource
{
	cocos2d::Size size; // The size that this resource is designed for
	cocos2d::Size useIfScreenOverSize; // If teh screen size is more than this value, this resource is a valid choice
	char directory[100]; // The name of the directory containing resources of this type
} Resource;
// Define all our resource types and locations
static Resource largeResource  =  { Size(1920, 1080), Size(1024, 768), "UHD"};
static Resource mediumResource =  { Size(1024, 768), Size(750, 544),  "HD" };
static Resource smallResource  =  { Size(480, 320), Size(0, 0),   "SD" };
// Declare the resolution we designed the game at
static Size designResolutionSize = Size(1024, 768);
// Declare an array containing the resource descriptions, from largest to smallest
static std::array<Resource,3>  resources{{largeResource, mediumResource, smallResource}};
bool AppDelegate::applicationDidFinishLaunching() {
	// initialize director
	auto director = Director::getInstance();
	auto glview = director->getOpenGLView();
	if(!glview) {
		glview = GLViewImpl::createWithRect("MultiRes", Rect(0,0,640,480));
		director->setOpenGLView(glview);
	}
	// Tell cocos our design resolution and our resolution policy
	glview->setDesignResolutionSize(designResolutionSize.width, designResolutionSize.height, ResolutionPolicy::FIXED_HEIGHT);
	// The vector we will use to build a list of paths to search for resources
	std::vector searchPaths;
	// Get the actual screen size
	Size frameSize = glview->getFrameSize();
	CCLOG("FrameSize is (%.0f, %.0f)", frameSize.width,frameSize.height);
	// Define a silly scale factor so we know when we have calculated it
	float scaleFactor = -1;
	// Look through our resource definitions
	for (auto resource : resources)
	{
		// If the screen is wider or higher than the resolution of the resource...
		if (frameSize.width > resource.useIfScreenOverSize.width )//|| frameSize.width > resource.useIfScreenOverSize.width)
		{
			// Add this directory to the search path
			searchPaths.push_back(resource.directory);
			CCLOG("searching in %s", resource.directory);
			// If we haven't already determined the scale factor, calculated it based on this resources resolution
			if (scaleFactor == -1)
				scaleFactor = resource.size.height /designResolutionSize.height;
			break;
		}
	}
	director->setContentScaleFactor(scaleFactor);
	FileUtils::getInstance()->setSearchPaths(searchPaths);
	CCLOG("Scale Factor = %f", scaleFactor);
	// turn on display FPS
	director->setDisplayStats(true);
	// set FPS. the default value is 1.0/60 if you don't call this
	director->setAnimationInterval(1.0 / 60);
	// create a scene. it's an autorelease object
	auto scene = HelloWorld::createScene();
	// run
	director->runWithScene(scene);
	return true;
}

Hopefully there are enough code comments for this to make sense.   One thing to note in particular is the way I select which resolution(s) to use. Most code snippets you see simply take the resolution of the asset and compare with the resolution of the device – and that generally means you will scale assets down. But you might not want to!   (I confess, I haven’t experimented too much, but it strikes me that it might look better to scale a small asset up by a small amount than to scale a larger asset down by a large amount)   so I introduced the concept of the useIfScreenOverSize property. So I can say   “I have an asset created at 1024 x 768 and want to use it if the device size is greater than 750 x 544 – and a resolution lower than 750 x 544 will use the next lowest resolution asset.

Another point of note is that I ‘break’ after finding the folder I want to support the resolution. This isn’t strictly necessary, you can add as many folders to the search path as you like, and cocos will keep searching until it finds the file matching the name you requested.

This can be useful in keeping app sizes down. You may have a graphic resource where size really isn’t important (maybe a cloud in the sky, for example) so you provide that image only at a lower resolution. Adding the lower resolution folders to the search path allows cocos to find the resource, but it will only scale it as if it were designed at the design size you specified – but sometimes that’s all that’s required for purely decorative images.





About List