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.
Debug mode wax says
Useful information. Fortunate me I found your web site unintentionally, and I’m surprised why this coincidence did not took place earlier!
I bookmarked it.
hustisya essay says
There’s definately a lot to find out about this issue.
I really like all the points you’ve made.
Jens Winslow says
A great blog on your experience and adventures learning Cocos2d-x.
I find it astounding how similar our paths have been (down to git and choosing storage and backup, setting up Cocos, doing scenes and sprites, etc.). Our journeys have been almost identical.
I have not yet done particles or parallax, but I have done some physics with the Chipmunk engine and created a labeled edit text class for simple text or number entry with a label (combining two labels and an edit text field with optional number filter). I.e My Number: [ -0.23]
I like a N-Tier Presentation-Logic-Data design in my programs, but I have not determined how to do so in a Cocos app yet – the 3 seem very tightly coupled in Cocos. Do you have any thoughts on how to decouple and combine with game loop?
Good luck, I look forward to see what else you learn about Cocos I may have missed.
Reza Ghobadinic says
Hi jens,
Thanks, it is good to see that people actually read my posts. Although I haven’t had time to publish a new blog since Covid started.
I think the next for me should be looking at internal Physics and then Chipmunk and Box2D and compare their speed, features, etc…
For separating the Logic, View and Data, you are right about not being easy to separate. I usually create the data classes for my characters separately and make sure from the inside of my game characters to just keep a pointer to those and not have their own member variables as much as possible. This way, you can easily serialize the data out or in if you want to.
Cheers,