Sunday, October 23, 2011

Adding 2d Physics Engine

For this update, I started adding the 2d physics engine, Chipmunk Physics.  As mentioned earlier, the 2d physics engine does so much work for a 2d programmer.  It performs all the movement and collision detection for every object in the game.   This update just adds some walls, and handles moving the player through the 2d physics world.
I also added a new class, SmashPcMap, which handles the loading and drawing the level.

You can get the source code here:
SmashPc 11-23 code

First, in SmashPc.cpp, main(), here's the new function:

int main(int argc, char *argv[])
{
    sf::Event Event;
    BOOL bFullScreen;

    GameSound::Init("SmashPcSounds.xml");
    Utilities::LoadConfig("SmashPcCfg.xml");

    Utilities::ScreenResolutionGet(gu32ScreenX, gu32ScreenY, bFullScreen);

 sf::RenderWindow App(sf::VideoMode(gu32ScreenX, gu32ScreenY, 32), "SmashPC", (bFullScreen ? sf::Style::Fullscreen : sf::Style::Default));

    cpInitChipmunk();

    gpMap = new SmashPcMap("", gu32ScreenX, gu32ScreenY, &App);

    // Create location; Typically, use location from level, but fake for now
    gpOurPlayer = new SmashPcPlayer(&App, gpMap->GetSpace(), cpv(200, 200));

    srand(time(NULL));

    App.ShowMouseCursor(true);
    App.SetCursorPosition(gu32ScreenX/2, gu32ScreenX/2);

    App.SetFramerateLimit(60);

    // main game loop
    while (App.IsOpened())
    {
        while (App.PollEvent(Event))
        {
            // Window closed
            if (Event.Type == sf::Event::Closed)
            {
                App.Close();
                GameSound::Shutdown();
            }

            // Escape key pressed
            if (Event.Type == sf::Event::KeyPressed)
            {
                if (Event.Key.Code == sf::Key::Escape)
                {
                    App.Close();
                    GameSound::Shutdown();
                }
            }
        }

        gpOurPlayer->CheckInput();

        cpSpaceStep(gpMap->GetSpace(), 1.0f/60.0f);

        /* clear screen and draw map */
        App.Clear();

        gpMap->Draw(gpOurPlayer->GetBody());
        gpOurPlayer->Draw();

        App.Display();
    }

    return 0;
}
You see we added the call to cpInitChipmunk(), which kicks off the physics engine.
 Later we call cpSpaceStep(). This function simulates all the objects defined in the space at a speed of 60 frames per second.
We also call create an instance of SmashPcMap(), which will create the physics space, loads the level, and draws it. Currently, we're not loading levels yet, so we just pass it an empty level.

 In SmashPcMap, we create and init the space:
    mpSpace = cpSpaceNew();
    cpSpaceInit(mpSpace);
    mpSpace->iterations = 10;

 And, In CreateSpace() we create fake walls that just surrounds the world:
       // use static body and add this wall shape
        pShape = cpSpaceAddShape(mpSpace, cpPolyShapeNew(&mpSpace->staticBody, 4, Verts, cpvzero));

        pShape->e = 0.9f;
        pShape->u = 0.3f;
        pShape->collision_type = WALL_COL_TYPE;
        pShape->data = this;

Looking in SmashPcPlayer, We added the starting world location of the player and the physics space to the constructor.  Here is is:

SmashPcPlayer::SmashPcPlayer(sf::RenderWindow *pApp, cpSpace *pSpace,
                             cpVect Location) :
mpApp(pApp),
mpSpace(pSpace),
mu32Health(100),
mu32Armor(0),
mu32LastFire(0),
mu32LastSpecialFire(0)
{

    // Load the temp image for this player
    mpImage = Utilities::ImageGet("Gfx/Player.bmp");

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

    // Set to middle of screen
    mpSprite->SetPosition(pApp->GetWidth()/2, pApp->GetHeight()/2);

    mpBody = cpBodyNew(1.0f, 1.0f);

 cpBodySetPos(mpBody, Location);
 mpBody->v = cpvzero;
 mpBody->v_limit = PLAYER_SPEED;

 cpBodySetAngle(mpBody, PI/2.0f);
 mpBody->w = 0.0f;

 mpShape = cpCircleShapeNew(mpBody, mpImage->GetWidth()/2, cpvzero);
 mpShape->e = 0.0f;  // not elatics, no bouncing
 mpShape->u = 1.0f; // 0.0 is frictionless
 mpShape->collision_type = PLAYER_COL_TYPE;
 mpShape->data = (void *)this;

    mpShape->layers = PLAYER_LAYER;

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

 // Create Collision handler here

    // Sound notifying a player has spawned
    GameSound::Play("PlayerSpawn");

}

You can see the code that creates the player's body and shape in the physics space (world).  The body is basically for collisions and holds the velocity, force, and position, and it's connected to the shape which defines the bodies we...shape.  We use circle for player.

For Draw(), we're just drawing the player in the middle of the screen, and moving the world around him.

CheckInput() has been modified and it probably needs some explanation.  Here it is:
void SmashPcPlayer::CheckInput(void)
{
    const sf::Input& Input = mpApp->GetInput();
    cpFloat xForce = 0, yForce = 0;
    cpVect Vel;
    U32 u32Time = timeGetTime();

    // Set all forces to 0
    mpBody->f = cpvzero;

    Vel = cpBodyGetVel(mpBody);

    // We'll just assign velocities to a hardcoded value for now
    if (Utilities::KeyIsDown(Input, GAME_KEY_LEFT))
    {
        xForce -= PLAYER_FORCE;
    }
    else if (Vel.x < 0.01f)
    {
        mpBody->v.x = 0;
    }

    if (Utilities::KeyIsDown(Input, GAME_KEY_RIGHT))
    {
        xForce += PLAYER_FORCE;
    }
    else if (Vel.x > 0.01f)
    {
        mpBody->v.x = 0;
    }

    if (Utilities::KeyIsDown(Input, GAME_KEY_DOWN))
    {
        yForce += PLAYER_FORCE;
    }
    else if (Vel.y > 0.01f)
    {
        mpBody->v.y = 0;
    }

    if (Utilities::KeyIsDown(Input, GAME_KEY_UP))
    {
        yForce -= PLAYER_FORCE;
    }
    else if (Vel.y < 0.01f)
    {
        mpBody->v.y = 0;
    }

    mpBody->f = cpv(xForce, yForce);

    // Check for fire
    if (Utilities::KeyIsDown(Input, GAME_KEY_FIRE))
    {
        // we'll fake a re-fire speed check, 400 mS
        if (mu32LastFire + 400 <= u32Time )
        {
            // Play Fake Sound, repeat
            GameSound::Play("PeaShooter", TRUE);
            mu32LastFire = u32Time;
        }
    }
}

First, a mention about moving things around in a physics engine.  Typically, in games, you just give something a velocity, and say go.  Well, that kind of work in physics engines, but problems occur when you have collisions.  If you constantly give an object a velocity, and it's colliding with an object, the physics engine will keep  moving it that way, whether it's collided with something or not.

So, to "fake" it, we 1st give a v_limit to the body, (see the Constructor).  This is the maximum velocity the body can have.  Then when we want the player to move a direction, we just give it a massive force in the direction it's moving.  If it's too low, the body will accelerate to the velocity we want, but for this game, we want full speed or no speed.

Also, we have to add some code where we actually set the velocity to 0; otherwise, if we're moving right, and let up on the left key, the player will continue to move right until friction stops it.

Next time we'll add bullets to the system, and how to handle collisions.

Till then!

Edit 11-24-11:
I forgot to mention the other change in SmashPcMap, how it draws.  Here is thespecific code:
BOOL SmashPcMap::Draw(cpBody *PlayerBody)
{
    cpFloat LeftX  = PlayerBody->p.x - mu32ScreenWidth/2;
    cpFloat TopY   = PlayerBody->p.y - mu32ScreenHeight/2;

    // Soon we will draw all items loaded from the level
    // but, now it's only the fake walls, so draw them
    // fake corners removed...

    for (int i = 0; i < 4; i++)
    {
        cpVect Verts[4] = // ... removed from snippet

        sf::Shape oShape;

        oShape = sf::Shape::Rectangle (Verts[0].x - LeftX, Verts[0].y - TopY,
                                       Verts[2].x  - Verts[0].x, Verts[2].y  - Verts[0].y,
                                       sf::Color(128, 128, 128, 255));

        mpApp->Draw(oShape);

    }

    return TRUE;
}

We now pass in the player's body so we can get the location of him within the physics world. We then draw the rest of the world around the player (since we want to keep the player in the middle of the screen and scroll the rest of the world around him). That's why it is all based on LeftX and Top Y of the player (again, in world coordinates).

No comments:

Post a Comment