Пример паттерна Прототип в Unreal Engine

в 7:16, , рубрики: c++, design patterns, Gamedev, pattern, patterns, prototype, unrealengine, проектирование, прототип
Пример паттерна Прототип в Unreal Engine - 1

В интернетах полно статей про паттерны. Но реальных примеров из живых проектов встречается немного. Понятно, что в случае с Прототипом, есть довольно популярный проектик Java Script или Lua. Но я хочу еще! Поэтому в этом посте приведу пример паттерна из Unreal Engine.

Общая информация для приличия

Прототип - шаблон проектирования, описывающий создание объектов, путем клонирования существующих, нежели их инстанцирования с нуля. Такой способ весьма пользителен, покуда инициализация новорожденных объектов обходится в копеечку, а нам требуется создать несколько экземпляров. 

Этот шаблон, также, облегчает жизнь, когда мы не знаем с каким именно типом работаем. Известен только его базовый интерфейс (далекий предок), а нам страсть как хочется сделать копию объекта, который в нашем распоряжении. Таким образом мы просто вызываем его метод clone(), а уж он сам знает как себя клонировать, будьте уверены.

Использование паттерна в Unreal Engine

В Анриле объекты (UObjects) создаются через вызов функции NewObject<T>. На первый взгляд, как будто ни разу не клонирование и вообще это скорей Фабрика, нежели Прототип. Но если копнуть глубже…

Каждому классу, объявленному под макросом UCLASS и отнаследованному от UObject, соответствует некий объект типа UClass (можно получить через функцию StaticClass()), в котором содержится Class Default Object. Этот CDO и есть прототип, из которого создаются новые инстансы. В частности, из этого образцового объекта копируются дефолтные значения свойств во вновь сконструированный клон.

Скрытый текст

Что интересно, из UClass можно вытащить CDO через GetDefaultObject(), поменять там значение по умолчанию какого-нибудь свойства (а то и всех), а затем подставить этот образец в качестве параметра Template в функцию NewObject. Но при этом важно параметр bCopyTransientsFromClassDefaults поставить в true. Тогда, созданные объекты будут иметь уже измененные значения полей. Не уверен насколько часто такое в жизни нужно. Но мне было интересно.

Здесь, в отличии от академических примеров, метода clone/copy в чистом виде нет. Клонирование прототипа происходит даже не в самом UObject. Это ответственное задание делегируется классу FObjectInitializer, который передается в конструктор UObject.
Изучая код и уткнувшись в этот класс, я не сразу нашел место, где копируются поля. Путем нескольких запусков процесса создания объекта под дебугом, “место преступления” было обнаружено… в деструкторе этого самого FObjectInitializer, а именно в точке вызова PostConstructInit :

FObjectInitializer::~FObjectInitializer()
{
  //Some code...
  
#if USE_CIRCULAR_DEPENDENCY_LOAD_DEFERRING
	bool bIsPostConstructInitDeferred = false;
	if (!FBlueprintSupport::IsDeferredCDOInitializationDisabled())
	{
		if (FObjectInitializer* DeferredCopy = FDeferredObjInitializationHelper::DeferObjectInitializerIfNeeded(*this))
		{
			DeferredCopy->bIsDeferredInitializer = true;
			// make sure this wasn't mistakenly pushed into ObjectInitializers
			// (the copy constructor should have been what was invoked, 
			// which doesn't push to ObjectInitializers)
			check(FUObjectThreadContext::Get().TopInitializer() != DeferredCopy);

			bIsPostConstructInitDeferred = true;
		}
	}

	if (!bIsPostConstructInitDeferred)
#endif // USE_CIRCULAR_DEPENDENCY_LOAD_DEFERRING
	{
		PostConstructInit();
	}

  // Some code...
}
void FObjectInitializer::PostConstructInit()
{
	//Some code...

	if (bShouldInitializePropsFromArchetype)
	{
		UClass* BaseClass = (bIsCDO && !GIsDuplicatingClassForReinstancing) ? SuperClass : Class;
		if (BaseClass == NULL)
		{
			check(Class==UObject::StaticClass());
			BaseClass = Class;
		}
	
		UObject* Defaults = ObjectArchetype ? ObjectArchetype : BaseClass->GetDefaultObject(false); // we don't create the CDO here if it doesn't already exist
		InitProperties(Obj, BaseClass, Defaults, bCopyTransientsFromClassDefaults);
	}

	// Some code...
}
void FObjectInitializer::InitProperties(UObject* Obj, UClass* DefaultsClass, UObject* DefaultData, bool bCopyTransientsFromClassDefaults)
{
	// Some code...

	if (!bNeedInitialize && bCanUsePostConstructLink)
	{
		// This is just a fast path for the below in the common case that we are not doing a duplicate or initializing a CDO and this is all native.
		// We only do it if the DefaultData object is NOT a CDO of the object that's being initialized. CDO data is already initialized in the
		// object's constructor.
		if (DefaultData)
		{
			if (Class->GetDefaultObject(false) != DefaultData)
			{
				for (FProperty* P = Class->PropertyLink; P; P = P->PropertyLinkNext)
				{
					bool bIsTransient = P->HasAnyPropertyFlags(CPF_Transient | CPF_DuplicateTransient | CPF_NonPIEDuplicateTransient);
					if (!bIsTransient || !P->ContainsInstancedObjectProperty())
					{
						if (P->IsInContainer(DefaultsClass))
						{
							P->CopyCompleteValue_InContainer(Obj, DefaultData);
						}
					}
				}
			}
			else
			{
				// Copy all properties that require additional initialization (e.g. CPF_Config).
				for (FProperty* P = Class->PostConstructLink; P; P = P->PostConstructLinkNext)
				{
					bool bIsTransient = P->HasAnyPropertyFlags(CPF_Transient | CPF_DuplicateTransient | CPF_NonPIEDuplicateTransient);
					if (!bIsTransient || !P->ContainsInstancedObjectProperty())
					{
						if (P->IsInContainer(DefaultsClass))
						{
							P->CopyCompleteValue_InContainer(Obj, DefaultData);
						}
					}
				}
			}
		}
	}
	else
	{
		// As with native classes, we must iterate through all properties (slow path) if default data is pointing at something other than the CDO.
		bCanUsePostConstructLink &= (DefaultData == Class->GetDefaultObject(false));

		UObject* ClassDefaults = bCopyTransientsFromClassDefaults ? DefaultsClass->GetDefaultObject() : NULL;	
		check(!GEventDrivenLoaderEnabled || !bCopyTransientsFromClassDefaults || !DefaultsClass->GetDefaultObject()->HasAnyFlags(RF_NeedLoad));

		for (FProperty* P = bCanUsePostConstructLink ? Class->PostConstructLink : Class->PropertyLink; P; P = bCanUsePostConstructLink ? P->PostConstructLinkNext : P->PropertyLinkNext)
		{
			if (bNeedInitialize)
			{		
				bNeedInitialize = InitNonNativeProperty(P, Obj);
			}

			bool bIsTransient = P->HasAnyPropertyFlags(CPF_Transient | CPF_DuplicateTransient | CPF_NonPIEDuplicateTransient);
			if (!bIsTransient || !P->ContainsInstancedObjectProperty())
			{
				if (bCopyTransientsFromClassDefaults && bIsTransient)
				{
					// This is a duplicate. The value for all transient or non-duplicatable properties should be copied
					// from the source class's defaults.
					P->CopyCompleteValue_InContainer(Obj, ClassDefaults);
				}
				else if (P->IsInContainer(DefaultsClass))
				{
					P->CopyCompleteValue_InContainer(Obj, DefaultData);
				}
			}
		}

		// Some code...
	}
}

Вызовы FProperty::CopyCompleteValue_InContainer и совершают акт копирования значений!

Заключение

Упрощенные примеры из обучалок - это хорошо, правильно и полезно. Но в жизни это может выглядеть иначе. Сей вариант показался мне интересным.

Вероятно, полезные ссылки:

💀

Автор: TheSkull

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js