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:
- First create four Sprites with the images and adjust their scales to fit in the screen.
- Next, create a ParallaxNode instance.
- Add each Sprite to the ParallaxNode as a child node.
- Parent the ParallaxNode to the Scene.
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 ¶llaxRatio, const Vec2 &positionOffset);
- z is the depth of the layer.
- parallaxRario is a vector for horizontal and vertical motion ratios. The larger the number the faster it will move.
- positionOffset is the initial offset of the layer comparing to the parent.
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.
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.