Intel sponsors gamedev.net search:   
Reinvention and Further Development of the WheelBy Staffan E      

Citizen gameplay clip. (April '09)

By: Staffan Einarsson

I'm a hobbyist game developer and in this journal I comment my work and the game projects that I work on. Right now I work alone, trying to become a more complete developer by tackling all areas of game making on my own.

Current projects:
  • Citizen - A 2D space shoot-em-up with fluid controls and intense combat action. I have two goals with this project. One is to find out how far I can take the shmup concept. The second is to get some first hand experience with designing a data-driven system.
    » The first prototype P1 (April '09) can be downloaded here.
    » To the right you can see a gameplay video.
Finished projects:
  • BlockStacker (June '08) - A tetris clone that I developed in Adobe Flash and ActionScript. This was my first completed game and I'm very proud of it, considering how many ambitious projects I had started before it. It took me three months to finish, working evenings and weekends or whenever I could find the time. Maybe I will make more small puzzle games some day.

Wednesday, November 4, 2009
One of the things I picked up while on the team was doing unit tests on written code. Mainly because the person I worked in pair with is sort of a TDD evangelist. He recommended me reading the book Test Driven Development By Example by Kent Beck, a good book which I've got half way through. I've felt deep in my stomach for some time that sooner or later I had to start testing the Citizen code-base but you know how it is.

Attacking from this new angle I've tried to test drive the design of new components that I've added to Citizen. This has been very informative and have proven why it is best to start test driving from the start. One of the inherent problems of testing code that has been written earlier is that the code is not likely that easy to test. Even though I consider myself to be a fan of clear structure and responsibility between modules I find myself in a mess of cross-dependencies and couplings when I begin test driving new classes that depend on old code. It is worth pointing out that with TDD the code automatically gets test-friendly which solves a serious problem in software design.

Now, it's time for me to get off the high horses and admit that the main reason I'm into TDD is that it allows me to look at my code with new eyes which is interesting. Whether I'll make a habit out of it or not remains to be seen.

I also had to restructure my project in order to easily test the code. Simply adding the same source files to the test app would cause a double compile so I needed to make the original Citizen app into a library that the test app could call into. So right now I use three projects: one static lib for the code base, one test app that runs the suites and one launcher app that simply pushes the run-button in the library. I also noticed that pre-compiled headers in VC++ don't play nice over project borders so I have to use one PCH per project even though they contain the same code. Oh well, you can't win all the time.

Comments: 0 - Leave a Comment

Link



Sunday, November 1, 2009
Hey you all

Things have been turbulent around Citizen during the last couple of months. I was briefly working with a small team here who is developing new server software to handle even more massive amounts of players than we know today. They needed a demo game and didn't want to spend their time making one. The idea was that I would work one-on-one with them to use Citizen as a client for their MMO server. Now Citizen was never meant to be an online game, even less an MMO, but the way it is designed I thought it might be possible to extend it this way.

First, I was just thrilled that someone professional took an interest in my game and that I would be able to work on my game under a professional roof. Also I was curious how the game engine would handle this new direction of development. I also felt that things had been leading up to this because just a few weeks earlier I put the finishing touches on the entity system that made this possible.

Any way, I worked with them intensively for some time, learning a lot about networking and the problems with it. The technology these guys are working on is awesome. However, in the end, I missed working on the original Citizen plan too much so I decided to leave the team and use what time I have to work on the game.

It's really an emotional decision. Citizen is my baby whom I want to spend time with. And I'm willing to sacrifice to do that.

Comments: 2 - Leave a Comment

Link



Tuesday, September 22, 2009
So as predicted I haven't had time to write any more long in-depth posts about Citizen (I had a good thing going there for a while.) I still have made some progress though; mostly tweaks and adjustments to existing solutions. Most notably I've moved most gameplay settings to several XML files through TinyXML.

I've brought the working copy download up to date, so I'f you feel like it you can give it a spin. Among the news are better instructions on how to play and how to modify the datafiles to bend the game experience.

Comments: 0 - Leave a Comment

Link



Monday, August 31, 2009
The semester is upon us. At the moment of writing I am a few hours away from the first set of lectures. For me this means that I'll have little time to spend on Citizen from now on. Because of this I spent most of yesterday trying to wrap up the work I'd done these last couple of weeks, tying up loose ends and leaving the project in a state where it is easy to start it up again.

So what's the situation?

System-wise I wrote a stub for a system to handle successive events during gameplay, basically a queue of elements containing a starting script, an ending script and an end condition. Script is a loose term here which could mean either an external script function or an internal function. Right now it only supports the latter since I have no scripting system yet.

The point here is to have a structured, data-driven way to configure chains of events during gameplay such as "Spawn 10 enemies of type A; When player defeats all, spawn 15 enemies of type B; when player defeats 5 of these, spawn boss enemy; etc."

I wrapped up my work on the vehicle components and made some tweaks to the enemy behaviors. Now, with a combination of seek, cohesion and separation, they move as a loose group trying to find a position half a screen away from the player, where they bomb away. Gameplay-wise this is ok; I can see enemies behaving this way, so I'll treat them as done. The next step here would be to add different kinds of enemies.

So, that's that. You can download a working copy of the program here, if you want to toy around with its current state. If I'm not going to update in a while I might as well show it. This does not constitute a second prototype because it still is flawed compared to P1 (there's no hud elements, the background is flat, and the program fails if the player dies.) Most of the changes are internal.

Thanks for interest shown. I'll be back soon.

Edit: Fixed above working copy so that the game does not fail if player dies.

Comments: 0 - Leave a Comment

Link



Wednesday, August 26, 2009
Another long post. There is a code treat at the bottom if you stay with me.

There are still a few days left before the semester starts and right now I'm trying to make the most of them. As I said in the last post I wanted to put the entity system aside for a while and work on A) gameplay mechanics and B) presentation. However as I played the game I realized that the collisions were in a much worse state than I had thought, so it had to be resolved. Then there was the sound component too. Short things first.

Sound component:

There isn't that much to say here really. I wrapped up the previous AudioSource object in a new component. In order to make a gun component able to play a sound when it shoots I had to add an event to trigger when shooting, then attach the play command to that event. I already had some events in the system, like an onKill event for entities and an onDone event for components that have limited operation time. It's a primitive system, simply a function pointer, but it works for now.

I had to do some modifications to the audio API layer to streamline the workings of the sound component. It feels great to be able to go back to old code and apply new ideas to it. It shows to me that my skills are improving. While there I noted down some things in the TODO list on how I'd like the API layer to work (but when will I find the time).

Improved collision response:

The most annoying thing about the collisions was that obstacles (the colliding objects) tended to get stuck together sometimes. This was a result of the system not doing any inter-frame checks, like sweeping or sub-sampling, and that it uses an impulse response that mirrors the velocity along the surface axis. Even worse it did not project the obstacles out of intersection, causing the them to remain so unless their velocities were great enough to move them apart in a single frame.

At this time the only thing I did was adding a projection step to guard them from sticking together. I could have done more but it solved the problem at hand and I didn't want to overwork it. The system uses circle-circle tests now and, eventually, I'll change it to use general polygons instead.

I still get some strange jumps at times, [déjà vu]but unless it acts up more than usual it can wait. It works most of the time. [/déjà vu]

Broad phase culling:

The fact that collisions slowed things down wasn't much of a surprise since I only did narrow testing on all participating obstacles. Given n obstacles the number of narrow tests amounted to T = n(n-1)/2, testing each pair once and not testing against self.

My first attempt was a naive implementation of a quad-tree. Had I done my homework I would have anticipated this to be a bad fit since the obstacles tend to be clustered together and not evenly distributed in space. Many cells were unpopulated while some became crowded so the cut in tests did not outweigh the overhead for using the tree. Lesson learned.

In my second attempt I used the fact that groups of obstacles tend to gang up. Each obstacle gets a group id, currently an enum but I'll likely use strings eventually. The obstacles is kept in separate collections, one for each group.
The manager keeps a mapping for which groups will be tested against each other. F.ex. an enemy will collide with other enemies, the player and with player fire but not with enemy fire. The culling is done as follows:
  1. Test on manager level.
    1. Compute an AABB for each group.
    2. For each active group, get a list of other group ids it is allowed to collide with.
    3. Check each group against each other. If two groups can collide, enter group level test.
  2. Test on group level.
    1. Make sure the two groups' AABBs intersect.
    2. For each obstacle in one of the groups, make sure that its AABB intersects the other group's AABB. If so, do narrow test on it against each obstacle in other group.
There are several levels where culling can occur. In 1.3 illegal tests are skipped, in 2.1 two groups' worth of testing can be avoided if the groups are not intersecting, saving nm tests if the groups have n and m obstacles respectively. Finally, in 2.2 n tests can be avoided if an obstacle does not intersect the other group at all.

The translation of which groups are allowed to collide with each other is implemented as a std::multimap<ObstacleGroupId, ObstacleGroupId> in the manager. It maps one id to a number of other allowed ids.

The manager also keeps a std::map<ObstacleGroupId, ObstacleGroup>, that maps group ids to group collection objects. When a new obstacle is added to the manager it is passed on and added to a group object based on its group id. This is the container being iterated over when group objects are tested against each other.

There is still some room for improvement here. In 2.2 we have two options, either test the members of group A against group B or test the members of group B against group A. The order would matter since the members of the groups can be distributed differently. One could argue that selecting the group with fewer members and testing them against the group with more could eliminate many tests in one go if the selected member is out of the group bounds. One could also argue that the members of the group with biggest bounding area should be tested against the other group, since chances are better that the members are not inside the smaller area. This would simply have to be tested and evaluated.

Yeah whatever, what about the results?

In my current setup I have one player, 10 enemies that fire periodically from two guns each. With the player firing away as well I end up with about 70 obstacles on average. With the broad phase culling the system does somewhere between 100-300 narrow tests. Without the broad phase we have by the formula 2415 narrow tests. Good show!

The relevant code:

I've tried to edit out irrelevant code to make it less confusing.
ObstacleManager.hpp
#ifndef CN_OBSTACLEMANAGER_H
#define CN_OBSTACLEMANAGER_H

#include "ComponentManagerBase.hpp"
#include "Obstacle.hpp"

namespace cn {

	class EntityManager;

	// --------------------------------------------------------------

	class ObstacleGroup {
	public:

		void updateBounds( );
		void testCollisions( ObstacleGroup &otherGroup, const Collider &collider );
		void addObstacle( Obstacle *o );
		void removeObstacle( Obstacle *o );

	private:

		typedef std::set<Obstacle *> ObstacleSet;
		ObstacleSet obstacles;
		rect boundingBox;

	};

	// --------------------------------------------------------------

	class ObstacleManager : public ComponentManagerBase<Obstacle> {
	public:

		ObstacleManager( EntityManager *entityManager );

		void updateByFrame( );

		void addObstacleToGroup( uint entityId );
		void removeObstacleFromGroup( uint entityId );

	private:

		CircleCollider defaultCollider;

		typedef std::multimap<ObstacleGroupId, ObstacleGroupId> GroupIdMap;
		typedef std::pair<GroupIdMap::iterator,GroupIdMap::iterator> GroupIdMapIteratorPair;
		typedef std::pair<ObstacleGroupId, ObstacleGroupId> GroupIdPair;
		GroupIdMap collisionTargets;

		typedef std::map<ObstacleGroupId, ObstacleGroup> GroupIdGroupMap;
		GroupIdGroupMap groups;

	};

}

#endif






The updateByFrame() function is the motor here and the entering pint to the whole process. If you'd like to trace the code, that's where you start. The add/removeFromGroup() functions are called externally at add and remove times. The CircleCollider class inherits Collider and works like a functor performing the narrow test. There are several extra typedefs in the manager that are used in updateByFrame() to make it cleaner.

ObstacleManager.cpp
#include "Prerequisites.hpp"
#include "ObstacleManager.hpp"
#include "Obstacle.hpp"
#include "EntityManager.hpp"

namespace cn {

	// --------------------------------------------------------------
	// ObstacleManager class
	// --------------------------------------------------------------

	ObstacleManager::ObstacleManager( EntityManager *entityManager ) 
		: ComponentManagerBase( entityManager ), defaultCollider( entityManager )
	{
		// Here the allowed collision targets for each id are defined.
		// Allies maps to Allies, Enemies and EnemyFire, etc.
		collisionTargets.insert( GroupIdPair( Allies, Allies ));
		collisionTargets.insert( GroupIdPair( Allies, Enemies ));
		collisionTargets.insert( GroupIdPair( Allies, EnemyFire ));
		collisionTargets.insert( GroupIdPair( Enemies, Enemies ));
		collisionTargets.insert( GroupIdPair( Enemies, Allies ));
		collisionTargets.insert( GroupIdPair( Enemies, AlliedFire ));
		collisionTargets.insert( GroupIdPair( AlliedFire, Enemies ));
		collisionTargets.insert( GroupIdPair( EnemyFire, Allies ));
	}

	// --------------------------------------------------------------

	void ObstacleManager::updateByFrame( ) {
		// Make the bounds up to date.
		// NOTE: However intermediate changes during collisions will not be caught.
		GroupIdGroupMap::iterator it;
		for ( it = groups.begin( ); it != groups.end( ); it++ ) {
			it->second.updateBounds( );
		}

		// A-loop: Iterate over all present groups.
		for ( it = groups.begin( ); it != groups.end( ); it++ ) {
			// Find which groups the current group responds to.
			GroupIdMapIteratorPair targetIds = collisionTargets.equal_range( it->first );
			// Proceed if the range is not empty.
			if ( targetIds.first != targetIds.second ) {
				// B-loop: Iterate over all groups at same position and after.
				GroupIdGroupMap::iterator jt;
				for ( jt = it; jt != groups.end( ); jt++ ) {
					// Proceed if A-element id responds to B-element id.
					GroupIdMap::iterator kt;
					for ( kt = targetIds.first; kt != targetIds.second; kt++ ) if ( kt->second == jt->first ) break;
					if ( kt != targetIds.second ) {
						// Now we know the two groups respond to each other.
						// TODO: Make a decision on the test order, f.ex. by greatest area or least count.
						it->second.testCollisions( jt->second, defaultCollider );
					}
				}
			}
		}
	}

	// --------------------------------------------------------------

	void ObstacleManager::addObstacleToGroup( uint entityId ) {
		groups[it->second->getGroupId( )].addObstacle( it->second );
	}
	
	// --------------------------------------------------------------

	void ObstacleManager::removeObstacleFromGroup( uint entityId ) {
		groups[it->second->getGroupId( )].removeObstacle( it->second );
	}

	// --------------------------------------------------------------
	// ObstacleGroup class
	// --------------------------------------------------------------

	void ObstacleGroup::updateBounds( ) {
		boundingBox = rect( 0, 0, 0, 0 );
		ObstacleSet::iterator it;
		for ( it = obstacles.begin( ); it != obstacles.end( ); it++ ) {
			(*it)->updateBoundingBox( );
			fit( boundingBox, (*it)->getBoundingBox( ));
		}
	}

	// --------------------------------------------------------------

	void ObstacleGroup::testCollisions( ObstacleGroup &otherGroup, const Collider &collider ) {
		PROFILE_FUNC( );
		// Check if groups intersect at all.
		if ( intersect( boundingBox, otherGroup.boundingBox )) {
			// Iterate over all local obstacles.
			ObstacleSet::iterator it;
			for ( it = obstacles.begin( ); it != obstacles.end( ); it++ ) {
				// Check if the current local obstacle intersects the other group.
				if ( intersect( (*it)->getBoundingBox( ), otherGroup.boundingBox )) {
					// Check against all in other group.
					ObstacleSet::iterator jt;
					for ( jt = otherGroup.obstacles.begin( ); jt != otherGroup.obstacles.end( ); jt++ ) {
						// Don't check against self.
						if ( (*it)->getId( ) != (*jt)->getId( )) {
							collider.apply( *it, *jt );
						}
					}
				}
			}
		}
	}

	// --------------------------------------------------------------

	void ObstacleGroup::addObstacle( Obstacle *o ) {
		obstacles.insert( o );
	}

	// --------------------------------------------------------------

	void ObstacleGroup::removeObstacle( Obstacle *o ) {
		obstacles.erase( o );
	}

}






Note that there are two classes here ObstacleManager and ObstacleGroup. The updateByFrame() function is really STL-messy, I know, so do ask.

Obstacle.hpp
#ifndef CN_OBSTACLE_H
#define CN_OBSTACLE_H

#include "ComponentManagerBase.hpp"
#include "ComponentBase.hpp"

namespace cn {

	class EntityData;
	class Obstacle;

	// This is the group id definition.
	enum ObstacleGroupId { Allies, Enemies, Neutrals, AlliedFire, EnemyFire, NeutralFire };

	// --------------------------------------------------------------

	class Obstacle : public ComponentBase {
	public:

		Obstacle( uint entityId, EntityData *entityData, const ComponentParameters<Obstacle> &params );

		uint getId( ) { return id; }
		const rect &getBoundingBox( ) { return boundingBox; }
		void updateBoundingBox( );
		ObstacleGroupId getGroupId( ) { return params.group; }

	private:

		// This object contains the group id for the obstacle.
		ComponentParameters<Obstacle> params;

		rect boundingBox;

	};

}

#endif






Here rect is simply a class describing a rectangle parallel to the coordinate axes.

Obstacle.cpp
#include "Prerequisites.hpp"
#include "Obstacle.hpp"
#include "EntityData.hpp"

namespace cn {

	// --------------------------------------------------------------

	void Obstacle::updateBoundingBox( ) {
		// I didn't include the two functions below but they should be obvious.
		vec2 pos = getPos( );
		real radius = getRadius( );
		boundingBox.left = pos.x - radius;
		boundingBox.right = pos.x + radius;
		boundingBox.bottom = pos.y - radius;
		boundingBox.top = pos.y + radius;
	}

}






Currently collisions are based on circles but if that changes, so will this function, obviously.

That's it for today folks.

Comments: 0 - Leave a Comment

Link


All times are ET (US)

 
S
M
T
W
T
F
S
2
3
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

OPTIONS
Track this Journal

 RSS 

ARCHIVES
November, 2009
September, 2009
August, 2009
July, 2009
June, 2009
May, 2009
April, 2009
February, 2009
January, 2009
September, 2008
August, 2008
June, 2008
May, 2008
December, 2005
November, 2005
June, 2005
May, 2005