Blog 5 min read

OGRE Engine: A Case Study on OOP Principles

Share this article
OGRE Engine: A Case Study on OOP Principles

OGRE (Object-Oriented Graphics Rendering Engine) is a scene-oriented, flexible 3D rendering engine written in C++ designed to make it easier and intuitive for developers to produce applications utilizing hardware-accelerated 3D graphics. The class library abstracts the details of using the underlying system libraries like Direct3D and OpenGL and provides an interface based on world objects and other high-level classes.

Let’s analyze it with CppDepend to discover its design advantages.



OGRE3D Architecture

The dependency graph shows the link between the OGRE3D projects:



The architecture is plugin-oriented, which is very useful for extending Ogre3D without any changes to the kernel project “OgreMain”. For that, the OgreMain project provides the classes needed to communicate with the plugins.

Let’s discover how the kernel project communicates with the plugins; for that, let’s search which classes of OgreMain are used by the RenderSystem_GL plugin by executing this query:

SELECT TYPES WHERE IsDirectlyUsedBy "RenderSystem_GL"


The plugin uses some utility classes and model entities, and overrides some abstract classes to integrate itself into the Ogre3D ecosystem.

Let’s filter the result below and search only for the abstract classes used by the RenderSystem_GL plugin:

SELECT TYPES WHERE IsDirectlyUsedBy "RenderSystem_GL" AND IsAbstract



The plugin can override the scenes, textures, rendering and more. Ogre3D provides an interesting API to easily customize the rendering, which makes adapting it to our needs very easy.

Inheritance and Polymorphism

Using an object-oriented approach can lead to overuse of inheritance to make the most of the polymorphism concept.

This is especially true for OgreMain, which represents the kernel of the Ogre3D framework, where many classes are designed to be overridden by the plugin projects. SELECT TYPES FROM PROJECTS "OgreMain" WHERE NbBaseClass >0





But multiple inheritance increases complexity, and we have to use it carefully. Let’s search for the classes with many base classes.





Only a few classes derive from more than one class.

Abstractness

For a plugin-oriented architecture, the host must have many abstract classes to be more flexible and extensible.

Let's search for abstract classes of OgreMain: SELECT TYPES FROM PROJECTS "OgreMain" WHERE IsAbstract





Namespaces layering

CppDepend provides a DSM graph, and we can triangularize this matrix to focus on the red borders, which highlight dependency cycles.



A dependency cycle exists between Ogre, Ogre::EmitterCommands and Ogre::OverlayElementCommands. Having this dependency is not necessarily problematic, but avoiding this kind of dependency enforces loose coupling; this interesting post explains the benefits of layering.

Let's search for the origin of dependency between Ogre and two other namespaces. SELECT TYPES WHERE IsDirectlyUsedBy "Ogre"





The Ogre namespace uses all the Cmd classes from the other namespaces, and all those classes are used by Ogre::ParticleEmitter as static fields; ParticleEmitter adds them to the CmdParam dictionary.

Maybe it’s possible to do it differently to avoid this dependency cycle, but it’s not a big issue.

Type cohesion

The single responsibility principle states that a class should not have more than one reason to change. Such a class is said to be cohesive. A high LCOM value generally pinpoints a poorly cohesive class. There are several LCOM metrics. LCOM takes its values in the range [0-1]. LCOMHS (HS stands for Henderson-Sellers) takes its values in the range [0-2]. Note that the LCOMHS metric is often considered more efficient at detecting non-cohesive types. An LCOMHS value higher than 1 should be considered alarming. SELECT TYPES WHERE LCOMHS > 0.95 AND NbFields > 10 AND NbMethods >10 AND !IsGlobal





Only a few classes are considered non-cohesive.

Design patterns used

Singleton

To ensure a class has only one instance, the best way is to use the singleton pattern.

Let’s search for classes that are singletons:

SELECT TYPES WHERE DeriveFrom "Ogre.Singleton"





As we can see, almost all manager classes are singletons — generally, a manager is a good candidate to be a singleton. And we can search for manager classes that do not derive from Singleton SELECT TYPES WHERE !DeriveFrom "Ogre.Singleton" AND NameLike "Manager$"





Only a few managers are not derived from Singleton, which is normal because those classes can be instantiated many times.

Factory

The factory pattern is very useful for abstracting the creation of objects; it enforces low coupling and high cohesion, as explained in this post.



But having a factory doesn’t imply that an instance is created by this factory, because we can instantiate the class directly; we have to define a rule to be sure that the factory is used for instantiation.

For example, for the Entity class we can define the following rule to discover each class that instantiates it: SELECT METHODS WHERE DepthOfCreateA "Ogre.Entity" == 1





As we can see, only EntityFactory instantiates the Entity class.

Manager

Manager classes give access to a subsystem and are very useful for modularizing the project; Ogre3D contains many managers, each one representing a different subsystem.

SELECT TYPES WHERE NameLike "Manager$"





Facade

A facade defines a higher-level interface that makes the subsystem easier to use, and we can detect facades in your project by using the Efferent Coupling metric. The Efferent Coupling of a particular type is the number of types it directly depends on. Types where TypeCe > 50 depend on too many other types. They are complex and have more than one responsibility. They are good candidates for refactoring.

But sometimes, in the case of a facade, having a high TypeCe can be normal.

Let’s search for classes with a high TypeCe: SELECT TYPES ORDER BY TypeCe DESC





Not all of those classes are facades, and for each of them we could maybe find an explanation for the high TypeCe — some classes can perhaps be refactored, but we confess that we don’t master Ogre3D enough to tell.

And the most used facade is the Root class; let’s see which subsystems this class uses.



As we can see, the Root class uses almost all manager classes.

And we can search for a manager not accessible by Root with the following CQL query:

SELECT TYPES WHERE !IsDirectlyUsedBy "Ogre.Root" AND NameLike "Manager$" AND !IsAbstract





Observer

The observer is a pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods. It is mainly used to implement event handling systems.

Ogre3D uses Listener classes to implement the observer pattern.

Let’s search for Listener classes in the OgreMain project. SELECT TYPES FROM PROJECTS "OgreMain" WHERE NameLike "Listener$"





Conclusion

Ogre3D is a very clean framework, very well designed and very well commented. You can easily understand the purpose of the design patterns used, and its modularity can help you learn its capabilities faster.

Share this article