Cocos2d-JS Tutorial – Chalk Board 3 – Drawing Grid Lines

Objective

In Continuation of our last Chalk Board Erasing post, let’s see how to automatically draw Grid lines in this post.

ENGINE VERSION : COCOS2D-JS 3.8.1

Preview

Contents

  1. Prerequisite
  2. Download
  3. Draw a Line
  4. Draw smoother Line
  5. Draw vertical Lines
  6. Draw Grid
  7. Center in Board
  8. Complete Board.js Code

At any point of time, refer to the source files Board.jsmain.js and resource.js for clarity.

1. Prerequisite

See this link to get started with Cocos2d-x.

2. Download Code

Download project @ https://github.com/GethuGames/cocos2dx-chalk-board-draw/archive/grid1.0.zip Complete Project can be found in GITHUB.

Note: This is not directly executable. This contains the source and resources excluding the cocos2dx bundle. So, create a new project, download this github and replace the files in ur new project with this github files to be able to run the project.

3. Draw a Line

Create a class variable `currentLinePercent` and 2 functions, `drawBoard` and `updateDrawLine`.

  1. currentLinePercent:The line will be drawn by a series of dots and this variable determines where the dot should be drawn at any given time. currentLinePercent will start at 1 and gradually decreased to 0 over time in the `update` function.
  2. drawBoard: This function will be invoked from the Class constructor. This will just schedule the other function `updateDrawLine` that has the core logic.
  3. updateDrawLine: This will be invoked in every game cycle. This function will decrement currentLinePercent by a small amount, then calculate the point to be drawn, and call `drawBrushAtPoint()` at that point. This will be repeated till currentLinePercent reaches 0, and hence the line touches the bottom of the screen.
    drawBoard : function() {
        this.currentLinePercent = 1.0;
        this.schedule(this.updateDrawLine);
    },

    updateDrawLine : function(dt) {
        this.currentLinePercent -=  0.02;
        to = cc.p(cc.winSize.width * 0.5, this.currentLinePercent * cc.winSize.height);
        this.drawBrushAtPoint(to, cc.color(250, 250, 50, 200));

        if (this.currentLinePercent < 0.0) {
            this.unschedule(this.updateDrawLine);
        }
    },
  1. Schedule the `updateDrawLine` to be invoked on every game loop.
  2. Line’s Y position is calculated from currentLinePercent,
    1. which is set to 1 in the beginning (= max height, top of the screen)
    2. and decremented to 0 over time (bottom of the screen)
  3. Point to draw is calculated as follows:
    1. X position: the center of the screen
    2. Y position: currentLinePercent * window height
  4. When the currentLinePercent reaches 0, which is the bottom of the screen, unschedule `updateDrawLine` and stop Drawing.

As with the free hand drawing in Part 1, here also, we get gaps between the points. We can interpolate the intermediate points as before. But just for the sake learning/exploration, let’s take another technique.

Line draw - First

4. Draw smoother Line

Since we know the lines are perfectly vertical, we can generate fixed number of points above and below the given point and draw them all. So, `drawBrustAtPoint()` changes to

    drawBrushAtPoint : function(pt, color, density) {
        this.renderTex.begin();
        for (var i = 0; i < density; i++) {
            this.chalkBrush = cc.Sprite.create(res.chalkBrush_png);
            this.chalkBrush.setRotation(Math.random() * 180);
            this.chalkBrush.setPosition(cc.p(pt.x, pt.y + 5 * (i - density * 0.5)));
            this.chalkBrush.setColor(color);
            this.chalkBrush.setScale(1.5);
            this.chalkBrush.visit();
        }
        this.renderTex.end();

        this.chalkSprite.setPosition(cc.p(pt.x - this.chalkSprite.getContentSize().width * 0.3,
            pt.y - this.chalkSprite.getContentSize().height * 0.2));
    }

We introduced a new parameter `density` and a `for` loop in the function.

  • Density determines how many points are drawn above and below to the given point. If density = 12, 6 points are drawn above and 6 points drawn below the given point.
  • For loop iterates `density` times, generating points with a gap of 5 pixels from each other.

Then in `updateDrawLine()`, change

this.drawBrushAtPoint(to, cc.color(250, 250, 50, 200));

to

this.drawBrushAtPoint(to, cc.color(250, 250, 50, 200), 16);

which means, we now draw 8 points above and below the given point `to`.

This should give us a smooth continuous line as this:

Line draw - Second

5. Draw vertical lines

We mastered drawing a single vertical line. Now let’s draw multiple vertical lines of the grid. Before starting to draw the lines, let’s create a `Config` object to make certain things simpler and configurable.

var Config = {};

Add this as the first line before the `Board` declaration. Then in the Board constructor, specify the properties of Config as follows:

Config.colCount         =   5;  // Number of columns in the grid
Config.rowCount         =   5;  // Number of rows in the grid
Config.drawSpeed        =   10; // Speed of Chalk Drawing
Config.lineColor        =   cc.color(250, 250, 50, 200); // Grid color

We dont want to draw in the full Board. But rather, let’s draw a square shapred box. To achieve that, add these 2 lines of code in the Board constructor:

this.boardSpace         =   cc.size(size.width * 0.75, size.width * 0.75);
this.cellSize           =   cc.size(this.boardSpace.width / Config.colCount, this.boardSpace.height / Config.rowCount);
  • boardSpace: This is pre-calculated area of the grid, which is now 75% of the width of the board.
  • cellSize: This is the size of each and every cell in the grid. It is calculated by dividing the boardSpace by row / column count.

Now we will replace the `updateDrawLine()` with a little more complex `updateVGridDraw()`. This will first draw a vertical line. Once it is over, it will increment x by some value and draw another vertical line. Keeps repeating this logic for `Config.colCount + 1` times. Here is the code for `updateVGridDraw()`

this.vLineIndex  = 0; // add this line in `drawBoard` function

updateVGridDraw              :   function(dt) {
    this.currentLinePercent         -=  0.01 * Config.drawSpeed;
    to                  =   cc.p(this.vLineIndex * this.cellSize.width, this.currentLinePercent * this.boardSpace.height);
    this.drawBrushAtPoint(to, Config.lineColor, 16);

    if (this.currentLinePercent < 0.01) {
        this.vLineIndex++;
        if (this.vLineIndex <= Config.colCount) {
            this.currentLinePercent = 1.0;
        } else {
            this.unschedule(this.updateVGridDraw);
        }
    }
}
  • vLineIndex: When drawing multiple lines, this variable keeps track of which line is being currently drawn. This also used to calculate the x position of the current line as in the following table.
    Table 1

    vLineIndex xPosition
    0 0
    1 cellSize.width
    2 cellSize.width * 2
    3 cellSize.width * 3
    n cellSize.width * n
  • Config.drawSpeed: This is a multiplier that determines by what percetange the `currentLinePercent` is getting changed. The more value it is, the point will be drawn at big distances and quickly.
  • Draw Point `to`: The draw point is then calculated with `vLineIndex` and `currentLinePercent`. For y position, rather than drawing to full board height, now we are only covering boardSpace.height, which is 75% of the width of the board.
  • We then call the `drawBrushAtPoint()` with calculated point `to`, which draws the dot with a given density of 16.
  • Other lines: The final `If` block is to draw further lines.
    • We check if the current line drawing is over, by checking if currentLinePercent reached 0.
    • When it is, we increment vLineIndex by 1 and reset currentLinePercent to 1. So a new line will get drawn but at the next x position.
    • When the vLineIndex reaches `Config.colCount`, we stop unschedule `updateVGridDraw()`, thus stopping the whole line drawing.

Vertical Lines Draw

6. Draw Grid

Now that we know how to draw Vertical lines, we can replicate the same and do it horizontally. Code update to draw horizontal lines.

updateHGridDraw              :   function(dt) {
    this.currentLinePercent         +=  0.01 * Config.drawSpeed;
    if (this.currentLinePercent > 0.93)
        this.currentLinePercent     =   0.931;
    to                  =   cc.p(this.currentLinePercent * this.boardSpace.width, this.hLineIndex * this.cellSize.height);
    this.drawBrushAtPoint(to, Config.lineColor, 16, false);

    if (this.currentLinePercent > 0.93) {
        this.hLineIndex--;
        if (this.hLineIndex != -1) {
            this.currentLinePercent =   -0.01;
        } else {
            this.unschedule(this.updateHGridDraw);
        }
    }
}

// update else block of `updateVGridDraw` with this code:
this.vLineIndex         =   -1;
this.hLineIndex         =   Config.rowCount;
this.currentLinePercent =   -0.01;
this.unschedule(this.updateVGridDraw);
this.schedule(this.updateHGridDraw, 0);

// update `drawBrushAtPoint` with this code:
drawBrushAtPoint            :   function(pt, color, density, vertical) { //new parameter vertical

// replace the line `this.chalkBrush.setPosition(...)` with this block inside `drawBrushAtPoint` function
if (vertical) {
    this.chalkBrush.setPosition(cc.p(pt.x, pt.y + 5 * (i - density * 0.5)));
} else {
    this.chalkBrush.setPosition(cc.p(pt.x + 5 * (i - density * 0.5), pt.y));
} 
  • `updateVGridDraw()` is very similar to `updateHGridDraw()`, except the x position is calculated by varying currentLinePercent and y position calculated by hLineIndex.
  • `drawBrushAtPoint()` gets a new boolean parameter vertical.
    • When vertical is true, adjacent filling dots are calculated vertically as in updateVGridDraw().
    • When vertical is false, adjacent filling dots are calculated horizontally as in updateHGridDraw().
  • Then the else block of updateVGridDraw() is replaced with above 5 lines of code, which unschedules the vertical line draw function, updateVGridDraw() and schedules the horizontal line draw function, updateHGridDraw().

At this point, we should be getting the output as follows:

Grid Lines Draw

7. Center in Board

The grid is now neatly drawn. But wait, it’s drawn over the edge wood portion. We need to bring it to the center of the board to make it look perfect. Considering what we done till now, centering the grid is just a piece of cake. Here is the logic:

Since the board size is 75% of the width, moving the board to 12.5% of the width will center the grid. So our layout would be like this.

|    space   |        board         |    space    |
      12%              75%                12%

To do this, create a new variable `offset` in the constructor as follows:

this.offset = cc.p(size.width * 0.12, size.height * 0.12);

Then update the point (to) calculation line in updateVGridDraw() and updateHGridDraw() as follows:

// updateVGridDraw()
to = cc.p(this.vLineIndex * this.cellSize.width + this.offset.x, this.currentLinePercent * this.boardSpace.height + this.offset.y);

// updateHGridDraw()
to = cc.p(this.currentLinePercent * this.boardSpace.width + this.offset.x, this.hLineIndex * this.cellSize.height + this.offset.y);

This will move our awesome Grid to the center.

Now you can tinker with the properties of Config object to draw grid of various sizes (7×7, 9×9), different chalk color and varying draw speed.

Center Grid Draw

Happy Chalking (y)

8. Complete Board.js Code

var Config                      =   {};

var Board = cc.Scene.extend({

    boardSprite                 :   null,

    renderTex                   :   null,

    chalkSprite                 :   null,

    chalkBrush                  :   null,

    prevPoint                   :   cc.p(0, 0),

    boardSpace                  :   null,

    cellSize                    :   null,

    offset                      :   null,

    mode                        :   0,

    ctor                        :   function () {

        this._super();

        var slf                 =   this;
        var size                =   cc.winSize;
        
        if( 'touches' in cc.sys.capabilities ) { 
            console.log('Can Touch');
            this._touchListener = cc.EventListener.create({
                event: cc.EventListener.TOUCH_ALL_AT_ONCE,
                onTouchesBegan: this.onTouchesBegan,
                onTouchesMoved: this.onTouchesMoved,
                onTouchesEnded: this.onTouchesEnded
            });

            cc.eventManager.addListener(this._touchListener, this);
        } else {
            console.log('No Touch Capabs');
        }

        Config.colCount         =   5;
        Config.rowCount         =   5;
        Config.drawSpeed        =   10;
        Config.lineColor        =   cc.color(250, 250, 50, 200);

        this.boardSpace         =   cc.size(size.width * 0.75, size.width * 0.75);
        this.cellSize           =   cc.size(this.boardSpace.width / Config.colCount, this.boardSpace.height / Config.rowCount);
        this.offset             =   cc.p(size.width * 0.12, size.height * 0.12);

        this.boardSprite        =   cc.Sprite.create(res.Board_BG);
        this.boardSprite.setPosition(cc.p(size.width / 2, size.height / 2));
        this.boardSprite.setScale(640 / this.boardSprite.getContentSize().width,
                size.height / this.boardSprite.getContentSize().height);
        this.addChild(this.boardSprite);

        this.renderTex     =   cc.RenderTexture.create(size.width, size.height);
        this.renderTex.setPosition(cc.p(size.width / 2, size.height / 2));
        this.addChild(this.renderTex);

        this.chalkSprite        =   cc.Sprite.create(res.chalk_png);
        this.chalkSprite.setPosition(cc.p(size.width * 0.7, this.chalkSprite.getContentSize().height * 0.8));
        this.addChild(this.chalkSprite, 2);

        this.duster             =   cc.Sprite.create(res.duster_png);
        this.duster.setPosition(cc.p(this.duster.getContentSize().width * 0.5, this.duster.getContentSize().height * 0.8));
        this.duster.setScale(0.5);
        this.duster.setRotation(30);
        this.addChild(this.duster, 2);

        this.drawBoard();
    }, 

    onTouchesBegan:function(touches, event) {
        var slf                 =   event.getCurrentTarget();
        var pos                 =   touches[0].getLocation();
        console.log('Began : ' + JSON.stringify(pos));
        
        slf.prevPoint           =   pos;
        slf.mode                =   0;

        dist                    =   Math.round(cc.pDistance(pos, slf.duster.getPosition()));
        if (dist < slf.duster.getContentSize().width) {
            slf.mode            =   1;
        }

        return                      true;
    },

    onTouchesMoved:function(touches, event) {
        var slf                 =   event.getCurrentTarget();
        var pos                 =   touches[0].getLocation();
        console.log('Move : ' + JSON.stringify(pos));
        //console.log('PrevPoint : ' + JSON.stringify(slf.prevPoint));

        var dist                =   Math.round(cc.pDistance(pos, slf.prevPoint));
        //console.log(dist);

        for (var i = 0; i < dist; i += 5) {
            var cPos            =   cc.pLerp(slf.prevPoint, pos, i/dist);
            if (slf.mode == 0) {
                slf.drawBrushAtPoint(cPos, cc.color(240, 240, 240, 230), 1, true);
            } else if (slf.mode == 1) {
                slf.eraseAtPoint(cPos);
            }
        }

        slf.prevPoint           =   pos;
    },

    onTouchesEnded              :   function(touches, event) {
        var slf                 =   event.getCurrentTarget();
        var pos                 =   touches[0].getLocation();
        var size                =   cc.winSize;

        console.log('End : ' + JSON.stringify(pos));

        if (slf.mode == 0) {
            slf.chalkSprite.runAction(cc.moveTo(0.2, cc.p(size.width * 0.7, slf.chalkSprite.getContentSize().height * 0.8)).easing(cc.easeIn(1.0)));
        } else if (slf.mode == 1) {
            slf.duster.runAction(cc.moveTo(0.2, cc.p(slf.duster.getContentSize().width * 0.5, slf.duster.getContentSize().height * 0.8)).easing(cc.easeIn(1.0)));
        }

    },

    drawBrushAtPoint            :   function(pt, color, density, vertical) {

        this.renderTex.begin();
        for (var i = 0; i < density; i++) {
            this.chalkBrush     =   cc.Sprite.create(res.chalkBrush_png);
            this.chalkBrush.setRotation(Math.random() * 180);
            if (vertical) {
                this.chalkBrush.setPosition(cc.p(pt.x, pt.y + 5 * (i - density * 0.5)));
            } else {
                this.chalkBrush.setPosition(cc.p(pt.x + 5 * (i - density * 0.5), pt.y));
            }
            this.chalkBrush.setColor(color);
            this.chalkBrush.setScale(1.5);
            this.chalkBrush.visit();
        }
        this.renderTex.end();

        this.chalkSprite.setPosition(cc.p(pt.x - this.chalkSprite.getContentSize().width * 0.3,
            pt.y - this.chalkSprite.getContentSize().height * 0.2));

    },

    eraseAtPoint                :   function(pt) {

        this.renderTex.begin();
        this.chalkBrush         =   cc.Sprite.create(res.chalkBrush_png);
        this.chalkBrush.setRotation(Math.random() * 180);
        this.chalkBrush.setPosition(pt);
        this.chalkBrush.setScale(10.0);
        this.chalkBrush.setOpacity(230);
        this.chalkBrush.setBlendFunc(  cc.ZERO, cc.ONE_MINUS_SRC_ALPHA );
        this.chalkBrush.visit();
        this.renderTex.end();

        this.duster.setPosition(pt);

    },

    drawBoard                   :   function(dt) {

        this.vLineIndex         =   0;
        this.hLineIndex         =   -1;
        this.currentLinePercent =   1.025;

        this.schedule(this.updateVGridDraw, 0);
    },

    updateVGridDraw              :   function(dt) {

        this.currentLinePercent         -=  0.01 * Config.drawSpeed;
        if (this.currentLinePercent < 0.08) 
            this.currentLinePercent = 0.079;
        to                  =   cc.p(this.vLineIndex * this.cellSize.width + this.offset.x, this.currentLinePercent * this.boardSpace.height + this.offset.y);
        this.drawBrushAtPoint(to, Config.lineColor, 16, true);

        if (this.currentLinePercent < 0.08) {
            this.vLineIndex++;
            if (this.vLineIndex <= Config.colCount) { this.currentLinePercent = 1.025; } else { this.vLineIndex = -1; this.hLineIndex = Config.rowCount; this.currentLinePercent = -0.01; this.unschedule(this.updateVGridDraw); this.schedule(this.updateHGridDraw, 0); } } }, updateHGridDraw : function(dt) { this.currentLinePercent += 0.01 * Config.drawSpeed; if (this.currentLinePercent > 0.93)
            this.currentLinePercent     =   0.931;
        to                  =   cc.p(this.currentLinePercent * this.boardSpace.width + this.offset.x, this.hLineIndex * this.cellSize.height + this.offset.y);
        this.drawBrushAtPoint(to, Config.lineColor, 16, false);

        if (this.currentLinePercent > 0.93) {
            this.hLineIndex--;
            if (this.hLineIndex != -1) {
                this.currentLinePercent =   -0.01;
            } else {
                this.unschedule(this.updateHGridDraw);
            }
        }
    }


});

Cocos2dx – Platformer Physics Tutorial

I tried implementing the platforming physics logic in cocos2d-x and it came out pretty well as per my expectations. This tutorial is about how I made it. It might not be the best solution out there, but it is something that works. Before diving in, here is the demo video showcasing the final output:

Downloads

1. Mid Project (with just the platforms render)
2. Full Project (all functionalities)

1. Create a Cocos2d-x Project

I am not going to discuss about how to setup a cocos2d-x project for iOS here. I will directly dwelve into the platforming mechanism here. For info on how to setup cocos2d-x project, refer the official wiki.

All our code will go into `HelloWorld` class that comes with the template.

2. Define the Platforms

Add the following struct in HelloWorld.h to model a Line

typedef struct {
    CCPoint p1, p2;
} Line;

Declare a pointer to Line struct in HelloWorld, which will be an array of all platforms and randomly add a few lines in it:

// in HelloWorld.h
Line *lines;

// in init function of HelloWorld.cpp
lines = (Line *)malloc(sizeof(Line) * COUNT);

lines[0].p1 = ccp(100, 100);
lines[0].p2 = ccp(400, 100);

lines[1].p1 = ccp(500, 600);
lines[1].p2 = ccp(700, 600);

lines[2].p1 = ccp(400, 400);
lines[2].p2 = ccp(600, 500);

lines[3].p1 = ccp(500, 200);
lines[3].p2 = ccp(1000, 200);

The platforms are defined well with their points. Now is the time to render them on screen. Add the `draw` function to the `HelloWorld` scene.

// in header file
virtual void draw();

// in cpp file
void HelloWorld::draw() {
    glLineWidth(4.0);
    for (int i = 0; i < COUNT; i++) {
        ccDrawLine( lines[i].p1, lines[i].p2 );
    }
    CCLayer::draw();
}

The above code sets the line width to 4 pixels, loops through our platform array and draws the lines. Our screen should look like this:

3. Character Sprite and Controls

The platforms are setup right. Now lets add the hero sprite which will run through the platforms and two button controls to make the character move. To add the hero and controls, do this:

// in header file
CCSprite* pSprite;
void menuCloseCallback(CCObject* pSender);

// in init function of HelloWorld.cpp
CCMenuItemImage *pCloseItem = CCMenuItemImage::create("CloseNormal.png",
                                                      "CloseSelected.png",
                                                      this,
                                                      menu_selector(HelloWorld::menuCloseCallback) );
pCloseItem->setPosition( ccp(CCDirector::sharedDirector()->getWinSize().width - 60, 60) );
pCloseItem->setTag(1);
pCloseItem->setScale(2.5f);

CCMenuItemImage *pCloseItem1 = CCMenuItemImage::create("CloseNormal.png",
                                                       "CloseSelected.png",
                                                       this,
                                                       menu_selector(HelloWorld::menuCloseCallback) );
pCloseItem1->setPosition( ccp(60, 60) );
pCloseItem1->setTag(2);
pCloseItem1->setScale(2.5f);

// controls for left and right movement
CCMenu* pMenu = CCMenu::create(pCloseItem, pCloseItem1, NULL);
pMenu->setPosition( CCPointZero );
this->addChild(pMenu, 1);   

// hero sprite
pSprite = CCSprite::create("CloseNormal.png");
pSprite->setPosition( ccp(600, 700) );
pSprite->setTag(-1);
this->addChild(pSprite, 0);

By now our screen should look like this:

4. Basic Movement

The stage is set with all the objects put in place. Now is the time to start moving the objects. Add two variables `speedX` and `speedY` that controls the movement of our hero and schedule the update function of CCLayer to get into the game loop.

// in header file
virtual void update(float dt);
float speedX, speedY;

// in init function of HelloWorld.cpp
speedX = 0;
speedY = 0;
this->scheduleUpdate();

// in HelloWorld.cpp
// moves the hero around the screen as per the speedX and speedY values
void HelloWorld::update(float dt) {
    CCPoint p = pSprite->getPosition();    
    pSprite->setPosition( ccp( p.x + speedX * dt, p.y + speedY * dt ) );
}

// makes the hero to move to left / right based on the touched control
void HelloWorld::menuCloseCallback(CCObject* pSender) {
    if (((CCNode *)pSender)->getTag() == 1) {
        speedX += 100;
    } else {
        speedX -= 100;
    }
}

Now touching the left control button, increases the movement of the hero in left direction and the same with the right control button. As of now, Movement happens only in x-axis and there is no gravity (falling down), friction and collision. The project at this point can be downloaded here for reference.

5. Physics Magic

First replace the `update` function with following code.

void HelloWorld::update(float dt) {
    CCPoint p = pSprite->getPosition();

    speedX *= 0.95; // friction reduces the speed over time

    if (pSprite->getTag() == -1) { // free falling
        CCLog("searching for platform... hero is falling");
        // iterate through all platforms
        for (int i = 0; i < COUNT; i++) {
            // if hero position lies between the x bounds of any platform
            if ( lines[i].p1.x < p.x && lines[i].p2.x > p.x ) {
                // also it is closer than 100 pixels to the platform
                if ( p.y - lines[i].p1.y < 100 && p.y - lines[i].p1.y > 0) {
                    // assign the platform to the hero
                    pSprite->setTag(i);
                    break;
                }
            }
        }
        // gravity pull decreases the y-speed
        speedY -= 10.0;

        // not allowing hero to go down indefinitely, by making him swap to the top
        if ( p.y < 0 ) {
             p.y = 768;
        }
    } else { // resting in or closer to some platform
        int tag = pSprite->getTag();
        CCLog("in platform %d", tag);

        // make sure if hero is still in the x bounds of the platform or moved away
        if ( lines[tag].p1.x < p.x && lines[tag].p2.x > p.x ) {
            float x2, y2;
            x2 = p.x;
            // platform may be a sloppy one, so interpolate the y value based on end points of platform and current hero position 
            y2 = (x2 - lines[tag].p1.x) * (lines[tag].p2.y - lines[tag].p1.y) / (lines[tag].p2.x - lines[tag].p1.x) + lines[tag].p1.y;
            if (p.y - y2 > 0) { // if hero is above platform, make him fall down
                speedY -= 10.0;
            } else {            // nullify the y speed and rest on platform
                speedY = 0;
                p.y = y2;
            }
        } else { // switch to free fall mode, if hero is moved away
            pSprite->setTag(-1);
        }
    }

    pSprite->setPosition( ccp( p.x + speedX * dt, p.y + speedY * dt ) );
}

I believe the above code is self-explanatory with all the comments put in. So I will just add few notes that deserve some explanation here.

Tag

`tag` property of Hero is used to refer to the platform he is in. If he is free falling (i.e., not attached to any platform) then `tag` will be -1.

Friction

 speedX *= 0.95;

Above code decrements the x speed of the hero by 5% on each update, thus simulating the friction effect.

Interpolation

If all the lines are perfectly horizontal, then y values of the end points or any other point in that line would be equal. If the lines are sloppy, things would be different and we need to calculate the y-value of any arbitrary point in the given line for the given x. If its confusing, see the picture below and read on.


For any platform, we have the end points A (x1, y1) and B (x2, y2) and we have the position of the hero represented here as H(hx, hy). But to check if the hero is above or below the line, we need the point that lies in the line which is P(x, y). Of course, x would be same as hx, but how to find y? Not to worry, we can interpolate the y value with the following formula:

y = y1 + (hx - x1) * (y2 - y1) / (x2 - x1)

The above formula is translated to CPP in our code with our variables as follows:

y2 = (x2 - lines[tag].p1.x) * (lines[tag].p2.y - lines[tag].p1.y) / (lines[tag].p2.x - lines[tag].p1.x) + lines[tag].p1.y;

Thats the end of the tutorial and here is the full project. This project can be easily extended to 2d side scrolling platformer games. I will try to write a post on that in future. There might be several places where a beginner can easily get lost. Post the queries in comments and I will try to resolve.