Tuesday, October 25, 2011

Bullets Added!

Last time I promised Bullets and collision detection.  Well, I'm providing 1/2 of that.  I haven't put in the chipmunk-physics driven collision detection callbacks yet, but I did provide bullets.  In doing so, I also now point the player towards the mouse, and fire the bullets in that direction.

Download this version of code here:
SmashPC 10-25-11 Code

Right now the bullets pool in a corner until the TimeToLive expires, but you can see the Pysics engine doing it's work.  Since the Bullets and Walls are in the same collision layer, they get stopped by the walls.  Let's get to the code.

First, here's my SmashPcWeapons.xml file which defines the weapons:
<smashpcweapons> <peashooter> <image>Bullet.bmp</image> <glowimage>Bullet_glow.bmp</glowimage> <velocity>1000</velocity> <damage>10</damage> <refire>290</refire> <timetolive>3000</timetolive> </peashooter> </smashpcweapons>


This only defines one weapon for now, the PeaShooter.  The SmashPcWeapon class will read through that file and load all the available weapons and their attributes.  We will be adding attributes as the weapons get mroe complex.

Now, Let's look at SmashPcWeapon.  This is designed to only be instanced once as we don't want to continually be reading off disk for the xml file.  The constructor opens the xml and stores the details in a map:
SmashPcWeapon::SmashPcWeapon(char *pcFilename, sf::RenderWindow *pApp,
                             cpSpace *pSpace) :
  mu32LastFireTime(0),
  mpApp(pApp),
  mpSpace(pSpace)
{
    SmashPcBullet::tBulletDetails Bullet;
    TiXmlDocument doc(pcFilename);

    // load and check if it was successful
    if (doc.LoadFile())
    {
        // root is GravithonWeapons
        TiXmlElement *root = doc.RootElement();

        for(TiXmlElement* WeapElem = root->FirstChildElement(); WeapElem;
            WeapElem = WeapElem->NextSiblingElement())
        {
            char cFile[64];

            Bullet.u32TimeToLive = 0;
            Bullet.pGlowImage = NULL;

            // Assign the other pieces
            Bullet.Speed =
                atof(WeapElem->FirstChild("Velocity")->FirstChild()->Value());

            Bullet.u32Damage =
                atoi(WeapElem->FirstChild("Damage")->FirstChild()->Value());

            Bullet.u32Refire =
                atoi(WeapElem->FirstChild("Refire")->FirstChild()->Value());

            // load the image
            sprintf(cFile, "Gfx/%s",
                    WeapElem->FirstChild("Image")->FirstChild()->Value());

            Bullet.pImage = Utilities::ImageGet(cFile);

            // Check for optional values
            if (WeapElem->FirstChild("TimeToLive"))
            {
                Bullet.u32TimeToLive =
                    atoi(WeapElem->FirstChild("TimeToLive")->FirstChild()->Value());
            }

            if (WeapElem->FirstChild("GlowImage"))
            {
                sprintf(cFile, "Gfx/%s",
                    WeapElem->FirstChild("GlowImage")->FirstChild()->Value());

                Bullet.pGlowImage = Utilities::ImageGet(cFile);
            }

            // Store off in map
            mBulletHash[WeapElem->ValueStr()] = Bullet;
        }
    }
    else
    {
        printf("Failed to open %s\n", pcFilename);
    }

    // Set the 1st weapon as the default
    mSelectedBullet = mBulletHash.begin()->first;
}

The function SelectBullet simply sets which type of bullet the weapon will fire.

The last function, FireBullet:
SmashPcBullet *SmashPcWeapon::FireBullet(cpBody *pPlayerBody, BOOL bOurBullet)
{
    SmashPcBullet *pBullet = NULL;
    U32 u32Time = timeGetTime();

    // Check if it's time to fire
    if (mu32LastFireTime + mBulletHash[mSelectedBullet].u32Refire <= u32Time )
    {
        pBullet = new SmashPcBullet(mpApp, mBulletHash[mSelectedBullet],
                  pPlayerBody, mpSpace, bOurBullet);

        // Play Sound (use name of bullet for sound)
        GameSound::Play(mSelectedBullet, TRUE);
        mu32LastFireTime = u32Time;
    }

    return pBullet;

}

You can see we only fire when the refire rate is past, and we return a bullet.  You'll see why we return a bullet in a sec.

Next we'll take a look at SmashPcBullet class.  Here's the constructor:
SmashPcBullet::SmashPcBullet(sf::RenderWindow *pApp,
                             tBulletDetails &BulletDetails,
                             cpBody *pPlayerBody, cpSpace *pSpace,
                             BOOL bOurBullet) :
mpApp(pApp),
mBulletDetails(BulletDetails),
mpSpace(pSpace),
mbDead(FALSE)

{
    // Set Time to Live to when it should die
    if (mBulletDetails.u32TimeToLive)
    {
        mBulletDetails.u32TimeToLive = timeGetTime() + mBulletDetails.u32TimeToLive;
    }

    mpSprite = new sf::Sprite();
    mpSprite->SetImage(*mBulletDetails.pImage);
    mpSprite->SetOrigin(mBulletDetails.pImage->GetWidth()/2,
                        mBulletDetails.pImage->GetHeight()/2);

    // Rotate sprite
    mpSprite->SetRotation(pPlayerBody->a*(180.0f/PI));

    if (mBulletDetails.pGlowImage)
    {
        mpGlowSprite = new sf::Sprite();
        mpGlowSprite->SetImage(*mBulletDetails.pGlowImage);
        mpGlowSprite->SetOrigin(mBulletDetails.pGlowImage->GetWidth()/2,
                                mBulletDetails.pGlowImage->GetHeight()/2);

        // for glow, set to additive blend
        mpGlowSprite->SetBlendMode(sf::Blend::Add);

        // Rotate sprite
        mpGlowSprite->SetRotation(pPlayerBody->a*(180.0f/PI));
    }

    mpBody = cpBodyNew(0.1f, 0.1f);
    cpFloat Angle = pPlayerBody->a;

    mpBody->p = pPlayerBody->p;
    mpBody->v.x=cos( Angle)*mBulletDetails.Speed;
    mpBody->v.y=sin( Angle)*mBulletDetails.Speed;

    cpBodySetAngle(mpBody,  Angle);
    mpBody->w = 0.0f;

    mpShape = cpCircleShapeNew(mpBody,
                            (mBulletDetails.pImage->GetWidth() + mBulletDetails.pImage->GetHeight())/4,
                            cpvzero);
    mpShape->e = 0.0f;  // not elatics, no bouncing
    mpShape->u = 1.0f; // 0.0 is frictionless
    mpShape->collision_type = BULLET_COL_TYPE;
    mpShape->data = (void *)this;

    if (bOurBullet)
    {
        mpShape->layers = BULLET_LAYER;
    }
    else
    {
        mpShape->layers = ENEMY_BULLET_LAYER;
    }

    cpSpaceAddBody(mpSpace, mpBody);
    cpSpaceAddShape(mpSpace, mpShape);

    // Create Collision handler here

}


Here, we create the sprite based on the image, but we also create the "glow" sprite if a glow image is passed in. This will put a glow around the bullet image, which is a nice effect.

 Then, we create the physics body and shape. We assign the location the same as the player's body, and the same thing with the angle. We do a little trig to set the X and Y velocities based on the given Speed and Angle the player's facing.

The layers is the collision layer.  Only objects whos collision layer mask's overlapp will collide.  So, we don't want our player to collide with is own bullets, so we want to make sure the player's layer doesn't over lap his own bullets.

The other major function in SmashPcBullet is Draw:
BOOL SmashPcBullet::Draw(cpVect PlayerLoc)
{
    // Compute where on the screen the bullet should be drawn
    cpFloat Bullx = (cpFloat)(mpApp->GetWidth()/2)  + (mpBody->p.x - PlayerLoc.x);
    cpFloat Bully = (cpFloat)(mpApp->GetHeight()/2) + (mpBody->p.y - PlayerLoc.y);

    if (((Bullx > 0) && (Bullx < (cpFloat)mpApp->GetWidth())) &&
        ((Bully > 0) && (Bully < (cpFloat)mpApp->GetHeight())))
    {
        mpSprite->SetPosition(Bullx, Bully);
        mpSprite->SetRotation(mpBody->a*(180.0f/PI));

        if (mBulletDetails.u32TimeToLive)
        {
            if (timeGetTime() > mBulletDetails.u32TimeToLive)
            {
                // Mark bullet for deletion
                mbDead = TRUE;
                return TRUE;
            }
        }

        mpApp->Draw(*mpSprite);

        if (mBulletDetails.pGlowImage)
        {
            mpGlowSprite->SetPosition(Bullx, Bully);
            mpGlowSprite->SetRotation(mpBody->a*(180.0f/PI));

            mpGlowSprite->SetColor(sf::Color(255, 0, 0, 180));

            mpApp->Draw(*mpGlowSprite);
        }
    }

    return TRUE;

}

We check the bullet's world location against the player location and we only draw the bullet if it's on the visible screen. And, if there's a glow image, we draw it, but with a Red tint, and partial alpha value.

Now, in SmashPcPlayer::CheckInput(), when the Fire key is pressed, we just tell the weapon to fire. I've also added the code to position the player towards the mouse:
    // Check for fire
    if (Utilities::KeyIsDown(Input, GAME_KEY_FIRE))
    {
        pBullet = mpWeapon->FireBullet(mpBody, TRUE);
    }
    else if (Utilities::KeyIsDown(Input, GAME_KEY_SPECIAL_FIRE))
    {
        // Handle later
    }

    // Finally, find location of Mouse and point player towards it
    cpFloat MouseX = (cpFloat)Input.GetMouseX();
    cpFloat MouseY = (cpFloat)Input.GetMouseY();

    cpFloat Angle = atan2(MouseY - (cpFloat)(mpApp->GetHeight()/2),
                          MouseX - (cpFloat)(mpApp->GetWidth()/2));
    cpBodySetAngle(mpBody, Angle);


Finally in the main loop, we have to keep track of the bullets, so we add them to a vector.  We then draw every bullet in the vector.  If a Bullet needs to be removed, we do that before drawing.  Here's the added code:
    SmashPcBullet *pBullet;
    std::vector BulletList;
.
.
.
        pBullet = pOurPlayer->CheckInput();

        if (pBullet)
        {
            BulletList.push_back(pBullet);
        }
.
.
.
        // Check for bullet removal, else Draw all the Bullets
        for (std::vector::iterator it = BulletList.begin();
             it != BulletList.end(); )
        {
            if ((*it)->IsDead())
            {
                delete *it;
                it = BulletList.erase(it);
                if (it == BulletList.end())
                {
                    break;
                }
                continue;
            }
            else
            {
                (*it)->Draw(pOurPlayer->GetBody()->p);
            }
            it++;
        }

Next time I'll add Collisions, I (almost) promise!

No comments:

Post a Comment