Cocos2D-X Tutorial: Making a Universal App: Part 1

Yiming Guo
Whack this Mole!

Whack this Mole!

Note from Ray: We ported this tutorial from Cocos2D to Sprite Kit as part of the iOS 7 Feast, but didn’t want to leave Cocos2D-X fans behind. So special guest Yiming Guo has ported this tutorial to Cocos2D-X as well – enjoy! :]

This tutorial was originally about how to create a mole whacking game with Cocos2D-iPhone, so why bother making a Cocos2D-X version? We thought this was worth doing for three reasons:

  1. In this tutorial you can review how to make an entire game with Cocos2D-X from scratch.
  2. It will be a good opportunity to learn a new and tough topic: how to make a game that supports multiple resolutions and aspect ratios.
  3. Cocos2D-X is now a very popular game engine (especially in China), which is designed specifically with multi-platform gaming in mind (unlike Cocos2D-iPhone or Android, which are iOS specific). So making this tutorial in Cocos2D-X is a particularly good match!

This Cocos2D-X tutorial builds on our other Cocos2D-X tutorials:

If you have not reviewed these Cocos2D-X tutorials already (or have similar knowledge), I recommend you go through them first.

This is a two-part Cocos2D-X tutorial series. In this first part, you’ll create the basics of the game – cute little moles popping out of holes. You’ll spend a lot of time thinking about how to organize the art and coordinates so that the game looks good on the iPhone, iPad, and Retina Display – and be efficient too!

Planning the Art: Overview

Since you want this game to work on every iPhone, iPad and also on Android devices, you need to take some time to figure out how to deal with the art before you get started.

To understand how to property size and set up the art, you need to learn two topics first:

  • Retina Display and Cocos2D-X
  • iPhone, iPad and Android Aspect Ratio

So let’s get started!

Retina Display and Cocos2D-X

Good news that the latest version 2.14 of Cocos2D-X has a perfect solution for the multi-resolution, so you can make it easier than before.

  1. Since Cocos2D-X version 2.04 the engine default enables support for Retina Display. You just need to initiate your resources in your app delegate.
  2. Add high definition sprites to your app, but instead of using the @2x extension or an -hd extension, you just need to put all your high definition sources in a folder named hd. When loading your sprites, according to the screen pixel, Cocos2D-X will load the hd images from this folder on the Retina Display.
  3. Now you can use points instead of pixels when positioning your sprites in Cocos2D-X.

When the rubber hits the road, the easiest thing to do is to have your artist make images at the highest-necessary resolution (i.e. the 2x size for the Retina Display), and you can easily scale down the images for the normal iPhone yourself from there.

You might wonder why even bother having two different sized images – why not just always load the bigger image and just scale it programmatically? Well, loading textures into memory is one of the most memory intensive aspects of an app, so if you’re running on a device that isn’t going to take advantage of the higher resolution images, it’s a big saving to load the smaller images that are intended for the device.

But don’t worry – you don’t need to be constantly scaling images down in Photoshop. TexturePacker actually has a nice feature that makes it easy to create scaled down images given a full-resolution image, and that’s what you’ll be using in this Cocos2D-X tutorial.

iPhone, iPad and Android Aspect Ratio

Dealing with Retina Display is easy now, but how about the iPad and Android?

In this tutorial you’ll just focus on iOS devices and, if you know how to deal with iOS devices, you can also handle the Android situation.

Well, it turns out that there is a very annoying thing about making a game that works on both the iPhone and the iPad – the aspect ratio between the devices is different!

The iPhone is 480×320, 960×640 or 1136×640 – a 1.5 aspect ratio or a 1.75 aspect ratio. However, the iPad is 768×1024 or 1536×2048 – a 1.33 aspect ratio.

This means that if you have an image that fills up the entire background of the iPad and want to re-use it for the iPhone too, it’s not going to fit exactly. Suppose you scale it down so it fits the width of the iPhone (multiply by 0.9375): you’ll get 720×960, and there will be extra stuff to the side that will get cut off!

Aspect Ratio of iPhone vs. iPad

This makes things kind of annoying, because not only do you run into problems with background images, but the aspect ratio also makes it difficult to use the same coordinates across devices.

There are several strategies for how to deal with this, here are a few developers always use:

  • Have a “playable area” in the middle of the screen that is the size of the iPhone retina display (640×960). This will leave a little extra are around the edges – you can just cover that with a background and the user probably won’t even notice. This allows you to easily convert coordinates between devices and re-use art (high res used on iPad and retina, and normal res used on normal iPhone or normal iPad). This is what you’ll be doing in this game tutorial.
  • You can make the iPad have a similar aspect ratio to the iPhone if you take 42 pixel gutters on each side of the iPad screen, and put the “main content” inside as 684×1024. If you make your content to fit within the 684×1024 box, you can scale down images for each device from there.
  • You could have different images for the iPhone, iPad, and Retina Display (i.e. 3 sets) and different coordinates too. This allows maximum flexibility, but larger binary sizes and having to redo the positions of objects on different devices.

One and another complication is Cocos2D-X can’t load images in the hd folder automatically, so hard-coding can’t work, etc. That is up to you!

Planning the Art: Conclusion

OK, so based on the above discussion, here is the plan for this Cocos2D-X tutorial.

  • The art has been designed to be within a 960×640 playable area, used full-screen on retina-display iPhones, and centered in the iPad screen.
  • The art will then be scaled by TexturePacker to be 1/2 the size for normal iPhones.
  • The full sized-art will be added in a folder with name hd, and the half size in the sd.
  • Backgrounds are a special case because they need to be fullscreen always. The backgrounds will be made to the 1024×768 size (iPad size) so the entire screen is filled. The same images will actually be used on the iPhone too since it’s close enough. Some of the background will be offscreen, but that doesn’t matter for this particular background.
  • The iPad version will contain code to use the hd images, convert coordinates to inside the playable area, use the appropriate radio, etc.

Go ahead and download the art for this Cocos2D-X tutorial, made by this lovely lady. Unzip the file and take a look at how things are set up:

  • In the foreground folder, the foreground is 1024×768 (the size of the iPad), but it is actually split into two parts: the lower part, and the upper part. It’s split into two parts so you can place the mole in-between the lower and upper parts, to make him look like he’s going underground.
  • In the background folder, the background has the 1.33 aspect ratio of the iPad, but is actually half sized (512×384). This is becase the background barely shows (just through the three mole holes), so it’s not worth the cost of a large 1024×1024 texture load. Instead a small texture is loaded and scaled up.
  • In the sprites folder, all sprites were sized to fit nicely within the 960×640 playable area. Note there’s a mole, and two animations for him (the mole laughing, and the mole being hit).

Ok – enough background info – it’s time to get started!

Let’s get started

Launch Xcode, select File\New\Project… and create a new project with the iOS\cocos2d-x\cocos2dx template, then click Next. Name the project WhackAMole, set the Device family as Universal, and then save the project.

figure1

Next, take the file that you just downloaded and unzip it into your WhackAMole project directory with Finder. It should be a sibling of the Classes folder.

figure2

Next, make sure you have TexturePacker installed and ready to go on your machine. If you don’t have it already or know how to use it, check out this tutorial for more information.

You will now set up TexturePacker to create the sprite sheets you’ll need for this project. You’ll be doing everything by TexturePacker‘s command line tools and Xcode integration, so no need to use the TexturePacker GUI at all!

Now choose the Resources folder, press Command+N, choose OS X\Other\Shell Script, and click Next. Name the file PackTexures.sh, and click Create.

Then replace the contents of PackTextures.sh with the following:

#!/bin/sh
TP="/usr/local/bin/TexturePacker"
if [ "${ACTION}" = "clean" ]
then
echo "cleaning..."
rm Resources/hd/background*
rm Resources/hd/foreground*
rm Resources/hd/sprites*
rm Resources/sd/background*
rm Resources/sd/foreground*
rm Resources/sd/sprites*
else
echo "building..."

${TP} --smart-update \
--format cocos2d \
--data Resources/hd/background.plist \
--sheet Resources/hd/background.pvr.ccz \
--dither-fs \
--opt RGB565 \
Art/background/*.png
${TP} --smart-update \
--format cocos2d \
--data Resources/sd/background.plist \
--sheet Resources/sd/background.pvr.ccz \
--dither-fs \
--scale 0.5 \
--opt RGB565 \
Art/background/*.png
${TP} --smart-update \
--format cocos2d \
--data Resources/hd/foreground.plist \
--sheet Resources/hd/foreground.pvr.ccz \
--dither-fs-alpha \
--opt RGBA4444 \
Art/foreground/*.png
${TP} --smart-update \
--format cocos2d \
--data Resources/sd/foreground.plist \
--sheet Resources/sd/foreground.pvr.ccz \
--dither-fs-alpha \
--scale 0.5 \
--opt RGBA4444 \
Art/foreground/*.png
${TP} --smart-update \
--format cocos2d \
--data Resources/hd/sprites.plist \
--sheet Resources/hd/sprites.pvr.ccz \
--dither-fs-alpha \
--opt RGBA4444 \
Art/sprites/*.png
${TP} --smart-update \
--format cocos2d \
--data Resources/sd/sprites.plist \
--sheet Resources/sd/sprites.pvr.ccz \
--dither-fs-alpha \
--scale 0.5 \
--opt RGBA4444 \
Art/sprites/*.png
fi
exit 0

Then move PackTextures.sh to the file path.

figure3

And don’t forget to move the PackTextures.sh reference of the file in the project.

figure4

This script runs TexturePacker to create a sprite sheets for the background image, the foreground images, and the sprite images – an HD and regular-quality image for each.

Note that each image is saved in the pvr.ccz format since it is the most efficient in terms of memory and disk space usage. Also the pixel format and dithering options were chosen to get a good tradeoff of quality and memory usage for each set of images.

If you’re unsure what the TexturePacker options do, load up Terminal and run TexturePacker –help to get a full description of each option.

Next, you need to set up your project to run this shell script when you compile. Now choose File\New\Target…, and choose OS X\Other\External Build System, and click Next. Name the Target TexturePacker, and click Finish.

Then select TexturePacker target and set up the settings as follows:

figure5

The final step is to set this target as a dependency of your app. Click on WhackAMole target right above the TexturePacker target, go to the Build Phases, click the + button in Target Dependencies, choose TexturePacker from the list, and click Add.

figure6

Compile your app, and you should see the output from TexturePacker from your build results if everything is working OK and then go to the Finder you’ll find two new folders hd and sd added to your Resources folder.

Add them to your project as below (make sure Create folder references for any added folders is checked).

figure7

Add the Art folder to the project.

addingArt
One more thing before writing some code, copy Default-568h@2x.png into your Resources folder and add it to your project as below, otherwise your project can not support iPhone5!!

addingDefault2x

Setting the Background

Next, open up HelloWorldScene.cpp and find the init function. Remove the line codes between “return false” and “return true”. Then go to the AppDelegate.cpp and find the applicationDidFinishLaunching function. Add the following code lines below the

pDirector->setOpenGLView(CCEGLView::sharedOpenGLView());

as follow:

CCSize screenSize = CCEGLView::sharedOpenGLView()->getFrameSize();
CCFileUtils *pFileUtils = CCFileUtils::sharedFileUtils();
std::vector<std::string> searchPaths;
    
CCSize designSize = CCSizeMake(480, 320);
CCSize resourceSize;
    
// if the device is iPad
if (screenSize.height >= 768) {
    searchPaths.push_back("hd");
    searchPaths.push_back("sd");
        
    resourceSize = CCSizeMake(1024, 768);
    designSize = CCSizeMake(1024, 768);
}
// if the device is iPhone
else{
    // for retina iPhone
    if (screenSize.height > 320) {
        searchPaths.push_back("hd");
        searchPaths.push_back("sd");
        resourceSize = CCSizeMake(960, 640);          
    }
    else{
        searchPaths.push_back("sd");
        resourceSize = CCSizeMake(480, 320);
    }
}
searchPaths.push_back("WhackAMoleSounds");
pFileUtils->setSearchPaths(searchPaths);
pDirector->setContentScaleFactor(resourceSize.width / designSize.width);
        
CCEGLView::sharedOpenGLView()->setDesignResolutionSize(designSize.width, designSize.height, kResolutionFixedWidth);

This block is how you deal with the multi-resolution. You always use three sizes in Cocos2D-X to solve this kind of problem: resourceSize is the art size, designSize is what you use to adapt different screen and finally the screenSize is the size you can see in the screen. With the help of the setContentScaleFactor and setDesignResolution functions you can deal with all kind of screens.

Then go to the HelloWorldScene.h add

USING_NS_CC;

under the #include section and add

CCSize _winSize;

as a private variable of HelloWorldScene.

Now back to HelloWorldScene.cpp, find the method init and add the code block between “return false” and “return true” as follow:

_winSize = CCDirector::sharedDirector()->getWinSize();
    
    // Load background
    CCSpriteFrameCache::sharedSpriteFrameCache()->addSpriteFramesWithFile("background.plist");

    // Load foreround
    CCSpriteFrameCache::sharedSpriteFrameCache()->addSpriteFramesWithFile("foreground.plist");

    // Add background
    CCSprite *dirt = CCSprite::createWithSpriteFrameName("bg_dirt.png");
    dirt->setScale(2.0);
    dirt->setPosition(ccp(_winSize.width * 0.5, _winSize.height * 0.5));
    this->addChild(dirt, -2);
    
    // Add foreground
    CCSprite *lower = CCSprite::createWithSpriteFrameName("grass_lower.png");
    lower->setAnchorPoint(ccp(0.5, 1));
    lower->setPosition(ccp(_winSize.width * 0.5, _winSize.height * 0.5));
    lower->getTexture()->setAliasTexParameters();
    this->addChild(lower, 1);
    
    CCSprite *upper = CCSprite::createWithSpriteFrameName("grass_upper.png");
    upper->setAnchorPoint(ccp(0.5, 0));
    upper->setPosition(ccp(_winSize.width * 0.5, _winSize.height * 0.5));
    upper->getTexture()->setAliasTexParameters();
    this->addChild(upper, -1);
    
    // Add more here later...

OK let’s go over this section by section, since there is a good amount of new material here.

  • Determine names of sprite sheets and plists to load. This section lists the names of the sprite sheets and plists generated by TexturePacker to be loaded. Note that Cocos2D-X will not automatically choose between the “hd” versions and the normal versions. You need to choose which version to be loaded by yourself according the screen pixel of the devices.
  • Load background and foreground. The next step is to load the information about each sprite for the background and foreground into the CCSpriteFrameCache so that they can be used later. Note that these sprites won’t actually be added to a CCSpriteBatchNode anywhere – since these images are just used once each it would be kind of pointless.
  • Add background. The background image is added as a child of the layer (with a zOrder of -2 so it appears beneath everything else) next. It scales the image by 2 because you made it smaller on purpose to conserve space, and centers the image. You may find that there is a black line on the middle of the screen if your code run on iPhone5 without this code line:
    setAliasTexParameters();
  • Add foreground. The foreground is added in two parts. As an easy way to place the image, it sets the anchor point to the middle/bottom for the top image, and the middle/top for the bottom image, and matches that anchor point up to the center of the screen. That way you don’t have to do any complicated math, and it shows up in the right place on all devices. Note that part of the background will be off-screen for iPhones, but that is OK for this background and barely even noticeable. Also note that the images are added with different zOrder values, so the lower image appears on top.

Compile and Run the code, and you should now see the background and foreground on the screen! Give it a try on both the iPhone and iPad simulators to make sure that it appears OK on both devices.

figure8

Placing the Moles

For this game, you’re going to add three moles to the scene – one for each hole. The moles will usually be “underground” beneath the lower part of the grass – but occasionally they will “pop up” so you can try to whack them.

First, let’s add the moles to the level underneath each of the holes. You’ll temporarily make them appear above all the other art so you can make sure they’re in the right spot, then you’ll put them underground once you’re happy with their position.

Open up HelloWorldScene.h and add an array as private variable of HelloWorldScene to keep track of the moles in the level, as shown below:

CCArray *_moles;

By storing the moles in this array, it will make it easy to loop through each of the moles later on.

Before you go to the next step, you have to create our own Sprite Class, press Command+N choose OS X/C and C++/C++ Class and name it GameSprite. Add the below codes to GameSprite.h as below

#include <iostream>
#include "cocos2d.h"

using namespace cocos2d;

class GameSprite : public CCSprite {
    CCSize _screenSize;
    bool userData;

public:
    GameSprite();
    ~GameSprite();
    
    static GameSprite* gameSpriteWithFile(const char *pszFileName);
    void setUserData(GameSprite *mole, bool flag);
    bool getUserData(GameSprite *mole);
};

Now add codes to the GameSprite.cpp as below

#include "GameSprite.h"

GameSprite::GameSprite(){
    
}

GameSprite::~GameSprite(){
    
}

GameSprite* GameSprite::gameSpriteWithFile(const char *pszFileName){
    GameSprite *sprite = new GameSprite();
    if (sprite && sprite->initWithSpriteFrameName(pszFileName)) {
        sprite->autorelease();
        return sprite;
    }
    CC_SAFE_DELETE(sprite);
    return NULL;
}

void GameSprite::setUserData(GameSprite *mole, bool flag){
    mole->userData = flag;
}

bool GameSprite::getUserData(GameSprite *mole){
 return mole->userData;
}

Next, in HelloWorldScene.h include GameSprite.h

#include "GameSprite.h"

and in HelloWorldScene.cpp add the code to place the moles at the end of your init function before return, as shown below:

// load sprites
CCSpriteBatchNode *spriteNode = CCSpriteBatchNode::create("sprites.pvr.ccz");
this->addChild(spriteNode, 999);
CCSpriteFrameCache::sharedSpriteFrameCache()->addSpriteFramesWithFile("sprites.plist");
     
float offset = 155;
float startPoint = 85 + offset;
CCSize frameSize = CCEGLView::sharedOpenGLView()->getFrameSize();
if ( fabs(frameSize.width - 1024) < FLT_EPSILON) {
    offset = offset * (1024 / 480.0);
    startPoint = startPoint * (1024 / 480.0) ;
}
    
// Add mole2
GameSprite *mole2 = GameSprite::gameSpriteWithFile("mole_1.png");
mole2->setPosition(HelloWorld::convertPoint(ccp(startPoint, 85)));
spriteNode->addChild(mole2);
    
// Add mole1
GameSprite *mole1 = GameSprite::gameSpriteWithFile("mole_1.png");
mole1->setPosition(HelloWorld::convertPoint(ccpSub(mole2->getPosition(), ccp(offset, mole2->getPositionY() - 85))));
spriteNode->addChild(mole1);
       
// Add mole3
GameSprite *mole3 = GameSprite::gameSpriteWithFile("mole_1.png");
mole3->setPosition(HelloWorld::convertPoint(ccpAdd(mole2->getPosition(), ccp(offset, 85 - mole2->getPositionY()))));
spriteNode->addChild(mole3);
_moles = CCArray::create(mole1, mole2, mole3, NULL);
_moles->retain();

This first creates a CCSpriteBatchNode for the sprites, so that drawing the three moles is done more efficiently, and adds it as a child of the layer. Note it’s setting the zOrder value to 999 temporarily, so that the moles appear on top so you can make sure they’re set up OK.

It then loads all of the sprite frames from the property list to the cache, so they can be pulled out later.

Then it goes through and creates a sprite for each mole, places them in the scene, and adds them to the list of moles. Note the coordinate for each mole is within the 480×320 “playable area” of the game (the size of the iPhone). For the iPad, these points will need to be converted, so you don’t use hard-coding. And following function is how you deal with multi-resolution.

Add the following method right above the init function:

CCPoint HelloWorld::convertPoint(CCPoint point){
    CCSize frameSize = CCEGLView::sharedOpenGLView()->getFrameSize();
    if ( fabs(frameSize.width - 1024) < FLT_EPSILON) {
        return ccp(0.9 * point.x + 47, point.y + 100);
    }
    if (fabs(frameSize.width - 1136) < FLT_EPSILON) {
        return ccp(point.x, point.y - 18);
    }
    return point;
}

and declare it under the public section of HelloWorldScene.h

CCPoint convertPoint(CCPoint point);

This function converts a point in the "playable area" to the appropriate screen position on the iPad. Remember that:

  • You're using the HD graphics on the iPad, so all points are doubled.
  • You're centering that 960x640 area in the 1024x968 iPad screen, so that leaves 32 pixel margins on the left and right, and 64 pixel margins on the top and bottom.

So this method simply does that math to give the right position on the iPad.

One more thing - before you forget, add the following lines to clean up the memory you allocated for the moles array in the destructor:

HelloWorld::~HelloWorld()
{
    CC_SAFE_RELEASE_NULL(_moles);
}

and don't forget to add the destructor declaration in HelloWorldScene.h

~HelloWorld();

Compile and Run your code, and you should see the three moles happily in the scene at the correct spots! You should try the code on the iPhone, iPhone Retina, and iPad to make sure that they're in the right spot on each device.

figure9

Popping the Moles

Now that you're sure the moles are in the right place, let's add the code to make them pop out of their holes.

First things first - switch the zOrder value of 999 for the mole sprite sheet back to 0 so the moles are underground.

Once that's done, add the following line of code to the bottom of your init function:

this->schedule(schedule_selector(HelloWorld::tryPopMoles), 0.5);

If you haven't seen this before, you can run the schedule function on a node to tell Cocos2D-X to call another function every so many seconds. In this case, you want to try popping some moles out of their holes every 1/2 second.

Next, add the implementation of tryPopMoles:

void HelloWorld::tryPopMoles(CCTime dt){
    GameSprite *mole;
    for (int i = 0; i < 3; i++) {
        mole = (GameSprite *)_moles->objectAtIndex(i);
        if (arc4random() % 3 == 0) {
            if (mole->numberOfRunningActions() == 0) {
                this->popMole(mole);
            }
        }
    }
}

This function will be called every 1/2 second, and each time it will loop through each mole and give it a 1 in 3 chance of popping out of its hole. But it will only pop out if it isn't moving already - and one easy way to check for this is to see if the number running actions is 0.

Finally, add the implementation of popMole:

void HelloWorld::popMole(GameSprite *mole){
    CCMoveBy *moveUp = CCMoveBy::create(0.2, ccp(0, _winSize.height * 0.25));
    CCEaseInOut *easeMoveUp = CCEaseInOut::create(moveUp, 3.0);
    CCDelayTime *delay = CCDelayTime::create(0.5);
    CCAction *easeMoveDown = easeMoveUp->reverse();
    mole->runAction(CCSequence::create(easeMoveUp, delay, easeMoveDown, NULL));
}

And don’t forget to add the interfaces in the head file:

class HelloWorld : public cocos2d::CCLayer
{
private:
    CCSize          _winSize;
    CCArray*        _moles;

public:
    ~HelloWorld();
    
    // Method 'init' in cocos2d-x returns bool, instead of 'id' in cocos2d-iphone (an object pointer)
    virtual bool init();

    // there's no 'id' in cpp, so we recommend to return the class instance pointer
    static cocos2d::CCScene* scene();
    
    // a selector callback
    void menuCloseCallback(CCObject* pSender);

    CCPoint convertPoint(CCPoint point);
    
    void tryPopMoles(CCTime dt);
    void popMole(GameSprite *mole);
        
    // preprocessor macro for "static create()" constructor ( node() deprecated )
    CREATE_FUNC(HelloWorld);
};

This code uses some Cocos2D-X actions to make the mole pop out of its hole, pause for half a second, then pop back down. Let’s go through this line-by-line to make sure you’re on the same page:

  1. Creates an action to move the mole move up along the Y axis as much as the mole is tall. Since you placed the mole right below the hole, it will look right.
  2. To make the movement look more natural, it wraps the move action with a CCEaseInOut action. This causes the action to go slower at the beginning and end, as if the mole is accelerating/decelerating, as it naturally would.
  3. To create an action to move the mole move back down again, an easy way is to call the reverse function on an action, which will give its opposite.
  4. Creates an action to pause for one second after the mole pops out.
  5. Now that the actions are ready to go, it runs them on the mole in a sequence: move up, delay, and finally move down. Note it has to terminate the sequence with a nil to show it’s done.

That's it! Compile and Run the code, and you'll see the moles happily popping out of their holes!

figure10

Where To Go From Here?

Here is a sample project with all of the code we’ve developed so far in this Cocos2D tutorial series.

Next check out Part 2, where you'll add some cute animations to the mole as he laughs and gets whacked, add gameplay so you can do the whacking and earn points, and of course add some gratuitous sound effects as usual.

Please add a comment below in if you have any thoughts, advice, or suggestions for future tutorials!

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 20 total!

Swift Team

... 15 total!

iOS Team

... 44 total!

Android Team

... 14 total!

macOS Team

... 11 total!

Unity Team

... 11 total!

Articles Team

... 12 total!

Resident Authors Team

... 17 total!