Sign in to follow this  
snowfell

level design and rendering procedure

Recommended Posts

Hey I was wondering if I could get some help with some questions I had about Opengl rendering and level design tools. At this point I was able to make a simple level design tool and a start to a game program (uses c/c++ and opengl). Basically my level design tool is made in game maker (for the time being, and because it was really easy to setup). In the tool, you use create shapes on a 3d plane and then when done hit compile. Then all shape data is compiled into a text-like file. This file contains commands friendly to my game. In my game I have been using functions to render shapes (not smart but simple). My problem is that every time my game goes through the rendering loop it has to open the map file, read data, and close the file for each coordinate. This is strongly inefficient and is causing massive amounts of lag. I own the book Beginning Opengl Game Programming and was reading through the part about improving performance. I was able to pick up that verticies can be read and saved to a array (a so called vertex array) and rendered from there save tons of time and preventing lag. I believe this is what most professional games use. My main question is what is the best way of setting up a level design tool and game relationship. Should I move past the basic shapes to render everything or will that effect anything. What are some of the key things to know about vertex arrays. If you need me to provide more information on anything please let me know. Thanks for any help you can provide!

Share this post


Link to post
Share on other sites
Which OpenGL commands are you using now to draw your shapes? Can you post the piece of source code that does the open_file-read_coordinate-close_file-draw_something part?

Share this post


Link to post
Share on other sites
This is just a simple code I use for rendering walls or any other flat vertical object:
void BuildWall(float x1, float y1, float z1, float x2, float y2, float z2, int tex_num, float tex_hor, float tex_vert)
{
glBindTexture(GL_TEXTURE_2D,texture_lib[tex_num]);
glBegin(GL_QUADS);
glTexCoord2f(0.0,0.0); glVertex3f(x1,y1,z1);
glTexCoord2f(tex_hor,0.0); glVertex3f(x2,y1,z2);
glTexCoord2f(tex_hor,tex_vert); glVertex3f(x2,y2,z2);
glTexCoord2f(0.0,tex_vert); glVertex3f(x1,y2,z1);
glEnd();
glBindTexture(GL_TEXTURE_2D,texture_lib[0]);
}






So instead of having to up in tons of separate vertices I just put functions like this together as a simple solution till I found something better. My level design tool just takes any shapes draw out and converts then to coordinates for these functions.

And this is the temporary code I have setup for reading a external file and then deciding what shape to render.
bool BuildObjectFromFile(std::ifstream &cur_file)
{
int pos,tex_num,str0;
float x1,y1,z1,x2,y2,z2,tex_hor,tex_vert;

cur_file >> str0;
cur_file >> x1 >> y1 >> z1 >> x2 >> y2 >> z2 >> pos >> tex_num >> tex_hor >> tex_vert;

if(cur_file.fail()) { return false; }

switch(str0)
{
case 0: BuildCube(x1,y1,z1,x2,y2,z2,tex_num,tex_hor,tex_vert); break;
case 1: BuildFlat(x1,y1,z1,x2,y2,z2,pos,tex_num,tex_hor,tex_vert); break;
case 2: BuildWall(x1,y1,z1,x2,y2,z2,tex_num,tex_hor,tex_vert); break;

default: return false;
}

return true;
}

bool BuildObject(std::ifstream &cur_file)
{
if(!BuildObjectFromFile(cur_file))
{
return false;
}

return true;
}

bool ImportMap(const std::string &filename)
{
std::ifstream cur_file(filename.c_str());

if(cur_file.fail())
{
ErrorPost(3,"One or more map files could not be loaded!");
return false;
}

if(!BuildObject(cur_file)) return false;

while(!cur_file.eof())
{
if(!BuildObject(cur_file)) return false;
}
return true;
}


Share this post


Link to post
Share on other sites
So, BuildWall is actually RenderWall. The simplest thing to do is not to get your shape parameters (the type, vertices, and texture stuff) from a file each frame, but get them from a simple structure of class.

The slowest location for shape parameters is in a file.
A much faster location is (system) memory.
The fastest location is GPU card memory.
(note that after loading from file once, the file will be in the OS file cache in system memory, speeding up file operations anyway)

Every shape you have consists of the same parameters at the moment, so you can define a simple structure to hold the parameters, and load the structures with information once when the program starts. Something like this:

enum eShapeType
{
ShapeType_Wall,
ShapeType_Flat,
ShapeType_Cube
} ;


struct sVertex
{
float x ;
float y ;
float z ;
} ;


struct sTextureVertex
{
float s ;
float t ;
} ;


struct sShape
{
eShapeType type ;
sVertex vertices [2] ;
sTextureVertex tex_vertex ;
} ;



sShape *LoadShape (iostream &file)
{
sShape *new_shape = new sShape ;

// ... load shape from opened file

return new_shape ;
}


static std::vector<sShape *> g_shapes ;


void LoadShapes (std::string file_name)
{
// ... open file and load shapes until EOF
for (;;)
{
if (end_of_file)
break ;

// Load single shape
sShape *new_shape = LoadShape (file) ;

// Add to list of loaded shapes
g_shapes.push_back (new_shape) ;
}
}



void RenderShape (sShape *shape)
{
switch (shape->type)
{
// ... Render depending on type
}
}



void RenderShapes (...)
{
for (int k = 0 ; k < g_shapes.size () ; k++)
RenderShape (g_shapes [k]) ;
}






Now, this is as simple as it gets, and not very object-oriented. If you have this working, however, you can change it easily to a more trendy implementation. For example:

- Change the shape into a class (e.g. cShape). Define member functions such as cShape::Load () and cShape::Render (). Derive specialised shape classes from cShape (cWall, cCube), and provide these with overloaded Render operations that render the derived shape.
- Make the shape generic, so that a shape is just a collection of a variable number of vertices, texture coordinates, and other stuff. This way, a cShape can hold any shape that you want (e.g. a cube with 8 vertices and 8 texture coordinates, a soldier with 236 vertices and 708 texture vertices).

Share this post


Link to post
Share on other sites
Oh, and using glVertex3f and glTexcoord2f is fine, until you have thousands and thousands of vertices per frame. Only then you will want to look into vertex and index buffers and glDrawRangeElements.

Share this post


Link to post
Share on other sites
First, some suggestions for code cleanliness. I assume that you intend the file to have each object described on its own line.

0) BuildObject() right now just calls BuildObjectFromFile(), and returns the same value that BuildObjectFromFile() does. You could just call it directly, or even simpler, just rename the function.

1) A lot of the error handling logic will be simpler using exceptions. That also allows the calling code to decide how to handle a file-not-found situation, instead of hard-coding the ErrorPost() here.

2) I assume you don't actually want to store a "pos" in the file unless it's needed. To make this work more easily, call a separate function for each possible "object ID". Of course, you already have these functions: they're the BuildX functions. You'll just need to change them to take in a stream, and read their arguments. Or you could overload the functions in question, if you still want to be able to call them the other way.

3) Don't loop "while(!cur_file.eof())". It doesn't do what you want. .eof() on a file will only be true after a read has already failed. Just after you finish reading the last object, the file doesn't have the 'eofbit' set yet, so you call the function one more time with no data.... What you want to do is loop while some read operation is successful, and then use the read-in data. This interacts with the idea of putting each object on its own line of the file: you can loop while "read a line of text" is successful, then feed the line of text to the "builder", and let it check whether there is an error. This way, you know whether the problem is due to EOF or something else.

4) Generalize. Your code doesn't rely on the data coming from a file, so don't dictate that it must. Just use the base std::istream class.


struct bad_data: std::exception {};
struct file_not_found: std::exception {};

void BuildCube(std::istream& data) {
int tex_num;
float x1, y1, z1, x2, y2, z2, tex_hor, tex_vert;

data >> x1 >> y1 >> z1 >> x2 >> y2 >> z2 >> tex_num >> tex_hor >> tex_vert;

if (!data) { throw bad_data(); }
// rest of BuildCube() goes here; or call the other version with those arguments.
}

// Similarly BuildWall and BuildFlat, except BuildFlat also reads 'pos'.

void BuildObject(std::string& line) {
// Ignore blank lines
if (line.empty()) { return; }

std::stringstream data(line);
int type;
data >> type;
if (!data) { throw bad_data(); }

switch(type) {
case 0: BuildCube(data); break;
case 1: BuildFlat(data); break;
case 2: BuildWall(data); break;
default: throw bad_data();
}
}

void ImportMap(const std::string& filename) {
std::ifstream cur_file(filename.c_str());

if (!cur_file) { throw file_not_found(); }
// check out how neatly we do this now:
std::string line;
while (std::getline(cur_file, line)) { BuildObject(line); }
}



Now we can go about the business of actually loading things into data structures, instead of just drawing them immediately. Notice how the return values have now been freed up to return the actual data: the constructed objects. :)

It'll look something like this:


struct bad_data: std::exception {};
struct file_not_found: std::exception {};

class Primitive {
// We move the data needed by each Model into a base Model object:
int tex_num;
float x1, y1, z1, x2, y2, z2, tex_hor, tex_vert;

public:
// We set up for polymorphism:
virtual ~Model() {}
// And describe the functionality that all Models have:
virtual void draw() = 0;
};

class Cube : public Primitive {
// We make them uncopyable, for now:
Cube(const Cube&); // don't implement this!
Cube& operator=(const Cube&); // likewise.

public:
Cube(std::istream& data) {
data >> x1 >> y1 >> z1 >> x2 >> y2 >> z2 >> tex_num >> tex_hor >> tex_vert;
if (!data) { throw bad_data(); }
}
virtual void draw() {
// Drawing logic goes here.
}
};

// Similarly for Flats and Walls.

// Now we need a data structure that represents a collection of these primitives:
typedef boost::shared_ptr<Primitive> Object;
typedef std::vector<Object> Model;

// Our creation function dynamically allocates one of the Primitives, which
// we then wrap up in a shared_ptr and return:
Object BuildObject(std::string& line) {
// Ignore blank lines
if (line.empty()) { return; }

std::stringstream data(line);
int type;
data >> type;
if (!data) { throw bad_data(); }

switch(type) {
case 0: Cube* result = new Cube(data); return Object(result);
case 1: Flat* result = new Flat(data); return Object(result);
case 2: Wall* result = new Wall(data); return Object(result);
default: throw bad_data();
}
}

Model ImportMap(const std::string& filename) {
std::ifstream cur_file(filename.c_str());

if (!cur_file) { throw file_not_found(); }
// Now we need to create storage for the data:
Model result;
// And we put each Object into the Model:
std::string line;
while (std::getline(cur_file, line)) {
result.push_back(BuildObject(line));
}
return result;
}

// To draw a Model, we draw each Primitive:
void drawModel(const Model& m) {
for (Model::iterator it = m.begin(), end = m.end(); it != end; ++it) {
// '*it' gives us the Object, and a second dereference gives us
// the Primitive.
(*it)->draw();
}
}

Share this post


Link to post
Share on other sites
Hey thanks Mattijs2 and Zahlman for your help. I will have to admit I am a bit over whelmed with the answers provided, but thats probably because I don't know all that much about Opengl or C++ yet. I will keep at it and see what I can do. Thanks again

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this