And the video:
A Blog detailing the steps I take to make a 2D game. The hope is to share my knowledge with others who have the same passion.
Saturday, October 29, 2011
Quick Update...
I haven't done anything real major lately, just tying some loose ends up, doing some under-the-cover work to get ready to integrate Enemies...the real hard part.
Anyway, here's a screenshot and a crappy video of what I have so far, with some ground tiles added:
Thursday, October 27, 2011
Inheriting From Physical Object
As I mentioned in the last post, I realized I should be inheriting from a Super Class for classes that represent a physical object. So, I made the PhysicalObject class:
The constructor handles all the physics stuff (except defining collision types and callbacks) and the image stuff.
I also made a Draw in the PhysicalObject class:
As you can see, we handle the images and the glow images here. We also added a Depth member to draw shadows under objects.
Here's the latest screenshot:
PhysicalObject::PhysicalObject(sf::RenderWindow *pApp, cpSpace *pSpace, std::string ImageName, std::string GlowImageName, cpVect Location, cpFloat Angle, cpFloat Speed, cpFloat Depth, cpFloat RotSpeed) : mpApp(pApp), mpSpace(pSpace), mpImage(NULL), mpGlowImage(NULL), mpSprite(NULL), mpGlowSprite(NULL), mDepth(Depth) { // Load the image mpImage = Utilities::ImageGet(ImageName.c_str()); 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); if (GlowImageName.size() > 0) { mpGlowImage = Utilities::ImageGet(GlowImageName.c_str()); mpGlowSprite = new sf::Sprite(); mpGlowSprite->SetImage(*mpGlowImage); mpGlowSprite->SetOrigin(mpGlowImage->GetWidth()/2, mpGlowImage->GetHeight()/2); // Set to middle of screen mpGlowSprite->SetPosition(pApp->GetWidth()/2, pApp->GetHeight()/2); // for glow, set to additive blend mpGlowSprite->SetBlendMode(sf::Blend::Add); } mpBody = cpBodyNew(1.0f, 1.0f); cpBodySetPos(mpBody, Location); // Set the Velocity mpBody->v.x=cos( Angle)*Speed; mpBody->v.y=sin( Angle)*Speed; cpBodySetAngle(mpBody, Angle); mpBody->w = RotSpeed; mpShape = cpCircleShapeNew(mpBody, (mpImage->GetWidth() + mpImage->GetHeight())/4, cpvzero); mpShape->e = 0.0f; // not elatics, no bouncing mpShape->u = 0.0f; // 0.0 is frictionless cpSpaceAddBody(mpSpace, mpBody); cpSpaceAddShape(mpSpace, mpShape); }
The constructor handles all the physics stuff (except defining collision types and callbacks) and the image stuff.
I also made a Draw in the PhysicalObject class:
BOOL PhysicalObject::Draw(cpVect PlayerLoc, sf::Color GlowColor) { cpFloat Angle = cpBodyGetAngle(mpBody); cpFloat Itemx = (cpFloat)mpApp->GetWidth()/2; cpFloat Itemy = (cpFloat)mpApp->GetHeight()/2; BOOL bDraw = TRUE; mpSprite->SetRotation(Angle*(180.0f/PI)); if (!cpveql(PlayerLoc, cpvzero)) { // Locate the sprite based off player's location Itemx = (cpFloat)(mpApp->GetWidth()/2) + (mpBody->p.x - PlayerLoc.x); Itemy = (cpFloat)(mpApp->GetHeight()/2) + (mpBody->p.y - PlayerLoc.y); if (!((Itemx > -(cpFloat)mpImage->GetWidth()) && (Itemx < (cpFloat)mpApp->GetWidth() + (cpFloat)mpImage->GetWidth()) && (Itemy > -(cpFloat)mpImage->GetHeight()) && (Itemy < (cpFloat)mpApp->GetHeight() + (cpFloat)mpImage->GetHeight()))) { bDraw = FALSE; } } if (bDraw) { mpSprite->SetPosition(Itemx, Itemy); // Draw shadow 1st if depth if (mDepth > 0.001f) { cpFloat Radius = cpCircleShapeGetRadius(mpShape); sf::Shape oCircle = sf::Shape::Circle(Itemx+5.0, Itemy+5.0, mDepth*Radius*0.80f, sf::Color(0, 0, 0, 150)); mpApp->Draw(oCircle); } mpApp->Draw(*mpSprite); // If we get glow alpha and we have glow image if (GlowColor.a && mpGlowSprite) { if (!cpveql(PlayerLoc, cpvzero)) { mpGlowSprite->SetPosition(Itemx, Itemy); } else { mpGlowSprite->SetPosition(mpApp->GetWidth()/2, mpApp->GetHeight()/2); } mpGlowSprite->SetRotation(Angle*(180.0f/PI)); mpGlowSprite->SetColor(GlowColor); mpApp->Draw(*mpGlowSprite); } } return TRUE; }
As you can see, we handle the images and the glow images here. We also added a Depth member to draw shadows under objects.
Here's the latest screenshot:
Wednesday, October 26, 2011
Added Level and Item introduction
So far, we've been using a hard-coded map, with 4 walls. Well, we need some way to create a real map, with multiple walls, items, etc. To accomplish this, I decided to use the tool Gleed2d (Gleed2d Site). The tool allows me to lay out a map and it outputs it as an xml file. using tinyxml, we can parse this and use SmashPcMap to render it.
Here's the source code:
SmashPc 10-26-11 Code
GameLevel::ParseItem()
GameLevel::ParsePath()
Basically, we open the xml, and read each "Item." In gleed2d speak, anything in the map is an item. For us, an Item is Either a "RectangleItem", which is just a Wall, a "TextureItem", which is an item in the game, a "PathItem", which is a connected wall, or a Spawn (our name). We read each of these out into a WallList, ItemList, or SpawnList.
In order to use these, we modify SmashPcMap to load the Level, and it reads the lists out and draws the walls and items.
Here's the new CreateSpace, which puts the walls into the physics space:
And here's the SmashPcMap::Draw that draws the walls and items:
You can see we created a new class, "SmashPcItem" which doesn't do alot yet, but it will Draw().
I have implemented the beginnings of collision between Player an Items. Here's how I have the SmashPcItem::CreatePhysicsObject():
You see at the end, I create a callback to SmashPcItem::PlayerCollision() when an item of collision type PLAYER_COL_TYPE and ITEM_COL_TYPE collide. That callback does this:
Basically, it get the player and item objects that collided and notifies that item of being hit. Currently NotifyHit() doesn't do anything.
Next time, I'm going to do a little re-design. I notice the classes SmashPcPlayer, SmashPcItem, and SmashPcBullet all have physicsal properties and can eb Drawn, which I should inherit from one class to stop from using the same code over and over.
Here's the source code:
SmashPc 10-26-11 Code
First, a picture of the level from gleed2d:
As you can see, it's not much more than 4 walls. I added a block in the middle. a little circular wall with armor inside, some Enemy Generator Platforms (the things in the corner, will spawn enemies), and the little circles are the players spawn points.
As I said, this creates a xml file. So, I created the GameLevel.h and .cpp to parse that xml file and generate data we can understand.
The code is below, you can expand them to see it if you want. It's got a lot of xml parsing. I'll explain a little below.
GamLevel::GameLevel():
The code is below, you can expand them to see it if you want. It's got a lot of xml parsing. I'll explain a little below.
GamLevel::GameLevel():
GameLevel::GameLevel(char *pcFileToLoad) : mbValid(FALSE) { TiXmlDocument doc(pcFileToLoad); // load and check if it was successful if (doc.LoadFile()) { TiXmlElement *root = doc.RootElement(); // extract level name, can check at some point printf("Level name %s\n", root->Attribute("Name")); for(TiXmlElement* LayersElem = root->FirstChildElement(); LayersElem; LayersElem = LayersElem->NextSiblingElement()) { printf("LayersElem %s\n", LayersElem->ValueStr().c_str()); // this should be Layers if (LayersElem->ValueStr() == string("Layers")) { for(TiXmlElement* LayerElem = LayersElem->FirstChildElement(); LayerElem; LayerElem = LayerElem->NextSiblingElement()) { printf("LayerElem %s\n", LayerElem->ValueStr().c_str()); // This should be Layer if (LayerElem->ValueStr() == string("Layer")) { for(TiXmlElement* ItemsElem = LayerElem->FirstChildElement(); ItemsElem; ItemsElem = ItemsElem->NextSiblingElement()) { printf("ItemsElem %s\n", ItemsElem->ValueStr().c_str()); // This should be Items if (ItemsElem->ValueStr() == string("Items")) { for(TiXmlElement* ItemElem = ItemsElem->FirstChildElement(); ItemElem; ItemElem = ItemElem->NextSiblingElement()) { // This should be Item if (ItemElem->ValueStr() == string("Item")) { ParseItem(ItemElem); } } } else { // it's scoll speed, we can ignore printf("Skip Scroll\n"); } } } } } } mbValid = TRUE; } else { printf("Failed opening %s, reason: %s\n", pcFileToLoad, doc.ErrorDesc()); } }
GameLevel::ParseItem()
void GameLevel::ParseItem(TiXmlElement* ItemElem) { tLevelWall LevelWall; tLevelItem LevelItem; map SpecValues; // store off position cpVect Position; Position.x = atof(ItemElem->FirstChild("Position")->FirstChild("X")->FirstChild()->Value()); Position.y = atof(ItemElem->FirstChild("Position")->FirstChild("Y")->FirstChild()->Value()); // check for custom properties and handle if found if (ItemElem->FirstChildElement("CustomProperties")->FirstChild() != NULL) { string Key, Value; for(TiXmlElement* PropElem = ItemElem->FirstChildElement("CustomProperties")->FirstChildElement(); PropElem; PropElem = PropElem->NextSiblingElement()) { Key = PropElem->Attribute("Name"); Value = PropElem->FirstChild()->ValueStr(); printf("SpecField Key %s Val %s\n", Key.c_str(), Value.c_str()); SpecValues[Key] = Value; } } // check for spawn 1st if (strncmp(ItemElem->Attribute("Name"), "Spawn", 5) == 0) { mSpawns.push_back(Position); } /* If it's a rectangle, it's made as a wall or spawn point */ else if (strcmp(ItemElem->Attribute("xsi:type"), "RectangleItem") == 0) { LevelWall.StartingPoint = Position; LevelWall.EndingPoint.x = atof(ItemElem->FirstChild("Width")->FirstChild()->Value()) + Position.x; LevelWall.EndingPoint.y = atof(ItemElem->FirstChild("Height")->FirstChild()->Value()) + Position.y; LevelWall.bIsRect = TRUE; printf("Rect Start x %f end %f\n", LevelWall.StartingPoint.x, LevelWall.EndingPoint.x); printf("Rect Start y %f end %f\n", LevelWall.StartingPoint.y, LevelWall.EndingPoint.y); // add to vector mWalls.push_back(LevelWall); } else if (strcmp(ItemElem->Attribute("xsi:type"), "TextureItem") == 0) { string TempString; cpVect SecondLoc = cpvzero; LevelItem.Location = Position; printf("SpriteName %s\n", ItemElem->FirstChild("texture_filename")->FirstChild()->Value()); // get name up to _ TempString = string(ItemElem->Attribute("Name")); LevelItem.ItemName = TempString.substr(0, TempString.find("_")); cout << "LevelItem name " << LevelItem.ItemName << endl; // load the texture here LevelItem.pImage = Utilities::ImageGet(ItemElem->FirstChild("texture_filename")->FirstChild()->Value()); mItems.push_back(LevelItem); } else if (strcmp(ItemElem->Attribute("xsi:type"), "PathItem") == 0) { // Parse the Path starting at WorldPoints ParsePath(ItemElem->Attribute("Name"), ItemElem->FirstChild("WorldPoints")->FirstChild("Vector2")); } }
GameLevel::ParsePath()
void GameLevel::ParsePath(const char */*pName*/, const TiXmlNode *WorldCoordNode) { tLevelWall LevelWall; BOOL bFirst = TRUE; // Might use name if we use paths for more than walls in the future for(const TiXmlNode* CoordNode = WorldCoordNode; CoordNode; CoordNode = CoordNode->NextSibling()) { if (bFirst) { LevelWall.StartingPoint.x = atof(CoordNode->FirstChild("X")->FirstChild()->Value()); LevelWall.StartingPoint.y = atof(CoordNode->FirstChild("Y")->FirstChild()->Value()); bFirst = FALSE; } else { cpFloat SwapVal; LevelWall.EndingPoint.x = atof(CoordNode->FirstChild("X")->FirstChild()->Value()); LevelWall.EndingPoint.y = atof(CoordNode->FirstChild("Y")->FirstChild()->Value()); LevelWall.bIsRect = FALSE; mWalls.push_back(LevelWall); printf("Path Start x %f end %f\n", LevelWall.StartingPoint.x, LevelWall.EndingPoint.x); printf("Path Start y %f end %f\n", LevelWall.StartingPoint.y, LevelWall.EndingPoint.y); LevelWall.StartingPoint = LevelWall.EndingPoint; } } }
In order to use these, we modify SmashPcMap to load the Level, and it reads the lists out and draws the walls and items.
Here's the new CreateSpace, which puts the walls into the physics space:
void SmashPcMap::CreateSpace(void) { GameLevel::tLevelItem LevelItem; GameLevel::tLevelWall LevelWall; cpShape *pShape; std::vector::iterator it; /* loop through all the walls 1st and draw them */ for (it = mpLevel->mWalls.begin(); it != mpLevel->mWalls.end(); it++) { if (it->bIsRect) { cpVect Verts[4] = {{it->StartingPoint.x, it->StartingPoint.y}, {it->StartingPoint.x, it->EndingPoint.y}, {it->EndingPoint.x, it->EndingPoint.y}, {it->EndingPoint.x, it->StartingPoint.y} }; pShape = cpSpaceAddShape(mpSpace, cpPolyShapeNew(&mpSpace->staticBody, 4, Verts, cpvzero)); } else { pShape = cpSpaceAddShape(mpSpace, cpSegmentShapeNew(&mpSpace->staticBody, it->StartingPoint, it->EndingPoint, 10.0f)); } pShape->e = 0.0f;//9f; pShape->u = 0.0f;//3f; pShape->collision_type = WALL_COL_TYPE; pShape->data = this; // Walls occupy all layers } }
And here's the SmashPcMap::Draw that draws the walls and items:
BOOL SmashPcMap::Draw(cpBody *PlayerBody) { cpFloat LeftX = PlayerBody->p.x - mu32ScreenWidth/2; cpFloat TopY = PlayerBody->p.y - mu32ScreenHeight/2; std::vector::iterator it; /* loop through all the walls 1st and draw them */ for (it = mpLevel->mWalls.begin(); it != mpLevel->mWalls.end(); it++) { sf::Shape oShape; if (it->bIsRect) { oShape = sf::Shape::Rectangle (it->StartingPoint.x - LeftX, it->StartingPoint.y - TopY, it->EndingPoint.x - it->StartingPoint.x, it->EndingPoint.y - it->StartingPoint.y, sf::Color(128, 128, 128, 255)); } else { oShape = sf::Shape::Line(it->StartingPoint.x - LeftX, it->StartingPoint.y - TopY, it->EndingPoint.x - LeftX, it->EndingPoint.y - TopY, 10, sf::Color(128, 128, 128, 255)); } mpApp->Draw(oShape); } // draw all the items std::vector::iterator ItemIt; for (ItemIt = ItemList.begin(); ItemIt != ItemList.end(); ItemIt++) { (*ItemIt)->Draw(PlayerBody->p); } return TRUE; }
void SmashPcItem::CreatePhysicsObjects(void) { if (mbPhysical) { cpFloat Inertia; mpBody = cpBodyNew(500.0f, 500.0f); cpBodySetPos(mpBody, mItemInfo.Location); mpBody->v = cpvzero; /* teleporter doesn't spin */ mpBody->w = 2.0f; mpShape = cpCircleShapeNew(mpBody, (cpFloat)((mItemInfo.pImage->GetWidth() + mItemInfo.pImage->GetHeight())/4), cpvzero); Inertia = cpMomentForCircle(5.0f, 0.0f, (cpFloat)((mItemInfo.pImage->GetWidth() + mItemInfo.pImage->GetHeight())/4), cpvzero); cpBodySetMoment(mpBody, Inertia); mpShape->e = 0.0f; // not elatics, no bouncing mpShape->u = 1.0f; // 0.0 is frictionless mpShape->collision_type = ITEM_COL_TYPE; mpShape->layers = MAP_ITEM_LAYER; mpShape->data = static_cast<void *>(this); cpSpaceAddBody(mpSpace, mpBody); cpSpaceAddShape(mpSpace, mpShape); // Create Collision handler here, only player notifies the item // data can be NULL since we have class stored in Shape cpSpaceAddCollisionHandler(mpSpace, ITEM_COL_TYPE, PLAYER_COL_TYPE, SmashPcItem::PlayerCollision, NULL, NULL, NULL, NULL); } }
int SmashPcItem::PlayerCollision(cpArbiter *arb, struct cpSpace *space, void *data) { SmashPcItem *pItem; SmashPcPlayer *pPlayer; cpShape *pItemShape, *pPlayerShape; cpArbiterGetShapes(arb, &pItemShape, &pPlayerShape); pPlayer = reinterpret_cast(pPlayerShape->data); pItem = reinterpret_cast(pItemShape->data); // We have the player that hit, and the item he hit pItem->NotifyHit(pPlayer); }
Basically, it get the player and item objects that collided and notifies that item of being hit. Currently NotifyHit() doesn't do anything.
Next time, I'm going to do a little re-design. I notice the classes SmashPcPlayer, SmashPcItem, and SmashPcBullet all have physicsal properties and can eb Drawn, which I should inherit from one class to stop from using the same code over and over.
Tuesday, October 25, 2011
Added Bullet/Wall Collision...
OK, I found a major no-no I was doing in my SmashPcBullet deconstructor; I wasn't removing and freeing the physics body/shape from the world. And, that was bad. So, I've fixed that.
Get the code here:
SmashPC 11-25-11_2 Collision
Here's the fixed deconstructor:
And, I added this line in the constructor to add a collision handler:
This call sets the function SmashPcBullet::WallCollision as the callback for collisions between type BULLET_COL_TYPE (which is all bullets) and WALL_COL_TYPE (all walls).
And, here's WallCollision (defined static in header file):
Here's a in-game picture shooting bullets against the wall:
Get the code here:
SmashPC 11-25-11_2 Collision
Here's the fixed deconstructor:
SmashPcBullet::~SmashPcBullet() { delete mpSprite; if (mBulletDetails.pGlowImage) { delete mpGlowSprite; } cpSpaceRemoveBody(mpSpace, mpBody); cpSpaceRemoveShape(mpSpace, mpShape); cpBodyFree(mpBody); cpShapeFree(mpShape); }
And, I added this line in the constructor to add a collision handler:
// Create Collision handler here, only wall notifies the bullet // other handlers get notifed of hit by bullet // data can be NULL since we have class stored in Shape cpSpaceAddCollisionHandler(mpSpace, BULLET_COL_TYPE, WALL_COL_TYPE, SmashPcBullet::WallCollision, NULL, NULL, NULL, NULL);
This call sets the function SmashPcBullet::WallCollision as the callback for collisions between type BULLET_COL_TYPE (which is all bullets) and WALL_COL_TYPE (all walls).
And, here's WallCollision (defined static in header file):
int SmashPcBullet::WallCollision(cpArbiter *arb, struct cpSpace *space, void *data) { SmashPcBullet *pBullet; cpShape *pBulletShape, *pWallShape; cpArbiterGetShapes(arb, &pBulletShape, &pWallShape); printf("Callback!\n"); pBullet = reinterpret_castIf you recall, in the SmashPcBullet constructor, we assign mpShape->data = this. So, in the callback, we can extract the Bullet class and set it to Dead, so it will be removed.(pBulletShape->data); // Mark bullet for removal pBullet->SetDead(); }
Here's a in-game picture shooting bullets against the wall:
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:
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:
The function SelectBullet simply sets which type of bullet the weapon will fire.
The last function, FireBullet:
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:
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:
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:
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:
Next time I'll add Collisions, I (almost) promise!
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::vectorBulletList; . . . 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!
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:
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:
And, In CreateSpace() we create fake walls that just surrounds the world:
Looking in SmashPcPlayer, We added the starting world location of the player and the physics space to the constructor. Here is is:
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:
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:
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).
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).
SmashPC is Started!
So, I've decided specifically what type of game I'll be making. It'll be a top-down Alien Swarm type game (for those old guys, think SmashTV). In fact, I've named it SmashPC.
What I've done for this initial code drop is implement the basics as far as SFML window, simple sprite drawing, playing Sounds, and Input and input configuration.
You can get the full source code and project here:
(Link Removed, get newer version)
First, we need to create a project. So, using Code::Blocks, I made a project, and pointed at all the libraries I would need.
Here's my settings for the Compiler and Linker:
What I've done for this initial code drop is implement the basics as far as SFML window, simple sprite drawing, playing Sounds, and Input and input configuration.
You can get the full source code and project here:
(Link Removed, get newer version)
First, we need to create a project. So, using Code::Blocks, I made a project, and pointed at all the libraries I would need.
Here's my settings for the Compiler and Linker:
(Check out the links for sfml, tinyxml, and chipmunk-physics in the 1st blog entry to get those libraries. I use sfml2.0, but 1.6 should work fine).
Next, I add the main source code (in SmashPc.cpp), and a simple main loop. Here's what I have so far:
int main(int argc, char *argv[]) { sf::Event Event; BOOL bFullScreen; GameSound::Init("SmashPcSounds.xml"); Utilities::LoadConfig("SmashPcCfg.xml"); Utilities::ScreenResolutionGet(gu32GravScreenX, gu32GravScreenY, bFullScreen); sf::RenderWindow App(sf::VideoMode(gu32GravScreenX, gu32GravScreenY, 32), "SmashPC", (bFullScreen ? sf::Style::Fullscreen : sf::Style::Default)); gpOurPlayer = new SmashPcPlayer(&App); srand(time(NULL)); App.ShowMouseCursor(true); App.SetCursorPosition(gu32GravScreenX/2, gu32GravScreenX/2); // main game loop while (App.IsOpened()) { float FrameTime; const sf::Input& Input = App.GetInput(); 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(); FrameTime = App.GetFrameTime(); if (FrameTime > 1/60.0f) { //printf("Frametime %f expected %f\n", FrameTime, 1/60.0f); } else { Sleep((U32)((1/60.0f - FrameTime)*1000.0f)); FrameTime = 1/60.0f; } /* clear screen and draw map */ App.Clear(); gpOurPlayer->Draw(); App.Display(); } return 0; }
You see some simple SFML setup, but we're also calling the functions GameSound::Init() and Utilities::LoadConfig().
Gamesound::Init loads the sounds stored in SmashPcSounds.xml. Here's that function:
void GameSound::Init(char *pcFilename) { if (bNeedsInit) { TiXmlDocument doc(pcFilename); // load and check if it was successful if (doc.LoadFile()) { TiXmlElement *root = doc.RootElement(); for(TiXmlElement* example = root->FirstChildElement(); example; example = example->NextSiblingElement()) { char cFile[32]; std::string SoundStr = example->ValueStr(); printf("Found SoundName %s .. ", SoundStr.c_str()); SoundMap[SoundStr] = new sf::SoundBuffer(); sprintf(cFile, "Sfx/%s", example->GetText()); if (!SoundMap[SoundStr]->LoadFromFile(cFile)) { printf("Failed loading sound %s!\n", cFile); } } } else { printf("Failed opening GravithonSounds.xml\n"); } bNeedsInit = FALSE; } }
This is using tinyxml to parse the xml file, and it uses std::map to associated a string to a sfml sound buffer. This way, you play a sound you'd just pass in the string to play, like GameSound::Play("PeaShooter");
The next piece you saw was the call to Utilities::LoadConfig(). This function loads the SmashPcCfg.xml file and setups the users prefrences with respect to controls, mouse sensitivty, and screen resolution. Take a look at the Utilities.cpp and.h for more info on how it does that.
Finally, there's SmashPcPlayer class. Here's the constructor:
SmashPcPlayer::SmashPcPlayer(sf::RenderWindow *pApp) : mpApp(pApp), 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); // Sound notifying a player has spawned GameSound::Play("PlayerSpawn"); }
We make a call to load the image (we use the Utilities function since we want to make sure we won't load the image more than once), and create a sprite from the image, set the sprite to the middle of the screen, and play a sound, noting the player has spawned in the world.
The SmashPcPlayer->Draw () currently just draws the sprite where the payer is.
And, SmashPcPlayer->CheckInput() simply moves the player around and plays a gunshot sound when you press the Fire button (configured as left mouse button).
Next time we'll add in chipmunk-physics 2d engine to handle all the big work for us as far as movement and collision detection.
Subscribe to:
Posts (Atom)