Personally I recommend keeping the draw function out of the Object class itself. I even recommend keeping the Objects location/coordinates out of the Object itself.
That draw() method is going to be dealing with low level rendering API of either OpenGL, OpenGL ES, Direct3D, your wrapping layer on those APIs or an engines API. It might be that you have to swap between then (If you wanted to support OpenGL + OpenGL ES + Direct3D for example.
That GameObject should just contain the basic information about it's visual appearance such as a Mesh or maybe a bigger bundle including shader inputs, animation state and so on.
Also you are going to want a flexible graphics pipeline. What happens if you want to order objects based on their distance to the camera. Or their material type. What happens if you want to draw a 'selected' object a different color. What about if instead of actually rending as soo as you call a draw function on an object, instead it puts it into a command list of actions for the render to take (might be needed for threading). You can do that kind of thing with the other system but it's a PITA.
What I recommend is instead of drawing directly, you bind all the objects you want to another data structure. That binding only really need to have a reference to the objects location and the rendering information.
Your levels/chunks/areas/maps/hubs/wholeworld/whatever get given a spacial index, this contains the objects and returns them based on coordinate queries and could be a simple list or something like an Octree. It could also be a wrapper to something implemented by a 3rd party physics engine as a physics scene. It allows for you to do things like "Query all objects that are in the view of the camera with some extra area around them", or for simpler games where you can just render everything grab the whole list.
Spacial Indexes don't have to contain the actual positioning information. They work by storing objects in tree structures in relation to the location of other objects. They can be though of as a kind of lossy cache allowing a quick lookup of an object based on its position. There's no real need to duplicate your actual X, Y, Z coordinates. Having said that you could if you wanted to keep
In fact your game objects don't even need to contain their own location information. For example an object that hasn't been put into a level shouldn't have x,y,z coordinates, that makes no sense. You can contain that in the special index. If you need to lookup the coordinates of the object based on its actual reference then you will want to have a binding between the object and the scene graph (scene graphs are for returning objects based on coordinates but are slow at returning coordinates based on objects).
When you add an Object to a Level. It will do the following:
1) Create a Location Structure:
class Location {
float x, y, z; // Or a special Coordinates class, or a vec3 or whatever.
SpacialIndex& spacialIndex; // Note this could be the area/level/map/whatever here
};
This could also be a reference to an object in a 3rd party physics engines. Or it could be an offset coordinates with a reference to another location (for a tracking camera or an attached object or example). With polymorphism it could be either depending on if it's a static or dynamic object. By keeping a reference to the spacial index here when the coordinates are updated the spacial index can be too.
If you are worried about dynamic memory allocation, use a memory pool.
2) A binding/linking between your object, its location and the scene graph.
typedef std::pair<Object, Location> SpacialBinding.
3) The Binding is added to the spacial index inside of the level at the appropriate point.
When you are preparing to render.
1) Get the camera (It will just be another object, except it's location will be tracking the players character and your renderer will have a special reference to it, in fact that's all it really needs).
2) Get the camera's SpacialBinding.
3) Get the spacial index from the binding.
4) Query the objects that are (possibly) visible to the camera.
5A) You need to have the visual information processed. Textures uploaded to the GPU and so on. This would be best done in advance (such as on level load) but perhaps could be done at runtime (for an open world, you could load stuff when you are nearing a chunk but should still be done in advance).
5B) Optionally build a cached render tree, if you want to depth/material sort or keep track of nearby objects the might be visible at a later time. Otherwise you can just query the spacial index everytime it will depend on your game/performance requirements.
Your renderer will likely need a RenderBinding object that will link between the Object, the coordinates
class RenderBinding {
Object& object;
RenderInformation& renderInfo;
Location& location // This could just be a coordinates class.
}
Then when you render, just run though the list.
I have used references above but they could be smart pointers, raw pointers, object handles and so on.
EDIT:
class Game {
weak_ptr<Camera> camera;
Level level1;
void init() {
Camera camera(75.0_deg, 1.025_ratio, 1000_meters);
auto template_player = loadObject("Player.json")
auto player = level1.addObject(move(player), Position(1.0, 2.0, 3.0));
level1.addObject(move(camera), getRelativePosition(player));
auto template_bad_guy = loadObject("BadGuy.json")
level1.addObject(template_bad_guy, {10, 10, 20});
level1.addObject(template_bad_guy, {10, 30, 20});
level1.addObject(move(template_bad_guy), {50, 30, 20});
}
void render() {
camera->getFrustrum();
auto level = camera->getLocation()->getLevel();
auto object = level.getVisible(camera);
for(object : objects) {
render(objects);
}
}
void render(Object& object) {
auto ri = object.getRenderInfo();
renderVBO(ri.getVBO());
}
Object loadObject(string file) {
Object object;
// Load file from disk and set the properties
// Upload mesh data, textures to GPU. Load shaders whatever.
object.setHitPoints(// values from file);
object.setRenderInfo(// data from 3D api);
}
}
class Level {
Octree octree;
vector<ObjectPtr> objects;
// NOTE: If your level is mesh based there might also be a BSP here. Or a hightmap for an openworld
// There could also be a physics scene here.
ObjectPtr addObject(Object&& object, Position& pos) {
Location location(pos, level, object);
objects.emplace_back(object);
object->setLocation(location)
return octree.addObject(location);
}
vector<Object> getVisible(Camera& camera) {
auto f = camera.getFtrustrum();
return octree.getObjectsInFrustrum(f);
}
void updatePosition(LocationPtr l) {
octree->updatePosition(l);
}
}
class Octree {
OctreeNode root_node;
ObjectPtr add(Location&& object) {
return root_node.add(location);
}
vector<ObjectPtr> getObjectsInRadius(const vec3& position, const float& radius) { // pass to root_node };
vector<ObjectPtr> getObjectsinFrustrum(const FrustrumShape frustrum;) {//...}
void updatePosition(LocationPtr* l) {
// Walk up from l.octree_node until you reach the new place
// Check if objects are colliding
// l.object.CollidedWith(other)
}
}
class Object {
Location location;
RenderInfo render_info;
Properties object_props;
Position getPosition() { return getLocation().position; }
Location getLocation() { return location; }
void collidedWith(ObjectPtr other) {
// if other.isPickup() && object.needs(other.pickupType()) pick it up, play sound whatever
}
}
class Location {
Position position;
LevelPtr level;
ObjectPtr object;
OctreeNote octree_node;
setPosition(Position position) {
position = position;
level.updatePosition(this);
}
}
class Position {
vec3 coordinates;
vec3 rotation;
}
class RenderInfo {
AnimationState anim;
}
class RenderInfo_OpenGL : public RenderInfo {
GLuint vbo_object;
GLuint texture_object;
GLuint shader_object;
}
class Camera: public Object {
Degrees fov;
Ratio aspect;
Meters draw_distance;
Frustrum getFrustrum() {
// Use above to make a skewed frustum box
}
}
As for making things 'aware' of each other. That's collision detection. It would be implemented in the Octree probably. You would need to provide some callback in your main object. This stuff is best handled by a proper physics engine such as Bullet. In that case just replace Octree with PhysicsScene and Position with a link to something like CollisionMesh.getPosition().
dynamic_cast<Human*>
, implement something like abool GameObject::IsHuman()
, which returnsfalse
by default but is overridden to returntrue
in theHuman
class. \$\endgroup\$IsA
overrides proved to be only marginally better than dynamic casting in practice for me. The best thing to do is for the user to have, wherever possible, sorted data lists instead of iterating blindly over the whole entity pool. \$\endgroup\$TeamASoldier
andTeamBSoldier
is really identical -- shot at anyone on the other team. All it needs of other entities is aGetTeam()
method at its most specific and, by congusbongus's example, that can be abstracted even further intoIsEnemyOf(this)
kind of interface. The code doesn't need to care about taxonomical classifications of soldiers, zombies, players, etc. Focus on interaction, not types. \$\endgroup\$