本文记录在 Niagara 和 Cascade 编辑器中分别对 NiagaraSystem 和 ParticleSystem 特效的发射器自动添加 Module 的方法,目的在于批量、自动添加美术(或程序)所需的模块,便于美术做通用效果,便于程序做通用接口
一、背景
Particle 和 Cascade (ParticleSystem 的编辑器的名称)相关知识和术语可以参考:ParticleSystem
Niagara 编辑器与 Cascade 表现上看结构类似,但是实际复杂很多,大体结构都如下图所示:
都是 Parameters -> Modules -> Emitters -> System
的四层结构,其中:
- System - 指的就是这整个特效资源
- Emitters - 指每一个发射器(由他们控制发射粒子,不管是 Sprite、Mesh、Ribbon 还是其他),编辑器中竖着的一整条
- Modules - 指每一个模块,控制发射器的不同属性和功能,编辑器中每一个横着的一条
- Parameters - 指每一个模块中的参数,可以由美术人员配置,也可在程序中由程序控制
其中:
- System - 运行时会变成 Component,ParticleSystem 创建出
UParticleSystemComponent
,NiagaraSystem 创建出UNiagaraComponent
- Emitters - 静态时是
UParticleEmitter
和UNiagaraEmitter
,运行时就是FParticleMeshEmitterInstance
和FNiagaraEmitterInstance
- Modules - Particle 的都是代码写死的,都在
Engine\Source\Runtime\Engine\Classes\Particles
文件夹下,可以用的就只有这么多种,比如图中的 ColorOverLife 代码中就是UParticleModuleColorOverLife
;而 Niagara 的是资产,可以认为都是蓝图,引擎自带的都在Engine\Plugins\FX\Niagara\Content\Modules
目录下,当然也可以自己连一下自己用,作为自己项目的资产,非常灵活好用。 - Parameters - 可以配置的参数,Cascade 也都是固定的,如果像开给外部,让蓝图或者程序设置,可以像下图这样设置成 Parameter 即可,蓝图或者代码就可以
SetVectorParameter("ABC", DummyVec);
这样控制了;Niagara 不太一样,需要在UserExposed
中添加一个参数,并且把参数和想用的输入关联上之后,在外部控制(外部只能设置 User Expoosed 的参数,感觉很不爽= =)
二、具体方法
两种特效进行自动添加 Module 的前提都是在代码中能拿到这个特效资源,不管是在 Content Browser 中打开还是多选,保存时触发还是有按钮统一执行,都可以(Niagara 可能不行,需要把编辑器打开)。
2.1 ParticleSystem
参考在 Cascade 中右键新增一个 Emitter 时的代码:void FCascade::OnNewEmitter()
,会自动添加几个默认的 Module(UParticleSpriteEmitter::SetToSensibleDefaults()
),比如 Life,直接照抄引擎即可:
// Lifetime module
UParticleModuleLifetime* LifetimeModule = NewObject<UParticleModuleLifetime>(GetOuter());
UDistributionFloatUniform* LifetimeDist = Cast<UDistributionFloatUniform>(LifetimeModule->Lifetime.Distribution);
if (LifetimeDist)
{
LifetimeDist->Min = 1.0f;
LifetimeDist->Max = 1.0f;
LifetimeDist->bIsDirty = true;
}
LifetimeModule->LODValidity = 1;
LODLevel->Modules.Add(LifetimeModule);
如果想要添加一个自定义而不是引擎自带的 Module,就需要照葫芦画瓢,抄一下引擎写法,自己在代码中新增 Module,很简单。这里可以直接设上默认值,比如像添加一个外部可以控制的参数,可以在 ::InitializeDefaults()
里 NewObject<UDistributionFloatParticleParameter>(
。
值的注意的是,有一些结构没有加 ENGINE_API
开放出来,如 FComposableFloatDistribution
,如果需要用到,需要自己加一下。
2.2 NiagaraSystem
Niagara 稍微复杂些,层级结构是:
- FNiagaraSystemViewModel - 编辑器中特效的整体
- FNiagaraEmitterHandleViewModel - 编辑器中每一个发射器
- UNiagaraStackViewModel - 每一个发射器的 Stack(Modules 的堆叠)
- UNiagaraStackItemGroup - 每一个Group(一共有7个,如下图所示)
- UNiagaraStackModuleItem - 每一个 Module
分别对应编辑器中的七个深色框及其下边的浅色框所代表的组。
具体方法参考 void UNiagaraEmitterConversionContext::InternalFinalizeStackEntryAddActions()
FNiagaraEditorModule& NiagaraEditorModule = FModuleManager::GetModuleChecked<FNiagaraEditorModule>("NiagaraEditor");
TSharedPtr<FNiagaraSystemViewModel> const SystemViewModel = NiagaraEditorModule.GetExistingViewModelForSystem(NS);
if (!SystemViewModel.IsValid())
{
return;
}
const TArray<TSharedRef<FNiagaraEmitterHandleViewModel>>& EmitterViewModels = SystemViewModel->GetEmitterHandleViewModels();
// 每一个发射器
for (TSharedRef<FNiagaraEmitterHandleViewModel> EmitterHandleViewModel : EmitterViewModels)
{
UNiagaraStackViewModel* StackViewModel = EmitterHandleViewModel->GetEmitterStackViewModel();
TArray<UNiagaraStackItemGroup*> StackItemGroups;
StackViewModel->GetRootEntry()->GetUnfilteredChildrenOfType<UNiagaraStackItemGroup>(StackItemGroups);
// 每一个 StackItemGroup
for (UNiagaraStackItemGroup* StackItemGroup : StackItemGroups)
{
// 筛选出 ParticleSpawn Stack
FName const ExecutionCategoryName = StackItemGroup->GetExecutionCategoryName();
if (ExecutionCategoryName != UNiagaraStackEntry::FExecutionCategoryNames::Particle)
{
continue;
}
FName const ExecutionSubcategoryName = StackItemGroup->GetExecutionSubcategoryName();
if (ExecutionSubcategoryName != UNiagaraStackEntry::FExecutionSubcategoryNames::Spawn)
{
continue;
}
// 自动添加一个预设 Module
UNiagaraClipboardContent* const ClipboardContent = UNiagaraClipboardContent::Create();
UNiagaraScript* const Script = LoadObject<UNiagaraScript>(nullptr, TEXT("/Game/Effects/NiagaraModules/Test.Test"), nullptr, LOAD_None, nullptr);
const UNiagaraClipboardFunction* const ClipboardFunction = UNiagaraClipboardFunction::CreateScriptFunction(ClipboardContent, "NS_ScaleSizeBySystem", Script, FGuid());
ClipboardContent->Functions.Add(ClipboardFunction);
// Commit the clipboard content to the target stack entry
FText PasteWarning = FText();
StackItemGroup->Paste(ClipboardContent, PasteWarning);
}
}
如果新增的 Module 有输入,需要关联参数,可以参考 SNiagaraStackFunctionInputValue::OnFunctionInputDrop
,这个是把一个参数拖到 Input Pin 上的函数,直接设置的话可以直接 FunctionInput->SetLinkedValueHandle(Handle);