Cocos2dx – Jedi’s Light Saber

Lets try creating a stunning Jedi’s Light Saber in this tutorial.

Downloads and Source

Project File (ZIP)

Project @ Github: https://github.com/saiy2k/cocos2dx-lightsaber

1. Create new Cocos2DX Project

Refer to official wiki on creating a new cocos2dx project.

2. `LightSaber` Class

  1. Create a C++ class named `LightSaber` that inherits from `CCLayer`
    //LightSaber.h
    class LightSaber : public CCLayer {
    public:
    
        // MEMBER VARIABLES
        // ****************
        CCSprite                        *textureSprite;
        CCArray                         *saberArray;
    
        // OBJECT LIFE CYCLE
        // *****************
        virtual bool                    init();
        CREATE_FUNC(LightSaber);
        ~LightSaber();
    
    };

    `textureSprite` will hold the texture to be used by the particle system and `saberArray` will hold the lightSaber particle systems. All particle systems in this array will be exact copies of each other, but together they intensify the effect.

  2. The `init` function and the `destructor` allocates memory to the `saberArray` and releases it.
    bool LightSaber::init() {
        if ( !CCLayer::init() ) return false;
    
        saberArray                  =   CCArray::create();
        saberArray->retain();
        return                          true;
    }
    
    LightSaber::~LightSaber() {
        saberArray->release();
    }
  3. Create an Instance of `LightSaber` and it to the running scene in `AppDelegate` as follows:
        // in applicationDidFinishLaunching() of AppDelegate.cpp
        CCScene *pScene = CCScene::create();
        pScene->addChild(LightSaber::create());
        pDirector->runWithScene(pScene);

    Its standard Layer setup logic till now. Fancier things will follow.

3. Enable Touches via Touch Dispatcher

  1. The user should be able to move the Light Saber by dragging. So enable touches in the layer as follows
        // in LightSaber.h
        virtual bool ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent);
        virtual void ccTouchMoved(CCTouch *pTouch, CCEvent *pEvent);
        virtual void ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent);
        virtual void ccTouchCancelled(CCTouch *pTouch, CCEvent *pEvent);
        void                            registerWithTouchDispatcher();
    
        // in LightSaber.cpp
        bool LightSaber::ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent) {}
        void LightSaber::ccTouchMoved(CCTouch *pTouch, CCEvent *pEvent) {}
        void LightSaber::ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent) {}
        void LightSaber::ccTouchCancelled(CCTouch *pTouch, CCEvent *pEvent) {}
        void LightSaber::registerWithTouchDispatcher() {
            CCDirector::sharedDirector()->getTouchDispatcher()->addTargetedDelegate(this, INT_MIN, true);
        }
    
        // `init()` of LightSaber
        this->setTouchEnabled(true);
  2. Write a function `createSabers()` that will create a particleSystem, when touch is made.
        void LightSaber::createSabers() {
            CCParticleSystem                *saber;
    
            textureSprite               =   CCSprite::create("swipeParticle.png");
            textureSprite->retain();
    
            saber                      =   CCParticleSun::create();
            saber->setTexture(textureSprite->getTexture());
            saber->setAutoRemoveOnFinish(true);
            saber->setEmissionRate(2000);
            saber->setLife(0.06);
            saber->setLifeVar(0.01);
            saber->setSpeed(0.0);
            saber->setSpeedVar(0.0);
            saber->setStartColor(ccc4f(0.2, 0.2, 0.9, 0.9));
            saber->setEndColor(ccc4f(0.1, 0.1, 0.9, 0.5));
            saber->setStartSize(16.0);
            saber->setStartSizeVar(2.0);
            saber->setEndSize(8.0);
            saber->setEndSizeVar(2.0);
            saber->setBlendAdditive(true);
            saber->setPosition( ccp(2020, 2020) ); // this is to place the saber out of screen initially.
            saberArray->addObject(saber);
            this->addChild(saber);
    
            textureSprite->release();
        }
    
        // in `ccTouchBegan()`
        this->createSabers();
        return true;
  3. Also, write `updateSabers()` and `removeSabers()` functions as follows and call them from `ccTouchMoved()` and `ccTouchEnded()` respectively.
        void LightSaber::updateSabers(CCArray *systems, CCPoint startPoint, CCPoint endPoint) {
            CCParticleSystem                *saber;
            for (int i = 0; i < systems->count(); i++) {
                saber           =   (CCParticleSystem *)systems->objectAtIndex(i);
                saber->setPosition( startPoint );
            }
        }
        void LightSaber::removeSabers() {
            CCObject                        *obj;
            CCParticleSystem                *particleSys;
            CCARRAY_FOREACH(saberArray, obj) {
                particleSys             =   (CCParticleSystem *)obj;
                particleSys->stopSystem();
                particleSys->setAutoRemoveOnFinish(true);
            }
        }

    Now we will have a nice light dot that moves around your finger with a mini trail.

4. Transformation of the `DOT` into a Real Saber (some Math)

Instead of creating a number of particle system along the line of the Saber, I planned to use the `setPosVar()` to spread the particles along the length of the saber. But spreading the available particles across a bigger length reduces the intensity of the whole system. To compensate for this, I create multiple copies of the Particle system and overlays one over other. That is why, we have a `saberArray` instead of a single `CCParticleSystem`.

Light Saber Geomtery

In the above diagram, the touchPoint is P1, which is the holding end of the Saber and the other end of the Saber is P2, which is an offset by certain distance from P1 for now. `ccTouchMoved()` determines this 2 points P1 and P2 and passes it onto `updateSabers()`. Update `updateSabers()` with the following code, which is to determine the spread area and rotation of the saber based on P1 and P2.

    float                           dist;
    CCPoint                         velocity;
    CCPoint                         delta, offset, final;
    CCPoint                         centerPoint;
    CCParticleSystem                *saber;
    float                           ang;

    delta                       =   ccpSub(endPoint, startPoint);
    centerPoint                 =   ccpAdd(ccpMult(delta, 0.5), startPoint);
    ang                         =   atan2f(delta.y, delta.x) * 180 / 3.14;
    dist                        =   ccpDistance(startPoint, endPoint);

    for (int i = 0; i < systems->count(); i++) {
        saber           =   (CCParticleSystem *)systems->objectAtIndex(i);
        saber->setPosition( centerPoint );
        saber->setPosVar( ccp(0, dist * 0.5) );
        saber->setRotation(90 - ang);
    }

In the above code, `delta` represents the distance vector between `startPoint` and `endPoint`, which when multiplied by 0.5, gives half the distance between startPoint and endPoint. This half-distance is then added to the `startPoint` which gives `centerPoint`, where our System will be positioned.

But without proper call to `setPosVar()`, this will again look like a dot offset by some position from the touchPoint. Since the System is now placed exactly in between P1 and P2, whose distance is `dist`, the emissions should be from -dist / 2 to +dist / 2, which is set by the call to `setPosVar()`

Its not yet complete, since the light saber will stand vertically through the centerPoint and it need to be rotated to align to P1 and P2, which is done by the `setRotation()` call.

5. Strengthening the Effect

Since the particle system is stretched along the length of the saber, the saber doesn’t look intensive. So to increase the intensity, lets replicate the particle system and overlay one over the other.

Since we already have an array for the particle system and all our `ccTouchMoved()` and `ccTouchEnded()` functions deal with the array, we need just one more change to achieve this, which is to create more number of Particle Systems and add it to the array, by modifying the `createSabers()` function.

Full Code

void LightSaber::createSabers() {
    CCParticleSystem                *saber;
    textureSprite               =   CCSprite::create("swipeParticle.png");
    textureSprite->retain();
    for (int i = 0; i < strength; i++) {         saber                      =   CCParticleSun::create();         saber->setTexture(textureSprite->getTexture());
        saber->setAutoRemoveOnFinish(true);
        saber->setEmissionRate(2000);
        saber->setLife(0.06);
        saber->setLifeVar(0.01);
        saber->setSpeed(0.0);
        saber->setSpeedVar(0.0);
        saber->setStartColor(ccc4f(0.2, 0.2, 0.9, 0.9));
        saber->setEndColor(ccc4f(0.1, 0.1, 0.9, 0.5));
        saber->setStartSize(16.0);
        saber->setStartSizeVar(2.0);
        saber->setEndSize(8.0);
        saber->setEndSizeVar(2.0);
        saber->setBlendAdditive(true);
        saber->setPosition( ccp(2020, 2020) );
        saberArray->addObject(saber);
        this->addChild(saber);
    }
    textureSprite->release();
}

void LightSaber::updateSabers(CCArray *systems, CCPoint startPoint, CCPoint endPoint) {
    float                           dist;
    CCPoint                         velocity;
    CCPoint                         delta, offset, final;
    CCPoint                         centerPoint;
    CCParticleSystem                *saber;
    float                           ang;
    delta                       =   ccpSub(endPoint, startPoint);
    centerPoint                 =   ccpAdd(ccpMult(delta, 0.5), startPoint);
    ang                         =   atan2f(delta.y, delta.x) * 180 / 3.14;
    dist                        =   ccpDistance(startPoint, endPoint);
    for (int i = 0; i < systems->count(); i++) {
        saber           =   (CCParticleSystem *)systems->objectAtIndex(i);
        saber->setPosition( centerPoint );
        saber->setPosVar( ccp(0, dist * 0.5) );
        saber->setRotation(90 - ang);
    }
}

void LightSaber::removeSabers() {
    CCObject                        *obj;
    CCParticleSystem                *particleSys;
    CCARRAY_FOREACH(saberArray, obj) {
        particleSys             =   (CCParticleSystem *)obj;
        particleSys->stopSystem();
        particleSys->setAutoRemoveOnFinish(true);
    }
}

#pragma mark -
#pragma mark TOUCH OVERRIDES

bool LightSaber::ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent) {
    this->createSabers();
    return                          true;
}

void LightSaber::ccTouchMoved(CCTouch *pTouch, CCEvent *pEvent) {
    CCPoint                         point;
    point                       =   pTouch->getLocation();
    this->updateSabers(saberArray, point, ccpAdd(point, ccp(-40, 100)));
}

void LightSaber::ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent) {
    this->removeSabers();
}

void LightSaber::ccTouchCancelled(CCTouch *pTouch, CCEvent *pEvent) {
}

void LightSaber::registerWithTouchDispatcher() {
    CCDirector::sharedDirector()->getTouchDispatcher()->addTargetedDelegate(this, INT_MIN, true);
}

#pragma mark -
#pragma mark OBJECT LIFE CYCLE

bool LightSaber::init() {
    if ( !CCLayer::init() ) {
        return                      false;
    }

    this->setTouchEnabled(true);
    strength                    =   8;
    saberArray                  =   CCArray::create();
    saberArray->retain();
    return                          true;
}

LightSaber::~LightSaber() {
    saberArray->release();
}