📜 ⬆️ ⬇️

(Translation) Introduction to C ++ Development in UE4 Part 2

Part 1. Introduction. Creating a class and adding properties. Extending the C ++ class with Blueprint.
Part 2. Classes of gameplay. Structures. Reflection (reflection) in Unreal. Object / Actor iterators. Memory manager and garbage collector.
Part 3. Prefixes in class names. Integer types. Types of containers. Container iterators For-each loop, hash functions.
Part 4. Unreal Engine 4 for Unity developers.
Part 5. ...

image

From the Author: The beginning of the summer turned out to be hot for projects, so the translation was postponed for a long time, then it will be faster.
')
This article is a continuation of the translation of part of the documentation on UE4. You can find the original article by following this link .

Nice to see you still with us. In the following sections, we will explore the hierarchy of gameplay classes. We start with the basic blocks and talk about how they are related to each other. We will also study how UE uses inheritance and composition to create custom gameplay features.

Gameplay classes: Object, Actor and Component


The four main gameplay classes are most often expanded. These are UObject, AActor, UActorComponent and UStruct. Further each of them will be described in detail. You can create types that do not extend any of these classes, but they will not allow you to use most of the built-in features of the engine. As a rule, classes that are created outside the UObject hierarchy (not inherited from any depth from the UObject ) exist for the following purposes: integration of third-party libraries, wrapping features of the operating system, etc.

Unreal Objects (UObject)


The base unit in a UE is the UObject class. This class, together with the UClass class, provides you with the most important basic engine features:


Each class inherited from UObject contains a UClass singleton created for it. This class contains all the metadata about a class instance. The capabilities of UObject and UClass lie (jointly) at the heart of all that the gameplay object does during its life cycle. It is best to think about the difference between UClass and UObject as if UClass describes exactly how the UObject instance looks, what properties are available for serialization, networking, etc. Basically, gameplay development is not connected with inheritance directly from UObject , but with inheritance from classes AActor and UActorComponents . You do not need to know the details of how UClass / UObject works. But knowledge of their existence will be useful.

AActor


The AActor class represents an object that is part of the gameplay. An instance of this class is either hosted by the designer at the level, or created at runtime (using gameplay systems). All objects placed on a level are descendants of this class. For example - AStaticMeshActor, ACameraActor and APointLight. AActor is inherited from UObject, that is, it uses the standard functions that were listed in the previous section. AActor can be destroyed using game code (C ++ or Blueprint) or the standard garbage collection mechanism (when unloading a level from memory). It provides high-level behavior of our game objects, as well as being the base type that provides replication capability for the network mode. When replicating (in multi-user mode), AActor also gives access to information about any of its UActorComponent, which is required to support the operation of the network mode.

AActor has its own behaviors (specializes in inheritance), but other than that, they are containers for the UActorComponents hierarchy (specializes in composition). This can be done with the help of our AActor RootComponent member. It contains one UActorComponent, which, in turn, may contain many others. Before placing an AActor on a level, it must contain at least a USceneComponent that stores the position, rotation, and scale of the AActor.

AActor provide access to several events that are triggered during the entire AActor life cycle . Below is a short list of some of them:

BeginPlayCalled once the object enters the game.
TickEach frame is called to execute code during this frame.
EndplayCalled when an object leaves the game.


For a more detailed study of the class AActor, you can follow the link .

Duration of life cycle


In the previous section, the topic of the AActor life cycle was touched on a bit. For Actors placed at the level of the life cycle, you can imagine the following - loading and the beginning of the existence of Actor and his subsequent destruction (at the unloading level). Let's see what are the processes of creation and destruction during execution. Creating an AActor is a bit more complicated than creating a regular object. This happens because the AActor must be registered with different systems (running at run-time) - the physics engine, the manager responsible for the information coming in every frame, etc. Therefore, to create an object, there is the UWorld :: SpawnActor () method . After the required Actor is created successfully, the BeginPlay () method is called, followed by the Tick ​​() method (in the next frame).

If Actor has existed the time you need, you can destroy it by calling the Destroy () method . In this case, the EndPlay () method will be called , in which you can write the necessary code that is executed when the object is destroyed. Another option for Actor's life control is to use the LifeSpan field (lifetime). You can set this value in the constructor (or later at run-time). If the specified time expires, the Destroy method will be called automatically.

You can learn more about creating Actors by clicking on the link.

UActorComponent


UActor Components have their own behavior and, as a rule, are responsible for the functionality that is common to different AActors — for example, displaying meshes, particle systems, camera work, and physical interactions. Unlike the AActors, who provide the high-level logic of their behavior in your game, the UActorComponents usually perform individual tasks required to support higher-level objects. Components can be children of other components. They can be attached to only one parent component or Actor, but the component itself can have many child components. You can imagine this relationship as a tree of components. It should be remembered that the child components have a position, rotation and scale relative to the parent components or Actor.

There are many options for using Actors and components. One of the ways to imagine the relations of Actors and components is the following - Actor answers the question “What is this thing? (what is this thing?) ”, and the components - to the question “ What is it made of? (what is this thing made of?) "



Dissect First Person Character


In the previous sections there were many words without demonstrations. To illustrate the relationship between AActor and its UActor Components, let's examine the Blueprint that is generated when opening a project based on the First Person template . The picture below shows the component tree for the FirstPersonCharacter Actor. Here RootComponent is CapsuleComponent. Attached to the CapsuleComponent components are ArrowComponent, Mesh and FirstPersonCameraComponent. The most deeply nested vertex is the Mesh1P component , the parent of which is FirstPersonCameraComponent. This means that the position of the mesh is relative to this camera.
image

Graphic this component tree looks like it is shown in the picture below, where you can see all the components in 3d space (except for the Mesh component)
image

This component tree is attached to one actor-class. As we can see, we can create complex gameplay objects using both inheritance and composition. Inheritance should be used when changing an existing AActor or UActorComponent, and composition, if the set of AActor types must have similar functionality.

Ustruct


To use UStruct, you do not need to inherit from any particular class, it is enough to note the structure with the USTRUCT () macro and the build tools will do the main work for you. Unlike UObject, the garbage collector does not track UStruct. If you dynamically create instances of them, you must independently manage their life cycle. UStructs are used so that POD types have support for UObject reflection for editing in the UE, management via Blueprint, serialization, networking, etc.

Now, after discussing the basics of hierarchies for creating our gameplay classes, it's time to choose your path again. You can learn more about the gameplay classes, explore our examples in search of more information, or continue to dive deeper into the features of C ++ to create a game.

Dive even deeper


Are you sure you want to know more? We will continue to study the work of the engine.

Unreal reflection system


Blog Post: Property System in Unreal (Reflection)

The gameplay classes use special markup, so before moving on to them, let's look at some basic things from the Unreal properties system . UE4 uses its own reflection system, which provides you with various dynamic capabilities — garbage collection, serialization, network replication, and Blueprint / C ++ interaction . These features are optional, that is, you must add the required markup yourself for your types, otherwise Unreal ignores them and does not generate the necessary data for reflection. Below is a brief overview of the main markup elements:



Example of the UCLASS class definition:

#include "MyObject.generated.h" UCLASS(Blueprintable) class UMyObject : public UObject { GENERATED_BODY() public: MyUObject(); UPROPERTY(BlueprintReadOnly, EditAnywhere) float ExampleProperty; UFUNCTION(BlueprintCallable) void ExampleFunction(); }; 

You may notice for the first time the inclusion of the “MyClass.generated.h” header . Unreal will place all generated data for reflection in this file. In the declaration of your type (in the list of included files) this file should be located last.

You may also have noticed that it is possible to add additional qualifiers to markup macros. In the code above, some of the most common ones are added for demonstration. They allow you to specify the specific behavior that our types possess:


The number of qualifiers is very large, so we will not list them here, but give links to the relevant sections of the documentation:


Object / Actor iterators


Object iterators are a very useful tool for iterating through all instances of a particular type of UObject and its subclasses.

 //     UObject for (TObjectIterator<UObject> It; It; ++It) { UObject* CurrentObject = *It; UE_LOG(LogTemp, Log, TEXT("Found UObject named: %s"), *CurrentObject.GetName()); } 

You can limit the search by providing a more specific type of iterator. Suppose you have a class called UMyClass, inherited from UObject. You can find all instances of this class (and all that are its descendants) as follows:

 for (TObjectIterator<UMyClass> It; It; ++It) { // ... } 

WARNING: Using iterator objects in PIE (Play in Editor) can produce unexpected results. While the editor is loaded, the object iterator will return all the UObjects created for your instance of the game world, in addition to those used in the editor.

Actor iterators work in a similar way, just like object iterators, but they work only for the heirs from the AActor. Actor iterators do not have the problem mentioned above and return only those objects that are used in the current exam of the game world.

When an Actor iterator is created, it needs to pass a pointer to a UWorld instance . Many child classes from UObject, for example, APlayerController provide this method. If you are not sure, you can check the return value of the ImplementsGetWorld method to see if a particular class supports the GetWorld method .

 APlayerController* MyPC = GetMyPlayerControllerFromSomewhere(); UWorld* World = MyPC->GetWorld(); // Like object iterators, you can provide a specific class to get only objects that are // or derive from that class for (TActorIterator<AEnemy> It(World); It; ++It) { // ... } 

Since the AActor is a heir from UObject, you can use TObjectIterator as well to find all AActors instances . But be careful in PIE!

Memory Manager and Garbage Collector


Actors are usually not collected by the garbage collector. You must manually call the Destroy method after spawning Actor. Removal will not happen immediately, but only during the next phase of garbage collection.

This case is most common if you have Actors with UObject properties .

 UCLASS() class AMyActor : public AActor { GENERATED_BODY() public: UPROPERTY() MyGCType* SafeObject; MyGCType* DoomedObject; AMyActor(const FObjectInitializer & ObjectInitializer) : Super(ObjectInitializer) { SafeObject = NewObject<MyGCType>(); DoomedObject = NewObject<MyGCType>(); } }; void SpawnMyActor(UWorld * World, FVector Location, FRotator Rotation) { World->SpawnActor<AMyActor>(Location, Rotation); } 

When we call this function, we generate Actor in our world. Its constructor creates two objects. One is labeled UPROPERTY, the other is a regular pointer. While Actors are part of the root object, SafeObject will not be collected by the garbage collector, since it can be accessed from this root. DoomedObject, however, will have a different life cycle. Since it is not marked as UPROPERTY, the garbage collector knows nothing about references to it and eventually it will be destroyed.

When a UObject is collected by the garbage collector, all UPROPERTY references will get nullptr values . This is done to safely check whether the object is destroyed by the garbage collector or not.

 if (MyActor->SafeObject != nullptr) { // Use SafeObject } 

This is an important note, because, as mentioned earlier, the actors for whom the Destroy () method was called are not deleted until the next phase of garbage collection. You can check whether the UObject is waiting for deletion using the IsPendingKill () method . If the method returns true, the object is considered dead and should not be used.

UStructs


As mentioned earlier, UStructs is a lightweight version of UObject. UStructs cannot be collected by the garbage collector. If you are using a dynamic copy of UStructs, you can use smart pointers, which we will talk about later.

Links not to UObject


Typically, non-UObjects may also have the ability to add an object reference to prevent them being deleted by the garbage collector. To do this, the object must inherit FGCObject and override the AddReferencedObjects method .

 class FMyNormalClass : public FGCObject { public: UObject * SafeObject; FMyNormalClass(Uobject * Object) : SafeObject(Object) { } void AddReferencedObjects(FReferenceCollector & Collector) override { Collector.AddReferencedObject(SafeObject); } } 


We use the FReferenceCollector to manually add a hard link to the required UObject, which should not be collected by the garbage collector. When an object is deleted (the destructor is triggered), all links added by it will be deleted.

PS: I ask all suggestions for correcting errors and inaccuracies sent in a personal.

Source: https://habr.com/ru/post/257827/


All Articles