{"id":1077,"date":"2012-05-22T20:52:37","date_gmt":"2012-05-22T18:52:37","guid":{"rendered":"http:\/\/www.frozax.com\/blog\/?p=1077"},"modified":"2015-04-04T19:46:39","modified_gmt":"2015-04-04T19:46:39","slug":"optimizing-graphics-performance-ios-android-cocos2dx","status":"publish","type":"post","link":"https:\/\/www.frozax.com\/blog\/2012\/05\/optimizing-graphics-performance-ios-android-cocos2dx\/","title":{"rendered":"Optimizing Graphics Performance on iOS and Android"},"content":{"rendered":"<p>The\u00a0main device is used during the development of <a href=\"http:\/\/www.frozax.com\/games\/dont-feed-the-trolls\">Don&#8217;t Feed the Trolls<\/a> is an <a href=\"http:\/\/acer.us\/ac\/en\/US\/content\/iconia-tab-a500\">Acer Iconia Tab A500<\/a>. It&#8217;s quite powerful and I never met performance issues with it.<\/p>\n<p>Of course, I tested the game on many other iOS and Android devices. I was surprised to discover strong performance issues on a HTC Desire. <\/p>\n<p><em>In this article, I will explain the reasons of these low performances and how I fixed them. I will also give you tips on how to find out where are the potential problems.<\/em><\/p>\n<h3>Problem 1 : in-game, the game was slower and slower when getting closer to the end of the level<\/h3>\n<p>O<a href=\"http:\/\/www.frozax.com\/blog\/wp-content\/uploads\/2012\/05\/03.-Screenshot-Slap-Party-Game-Mode.png\"><img decoding=\"async\" loading=\"lazy\" class=\"alignleft size-thumbnail wp-image-1184\" title=\"03. Screenshot - Slap Party Game Mode\" src=\"http:\/\/www.frozax.com\/blog\/wp-content\/uploads\/2012\/05\/03.-Screenshot-Slap-Party-Game-Mode-150x150.png\" alt=\"03. Screenshot - Slap Party Game Mode\" width=\"150\" height=\"150\" \/><\/a>Of course, the difference at the end of the level is that many bears and trolls have been displayed so the problem probably has to do with that. However, there was never more than 3 or 4 characters visible at the same time.<\/p>\n<p>Also, to avoid memory allocation during gameplay, I pre-allocate all the character sprites used in the level at the start of the level. So the problem was not memory related. Finally, the game code dealing with inactive or invisible characters was doing nearly nothing.<\/p>\n<p>I finally found out the problem is caused by characters that disappeared. They are displayed, but fully transparent. I used a <a href=\"http:\/\/www.cocos2d-x.org\/embedded\/cocos2d-x\/d6\/d6d\/classcocos2d_1_1_c_c_fade_out.html\">CCFadeOut<\/a> action to hide the sprites. At the end of the <em>CCFadeOut<\/em>, the object was still displayed, but with an opacity of 0. An easy fix is to hide the sprite (<a href=\"http:\/\/www.cocos2d-x.org\/embedded\/cocos2d-x\/d1\/d72\/classcocos2d_1_1_c_c_hide.html\">CCHide<\/a> action) at the end of the fade out to avoid a useless draw call.<\/p>\n<p><span style=\"color: #ff0000;\"><strong>Fix 1 : Make sure you do not have invisible sprites (completely transparent or with very low opacity). Destroy or hide them instead.<\/strong><\/span><\/p>\n<h3>Problem 2 : the main menu is very slow<\/h3>\n<p><a href=\"http:\/\/www.frozax.com\/blog\/wp-content\/uploads\/2012\/05\/05-Screenshot-Menu-Achievements-List.png\"><img decoding=\"async\" loading=\"lazy\" class=\"alignleft size-thumbnail wp-image-1186\" title=\"05 - Screenshot - Menu - Achievements List\" src=\"http:\/\/www.frozax.com\/blog\/wp-content\/uploads\/2012\/05\/05-Screenshot-Menu-Achievements-List-150x150.png\" alt=\"05 - Screenshot - Menu - Achievements List\" width=\"150\" height=\"150\" \/><\/a>My menus have many animated transitions, for instance when you choose the &#8220;achievements&#8221; menu, the game mode selection screen leaves to the left, while the achievements screen comes from the right. There is also a credits screen, a level selection screen (for the classic mode) and a difficulty selection screen (for the versus mode). All these screens are built once when loading the main menu. They are simply off-screen when not displayed to the player.<br \/>\nThe performance problem was therefore obvious: all the offscreen items were slowing down the whole program. I thought cocos2dx or the device itself would not display offscreen items but I was wrong. Simply hiding these items improved the performance greatly. In the achievements list (see screenshot on the left), I hide every line and sprite as soon as it&#8217;s out of the screen. It requires specific code but it&#8217;s really worthwhile.<\/p>\n<p><span style=\"color: #ff0000;\"><strong>Fix 2 : Do not have elements displayed off screen. Remove them from the scene or hide them instead.<\/strong><\/span><\/p>\n<h3>Problem 3 : the main menu is slow when the rankings are displayed<\/h3>\n<p><a href=\"http:\/\/www.frozax.com\/blog\/wp-content\/uploads\/2012\/05\/07-Screenshot-Menu-Rankings.png\"><img decoding=\"async\" loading=\"lazy\" class=\"alignleft size-thumbnail wp-image-1190\" title=\"07 - Screenshot - Menu - Rankings\" src=\"http:\/\/www.frozax.com\/blog\/wp-content\/uploads\/2012\/05\/07-Screenshot-Menu-Rankings-150x150.png\" alt=\"07 - Screenshot - Menu - Rankings\" width=\"150\" height=\"150\" \/><\/a>In the main menu, when the rankings are displayed (see screenshot on the left), the fps drops heavily. Obviously, this is caused by the quantity of text. Cocos2dx optimizes the texts by using a single OpenGL draw call to display a string, instead of drawing each character one by one. It&#8217;s good but not enough in my case. I initially used a simple (and logical) approach and had one <a href=\"http:\/\/www.cocos2d-x.org\/embedded\/cocos2d-x\/dc\/dfb\/classcocos2d_1_1_c_c_label_b_m_font.html\">CCLabelBMFont<\/a> per element of the rankings. There are up to 10 lines with 3 elements per line (position, name, score), and two headers. That&#8217;s a total of 32 draw calls here, only for texts. I updated the code so that all the rankings texts are now displayed with a single draw call (see below for implementation details). The game is now running smoothly.<\/p>\n<p><span style=\"color: #ff0000;\"><strong>Fix 3 : Use as few draw calls as possible by grouping sprites together<\/strong><\/span><\/p>\n<h3>How to reduce draw calls with <a href=\"http:\/\/www.cocos2d-x.org\">Cocos2d-x<\/a>?<\/h3>\n<p>The easiest way to reduce draw calls is to use a\u00a0<a href=\"http:\/\/www.cocos2d-x.org\/embedded\/cocos2d-x\/dd\/d95\/classcocos2d_1_1_c_c_sprite_batch_node.html\">CCSpriteBatchNode<\/a>. If you have multiple sprites using the same texture, they can be displayed in one call if they share the same <em>CCSpriteBatchNode<\/em> parent. Hopefully, as I use texture pages, my sprites are nearly all in the same texture (see <a href=\"http:\/\/www.frozax.com\/blog\/2011\/12\/why-how-pack-textures-ios-android\/\">How to pack your textures<\/a>). Here are some examples of optimizations I did in Don&#8217;t Feed the Trolls:<\/p>\n<ul>\n<li>All the elements of the UI (timer bar, troll head, red cross&#8230;) now share the same <em>CCSpriteBatchNode<\/em> (up to five draw calls removed)<\/li>\n<li>The bears are displayed with two sprites, the trolls with three sprites. Each character is now displayed as a <em>CCSpriteBatchNode<\/em> (one or two draw calls removed per character)<\/li>\n<li>In the main menu, the rankings background is using the same sprite twice, they now share the same <em>CCSpriteBatchNode<\/em> (one draw call removed)<\/li>\n<\/ul>\n<p>For the fonts, the <a href=\"http:\/\/www.cocos2d-x.org\/embedded\/cocos2d-x\/dc\/dfb\/classcocos2d_1_1_c_c_label_b_m_font.html\">CCLabelBMFont<\/a> is a <em>CCSpriteBatchNode<\/em> itself (derivation). However, I have so much text in the rankings that it still slows down the game (Problem 3). I made some code to regroup all the texts of the rankings together under one <em>CCLabelBMFont<\/em>. As I used different scale, colors, and alignment, it&#8217;s not trivial and can&#8217;t be done automatically with a single <em>CCLabelBMFont<\/em>. I decided to have the <em>CCLabelBMFont<\/em> class positions the letters properly, and then move each letter under a &#8220;master label&#8221;, and finally deleting the original label. Here is the raw code I used (<em>_mega_label<\/em> is the node that contains all the text, <em>bmf<\/em> is the label we want to add to the mega label (temporary object)) :<\/p>\n<pre><code class=\"cpp\">\r\n\t\/\/ copy the label to mega_label and delete it\r\n\tCCArray *children = bmf-&gt;getChildren();\r\n\tif( children )\r\n\t{\r\n\t\t\/\/ loop through all characters\r\n\t\twhile( children-&gt;count() )\r\n\t\t{\r\n\t\t\tCCNode *ch = (CCNode*)(children-&gt;objectAtIndex( 0 ));\r\n\t\t\t\/\/ remove characters from current label (retain it before removing it)\r\n\t\t\tch-&gt;retain();\r\n\t\t\tbmf-&gt;removeChildAtIndex( 0, true );\r\n\t\t\tVector2 char_pos = ch-&gt;getPosition();\r\n\t\t\t\/\/ compute proper position taking scale into account\r\n\t\t\tchar_pos.x = char_pos.x*scale + p.x;\r\n\t\t\tchar_pos.y = char_pos.y*scale + p.y;\r\n\t\t\tch-&gt;setPosition( char_pos );\r\n\t\t\tch-&gt;setScale( scale );\r\n\t\t\t\/\/ add it to label\r\n\t\t\t_mega_label-&gt;addChild( ch );\r\n\t\t\t\/\/ Don't Feed the Trolls specific : I keep a list of animated sprites\r\n\t\t\t\/\/ (they have there opacity animated)\r\n\t\t\t\/\/ I do it manually instead of using an action\r\n\t\t\tif( is_local_player )\r\n\t\t\t{\r\n\t\t\t\t\/\/ need an animation, add the sprite to a specific list\r\n\t\t\t\t_animated_sprites.push_back( (CCSprite*)ch );\r\n\t\t\t}\r\n\t\t\tch-&gt;release();\r\n\t\t}\r\n\t}\r\n\t\/\/ free the original label\r\n\tbmf-&gt;release();\r\n<\/code><\/pre>\n<h3>How to find out where to optimize?<\/h3>\n<p>You don&#8217;t need to optimize everything, just the slow sections of your game. Here is how I found out where to optimize:<\/p>\n<ul>\n<li>I display on screen the draw call count. To do that, I hacked into cocos2d-x code and simply added a global counter before each call to <em>glDrawArray<\/em> or <em>glDrawElements<\/em>. I used a <em>#define<\/em> to easily remove this code in released versions of the game. Be careful to ignore the sprites used to display the debug text itself.<\/li>\n<li>I also have a function that goes recursively inside a CCNode and count visible\/hidden nodes, nodes with low opacity (less than 10) and so on. Very handy to detect items not visible because they are out of the screen or with a very low opacity.<br \/>\nHere is the code used, adapt it to your needs:<\/p>\n<pre><code class=\"cpp\">\r\nint nb_nodes, nb_visible_nodes, nb_rgba_nodes, nb_not_letters_nodes,\r\n\tnb_not_letters_visible_nodes, nb_low_opacity_nodes;\r\n\r\n\/\/ recursive function counting\r\n\/\/ please note that I'm using an old version of cocos2d-x, with\r\n\/\/ convertToLabelProtocol and convertToRGBAProtocol: I found the\r\n\/\/ new version with RTTI a terrible idea and reverted it\r\nvoid DEBUGCountNodes( CCNode *n, bool parent_visible, bool is_letter )\r\n{\r\n\tnb_nodes++;\r\n\tbool we_are_visible = n-&gt;getIsVisible() &amp;&amp; parent_visible;\r\n\tbool father_is_label = is_letter;\r\n\tbool we_are_label = is_letter || (n-&gt;convertToLabelProtocol() != NULL);\r\n\r\n\tif( we_are_visible )\r\n\t\tnb_visible_nodes++;\r\n\tif( we_are_visible &amp;&amp; !father_is_label )\r\n\t\tnb_not_letters_visible_nodes++;\r\n\tif( !father_is_label )\r\n\t\tnb_not_letters_nodes++;\r\n\r\n\tif( n-&gt;convertToRGBAProtocol() )\r\n\t{\r\n\t\t\/\/ count low opacity nodes (if below 10\/255)\r\n\t\tif( n-&gt;convertToRGBAProtocol()-&gt;getOpacity() &lt; 10 &amp;&amp; we_are_visible )\r\n\t\t\tnb_low_opacity_nodes++;\r\n\t\tnb_rgba_nodes++; \r\n\t}\r\n \t\/\/ recursive part\r\n\tif( n-&gt;getChildren() )\r\n\t{\r\n\t\tfor( unsigned int i = 0; i &lt; n-&gt;getChildren()-&gt;count(); i++ )\r\n\t\t{\r\n\t\t\tDEBUGCountNodes( (CCNode*)(n-&gt;getChildren()-&gt;objectAtIndex(i)),\r\n\t\t\t\twe_are_visible, we_are_label );\r\n\t\t}\r\n\t}\r\n}\r\n\r\n\/\/ initial function to call, usually with the initial node of your sprite\r\n\/\/ hierarchy (mine is called _main_node)\r\nstring fgGame::DEBUGCocosNodes( CCNode *start )\r\n{\r\n\tif( start == NULL )\r\n\t\tstart = _main_node;\r\n\tnb_nodes = nb_visible_nodes = nb_rgba_nodes = nb_not_letters_nodes =\r\n\t\t nb_not_letters_visible_nodes = nb_low_opacity_nodes = 0;\r\n\tDEBUGCountNodes( start, true, false );\r\n\tchar str[256];\r\n\tsprintf( str, \"All:%d\/%d NoTxt:%d\/%d LowOp:%d\", nb_visible_nodes, nb_nodes,\r\n\t\tnb_not_letters_visible_nodes, nb_not_letters_nodes, nb_low_opacity_nodes );\r\n\treturn str;\r\n}\r\n<\/code><\/pre>\n<\/li>\n<\/ul>\n<p>The great thing with this logging is that you can run it on the Windows build.<\/p>\n<h3>Conclusion<\/h3>\n<p>Make sure you test your app on a few less powerful devices. If you keep in mind the tips in this article and display debug information from time to time, you should easily be able to optimize the slowest code of your game.<br \/>\nFrom my tests, I found out that trying to have a maximum of 30 draw calls is a good way if you want to achieve 50 or 60 frames per second on nearly all medium-end devices. Of course, this is heavily empirical and you&#8217;ll have to do you own tests. <\/p>\n<p>I hope this helped, feel free to comment and talk about your optimization techniques in the comments, on <a href=\"http:\/\/twitter.com\/frozax\">twitter<\/a> or on <a href=\"http:\/\/www.facebook.com\/frozax\">facebook<\/a>. <\/p>\n<p>And don&#8217;t forget to try and rate <a href=\"http:\/\/www.frozax.com\/games\/dont-feed-the-trolls\">Don&#8217;t Feed the Trolls<\/a> (Free download, <a href=\"http:\/\/itunes.apple.com\/app\/id510201848?mt=8\">iTunes<\/a>, <a href=\"http:\/\/play.google.com\/store\/apps\/details?id=com.frozax.dontfeedthetrolls\">Google Play<\/a>)!<\/p>\n<div class=\"addtoany_share_save_container addtoany_content_bottom\"><div class=\"a2a_kit a2a_kit_size_32 addtoany_list a2a_target\" id=\"wpa2a_1\"><a class=\"a2a_button_twitter\" href=\"http:\/\/www.addtoany.com\/add_to\/twitter?linkurl=https%3A%2F%2Fwww.frozax.com%2Fblog%2F2012%2F05%2Foptimizing-graphics-performance-ios-android-cocos2dx%2F&amp;linkname=Optimizing%20Graphics%20Performance%20on%20iOS%20and%20Android\" title=\"Twitter\" rel=\"nofollow\" target=\"_blank\"><\/a><a class=\"a2a_button_facebook\" href=\"http:\/\/www.addtoany.com\/add_to\/facebook?linkurl=https%3A%2F%2Fwww.frozax.com%2Fblog%2F2012%2F05%2Foptimizing-graphics-performance-ios-android-cocos2dx%2F&amp;linkname=Optimizing%20Graphics%20Performance%20on%20iOS%20and%20Android\" title=\"Facebook\" rel=\"nofollow\" target=\"_blank\"><\/a><a class=\"a2a_button_google_plus\" href=\"http:\/\/www.addtoany.com\/add_to\/google_plus?linkurl=https%3A%2F%2Fwww.frozax.com%2Fblog%2F2012%2F05%2Foptimizing-graphics-performance-ios-android-cocos2dx%2F&amp;linkname=Optimizing%20Graphics%20Performance%20on%20iOS%20and%20Android\" title=\"Google+\" rel=\"nofollow\" target=\"_blank\"><\/a><a class=\"a2a_button_reddit\" href=\"http:\/\/www.addtoany.com\/add_to\/reddit?linkurl=https%3A%2F%2Fwww.frozax.com%2Fblog%2F2012%2F05%2Foptimizing-graphics-performance-ios-android-cocos2dx%2F&amp;linkname=Optimizing%20Graphics%20Performance%20on%20iOS%20and%20Android\" title=\"Reddit\" rel=\"nofollow\" target=\"_blank\"><\/a>\n<script type=\"text\/javascript\"><!--\nwpa2a.script_load();\n\/\/--><\/script>\n<\/div><\/div>","protected":false},"excerpt":{"rendered":"<p>The\u00a0main device is used during the development of Don&#8217;t Feed the Trolls is an Acer Iconia Tab A500. It&#8217;s quite powerful and I never met performance issues with it. Of course, I tested the game on many other iOS and Android devices. I was surprised to discover strong performance issues on a HTC Desire. In&hellip; <a class=\"more-link\" href=\"https:\/\/www.frozax.com\/blog\/2012\/05\/optimizing-graphics-performance-ios-android-cocos2dx\/\">Continue reading <span class=\"screen-reader-text\">Optimizing Graphics Performance on iOS and Android<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":[],"categories":[2,3,7,11,12],"tags":[],"_links":{"self":[{"href":"https:\/\/www.frozax.com\/blog\/wp-json\/wp\/v2\/posts\/1077"}],"collection":[{"href":"https:\/\/www.frozax.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.frozax.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.frozax.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.frozax.com\/blog\/wp-json\/wp\/v2\/comments?post=1077"}],"version-history":[{"count":1,"href":"https:\/\/www.frozax.com\/blog\/wp-json\/wp\/v2\/posts\/1077\/revisions"}],"predecessor-version":[{"id":1671,"href":"https:\/\/www.frozax.com\/blog\/wp-json\/wp\/v2\/posts\/1077\/revisions\/1671"}],"wp:attachment":[{"href":"https:\/\/www.frozax.com\/blog\/wp-json\/wp\/v2\/media?parent=1077"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.frozax.com\/blog\/wp-json\/wp\/v2\/categories?post=1077"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.frozax.com\/blog\/wp-json\/wp\/v2\/tags?post=1077"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}