In this guide, we will explore the C++ game engine, covering its origins, development process, and various components.
What is a Game Engine?
A compilation of software utilities is referred to as a "game engine". Its primary purpose is to simplify the process of developing video games. These engines vary in size and complexity, ranging from simple ones that provide basic game loops and rendering features to intricate ones that resemble integrated development environments (IDEs). Developers can utilize these comprehensive engines to write scripts, troubleshoot issues, customize level logic and artificial intelligence (AI), design, publish, collaborate, and ultimately build a game entirely within the engine without the need to switch between different tools.
Game frameworks and platforms commonly provide users with an API for communication. Through this interface, developers can engage with engine functionalities and execute complex operations similar to sealed containers.
- To better grasp the concept of this API mechanism, let's delve into it further. For example, a typical game engine API might offer a function such as "IsColliding" for developers to ascertain the collision status between two game entities.
The algorithm is implemented to determine with precision if two shapes intersect, without necessitating the programmer's understanding of its internal workings. The IsColliding function is perceived as an enigmatic entity that executes a specific operation and reliably provides a true or false outcome based on the collision status of the objects. This showcases a functionality commonly offered to users by the majority of gaming engines.
if (IsColliding(player, bullet)) {
lives--;
if (lives == 0) {
GameOver();
}
}
- In addition to serving as a programming API, a game engine's primary duty is to abstract hardware. For instance, 3D engines are often constructed using a specific graphics API, such as Direct3D, Vulkan, or OpenGL . The Graphics Processing Unit (GPU) can be software-abstracted using these APIs.
- Low-level libraries (like DirectX, OpenAL, and SDL ) that enable abstraction and cross-platform access to numerous other hardware components are another example of hardware abstraction . These libraries allow us to access and manage mouse movements, network connections, keyboard events, and audio.
- The code was created in the early years of the game business to get the most performance out of slower equipment . Games were created utilizing a bespoke rendering engine. More developers did not afford code reuse or generic functions used in various situations.
- Most studios employed the same functions and subroutines throughout their games as the size and complexity of games and development teams increased. Studios created internal engines, essentially a jumble of internal files and libraries that handled low-level functions. Due to these features, other development team members could concentrate on more intricate aspects of gameplay, map design , and level customization .
- The id Tech, Build, and AGI classic engines are a few examples of game engines. These engines, which were developed to aid in creating particular games, allowed other team members to quickly design new levels, add unique objects , and modify maps on the fly. These unique engines were also utilized to modify or produce expansion packs for the games' original releases.
- Id Software created id Tech . Each version of the engines that make up id Tech is connected to a particular game. Developers frequently refer to id Tech 0, id Tech 1 , and id Tech 2 as "the Wolfenstein3D engine", "the Doom engine", and "the Quake engine" .
- Another engine that contributed to the development of games in the 1990s is called Build. Ken Silverman developed it to aid in first-person shooter customization. Like ID Tech, Build changed over time, and its several iterations assisted programmers in creating games like Duke Nukem 3D, Shadow Warrior , and Blood . These games are frequently called "The Big Three" and are undoubtedly the most well-known ones made with the Build engine .
- The "Script Creation Utility for Manic Mansion" (SCUMM) is another example of a game engine from the 1990s. SCUMM , an engine created at LucasArts , was the foundation for numerous well-known point-and-click games, including Full Throttle and Monkey Island .
- Full Throttle's dialogues and actions were managed using the SCUMM programming language.
- Game engines also advanced and gained power as machines did. The feature-rich tools in modern engines demand lightning-fast processors , absurd quantities of memory, and specialized graphics cards.
The Rise of Game Engines:
Current processors trade computational cycles for increased levels of abstraction due to their enhanced capabilities. This compromise enables the perception of modern game engines as versatile instruments for the efficient and cost-effective creation of intricate games.
How to Make a Game Engine?
There are multiple procedures involved in developing a game engine. These steps include:
1. Choosing a Programming Language
- Selecting the programming language is one of the first choices we must make that will be used to create the core engine code. There are several high-level languages, like C#, Java, Lua, JavaScript, and raw assembly, C, and C++, which are used to create game engines.
- C++ is one of the most widely used languages for creating gaming engines. Object-oriented programming (OOP) and other programming paradigms that aid in planning and designing big software projects are available in the C++ programming language, which combines speed with both features.
- C++ has the benefit of being a compiled language because performance matters when creating games. If a language is compiled, its final executables can execute directly on the target machine's processor.
- For instance, developers can access the Xbox controller using Microsoft's own C++ APIs.
- We could access memory addresses and specific places mapped to various hardware components in earlier operating systems, such as the MS-DOS . To "paint" a pixel with a specific color, for instance, load a specific memory location with the number corresponding to the right shade in VGA palette ; the display driver then translated that modification to the actual pixel in the CRT monitor.
- Operating systems are now responsible for shielding hardware from programmers due to their evolution. Modern operating systems forbid code from altering memory locations outside of the permitted addresses provided to the process by the OS.
- For instance, if we want to draw and paint pixels on the screen or communicate with any other hardware component when running Windows, macOS, Linux , or *BSD , we must ask the OS for the appropriate permissions. Operating system API must be used to carry out even the most basic operations, such as opening a window on the OS desktop.
- As a result, operating system-specific operations include starting a process , o pening a window , generating graphics on the screen, painting pixels inside that window, and even reading input events from the keyboard.
- SDL is a highly well-known library that aids with multi-platform hardware abstraction. SDL serves as a link between many CPU architectures ( Intel, ARM, Apple M1 , etc.) and various operating systems. The SDL library "translates" the code to make it compatible with these many platforms by abstracting the low-level hardware access.
- Here is a short code that opens an operating system window using SDL .
- For the sake of simplicity, the code below will work on Windows, macOS, Linux, BSD, and even the Raspberry Pi.
2. Hardware Access
#include <SDL2/SDL.h>
void OpenNewWindow() {
SDL_Init(SDL_INIT_VIDEO);
SDL_Window* window = SDL_CreateWindow("My Window", 0, 0, 800, 600, 0);
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, 0);
}
- SDL represents just one of the various libraries available for interfacing with hardware on different platforms. It is particularly favored for 2D gaming applications and for porting existing code to various platforms and gaming consoles. Another popular cross-platform library is GLFW, commonly employed in 3D gaming and game engine development. GLFW seamlessly interacts with advanced 3D APIs such as OpenGL and Vulkan.
- After opening the OS window, we must establish a regulated game loop .
- We prefer that our games run at a frame rate of 60 . To put things in perspective, films filmed on film run at a 24 FPS rate ( 24 pictures fly through your eyes every second), whereas the framerate may vary depending on the game.
3. Game Loop
During gameplay, a game loop runs continually, and the game engine needs to perform some crucial operations on each loop pass. A standard game loop has to:
- Process input events without blocking.
- Revise the properties of all game objects for the current frame.
- Display the game's objects and other crucial data on the screen.
while (isRunning) {
Input();
Update();
Render();
}
There is a direct correlation between a game loop and real-time. This ensures that, irrespective of the processing power of the system, the game characters will maintain a consistent pace of movement.
It is a fascinating subject to manage the frame rate and adjust it to a designated number of frames per second. We typically monitor the duration between frames and perform fundamental computations to guarantee that our games run seamlessly at a minimum frame rate of 30 FPS.
4. Input
- A game that does not read some user input event . These can originate from a gamepad, a keyboard, a mouse , or a VR headset . Therefore, we must process and handle various input events within our game loop.
- We must use the operating system API to request access to hardware events to handle user input. We can manage user input using a multi-platform hardware abstraction library ( SDL, GLFW, SFML , etc.).
- If we use SDL , we can poll events and then use a few lines of code to handle them appropriately.
void Input() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event. type) {
case SDL_KEYDOWN:
if (event.key.keysym.sym == SDLK_SPACE) {
ShootMissile();
}
break;
}
}
}
We can focus less on OS-specific details by utilizing a cross-platform library such as SDL to manage input in our C++ code. This approach ensures consistency across different platforms we target.
After establishing a functional game loop and implementing a user input handling mechanism, we proceed to structure our game entities in the computer's memory.
5. Representing Game Objects in Memory
- Data structures must be put up to store and access game objects when creating a gaming engine.
- When building a gaming engine , programmers employ a variety of strategies. While some engines may organize their objects using a straightforward object-oriented strategy based on classes and inheritance , others may group their objects into entities and components.
- If we're using C++, utilizing the STL (standard template library) is one alternative option, which includes a variety of data structures like vectors, lists, queues, stacks, maps , and sets . It can be a good chance to practice working with templates and observe their use in a real project since the C++ STL primarily relies on templates.
- As we learn more about game engine architecture, we'll discover that one of the most common design patterns games use is built on entities and components. The items in our game scene are organized into entities (also known as "game objects" in Unity and "actors" in Unreal) and components (the data we may add to or attach to our entities) using an entity-component design.
- Consider a straightforward gaming setting to understand how entities and components interact. The entities will consist of our primary character, the adversaries, the ground, and the projectiles. At the same time, the components will be the significant pieces of information that we "attach" to our entities, such as position, velocity, rigid body collider , etc.
Organizing game elements as entities and components is a common design approach for game engines. We can select to attach several components to our entities, as follows:
- Position component: Maintains a record of our entity's x-y position coordinates in the outside world (or x-y-z in three dimensions).
- Velocity component: It is used to measure how quickly the object travels along the x-y axis (or the x-y-z axis in 3D).
- Sprite component: The PNG image that we should render for a particular object is often stored in the sprite component .
- Animation component: The animation component is used to track the entity's animation speed and the evolution of the animation frames.
- Collider component: It determines the geometry of an entity that will collide (such as a bounding box, bounding circle, mesh collider, etc.) and is typically tied to the mechanics of a rigid body.
- Component for health: It saves an entity's current health value. Typically, it is only a number or a percentage value (like a health bar).
- Script component: Occasionally, we may attach a script component to an object. This script component may be an external script file (such as one written in Lua, Python, ) that our engines must parse and execute in the background.
- It is a highly common method for providing crucial game information and objects. We "plug" various components into our entities to create entities.
- Many books and articles discuss how to implement an entity-component design and the data structures that should be used. Regularly developers talk about things like Data-Oriented Design, Entity-Component-System (ECS), data locality , and many other concepts that have everything to do with how game data is stored in memory and how access this data efficiently. The data structures used and how access them directly impact game's performance.
- It can be challenging to represent and access game objects in memory. Either personally code an entity-component implementation or use an already-existing third-party ECS module .
- We can start constructing entities and attaching components to them by including several well-liked, ready-to-use ECS libraries in our C++ project without worrying about how they are implemented inside. EnTT and Flecs are two examples of C++ ECS libraries.
- Even if the implementation is imperfect, building an ECS system from Scratch will make you evaluate the performance of the underlying data structures.
- After the custom ad-hoc ECS solution is complete, we advise using any well-known third-party ECS libraries ( EnTT, Flecs , etc.). These are expert libraries that have been created and put through testing by the market for many years. They are undoubtedly far superior to anything we might create on our own.
- In conclusion, building a professional ECS from Scratch is challenging. It is acceptable as a learning exercise, but once we've finished the brief learning project, choose a reputable third-party ECS library and incorporate it into the code of the game engine.
- The complexity of our game engine is gradually increasing. Now that we've covered methods for storing and accessing game objects in memory, we need to discuss how we render things on the screen.
- The first stage is to think about the types of games does engine will be used to make. Building an engine to make 2D games? If so, engine should consider drawing sprites, managing layers, rendering textures , and using graphics card acceleration. The good news is that 2D games are typically easier to understand than 3D games, and 2D maths is much simpler than 3D maths.
- SDL can assist with cross-platform rendering to create a 2D engine . SDL can decode and display PNG pictures, draw sprites , and render textures inside our game window. It can also encapsulate accelerated GPU hardware.
- If our objective is to create a 3D engine , we'll need to specify how to deliver additional 3D data to the GPU, such as vertices, textures , and shaders . The most well-liked choices for a software abstraction of the graphics hardware are OpenGL, Direct3D, Vulkan , and Metal . Choosing which API to utilize may be influenced by our target platform. For instance, Metal will only be compatible with Apple devices , but Direct3D would power Microsoft software .
- A graphics pipeline is used to process 3D data in 3D applications. The way your engine must provide graphical data to the GPU (such as vertices, texture coordinates, normals , etc.) is determined by this pipeline.
- We must develop programmable shaders by the graphics API and pipeline to adapt and alter the vertices and pixels of our 3D scene .
- Programmable shaders determine the GPU's processing and display of 3D objects . Different scripts can be used to adjust reflection, smoothness, color, transparency , etc., for each vertex and each pixel (fragment).
- Speaking of vertices and 3D objects, it is a good idea to trust a library with the task of translating different mesh formats. The majority of third-party 3D engines have to be familiar with the several common 3D model formats.
- Some libraries have been well-tested and supported to handle OBJ loading with C++. Many game engines employ the excellent choices TinyOBJLoader and AssImp .
- We want the entities that we add to our engine to move, spin , and bounce around our scene. The physics simulation is a part of a gaming engine . It can be manually constructed or imported from a physics engine.
- It's also important to consider the physics we want to model here. Although 2D physics is typically easier to understand than 3D , both 2D and 3D engines share many fundamental aspects.
- Several excellent solutions are available if we want to add a physics library to our project.
- We suggest checking out Box2D and Chipmunk2D for 2D physics . Libraries like PhysX and Bullet are strong options for a reliable and professional 3D physics simulation. If physics stability and development speed are essential for our project, using a third-party physics engine is always a good choice.
- Every programmer should learn how to create a basic physics engine at some point in their careers. Again, we don't have to create an ideal physics simulation; instead, we pay attention to how well things can accelerate and how different forces may be applied to the objects in our game.
- Once mobility is complete, consider putting some basic collision detection and resolution .
- We can utilize some excellent books and online resources to learn more about physics engines . We can look at the Box2D source code and Erin Catto's slides for 2D rigid-body physics. However, 2D Game Mechanics from Scratch is a decent place to start if we're seeking a thorough introduction to game mechanics.
- A fantastic resource is David Eberly's book "Game Physics" if you want to learn about 3D physics and how to create a reliable simulation .
- Modern game engines like Unity or Unreal conjure images of intricate user interfaces with many panels, sliders, drag-and-drop choices , and other attractive UI components that let players personalize the game world. The UI enables the developer to quickly adjust game variables, add and remove entities , and change component settings on the fly.
- Creating a UI framework from Scratch is one of the most frustrating jobs a novice programmer can attempt to add to a game engine. We'll need to design buttons, panels, dialogue boxes, sliders , and radio buttons, manage colors , handle the UI's events correctly, and maintain its state at all times. If we add UI tools in our engine, the application will become more complex, and the source code will become quite noisy.
- We suggest using an existing third-party UI library if the goal is to develop UI tools for the engine. As we can see from a short Google search , the most well-liked choices are Dear ImGui, Qt , and Nuklear .
- We can quickly set up user interfaces for engine tooling with Dear ImGui . A design pattern employed by the ImGui project is known as "immediate mode UI". It is popular with game engines because it effectively interfaces with 3D applications by utilizing GPU-accelerated rendering.
- In conclusion, we recommend utilizing Dear ImGui if you want to add UI capabilities to your game engine.
- As our game engine develops, one frequent choice is to make level customization possible using a straightforward scripting language .
- The concept is straightforward . We integrate a scripting language into our native C++ application so non-programmers can script entity behavior, AI logic, animation , and other crucial game elements.
- Lua, Wren, C#, Python , and JavaScript are some of the widely used scripting languages for video games . All of these languages function at a level that is significantly higher than our native C++ code . People using the scripting language to program game behavior should be concerned with memory management or other low-level aspects of the core engine's functions. All they have to do is script the levels; our engine will interpret the scripts and handle the difficult work in the background.
- Small, quick , and incredibly simple to combine with native C and C++ programs is Lua . The Sol package provides numerous auxiliary methods to enhance the default Lua C-API , making it easier to start with Lua.
- We can discuss more complicated topics related to our game engine if we enable scripting. Using external scripts, we can easily control AI logic , alter animation frames and movement , and define additional game behavior that does not need to be contained within our native C++ code.
- We should also consider integrating support for audio in a game engine.
- It should come as no surprise that we must access audio devices through the OS once more to poke audio values and generate sound. It uses a multi-platform library that abstracts audio hardware access since we typically don't want to create OS-specific functionality .
- Extensions from cross-platform libraries like SDL can assist our engine in handling audio elements like music and sound effects .
- Only take on audio until the engine's other components function properly. It can be simple to generate sound files , but things can get complicated when we synchronize the audio with animations, events , and other game components.
- Audio can be challenging due to multi-threading management if we are truly doing things manually. Although possible, we recommend leaving this task to a specialized library if anyone wants to develop a straightforward game engine.
- SDL_Mixer, SoLoud , and FMOD are three excellent audio libraries and technologies we might consider integrating with our game engine.
- Tiny Combat Arena is one game that uses the FMOD library for audio effects like compression and Doppler . The sounds of the other passing jets' 3D effects and afterburners are audible.
- We'll include AI as the last component of our conversation. We could create AI by using scripting , allowing level designers to script the AI logic . Another choice would be to integrate a genuine AI system within the native core code of our game engine.
- AI is applied to gaming elements to provide responsive, adaptable , or intelligent-like behavior. Most AI logic is introduced to NPCs and adversaries to emulate human intelligence.
- A common application of AI in video games is enemies . Game engines can abstract path-finding algorithms or interesting human-like behavior when enemies pursue things on a map.
- AI for Games by Ian Millington is a thorough book about the theory and use of artificial intelligence for video games.
6. Rendering
7. Physics
8. UI (User Interface)
9. Scripting
10. Audio
11. Artificial Intelligence
Don't Try to do Everything at Once
We have recently discussed several essential concepts that we aim to integrate into a basic C++ gaming engine. However, it is important to consider the following points before we proceed with combining all these elements:
- Developing a gaming engine can pose challenges as developers often fail to define clear boundaries, and there is no apparent "finish line." To clarify, programmers may initiate a gaming engine project, start rendering elements, incorporate entities and components, and then encounter difficulties. Without limitations, it is easy to continuously add functionalities and lose focus on the main objective. The risk of the game engine never being utilized becomes significantly high under such circumstances.
- Moreover, apart from the absence of constraints, managing the rapid expansion of code can be overwhelming as the project progresses at a fast pace. A game engine project can quickly evolve into a complex system. Within a few weeks, the C++ project may accumulate numerous dependencies, require a sophisticated build structure, and experience reduced readability as new features are integrated into the engine.
- Take pleasure in your minor accomplishments if you're building your game engine as a learning exercise.
- At the outset of the project, most of them are quite enthusiastic, but anxiety sets in as time passes . It is simple to become overwhelmed and lose steam when building a game engine from Scratch , especially when utilizing a difficult language like C++.
- Own the fundamentals by concentrating on them. No matter how modest or straightforward a thought is, own it.