Kolor

Kolor is a first-person 3D shooter game developed using only C++ & OpenGL. The objective of the game is to gain dominance in the world by coloring other players to your own color. Players belong to a team, represented by a captain and his color. You can Kolor(paint) all the players on the team or just kolor the master to claim the team.

I built a prototype using C# & XNA to determine if the game would be fun. XNA takes care of a lot of details, which forced me to dig deeper into the math involved. This motivated me to adopt C++/OpenGL as the only tools. This turned out to be a huge undertaking. As a part of this, I implemented my own COLLADA DAE model library.

Interesting things I did along the way:


COLLADA DAE Model Library
I needed a way to load & render 3D models for players/weapons/world. I was advised to use COLLADA DAE since it's the industry standard. I had to write my own Collada importer since there was no open source library at the time. I interpreted DAE's Directed Acyclic Graph into my own scene-graph. Take a look at:

  • VSGNode & VSceneGraph.h / cpp (DAE Model is accessed using this facade)
The collada format is very extensive so I only implemented the required subset. My library supported the following:
  • Scene Graph
  • Model Mesh information (organised in a tree structure)
  • Node in scenegraph can have the following operations defined in DAE (VSGNode.h / cpp):
    • Lookat
    • Transformation defined by a 4x4 matrix
    • Translate along X / Y or Z axis
    • Rotation at a given angle about an arbitrary axis
    • Scale / Skew
  • Primitives supported:
    • Triangle List
    • Polygon List
  • Vertex (.h) represents the heart of the model which stores data attributes as boost::hash_map { Semantic, (float*) }

Indexed Vertex Buffer Objects
Initially, I was using glBegin/glEnd to render models. However, when I loaded a high poly count model, the FPS crippled. I resorted to using Indexed Vertex Buffer Objects (VBOs) which helped FPS. For generating vertex indices, I ran the list of vertices through a hash-map and assign a unique index for each vertex. This can be seen in GeomMesh.cpp constructor and bool VertexExists(...) function. Top

FrameTransform
Once the model was rendered, I wanted to create a "player" who could move around in the world, keeping track of its position and orientation. This gave birth to FrameTransform. I did this using three vectors:

  • Origin
  • Up
  • Forward vector
Using right hand thumb rule, I wrote functions that would let the entity move or rotate in the world. I manage the transformation matrix on the opengl stack manually! The details can be found in FrameTransform.h/cpp.
FrameTransform was generic in nature and players could fly. PlayerTransform restricts this behavior by enabling movement only along the X/Z axis:
  • Move (..)
  • Rotate(...)

Top

My own gluLookAt

For the First-person view, I needed to position world and everything else relative to player's coordinate system. Camera class uses the OpenGL transformation stack to transform everything as per player's Coordinate system. It is a 'friend' of (Human)Player, since the camera is tightly coupled to the player.

		 class FrameTransform (6 DOF)
		-    glm::vec3 m_Origin;
		-    glm::vec3 m_Forward;
		-    glm::vec3 m_Up;
		•    MoveForward/Right/Up ( amt )
		•    RotateAround X/Y/Z ( amt )
		•    void ApplyActorTransform () => calls glMultMatrix(..)

		class Player : public GameObject 
		-    FrameTransform m_Transform; => Coordinate system w.r.t the world CS

		class World : public GameObject (level class)
		-    FrameTransform m_Transform; => Coordinate system w.r.t the world CS
		
		
A first person camera helps to view the world from the player’s eyes. Thus, we need a transformation which enables the view from player’s reference.
Example:
  • WCS (0, 0)W
  • PCS (2, 2)W => Player is at 2, 2 in the world coordinate system => PW<=P (2, 2)
  • Chair P = (.5, .5)P
  • => Chair W = PW<=P * ChairP = (2.5, 2.5)W
This way we can get everything in World’s frame of reference. For First Person Camera, everything needs to be brought into Player’s coordinate system.
Player.m_Transform represents PW<=P . If we invert this matrix, we get P’P<-W which brings everything from World coordinate space to Player Coordinate space.
void Camera :: ApplyCameraTransform()
{
    glm::mat4 l_TransformationMatrix;
    m_Player->m_Transformation.GetTransformation(l_TransformationMatrix, false);

    l_TransformationMatrix = glm::core::function::matrix::inverse(l_TransformationMatrix);
    glMultMatrixf(glm::value_ptr(l_TransformationMatrix));
}
It is invoked as follows:

		void GameGL :: paintGL()
		{
		    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		    
		    glPushMatrix();
		        mp_Camera->ApplyCameraTransform();
		        std::list<GameObject*>::iterator it_GameObjs;

		        for(it_GameObjs = m_GameObjectList.begin(); it_GameObjs != m_GameObjectList.end(); it_GameObjs++) {
		                (*it_GameObjs)->Render();
		            }
		... 
		}

Top

Coloring Players - When player and bullet collide! As players move about and fire bullets, I manage collision detection/resolution by splitting it into broad and narrow phase.
The involved actors are :-

  • Players are approximated by a bounding sphere, located at a position and moving with a velocity. Player can potentially collide with weapons, world & bullets.
  • Bullets are represented by a segment with a start point & end point (using posn, posn + vel). Bullet can hit other fired bullets, players or world.
As each player and bullet is created, reference to each entity is stored within CollisionMgr. On each update frame, this happens:
  • For each player, find Cell(s); it is associated with :=> list< Cell* > PlayerCells
  • For each Bullet (segment), find Cell(s); it is associated with :=> list< Cell* > BulletCells
  • For each entity, the cell(s) can be found out based on the entity.
    • Player: Bounding sphere: The grid size are uniformly sized cell(M) such that, M = 2R. At any given time, the bounding sphere can be in contact with max 4 cells. To determine the cells associated, find the 8 points for a given sphere (each at 45 degrees X,Y,Z axis along 8 octants). (And then find out, in which grid cell does each of the point lie. This could return max 4 cells).
    • Bullet: Segmented Ray with endpoints at: Posn and (Posn + vel). All the cells that the line passes through would yield the cells.
  • Cell can be a structure like:
    Cell {
    List < Player*>
    List < Bullet* >
    Value (x,y,z) => unique value based on these three coordinates
    }
The hash-map iteration yields all possible pair of colliding players & bullets in a given cell. These are resolved using narrow-phase collision. Narrow phase would be a Bounding-Sphere/Segment (Player/Bullet) collision check. This required me to compute the bounding sphere for the given DAE model:
  • Iterate through all the points on the model tracking min/max point
  • Compute Sphere center & radius = distance(minimumPt, maximumPt) * 0.5;
  • This is run recursively on the scenegraph to return a hierarchy of bounding spheres
  • The resulting bounding spheres tight-approximation for the underlying model

Top

Team architecture