Reza Ghobadinic

Parallax

Parallax effect divides the background into multiple images and move them differently depending on how far they should look to give a sense of depth. It is useful for plat-former or vertical scrolling games to make them look more interesting.

For the sake of this example, I found an image from the internet that I like and is suitable to use for this parallax effect.

After working a bit in Photoshop, I managed to separate and paint this image into four layers like this. Notice that I’ve made these images to be tile-able from sides.

Now that we have our four images, we can use them to create a parallax effect by putting them on top of each other and moving them with different speeds!

ParallaxNode Class

Cocos3dx has a built-in class called ParallaxNode that automatically takes care of things when you give it images with different speed and depth parameters.

Here is how we can create one:

I skip the header class ParallaxScene.h as there is nothing special about it. Here is what is inside the init() method of ParallaxScene.cpp:

bool ParallaxScene::init()
{
    if ( !Scene::init() )
    {
        return false;
    }

    // Max Speed
    m_speed = 200;

    // First get the screen coordinates
    auto visibleSize = Director::getInstance()->getVisibleSize();   // Screen's visible size
    Vec2 origin = Director::getInstance()->getVisibleOrigin();      // Screen's visible origin
    float x = origin.x + visibleSize.width * 0.5;                   // X for the center of the screen
    float y = origin.y + visibleSize.height * 0.5;                  // y for the center of the screen
    
    // Create one sprite for each parallax layer
    auto layer1 = Sprite::create("Layer1.png");
    auto layer2 = Sprite::create("Layer2.png");
    auto layer3 = Sprite::create("Layer3.png");
    auto layer4 = Sprite::create("Layer4.png");

    // Scale the sprites to fit the screen
    layer1->setScale(1.7);
    layer2->setScale(1.7);
    layer3->setScale(1.7);
    layer4->setScale(1.7);
    
    // Create a parallax node
    auto paraNode = ParallaxNode::create();
    
    // Add parallax layer sprites to the parallax node
    paraNode->addChild(layer1, 3, Vec2(4.0, 0), Vec2(x, y));
    paraNode->addChild(layer2, 2, Vec2(2.0, 0), Vec2(x, y));
    paraNode->addChild(layer3, 1, Vec2(1.0, 0), Vec2(x, y));
    paraNode->addChild(layer4, 0, Vec2(0.5, 0), Vec2(x, y));
    
    // Add parallax node to the scene
    this->addChild(paraNode, 0, 1);

    // Setup the accelerometer event handler
    Device::setAccelerometerEnabled(true);
    
    // The callback method
    function<void(Acceleration*, Event*)> callback = CC_CALLBACK_2(ParallaxScene::accellerometerCallback, this);
    auto listener = EventListenerAcceleration::create(callback); // Create an accelerometer listener
    _eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
    
    return true;
}

The addChild method:

paranode->addChild(Node *child, int z, const Vec2 &parallaxRatio, const Vec2 &positionOffset);

In the code, you see that as the depth becomes a bigger number (closer to the camera), I’ve given it a bigger horizontal motion ratio for a faster movement. parallaxRatio of 1 means the same speed as the parent node, 2 means twice the speed, etc…

At the end, to see the result of the Parallax, I activate the accelerometer to use the device x motion to move my parallax node to the left or right.

Here is my accellerometer callback method.

I get the accelerometer.x and position of the parallax node. We know that the accellerometer values are between -1.0 and 1.0 so to have a faster speeds than 1 or -1, I multiply it by my maximum speed value and add it to the current position of the node.

>void ParallaxScene::accellerometerCallback(Acceleration *acceleration, Event *event)
{
    // Get Parallax' position
    auto paraNode = this->getChildByTag(1);
    auto pos = paraNode->getPosition();
    
    // Get X Acceleration
    auto x = acceleration->x;
    
    // Calculate Parallax' new position
    pos.x += m_speed * x;
    
    // Make sure the sprites stay in the screen
    if(pos.x > 13)
        pos.x = 13;
    if(pos.x < -13)
        pos.x = -13;
    
    // Update Parallax' position
    paraNode->setPosition(pos);
}

Here is how the result looks like.

You may wonder why I have limited the pos.x to be limited between certain numbers!

That is because the default ParallaxNode in Cocos2dx does not loop the images automatically to create an infinite effect! Because of that, if we don’t stop it from moving too far from either side, the images will move out of the screen and all you will see is a black screen.

However, we can design our own ParallaxNode class to fix this problem.

Download Source

Custom ParallaxInfiniteNode Class

This part of the post will be quite code heavy, more so than usual.

To make an infinite parallax class that I will call ParallaxInfiniteNode, I inherit from the Node class and will write all of its functionality from scratch. There is nothing new in technical terms. You just need to understand the logic behind the code.

Each layer will need two sprites instead of one! That is because at any certain time, one sprite will be the main sprite and the other will be used to fill the empty area that the main can not cover!

In this class I will override setPosition and getPosition methods so that every time a new position is set, I can calculate how much the different child layers should move according to their parallax ratio. Unlike the standard Parallax Node that we had to create sprites first and then add them to it using the addChild method, mine will take image names and their parameter and it will create the sprites itself internally.

I will also need a container class to hold all the information for each new layer. Things like offset for position and scale, parallax ratio and those two Sprite pointers!

Starting by the container class in ParallaxInfiniteNode.h, this is what I came up with:

struct ParallaxLayerData{
    Vec2 ratio;
    Vec2 positionOffset;
    Vec2 scaleOffset;
    
    Sprite *spriteA;
    Sprite *spriteB;
    
    ParallaxLayerData(){
        spriteA = nullptr;
        spriteB = nullptr;
    };
    
    void swapSprites(void){
        Sprite *temp = spriteA;
        spriteA = spriteB;
        spriteB = temp;
    };
};

It has a swapSprites() method that simply swaps the pointers of those two sprites.

Then, I define my ParallaxInfiniteNode class as bellow:

class ParallaxInfiniteNode:public Node
{
    Size m_visibleSize;
    Vec2 m_origin;
    Vec2 m_position;
    Vec2 m_speed;
    
    map<const string, ParallaxLayerData> m_layerData;
    
public:
    static Node* createNode();
    virtual bool init();
    
    virtual void setPosition(const Vec2 &position);
    virtual const Vec2& getPosition() const;
    
    void addLayer(const string &fileName,
                  int z,
                  const Vec2& parallaxRatio,
                  const Vec2& positionOffset,
                  const Vec2& scaleOffset
                  );
    void update(float delta);
    
    CREATE_FUNC(ParallaxInfiniteNode);
};

In ParallaxInfiniteNode.cpp I define the functionalities.

The init() method only gets the screen size and the visible origin coordinate and starts the update() method.

bool ParallaxInfiniteNode::init()
{
    // Save the screen coordinates
    m_visibleSize = Director::getInstance()->getVisibleSize();   // Screen's visible size
    m_origin = Director::getInstance()->getVisibleOrigin();      // Screen's visible origin
    
    // Schedule Update
    scheduleUpdate();
    
    return true;
}

In the addLayer() method, We create two identical sprites and fill in the data for the container of that layer.

void ParallaxInfiniteNode::addLayer(const string &fileName,
                                    int z,
                                    const Vec2& parallaxRatio,
                                    const Vec2& positionOffset,
                                    const Vec2& scaleOffset
                                    )
{
    // Create Sprite A
    auto spriteA = Sprite::create(fileName);
    spriteA->setLocalZOrder(z);
    spriteA->setPosition(positionOffset.x, positionOffset.y);
    spriteA->setScale(scaleOffset.x, scaleOffset.y);
    this->addChild(spriteA);
    
    // Create Sprite B
    auto spriteB = Sprite::create(fileName);
    spriteB->setLocalZOrder(z);
    spriteB->setPosition(positionOffset.y , positionOffset.y);
    spriteB->setScale(scaleOffset.x, scaleOffset.y);
    this->addChild(spriteB);
    
    // Create Layer Data and save the information
    ParallaxLayerData data;
    data.ratio = parallaxRatio;
    data.positionOffset = positionOffset;
    data.scaleOffset = scaleOffset;
    data.spriteA = spriteA;
    data.spriteB = spriteB;
    m_layerData[fileName] = data;
}

The getPosition() and setPosition() are redefined to update the position member variable, but not actually moving the node itself.

// This doesn't actually change the position of the node!
// It just records where it should have been, so that it can move the layers accordingly
void ParallaxInfiniteNode::setPosition(const Vec2 &position)
{
    m_speed = position - m_position; // Calculate the amount of movement
    m_position = position;           // Update the position to the current position
}

const Vec2& ParallaxInfiniteNode::getPosition() const
{
    return m_position;               // Return the current position
}

The update() method does the bulk of the work.

// On every frame, calculate the position of A and B sprites for each layer
void ParallaxInfiniteNode::update(float delta)
{
    // Loop over all the layers
    for(auto &layer: m_layerData)
    {
        // Calculate speed for this layer considering the movement ratio
        Vec2 speed;
        speed.x = m_speed.x * layer.second.ratio.x * delta;
        speed.y = m_speed.y * layer.second.ratio.y * delta;
        
        // Calculate and update the position for sprite A (master)
        auto posA = layer.second.spriteA->getPosition();
        posA += speed;
        layer.second.spriteA->setPosition(posA);
        
        // Calculate the position of Sprite B relative to sprite A
        if(posA.x <= m_visibleSize.width * 0.5) // If sprite A is on the left side of the screen
        {
            // Place the slave sprite on the right
            // -1 overlap is to cover any possible gaps between the two layers
            auto x = posA.x + layer.second.spriteA->getContentSize().width * layer.second.scaleOffset.x - 1;
            auto y = posA.y;
            layer.second.spriteB->setPosition(x, y);
        }
        else // If sprite A is on the right side of the screen
        {
            // Place the slave sprite on the left
            // +1 overlap is to cover any possible gaps between the two layers
            auto x = posA.x - layer.second.spriteA->getContentSize().width * layer.second.scaleOffset.x + 1;
            auto y = posA.y;
            layer.second.spriteB->setPosition(x, y);
        }
        
        // The sprite that is covering most of the screen should become sprite A
        auto limitL = (m_visibleSize.width - layer.second.spriteA->getContentSize().width * layer.second.scaleOffset.x) * 0.5;
        auto limitR = (m_visibleSize.width + layer.second.spriteA->getContentSize().width * layer.second.scaleOffset.x) * 0.5;
        
        // If the master sprite (A) goes beyond the limit, swap sprite
        if(posA.x < limitL || posA.x > limitR)
        {
            //m_layerData[layer.first].swapSprites();
            layer.second.swapSprites();
        }
    }
}

Using our ParallaxInfiniteClass makes the init() method of the ParallaxScene a bit shorter too.

bool ParallaxScene::init()
{
    if ( !Scene::init() )
    {
        return false;
    }
    
    // Max Speed
    m_speed = 200;
    
    // Screen coordinates
    auto visibleSize = Director::getInstance()->getVisibleSize();   // Screen's visible size
    Vec2 origin = Director::getInstance()->getVisibleOrigin();      // Screen's visible origin
    float x = origin.x + visibleSize.width * 0.5;                   // X for the center of the screen
    float y = origin.y + visibleSize.height * 0.5;                  // y for the center of the screen
        
    // Create a parallax node
    auto paraNode = ParallaxInfiniteNode::create();
    
    // Add parallax layer sprites to the parallax node
    paraNode->addLayer("Layer1.png", 3, Vec2(4.0, 0), Vec2(x, y), Vec2(1.0, 1.0));
    paraNode->addLayer("Layer2.png", 2, Vec2(2.0, 0), Vec2(x, y), Vec2(1.0, 1.0));
    paraNode->addLayer("Layer3.png", 1, Vec2(1.0, 0), Vec2(x, y), Vec2(1.0, 1.0));
    paraNode->addLayer("Layer4.png", 0, Vec2(0.5, 0), Vec2(x, y), Vec2(1.0, 1.0));
    
    // Add parallax node to the scene
    this->addChild(paraNode, 0, 1);
    
    // Setup the accelerometer event handler
    Device::setAccelerometerEnabled(true);
    
    // The callback method
    function<void(Acceleration*, Event*)> callback = CC_CALLBACK_2(ParallaxScene::accellerometerCallback, this);
    auto listener = EventListenerAcceleration::create(callback); // Create an accelerometer listener
    _eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
    
    scheduleUpdate();
    
    return true;
}

The result will look like as if it is a never ending background.

Download Source

Exit mobile version