效果图
一个简单的切换角色的 Niagara 特效,粒子从模型 A 出发,扩散一会后吸附到模型 B 上,写了些 Niagara Module Script ,并且开放 UserParameters
参数,在 C++ 里设置切换时候的特效模型。
先预览一下发射器,就一个发射器(美术较弱,就不做那么好看了,主要是对 Niagara 的使用),其中 NMS_MeshLerp 和 NMS_NoiseLerp 是自己写的 Module :
首先是设成 GPU 粒子,要开 FixedBounds
:
发射器只要发射一次就好:
在 EmitterUpdate 里使用 SpawnRate
,尽量多生成一些粒子。
Render 是使用 SpriteRender
,这个时候已经能看到粒子了。
在 InitializeParticle 里设置 Lifetime
的随机, SpriteSize
大小也随机一下。在 ParticleSpawn
下再加一个 SkeletalMeshLocation
,让粒子从模型的表面的三角面出发,这个节点有多种模式,Bones
是从骨骼上发射,Sockets
是从插槽上,或者骨骼和插槽发射,还有顶点Vertices
,这里使用三角面发射。这里的模型使用在 UserExposed 里新建的变量 MeshA
,这里还建了另一个变量 MeshB
。UserExposed 命名空间下的变量是开发给代码逻辑里可以传输的,类似于材质的参数。
下面是 ParticleUpdate,加上自己写的 NMS_MeshLerp :
这里就是获取两个模型 三角面的随机点坐标 输出到 PositionA
和 PositionB
。对了,这里 ModuleUsageBitmask
设置一下,我们只需要它用在 ParticleUpdate 里。NMS_MeshLerp 外边的参数用UserExposed 的 MeshA
和 MeshB
设置:
添加一个 CurlNoiseForce 模块,给粒子一个向外扩散的扰动,NoiseStrength
使用曲线控制,一开始不需要扰动,然后逐渐加强,在 0.2 之前都是 0 。按惯例,添加一个 Drag,增加摩擦。
然后添加一个 NMS_NoiseLerp,里面也很简单,就是将当前粒子的坐标和 NoisePosition
做一个 Lerp
:
外边的赋值:
NoisePosition
的值在外边再做一个 Lerp
,从前面存储的 PositionA
和 PositionB
,而 Alpha
是在 SystemAttribute 里定义的变量 ValueAlpha
,同时这个 ValueAlpha
也设置给 A,这个 ValueAlpha
就是控制所有效果的一个 Lerp
值了。它的值是在 SystemUpdate 中进行更新的。设一个曲线,在 3.5 秒前都是 0,这样 Lerp
的坐标就是从模型 A 出发向外扩散的运动,然后从 3.5 秒开始到最后的 5 秒升为 1,粒子最后就聚集到模型 B 身上了,完成从模型 A 扩散后最后吸附到模型 B 表面的粒子效果。这里需要 注意 的是,SystemState 里,要把 LoopDuration
设置为 Infinite
,并且 LoopDuration
设为 5 秒。虽然我们每次变身,粒子使用完就销毁,但后来才发现,用同一个NiagaraSystem
创建的 NiagaraComponent
,其实是 共用 同一个 System 的,如果设 LoopBehavior
为 Once
,后面再用这个 System 发射的粒子,都 不会更新 SystemUpdate 里的 ValueAlpha
。
对了,为了稍微好看点,在最后添加一个 Color 模块,添加一个颜色曲线,然后粒子的颜色有一些变化,并且透明度最后慢慢变为透明,避免消散得太突然。好了,到这里特效就做完了,下面是 C++ 实现变身的部分。
在自己的 PlayerController
类里添加 ChangeCharacter
作为变身输入绑定的函数:
void AXXPlayerController::ChangeCharacter()
{
LastPawn = GetPawn();
if (LastPawn)
{
/*当前如果不是玩家角色,不允许切换*/
if (IShapeshiftInterface* Interface = Cast<IShapeshiftInterface>(LastPawn))
{
if (Interface->CanShapeshift())
{
ShapeshiftIndex += 1;
if (ShapeshiftIndex >= ShapeshiftList.Num())
{
ShapeshiftIndex = 0;
}
if (ShapeshiftList.IsValidIndex(ShapeshiftIndex))
{
LastPawn->SetActorEnableCollision(false);
Interface->StopMovementImmediately();
LastPawn->DisableInput(this);
LastPawn->SetActorHiddenInGame(true);
LastPawn->GetRootComponent()->SetVisibility(false, true);
DisableInput(this);
FActorSpawnParameters Param;
Param.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
NextPlayerPawn =
GetWorld()->SpawnActor<APawn>(ShapeshiftList[ShapeshiftIndex].PawnClass, LastPawn->GetActorLocation(),
FRotator(0.0f, LastPawn->GetActorRotation().Yaw, 0.0f), Param);
if (NextPlayerPawn)
{
OnPossess(NextPlayerPawn); /*控制新的角色*/
NextPlayerPawn->DisableInput(this); /*展示不能控制,等变身完毕*/
/*暂时隐藏,等变身完毕再显示*/
NextPlayerPawn->SetActorHiddenInGame(true);
NextPlayerPawn->GetRootComponent()->SetVisibility(false, true);
}
if (ShapeshiftList[ShapeshiftIndex].VFX && LastPawn && NextPlayerPawn)
{
if (UNiagaraComponent * NiagaraComponent =
UNiagaraFunctionLibrary::SpawnSystemAtLocation(GetWorld(), ShapeshiftList[ShapeshiftIndex].VFX, LastPawn->GetActorLocation()))
{
USkeletalMeshComponent* MeshAComp = LastPawn->FindComponentByClass<USkeletalMeshComponent>();
USkeletalMeshComponent* MeshBComp = NextPlayerPawn->FindComponentByClass<USkeletalMeshComponent>();
if (MeshAComp && MeshBComp)
{
UNiagaraFunctionLibrary::OverrideSystemUserVariableSkeletalMeshComponent(NiagaraComponent, TEXT("MeshA"), MeshAComp);
UNiagaraFunctionLibrary::OverrideSystemUserVariableSkeletalMeshComponent(NiagaraComponent, TEXT("MeshB"), MeshBComp);
}
}
}
FTimerHandle DelayControlHandle;
GetWorld()->GetTimerManager().SetTimer(DelayControlHandle,
this, &ARPlayerController::DelayControlNewPawn, ShapeshiftList[ShapeshiftIndex].Delay, false);
}
}
}
}
}
这里我建了一个变身的接口 IShapeshiftInterface
,里面很简单:
UINTERFACE(MinimalAPI)
class UShapeshiftInterface : public UInterface
{
GENERATED_BODY()
};
/**
* 变身接口
*/
class R_API IShapeshiftInterface
{
GENERATED_BODY()
// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
virtual bool CanShapeshift() const { return true; }
/*立即停止运动*/
virtual void StopMovementImmediately() = 0;
};
所有玩家角色类继承实现一下这些接口,比如 CanShapeshift
,角色可能在某些状况下不能变身,可以实现时判断一下,还有 StopMovementImmediately
接口,在变身的时候,是要调用OnPossess
函数,丢失对旧角色的控制,但如果玩家当时正在跑,如果不手动停掉移动,角色会在原地播放踏步动画,而变身可能变的都是 ACharacter
,也可能是变身能飞的 APawn
,所以交由子类实现运动的制停。
ShapeshiftIndex
是当前角色的序号,在 ShapeshiftList
中提前配置好了变身队列,每次变身从数组 ShapeshiftList
中获取当前变身的数据。这个数组是我定义的结构体 FPlayerPawn
:
里面是给策划配置的变身数据,包括对应的 APawn
的 UClass
,还有变身的特效,角色前面做好的那个,目前就做这一个吧,按惯例肯定有不同角色变身要不同特效的需求。然后是变身间隔 Delay
,因为有一个特效播放的时间,这段时间就禁掉输入,等待变身完毕。变身的时候先把老角色碰撞关闭,避免生成角色因为碰撞生成错误,调 StopMovementImmediately
将速度归零,禁掉当前的输入,避免再次按下变身,当然如果还想要接收别的输入就不要这么粗暴的禁。然后是生成新角色,调OnPossess
控制新角色,但暂时也禁掉输入,模型也暂时隐藏,等变身特效播放结束,因为我们这模型根组件上挂了一个描边的黑色模型,所以光设置 Actor
的隐藏不够,所以这里还要将根组件以下的都隐藏。
然后就是播放特效了,在老角色的位置生成特效,然后使用UNiagaraFunctionLibrary::OverrideSystemUserVariableSkeletalMeshComponent
将新老角色的模型组件传给 Niagara 的参数,这样特效就能从新旧角色的模型上发射和吸附了。
然后设一个结束的计时器,在 DelayControlNewPawn
里恢复角色的控制,并且销毁旧角色。