patterncppMinor
Game engine Scene class using raw pointers
Viewed 0 times
enginerawgameusingpointersclassscene
Problem
I'm writing a C++ game engine and I have a scene class that I'd like to present as a code example to a team that is interested in my skills.
The scene class holds games entities, updating and rendering them using a camera.
I'm interested in the stylistic or functional improvements that could be made to improve this class, as well as any possible optimisations. In particular, I was told by an industry professional that smart pointers (like
Scene.h
```
#pragma once
#include
#include "Entity.h"
#include "Renderer.h"
#include "Window.h"
#include "Components\Camera.h"
namespace Core
{
// A class that represents a level or screen within the game. It maintains
// a collection of entities which make up the objects in the scene. These are
// updated and rendered each frame. The scene must be given a specified camera
// entity to view the world.
class Scene
{
public:
// An exception used by a scene to declare that an entity is missing
// a required component.
class MissingComponentException : public std::runtime_error
{
public:
MissingComponentException(const std::string& component, const std::string& message)
: std::runtime_error(component + " component missing: " + message){}
};
// Construct a new empty scene
Scene();
// Deconstructs the scene and any entities that it contains
~Scene();
// Adds an entity to the current scene. The scene will delete this
// entity when it is deleted.
void AddEntity(Core::Entity* entity);
// Gets an entity from the scene by name and returns it by pointer.
Core::Entity* GetEntity(std::string name);
// Sets the entity which will be used as the camera for rendering the
// scene. This entity must have a camera component otherwise
// MissingComponent
The scene class holds games entities, updating and rendering them using a camera.
I'm interested in the stylistic or functional improvements that could be made to improve this class, as well as any possible optimisations. In particular, I was told by an industry professional that smart pointers (like
shared_ptr) are frowned upon in game engines because of instances where reference counting adds an overhead. Is there truth in this? Scene.h
```
#pragma once
#include
#include "Entity.h"
#include "Renderer.h"
#include "Window.h"
#include "Components\Camera.h"
namespace Core
{
// A class that represents a level or screen within the game. It maintains
// a collection of entities which make up the objects in the scene. These are
// updated and rendered each frame. The scene must be given a specified camera
// entity to view the world.
class Scene
{
public:
// An exception used by a scene to declare that an entity is missing
// a required component.
class MissingComponentException : public std::runtime_error
{
public:
MissingComponentException(const std::string& component, const std::string& message)
: std::runtime_error(component + " component missing: " + message){}
};
// Construct a new empty scene
Scene();
// Deconstructs the scene and any entities that it contains
~Scene();
// Adds an entity to the current scene. The scene will delete this
// entity when it is deleted.
void AddEntity(Core::Entity* entity);
// Gets an entity from the scene by name and returns it by pointer.
Core::Entity* GetEntity(std::string name);
// Sets the entity which will be used as the camera for rendering the
// scene. This entity must have a camera component otherwise
// MissingComponent
Solution
Smart Pointers or Raw Pointers
I was told by an industry professional that smart pointers (like
Yes,
You don't provide a copy constructor, so the default just is a simple copy of all your raw pointers. Which will then get
I don't know if
This solves all of the problems above. Now,
This leaves the camera. You don't
Although, you have
Bug
In
When
Then, you call
The correct way to erase elements from a vector which match a certain condition is the Erase-Remove idiom:
And switching to smart pointers takes care of the
While we're in this function, the moving from
Excessive Comments
Comments are good, and many of course comments are quite valuable. Some are completely unnecessary though. Clearly the default constructor for
Another example:
Yeah, that's pretty obvious.
Other code comments
In
I was told by an industry professional that smart pointers (like
shared_ptr) are frowned upon in game engines because of instances where reference counting adds an over head. Yes,
shared_ptr has overhead over raw pointers. But shared_ptr also solves problems that raw pointers have. Namely, what happens if you do:Scene s;
s.AddEntity(e1);
s.AddEntity(e2);
...
{
Scene s2 = s;
// <== s2 destroyed here
}
// <== s destroyed hereYou don't provide a copy constructor, so the default just is a simple copy of all your raw pointers. Which will then get
deleted twice! Also, what if you never called Post()? Now you're leaking memory.I don't know if
Scene should be copyable or not - but right now it is and that's broken. So at the very least, we can easily fix it by using a smart pointer:void AddEntity(std::unique_ptr );
std::vector> entities;
std::vector> entitiesToAdd;This solves all of the problems above. Now,
Scene isn't copyable. It is moveable, and that just works correctly. You're also no longer leaking anything from entitiesToAdd. This also obviates the need for a destructor, so you don't have to provide one. This leaves the camera. You don't
delete it, so it's unclear who actually owns it. If the Scene does not own it, then a raw pointer is of course fine.Although, you have
GetEntity(). What do you do with the entities returned from that function? Are they stored somewhere else? If you need shared ownership, then you need to use a shared_ptr (or something similar, like intrusive_ptr). If you need shared ownership, it doesn't really matter how much overhead those smart pointers add... Bug
In
Post(), you will fail to erase all the destroyed entities based on their configuration. Also, you're deleting the wrong ones. Let's say we have two entities, A and B, the first of which needs to be destroyed:0] A, IsDestroyed() is true
1] BWhen
i==0, the check passes, then we call erase(), at which point our vector now looks like0] BThen, you call
delete entities[i];, which deletes B. Now we have a vector of one element, which isn't even a valid element, and we will have leaked A. If B also needed to be erased, at this point i becomes 1 so we decide we're done, and don't erase it. The correct way to erase elements from a vector which match a certain condition is the Erase-Remove idiom:
entities.erase(
std::remove_if(entities.begin(), entities.end(), std::mem_fn(&Entity::IsDestroyed)),
entities.end());And switching to smart pointers takes care of the
delete.While we're in this function, the moving from
entitiesToAdd to entities could be done using vector's insert() member function, which will be more efficient than repeated push_back's:entities.insert(entities.end(),
std::make_move_iterator(entitiesToAdd.begin(),
std::make_move_iterator(entitiesToAdd.end()));
entitiesToAdd.clear();Excessive Comments
Comments are good, and many of course comments are quite valuable. Some are completely unnecessary though. Clearly the default constructor for
Scene should construct a new empty scene, and the destructor destroys it, and so forth. When you're writing comments, ask yourself if somebody reading your code actually needs the comment. Another example:
// Renders the scene using the given renderer and window.
void Render(std::shared_ptr renderer, std::shared_ptr window);Yeah, that's pretty obvious.
Other code comments
UpperCase for member functions is not very common - I would strongly suggest either camcelCase or snake_case.GetEntity() is searching for a given entity that matches a name. In other words, you're finding an entity if the name matches. Let's just use std::find_if! Also, take your argument by reference to const to avoid an unnecessary copy:Core::Entity* Scene::getEntity(std::string const& name)
{
auto it = std::find_if(entities.begin(), entities.end(),
[&](std::unique_ptr& entity){
return entity->getName() == name; // ==, not compare()
});
return it != entities.end() ? *it : nullptr;
}In
Update(), prefer to use a range-based for expression:for (auto& entity : entities) {
entity->Update();
}Code Snippets
Scene s;
s.AddEntity(e1);
s.AddEntity(e2);
...
{
Scene s2 = s;
// <== s2 destroyed here
}
// <== s destroyed herevoid AddEntity(std::unique_ptr<Core::Entity> );
std::vector<std::unique_ptr<Core::Entity>> entities;
std::vector<std::unique_ptr<Core::Entity>> entitiesToAdd;0] A, IsDestroyed() is true
1] Bentities.erase(
std::remove_if(entities.begin(), entities.end(), std::mem_fn(&Entity::IsDestroyed)),
entities.end());entities.insert(entities.end(),
std::make_move_iterator(entitiesToAdd.begin(),
std::make_move_iterator(entitiesToAdd.end()));
entitiesToAdd.clear();Context
StackExchange Code Review Q#116138, answer score: 8
Revisions (0)
No revisions yet.