관리 메뉴

fatalite

언리얼 엔진 프레임 워크 : int main()에서 Begin Play까지 본문

게임/Unreal

언리얼 엔진 프레임 워크 : int main()에서 Begin Play까지

fataliteforu 2023. 8. 30. 14:41

다음은 언리얼 엔진이 시작해서 Begin Play까지 어떤 일이 일어나는지 설명하는 영상이다.

시각적으로 잘 설명되어있어, 이 부분을 번역하고 엔진의 기초적인 구조를 이해하기 위해 짧은 영어로 번역해보기로 했다.

일차적으로 문서로 정리할 예정

 

https://www.youtube.com/watch?v=IaU2Hue-Ap 

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.

 

Unreal's scary initialization code 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.


GuardedMain and FEngineLoop

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.


PreInit : loading engine, project, and plugin modules (IModuleInterface, UObject)

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 (1)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 (2)higher-level  Engine modules are loaded. After that, we come to the default point where  project and plugin modules are loaded. #우리가 쓴 C++ Code가 처음 주입되는 시점. 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 

chance to handle any initialization that needs  to be tied to the lifetime of the module.


Init: Creating and starting the Engine (UEngine / UGameEngine)

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.

FGuardedMain -> FEngineLoop -> PreInit -> Initializing UEngineInstance(Global Variable, GEngine)
GEngine에서 제일 중요한 역할은 Map을 Browse하고 Load하는 일이다.
Browse의 URL은 서버의 주소를 의미할 수도, 로컬(싱글 게임) 맵의 이름을 의미할 수 도있다.
DefaultEngine.ini 를 수정해서 첫 부팅 후 들어갈 맵을 설정할 수 있다.
Development Build에서는 Command line으로 URL을 입력하여 디폴트 맵을 변경하거나 게임 플레이 중에 다른 맵이나 서버를 찾을 수 있다.

Engine initialization (UGameInstance, UGameViewportClient, ULocalPlayer)

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.

FGuardedMain -> FEngineLoop -> PreInit -> Initializing UEngineInstance(Global Variable, GEngine) -> Game Instance , UGameViewportClient, ULocalPlayer -> Load Map
Local Player은 스크린 앞에 있는 유저를 의미하고 ViewportClient는 스크린을 의미한다.
UGameInstance는 Unreal 4.4 이후로 추가됐다. 엔진의 기능 중, 좀 더 게임과 연관된 기능을 위해서 추가된 듯하다.
Engine이 초기화 되면 우리는 위 세 개를 갖게되며 게임을 시작할 준비가 되었으며 이 곳에서 Load Map이 실행된다. 
Load Map의 끝 부분에서 우리는 모든 Actor(Game Mode, Game Session, Game State, Game Network Manamger, Player Controller...)를 가지고 있는 UWorld를 가지게 되며 이 부분이 오브젝트의 Lifetime을 구분하는 중요한 지점이다. 좀 더 추상적인 관점에서 보면 맵이 로드되기 전과 후, 두 가지 관점의 Lifetime 을 생각해볼 수 있다.

LoadMap: Reloading the world from disk (UWorld, ULevel)

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.

FGuardedMain -> FEngineLoop -> PreInit -> Initializing UEngineInstance(Global Variable, GEngine) -> Game Instance , UGameViewportClient, ULocalPlayer -> Load Map - > WorldContext -> UWorld(Consist of ULevels) -> -> Set World Settings, System -> Game Mode
Load Map에서는 맵이 바뀌는 것을 의미하는 글로벌 델리게이트를 실행하고, 맵이 로드되었다면 World를 파괴한다. (이미 있던 World를 파괴한다는 뜻인듯?) 그리고 WorldContext를 가지게 된다. 이것은 Game Instance에 의해서 엔진 초기화 과정중에 생긴다. 맵을 로드하기 전에 게임 인스턴스는 에셋 등을 프리로드할 수 있다. 그리고 UWorld가 생긴다.  UWorld가 메모리에 올라가게 되고, 이 UWorld는 하나 이상의 ULevel을 지닌다. 
UWorld가 생기고는 이걸 초기화 한다. 엔진이 World에 Game instance의 Reference를 준다. GC의 Root Set에 넣어 콜렉팅되는 것을 방지한다.  
InitWorld는 월드의 시스템(물리, 네비게이션, AI, Audio) 등을 설정한다. SetGameMode를 호출하면 World가 Game Instance에게 GameMode를 World 내에 생성하도록 지시한다. 만약 Game Mode가 존재한다면, Engine은 맵을 모두 로드했다는 것이며 이는 모든 서브 레벨이 에셋 레퍼런스와 함께 로드 되었다는 것을 의미한다.

 


LoadMap: Bringing the world up for play (AGameModeBase, AGameStateBase, AGameSession)

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.

FGuardedMain -> FEngineLoop -> PreInit -> Initializing UEngineInstance(Global Variable, GEngine) -> Game Instance , UGameViewportClient, ULocalPlayer -> Load Map - > WorldContext -> UWorld(Consist of ULevels) -> -> Set World Settings, System -> Game Mode -> Initialize Actors For Play
Load Map에서는 맵이 바뀌는 것을 의미하는 글로벌 델리게이트를 실행하고, 맵이 로드되었다면 World를 파괴한다. (이미 있던 World를 파괴한다는 뜻인듯?) 그리고 WorldContext를 가지게 된다. 이것은 Game Instance에 의해서 엔진 초기화 과정중에 생긴다. 맵을 로드하기 전에 게임 인스턴스는 에셋 등을 프리로드할 수 있다. 그리고 UWorld가 생긴다.  UWorld가 메모리에 올라가게 되고, 이 UWorld는 하나 이상의 ULevel을 지닌다. 
UWorld가 생기고는 이걸 초기화 한다. 엔진이 World에 Game instance의 Reference를 준다. GC의 Root Set에 넣어 콜렉팅되는 것을 방지한다.  
InitWorld는 월드의 시스템(물리, 네비게이션, AI, Audio) 등을 설정한다. SetGameMode를 호출하면 World가 Game Instance에게 GameMode를 World 내에 생성하도록 지시한다. 만약 Game Mode가 존재한다면, Engine은 맵을 모두 로드했다는 것이며 이는 모든 서브 레벨이 에셋 레퍼런스와 함께 로드 되었다는 것을 의미한다.

LoadMap: Logging the player into the game (APlayerController, APlayerState, UPlayer / UNetConnection)

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).


LoadMap: Restarting the player (APawn, APlayerStart, AController / AAIController)

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.


LoadMap: Routing the BeginPlay event (AWorldSettings)

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. Animated callstack summary 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'veofficially started the game, and we've finished  the initialization stage of our engine loop.


Base game mode classes (AGameMode, AGameState)

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. Characters and Pawns (ACharacter, UCharacterMovementComponent) 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. Where to specify custom subclasses 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.


Delegates and subsystems (UGameInstanceSubsystem, UWorldSubsystem)

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.


Conclusion

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!

'게임 > Unreal' 카테고리의 다른 글

언리얼의 루멘(Lumen)  (0) 2023.09.25
Unreal Custom Plugin 사용법  (0) 2023.09.12
언리얼 MoveComponent (작성중)  (0) 2023.08.29
Unreal Optimization Overview Reference  (0) 2023.07.10
Unreal Optimization - Cull Distance Volume  (0) 2023.07.10