martes, 13 de mayo de 2014

Multi-resolution Support in iOS with Cocos2d-x v3

EDIT: I updated this post to include new iPhone models, and to target Android devices. You can check it here.

Hi all!

Cocos2d-x is a branch of Cocos2d, one of the most used frameworks for developing iOS games, and the one I myself used to develop Chubby Buddy. Cocos2d-x was released to target cross-platform development, including iOS, Android and Windows Phone. It's powerful and widely used, but recently version 3 came out and brought some drastic changes with respect to versions 2.x.x.

Cocos2d-x offers a C++ front-end (also a Lua, a Javascript and HTML5 ones, but we'll let those aside in this post) as opposed to the Objective-C front-end provided by its cousin Cocos2d. 

In addition to the language, one of the first changes that a developer may find is the scheme to support multi-resolution support. In Cocos2d, you simply used a scheme based on suffixing assets according to their resolution. Therefore, if you had a bitmap called Player.png and you wanted to support all resolutions in iOS, you had to create four versions of it and name them like this:

Player.png (iPhone low resolution)
Player-hd.png (iPhone retina)
Player-ipad.png (iPad low resolution)
Player-ipadhd.png (iPad retina)

During application launch, you could specify different suffixes, but the above were the default choices.

This scheme is no longer used by Cocos2d-x. Instead, you must specify different directories where you can find the assets for each resolution. This is done in the applicationDidFinishLaunching() method in AppDelegate.cpp file, which is automatically called by the iOS framework during initialization. The resulting code is shown next:

bool AppDelegate::applicationDidFinishLaunching() {
    // initialize director
    auto director = Director::getInstance();
    auto glview = director->getOpenGLView();
    if(!glview) {
        glview = GLView::create("My Game");
        director->setOpenGLView(glview);
    }
    

    // turn off display FPS
    director->setDisplayStats(false);

    // set FPS. the default value is 1.0/60 if you don't call this
    director->setAnimationInterval(1.0 / 60);

    auto screenSize = glview->getFrameSize();
    
    auto fileUtils = FileUtils::getInstance();
    std::vector<std::string> searchPaths;
    
    if (screenSize.width == 2048 || screenSize.height == 2048)
    {
        glview -> setDesignResolutionSize(1536, 2048, ResolutionPolicy::NO_BORDER);
        searchPaths.push_back("ipadhd");
        searchPaths.push_back("ipadsd");
        searchPaths.push_back("iphone5");
        searchPaths.push_back("iphonehd");
        searchPaths.push_back("iphonesd");
    }
    else if (screenSize.width == 1024 || screenSize.height == 1024)
    {
        glview -> setDesignResolutionSize(768, 1024, ResolutionPolicy::NO_BORDER);
        searchPaths.push_back("ipadsd");
        searchPaths.push_back("iphone5");
        searchPaths.push_back("iphonehd");
        searchPaths.push_back("iphonesd");
        
    }
    else if (screenSize.width == 1136 || screenSize.height == 1136)
    {
        glview -> setDesignResolutionSize(640, 1136, ResolutionPolicy::NO_BORDER);
        searchPaths.push_back("iphone5");
        searchPaths.push_back("iphonehd");
        searchPaths.push_back("iphonesd");
        
    }
    else if (screenSize.width == 960 || screenSize.height == 960)
    {
        glview -> setDesignResolutionSize(640, 960, ResolutionPolicy::NO_BORDER);
        searchPaths.push_back("iphonehd");
        searchPaths.push_back("iphonesd");
    }
    else
    {
        searchPaths.push_back("iphonesd");
        glview -> setDesignResolutionSize(320, 480, ResolutionPolicy::NO_BORDER);
    }
    
    fileUtils->setSearchPaths(searchPaths);
    
    //Load your first scene
    auto scene = GameManager::createScene();
    director->runWithScene(scene);

    return true;
}

If we look into the setSearchPaths method, we see that the search path is suffixed to a base search path that is platform-dependent. In the case of Xcode, the base search path is the Resources folder. Therefore, according to previous code, we would need the following directory structure:

Resources/
*ipadhd/  (assets for iPad retina)
*ipadsd/  (assets for iPad non-retina)
*iphone5/ (assets for iPhone5)
*iphonehd/ (assets for iPhone retina)
*iphonesd/ (assets for iPhone)

Note in the code that for each resolution, we also include directories for lower resolutions. We simply want to ensure that if an asset is not found for the targeted resolution, at least we try to find the next one with the best resolution. Also, note that I want to present the game in portrait mode, but it would be better to use screenSize.width and screenSize.height as parameters of setDesignResolutionSize(), just to ensure that everything will work in landscape mode as well. 

Once you have the directory structure, you can drag all the folders in Xcode. In this step, Xcode will ask how you want to deal with these folders. According to some problems found by other people, I suggest you to uncheck "Copy items into destination group's folder (if needed)" and to check "Create folder references for any added folders". You can read more information on this options here

That's all, tell me if you have any problems and we'll try to solve it.
FM

3 comentarios:

  1. If you are setting a different design resolutions for each device resolution, how do you calculate de positions of the elements in the screen (buttons, labels, etc)? And how do you manage resolutions in Android?

    ResponderEliminar
    Respuestas
    1. In general, you should use always relative positions. You can always retrieve the actual resolution from a simple call. Then, you can play with that. Say, for instance, that you want to place something in the middle of the screen. You'd do something like:

      //This is how to retrieve the actual size (in pixels)
      cocos2d::Size visibleSize = Director::getInstance() -> getVisibleSize();

      //You want sprite1 to be in the center of the screen
      sprite1 -> setPosition(visibleSize.x / 2, visibleSize.y / 2);

      //You want next sprite (sprite2) to be right next to the previous one
      sprite2 -> setPosition(visibleSize.x/2 + sprite1 -> getContentSize().width, visibleSize.y/2);

      This works the same for all resolutions as you're not using absolute coordinates.

      Eliminar