The Unreal Engine Game Framework: From int main() to BeginPlay
Summary
TLDRThe video script delves into the intricacies of the Unreal Engine's game framework, illustrating the process from the game's initialization to the start of gameplay. It explains the concept of the game loop and how Unreal Engine abstracts it through classes like GameMode and PlayerController. The script outlines the engine's initialization stages, including module loading, engine object creation, and map loading, leading to the game's start with the BeginPlay event. It emphasizes the engine's flexibility for both single-player and multiplayer game development, highlighting the importance of understanding the framework for clean and efficient game design. The summary serves as a guide for developers to leverage Unreal Engine's powerful tools and systems for creating engaging game experiences.
Takeaways
- 🔄 **Game Loop Concept**: The fundamental concept in game programming is the game loop, which involves initialization, processing input, updating game state, and rendering to screen.
- 🚀 **Unreal Engine Workflow**: In Unreal Engine, you don't deal directly with the game loop. Instead, you work with subclasses like GameMode and functions such as InitGame, BeginPlay, or Tick.
- 🛠️ **Extensibility and Flexibility**: Unreal Engine offers power and flexibility, being open-source and designed to be extended through various means, including subclassing and overriding functions.
- 🧩 **GameFramework Overview**: Understanding classes like GameMode, GameState, PlayerController, Pawn, and PlayerState is crucial for leveraging the engine's full capabilities.
- 📚 **Source Code Exploration**: Gaining familiarity with the engine involves looking at its source code to understand how the game boots up and the sequence of operations.
- 🔧 **Engine Initialization Process**: The engine runs thousands of lines of code to set up global state and initialize systems before reaching higher-level abstractions.
- 🌐 **Module System**: The engine is divided into source modules, which are loaded in phases to manage dependencies and ensure only necessary parts are loaded for a given configuration.
- 🏗️ **Game Instance and World Creation**: The engine creates a GameInstance, GameViewportClient, and LocalPlayer before loading the game map and initializing the world.
- 👾 **Actor Initialization and Registration**: Actors and their components are registered, initialized, and brought up for play during the LoadMap process.
- 👥 **Player Login and Pawn Possession**: Player login is handled by the GameMode, which spawns PlayerControllers and PlayerStates, leading to Pawn possession and player setup in the game world.
- 🔄 **Game Loop and Map Loading**: The game loop manages the entire process from initialization to shutting down, with map loading being a significant part of the game's startup sequence.
Q & A
What is the fundamental concept in game programming that the script begins with?
-The fundamental concept in game programming that the script begins with is the game loop, which involves initialization, processing input, updating the game world state, and rendering the results to the screen.
How does Unreal Engine abstract the game loop for developers?
-Unreal Engine abstracts the game loop by allowing developers to start with defining a GameMode subclass and overriding functions like InitGame, BeginPlay, or Tick, rather than dealing with the main function and the game loop directly.
What are some of the key classes in Unreal Engine's GameFramework?
-Some of the key classes in Unreal Engine's GameFramework include GameMode, GameState, PlayerController, Pawn, and PlayerState.
How does the engine handle the transition from the entry point to running game code?
-The engine handles the transition from the entry point to running game code by initializing various systems and setting up global state through thousands of lines of code, which, while complex, are necessary for the engine to function correctly.
What is the role of FEngineLoop in the Unreal Engine?
-FEngineLoop is a class that implements the main loop for the Unreal Engine. It manages the PreInit stage, full initialization of the engine, and then ticks every frame until the program is ready to exit.
How does the module system in Unreal Engine help with managing dependencies and configurations?
-The module system in Unreal Engine helps manage dependencies and configurations by splitting the engine into different source modules, allowing for essential systems to be initialized first and ensuring that only the necessary modules for a given configuration are loaded.
What is the purpose of a CDO (Class Default Object) in Unreal Engine?
-A CDO (Class Default Object) in Unreal Engine serves as a record of a class in its default state and acts as a prototype for further inheritance. It is used to allocate a default instance of a class and run its constructor, passing in the CDO of the parent class as a template.
What is the significance of the UEngine class in Unreal Engine's initialization process?
-The UEngine class is significant in Unreal Engine's initialization process as it is responsible for creating an instance of the GameEngine class, initializing it, and starting the game. It also contains functions for loading maps and browsing to URLs.
How does the LoadMap function contribute to the game's initialization?
-The LoadMap function contributes to the game's initialization by finding and loading the map package, initializing the World, registering components, initializing GameMode and GameSession, and finally calling BeginPlay on all actors, completing the game's startup sequence.
What is the difference between a PlayerController and a Pawn in Unreal Engine?
-A PlayerController in Unreal Engine represents the player within the game world and is responsible for controlling the player's interactions. A Pawn, on the other hand, is a specialized type of actor that can be possessed by a Controller and represents the in-world entity that the player or AI controls.
Why is it important to understand the lifetime of different game framework actors in Unreal Engine?
-Understanding the lifetime of different game framework actors is important because it determines when and how these actors are created, used, and destroyed in the game. This knowledge helps developers manage resources effectively and ensures that game state is maintained correctly across different game phases.
How can developers extend functionality in Unreal Engine beyond class inheritance?
-Developers can extend functionality in Unreal Engine beyond class inheritance by binding callback functions to delegates that represent specific engine events, or by using the subsystem feature which allows for the automatic creation of instances of custom classes tied to the lifetime of corresponding objects.
Outlines
🎮 Game Loop and Unreal Engine's Extensibility
The first paragraph introduces the concept of the game loop, a fundamental aspect in game programming where the game initializes, runs a loop for gameplay, and cleans up after shutdown. It contrasts the direct handling of the game loop with the approach in Unreal Engine, where developers work with a GameMode subclass and override functions like InitGame. The paragraph also touches on the engine's open-source nature and extensibility, the GameFramework's key classes, and the importance of understanding the engine's source code for deeper familiarity.
📚 Engine Initialization and Module Loading
The second paragraph delves into the engine's initialization process, explaining the PreInit stage where modules are loaded based on their LoadingPhase. It details how the engine loop transitions from PreInit to the Init function, which involves creating a UEngine instance and preparing the game for map loading. The paragraph also discusses the UEngine class's responsibilities, including managing the game's map and handling URL browsing for server connections or local map loads.
🌐 World Creation and Game Framework Setup
The third paragraph describes the process of creating a UWorld and setting up the game framework. It explains how the engine loads a map package, initializes the world, and sets up systems like physics and AI. The paragraph outlines the steps for initializing actors for play, including registering components, spawning gameplay actors like GameMode, and setting up the player's environment. It also discusses the lifetime of different game objects and the transition from the process lifetime to the game map lifetime.
👤 Player Login and Pawn Possession
The fourth paragraph focuses on the player login process and the creation of a PlayerController. It details how the GameMode handles login requests, the spawning of a PlayerController and PlayerState, and the networking initialization. The paragraph also explains the concept of Pawn possession, where a PlayerController takes control of a Pawn actor, and how the game mode can customize player startup, including spawning and restarting players.
🔄 Game Loop Continuation and Engine Ticking
The fifth paragraph continues the discussion on the game loop, emphasizing the initialization stage and the transition into the game's running state. It outlines the steps from routing the BeginPlay event to the game being fully up and running. The paragraph also provides a review of the game framework components, such as GameModeBase and GameStateBase, and the utility of the Character class for movement and multiplayer games.
🤔 Data Structure and Engine Extension Methods
The sixth and final paragraph emphasizes the importance of critical thinking in data structure and object interaction for game development. It highlights alternative methods for extending engine functionality, such as binding callback functions to delegates or using the subsystem feature for modular functionality. The paragraph concludes with an encouragement to understand the engine's design decisions and a call for support on Patreon for continued content creation.
Mindmap
Keywords
💡Game Loop
💡GameMode Subclass
💡Actor and Component Classes
💡Game Framework
💡Source Modules
💡UObject Classes
💡GameInstance
💡LoadMap Function
💡PlayerController
💡Pawn
💡GameSession
Highlights
The game loop is a fundamental concept in game programming, consisting of an initialization phase, a main loop for processing input, updating game state, and rendering, followed by cleanup when the game shuts down.
Unreal Engine abstracts the game loop, allowing developers to focus on defining game modes, actors, and components without directly handling the loop.
The engine is extensible and open-source, offering power and flexibility to programmers through its game framework, which includes classes like GameMode, GameState, PlayerController, Pawn, and PlayerState.
Unreal Engine's source code can be examined to understand the game's boot-up process, which involves a complex initialization sequence and management of different systems and threads.
The FEngineLoop class implements the main loop for the engine, with stages including PreInit, full initialization, and per-frame ticking until exit.
During PreInit, the engine loads essential modules and sets up global state, with the process involving a lot of behind-the-scenes complexity.
The engine's module system ensures manageable dependencies and selective loading of modules based on platform and configuration.
The UEngine class, part of the Engine module, is responsible for high-level engine functionalities like loading maps and managing the game instance.
The GameInstance, GameViewportClient, and LocalPlayer are core objects created during engine initialization, representing the game's project-specific functionality, user interface, and the player, respectively.
The LoadMap function is central to game startup, involving loading the map package, initializing the world, and setting up game mode and session actors.
The lifetime of game objects is divided into before and after the LoadMap call, with different objects persisting across map transitions.
GameMode, GameState, and PlayerController are server-authoritative and are tied to the game's state and player experiences.
Player login and joining processes are managed by the GameMode, which handles login requests, player spawning, and initialization.
The Pawn class represents a special type of actor that can be controlled by a PlayerController, with the ability to possess and drive in-game characters.
Unreal Engine supports networked multiplayer games out of the box, with built-in functionalities for online integration, login requests, and network replication.
The Game Framework is opt-in for developers, allowing for a clean design when used as intended, and can be extended through inheritance or by binding to engine delegates.
Unreal Engine 4.22 introduces a subsystem feature for adding modular functionality, allowing for cleaner and more maintainable code when extending engine capabilities.
The Character class, a specialized type of Pawn, includes features like movement replication, animation root motion, and integrated navigation and pathfinding, providing a solid foundation for player and AI movement.
Transcripts
One of the most fundamental concepts in game programming - and one of the
simplest - is the idea of the game loop.
When your program runs, you do some initialization
to set things up, and then you run a loop for as long the player wants to keep playing:
each frame, you process input, you update the state of the game world,
and you render the results to the screen.
When the player shuts down the game, you do some cleanup, and you're done.
But if you're writing game code in Unreal Engine, you're not dealing with a game loop directly.
You don't start at the main function, you start by defining a GameMode subclass and
overriding a function like InitGame. Or you're writing one-off Actor and Component classes,
and you override their BeginPlay or Tick functions to add your own logic.
And that's really all you have to do at a minimum: the Engine takes care of
everything else for you, which is exactly what you want when you're starting out.
But Unreal also offers you a lot of power and flexibility as a programmer:
the Engine is open-source, of course, but it's also designed to be extensible
in a number of different ways. And even if you're a beginner, before long you'll want
to have a decent understanding of the Engine's GameFramework: classes like GameMode, GameState,
PlayerController, Pawn, and PlayerState.
And one of the best ways to get more familiar
with the Engine is to look at its source code and see how it boots up your game.
If you crack open the Unreal Engine codebase and find the main function - that is,
the entry point of the program - it may be difficult to find your way from that point
to where your game code actually runs.
There are lots of different systems at play,
there's indirection for supporting different platforms, there's a whole
lot of conditional compilation going on to support different build configurations,
there are separate game and render threads, and there are object-oriented abstractions built on
top of the core "game loop" functionality to make all of that complexity manageable.
And if you start looking at the code that initializes the Engine,
you might find some scary-looking stuff.
When the Engine starts up, before it gets
into those higher-level abstractions, it runs thousands of lines of code to do lots and lots
of little things that set up global state and initialize various systems, and it's all kind
of gross to look at, but it's an ugly truth in any kind of software engineering that if it's
long and it's complicated and it was written 20 years ago and it works,
you don't touch it.
And honestly, some messy complexity is kind of inevitable at this stage.
It's like you're witnessing the first few moments after the Big Bang: there's tons of
stuff happening, and a lot of systems overlapping each other, in a very small surface area.
By the time you get to InitGame or BeginPlay - and game code that you've written,
the universe has expanded and things have settled into a more ordered form.
But I think it can be instructive to cut through the chaos and look at how the engine
gets from the entry point of the program to actually running your game code.
It all begins in the Launch module, where you'll find different main functions
defined for different platforms.
Eventually, they all find their way to this GuardedMain function in Launch.cpp.
If we squint a little, and maybe cut out
some of this extraneous code, we can see a basic game loop here.
The main loop for the Engine is implemented in a class called FEngineLoop.
We can see that the engine loop has a PreInit stage, then the engine gets fully initialized,
and then we tick every frame until we're ready to exit.
Let's break down what happens in these function calls.
PreInit is where most of the modules are loaded.
When you make a game project or a plugin that has C++ source code,
you define one or more source modules in your .uproject or .uplugin file,
and you can specify a LoadingPhase to dictate when that module will be loaded.
The Engine is split into different source modules as well.
Some modules are more essential than others, and some are only loaded on certain platforms
or in certain situations - so a module system helps
to make sure that the dependencies between different parts of the codebase are manageable,
and it makes sure that we can just load what we need for any given configuration.
When the engine loop begins its PreInit phase, it loads up some low-level Engine modules
so that the essential systems are initialized and the essential types are defined.
Then, if your project or any enabled plugins have
source modules that are in these early loading phases, those are loaded next.
After that, the bulk of higher-level Engine modules are loaded.
After that, we come to the default point where project and plugin modules are loaded.
This is typically the point where your game's C++ code is first injected
into what was previously just a generic instance of Unreal Engine.
Your game module comes into being at a point where all the essential Engine functionality
has been loaded and initialized, but before any actual game state has been created.
So what happens when your module is loaded?
First, the Engine registers any UObject classes that are defined in that module.
This makes the reflection system aware of those classes,
and it also constructs a CDO, or class default object, for each class.
The CDO is a record of your class in its default state, and it serves as
a prototype for further inheritance.
So if you've defined a custom Actor type,
or a custom Game Mode, or anything declared with UCLASS
in front of it; the engine loop allocates a default instance of that class,
then runs its constructor, passing in the CDO of the parent class as a template.
This is one of the reasons why the constructor shouldn't contain any gameplay-related code:
it's really just for establishing the universal details of the class, not for modifying
any particular instance of that class.
After all your classes are registered, the
engine calls your module's StartupModule function, which is matched with ShutdownModule, giving you a
chance to handle any initialization that needs to be tied to the lifetime of the module.
So at this point, the Engine loop has loaded all the required engine, project, and plugin modules,
it's registered classes from those modules, and it's initialized all the low-level systems that
need to be in place. That finishes the PreInit stage, so we can move onto the Init function.
The Engine loop's Init function is comparatively straightforward.
If we simplify it just a little, we can see that it hands things off to a class called UEngine.
Prior to this point, when I've said "engine," we've been talking about the
engine with a lowercase e: basically, the executable that we're starting up,
consisting of code that we didn't write ourselves.
Here we're introducing THE Engine, capital-E Engine. The engine is a software product,
and it contains a source module called Engine, and in that module is a header called Engine.h,
and in that header is defined a class called UEngine, which is implemented in
both UEditorEngine and UGameEngine flavors.
During the Init phase for a game, FEngineLoop
checks the Engine config file to figure out which GameEngine class should be used.
Then it creates an instance of that class and enshrines it as the global UEngine instance,
accessible via the global variable GEngine, which is declared in Engine/Engine.h.
Once the Engine is created, it's initialized, which we'll have more
to say about in just a second.
When that's done, the engine loop
fires a global delegate to indicate that the Engine is now initialized,
and then it loads any project or plugin modules that have been configured for late loading.
Finally, the Engine is started, and initialization is complete.
So what does the Engine class actually do? It does a lot of things, but its
main responsibility lies in this set of big, fat functions here, including Browse and LoadMap.
We've looked at how the process boots up and gets all the engine systems initialized,
but in order to get into an actual game and start playing, we have to load into a map,
and it's the UEngine class that makes that happen for us.
The Engine is able to Browse to a URL, which can represent either a server address to connect to as
a client, or the name of a map to load up locally. URLs can also have arguments added onto them.
When you set a default map in your project's DefaultEngine.ini file,
you're telling the Engine to browse to that map automatically when it boots up.
Of course, in development builds, you can also override that default map by
supplying a URL at the command-line, and you can also use the open command to browse to a
different server or map during gameplay.
So let's look at Engine initialization.
The Engine initializes itself before the map is loaded, and it does so by creating
a few important objects: a GameInstance, a GameViewportClient, and a LocalPlayer.
You can think of the LocalPlayer as representing the user who's sitting in front of the screen,
and you can think of the viewport client as the screen itself: it's essentially
a high-level interface for the rendering, audio, and input systems, so it represents
the interface between the user and the Engine.
The UGameInstance class was added in Unreal 4.4,
and it was spun off from the UGameEngine class to handle
some of the more project-specific functionality that was previously handled in the Engine.
So after the Engine is initialized, we have a GameInstance,
a GameViewportClient, and a LocalPlayer.
Once that's done, the game is ready to start:
this is where our initial call to LoadMap occurs. By the end of the LoadMap call, we'll have a
UWorld that contains all the actors that were saved into our map, and we'll also have a handful
of newly-spawned actors that form the core of the GameFramework: that includes a game mode, a game
session, a game state, a game network manager, a player controller, a player state, and a pawn.
One of the key factors that separates these two sets of objects is lifetime.
At a high level, there are two different lifetimes to think about: there's everything
that happens before a map is loaded, and then there's everything that happens after.
Everything that happens before LoadMap is tied to the lifetime of the process.
Everything else - things like GameMode, GameState, and PlayerController - are created after the map
is loaded, and they only stick around for as long as you're playing in that map.
The engine does support what it calls "seamless travel", where you can transition to a different
map while keeping certain actors intact.
But if you straight-up browse to a new map,
or connect to a different server, or back out to a main menu - then all actors are destroyed,
the world is cleaned up, and these classes are out of the picture until you load another map.
So let's look at what happens in LoadMap. It's a complicated function,
but if we pare it back to the essentials, it's not that hard to follow.
First the engine fires a global delegate to indicate that the map is about to change.
Then, if there's already a map loaded, it cleans up and destroys that world. We're mostly concerned
with initialization right now, so we'll just wave our hand at that. Long story short,
by the time we get here, there's no World.
What we do have, though, is a World Context.
This object is created by the Game Instance during Engine initialization, and it's
essentially a persistent object that keeps track of whichever world is loaded up at the moment.
Before anything else gets loaded, the GameInstance has a chance to preload any assets that it might
want, and by default, this doesn't do anything.
Next we need to get ourselves a UWorld.
If you're working on a map in the editor, the editor has a UWorld loaded into memory,
along with one or more ULevels, which contain the Actors you've placed. When you save your
persistent level, that World, its Level, and all its Actors, get serialized to a map package,
which is written to disk as a .umap file.
So during LoadMap, the engine finds that map
package and loads it. At this point, the World, its persistent level, and the actors in that
level - including the WorldSettings - have been loaded back into memory.
So we have a World, and now we have to initialize it.
The Engine gives the World a reference to the GameInstance, and then it initializes a global
GWorld variable with a reference to the World.
Then the World is installed into the WorldContext, it has its world type initialized - to Game,
in this case - and it's added to the root set, which prevents it from being garbage collected.
InitWorld allows the world to set up systems like physics, navigation, AI, and audio.
When we call SetGameMode, the World asks the GameInstance
to create a GameMode actor in the world.
Once the GameMode exists, the Engine fully loads
the map, meaning any always-loaded sublevels are loaded in, along with any referenced assets.
Next, we come to InitializeActorsForPlay. This is what the Engine refers to as
"bringing the world up for play."
Here, the World iterates over all actors in a few different loops.
The first loop registers all actor components with the world. Every ActorComponent within
every Actor is registered, which does three important things for the component:
First, it gives it a reference to the world that it's been loaded into.
Next, it calls the component's OnRegister function, giving it a
chance to do any early initialization.
And, if it's a PrimitiveComponent
when all is said and done, after registration the component will have a FPrimitiveSceneProxy
created and added to the FScene, which is the render thread's version of the UWorld.
Once components have been registered, the World calls the GameMode's InitGame function.
That causes the GameMode to spawn a GameSession actor.
After that, we have another loop where the world goes level-by-level,
and has each level initialize all its actors. That happens in two passes. In the first pass,
the Level calls the PreInitializeComponents function on each Actor. This gives Actors a chance
to initialize themselves fairly early, at a point after their components are registered but before
their components have been initialized.
The GameMode is an actor like any other,
so its PreInitializeComponents function is called here too.
When that happens, the GameMode spawns a GameState object and associates it with the World, and it
also spawns a GameNetworkManager, before finally calling the game mode's InitGameState function.
Finally, we finish by looping over all actors again, this time calling InitializeComponents,
followed by PostInitializeComponents.
InitializeComponents loops over all the Actor's components and checks two things:
If the component has bAutoActivate enabled, then the component will be activated.
And if the component has bWantsInitializeComponent enabled,
then its InitializeComponent function will be called.
PostInitializeComponents is the earliest point where the actor is in a fully-formed state,
so it's a common place to put code that initializes the actor at the start of the game.
At this point, our LoadMap call is nearly done:
all Actors have been loaded and initialized, the World has been brought up for play,
and we now have a set of actors used to manage the overall state of the game:
GameMode defines the rules of the game, and it spawns most of the core gameplay actors.
It's the ultimate authority for what happens during gameplay, and it only exists on the server.
GameSession and GameNetworkManager are server-only as well.
The network manager is used to configure things like cheat detection and movement prediction.
And for online games, the GameSession approves login requests, and it serves
as an interface to the online service (like Steam or PSN, for example).
The GameState is created on the server, and only the server has the authority to change it,
but it's replicated to all clients: so it's where you store data that's relevant to the
state of the game, that you want all players to be able to know about.
So now the world has been fully initialized, and we have the game framework actors that represent
our game. All we're missing now are the game framework actors that represent our player.
Here, LoadMap iterates over all the LocalPlayers present in our GameInstance: typically there's
just one. For that LocalPlayer, it calls the SpawnPlayActor function. Note that "PlayActor"
is interchangeable with "PlayerController" here: this function spawns a PlayerController.
LocalPlayer, as we've seen, is the Engine's representation of the player,
whereas the PlayerController is the representation of the player within the game world.
LocalPlayer is actually a specialization of the base Player class. There's another Player class
called NetConnection which represents a player that's connected from a remote process.
In order for any player to join the game, regardless of whether it's local or remote,
it has to go through a login process.
That process is handled by the GameMode.
The GameMode's PreLogin function is only called for remote connection attempts:
it's responsible for approving or rejecting the login request.
Once we have the go-ahead to add the player into the game, either because the remote
connection request was approved or because the player is local, Login gets called.
The Login function spawns a PlayerController actor and returns it to the World.
Of course, since we're spawning an actor after the world has been brought up for play,
that actor gets intialized on spawn. That means our PlayerController's
PostInitializeComponents function gets called, and it in turn spawns a PlayerState actor.
The PlayerController and PlayerState are similar to the GameMode and GameState in that one is the
server-authoritative representation of the game (or the player), and the corresponding
state object contains the data that everyone should know about the game (or the player).
Once the PlayerController has been spawned,
the World fully initializes it for networking and associates it with the Player object.
With all that done, the game mode's PostLogin function gets called, giving the game a chance
to do any setup that needs to happen as a result of this player joining.
By default, the game mode will attempt to spawn a Pawn for the new PlayerController on PostLogin.
A Pawn is just a specialized type of actor that can be possessed by a Controller.
PlayerController is a specialization of the base Controller class,
and there's another subclass called AIController that's used for non-player characters.
This is a longstanding convention in Unreal: if you have an actor that moves around the world
based on its own autonomous decision-making process - whether that's a human player making
decisions and translating them into raw inputs, or an AI making higher-level decisions about
where to go and what to do - then you typically have two actors.
The Controller represents the intelligence driving the actor,
and the Pawn is just the in-world representation of the actor.
So when a new player joins the game, the default GameMode implementation spawns a
Pawn for the new PlayerController to possess.
The game framework does also support spectators:
your PlayerState can be configured to indicate that the player should spectate,
or you can configure the GameMode to start all players as spectators initially. In that case,
the GameMode won't spawn a Pawn, and instead the PlayerController will spawn its own
SpectatorPawn that allows it to fly around without interacting with the game world.
Otherwise, on PostLogin the game mode will do what it calls "restarting the player." Think
of "restarting" in the context of a multiplayer shooter: if a player gets killed, their Pawn is
dead - it's no longer being controlled; it just hangs around as a corpse until it's destroyed.
But the PlayerController is still around, and when the player's ready to respawn,
the game needs to spawn a new Pawn for them. So that's what RestartPlayer does:
given a PlayerController, it'll find an actor representing where the new Pawn should be spawned,
and then it'll figure out which Pawn class to use, and it'll spawn an instance of that class.
By default, the game mode looks through all the PlayerStart actors that have been
placed in the map and picks one of them. But all of this behavior can be overridden and
customized in your own GameMode class.
In any event, once a Pawn has been spawned,
it'll be associated with the PlayerController, and the PlayerController will possess it.
Now, back in LoadMap, we've got everything ready for the game to actually start. All that's left
to do is route the BeginPlay event. The Engine tells the World, the World tells the GameMode,
the GameMode tells the WorldSettings, and the WorldSettings loops over all actors.
Every Actor has its BeginPlay function called, which in turn calls BeginPlay on all components,
and the corresponding BeginPlay events are fired in Blueprints.
With all that done, the game is fully up and running, LoadMap can finish up,
and we've made it into our game loop.
Let's run through that one more time, quickly, just to review.
When we run our game in its final, packaged form, we're running a process.
The entry point of that process is a main function,
and the main function runs the engine loop.
The engine loop handles initialization,
then it ticks every frame, and when it's done, it shuts everything down.
Right now we're mostly concerned with what happens during initialization.
The first point where your project or plugin code runs is going to be when your module is loaded.
That can happen at a number of points, depending on the LoadingPhase, but typically
it happens toward the end of PreInit.
When your module is loaded, any UObject
classes get registered, and default objects get initialized via the constructor. Then your
module's StartupModule function is called, and this is the first place where you might
hook into delegates to set up other functions to be called later.
The Init stage is where we start setting up the Engine itself.
In short, we create an Engine object, we initialize it, and then we Start the game.
To initialize the Engine, we create a GameInstance and a GameViewportClient,
and then we create a LocalPlayer and associate it with the GameInstance.
With those essential objects in place, we can start loading up the game.
We figure out which map to use, we browse to that map,
and we let the GameInstance know when that's finished.
The rest of our startup process happens in the LoadMap call.
First we find our map package, then we load it: this brings any actors placed
into the persistent level into memory, and it also gives us a World and a Level object.
We find that World, we give it a reference to the GameInstance, we initialize some systems in the
World, and then we spawn a GameMode Actor.
After that, we fully load the map,
bringing in any always-loaded sublevels and any assets that need to be loaded.
With everything fully loaded, we start bringing the world up for play.
We first register all components for every actor in every level...
And then we initialize the GameMode, which in turn spawns a GameSession actor.
And then we initialize all the Actors in the world.
First, we call PreInitializeComponents on every actor in every level:
when this happens for the GameMode, it spawns a GameState,
and a GameNetworkManager, and then it initializes the GameState.
Then, in another loop, we initialize every actor in every level: this
calls InitializeComponent (and potentially Activate) for all components that need it,
and then our actors become fully-formed.
Once the world is brought up for play, we can log our LocalPlayer into the game.
Here we spawn a PlayerController, which in
turn spawns a PlayerState for itself and adds that PlayerState to the GameState...
And then we register that player with the GameSession and cache an initial start spot.
With the PlayerController spawned, we can now initialize it for networking
and associate it with our LocalPlayer...
And then we proceed to PostLogin, where, assuming everything is set up for it,
we can restart the player, meaning we figure out where they should start in the world,
we figure out which Pawn class to use, and then we Spawn and initialize a Pawn.
And then we have the PlayerController possess
the Pawn, and we have a chance to set up defaults for a player-controlled pawn.
Finally, all we have to do is route the BeginPlay event.
This results in BeginPlay being called on all Actors in the World, which registers
tick functions and calls BeginPlay on all components, and then finally, that's where
our BeginPlay Blueprint event gets fired.
At that point, we're done loading the map, we've
officially started the game, and we've finished the initialization stage of our engine loop.
We've covered a lot of ground here, so here are just a few quick points to wrap up:
We looked at the GameModeBase and GameStateBase classes,
rather than GameMode and GameState. These base classes were added in Unreal 4.14, in order to
factor out some of the Unreal-Tournament-flavored functionality from the game mode.
Whereas GameModeBase contains all the essential game mode functionality,
the GameMode class adds the concept of a "match", with match state changes that occur
after BeginPlay. This handles overall game flow, like spectating before all players are ready,
deciding when the game start and ends, and transitioning to a new map for the next match.
We also looked at the Pawn class, but the GameFramework also defines a Character class,
which is a specialized type of Pawn that includes several useful features.
A Character has a collision capsule that's used primarily for movement sweeps,
it has a skeletal mesh, so it's assumed to be an animated character,
it has a CharacterMovementComponent - which is kind of tightly coupled to the Character
class and does a few very useful things:
The most important thing is that Character
movement is replicated out of the box, with client-side movement prediction.
That's a very useful feature to have if you're making a multiplayer game.
Characters can also consume root motion from animation playback
and apply it to the actor, with replication.
Character Movement also handles navigation and
pathfinding, so you can have an AIController possess a Character
and it'll be able to move anywhere on the navmesh that you tell it to, without you
having to run your own navigation queries.
And finally, Character Movement implements a
full kitchen-sink range of movement options for walking, jumping, falling, swimming,
and flying; and there are lots of different parameters for tuning movemenet behavior.
You can take advantage of most of that functionality at a lower level, at least in C++,
but the Character class is a great starting point. Just keep in mind that if you leave
the default Character settings untouched, then your game is just going to feel like an Unreal
tutorial project through no fault of its own. So it's a good idea to think about how you want
your game's movement to feel, in the abstract, and then tune the movement parameters accordingly.
So, all of these classes that we've looked at (with the exception of UWorld and ULevel)
are here for you to extend as needed.
We've seen how Unreal has this mature Game Framework that has an established design
for handling things like online integration, login requests, and network replication.
That means that you can develop multiplayer games pretty easily out of the box,
and the design of the engine allows you to add custom functionality at pretty much any level.
If you're mostly interested in making simple, purely single-player games,
then the complexity of the Game Framework might feel kind of pointless to you.
Just bear in mind that it's purely opt-in: for example, if you don't need to do anything special
before the map is loaded, then you probably don't need a custom GameInstance class,
and the default GameInstance implementation will just do its job and stay out of your way.
I still think it's useful to know what these classes are designed for, though,
because once you know what you're doing, it doesn't cost you anything to use them as intended,
and you'll generally end up with a cleaner design that way.
For example, if you have some information about a player that you need to keep track of,
there are a number of different places you could put that data.
For a multiplayer game, you need to choose wisely, or you might find that
the data isn't accessible where you need it to be. If you're making a singleplayer game,
you could pick pretty much any object, including the GameMode, and the worst that happens
is that you have to follow an awkward chain of references to get to the data when you need it.
But regardless of what kind of game you're making, it's a good idea to think critically
about how your data is structured, and how different objects interact with each other.
It'll make you a better programmer in the long run.
It's also worth pointing out that extending these
classes through inheritance isn't the only way to add your own functionality to the engine.
If you just need to run some code in response to something that the Engine does, the simplest
approach is just to bind a callback function to a delegate that represents that event.
In particular, the Engine defines a few different sets of static delegates that
you can bind to at any point.
That includes CoreDelegates,
CoreUObjectDelegates, GameViewportDelegates, GameDelegates, and WorldDelegates.
As of Unreal 4.22, the Engine also has a "subsystem" feature that makes it easy to
add modular functionality. All you have to do is define a class that extends one
of these subsystem types, and the Engine will automatically create
an instance of your subsystem that's tied to the lifetime of the corresponding object.
For example, a plugin might add custom functionality to your project by having
you use a custom GameInstance that's defined in that plugin. That would work, but you'd be locked
into that class: if there was a second plugin that did the same thing, you'd be out of luck.
Using a GameInstanceSubsystem instead of a custom GameInstance would solve that problem,
and that's generally a cleaner approach for modular, self-contained functionality.
So that's a look at how Unreal starts up your game. I hope it's helped you
understand how all the different pieces of the Unreal game framework fit together,
and I hope it's given you some decent context for how the Engine works.
It may feel like a lot to take in up front, but I think it's useful to just be exposed to these
design decisions and have them rattling around in your head for when you need them later.
These videos take a whole lot of work to put together, but I like to think that the effort
put into research, writing, editing, and the accompanying motion graphics pays off
in the end result. If you'd like to support that work and see more videos like this,
please consider tossing me a couple bucks on Patreon. And thanks for watching!
Browse More Related Video
UE5.4 State Tree Data Management
Unreal Engine C++ Project Setup, From Scratch
EA WRC Official VR Mode - OpenXR Best VR Settings - Quest 3
The Blueprint to Making Games with No Experience (2024)
ALAN WAKE 2 🌲 التحقيق في الجريمة الغامضة
Tutorial Pixels: Jogo NFT na Ronin | Guia Completo para iniciantes + Dicas Avançadas
5.0 / 5 (0 votes)