Cocos2d-x v3.x Multi Resolution Support

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 size
b)   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.

 

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.

screen1

You can see the entire border.

Running at 640 x 512

screen2

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.

2 thoughts on “Cocos2d-x v3.x Multi Resolution Support

  1. Hi There,

    At last!! today i am feeling lucky that i digested the multi-resolution support of cocos2d-X after reading your blog. I was reading the wiki’s , watching videos etc all over the internet at least 50 times each of them, but dont know why i was not getting the concept. May be I am that dumb and take time to understand.

    I understood your blog, but there is some itching in my mind running which I believe will go away once i write the code for my game.

    I am not able to take a decision on WHAT SHOULD BE MY DESIGN-RESOLUTION for my game. I want to support all the ios device (http://www.iosres.com/index-legacy.html) and want to try Android devices as well.

    I am interested to follow your approach and want to support all IOS and max android devices. So what are all resolution of assets i should ask my graphic designer to provide to me.

    I saw your spreadsheet (which of course shows how much research you have done), how to use it properly. When i saw it at first i thought i will quit game coding…. 🙂

    Would you mind throwing some hints to me in deciding these above concerns.

    Once again thank you very much for this blog and wonderful write-up and explanation with practical.

    Regards
    Krishna

    1. First – sorry for not replying sooner – so many things to do, so little time to do them!

      Here’s my setup code showing what resolutions I use for design and the four asset sizes I use…

      // Define all our resource types and locations
      static Resource ultraResource   =  { Size(2048, 1536), "UD"};
      static Resource hiResource      =  { Size(1920, 1080),  "HD" };
      static Resource stdResource     =  { Size(960, 640),   "SD" };
      static Resource lowResource     =  { Size(570, 320),   "LD" };
      
      // Declare the resolution we designed the game at
      static Size designResolutionSize = Size(1920, 1080);
      ...
      // 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();
          // Define a silly scale factor so we know when we have calculated it
          float scaleFactor = -1;
          int widthDiff = 999;
          Resource found = ultraResource; // Default to this in case we find a resolution we plain can't figure out!   
          // Look through our resource definitions
          for (auto resource : resources)
          {
              // Calculate the Horizontal ratio for the resouces (i.e. Resource Height / Device Height
              float ratio = resource.size.height / frameSize.height;
              
              // Calculate the scaled widths based on the horizontal ratio
              float scaleWidth = resource.size.width / ratio;
              
              // Calculate the Width Differences (i.e. how much bigger is the resource than the screen, width wise- so how many pixels are we going to lose?
              int diff = scaleWidth - frameSize.width;
              
              // If the width difference is < -1 we'd have black bars, so ignore it (single pixel is fine!)
              // If the scale factor is > 2 we're scaling a bit too much, ignore it
              // If the scale factor is < 0.5 we're scaling a bit to much, ignore it
              
              if (diff >= -1 && ratio < = 2 && ratio >= 0.5)
              {
                  // Use this one if it is the lowest width difference
                  if (diff < widthDiff)
                  {
                      widthDiff = diff;
                      found = resource;
                      scaleFactor =resource.size.height /  designResolutionSize.height ;
                      CCLOG("Trying %s widthDiff %d ScaleFactor %.2f VisibleOffset %.0f, %.0f", found.directory, widthDiff, scaleFactor, director->getVisibleOrigin().x, director->getVisibleOrigin().y);
                  }
              }
              
              
          }
          // so now we should have found which resource to use
          searchPaths.push_back(found.directory);
          
          Globals::resourcePath = found.directory;
          
          CCLOG("Using %s widthDiff %d ScaleFactor %.2f VisibleOffset %.0f, %.0f", found.directory, widthDiff, scaleFactor, director->getVisibleOrigin().x, director->getVisibleOrigin().y);
          
          director->setContentScaleFactor(scaleFactor);

      I know what you mean about thinking about giving up when you see all the different resolutions and thing about all the scaling issues there are likely to be!

      I hope this code helps – if nothing else you can see the resolutions I’m using.

      I don’t profess to be an expert on this – and certainly the code can be improved – but it does me fine for now!

Leave a Reply

Your email address will not be published. Required fields are marked *