My font class was usable, just missing things like individual character widths (instead it was like monospace for everything), and some orientation options. It still doesnt have alignment options, but will once it becomes necessary. I found Codehead's Bitmap Font Generator which is a nice little utility that will export character width data along with the bitmap. I export the binary font data, and keep that separate from the font image instead of trying to tie them together in the same file. Text in the game is rendered using display lists, which are called in a variable-argument function. Because lists are called using glCallLists for efficiency, the character advance has to be compiled in the display list. So to have vertical orientation instead of horizontal, I have another display list. In the list, the characters' quads are rendered using glVertex3f which works in ortho and perspective. Each character has a height of 1.0f, and the width is proportional. This way, I can just scale to the pixel height I want in ortho mode, and the unit height I need in perspective. Now, creating any font from a TTF file, and rendering with correct spacing is a snap!
It looks like this:
Font::Font(): rows(8), cols(8), texc_w(1.0f), texc_h(1.0f), w_scale(20.0f), h_scale(20.0f){ horizontal = 0; vertical = 0; texture = 0; orientation = OR_HORIZONTAL; is_loaded = false; notice1 = true;}Font::~Font(){ if(glIsList(horizontal)==GL_TRUE) glDeleteLists(horizontal, CHAR_COUNT); if(glIsList(vertical)==GL_TRUE) glDeleteLists(vertical, CHAR_COUNT); }void Font::Load(const std::string& filename){ if(is_loaded) return; std::string dataName = filename + ".dat"; std::string textureName = filename + ".png"; //// Find the font data file File* dataFile = resource::findFile(dataName); if( dataFile == NULL ) { log_write("- Font data file not found"); return; } // if found, read in the data dataFile->open("rb"); dataFile->read(&info, sizeof(font_info), 1); dataFile->close(); texture = texture::MJS_LoadTexture(textureName).gl_id; if(!texture) { log_write("- Font texture was NULL"); return; } if(glIsList(horizontal)==GL_TRUE) glDeleteLists(horizontal, CHAR_COUNT); if(glIsList(vertical)==GL_TRUE) glDeleteLists(vertical, CHAR_COUNT); horizontal = glGenLists(CHAR_COUNT); vertical = glGenLists(CHAR_COUNT); texc_w = 1.0f/(float)cols; texc_h = 1.0f/(float)rows; float x=0, y=0; for(int i=0; i<CHAR_COUNT; i++) { x = (i%cols)*texc_w; y = 1.0f- ((i/rows)*texc_h); float charWidth = ((float)info.charWidths[i + 24]) / ((float)info.cellWidth); glNewList(horizontal+i, GL_COMPILE); glBegin(GL_QUADS); glNormal3f(0,0,1); glTexCoord2f(x, y); glVertex3f(0.0f,0.0f,0.0f); glTexCoord2f(x+texc_w, y); glVertex3f(1.0f,0.0f,0.0f); glTexCoord2f(x+texc_w, y+texc_h); glVertex3f(1.0f,1.0f,0.0f); glTexCoord2f(x, y+texc_h); glVertex3f(0.0f, 1.0f,0.0f); glEnd(); glTranslatef(charWidth, 0.0f, 0.0f); glEndList(); glNewList(vertical+i, GL_COMPILE); glBegin(GL_QUADS); glNormal3f(0,0,1); glTexCoord2f(x, y); glVertex3f(0.0f,0.0f,0.0f); glTexCoord2f(x+texc_w, y); glVertex3f(1.0f,0.0f,0.0f); glTexCoord2f(x+texc_w, y+texc_h); glVertex3f(1.0f,1.0f,0.0f); glTexCoord2f(x, y+texc_h); glVertex3f(0.0f, 1.0f,0.0f); glEnd(); glTranslatef(0.0f, -1.0f, 0.0f); glEndList(); } is_loaded = true;}void Font::setSize(float w, float h){ w_scale = w; h_scale = h;}void Font::setOrientation(int or){ if( or>OR_FIRST && or<OR_LAST )orientation = or;}void Font::print(int x, int y, const char *str, ...) { if(!is_loaded) return; char text[256]; va_list args; if (str == NULL) return; va_start(args, str); vsprintf(text, str, args); va_end(args); glPushMatrix(); glTranslatef((float)x, (float)y, 0); glScalef(w_scale, h_scale, 0); //glActiveTexture(GL_TEXTURE0); //glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, texture); glPushAttrib(GL_LIST_BIT); switch( orientation ) { case OR_HORIZONTAL: glListBase(horizontal-24); break; //-20 case OR_VERTICAL: glListBase(vertical-24); break; default: if(notice1) { log_write("-!- Font orientation not implemented: %d", orientation); notice1 = false; }break; } glCallLists(strlen(text), GL_UNSIGNED_BYTE, text); glPopAttrib(); glPopMatrix();}My camera class had everything it needed to be controlled directly by adding to it's velocity. It was lacking movement commands, which are straight forward to implement, but get kinda confusing to use directly. What I mean is that when the camera moves anywhere, its to look at something of interest instead of some arbitrary point in space. The camera class has three targets: the object its locked on to, the object its seeking to catch, and a viewing target. Each of those object pointers can be NULL, signaling that the camera is not locked on target, seeking one, or oriented towards a view target. I wont use the view target for this project because the camera will never rotate. I changed my basic moveTo() commands into the seek target because if the object is moving, the camera goes to the location and then checks the target position, leaving it always behind. While seeking, the camera takes the average between the target's position, and the position plus the velocity, and moves in that direction at the current speed. For complex camera behavior, I think I need triggers.
the camera code:
if( lock_target ) { vec3 targetPos = lock_target->getRenderPosition(); current_state.pos = vec3(targetPos.x, targetPos.y, current_state.pos.z); }else if( seek_target) { vec3 vPos = seek_target->getPosition() + seek_target->getVelocity(); vPos.z = current_state.pos.z; vec3 tPos = seek_target->getPosition(); tPos.z = current_state.pos.z; if( (tPos - current_state.pos).magnitude() > 1.0f ) { vec3 fPos = (((vPos - current_state.pos).normalize() + (tPos - current_state.pos).normalize()) / 2.0f); current_state.pos += (fPos.normalize() * speed * d_time); }else { if(locked) lock_target = seek_target; seek_target = NULL; } }else { if( movingTo ) { if( (movingToPosition - current_state.pos).magnitude() > 1.0f ) { current_state.pos += (movingToPosition - current_state.pos).normalize() * speed * d_time; }else { movingTo = false; } } }// update camera position here...Triggers:
I wanted an easy way to control the in-game camera's movement from the editor. I've decided to use Box2d's collision shapes as sensors to trigger the camera's specialty movements, controlled by the appropriate trigger class. By default the camera is following a target object, the player. When the trigger's collision shape is hit with the camera target's shape, the trigger class controls the camera's movement. The trigger stores the current camera target, and controls the camera directly until a end condition is met. Afterward, the stored camera target is set as the camera's target once again. I can make a trigger class for each kind of behavior I need, like keeping the camera in a room while the player is inside, or following the path of a bezier curve, which can be easily exported from Max. The triggers are created with static sensor shapes that don't generate a collision response, but still report the collision to the shape's owner object (which calls the appropriate object's collision fucntion with polymorphism). The trigger has a single rigid body, but can have any number of shapes as sensors, and the sensor shapes are identified by the name given and exported with them from Max. Triggers can used for anything, but the smallest difference in behavior has to be hard-coded in a new class. A Trigger is stored as an Entity in the level data because Entities can have a collision body. At the moment, I have no creation system for the entites as the level is loaded. Its all in a switch statement that creates the correct type of pointer. I need to find a creational pattern to use for this, but haven't taken the time to find one just yet. I have the book "Design Patterns", so I'm gonna read up on the Factory pattern for some enlightenment. Any suggestions?
This is the Trigger class:
class Trigger : public Entity{ public: Trigger() { playerIn = false; player=NULL; } virtual ~Trigger(){} protected: virtual void onLoad(){} virtual void loadAttribute(FILE* fp, const std::string& att, const std::string value){ } virtual void onUpdate(ulong dT){ } virtual void onRender(float blend = 1.0f){ } inline virtual void onCollision(const contactPoint& point) { Camera* cam = Camera::getActive(); if( ((Entity*)point.obBody->GetUserData())->getName() == "[player]") { std::string shapeName = (char*)point.myShape->GetUserData(); if( shapeName == "TRIGGER_ON" ) { if( !playerIn ) { if( cam->getTarget()) player = cam->getTarget(); cam->lockTarget((Object*)this); // Here the camera locks to the trigger's position playerIn = true; } } else if( shapeName == "TRIGGER_OFF" ) { if( playerIn ) { cam->Unlock(); cam->lockTarget(player); playerIn = false; } } } } private: bool playerIn; Object* player;};Eventually I'll need some kind of cutscene command list that includes camera paths and everything. I'd consider LUA to script the cutscenes, but I don't want the project to have a scripting language for simplicity's sake.
COLLISION CATEGORIES:
Box2d uses collision categories to control which kind of objects can collide with others. Editing and exporting the collision category directly from the editor is an option, but to avoid thinking about bits when designing a level, I'm going to use collision profiles. The actual data about a profile's collision category and the categories that it collides are retrieved from a switch statement in a function that returns a filled collision_profile structure.
For now, this is all I need, and there can only be 16 categories anyway, as set by box2d.
The image below shows the player standing in the doorway of an empty room that has two camera sensors (highlighted in orange). When he hits the big sensor, the camera moves to the center of the room and stays there until he hits the small sensor by the exit. The camera returns to following the play when he leaves the room. I'll need a sensor for every exit in the room. Also, there are some test signs using the font class. Note that the camera will never dolly back quite this far.
