Wednesday, November 16, 2011

SmashPc: The Postmortem

When I set out to make SmashPC, the main point was to try and share the ease of making a 2D game using SFML, chipmunk-physics, tinyxml, and Gleed2d.

For this post, I'll go over the main points, and cover some of the improvements I could have done.

Overview of the Main Points

To me, the biggest point I want to make is how a 2d physics engine makes writing games a snap.  No longer do I have to handle moving every object within the world, checking all the collision possibilities, handling the odd collision detection issues we used to have, or any other number of things.  Sure, there was a time when it was "fun" to handle all that stuff, but it seriously impeded making a cool game.

Instead, we tell the physics engine what object are in the world; their mass, force, angle, rotational velocity, friction  and all the other physical properties.  We tell the physics engine to notify us when certain objects collide, so we can do fun stuff with them.  Then, we basically say, "Go", and it goes.  With little effort, I had 200 enemies all running towards me, pushing each other around, in a natural group.  I think it's really cool stuff.

I also really like the power SFML provides.  It makes doing anything you want with sprites a snap (scaling, rotation, etc.).  Input is handled easily, and sound is very well done.  I didn't even bother with the spatial sound available, but it's another strong reason to use SFML.

Throw in tinyxml, to handle parsing all the data, and gleed2d, for creating the levels, and saving them in XML, and you have the recipe for making games very easily.

What I could have done better


The first thing I want to point out is the lack of a proper design before I began coding.  I had an idea of what I wanted, but nothing concrete.  I didn't always write code with the end result in mind, and that caused me to do some re-writes a few times (ie, adding the PhysicalObject parent class, and moving all objects to the SmashPcData class).  This also added to my other issues.

I should have used xml to detail enemies; instead, I just wrote the properties in the SmashPcEnemy class (Their speed, movement update time, etc.).  This would also have been used to define what items are released by the "special" enemy; instead, I just hard-coded it in the SmashPcData class.


I would re-do the GameLevel class.  Initially, I included the option to add items to the map, but I didn't once use it.  That caused me to waste some time and code.


I would like to re-do the weapons class.  How I handled switching between bullets was poor.

Final Word


In the end, I just wanted to get it out, and I took some short-cuts to get there, but, I think the main point of this blog was fulfilled.  I hope I showed how you can use the tools to make interesting 2D games.

I may use this blog for other games I make.  I'm thinking of a side-scroller type game, kinda like the Contra games.  We'll just have to see.

Until then!

Tuesday, November 15, 2011

Tying up loose SmashPc ends

Last night I put my game as it sits on the blog.  I wanted to tie up the last few steps I added.

Here's the latest source code:
SmashPc 11-15-11

First, the Rocket, I added Smoke trails. I added the <SmokeTrails> tag to the SmashPcWeapons.xml.
I also added this Smoke element to the SmashPcItems.xml:
<Smoke Type="Fadeout"> <Image>smoke-small.png</Image> <Interact>No</Interact> <Value>0</Value> <TimeToLive>500</TimeToLive> <Rotate>0</Rotate> </Smoke>

In SmashPcBullet::Draw(), I added this check at the end:
// If we're drawing smoketrails, then lay one down, and set it to fade out if (mbSmokeTrail) { cpVect Loc = mpBody->p; GameLevel::tLevelItem LevelItem; LevelItem.ItemName = "Smoke"; // Set location at back of bullet Loc.x -= cos(mpBody->a)*mpImage->GetWidth()/2; Loc.y -= sin(mpBody->a)*mpImage->GetHeight()/2; LevelItem.Location = Loc; LevelItem.ImageName = ""; // get it from xml SmashPcItem *pSmokeItem = new SmashPcItem(mGameData, mpApp, LevelItem, mpSpace); mGameData.AddActiveItem(pSmokeItem); }

Basically, we add a "Smoke" item every time we update drawing the rocket.  Since the TimeToLive is short, though, it won't be on the screen long.

Now, if we check in SmashPcItem::Draw(), we do this:
// if the type is FadeOut, then lessen tghe alpha if (mpItemDetails->Type == "Fadeout") { sf::Color AlphaColor(0, 0, 0, 255); U32 u32TimeDiff = (timeGetTime() - mu32StartTime)+1; AlphaColor.a = 255 - ((255*u32TimeDiff)/mu32TimeToLive); PhysicalObject::Draw(PlayerLoc, AlphaColor); }

This basically continually lowers the alpha on the image, based on the time to live, and the amount of time it has left to live.  So, this give the "Smoke Trail" as seen in this screen shot:

The other stuff I did was handle changing levels, releasing an item when you kill a specific enemy, game over, and game won.  This wasn't actually a big deal, but I took short cuts.  I really should have created another configuration file that defines the levels, and modified the EnemySpawn items to detail what item to release.

Here's the code, in the main loop before pOurPlayer->CheckInput(), that handles these pieces:

if (pOurPlayer->mu32Health == 0) { // Check for Lives? if (u32Lives > 0) { Sleep(1000); u32Lives--; delete pOurPlayer; pOurPlayer = new SmashPcPlayer(GameData, &App, pMap->GetSpace(), pMap->GetSpawnLocation(), &Weapon); } else { // just quit I guess GameSound::Play("GameOver"); Sleep(4000); break; } } if (GameData.IsLevelOver()) { U32 u32OldArmor = pOurPlayer->mu32Armor; BOOL bGameOver = FALSE; // Change Level if (Level == NumLevels) { bGameOver = TRUE; } else { Level++; } // Call the intermission function // also handles end-game Intermission(&App, pMap, pOurPlayer, &GameData, bGameOver, u32ScreenX, u32ScreenY); // Reset GameData to make sure everything is cleared GameData.Reset(); delete pOurPlayer; delete pMap; // Create new amp with new level data pMap = new SmashPcMap(Levelnames[Level-1].c_str(), GameData, &App); Weapon.AssignSpace(pMap->GetSpace()); // Start player at new spawn locations pOurPlayer = new SmashPcPlayer(GameData, &App, pMap->GetSpace(), pMap->GetSpawnLocation(), &Weapon); // reset old armor pOurPlayer->mu32Armor = u32OldArmor; }

We check 1st if our guy is dead.  if so, delay a second (the SmashPcPlayer->NotifyHit() handles playing the death sound), then restart the guy.  If we're out of lives, play the Game Over sound and then quit.

Then, I check if we've killed all the enemies in the level, and if so, Call the Intermission() function, which basically continues to draw the level, plays music, and puts up the "Level Complete" (or the "You've Won") status.  Reset the GameData (which clears off all items and bullets from the active lists), deletes the player and map, and re-creates the map with the next level.

We tell the Weapon class there's a new Physics Space, create a new player in the new map, and reset the armor.

That's basically it!  The next post will be a kind of postmortem, going of the specifics, and what I could've done better.

SmashPC 1.0 Ready!

I managed to cobble together enough code, and make a few levels to be able to release an actual game.  It's late, so I'll wait to go over the specific's tomorrow.  For now, download and extract the .zip (windows only), check the readme.txt, modify the SmashPcCfg.xml to configure your controls if you wish, and go!

Let me know if there are any issues.

SmashPc 1.0

EDIT: The previous version didn't have all the .dll's required to play.  If it failed to run previously, try this one.

Saturday, November 12, 2011

Some Flamethrowing goodness!

I just wanted to post a video and picture of the enemies, and the flame-thrower I added in action.

In the SmashPcWeapons.xml, I added this:
<FlameThrower> <Image>flame.png</Image> <Velocity>500</Velocity> <Damage>6</Damage> <Refire>10</Refire> <TimeToLive>700</TimeToLive> <Fadeout>Yes</Fadeout> <DegreesOff>10</DegreesOff> <ContinuousSound>Yes</ContinuousSound> </FlameThrower>

Fadeout means I want the bullet to fadeout during it's time to live.  DegreesOff means I want it to fire at a random angle of 10 degrees off the straight angle.
And, I had to modify SmashPcBullet.cpp to handle these changes:

In SmashPcBullet Constructor, I added this:
// If this gun shoots off a bit, modify the directions if (BulletDetails.u32DegreesOff) { cpFloat RandomDegrees = (cpFloat)(((S32)rand() % (S32)BulletDetails.u32DegreesOff) - (S32)BulletDetails.u32DegreesOff/2); mpBody->a += RandomDegrees*(PI/180.0f); mpBody->v.x=cos( mpBody->a)*BulletDetails.Speed; mpBody->v.y=sin( mpBody->a)*BulletDetails.Speed; }

And, in the SmashPcDraw(), I added this check:
if (mbFadeout) { U32 u32TimeDiff = (timeGetTime() - mu32StartTime)+1; GlowColor.a = 255 - ((255*u32TimeDiff)/mu32TimeToLive); // Change glow color to just alpha and make fade GlowColor.r = 0; } PhysicalObject::Draw(PlayerLoc, GlowColor);

Anyway, here's the picture and video!





Friday, November 11, 2011

I'm not dead! Enemies added!

I apologize, it's been a while since I updated.  I have been busy, but I finally am able to get back to this.

And, the big news is, I've added our 1st enemies!  OK, they're REALLY stupid, they just kind of move towards the player, shooting occasionally.  Anyway, onto the code.

Here's a link to the code:
SmashPc Code 11-12-11

Before I get to the good stuff, I did a little more house cleaning..  I moved all the objects that live in the game (Items, Bullets, and Enemies) into the SmashPcData object, and let it keep the lists and call the update functions.  So, now my main loop looks like this:

// main game loop while (App.IsOpened()) { pOurPlayer->CheckInput(); cpSpaceStep(pMap->GetSpace(), 1.0f/60.0f); /* clear screen and draw map */ App.Clear(sf::Color(200, 200, 200, 255)); pMap->Draw(pOurPlayer->GetBody()); // Update all the bullets in the world GameData.UpdateBullets(pOurPlayer->GetBody()); // check for items as well GameData.UpdateItems(pOurPlayer->GetBody()); // Finaly update all enemies GameData.UpdateEnemies(pOurPlayer->GetBody()); pOurPlayer->Draw(); App.Display(); if (CheckGameEnd(&App)) { App.Close(); break; } }

You can see it's pretty simple, and probably won't have much more to add.  I suppose handling a dead player and changing levels.

So, how do Enemies get added?  In the level, I have "EnemySpawn" items, and they spawn enemies at a given rate.  The plan is to give these spawn points levels, and the different enemy levels will make the enemies smarter, faster, tougher, etc.

Here's the piece from SmashPcItem.xml for EnemySpawn1 (for level 1 enemies):
<EnemySpawn1 Type="EnemySpawn"> <Interact>No</Interact> <Value>50</Value> <ReleaseRate>5000</ReleaseRate> <EnemyLevel>1</EnemyLevel> <Rotate>0</Rotate> </EnemySpawn1>

Which means we release a level 1 enemy every 5 seconds until 50 have been released.

So, in SmashPcItem::Update(), we have this (Let me say, I really need to inherit from SmashPcItem, and make a SmashPcEnemySpawn class, but that'll be later):

BOOL SmashPcItem::Update(void) { if (mu32TimeToLive) { if (timeGetTime() > mu32TimeToLive) { // Mark Item for deletion mbActive = FALSE; GameSound::Play("Despawn"); return FALSE; } } // Release Enemies into the level if (mpItemDetails->Type == "EnemySpawn") { if (mu32NumReleaseLeft && (mu32LastRelease + mpItemDetails->u32ReleaseRate < timeGetTime())) { SmashPcEnemy *pEnemy; mu32LastRelease = timeGetTime(); mu32NumReleaseLeft--; // Create an enemy pEnemy = new SmashPcEnemy(mGameData, mpApp, mpSpace, mpBody->p, mpItemDetails->u32EnemyLevel); // Check if this enemy should release something mGameData.AddActiveEnemy(pEnemy); } } return mbActive; }

And, our enemy class looks like this (I'm including it all here):
/****************************************************************************** * * SmashPcEnemy() - Creates a player, and places him in the space * ******************************************************************************/ SmashPcEnemy::SmashPcEnemy(SmashPcData &GameData, sf::RenderWindow *pApp, cpSpace *pSpace, cpVect Location, U32 u32Level) : PhysicalObject(pApp, pSpace, "Gfx/blueenemy.bmp", "", Location, PI/2.0f, 0.0f, TRUE), mu32Health(20*u32Level), mu32LastFire(timeGetTime()), mu32LastMoveUpdate(0), mGameData(GameData) { printf("Enemy created!\n"); // Set the Velocity limit to our fake movement mpBody->v_limit = ENEMY_SPEED; mpShape->collision_type = ENEMY_COL_TYPE; mpShape->layers = ENEMY_PLAYER_LAYER; mpShape->data = (void *)this; // Create Collision handler here cpSpaceAddCollisionHandler(mpSpace, ENEMY_COL_TYPE, BULLET_COL_TYPE, SmashPcEnemy::BulletCollision, NULL, NULL, NULL, NULL); // Sound notifying a player has spawned GameSound::Play("EnemySpawn"); } /****************************************************************************** * * ~SmashPcEnemy() - Removes a player * ******************************************************************************/ SmashPcEnemy::~SmashPcEnemy() { } /****************************************************************************** * * Update() - Do enemy things * ******************************************************************************/ void SmashPcEnemy::Update(cpBody *pPlayerBody) { U32 u32Time = timeGetTime(); // check if we need to update the movement if (mu32LastMoveUpdate + ENEMY_UPDATE_MOVE < u32Time) { cpFloat Angle = atan2(pPlayerBody->p.y - mpBody->p.y, pPlayerBody->p.x - mpBody->p.x); // add randomization to angle cpFloat RandomDegrees = (cpFloat)(((S32)rand() % 20) - 10); Angle += (RandomDegrees*PI)/180.0f; cpBodySetAngle(mpBody, Angle); // Change force towards player mForce = cpv(ENEMY_FORCE*cos(Angle), ENEMY_FORCE*sin(Angle)); //mpBody->f = cpv(ENEMY_FORCE*cos(Angle), ENEMY_FORCE*sin(Angle)); mu32LastMoveUpdate = u32Time; } mpBody->f = mForce; if (mu32LastFire + ENEMY_REFIRE_TIME < u32Time) { SmashPcData::tBulletList BulletList; mGameData.GetBulletList(BulletList); SmashPcBullet *pBullet = new SmashPcBullet(mpApp, BulletList[0], mpBody, mpSpace, FALSE); mGameData.AddActiveBullet(pBullet); mu32LastFire = u32Time; } } /****************************************************************************** * * NotifyHit() - Notify the enemy it has been hit by a bullet * ******************************************************************************/ void SmashPcEnemy::NotifyHit(SmashPcBullet *pBullet) { if (mu32Health < pBullet->GetDamage()) { // Enemy Dead mu32Health = 0; // Play dead sound } else { GameSound::Play("PlayerHit"); mu32Health -= pBullet->GetDamage(); } pBullet->SetDead(); } /****************************************************************************** * * BulletCollision() - Callback for when Enemy hits a bullet * ******************************************************************************/ int SmashPcEnemy::BulletCollision(cpArbiter *arb, struct cpSpace *space, void *data) { SmashPcBullet *pBullet; SmashPcEnemy *pEnemy; cpShape *pBulletShape, *pEnemyShape; cpArbiterGetShapes(arb, &pEnemyShape, &pBulletShape); pEnemy = reinterpret_cast<SmashPcEnemy*>(pEnemyShape->data); pBullet = reinterpret_cast<SmashPcBullet*>(pBulletShape->data); // We have the enemy that hit, and the bullet he hit pEnemy->NotifyHit(pBullet); return 0; }

OK, some explanation. In the constructor, we setup the specific physics attributes, including the collision handler.  We're basically saying we want the enemy to get a callback when it's hit by the player's bullet.  The Layer is set so it will be in the same collision layer as the player and the player bullets, even though we only want to get notified of hitting bullets (see chipmunk-physics for more on collision layers).

In SmashPcEnemy::Update(), we move the enemy towards the player every so often.  Notice we don't reset the velocity when we change the force, so the enemy will somewhat slowly change direction, continuing on it's previous path until the force can change it's direction fully.  This will change for different "levels" of enemies, but this is it for now.

The BulletCollision() simply calls the enemies NotifyHit(), with the bullet it hit, and in NotifyHit(), we reduce the health of the enemy.

Finally, there is no specific enemy Draw, it uses the PhysicsObject::Draw() by default (the inherited class).

The other thing I did was add a Collision Handler to the SmashPcPlayer class, which I didn't have before.  It's basically identical to the Enemy's, except we subtract the damage from the armor 1st if the player has armor.

Well, this results in some bad enemies.  I'll have to adjust the AI some, but here's some pics.  The first one, I just let the enemies pile up, and watched em run around.  It's def a group mentality.


OK, until next time!