FIT CTU

Adam Vesecký

vesecky.adam@gmail.com

Lecture 2

Engines

Architecture of Computer Games

Adam Vesecký

Game Engines Overview

Unreal Engine

Unreal Engine 1

  • Released in 1998 by Epic Games as FPS game engine
  • Unreal - the first game powered by this engine
  • Introduced the concept of assets, HW & SW rendering,...

Unreal Engine 4

  • Film-Quality postprocessing
  • VFX & Particle Systems
  • Beta Raytracing

Unreal Engine 5

  • available in early 2021
  • Nanite virtualized geometry
  • Lumen illumination engine

Unity

  • all-Purpose (not only) game engine
  • first announced for MacOS in 2005
  • over 27 platforms supported
  • most common choice for indie developers
  • supports 2D and 3D games

Unity 2020.1.6 - current version

  • Unity 2020.2 Beta was released also
  • DirectX, OpenGL, Vulkan
  • new shader graph
  • C# 8 support

Ori

Besiege

Cuphead

Godot

  • 2D and 3D game engine
  • GDScript scripting language
  • component architecture
  • MIT license, completely free
  • no famous games so far

Which engine do I need?

  • some engines are easy to learn but difficult to master
  • Unity is good for 2D and 3D indie games, Unreal for 3D AAA games, Godot for smaller, modest games

Still, they all have similar core components

  • every game engine needs a game loop
  • almost every game needs to manage assets
  • every game needs a rendering engine
  • game objects are structured in a scene graph
  • we couldn't do much without a physics engine
  • interactive games require an advanced audio engine
Rocket engine works everywhere!

Engine Architecture

Engine Architecture

Hardware

  • Playstation, Xbox, Nintendo Switch, iPhone, Samsung Galaxy, PC

Drivers

  • NVidia, Realtek audio, Gamepad driver

OS

  • Windows, Linux, MacOS, Android, iOS

3rd Party SDKs

  • DirectX, OpenGL, Vulkan, Boost, Havok, STL, Hardware-specific dev kit

Core Systems

  • Threading lib, File system, Network layer, Movie player, Math library, Parsers, Memory Allocator

Modules

  • Scripting system, Messaging system, DSP, match-making management, skeletal animation, inverse kinematics, LERP, Rigid bodies, Spatial subdivision, LOD system, Particle system, Shading system

Game

  • HUD, Terrain renderer, Vehicle system, Puzzle system, Dialogue system, State machines, Camera

Game Engine Primary Modules

  • Game Loop - heartbeat of all games
  • Scene Manager - manages objects and structures them in a scene graph
  • Resource Manager - manages assets, controls a cache
  • Input Manager - handles inputs (keyboard, mouse, touch, joystick, gamepad,...)
  • Memory Manager - memory allocator and deallocator
  • Rigidbody Engine - event-based collision detection
  • Physics Engine - handles behavior of objects based on forces and impulses
  • Rendering Engine - renders the game, takes care of the rendering pipeline
  • Animation Engine - handles animations
  • Scripting Engine - a bridge between the engine and interpreted languages (JS, C#,...)
  • Audio Engine - plays music, clips, sounds, calculates 3D sound
  • AI Engine - abstract engine for AI (state machines, behavioral trees,...)
  • Networking Engine - handles multipeer communication
  • Other modules - GUI framework, Level Editor, Camera, Event System, LOD system,...

Game Loop

Application and Loop

Initialization process

  1. Initialize engine object
  2. Register all systems
  3. Initialize rendering window
  4. Run game loop
  5. Terminate

Game Loop process

  1. Process inputs
  2. Update game state
  3. Render game objects
  4. Repeat

Example: Atomic Game Engine init

Game Loop

  • simple, yet the most important part of the game engine
  • each turn advances the state of the game
  • the loop is usually coordinated with the event loop of the platform/virtual machine
  • optimal time step for rendering: 60 FPS = 16.6 ms per frame
  • audio and input processing are usually separated as they require more frequent updates
In general, a program spends 90% of its time in 10% of its code. The game loop will be firmly in those 10%.

Simple Game Loop

Game loop with separated rendering

Cooperative game loop

  • implemented by small, relatively independent jobs
  • firstly used in Ultima VII (1994)

Update method

Fixed time step

  • each update advances game time by a certain amount of time
  • precise and stable
  • the game may slow down

Variable time step

  • each update advances game time based on how much real time passed since the last frame
  • natural
  • non-deterministic and unstable

Adaptive time step

  • switches between variable and fixed time step
  • based either on thresholds or a more sophisticated approach
  • deals with long breaks better than the other two
Some old games were tied with the system clock, hence their CPU-sensitivity.

Example: Atomic Game Engine Loop 1/3

1 int Application::Run() {
2 Setup();
3 if (!engine_->Initialize(engineParameters_)) {
4 return ErrorExit();
5 }
6
7 Start();
8
9 #if !defined(IOS) && !defined(__EMSCRIPTEN__)
10 while (!engine_->IsExiting())
11 engine_->RunFrame();
12 Stop();
13 #else
14 #if defined(IOS)
15 SDL_iPhoneSetAnimationCallback(GetWindow(), 1, &RunFrame, engine_);
16 #elif defined(__EMSCRIPTEN__)
17 emscripten_set_main_loop_arg(RunFrame, engine_, 0, 1);
18 #endif
19 #endif
20 return exitCode_;
21 }

Example: Atomic Game Engine Loop 2/3

1 void Engine::RunFrame() {
2 Time* time = GetSubsystem<Time>();
3 Input* input = GetSubsystem<Input>();
4 Audio* audio = GetSubsystem<Audio>();
5
6 time->BeginFrame(timeStep_);
7 // ... process input and audio
8 Update();
9
10 fpsTimeSinceUpdate_ += timeStep_;
11 ++fpsFramesSinceUpdate_;
12 if (fpsTimeSinceUpdate_ > ENGINE_FPS_UPDATE_INTERVAL) {
13 fps_ = (int)(fpsFramesSinceUpdate_ / fpsTimeSinceUpdate_);
14 fpsFramesSinceUpdate_ = 0;
15 fpsTimeSinceUpdate_ = 0;
16 }
17
18 Render();
19 ApplyFrameLimit();
20 time->EndFrame();
21 }

Example: Atomic Game Engine Loop 3/3

1 void Engine::Update() {
2 VariantMap& eventData = GetEventDataMap();
3 eventData[P_TIMESTEP] = timeStep_;
4 SendEvent(E_UPDATE, eventData);
5
6 // Logic post-update event
7 SendEvent(E_POSTUPDATE, eventData);
8 // Rendering update event
9 SendEvent(E_RENDERUPDATE, eventData);
10 // Post-render update event
11 SendEvent(E_POSTRENDERUPDATE, eventData);
12 }
13
14 void Engine::Render() {
15 // If device is lost, BeginFrame will fail and we skip rendering
16 Graphics* graphics = GetSubsystem<Graphics>();
17 if (!graphics->BeginFrame()) return;
18
19 GetSubsystem<Renderer>()->Render();
20 GetSubsystem<UI>()->Render();
21 graphics->EndFrame();
22 }

Example: Godot Engine Game Loop

1 bool Main::iteration() {
2 iterating++;
3
4 uint64_t ticks = OS::get_singleton()->get_ticks_usec();
5 Engine::get_singleton()->_frame_ticks = ticks;
6 // update elapsed ticks
7 // ...
8 MainFrameTime advance = main_timer_sync.advance(frame_slice, physics_fps);
9
10 for (int iters = 0; iters < advance.physics_steps; ++iters) {
11 // ... update PhysicsServer3D
12 }
13
14 if (DisplayServer::get_singleton()->can_any_window_draw()) {
15 // ... draw frame
16 }
17
18 for (int i = 0; i < ScriptServer::get_language_count(); i++) {
19 // ... update scene for each scripting language
20 ScriptServer::get_language(i)->frame();
21 }
22
23 AudioServer::get_singleton()->update();
24 frames++;
25 return exit || auto_quit;
26 }

Example: Unity Game Loop

Update Inconsistencies

  • game objects are consistent before and after every update
  • they may get to an inconsistent state during update - one-frame-off lag
  • possible solutions: bucket update, script execution order (Unity)

Object A reads previous state of Object B and Object B reads previous state of Object C

Scene Graph

Scene Graph

Scene Graph

  • essential structure of every interactive application
  • a way of ordering the data into a hierarchy
  • represented as N-Tree or a regular graph
  • implemented as an array, oct-tree, quad-tree, bounding volume hierarchy,...
  • parent nodes affect child nodes (translation, rotation, scale,...)
  • leaves represent atomic units (shapes, vertices, imported meshes)

Scene Manager

  • manages objects in the scene
  • similar to HTML DOM along with Event Manager
  • responsibility: sending events, searching for objects, applying transformations,...
  • Unity Engine - game objects form a hierarchy
  • Unreal Engine - components form a hierarchy
  • Godot Engine - everyting forms a hierarchy

Example: Scene Hierarchy

Example: Complex Object Hierarchy

  • T - Translation
  • R - Rotation
  • I - Identity

Example: Unity Scene Graph

Example: Godot Nodes

Input

Input Manager

Detects input events from devices

Atomic events

  • KEY_DOWN
  • KEY_UP
  • MOUSE_WHEEL
  • JOYPAD_A

Compound events

  • FLING
  • PINCH_TO_ZOOM
  • DOUBLE_TAP

Special events

  • cheat codes
  • fighting combos

Input Devices

Receiving the state of the device

  • polling - compare against previous state
  • callbacks - handled by upper SW layer

Devices

  • keyboard, touch sensor, camera, Oculus Touch,...
  • one-axis controller - single analog state
  • two-axis controller - mouse and joystick
  • three-axis controller - accelerometer

Dead zone

  • area of a control interface that has no input effect (analog joystick)

Normalization

  • analog axis are mapped to a Cartesian space, not a circular one
  • input must be normalized

Keyboards for various games

Normalized input

Example: Unity Input Manager

Special Events

Sequence detection

  • cheats: IDDQD, IDKFA
  • chords: combo moves in fighting games

Controller input remapping

  • button ID -> action ID mapping
  • can be implemented as a table

Context-sensitive inputs

  • handlers vary by a mode (walking, driving, flying)
  • implemented via a state machine, table or polymorphism

Example: Doom 2 Cheat Detection

1 // Returns 1 if the cheat was successful, 0 if failed.
2 int cht_CheckCheat(cheatseq_t* cht, char key ) {
3 int i;
4 int rc = 0;
5
6 if (firsttime) {
7 firsttime = 0;
8 for (i=0;i<256;i++) cheat_xlate_table[i] = SCRAMBLE(i);
9 }
10
11 // initialize if first time
12 if (!cht->p) cht->p = cht->sequence;
13 if (*cht->p == 0) *(cht->p++) = key;
14 else if (cheat_xlate_table[(unsigned char)key] == *cht->p) cht->p++;
15 else cht->p = cht->sequence;
16
17 if (*cht->p == 1) cht->p++;
18 else if (*cht->p == 0xff) { // end of sequence character
19 cht->p = cht->sequence;
20 rc = 1;
21 }
22 return rc;
23 }

Memory

Memory Manager

  • default managers are not suitable for games (i.e. malloc function)
  • game engines usually have their own allocators

Custom allocators

  • stack allocator
  • pool allocator
  • heap allocator
  • bucket allocator

Stack allocator

  • suitable for level-based games
  • implementations: single-ended, double-ended
  1. allocate a large contiguous block of memory
  2. maintain a pointer to the top of the stack
  3. everything above the pointer is considered as a free area
  4. deallocate in an order opposite to that which blocks were allocated

Pool allocator

  • allocates lots of small blocks of memory, each of the same size
  • doesn't suffer from memory fragmentation
  • entities have to be of the same size

Other allocators

Bucket allocator

  • several pool allocators for objects of various sizes
  • great solution for demanding games
  • difficult to manage

Heap allocator

  • more or less the same as heaps in virtual machines
  • very flexible
  • requires garbage collector

Example: Atomic GE Allocation

1 void* AllocatorReserve(AllocatorBlock* allocator) {
2 if (!allocator->free_) {
3 // Free nodes have been exhausted. Allocate a new larger block
4 unsigned newCapacity = (allocator->capacity_ + 1) >> 1;
5 AllocatorReserveBlock(allocator, allocator->nodeSize_, newCapacity);
6 allocator->capacity_ += newCapacity;
7 }
8
9 // We should have new free node(s) chained
10 AllocatorNode* freeNode = allocator->free_;
11 void* ptr = (reinterpret_cast<unsigned char*>(freeNode)) + sizeof(AllocatorNode);
12 allocator->free_ = freeNode->next_;
13 freeNode->next_ = 0;
14 return ptr;
15 }
16 ============================
17 // create node from void* and call the constructor
18 Node* newNode = static_cast(AllocatorReserve(allocator_));
19 new(newNode) Node();
20 // ... do some stuff
21 // delete node
22 (newNode)->~Node();
23 AllocatorFree(allocator_, newNode);

Example: Godot Engine Allocation

1 PoolAllocator::ID PoolAllocator::alloc(int p_size) {
2 int size_to_alloc = aligned(p_size);
3
4 EntryIndicesPos new_entry_indices_pos;
5
6 if (!find_hole(&new_entry_indices_pos, size_to_alloc)) {
7 /* No hole could be found, try compacting mem */
8 compact();
9 /* Then search again */
10 if (!find_hole(&new_entry_indices_pos, size_to_alloc)) {
11 ERR_FAIL_V_MSG(POOL_ALLOCATOR_INVALID_ID, "Memory can't be compacted further.");
12 }
13 }
14 EntryArrayPos new_entry_array_pos;
15 bool found_free_entry = get_free_entry(&new_entry_array_pos);
16
17 if (!found_free_entry) {
18 ERR_FAIL_V_MSG(POOL_ALLOCATOR_INVALID_ID, "No free entry found in PoolAllocator.");
19 }
20 // ... move all entry indices up and allocate the entry
21 // ...
22 return retval;
23 }

Data loading

Level loading

  • used in Tomb Raider, Doom, Quake,...
  • requires a loading screen
  • only one game chunk is loaded at a time
  • appropriate for games with levels, separated scenes or star topology

Air locks

  • used in Half-Life, Inside, Portal (partially)
  • air lock is a small scene (room, hall)
  • when the player enters the area from which can't see the previous one, next scene is loaded

World streams

  • used in GTA, WoW, Arma, Spiderman
  • the world is divided into regions
  • the engine unloads chunks too far away and loads new chunks the player is heading to
  • uses LOD system - chunks are loaded at variable granularity

Example: Loading Screen

Raptor (1994)

Shadow of the Tomb Raider (2018)

Example: Air Lock

Duke Nukem (1991)

Portal (2007)

Example: World Streaming

World of Warcraft (2004)

Witcher 3 (2015)

Scripting Engine

Scripts

Scripting language

  • high-level language that can be interpreted by another program at runtime
  • it is more about an environment than the language itself (even C# can be considered as a scripting language)

Common characteristics

  • economy of expression
  • flexible dynamic typing
  • easy access to other programs/components

Scripts in games

  • allow rapid prototyping
  • can be reloaded at runtime
  • can separate gameplay and core components
  • can be exposed to the end users for modding

Example: Arma 2 SQS Script

1 // Creates boards and markers around mission Area
2 _xPos = position (_this select 0) select 0;
3 _yPos = position (_this select 0) select 1;
4
5 _howBigA = _this select 1;
6 _howBigB = _this select 2;
7 _tablesC = _this select 3;
8 _angle = _this select 4;
9 _i = 0;
10
11 while (_i < 360) do {
12 _x = (_howBighA * (sin _i));
13 _y = (_howBigB * (cos _i));
14 _x_rot = _xPos + _x*(cos _angle) - _y*(sin _angle);
15 _y_rot = _yPos + _x*(sin _angle) + _y*(cos _angle);
16 _k = createVehicle ["Danger", [_x_rot, _y_rot, 0], [], 0, "NONE"];
17 _m = createMarker [format ["Marker" + str _i], [_x_rot, _y_rot, 0]];
18 format ["Marker" + str _i] setMarkerType "Dot";
19 _k setDir _i;
20 format ["Marker" + str _i] setMarkerDir(_i - _angle);
21 _i = _i + 360/_tablesC;
22 };

Example: Hexen ACS Script

1 script 137 (int dir)
2 {
3 if(!dir)
4 {
5 Floor_LowerByValue(DoorTag, 16, 64)
6 Ceiling_RaiseByValue(DoorTag, 16, 64)
7 Delay(120);
8 Floor_RaiseByValue(DoorTag, 16, 64)
9 Ceiling_LowerByValue(DoorTag, 16, 64)
10 }
11 }

Example: World of Warcraft

1 local total, completed = GetNumCompletedAchievements();
2
3 if total > completed then
4 print("You have completed ", completed, " out of " ,total," achievements");
5 x= completed/total*100;
6 print("That is only ",x," percent");
7 end

Scripting architectures

Scripted callbacks

  • certain functions are customizable via scripts
  • a game object can respond to some relevant occurrence within the game world (in-game scripts)

Scripted components

  • new components/objects/properties can be constructed entirely in a script
  • firstly used in Dungeon Siege

Script-driven game

  • script is running the game and the core systems are written in the engine
  • used in Unreal, Unity, Godot, GameMaker,...

Script-driven engine

  • script drives the entire engine (PixiJS, ThreeJS, p5.js, BabylonJS)

Game Engine Scripting API

Game Engine Scripting API

  • the engine needs to communicate with the script - provided by bridging
    • JNI (Java and C++)
    • P/Invoke (.NET and C++)
    • Dukbind (Duktape JS and C++)
  • bridge is a performance bottleneck, especially for per-frame calls
  • more scripting languages = more bridges to maintain
  • Marshalling
    • transforming memory representation of an object between two domains (different programming languages)
  • Semantic gap
    • descriptive difference of an object in various representations (e.g. relational database vs object-oriented structure)

Example: Atomic Game Engine JS API

1 // Duktape JS mapping
2 static void jsb_class_define_FileSystem(JSVM* vm) {
3 duk_context* ctx = vm->GetJSContext();
4 js_class_get_constructor(ctx, "Atomic", "FileSystem");
5 js_class_get_prototype(ctx, "Atomic", "FileSystem");
6 duk_pop_2(ctx);
7 js_class_get_prototype(ctx, "Atomic", "FileSystem");
8 duk_push_c_function(ctx, jsb_class_FileSystem_SetCurrentDir, 1);
9 duk_put_prop_string(ctx, -2, "setCurrentDir");
10 duk_push_c_function(ctx, jsb_class_FileSystem_CreateDir, 1);
11 duk_put_prop_string(ctx, -2, "createDir");
12 ...
13 }
14
15 // CreateDir method
16 static int jsb_class_FileSystem_CreateDir(duk_context* ctx) {
17 String __arg0 = duk_to_string(ctx, 0);
18 duk_push_this(ctx);
19 FileSystem* native = js_to_class_instance<FileSystem>(ctx, -1, 0);
20 bool retValue = native->CreateDir(__arg0);
21 duk_push_boolean(ctx, retValue ? 1 : 0);
22 return 1;
23 }

Example: Atomic Game Engine C# API

  • mapping from C++ to C# is easy
1 ATOMIC_EXPORT_API bool csb_Atomic_FileSystem_SetCurrentDir_4667(FileSystem* self, const char* pathName)
2 {
3 return self->SetCurrentDir(pathName ? String(pathName) : String::EMPTY);
4 }
5
6
7 ATOMIC_EXPORT_API bool csb_Atomic_FileSystem_CreateDir_4668(FileSystem* self, const char* pathName)
8 {
9 return self->CreateDir(pathName ? String(pathName) : String::EMPTY);
10 }
11
12
13 ATOMIC_EXPORT_API void csb_Atomic_FileSystem_SetExecuteConsoleCommands_4669(FileSystem* self, bool enable)
14 {
15 self->SetExecuteConsoleCommands(enable);
16 }

Example: Openframeworks LUA Api

1 // ofvec2f 2D vectors mapping via luabridge library
2 luabridge::getGlobalNamespace(L)
3 .beginClass<ofVec2f>("ofVec2f")
4 .addConstructor<void(*)(float, float)>()
5 .addFunction(LUA_OPERATOR_PLUS,
6 static_cast<ofVec2f(ofVec2f::*)(const ofVec2f &)const>(&ofVec2f::operator+))
7 .addFunction(LUA_OPERATOR_MULT,
8 static_cast<ofVec2f(ofVec2f::*)(const ofVec2f &)const>(&ofVec2f::operator*))
9 .addFunction(LUA_OPERATOR_EQ,
10 static_cast<bool(ofVec2f::*)(const ofVec2f &)const>(&ofVec2f::operator==))
11 .addData("x", &ofVec2f::x)
12 .addData("y", &ofVec2f::y)
13 .addFunction("length", &ofVec2f::length)
14 .addFunction("dot", &ofVec2f::dot)
15 .endClass();

Lecture Summary

  • I know primary modules of game engines
  • I know how game loop works
  • I know the options of game update and inconsistencies that may occur
  • I know what scene graph is and what elements it contains
  • I know categories of input events and input device
  • I know what dead-zone of input is
  • I know the purpose of a memory manager
  • I know the purpose of pool allocator
  • I know the options of data loading in games
  • I know the types of scripting architectures
  • I know what marshalling is

Goodbye Quote

In dangerous testing environments, the Enrichment Center promises to always provide useful advice. For instance, the floor here will kill you. Try to avoid it.GLaDOS, Portal