Перейти к основному содержимому
Iris Replication System: Лес десяти тысяч деревьев

Iris Replication System: Лес десяти тысяч деревьев

·895 слов·5 минут
Iris - Эта статья является частью серии.
Часть 2: Эта статья
Вырастим густой лес и посмотрим, как с этим справятся системы репликации.

Вступление
#

Добавить больше реплицируемых сущностей и сделать сценарий ближе к реальной игровой ситуации.

Говоря это в прошлый раз, я немного слукавил — с реальной игровой ситуацией это ничего общего иметь не будет. Зато сущностей действительно станет больше.
Я решил засадить игровую локацию лесом: каждое дерево — это реплицируемый AActor. Каждое дерево получит параметр здоровья и будет терять его раз в секунду. Просто реплицировать float или int показалось скучным, поэтому я добавил на каждое дерево UAbilitySystemComponent с UHealthAttrSet.

В реальном проекте создавать лес таким образом не стоит — существуют гораздо более оптимальные подходы.

Подготовка
#

Для начала нам потребуется актор дерева, на котором будет визуально отображаться его состояние здоровья.
Чем меньше значение здоровья, тем краснее становится дерево. Для этого будем менять параметр HealthRatio в MaterialInstance.

void ATreeActor::BeginPlay()
{
	// Создаём MaterialInstance, чтобы управлять цветом дерева в зависимости от здоровья
	ForEachComponent(false,
		[this](UActorComponent* Component)
		{
			if (auto Primitive = Cast<UPrimitiveComponent>(Component))
				if (auto Instance = Primitive->CreateAndSetMaterialInstanceDynamic(0))
					MaterialInstances.Add(Instance);
		});

	// Подписываемся на изменение здоровья
	AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(HealthAttrSet->GetHealthAttribute())
		.AddUObject(this, &ThisClass::OnHealthValueChanged);
}

void ATreeActor::OnHealthValueChanged(const FOnAttributeChangeData& Data)
{
	auto HealthRatio = Data.NewValue / FMath::Max(HealthAttrSet->GetHealthMax(), UE_KINDA_SMALL_NUMBER);
	for (auto Instance : MaterialInstances)
		Instance->SetScalarParameterValue(TEXT("HealthRatio"), HealthRatio);
}
Интересный факт: UAbilitySystemComponent не требует включённого тика, поэтому тик у ATreeActor можно отключить.

Так выглядит класс UHealthAttrSet:

UCLASS()
class THEGAME_API UHealthAttrSet : public UAttributeSet
{
	GENERATED_BODY()
public:
	UPROPERTY(EditAnywhere, BlueprintReadOnly, ReplicatedUsing = OnRep_Health)
	FGameplayAttributeData Health;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, ReplicatedUsing = OnRep_HealthMax)
	FGameplayAttributeData HealthMax;

	ATTRIBUTE_ACCESSORS(UHealthAttrSet, Health)
	ATTRIBUTE_ACCESSORS(UHealthAttrSet, HealthMax)
};

Чтобы отнимать здоровье, я создал GE_DamageOverTime, который раз в секунду уменьшает значение здоровья на 1 единицу. Этот эффект я накладываю на каждое дерево через 30 секунд после старта сервера.
Технически это не самое удачное решение, поскольку все деревья начинают терять здоровье одновременно, что вызывает подтормаживания во время игры.

Ну и конечно, нам нужен сам меш дерева.
Поскольку деревьев будет много, я решил сделать максимально простой вариант — с минимальным количеством полигонов.
Кроме того, у дерева следует отключить коллизии, чтобы клиенты не сталкивались с ним, и тени — чтобы во время тестов FPS был выше.

Низкополигональное дерево
Моё великолепное дерево: 33 вершины, 25 полигонов

Чтобы разместить большое количество деревьев, я использую Procedural Content Generation (PCG) с простым графом, который создаёт около 10 000 деревьев на площади 2 км². При плотности 0.0026 деревьев на м² я получил 10 201 дерево.

Тестирование
#

Все будет примерно как в прошлый раз

Iris Replication System
#

Запускаем тестирование и подключаемся к серверу. Предчувствие сразу не самое лучшее — ощущаются лаги и сетевые коррекции.
Когда возникает сильный рассинхрон между положением персонажа на сервере и на клиенте, сервер принудительно “возвращает” клиента в правильную позицию.
Это проявляется резким дёрганьем камеры и персонажа.

Лог завален предупреждениями, которые указывают на то, что сервер не справляется с обработкой входящих пакетов:

LogNetPlayerMovement: Warning: ServerMove: TimeStamp expired: 53.433105, CurrentTimeStamp: 54.618511, Character: BP_PlayerCharacter_C_2147471470
LogNetPlayerMovement: Warning: ServerMove: TimeStamp expired: 53.433105, CurrentTimeStamp: 54.643517, Character: BP_PlayerCharacter_C_2147471470
LogNetPlayerMovement: Warning: ServerMove: TimeStamp expired: 53.433105, CurrentTimeStamp: 54.668522, Character: BP_PlayerCharacter_C_2147471470

Результат, конечно, разочаровывающий…
Но если взглянуть с другой стороны — вдохновляющий!

Стандартная система репликации по умолчанию передаёт данные только об акторах, находящихся в радиусе 150 метров вокруг игрока.
Iris же реплицирует так только APawn, а все остальные акторы по умолчанию помечены как AlwaysRelevant — то есть реплицируются всем игрокам и всегда.

; BaseEngine.ini from Engine

[/Script/IrisCore.ObjectReplicationBridgeConfig]
RequiredNetDriverChannelClassName=/Script/Engine.DataStreamChannel
; Filters
DefaultSpatialFilterName=Spatial
; Filter configs should typically be configured per game
; Set the class-based dynamic filters configs here. Using None means the objects of this class are always relevant.
!FilterConfigs=ClearArray
; NotRouted means that this type will not be replicated
;+FilterConfigs=(ClassName=/Script/Engine.LevelScriptActor, DynamicFilterName=NotRouted) 
; By default classes derived from AActor are always relevant (because not filtered dynamically)
+FilterConfigs=(ClassName=/Script/Engine.Actor, DynamicFilterName=None)) 
; Info types aren't supposed to have physical representation and rely on a static priority
+FilterConfigs=(ClassName=/Script/Engine.Info, DynamicFilterName=None)
+FilterConfigs=(ClassName=/Script/Engine.PlayerState, DynamicFilterName=None)
; Pawns can be spatially filtered...
+FilterConfigs=(ClassName=/Script/Engine.Pawn, DynamicFilterName=Spatial))

By default classes derived from AActor are always relevant (because not filtered dynamically)

Все настройки Iris выполняются через файл конфигурации DefaultEngine.ini.
Добавим наш ATreeActor в Spatial-фильтр.

; DefaultEngine.ini
[/Script/IrisCore.ObjectReplicationBridgeConfig]
+FilterConfigs=(ClassName=/Script/TheGame.TreeActor, DynamicFilterName=Spatial))

Вот это результат — Iris демонстрирует впечатляющую производительность.

Никаких заметных сетевых коррекций не наблюдается — всё работает плавно.
Когда персонаж приближается к зелёным деревьям (они зелёные, потому что находятся за пределами радиуса репликации), те сразу становятся красными, как только попадают в зону видимости, реплицируя своё текущее здоровье.
Пришло время сравнить её с стандартной реализацией.

Default Replication System
#

Здесь ситуация, мягко говоря, далека от идеала. Стандартная система репликации с трудом удерживает стабильность, выдавая около 7 кадров в секунду. На выполнение игровой логики не остаётся времени — сервер перегружен подготовкой сетевых данных.

Этот эксперимент наглядно показывает, насколько Iris превосходит стандартную систему.
Остаётся лишь дождаться, когда Iris перейдёт из статуса Experimental в Beta и станет полноценным решением для продакшена.

Выводы
#

В таком сценарии Iris демонстрирует впечатляющий результат — система эффективно фильтрует и обрабатывает огромное количество реплицируемых сущностей без огромных потерь производительности.


Про графики
#

Как вы навреное заметили, на графиках в статье данные выглядят плавно, поскольку я применяю сглаживание значений для большей наглядности и удобства чтения.
В данной статье меня интересует в первую очередь площадь под графиком, а не отдельные пики.
Резкие скачки времени кадра, возникающие при одновременном отнимании здоровья у всех 10 000 деревьев (достигающие 200 мс), не оказывают существенного влияния на общий анализ.

Вот пример:

Как можно заметить, сглаженный график немного смещается вверх, переходя выше линии бюджета в 60 FPS, в то время как исходный, несглаженный - имеет точки, расположенные ниже этой границы.

Iris - Эта статья является частью серии.
Часть 2: Эта статья