Tutorial: Scrollview Optimization in Cocos Creator

Cocos Creator ScrollView Performance Optimization

Note: This article is based on Cocos Creator 2.1.2 version

Note: You can reference to this demo project

Introduction and why are there performance problems

The ScrollView component of Cocos Creator is a common component in game development. It is often used in game interfaces, like ranking interface, task list, package system and other modules. However, it can suffer from performance issues. When needing to display many items, performance can be a blocker because Cocos Creator only implements the most basic features of scrolling. Developers to do some optimization depending on their usage situation.
ScrollView can suffer from these performance issues:

  • Higher quantities of DrawCall and lower rendering performance
  • Too many nodes in a ScrollView, can cause higher performance overhead when invoking the Enable and Disable functions when we hide or show the interface

For example, this interface suffers from performance issues:

The ScrollView component in this scene has 20 cells,the total DrawCall has reached 790. A single cell has about 50 child-nodes, and in total about 1000 Node objects in this interface. Can you see why performance is an issue?

General optimizations

First, let’s do some general optimizations to help gain some performance.

  • We need to merge the rendering batches to reduce the number of DrawCalls, so that we can improve the rendering performance, there are two different way to achieve it:

    • Plan A: Packaging textures in an atlas texture using Auto-atlas or TexturePacker

      This allows multiple Sprite objects rendered with the same atlas texture, so that we can merge those rendering batches and optimize the number of DrawCalls and the CPU performance.
      Using Auto-atlas, you can reference this document to get more information on Auto-atlas Assets

      Let’s see the result of using Auto-atlas:

      Notice the number of DrawCalls had dropped to 556 which was lower than before, it is also only 20 cells.

    • Plan B: using the dynamicAtlas feature, you can add the following code to your main.js file to open this feature:

        cc.macro.CLEANUP_IMAGE_CACHE = false;
          cc.dynamicAtlasManager.enabled = true;
      

      After opening, Cocos Creator, will help us merge textures into a atlas texture automatically and dynamically when the project is running. This shows us a result, the number of DrawCalls had dropped.

      If you are debugging on Google’s Chrome browser, you can use a plug-in named spector.js to debug the number of DrawCalls and can see how each DrawCall handles textures. For native development, we can use the GPU analysis in XCode

  • Handling the Label objects。

    • If we use Auto-atlas or TexturePacker to handle textures, we can BMFont objects with Label objects instead of SystemFont objects, so that we can package our font image with other textures into an atlas texture.

      You can see the DrawCalls has been further reduced, and is now 330.

    • If we used dynamicAtlas feature, then we can use SystemFont objects with Label objects, and can make sure the Label objects CacheModde property has changed to BITMAP mode.

      In BITMAP mode, the Label object’s texture will be handled as a Sprite texture, and flow into dynamicAtlas. Then the texture between Label and Sprite could be merged. The result:

      It should be noted that the dynamicAtlas feature will bring more CPU calculations and overhead due to rendering the dynamicAtlas texture.

  • Only displaying what we need

    • The Node objects that are off screen (outside what we can see) do not need to be rendered. Through the calculation of the cell’s position, we can know what cell should be visible and what cell should be hidden. We can use this to set the opacity property of Node objects that do not need to be displayed to 0, then we can reduce the cost of GPU cycles, and therefore reduce the number of DrawCalls. This is some example code:
    update (dt) {
          var viewRect = cc.rect(- this.view.width / 2, - this.content.y - this.view.height, this.view.width, this.view.height);
          for (let i = 0; i < this.content.children.length; i++) {
              const node = this.content.children[i];
              if (viewRect.intersects(node.getBoundingBox())) {
                  node.opacity = 255;
              }
              else {
                  node.opacity = 0;
              }
          }
    }
    
    • The logic is in the update function, you can also put this code in the scrolling callback of ScrollView, then we do not need to calculate every frame, this is then only calculated when we need to reduce the cost of CPU.

      We can see the number of DrawCalls has dropped to 68 based on using dynamicAtlas.

  • Try to implement your interface without using mask components, or use mask components as minimal as possible.

    • Because the mask component needs to add render commands to change the GL state before and after stencil and content, so the mask component will break the merge and can add additional DrawCalls.

    • When we need to display some special display, Like icon with rounded corners, try to implement it without mask component if possible. For example, ask your artist to provide you rounded resources.

    • Currently, mask components, spine components, and dragonBone components will interrupt the batching process, we should avoid them and instead optimize your scene graph.

    • In the demo project, each following icon has a child node which used mask component:

      10

      Disabling the mask component results in the following:

      Finally, just 18 DrawCalls!

      ScrollView has a mask component which is used for culling the display and we can not avoid this operation.

  • Reuse those cells and reduce the number of Node objects.

    • We do a lot of optimization on the number of DrawCalls already, but the actual number of Node objects is still very large. When the interface is shown or hidden, a large number of Node objects means a large cost of invoking Enable and Disable. We can reuse those cells and update the location and display to reduce the number of Node objects.

    • You can reference the ListView case in Cocos Creator examples. It works as follows:

    You can see the detailed code in the ScrollView3 scene of the demo project.

Conclusion

Finally result as follows:

We use only 7 cell nodes to achieve a list of 20 cells, the number of DrawCalls is down to 18, and the actual number of nodes used is about 300, much less than the 1000 we started out with. If you take into account some of these common ScrollView optimizations it can greatly improve the performance of your game.

Thank you for reading!

12 Likes

That’s what I’m talking about.

Kudos for the wonderful informative article!!

Really need similar articles over the time to be accessible on cocos docs section somewhere.

1 Like

I think so as well and I’ll see what we can do.

1 Like

Solution for c++ . I tested it ! Hope this solution help you ! If you have a better solution , please leave a comment below !

	//I don't know why the ScrollView::addEventListener function does not work properly at the first time.
	//so children of listview will be set invisible (setVisible=flase) ,
	//and only index at 0 is visible.
	//so we need to auto-scroll the _listView to refresh this.
	this->runAction(Sequence::create(DelayTime::create(0.1f), CallFunc::create([=]() {
		_listView->scrollToItem(1, Vec2::ANCHOR_MIDDLE, Vec2::ANCHOR_MIDDLE);
	}), DelayTime::create(0.1f), CallFunc::create([=]() {
		_listView->scrollToItem(0, Vec2::ANCHOR_MIDDLE, Vec2::ANCHOR_MIDDLE);
	}),nullptr));

_listView->ScrollView::addEventListener([this](Ref* ref, ScrollView::EventType eventType) {
            ListView* listView = dynamic_cast<ListView*>(ref);
            if(listView == nullptr || eventType != ScrollView::EventType::CONTAINER_MOVED)
            {
                return;
            }
            auto left = listView->getLeftmostItemInCurrentView();
            auto right = listView->getRightmostItemInCurrentView();
            auto top = listView->getTopmostItemInCurrentView();
            auto bottom = listView->getBottommostItemInCurrentView();
            auto center = listView->getCenterItemInCurrentView();
            
			for (size_t i = 0; i < listView->getChildrenCount(); i++)
			{
				listView->getChildren().at(i)->setVisible(false);
			}

			for (size_t i = listView->getIndex(top); i <= listView->getIndex(bottom); i++)
			{
				if (auto node = listView->getItem(i))
				{
					node->setVisible(true);
				}
			}

			//top->setVisible(true);
			//bottom->setVisible(true);
			//center->setVisible(true);
        });
4 Likes