In-depth UE - UObject (12) type system - summary

introduction

It’s a summary again. We used a long paragraph to explain the structure, information collection, structure registration and binding links of the type system in UE4 one by one. But there are still some small topics and some sorting out that have not been finished, and they will be taken away in the final wave.

Several stages of UClass object growth

I believe that many readers, after researching for a period of time or reading this series of articles, although they feel that they understand a lot of things, they may still be like me when I first started researching on my own, thinking about the whole process in my head, but... It's still a little blurry and I don't feel like I have a solid grip on it. At first, we are ignorant, and we are all blind and trying to figure out the elephant. But if we want to be wanton and playful, we must get into science and observe from multiple angles. In my experience, two perspectives are useful at this point: process and data. The process focuses on how each step connects the previous and the next, and the data focuses on how tall buildings rise from the ground.

Regarding the type system, the sequence of each function call has been discussed previously, interspersed with the several stages of UClass* object construction. But fragmentary narration and specialized compilation are two different things!

A UClass* goes through several stages:

  1. Memory structure. I just created a blank piece of memory and simply called the UClass constructor. For the construction of an object in UE, calling the constructor is just the starting point.
  2. register. Give yourself a name and register yourself in the object system. This step is done through DeferredRegister instead of Register.
  3. Object construction. Fill the object with information about properties, functions, interfaces, and metadata. We should remember that this step is completed by those functions in gen.cpp.
  4. Binding link. Now that we have the attribute functions, we need to sort them out and try to optimize the storage structure to provide higher performance and convenience for future use.
  5. CDO creation. Now that the type is there, everything is ready and all that is left is for the country package to allocate a class default object. Each UClass has a CDO (Class Default Object). With CDO, it is equivalent to having an archive backup and reference, and other objects will not panic.
  6. Quote token stream construction. How a Class can refer to other objects and how to construct this reference tree efficiently are also a very important topic in GC. With the reference token stream, you can efficiently analyze how many other objects an object refers to.

UMetaData

I almost skipped this metadata before because it is only used in Editor mode. One step in the Construct of all type objects is AddMetaData:

#if WITH_METADATA
void AddMetaData(UObject* Object, const FMetaDataPairParam* MetaDataArray, int32 NumMetaData)
{
    if (NumMetaData)
    {
        UMetaData* MetaData = Object->GetOutermost()->GetMetaData();//得到Package属于的MetaData
        for (const FMetaDataPairParam* MetaDataParam = MetaDataArray, *MetaDataParamEnd = MetaDataParam + NumMetaData; MetaDataParam != MetaDataParamEnd; ++MetaDataParam)
        {
            MetaData->SetValue(Object, UTF8_TO_TCHAR(MetaDataParam->NameUTF8), UTF8_TO_TCHAR(MetaDataParam->ValueUTF8)); //添加数据
        }
    }
}
#endif
UMetaData* UPackage::GetMetaData()
{
#if WITH_EDITORONLY_DATA
    if (MetaData == NULL)
    {
        MetaData = FindObjectFast<UMetaData>(this, FName(NAME_PackageMetaData));
        if(MetaData == NULL)
        {
            MetaData = NewObject<UMetaData>(this, NAME_PackageMetaData, RF_Standalone | RF_LoadCompleted);
        }
    }
    if (MetaData->HasAnyFlags(RF_NeedLoad))
    {
        MetaData->GetLinker()->Preload(MetaData);
    }
    return MetaData;
#else
    return nullptr;
#endif
}

There are three things we can know from here. One is that UMetaData is associated with UPackage and is not directly bound to a UField. The second is that UMetaData is skipped under Runtime. Third, UMetaData is also an object. Continue to look at the definition of UMetaData:

class COREUOBJECT_API UMetaData : public UObject
{
public:
    TMap< FWeakObjectPtr, TMap<FName, FString> > ObjectMetaDataMap;//对象关联的键值对
    TMap< FName, FString > RootMetaDataMap;//包本身的键值对
};

const FString& UField::GetMetaData(const FName& Key) const
{
    UPackage* Package = GetOutermost();
    UMetaData* MetaData = Package->GetMetaData();
    const FString& MetaDataString = MetaData->GetValue(this, Key);
    return MetaDataString;
}

The definition of this ObjectMetaDataMap is very interesting. FWeakObjectPtr uses a weak pointer to reference the UObject object, so that it will not hinder the GC of the object; the key uses FName, because there are only fixed keys (Category, Tooltip, etc.); the value uses FString, you can write whatever you like. What did you write?

Thinking: Why not put Map<FName,FString> MetaDataMap directly into UObject?  A straightforward idea might be to add a field directly to UObject, so that all UObjects can have additional metadata. But sometimes being too direct can be harmful. Straight men should all know... UE is designed like this to decouple! Embedding has visible disadvantages:

  • The size of the UObject itself is increased, and whether metadata or metadata is used, it has to bear the burden of one more field. Even if it is only put into UField, it will only be relieved, because there are still many UFields that do not have redundant metadata to record.
  • It is easy to ask for help but difficult to give away. Once it is embedded, it is more difficult to take it out. These metadata information are only provided for the editor interface behavior in editor mode and should be omitted during Cook. At this time, if MetaDataMap is in a real object. If you want to separate a part of it from a binary stream, this operation is relatively difficult. And if UMetaData is an independent object, then even if it is saved, it will be saved in a whole place in the file, and it will be much easier to dismantle the building alone.

Of course, removing it will create an additional layer of indirect access, which will increase the efficiency burden and the opportunity for CacheMiss. But judging from the situation, UMetaData can only be used in the editor. It doesn't matter if the editor is a little slower. There are no frame rate requirements like games. And the frequency of accessing UMetaData is not high. It is only obtained when initializing the interface to change the UI. UMetaData is redesigned so that UPackage is associated (Outer is UPackage), and UPackage is the unit of serialized storage, so that UMetaData can be loaded or released as an independent part.

The role of GRegisterCast and GRegisterNative

As mentioned before, there are two collection points in a static initialization phase: IMPLEMENT_CAST_FUNCTIONGRegisterCast IMPLEMENT_VM_FUNCTIONand GRegisterNative, but there is no chance to explain what they are used for. These are actually some functions used to convert objects and some basic functions of the blueprint virtual machine. Bind the instruction code running in the virtual machine to the real function pointer. These are the parts of the blueprint, which will be introduced later when we talk about blueprints.

Flags

There is another thing that is a bit skipped, which is the enumeration of various Flags. The UE uses these enumeration flags to determine the status and characteristics of the object. The important ones are:

  • EObjectFlags: Flags of the object itself.
  • EInternalObjectFlags: Object storage flags, used to check reachability during GC.
  • EObjectMark: A flag used to additionally mark object characteristics and is used to identify status during serialization.
  • EClassFlags: Class flags, which define the characteristics of a class.
  • EClassCastFlags: Conversion between classes, you can quickly test whether a class can be converted to a certain type.
  • EStructFlags: Characteristic flags of the structure.
  • EFunctionFlags: Feature flags of the function.
  • EPropertyFlags: Characteristic flags of the property.

Readers are asked to check the definitions themselves for details, as there are too many to explain one by one. This is also a common C++ idiom, enumeration flags to represent overlay characteristics.

Summarize

After talking about such a large set of processes, I don’t know how readers feel about this part of the UE process. Is this process designed to be smooth and reasonable? In retrospect, is the overall context clear? the answer is negative. As a person who can write this part into ten articles, I feel that there are too many twists and turns, let alone ordinary developers to read. Although there are practical trade-offs in this, in fact it is also neglected and the historical baggage is too heavy.

From one aspect, this part of CoreUObject does not need to be exposed to ordinary users, so it doesn't matter how complicated it is internally. We often say that a module should have high cohesion and low coupling, but the context of this sentence is actually about the boundaries of the module, but the judgment of the boundaries is just a matter of perspective. From another perspective, the outside of a function can also be considered a boundary. Therefore, for a module, if it is entangled internally, it is actually only strong on the outside and weak on the inside. But everything is also a matter of degree. As long as a minor illness does not affect the quality of life, it will be fine even if it lasts a lifetime. The internal processes of the engine are complicated and not elegant enough from a perfectionist perspective, but in fact, as long as the person in charge can If you can hold it, you can actually accept it calmly.

Can it just go away like this? It is true that we have no say in modifying such a core module of UE, but as a technician, with an attitude of striving for excellence, we must try our best to improve the project modules we are responsible for in our own one-third-acre land. It's elegantly designed. This is especially true for modules provided for others to use.

A very important topic I want to talk about here when it comes to game engines is confidence! The confidence of engine developers and the confidence of engine users. In economics, confidence is very important. People's expectations for the future will greatly affect the social economy. It can even be said that the essence of currency is confidence. In the field of game engines, confidence is also very important. Game developers believe that the engine can help them achieve the desired effects, and engine developers believe that they can continuously upgrade and iterate the engine to make it better and better, so that a positive cycle can continue.

What does confidence have to do with structure? In fact, the relationship is very big. When the code of an engine or module seems to be difficult to read and understand and to modify, people will gradually begin to lose confidence. Take CoreUObject as an example. An ordinary developer studies its code by himself and finds that it is too complicated and difficult to understand. Will he have strong confidence in using this engine well? He would only think that there are many secrets hidden in this engine, so I should just use it as an ordinary person, and be careful not to step into pitfalls. People tend to lose their sense of control over vague and unclear things. If they don’t feel safe, they will be timid and start to become conservative. Gradually, subconsciously, even if the engine has new powerful functions, they will not dare to use them because of fear. Step into the trap. I am afraid because I don’t understand the new technology, and my impression is that it is difficult to understand, because it was difficult to understand in the past. Gradually, there is a trend in the game engine market that this engine is difficult to use. For engine developers themselves, if they understand that the cost of maintaining a module is too high, they will gradually lose confidence. When developing software projects, sometimes an out-of-control situation occurs. When a bug occurs, fixing it depends on luck and defense, and developers try not to touch it. In a sense, this kind of module has completed its life cycle and is about to die. The engine is a product, and the code is actually a product, both externally and internally. The code is meant to be read by people, and the code provided to others must be beautiful, otherwise everyone will just type 01.

In fact, I sometimes hesitate, what is the use of knowing the internal construction process of the type system? It takes so much effort, and the more difficult the content, the fewer people will read it. From a utilitarian point of view, it would be better if I just draw a structure diagram and then illustrate several usages. But then I thought about it, and there are actually two advantages to it. The first is confidence. If you cut something open, there will be no secrets. Once everyone uses it and transforms it, their confidence will be at ease. Secondly, it broadens the breadth of thinking and provides more ideas for actual programming.

To give a small example, if you see a certain attribute in the editor and want to modify its value in C++, it turns out that it is not public, and even the header file may be private. At this time, if the type system People who don't understand the structure deeply may give up, but people who understand know that they can traverse the UProperty through this object to find this property and modify it.

Another example is if you make a plug-in and call the Details panel properties of the engine editor itself, but want to hide some fields in it. At this time, it is often difficult to do it without modifying the engine, but if you know the properties The properties in the panel actually come from UProperties one by one, so you can get this property through the object path, and then turn on and off some of its Flags to achieve the effect. This is also considered a conventional Hack method.

So, the more you know, the more it will return to you in places you never imagined.

The next article will talk about the possible applications of type system reflection in actual combat.

Guess you like

Origin blog.csdn.net/ttod/article/details/133265802