Skip to content

10 — Інтеграція на C++

🇬🇧 English | 🇺🇦 Українська

Модулі

Модуль Призначення
FuzzyLogic Типи, рушій висновку, компонент, підсистема, Data Asset, JSON
FuzzyLogicUMG Малювання кривих у UMG
FuzzyLogicDemo Runtime. Актори та панель fire control для двох демо-карт. Ніщо інше від нього не залежить, але це runtime-модуль, і він таки лінкується в запаковану гру — див. 16 — Demo Content
FuzzyLogicEditor Редактор Data Asset; не додавайте до game-модуля
FuzzyLogicTests Automation-тести; не додавайте до game-модуля

У .Build.cs модуля гри:

PublicDependencyModuleNames.AddRange(new[]
{
    "Core",
    "CoreUObject",
    "Engine",
    "FuzzyLogic"
});

Додайте FuzzyLogicUMG, лише якщо використовуєте UFuzzyCurveDrawLibrary.

Рушій як value type

FFuzzyInferenceEngine не є UObject і може жити на стеку, у підсистемі або звичайній структурі:

#include "Inference/FuzzyInferenceEngine.h"

FFuzzyInferenceEngine Engine;
if (!Engine.Compile(System))
{
    for (const FFuzzyDiagnostic& Diagnostic : Engine.GetCompileDiagnostics())
    {
        UE_LOG(LogTemp, Warning, TEXT("%s"), *Diagnostic.ToString());
    }
}

const TMap<FName, float> Inputs = {
    { TEXT("Distance"), 450.0f },
    { TEXT("Health"), 72.0f }
};
const FFuzzyInferenceResult Result = Engine.Evaluate(Inputs);
const float Aggression = Result.GetOutput(TEXT("Aggression"), 0.0f);

Compile парсить і зв’язує правила з індексами один раз. Evaluate після цього не шукає рядки правил.

Побудова системи кодом

#include "Types/FuzzyMembershipFunction.h"

FFuzzySystem System;

FFuzzyVariable Distance(TEXT("Distance"), FFuzzyRange(0.0f, 2000.0f));
Distance.DefaultValue = 1000.0f;
Distance.AddSet(FFuzzySet::Make<FFuzzyMF_Trapezoid>(
    TEXT("Near"), 0.0f, 0.0f, 300.0f, 700.0f));
Distance.AddSet(FFuzzySet::Make<FFuzzyMF_Trapezoid>(
    TEXT("Far"), 500.0f, 900.0f, 2000.0f, 2000.0f));
System.AddInput(MoveTemp(Distance));

FFuzzyVariable Aggression(TEXT("Aggression"), FFuzzyRange(0.0f, 1.0f));
Aggression.AddSet(FFuzzySet::Make<FFuzzyMF_Triangle>(
    TEXT("Cautious"), 0.0f, 0.0f, 0.6f));
Aggression.AddSet(FFuzzySet::Make<FFuzzyMF_Triangle>(
    TEXT("Bold"), 0.4f, 1.0f, 1.0f));
System.AddOutput(MoveTemp(Aggression));

System.AddRule(TEXT("IF Distance IS Near THEN Aggression IS Cautious"));
System.AddRule(TEXT("IF Distance IS Far THEN Aggression IS Bold"));

Компонент

#include "FuzzyLogicComponent.h"

FuzzyLogic->SystemAsset = BehaviorAsset;
FuzzyLogic->SetInput(TEXT("Distance"), DistanceToTarget);
const FFuzzyInferenceResult Result = FuzzyLogic->Evaluate();

Компонент компілює ліниво й кешує результат. Він порівнює те, з чого було зібрано кеш, тож якщо вказати йому на інший SystemAsset — у рантаймі, з Blueprint чи C++, — наступний виклик перекомпілює систему сам.

Єдина зміна, якої він не помічає, — редагування структури FFuzzySystem на місці. Після неї викличте MarkSystemDirty:

FuzzyLogic->InlineSystem.Settings.SampleCount = 401;
FuzzyLogic->MarkSystemDirty();

Зміни властивостей у редакторі та PostLoad скидають кеш автоматично.

FFuzzyInferenceEngine::Compile копіює всю систему всередину рушія, і кожен компонент володіє власним рушієм — тож система, спільна через один UFuzzySystemAsset, усе одно дублюється й перекомпільовується для кожного компонента. Для сотень агентів краще поділитися одним скомпільованим рушієм — див. 14 — Architecture And Performance.

Спільна скомпільована система

UFuzzyLogicSubsystem тримає один скомпільований рушій на асет протягом життя game instance. Тоді агенти не мають власної системи — лише свої вхідні значення.

#include "FuzzyLogicSubsystem.h"

void AMyAgent::BeginPlay()
{
    Super::BeginPlay();

    if (UGameInstance* GameInstance = GetGameInstance())
    {
        // Compiled on the first agent that asks; every agent after that gets the same engine.
        Brain = GameInstance->GetSubsystem<UFuzzyLogicSubsystem>()->GetEngine(BehaviorAsset);
    }
}

void AMyAgent::Tick(float DeltaSeconds)
{
    Super::Tick(DeltaSeconds);

    if (!Brain.IsValid())
    {
        return;
    }

    Inputs.Add(TEXT("Distance"), DistanceToTarget);
    Inputs.Add(TEXT("Health"), CurrentHealth);

    Aggression = Brain->Evaluate(Inputs).GetOutput(TEXT("Aggression"), 0.5f);
}

із такими членами:

UPROPERTY(EditAnywhere, Category = "AI")
TObjectPtr<UFuzzySystemAsset> BehaviorAsset;

TSharedPtr<const FFuzzyInferenceEngine> Brain;
TMap<FName, float> Inputs;

Тримайте рушій на весь час життя агента, а не викликайте GetEngine щокадру. Пошук дешевий, але тримати — безкоштовно, і це зберігає рушій живим крізь інвалідацію, тож робота, що вже виконується, ніколи не втратить ґрунт під ногами.

Метод Призначення
GetEngine Спільний скомпільований рушій, компіляція на перший запит. Null для null-асета
EvaluateAsset / EvaluateAssetOutput Обчислення одним викликом, доступне й у Blueprint
PrepareAsset Скомпілювати завчасно, наприклад на екрані завантаження
ValidateAsset Скомпілювати за потреби й зібрати діагностику
InvalidateAsset / InvalidateAll Скинути кеш після зміни системи в рантаймі
IsAssetPrepared, GetCachedSystemCount Інтроспекція

Система, що не скомпілювалася, теж кешується, а її діагностика пишеться в лог один раз і повторюється в кожному результаті: щокадрові спроби зібрати зламану систему лише передрукували б ті самі повідомлення.

Потоки

GetEngine може компілювати, тому викликайте його з ігрового потоку. Те, що він повертає, незмінне, а Evaluate є const — тож будь-яка кількість потоків може обчислювати на одному рушії одночасно:

ParallelFor(Agents.Num(), [this](int32 Index)
{
    // Safe: every agent reads the same engine and writes only its own result.
    Agents[Index].Aggression = Brain->Evaluate(Agents[Index].Inputs).GetOutput(TEXT("Aggression"));
});

Підготуйте рушій до початку паралельної роботи — PrepareAsset на екрані завантаження або GetEngine у BeginPlay, як вище.

Поверхня й дефазифікатори

FFuzzyAggregatedSurface Surface;
if (Engine.BuildOutputSurface(TEXT("Aggression"), Result.RuleActivations, Surface))
{
    const float Center = FuzzyLogic::Defuzzify::Centroid(Surface);
}

Доступні Centroid, Bisector, MeanOfMaxima, SmallestOfMaxima, LargestOfMaxima, WeightedAverage та універсальний Apply.

Потоки

Після Compile метод Evaluate є const, тому незмінний скомпільований engine можна використовувати для паралельних оцінювань. Не викликайте Compile одночасно з Evaluate для того самого екземпляра. UObject-компонент має використовуватися відповідно до правил потоків Unreal.