📜 ⬆️ ⬇️

As I wrote a cross-platform 3d game engine

Greetings Habr! Many of us probably wondered "if I didn't write the game myself." Now I am running the project “Open tomb” - an attempt to create a portable game engine for the first 5 parts of the “Tomb raider”, which is posted on sourceforge.com, however, judging by my experience, many will be interested in the story with some details about how the engine was written from scratch and with virtually no knowledge in this area. Even now, a lot of knowledge is not enough, and sometimes there is simply not enough motivation to do something better, or more correctly, but it is better to move on to how the project has come to life, step by step.

What motivated me to write the engine

The story began a long time ago and from the fact that I wanted to play a wonderful logical puzzle game “Pusher” on Vista 64. However, the original game was 16 bit and completely refused to start. Nothing better than writing a clone on C, I did not come up with (sometimes the best solution initially leads to more useful results). After a short time, I implemented the game on the SDL v1.2 + OpenGL platform.

image

For the convenience of transferring maps added level editor and manually cloned all 64 maps. After some time, the interest in “Pusher” weakened, and I already wanted to drive in Tomb raider 1 with 3dfx graphics. And as many have already guessed, the existing solutions didn’t make me particularly pleased (more likely even subjectively) and I started looking for ports. In addition to the well-known project Open raider, I found nothing I still remember how I suffered with its build under windows using the mingw compiler (I don’t remember the development environment, either code :: blocks , or netbeans ). I was not at all pleased with the result of the assembly: loading the level for about a minute and a black screen as a result. I didn’t have the skills to pick someone else’s code, to understand its structure and the meaning of the functions. Attempts to collect "better" stopped. However, I set about trying to assemble at least one of the open engines manually, not with the auto-config Die GNU Autotools , but from a self-assembled designer in the development environment.
')
Thus, after a heap of time behind the monitor, a certain amount of mate, etc., I gathered Quake Tenebrae without sound. But he worked! It was a small victory that bore fruit: I began to better understand someone else's code and finally began to at least understand something about the organization of the compiler's work - without which nothing is possible at all. After a few minor improvements were made, some bugs were fixed and the sound was started, but the project was never uploaded to the Internet (even then it was morally obsolete, especially considering the presence of Dark places engine ). However, I found out from the code of the Quake Tenebrae engine how the game as a whole is organized, its individual components and the memory manager (I added the realloc function to it, albeit a fairly simple one, but everything worked without crashes).

We write the engine

When I got a little comfortable, I decided to start writing my own engine from scratch. Just for the sake of interest and self-development. The basis for creating the engine was the following: GCC-TDM compiler v4.. + msys and Netbeans development environment; libraries: SDL v1.2 + OpenGL . The first implemented function was to create a screenshot and save it to a * .bmp file using a handwritten bibliteck for working with this format. Which engine can do without the console to enter command cheats and text output — probably none, so the next thing I learned was how to display text in the OpenGL window and chose a bunch of freetype 1 + gltt . The first recognizable team was the exit command, and after that - commands for playing with the sizes of fonts, strings, etc .... For reference: I liked the code used in Quake I for parsing lines and sequentially breaking it into tokens, which is still present in the engine:

char *parse_token(char *data, char *token) { int c; int len; len = 0; token[0] = 0; if(!data) { return NULL; } // skip whitespace skipwhite: while((c = *data) <= ' ') { if(c == 0) return NULL; // end of file; data++; } // skip // comments if (c=='/' && data[1] == '/') { while (*data && *data != '\n') data++; goto skipwhite; } // handle quoted strings specially if (c == '\"') { data++; while (1) { c = *data++; if (c=='\"' || !c) { token[len] = 0; return data; } token[len] = c; len++; } } // parse single characters if (c=='{' || c=='}'|| c==')'|| c=='(' || c=='\'' || c==':') { token[len] = c; len++; token[len] = 0; return data+1; } // parse a regular word do { token[len] = c; data++; len++; c = *data; if (c=='{' || c=='}'|| c==')'|| c=='(' || c=='\'' || c==':') { break; } } while (c>32); token[len] = 0; return data; } 


Looking ahead: when I needed script support, I decided to use the approach to developing the engine as in “ID software” using the example of “DOOM 3 engine”, which was very well described here on Habré =) (I want to say again many thanks to the authors for the article translation and write interesting comments to her people). Impressed by the article, I decided to implement LUA in my engine not only for in-game script needs, but also for parsing configuration files and console commands (i.e., a uniform system is used everywhere). The approach justified itself absolutely.

Let's go to 3d

I was lucky that at the institute I liked linear algebra, matrix transformations, vectors and numerical methods. Without these fundamentals, it is very imprudent to start programming the engine from scratch (except to study these sections with examples, but without a certain theoretical knowledge base this will not be very realistic). The book by A. Boreskov “Graphics of a three-dimensional computer game based on OPENGL” helped me greatly in mastering graphics. I reread it more than once (to get acquainted with the mathematical apparatus, types of renderers and the structure of engines). Without understanding the principle of constructing the scene and the purpose of the species matrix and the projection matrix, it is impossible to move anywhere. After studying some material on the Internet and just literature, I decided to do a portal renderer. The first thing that was implemented in the engine is a free-flying camera and several portals that could only be seen through each other.

After the hardcode, a wireframe scene of 3 rooms was added (two more and a corridor). But is it really interesting to fly in such a primitive world ... And then I decided to use the resource loader from the Open raider and render the levels. I was pleased with the result and here it was finally decided to implement the idea of ​​creating a port for playing the Tomb raider , at least in its first part. For positioning objects in space, I used the matrix in the OpenGL format because it allows you to access the base vectors of the local coordinate system of the object, set the position of the object with one command glMultMatrixf (transform) and set the orientation of the object in the physical bullet engine with one command setFromOpenGLMatrix (transform), about which a little bit later. Below is an image with the OpenGL matrix structure from the above link:

image

For keeping a history of changes in the engine and the possibility of backup and just for self-development, it was decided to use the version control system mercurial . Its use has allowed not only to track the progress in writing code, but also made it possible to upload the results to sourceforge.com . It should be noted that when the information about portals from real maps began to load into the engine, a huge amount of flaws in my implementation of the portal system immediately surfaced. It took a lot of time to fight the disappearing objects and departures, and even now I think that the portal module needs serious improvement. Now the engine renderer, depending on the position of the camera and its orientation, starts passing through the room portals and adds only visible rooms to the list. Then the renderer draws the rooms and their contents from the list. It is clear that for large open spaces such an approach is not the most successful, but for the purposes of the project it is quite enough. Here is an example of the recursive walk around room:

 ** * The reccursion algorithm: go through the rooms with portal - frustum occlusion test * @portal - we entered to the room through that portal * @frus - frustum that intersects the portal * @return number of added rooms */ int Render_ProcessRoom(struct portal_s *portal, struct frustum_s *frus) { int ret = 0, i; room_p room = portal->dest_room; //    room_p src_room = portal->current_room; //    portal_p p; //      - frustum_p gen_frus; //    if((src_room == NULL) || !src_room->active || (room == NULL) || !room->active) { return 0; } p = room->portals; for(i=0; i<room->portal_count; i++,p++) //      { if((p->dest_room->active) && (p->dest_room != src_room)) //      { gen_frus = Portal_FrustumIntersect(p, frus, &renderer); //  -  .    if(NULL != gen_frus) //        { ret++; Render_AddRoom(p->dest_room); Render_ProcessRoom(p, gen_frus); } } } return ret; } 


Skeleton Animated Models

And so began one of the most laborious works in the project. Rendering static rooms with static objects turned out to be relatively easy, but when it came to animated skeleton models ... The first thing I realized was that the Open raider resource loader does not load all the necessary information about the skeleton model. The number of frames in the animation is determined incorrectly, because of which one animation contains frames from several at once.
During attempts to solve these problems, I found various documentation on the format of the Tomb raider levels and at the same time the vt project, which had its own resource loader. Suppose that in this project there was no loading of frames of animation models, but there was more structured code in it, convenient for reading and bringing to mind. So I replaced the bootloader in the project with vt . For example: in the Open raider all 5 parts of the Tomb raider are loaded with one long function with a bunch of if and switch by the version number of the game, which noticeably complicated reading the code and finding errors. There were 5 modules in vt , each of which was responsible for its own version of the level, thanks to which the code was read fairly easily and making changes was not difficult.

The main problem with animations was the extraction of the angles of the bones in the skeletal model. The fact is that to save space, the corners were stored in bytecode in 2-4 byte increments. The first 2 bytes include a flag about whether there is one turn and around which axis, or just three and the corners themselves. In the case of 3 turns, flags and corners are stored in 4 bytes, in the case of one, only 2 bytes are used. In this case, the angles for all models, animations and frames are stored in a single array and the offset must be calculated. In addition, this bytecode still stores the headers of the individual frames of the model and confusion with offsets is critical, and now we add that the number of frames is loaded incorrectly (later it turned out that the number of frames is given for “interpolated” animations with a frequency of 30 fps, but in reality frames can be stored in a “narrowed” form with fps with multipliers 1, 1/2, 1/3 and 1/4). After finishing the frame loader animations, the skeleton models no longer turn inside out and turn into a mess of distorted polygons! Now we need to "revive" Lara. Below is the code of the function that generates the skeleton model, saved spelling and commented out sections of the code for debugging:

 void GenSkeletalModel(struct world_s *world, size_t model_num, struct skeletal_model_s *model, class VT_Level *tr) { int i, j, k, l, l_start; tr_moveable_t *tr_moveable; tr_animation_t *tr_animation; uint32_t frame_offset, frame_step; uint16_t *frame, temp1, temp2; ///@FIXME: "frame" set, but not used float ang; btScalar rot[3]; bone_tag_p bone_tag; bone_frame_p bone_frame; mesh_tree_tag_p tree_tag; animation_frame_p anim; tr_moveable = &tr->moveables[model_num]; // original tr structure model->collision_map = (uint16_t*)malloc(model->mesh_count * sizeof(uint16_t)); model->collision_map_size = model->mesh_count; for(i=0;i<model->mesh_count;i++) { model->collision_map[i] = i; } model->mesh_tree = (mesh_tree_tag_p)malloc(model->mesh_count * sizeof(mesh_tree_tag_t)); tree_tag = model->mesh_tree; tree_tag->mesh2 = NULL; for(k=0;k<model->mesh_count;k++,tree_tag++) { tree_tag->mesh = model->mesh_offset + k; tree_tag->mesh2 = NULL; tree_tag->flag = 0x00; vec3_set_zero(tree_tag->offset); if(k == 0) { tree_tag->flag = 0x02; vec3_set_zero(tree_tag->offset); } else { uint32_t *tr_mesh_tree = tr->mesh_tree_data + tr_moveable->mesh_tree_index + (k-1)*4; tree_tag->flag = tr_mesh_tree[0]; tree_tag->offset[0] = (float)((int32_t)tr_mesh_tree[1]); tree_tag->offset[1] = (float)((int32_t)tr_mesh_tree[3]); tree_tag->offset[2] =-(float)((int32_t)tr_mesh_tree[2]); } } /* * ================= now, animation loading ======================== */ if(tr_moveable->animation_index < 0 || tr_moveable->animation_index >= tr->animations_count) { /* * model has no start offset and any animation */ model->animation_count = 1; model->animations = (animation_frame_p)malloc(sizeof(animation_frame_t)); model->animations->frames_count = 1; model->animations->frames = (bone_frame_p)malloc(model->animations->frames_count * sizeof(bone_frame_t)); bone_frame = model->animations->frames; model->animations->id = 0; model->animations->next_anim = NULL; model->animations->next_frame = 0; model->animations->state_change = NULL; model->animations->state_change_count = 0; model->animations->original_frame_rate = 1; bone_frame->bone_tag_count = model->mesh_count; bone_frame->bone_tags = (bone_tag_p)malloc(bone_frame->bone_tag_count * sizeof(bone_tag_t)); vec3_set_zero(bone_frame->pos); vec3_set_zero(bone_frame->move); bone_frame->v_Horizontal = 0.0; bone_frame->v_Vertical = 0.0; bone_frame->command = 0x00; for(k=0;k<bone_frame->bone_tag_count;k++) { tree_tag = model->mesh_tree + k; bone_tag = bone_frame->bone_tags + k; rot[0] = 0.0; rot[1] = 0.0; rot[2] = 0.0; vec4_SetTRRotations(bone_tag->qrotate, rot); vec3_copy(bone_tag->offset, tree_tag->offset); } return; } //Sys_DebugLog(LOG_FILENAME, "model = %d, anims = %d", tr_moveable->object_id, GetNumAnimationsForMoveable(tr, model_num)); model->animation_count = GetNumAnimationsForMoveable(tr, model_num); if(model->animation_count <= 0) { /* * the animation count must be >= 1 */ model->animation_count = 1; } /* * Ok, let us calculate animations; * there is no difficult: * - first 9 words are bounding box and frame offset coordinates. * - 10's word is a rotations count, must be equal to number of meshes in model. * BUT! only in TR1. In TR2 - TR5 after first 9 words begins next section. * - in the next follows rotation's data. one word - one rotation, if rotation is one-axis (one angle). * two words in 3-axis rotations (3 angles). angles are calculated with bit mask. */ model->animations = (animation_frame_p)malloc(model->animation_count * sizeof(animation_frame_t)); anim = model->animations; for(i=0;i<model->animation_count;i++,anim++) { tr_animation = &tr->animations[tr_moveable->animation_index+i]; frame_offset = tr_animation->frame_offset / 2; l_start = 0x09; if(tr->game_version == TR_I || tr->game_version == TR_I_DEMO || tr->game_version == TR_I_UB) { l_start = 0x0A; } frame_step = tr_animation->frame_size; //Sys_DebugLog(LOG_FILENAME, "frame_step = %d", frame_step); anim->id = i; anim->next_anim = NULL; anim->next_frame = 0; anim->original_frame_rate = tr_animation->frame_rate; anim->accel_hi = tr_animation->accel_hi; anim->accel_hi2 = tr_animation->accel_hi2; anim->accel_lo = tr_animation->accel_lo; anim->accel_lo2 = tr_animation->accel_lo2; anim->speed = tr_animation->speed; anim->speed2 = tr_animation->speed2; anim->anim_command = tr_animation->anim_command; anim->num_anim_commands = tr_animation->num_anim_commands; anim->state_id = tr_animation->state_id; anim->unknown = tr_animation->unknown; anim->unknown2 = tr_animation->unknown2; anim->frames_count = GetNumFramesForAnimation(tr, tr_moveable->animation_index+i); //Sys_DebugLog(LOG_FILENAME, "Anim[%d], %d", tr_moveable->animation_index, GetNumFramesForAnimation(tr, tr_moveable->animation_index)); // Parse AnimCommands // Max. amount of AnimCommands is 255, larger numbers are considered as 0. // See http://evpopov.com/dl/TR4format.html#Animations for details. if( (anim->num_anim_commands > 0) && (anim->num_anim_commands <= 255) ) { // Calculate current animation anim command block offset. int16_t *pointer = world->anim_commands + anim->anim_command; for(uint32_t count = 0; count < anim->num_anim_commands; count++, pointer++) { switch(*pointer) { case TR_ANIMCOMMAND_PLAYEFFECT: case TR_ANIMCOMMAND_PLAYSOUND: // Recalculate absolute frame number to relative. ///@FIXED: was unpredictable behavior. *(pointer + 1) -= tr_animation->frame_start; pointer += 2; break; case TR_ANIMCOMMAND_SETPOSITION: // Parse through 3 operands. pointer += 3; break; case TR_ANIMCOMMAND_JUMPDISTANCE: // Parse through 2 operands. pointer += 2; break; default: // All other commands have no operands. break; } } } if(anim->frames_count <= 0) { /* * number of animations must be >= 1, because frame contains base model offset */ anim->frames_count = 1; } anim->frames = (bone_frame_p)malloc(anim->frames_count * sizeof(bone_frame_t)); /* * let us begin to load animations */ bone_frame = anim->frames; frame = tr->frame_data + frame_offset; for(j=0;j<anim->frames_count;j++,bone_frame++,frame_offset+=frame_step) { frame = tr->frame_data + frame_offset; bone_frame->bone_tag_count = model->mesh_count; bone_frame->bone_tags = (bone_tag_p)malloc(model->mesh_count * sizeof(bone_tag_t)); vec3_set_zero(bone_frame->pos); vec3_set_zero(bone_frame->move); bone_frame->v_Horizontal = 0.0; bone_frame->v_Vertical = 0.0; bone_frame->command = 0x00; GetBFrameBB_Pos(tr, frame_offset, bone_frame); if(frame_offset < 0 || frame_offset >= tr->frame_data_size) { //Con_Printf("Bad frame offset"); for(k=0;k<bone_frame->bone_tag_count;k++) { tree_tag = model->mesh_tree + k; bone_tag = bone_frame->bone_tags + k; rot[0] = 0.0; rot[1] = 0.0; rot[2] = 0.0; vec4_SetTRRotations(bone_tag->qrotate, rot); vec3_copy(bone_tag->offset, tree_tag->offset); } } else { l = l_start; for(k=0;k<bone_frame->bone_tag_count;k++) { tree_tag = model->mesh_tree + k; bone_tag = bone_frame->bone_tags + k; rot[0] = 0.0; rot[1] = 0.0; rot[2] = 0.0; vec4_SetTRRotations(bone_tag->qrotate, rot); vec3_copy(bone_tag->offset, tree_tag->offset); switch(tr->game_version) { case TR_I: /* TR_I */ case TR_I_UB: case TR_I_DEMO: temp2 = tr->frame_data[frame_offset + l]; l ++; temp1 = tr->frame_data[frame_offset + l]; l ++; rot[0] = (float)((temp1 & 0x3ff0) >> 4); rot[2] =-(float)(((temp1 & 0x000f) << 6) | ((temp2 & 0xfc00) >> 10)); rot[1] = (float)(temp2 & 0x03ff); rot[0] *= 360.0 / 1024.0; rot[1] *= 360.0 / 1024.0; rot[2] *= 360.0 / 1024.0; vec4_SetTRRotations(bone_tag->qrotate, rot); break; default: /* TR_II + */ temp1 = tr->frame_data[frame_offset + l]; l ++; if(tr->game_version >= TR_IV) { ang = (float)(temp1 & 0x0fff); ang *= 360.0 / 4096.0; } else { ang = (float)(temp1 & 0x03ff); ang *= 360.0 / 1024.0; } switch (temp1 & 0xc000) { case 0x4000: // x only rot[0] = ang; rot[1] = 0; rot[2] = 0; vec4_SetTRRotations(bone_tag->qrotate, rot); break; case 0x8000: // y only rot[0] = 0; rot[1] = 0; rot[2] =-ang; vec4_SetTRRotations(bone_tag->qrotate, rot); break; case 0xc000: // z only rot[0] = 0; rot[1] = ang; rot[2] = 0; vec4_SetTRRotations(bone_tag->qrotate, rot); break; default: // all three temp2 = tr->frame_data[frame_offset + l]; rot[0] = (float)((temp1 & 0x3ff0) >> 4); rot[2] =-(float)(((temp1 & 0x000f) << 6) | ((temp2 & 0xfc00) >> 10)); rot[1] = (float)(temp2 & 0x03ff); rot[0] *= 360.0 / 1024.0; rot[1] *= 360.0 / 1024.0; rot[2] *= 360.0 / 1024.0; vec4_SetTRRotations(bone_tag->qrotate, rot); l ++; break; }; break; }; } } } } /* * Animations interpolation to 1/30 sec like in original. Needed for correct state change works. */ SkeletalModel_InterpolateFrames(model); GenerateAnimCommandsTransform(model); /* * state change's loading */ #if LOG_ANIM_DISPATCHES if(model->animation_count > 1) { Sys_DebugLog(LOG_FILENAME, "MODEL[%d], anims = %d", model_num, model->animation_count); } #endif anim = model->animations; for(i=0;i<model->animation_count;i++,anim++) { anim->state_change_count = 0; anim->state_change = NULL; tr_animation = &tr->animations[tr_moveable->animation_index+i]; j = (int)tr_animation->next_animation - (int)tr_moveable->animation_index; j &= 0x7fff; if(j >= 0 && j < model->animation_count) { anim->next_anim = model->animations + j; anim->next_frame = tr_animation->next_frame - tr->animations[tr_animation->next_animation].frame_start; anim->next_frame %= anim->next_anim->frames_count; if(anim->next_frame < 0) { anim->next_frame = 0; } #if LOG_ANIM_DISPATCHES Sys_DebugLog(LOG_FILENAME, "ANIM[%d], next_anim = %d, next_frame = %d", i, anim->next_anim->id, anim->next_frame); #endif } else { anim->next_anim = NULL; anim->next_frame = 0; } anim->state_change_count = 0; anim->state_change = NULL; if((tr_animation->num_state_changes > 0) && (model->animation_count > 1)) { state_change_p sch_p; #if LOG_ANIM_DISPATCHES Sys_DebugLog(LOG_FILENAME, "ANIM[%d], next_anim = %d, next_frame = %d", i, (anim->next_anim)?(anim->next_anim->id):(-1), anim->next_frame); #endif anim->state_change_count = tr_animation->num_state_changes; sch_p = anim->state_change = (state_change_p)malloc(tr_animation->num_state_changes * sizeof(state_change_t)); for(j=0;j<tr_animation->num_state_changes;j++,sch_p++) { tr_state_change_t *tr_sch; tr_sch = &tr->state_changes[j+tr_animation->state_change_offset]; sch_p->id = tr_sch->state_id; sch_p->anim_dispath = NULL; sch_p->anim_dispath_count = 0; for(l=0;l<tr_sch->num_anim_dispatches;l++) { tr_anim_dispatch_t *tr_adisp = &tr->anim_dispatches[tr_sch->anim_dispatch+l]; int next_anim = tr_adisp->next_animation & 0x7fff; int next_anim_ind = next_anim - (tr_moveable->animation_index & 0x7fff); if((next_anim_ind >= 0) &&(next_anim_ind < model->animation_count)) { sch_p->anim_dispath_count++; sch_p->anim_dispath = (anim_dispath_p)realloc(sch_p->anim_dispath, sch_p->anim_dispath_count * sizeof(anim_dispath_t)); anim_dispath_p adsp = sch_p->anim_dispath + sch_p->anim_dispath_count - 1; int next_frames_count = model->animations[next_anim - tr_moveable->animation_index].frames_count; int next_frame = tr_adisp->next_frame - tr->animations[next_anim].frame_start; int low = tr_adisp->low - tr_animation->frame_start; int high = tr_adisp->high - tr_animation->frame_start; adsp->frame_low = low % anim->frames_count; adsp->frame_high = (high - 1) % anim->frames_count; adsp->next_anim = next_anim - tr_moveable->animation_index; adsp->next_frame = next_frame % next_frames_count; #if LOG_ANIM_DISPATCHES Sys_DebugLog(LOG_FILENAME, "anim_disp[%d], frames_count = %d: interval[%d.. %d], next_anim = %d, next_frame = %d", l, anim->frames_count, adsp->frame_low, adsp->frame_high, adsp->next_anim, adsp->next_frame); #endif } } } } } } 


The publication

When the skeletal models started working, it was already possible to move on to their leveling and “liven up” Lara, which required the presence of physics. To begin with, it was decided to write my physics engine in order to become better acquainted with the topic and then more thoroughly approach the choice of ready-made products. The first thing that is required to create a character controller is to define heights. The function for determining the intersection of a triangle and a ray was originally written (based on a barycentric algorithm ). After that, such basic methods were added as the definition of the intersection of moving segments, a triangle and a sphere, a triangle and a triangle. It should be noted that this approach eliminates the possibility of the appearance of the so-called “tunnel effect” (when due to high speed objects with high speeds can fly through each other without a collision), inherent to physical engines based on impulse.

And Lara runs through the levels, even if bypassing all the steps of any size, but does not fall out of the map! When the project was in such a state, I wrote to Anatoly Lwmte that it was cool that at least someone was interested in the first parts of the Tomb raider . Thus began the correspondence, thanks to which interest in the project began to reappear. After I signed up for tombraiderforums.com (Anatoly was already there long enough, with his project to improve the fourth part of the Tomb raider engine). Thanks to him, a topic with my engine and many improvements in the code appeared on this forum: sound manager, alteration of the state control system (before that I had a switch on animation numbers, now it is on state numbers), etc. The presence of people interested in the project is well motivated to develop the project.

Physics + Renderer Optimization

Since I used my physics, and even with poor optimization, fps began to sink in some places. By long picking various open source physics engines, the bullet was chosen. First, I added a collision filter in the case of intersecting rooms. The fact is that the design of the original levels allows the intersection of 2 or more completely different rooms in one place, while the objects of one room should not affect the objects of another; similarly with rendering. Currently, I try to bring the character controller to mind: eliminate the possibility of passage through walls (occurs in a series of animations against the wall) and finish the character's reaction and behavior in the case of climbing on walls and ceiling.

Let's go back to OpenGL. Initially, in the engine, polygons were drawn using glVertex3fv (...), etc .; About the performance of this approach and the speed of the engine, we can say one thing: they are not. Therefore, after studying the part concerning the VBO (Vertex Buffer Object), I did the optimization and began, if possible, to store the vertex data of the polygons in the video memory and draw the mesh in one go. The speed has increased markedly. However, because for one mesh, textures could lie in different pixel arrays, switching OpenGL textures was more often than necessary, and the fact that the textures of many different objects could be stored in one pixel array created “artifacts” with anti-aliasing turned on. Cochrane with tombraiderforums.com took up the renderer optimization and wrote a texture atlas with the boundaries between textures. Thanks to this innovation, all level textures are stored in 1 - 2 OpenGL textures and anti-aliasing does not lead to the appearance of “artifacts”. In addition, he made the project port on MacOS.

When there were no ideas at all for what and how to tackle the engine, I simply looked for errors in the code, corrected its structure or changed the plug-in libraries. Thus, a “relocation” was carried out from SDL1 to SDL2 , from freetype1 + gltt to freetype2 + ftgl . Similarly, I got the idea to add anti-aliasing to animations using spherical interpolation slerp . Here I want to add: be attentive to mathematical algorithms, especially when it comes to "arches" (asin, acos, atan ...) - the loss of a sign is fraught with killer frames with a skewed and twisted skeleton. I advise you to look at the implementation of the slerp source code bullet. After adding anti-aliasing, I could no longer look at unsmoothed animations. Then there was the need to load and play the sound, and then run through the levels in deathly silence is not very, though Tomb raider .

Add sound

To use sound, using SDLAudio + SDLMixer is completely inadequate, and to go into the audio stream conversion algorithms and make bicycles to create effects is a completely bad idea. After consulting with Anatoly, it was decided to use OpenAL . Since I was guided by the fact that as much as possible of the platform-dependent code shifted to the SDL , I did not invent anything better than writing the SDL_backend for OpenAL .However, it worked, I added a tool to the engine, and Anatoly made everything play when necessary, where necessary, and with the necessary effects.

And now it's time to revive all kinds of levers, traps and other triggers of the game world. In fact, the development here was based on logic: I need to implement something, what tools are needed for this, how to implement them. The main function used for script operation is to get a pointer to an object by a numeric id, then any LUA function can process all the necessary objects. To dynamically add and remove objects and the ability to quickly access them by id, I applied red-black trees . In theory, it was possible to apply a hash table , but then personal preferences were more likely to work.
As a result, now the scripting system allows you to carry out practically any manipulations with objects and animations, create tasks (and timers based on them), pick up objects, press a growl and buttons, opening and closing doors thereby and not only. Thanks to the efforts of people from the tombraiderforums.com community, gameflow_manager was added, responsible for switching from one level to another, loading necessary scripts and screen savers, loading information about light sources and implementing a simple lightmap based on correcting vertex colors and a cmake script for building under OS Lunux .

Afterword

In the end, I would like to draw attention to the fact that when you use third-party resources, it’s easier with tests and you don’t have to be loaded with content creation, but this places limitations on the architecture of the engine or makes it necessary to convert formats when loading in order to avoid cracking crutches inside game engine. And there are a lot of crutches in the original Tomb raider .

Further plans in the project are simple:

1) fix existing bugs, especially with physics, and expand the capabilities of the character controller;
2) “revive” the enemies on the maps, add AI and weapons;
3) expand the system for managing skeletal model animations to switch meshes;
4) expand the capabilities of the scripting system and write key level scripts so that you can go through the normal game;
5) to improve the graphics in the game, add effects, but here I am counting on the help of more qualified OpenGL programmers;

Finally, some videos with an example of the engine:




Thanks for attention!

Source: https://habr.com/ru/post/239751/


All Articles