# Lecture 10 - Game Engines and ECM ### SET09121 - Games Engineering
Babis Koniaris
School of Computing. Edinburgh Napier University --- # Requirements of a Game --- # What does a game need? From a Programmer's POV: - **Content** - 3d Models, Shaders, Textures, Text, Fonts, Music, Video, Saves, Levels/Game State etc.. - **Processing & I/O** - Rendering, User input, Networking, Audio, Loading/Unloading/Streaming - **Logic and Mechanics** - Physics, AI, Gameplay rules. --- # Game Engine Architecture --- # Complexity
![image](assets/images/2d_engine_architecture_b.png)
--- # Complexity
![image](assets/images/2d_engine_architecture_a.png)
--- # Combating the Complexity - Game Codebases Get Big Fast - Taming and maintaing it tests your ability as a Software Engineer - We've covered some Software Patterns that you can pull out of your toolbox to help. These help solve small isolated design problems. - Use standard approaches to deal with complexity: abstraction, decoupling, encapsulation - E.g. separating your gameplay logic (jump!) from the core engine logic (load a file!). --- # Build The Wall ![image](assets/images/api_wall.png) --- # Abstraction --- # Abstraction - And so we build Games Engines Do we need them? After all we didn't always have them? Q: When do you think you need to separate engine code? - A: From the beginning - A: Later, when you identify things that can be abstracted - A: Never! Who's got the time for that Not all games need an 'engine' - Some are simplistic enough to not need it. - We already have an engine somewhat: SFML. - This is already isolated from our code. But it doesn't do everything we need. --- # Build The Wall ![image](assets/images/api_wall2.png) --- # Software Abstraction techniques --- # Object Orientation. OO is hammered into you since 1st year as the solution to software complexity. ![image](assets/images/software_development.png) ... But it's not perfect. Enter: The Evil Tree Problem --- # Object Orientation & the Evil Tree ![image](assets/images/oo_strcuture.PNG) --- # Possible Evil Tree Solutions To fix this we need to either: - Use multiple Inheritance (Danger Zone, bugs) - Use interfaces (which C++ can emulate, tediously) - Throw our design in the bin, unceremoniously, and... - **USE COMPOSITION, NOT INHERITANCE** --- # The Evil Tree Solution - The Entity Component Model ![image](assets/images/ecm_strcuture.png) --- # ECM ECM enables Data Oriented design. ![image](assets/images/ecs2.png) --- # ECM PseudoCode ```cpp class Entity { protected: List_of_components; public: update(delta_time); render(); addComponent(Component); getComponents(); removeComponent(Component); }; class Component { Entity* _parent; update(delta_time); render(); }; ``` --- # ECM Code ```cpp class Entity { protected: std::vector<std::shared_ptr
> _components; public: virtual void update(double dt); virtual void render(); template <typename T, typename... Targs> std::shared_ptr
addComponent(Targs... params); template
const std::vector<std::shared_ptr
>& getComponents() const; void removeComponent(std::shared_ptr
); }; class Component { Entity* const _parent; virtual void update(double dt) = 0; virtual void render() = 0; }; ``` --- # ECM Code ```cpp auto pl = make_shared
(); auto s = pl->addComponent
(); s->setShape<sf::CircleShape>(12.f); s->getShape().setFillColor(Color::Yellow); pl->addComponent
(); // later on... pl->getComponents
()[0]->setSpeed(150.f); ``` --- ## C++ TEMPLATES have arrived! `template
!` --- # Why not just use classes? ```cpp class Component {}; class ShapeComponent : public Component{} //no change class Entity { protected: std::vector<std::shared_ptr
> _components; //No change public: //template
//std::shared_ptr
addComponent(Targs... params){} //Or no templates: Component* addComponent(Component*){} } ``` ```cpp auto pl = make_shared
(); ShapeComponent* sc = new ShapeComponent(); auto s = pl->addComponent(sc); // later on, we want to get the first-found PlayerMovement component, to set the speed... // With templates, it could look like this // pl->getComponent
()->setSpeed(150.f); // Without templates, we would have to do something like this: for(int i=0;i < pl->getComponentNum(); ++i) { PlayerMovementComponent playerMovementComponent = std::dynamic_cast
(pl->getComponent(i)); if( playerMovementComponent != null) { playerMovementComponent->setSpeed(150.0f); break; } } // The main problem is that we can't generalize this code (by wrapping into a function) so support more component types ``` --- # ECM: generalising with templates ```cpp enum class ComponentType { PlayerMovement, AI, Rendering } class Component { public: virtual ComponentType componentType() const=0; }; class PlayerMovementComponent : public Component { public: static const ComponentType kComponentType = ComponentType::PlayerMovement; ComponentType componentType() const override { return kComponentType; } } class Entity { // ... (more code) template
std::shared_ptr
getComponent() { for(int i=0;i < getComponentNum(); ++i) { auto component = getComponent(i); if(component->componentType() == T::kComponentType) return component; } } // ... (more code) }; ``` --- # ECM template Deep Dive 1 ```cpp class Entity { template <typename T, typename... Targs> std::shared_ptr
addComponent(Targs... params) { static_assert(std::is_base_of<Component, T>::value, "T != component"); std::shared_ptr
sp(std::make_shared
(this, params...)); _components.push_back(sp); return sp; } }; ``` --- # ECM template Deep Dive 2 ```cpp class Entity { template <typename T, typename... Targs> std::shared_ptr
addComponent(Targs... params) { static_assert(std::is_base_of<Component, T>::value, "T != component"); std::shared_ptr
sp(std::make_shared
(this, params...)); _components.push_back(sp); return sp; } }; ``` ```cpp class Component { private: Entity* _parent; public: Component(Entity* const p); }; class PickupComponent : public Component { private: bool _isBig; public: PickupComponent() = delete; PickupComponent(Entity* p, bool big = false); }; ``` --- # ECM template Deep Dive 3 Replace T with PickupComponent ```cpp class Entity { std::shared_ptr
addPickupComponent(bool big) { std::shared_ptr
sp(std::make_shared
(this,big)); _components.push_back(sp); return sp; } }; ``` ```cpp class Component { private: Entity* _parent; public: Component(Entity* const p); }; class PickupComponent : public Component { private: bool _isBig; public: PickupComponent() = delete; PickupComponent(Entity* p, bool big = false); }; ``` --- # ECM template Deep Dive 4 With Old Raw Pointers ```cpp class Entity { PickupComponent* addPickupComponent(bool big) { PickupComponent* sp = new PickupComponent(this, big); _components.push_back(sp); return sp; } }; ``` with New Safe Smart Pointers. ```cpp class Entity { std::shared_ptr
addPickupComponent(bool big) { std::shared_ptr
sp(std::make_shared
(this,big)); _components.push_back(sp); return sp; } }; ``` --- # Summary --- # Summary - Games are complicated systems! - This is why we use clever tricks to help simplify the problem - ECM is a great way to reuse code - But we do have to use C++ templates to do these nicely