Skip to main content
gamedev.net
🔒 Locked

design advice

Started by invinate 4 replies 750+ views
Original Post
invinate
invinate
Hallo, I'm doing a simple solar system modelling program with OpenGL. my main goal is to understand better and practice the advanced features of c++ as well as learn to design classes. I wrote code to read in the data about planets from a file and render the planets. What I need help with is design. It would be great if you could tell me if there's anything wrong with my current design or maybe suggest something better. Another thing i need help with is error checking. What to do with errors? throw an exeption? call exit() or what? how do you handle errors in game programming? here's the .h file:

#ifndef SOLAR_SYS_H
#define SOLAR_SYS_H

#include <windows.h>
#include <GL\gl.h>
#include <GL\glu.h>

#include <fstream>
#include <string>
#include <vector>
#include <list>
#include <stdexcept>

#include <cmath>
#include <cstdlib>


//Abstract base class
class Shape
{ 
  public:
  virtual void draw() = 0;
  virtual bool create() = 0;
};

//Sphere class, based on Shape
class Sphere : public Shape
{    
  public:
    
  virtual ~Sphere()
  {  
    dlat = dlng = 0;
    r = 0.0;
  
    vert.clear();
    vnum = 0;
  }
  
  //explicit Ctor: radius, points for lattitude, points for longitude
  explicit Sphere(GLfloat rad = 1.0, GLint dl = 42, GLint dg = 84);
  
  //draw the sphere
  void draw();
  
  protected:
  bool create(); //virtual
  
  // gen_sphere: create() wrapper, if change == true, then replace the params given in Ctor
  void gen_sphere(GLfloat, GLint, GLint, bool change = false); 
  
  struct Vertex //coords
  {
    GLfloat z, y, x;
  };
  
  private:
  GLint dlat, dlng; //lattitude & longitude
  GLfloat r; //radius
  
  std::vector<Vertex> vert; //coords vector
  size_t vnum; //should be same as vert.size()
  
  //no copy ctor & op=
  Sphere(const Sphere&);
  Sphere & operator=(const Sphere &);
  
};

//defined later
class Planet_info;

// Planets class holds information about all planets
// of the solar system. Only one instance should be
// created for all planets. This is because Planets
// inherits from Sphere and only one sphere should be
// generated in the program. All planets will reuse this
// one sphere.
class Planets : public Sphere
{
  public:
  Planets(const char *s);
  Planets();
  ~Planets();
  
  void draw(); //replace the virual Sphere::draw()
  
  
  protected:
  std:: istream & add_planet(std::istream &); //add a planet from file
  //void del_planet(const Planet_info &);
  
  size_t planet_num; //number of loaded planets
  
    
  private:
  //no copy ctor & op=
  Planets(const Planets &);
  Planets & operator=(const Planets &);
  
  // a std::list with info about all planets 
  std::list<Planet_info*> planets; 
  
  //TEXTURE support goes here (?) 
};


// friend of Planets, with info such as
// radius, orbit, name and other.
class Planet_info
{
  public:
  friend class Planets;

  Planet_info();
  Planet_info(const Planet_info &);
  
  //op=
  Planet_info & operator=(const Planet_info &);   
  
  //read info from file 
  friend std::istream & operator>>(std::istream &, Planet_info &);
  
  private:
  std::string name;
  GLfloat radius;
  GLfloat orbit;
  GLfloat mass;
  GLfloat spd;
  GLfloat revol; 
  //std::list<Planet_info*> satellites;
  
  GLfloat rot1, rot2; //orbit- and revolution-angles
};


#endif
jpetrie
jpetrie
My suggestions:

1) Too much inheritance

Sphere IS-A shape, true, but just because you have to do something doesn't mean you should. The "shape" example for explaining inheritance is basically worthless for anything except trying to explain inheritance to students. It can very easily violate one of the key princpals of object-oriented design, which is that your subclasses must be able to behave like your base classes so that systems using your base classes don't know or care if they are actually using base or derived objects -- this is called the Liskov Substitution Principal" (much credit and thanks to Washu for giving me that link).

With something as generic as a "shape" for a base class, you run the risk of violating that principal. In fact, you're toeing the line already: Shape::create() is pointless as a base class function because it can't be used to adequately construct any kind of shape. Besides, that's what constructors are for. In any case, my first suggestion is to remove "Shape" as a base class, or at least rename it ("Renderable" or something) and remove the create() function.

2) Keep simulation data and render data distinct

This also has ties to my first point. You have a Planet class that holds a bunch of simulation data, and a Planet_info class that holds more simulation data. Roll both of them together, and make Planet its own class (don't derived from Sphere). If you follow my above advice, a sphere IS-A renderable object, but there is nothing extra in terms of rendering that a "planet" needs to be doing that a sphere could not do, so it doesn't need to be its own subclass in the hierarchy. Furthermore, since it contains extra simulation data (that isn't render data) it should not be in the same hierarchy of renderable objects). Instead, a Planet should possibly contain a pointer to a Renderable object that is used to represent the planet when the need arises (depending on how abtract and complex your system will ultimately be, even that may be introducing too many dependancy -- but I'm guessing it will be sufficient for now), and otherwise should be entirely distinct from the hierarchy of renderable objects.

Error handling

Error handling is highly context sensitive. Sometimes its best to return an error code, sometimes the error is entirely unrecoverable from the point you're currently at and you have to throw an exception. A mix of both usually results in a very robust system that handles itself gracefully in the presence of failure -- but not that proper error handling is much than just returning the code or throwing the exception. Doing so appropriately and safely (especially for exceptions, writing exception safe code is not trivial -- its a lot more than putting one try/catch block in main() and assuming you are now "exception safe") requires careful thought and consideration of which method to employ when.

[Edited by - jpetrie on August 4, 2006 8:22:27 AM]
invinate
invinate
First of all thank you for an informative and comprehensive reply. this is just what i wanted it to be.
Let me see if understand correctly what you said:
Shape class is useless - i agree to this, it really has no meaning. i've put it there just to use inheritance i guess. inheritance for inheritance =)
Second, keep different data in different classes. i'm not sure what you mean by rendering data. the points that make up a sphere are actually in Sphere class. the values of radius, orbit are in Planets class and they are also used in transformation (glScalef, glTranslatef). I can't keep them in Sphere because i want only one Sphere object to be present. I've made Planets and Planet_info separate because again i dont want to generate a sphere for each planet. but if i use a pointer to Sphere i could then have a class with everything for every planet.
And where should texture information go?
And another question. If i make some more classes for space objects and stuff, how do i put (should i?) them in one major class (a scene class?). and then call a method from this main class that does drawing of planets, asteroids, stars etc. I kind of confused about this, like i said, i've never done large projects with graphics and i don't know how to organize all this.
jpetrie
jpetrie
Quote:

Second, keep different data in different classes. i'm not sure what you mean by rendering data. the points that make up a sphere are actually in Sphere class. the values of radius, orbit are in Planets class and they are also used in transformation (glScalef, glTranslatef). I can't keep them in Sphere because i want only one Sphere object to be present. I've made Planets and Planet_info separate because again i dont want to generate a sphere for each planet. but if i use a pointer to Sphere i could then have a class with everything for every planet. And where should texture information go?


Render information should go in Sphere. Planet information should go in Planet. Planet should contain a pointer to a Sphere; you need not create a single Sphere for every Planet this way, and you can still roll Planet_info into Planet since that's where it belongs.

Information such as where the planet is, what size it is, and all that, can remain inside Planet for the time being. It is per-instance data that doesn't make any sense inside Sphere. Ideally, a larger system would have its renderable data (objects, like sphere, that you only want to load one of), and render instance data (which contains per-instance renderable information like the world transformation, and textures or shaders that override the mesh's default). Then Sphere would have a "Render Instance" object that contains the appropriate per-instance render data and pointed to the mesh/sphere object that it actually used to render.

I understand you're trying, in some ways, to reduce duplicated data (for example by not storing a "sphere radius" and a "planet radius"), however while your intentions are correct your method isn't. Since Planet should represent a simulation's (i.e., depiction-agnostic) view of the data, and Sphere should represent a render-side object, Planet::radius and Sphere::radius are actually different pieces of information, event though they have the same name and type. Simulation-wise, you may want the two to vary independantly.

For example, you'll generally want Planet::radius to remain fixed throughout the execution of your problem, because the rest of the simulation may be reading that data to operate (for example, to determine if two planets should collide!). However, you may want to change Sphere::radius briefly during runtime to shrink the visual representation of the planets so you can better see some other objects in the scene (for example). You wouldn't want the rest of your simulation to change just because you're observing it differently.

Organizing large amounts of varied renderable data can be tricky, but making the right design choices early on, at this small scale, can make it a lot easier for you in the long run. A very high-level, crude description of a more complex rendering system might look like this:

You have your "render object" (a mesh, or sphere, or something -- these objects are shared; for example, all the planets might share the same sphere render object). You have your "render instance" object, which contains aforementioned transformations to position and orient a specific rendered instance of a render object (it points to a render object). Optionally, this class may be able to overload shaders or textures or other properties from the render object's defaults. Then you have your scene object, that actually deals with rendering things (possibly directly, possibly via some kind of queuing mechanism). At some point during execution, you loop through all the objects in your program and render them by submitting them to the scene -- what you submit for rendering are the "render instance" objects. The simulation data for your application is distinct from all this; it operates mostly independantly (sometimes simulation data, like Planet, would hold a render instance object, but that's really it).

Does that help any?
invinate
invinate
thanks again. a very detailed reply. i think i understand better now what i should do!

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.