Windows Demo (.zip) (untested for Vista/7, should work fine on XP) 8.5MB
I'd be interested to know if/how it runs on your system. Any comments welcome.
IMPORTANT: The first line in "config.cfg" needs to be the path of the folder. If it doesn't work, please let me know. Also, a value of '1' for VSYNC doesn't really work yet.
Controls:
E,S,D,F = movement (this is so your pinky can use 'A')
A = dash (too powerful, and not working correctly)
1-5 = cycle camera size
Z,X,C,V,B,N = the different weapons
LMB = fire weapon
mouse wheel up/down, and button = changes the camera between 2-4 (for ease)
Esc = quits
F5 = take screenshot
I've figured out why I'm so slow to implement some things. I tend to write my abstract classes first, and gradually define the more specific classes to fit my class hierarchy. This takes forever because I get really picky if something doesn't fit perfectly with what's established, and I waste a lot of time trying to make everything perfect. My new method (common sense) is to write the classes that actually DO SOMETHING first, so I have new features implemented even if the code design isn't pretty. Then I can consolidate the common functionality afterward.
My goals for this month are to get a weapon/projectiles system going, get sound working, add some content, and if time allows, experiment with gameplay. Gameplay is my highest priority as a designer, but I need to focus on other important things for now.
I've started the weapons system. The system will manage all projectiles within the level, fired from any source. At first I thought it would make sense for each armed character to "own" it's own projectile system, but then enforcing a memory limit would be difficult. So instead the level "owns" the projectile manager and gives the pointer to a static member of the character base class.
// .htypedef enum { WEAPON_NULL, WEAPON_BLASTER} weaponType; class WeaponSystem{ public: WeaponSystem(); ~WeaponSystem(); void Fire( b2World* world, weaponType type, const vec2& origin, const vec2& trajectory, int weaponMode ); void Update(ulong dT); void Render(); private: static const int blasterNum = 100; std::vector< BlasterRound* > blasterRounds; std::vector< BlasterRound* >::iterator blasterRoundsIt; BlasterRound* blasterMother;};// .cppWeaponSystem::WeaponSystem(){ blasterMother = new BlasterRound; blasterMother->LoadXml( "blaster.xml" ); blasterRounds.reserve( blasterNum ); for( int i=0; i < blasterNum; i++ ) { blasterRounds.push_back( new BlasterRound( *blasterMother ) ); } blasterRoundsIt = blasterRounds.begin(); //delete blasterMother; //blasterMother = NULL;}WeaponSystem::~WeaponSystem(){ for( int i=0; i < blasterNum; i++ ) delete blasterRounds; blasterRounds.clear(); //////////////////////////////////////////////////////////////////////////// // no heap corruption when this is released after the blasterRounds! delete blasterMother; ////////////////////////////////////////////////////////////////////////////}void WeaponSystem::Fire( b2World* world, weaponType type, const vec2& origin, const vec2& trajectory, int weaponMode ){ switch( type ) { case WEAPON_BLASTER: for( int i = 0; i < blasterNum; i++, blasterRoundsIt++ ) { if( blasterRoundsIt == blasterRounds.end() ) blasterRoundsIt = blasterRounds.begin(); if( (*blasterRoundsIt)->IsActive() == false ) { (*blasterRoundsIt)->Fire( world, origin, trajectory, weaponMode ); break; } } break; default: break; }}void WeaponSystem::Update(ulong dT){ for( int i=0; i < blasterNum; i++ ) blasterRounds->Update( dT );}void WeaponSystem::Render(){ for( int i=0; i < blasterNum; i++ ) blasterRounds->Render();}
So here's the weapon system class. It just creates a round, called blasterMother, and copies the instance so the XML file is only loaded once. The number of active rounds is limited to 100 at the moment. I was thinking of borrowing an idea from Halo and making some weapons overheat if you shoot them for too long. That would give a reason for why you can't shoot an unlimited number of rounds (other than ammo), and force the player to break for cover once in awhile.
If I delete the blasterMother pointer in the constructor, it causes a heap corruption. If I delete it in the weaponSystem destructor its fine, but I don't want to have unused instances laying around because I can't figure it out. I think my Entity/Sprite/BlasterRound copy contructors are good. Nothing else causes the heap corruption...
// .h#include "Sprite.h"// temporary!enum{ MODE_NORMAL = 0, MODE_BOUNCY, MODE_FLAME, MODE_HEAVY, MODE_HIGHCALIBER, MODE_STICKY};class BlasterRound : public Sprite{ public: BlasterRound(); BlasterRound( const BlasterRound& ); virtual ~BlasterRound(); bool IsActive(){ return active; } void Fire( b2World* world, const vec2& origin, const vec2& trajectory, int weaponMode ); virtual void onLoad(); virtual void onUpdate( ulong dT ); virtual void onRender( float blend ); virtual void onCollision(const contactPoint& point); protected: int damage; bool active; bool render; bool exploded; int hitCount; int timeLeft; int explodeTime; b2Vec2 initialPos; b2Vec2 collidePosition; // temporary!!! int mode; bool stickyFlag; b2Joint* stickyJoint; b2Body* stickToBody; //b2CircleDef expDef; //b2Shape* expShape;};// .cppBlasterRound::BlasterRound(): damage( 1 ), active( false ), render( false ), exploded( false ), hitCount( 0 ), timeLeft( 1000 ), explodeTime( 0 ) //expShape( NULL ){ mode = MODE_NORMAL; stickyFlag = false; stickyJoint = NULL; stickToBody = NULL;}BlasterRound::BlasterRound( const BlasterRound& rhs ): Sprite( rhs ), damage( rhs.damage ), active( rhs.active ), render( rhs.render ), exploded( rhs.exploded ), hitCount( rhs.hitCount ), timeLeft( rhs.timeLeft ), explodeTime( rhs.explodeTime ) //expShape( NULL ){ mode = MODE_NORMAL; stickyFlag = false; stickyJoint = NULL; stickToBody = NULL; exploded = false;}BlasterRound::~BlasterRound(){}void BlasterRound::Fire( b2World* world, const vec2& origin, const vec2& trajectory, int weaponMode ){ active = true; hitCount = 0; explodeTime = 200; exploded = false; render = true; ////////////// //bodies[0]->pointer->SetBullet(true); // this didn't work bodies[0]->isBullet = true; //bodies[0]->shapes[0]->SetCollisionProfile( CB_FRIENDLY ); //bodies[0]->shapes[0]->SetCollisionGroup( 2 ); //////////// vec2 variance; variance.x = (random_f()-0.5f) * 0.04f; // accuracy (between .5 and .01) variance.y = (random_f()-0.5f) * 0.04f; float multiplier = 20.0f; mode = weaponMode; switch( mode ) { case MODE_FLAME: SetCurrentSequence("explode"); multiplier = 20.0f; timeLeft = 300; break; case MODE_HIGHCALIBER: SetCurrentSequence("idle"); multiplier = 80.0f; timeLeft = 500; variance.x = (random_f()-0.5f) * 0.01f; // accuracy (between .5 and .01) variance.y = (random_f()-0.5f) * 0.01f; break; case MODE_HEAVY: SetCurrentSequence("idle"); multiplier = 20.0f; timeLeft = 5000; break; case MODE_BOUNCY: SetCurrentSequence("idle"); multiplier = 30.0f; timeLeft = 300; break; default: SetCurrentSequence("idle"); multiplier = 40.0f; timeLeft = 1000; break; } setPosition( origin ); Create( world ); initialPos = b2Vec2( origin.x, origin.y ); variance += trajectory; b2Vec2 velocity( variance.x, variance.y ); velocity *= multiplier; bodies[0]->pointer->SetLinearVelocity( velocity ); }void BlasterRound::onLoad(){ timeLeft = 0; explodeTime = 0;}void BlasterRound::onUpdate( ulong dT ){ if( !active ) return; if( exploded ) { if( explodeTime <= 0 ) { /*switch( mode ) // trying to release the joint apparently deletes twice. Need to find out why. { case MODE_STICKY: if( stickyJoint ) { world->DestroyJoint( stickyJoint ); stickyJoint = NULL; } break; default: break; }*/ active = false; render = false; exploded = false; DestroyCollisionModel(); }else explodeTime -= dT; } else { // the round needs to start the explode/death sequence if( timeLeft <= 0 ) { exploded = true; SetCurrentSequence("explode"); switch( mode ) { case MODE_BOUNCY: DestroyCollisionModel(); break; case MODE_STICKY: break; default: setPosition( vec2(collidePosition.x, collidePosition.y) ); break; } } else // the round is still active, update timer { timeLeft -= dT; switch( mode ) { case MODE_FLAME: bodies[0]->pointer->ApplyForce( b2Vec2(0,(float)dT * 0.05f), bodies[0]->pointer->GetWorldPoint(b2Vec2(0,0)) ); break; case MODE_STICKY: { // stuck to something if( stickyFlag ) { setPosition( vec2(collidePosition.x, collidePosition.y) ); bodies[0]->pointer->SetLinearVelocity( b2Vec2(0.0f,0.0f) ); if( bodies[0]->pointer && stickToBody ) { b2DistanceJointDef stickyDef; stickyDef.Initialize( bodies[0]->pointer, stickToBody, bodies[0]->pointer->GetWorldPoint( b2Vec2(0,0) ), collidePosition); //stickyDef.body1 = bodies[0]->pointer; //stickyDef.body2 = stickToBody; //stickyDef.localAnchor1 = bodies[0]->pointer->GetLocalPoint( b2Vec2(0,0) ); //stickyDef.localAnchor2 = stickToBody->GetLocalPoint( collidePosition ); //b2Vec2 d = stickyDef.localAnchor1 //stickyDef.length = 0.0f; stickyJoint = world->CreateJoint( &stickyDef ); } stickyFlag = false; } } break; //case MODE_HIGHCALIBER: //break; default: break; } } } if( render ) Sprite::onUpdate( dT );}void BlasterRound::onRender( float blend ){ if( !render ) return; SetColor( WHITE ); if( exploded && explodeTime > 0 ) { if( mode == MODE_HEAVY ) { glPushMatrix(); glScalef( 6.0f, 6.0f, 1.0f ); Sprite::onRender( blend ); glPopMatrix(); } else Sprite::onRender( blend ); }else { if( bodies[0]->pointer ) { vec2 velocity( bodies[0]->pointer->GetLinearVelocity().x, bodies[0]->pointer->GetLinearVelocity().y); b2Vec2 shotDistance = bodies[0]->pointer->GetPosition() - initialPos; float mag = min( velocity.magnitude() / 10.0f, shotDistance.Length() ); velocity.normalize(); float aim_angle = acosf( velocity.dot(vec2(0,1)) ); aim_angle = ( velocity.x <= 0 )? aim_angle : -aim_angle; float roundScale = 1.0f; if( mode == MODE_HEAVY ) roundScale = 3.0f; glPushMatrix(); glRotatef( RAD_DEG( -render_state.angle + aim_angle ), 0, 0, 1 ); glScalef( roundScale, ( roundScale + mag ), 1.0f ); Sprite::onRender( blend ); glPopMatrix(); } }}void BlasterRound::onCollision(const contactPoint& point){ if( point.obShape->IsSensor() ) return; /*std::string shapeName = (char*)point.obShape->GetUserData(); if( shapeName == "blasterShape" ) return;*/ if( ++hitCount == 1 ) { //m_body->SetBullet(false); collidePosition = point.position; switch( mode ) { case MODE_BOUNCY: break; case MODE_STICKY: if( stickyFlag == false ) { stickyFlag = true; stickToBody = point.obBody; } break; default: FlagForDestruction(); timeLeft = 0; break; } }}
Here's the BlasterRound class in it's entirety. I wanted to post it before I break it into separate classes. I added a temporary parameter to the Fire method, "weaponMode". This is to quickly test different projectile behaviors without writing more classes.
The Fire() method creates the collision model and gives it an initial velocity. The timeLeft var puts a timer on the round in case it doesn't hit anything. The current animation sequence is chosen here, too. There are only two sequences for my tests, "idle" and "explode". I realize now that the finished weapon classes won't load from an XML file, they'll just create their collision definitions when constructed.
onUpdate() checks whether the round has exploded and needs to destroy it's collision model. If it doesn't, it updates the timer variable 'timeLeft'. So far, only the flame-thrower (and stickybomb) mode needs to affect the body during update. I'm experimenting with the stickybomb and its not working correctly just yet. It stretches upward after its attached, its weird.
All the weapon "modes" have the same firing interval at the moment, I'll fix that soon. I left it at 30ms so the flame-thrower mode looks okay. All the weapons shoot fast, which sometimes isn't appropriate.
If you playtest in the level editor, it will probably crash when you exit. There's an unsolved bug involving the weapon system. Also, be aware that 'c' toggles collision rendering AND the flame-thrower, 'b' toggles the background/sniper gun. That just means you have to press it twice, I'm not going to change the bindings. I'll probably have the mouse wheel cycle the weapons soon.
Well, there's a lot to improve, but its a start. Next step is audio!
I have some old code laying around that might come in handy for your filesystem though :)
Not the best implementation looking back, but that's a start.