Cocos2d-x resources - On-screen logging


On-screen logging (C++)


When running your app on a device, you can usually view logging output in real-time on your computer (eg. in the Xcode debug output panel for iOS, or the logcat for Android). However, this requires that the device is connected to the computer. It also means you need to watch two different screens if you need to actually play your game while expecting something to show up in the log. And if your logging outputs a useful message when you're not expecting it, or when you don't have the log open, you may never even notice it happened.

To make logging output a bit more convenient, I like to have it displayed directly on the screen of my game so I can see what's happening all in one view. This also means that the device does not need to be connected to the computer to view the log, so even users without a development environment can view the output. If you have a beta testing phase before release, you may find it handy to let your testers see warnings or errors to help you pinpoint problems.


Below you will find source code and explanation for a class (sub-class of cocos2d::Layer) which lets you add this functionality quite easily to a Cocos2d-x application. The HelloWorld sample is used as a base for demonstration. So far I have used it on only iOS and Android.


Source code


ScreenLog.h
ScreenLog.cpp

This code was confirmed to work with Cocos2d-x v3 RC1, but was originally developed in a project using v2.1.5 so it should work for either. Please let me know if I've changed anything that breaks it for v2.1.5.


Instructions for use


1. Add the two files above to your project


2. In your AppDelegate class, include ScreenLog.h and initialize the global instance of the screenlog class somewhere in the startup sequence. I like to add it in the constructor:
1
2
3
4
5
6
  AppDelegate::AppDelegate() {
      g_screenLog = new ScreenLog();
      g_screenLog->setLevelMask( LL_DEBUG | LL_INFO | LL_WARNING | LL_ERROR | LL_FATAL );
      g_screenLog->setFontFile( "UbuntuMono-R.ttf" );
      g_screenLog->setTimeoutSeconds( 15 );
  }
The level mask is a bitmask specifying which types of log message should be output, and lets you turn subsets of logging on and off depending on your needs. The logging levels are completely arbitrary and you add more or change them to whatever you want. I usually have this set of levels:
1
2
3
4
5
6
  #define LL_FATAL     0x01
  #define LL_ERROR     0x02
  #define LL_WARNING   0x04
  #define LL_INFO      0x08
  #define LL_DEBUG     0x10
  #define LL_TRACE     0x20
Usually during development I will use the five levels shown in the code example above. Occasionally when I'm trying to trace the position of the program, I will also turn on the trace level which I usually use to log entry and exit of functions (see below for a useful tip on that). For release, the log level will typically be zero.

Depending on the device you are using, it may not be necessary to set a font file. If you want to log non-ascii characters you will need to use a font that supports them.

The timeout specifies how long log messages will be shown before disappearing.


3. Near the end of the AppDelegate::applicationDidFinishLaunching() function, attach the screenlog to the starting scene.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  bool AppDelegate::applicationDidFinishLaunching() {
  
      ... blah blah blah ...
      
      // create a scene. its an autorelease object
      auto scene = HelloWorld::scene();
  
      // attach screenlog to the scene about to become active
      g_screenLog->attachToScene( scene );
  
      // run
      director->runWithScene( scene );
  
      return true;
  }
The attachToScene function makes the screenlog attach itself to the given scene as a child (remember, ScreenLog is a sub-class of cocos2d::Layer). The default child level is 1000 which will probably be higher than any other layers you have in the scene, but if it's not, you can raise the value of SCREENLOG_LAYER_LEVEL in ScreenLog.cpp to make sure the screenlog layer will be the top-most.


4. Anywhere you want to log a message to the screen, include ScreenLog.h and call the log function like this:
1
  g_screenLog->log( LL_INFO, "Touch began: (%d,%d)", x, y );
The formatting of the output string is the same as for printf.

Here is an example of some log messages added to the init() function of the sample HelloWorld project, using this code:
1
2
3
4
  g_screenLog->log( LL_DEBUG, "A debug message... フォント" );
  g_screenLog->log( LL_INFO, "An info message... によって" );
  g_screenLog->log( LL_WARNING, "A warning message... 日本語も" );
  g_screenLog->log( LL_ERROR, "An error message... 表示可能" );
Cocos2d-x on-screen logging I found that on most devices, the Japanese text would show correctly with the default font (leave out the setFontFile call).


5. Important! When changing scenes, make sure you also tell the screenlog to attach itself to the new scene:
1
2
3
  auto scene = MyNextScreen::scene();
  g_screenLog->attachToScene( scene ); // important
  CCDirector::sharedDirector()->replaceScene( scene );
If you forget this, the screenlog will still exist and run, but you will not see it.


Altering text in existing messages


The 'log' function returns a reference to the message that is created, which you can use to change the text of that message later. This can be very useful for output which would normally spew out a lot of messages and make the log hard to follow. Examples of this would be a progress indicator, or fast/frequent updates like logging the position of touches as they move.

For example let's say I want to continously update a message to show the download progress of a file. I can keep a reference to the log message from when I first create it:
1
  screenLogMessage* slm = g_screenLog->log( LL_INFO, "Downloading file: 0% complete" );
... and use this later to change the text of the same log message instead of adding new ones:
1
  g_screenLog->setMessageText( slm, "Downloading file: %d% complete", progress );
When the text of a log message is updated like this, the timeout countdown for the message is also reset. If the message has already timed out and has been removed from the screenlog, nothing will happen.


Threading issues


In ScreenLog.cpp you will see a bunch of code to do with threads and locking which might seem unnecessary at first. The reason for that is because only the main thread of the application is able to correctly create the CCLabelTTF labels used for the log display. If any other thread tries this, the labels will not be created because the OpenGL rendering context is not current for that thread. To allow any other thread to call 'log' at any time, the actual label-creation part of the process is buffered until the main thread of the application can do it. This allows logging from other threads in situations where processing is being done concurrently, such as long loading routines, or network transfers as mentioned above.

If you didn't understand that previous paragraph, just know that you can call 'log' at any time from anywhere in your code without worrying about threading issues :)


Settings to adjust


Take a look at the beginning of ScreenLog.cpp if you want to adjust the number of lines shown on screen, or the starting position of the lowest row. I figure that most people will leave the framerate counter showing while developing, so the default position for the log rows to start is set to be a little above that.

In the function ScreenLog::createLabel you can set the colors used for each log level.


Other useful tips


Included in the header file you will find a small class that can be useful for logging the progress of the program through function calls. Click here to see the class code.)
1
2
3
4
5
6
7
8
9
10
11
  class ScopeLog {
  public:
      std::string m_functionName;
      ScopeLog(std::string fn) {
          m_functionName = fn;
          g_screenLog->log( LL_TRACE, "Entered %s", m_functionName.c_str() );
      }
      ~ScopeLog() {
          g_screenLog->log( LL_TRACE, "Exiting %s", m_functionName.c_str() );
      }
  };
This uses the scope timing of a class instance on the stack to log two messages. For example if we add one line each to the beginning of the scene() and init() functions of the HelloWorld class like this:
1
2
3
  Scene* HelloWorld::scene()
  {
      ScopeLog log(__PRETTY_FUNCTION__);
... and this:
1
2
3
  bool HelloWorld::init()
  {
      ScopeLog log(__PRETTY_FUNCTION__);
... we would get this result, illustrating how the init() takes place within the scene() function: Cocos2d-x on-screen logging