2012年7月16日 星期一

優化AnimTrail

AnimTrail讓粒子跟隨動作拉出拖曳效果,可以用來製作武器揮動的特效。它需要指定一個資料型別為AnimTrail的粒子系統,並且在動作資料AnimSequence加上AnimNotify_Trails來指定特效的起始和結束時間。在AnimNotify_Trails上還要指定三個跟隨骨架移動的socket名稱,用來定位粒子。在編輯AnimNotify_Trails時,就會以指定的更新頻率取樣socket位置並且記錄下來,以供播放特效時內插粒子多邊形的位置。

在使用AnimTrail時,我發現幾個不盡理想的地方。我把其中兩個的問題和解法記錄在此,以供參考。

已建粒子系統元件的判斷方法


AnimNotify_Trails觸發後會建立一個粒子系統元件放在Actor身上,之後再觸發時會重覆利用先前建立的粒子系統元件。但是判斷粒子系統元件是否已經建立時,僅檢查Actor身上是否有粒子系統元件的粒子系統跟AnimNotify_Trails指定的是一樣的。這造成如果有動作同時有兩個AnimNotify_Trails使用同一個粒子系統(例如雙刀各拖一條刀光),那這兩個AnimNotify_Trails同時都會使用Actor身上的同一個粒子系統元件,因此導致bug。

解決方法很簡單,改變AnimNotify_Trails尋找已建粒子系統元件的方法,讓它會根據不同AnimNotify_Trails建立不同粒子系統元件即可。以下程式碼示範如何修改,為了不直接變更引擎程式,另外自訂粒子系統元件和動作事件。

自訂的粒子系統元件會記錄建立它的動作事件:
class MyTrailParticleComponent extends ParticleSystemComponent
    native(Particle);
    
var transient MyAnimNotify_Trails TrailNotify;
本來只需要修改尋找已建粒子系統元件的方法和粒子系統元件的型別,不過HandleNotify()和GetPSysComponent()不是虛擬函式,所以需要覆載呼叫它們的Notify()、NotifyTick()、NotifyEnd()這三個虛擬函式才能在子類別修改。
class MyAnimNotify_Trails extends AnimNotify_Trails
    native(Anim);
    
cpptext
{
    class UMyTrailParticleComponent* GetPSysComponent(class USkeletalMeshComponent* SkelComp);
    void HandleNotify(class UAnimNodeSequence* NodeSeq, ETrailNotifyType NotifyType);
    
    // AnimNotify interface
    virtual void Notify(class UAnimNodeSequence* NodeSeq)
    {
        AnimNodeSeq = NodeSeq;
        CurrentTime = LastStartTime;
        TimeStep = 0.f;
        
        HandleNotify(NodeSeq, TrailNotifyType_Start);
    }
    
    virtual void NotifyTick(class UAnimNodeSequence* NodeSeq, FLOAT AnimCurrentTime, FLOAT AnimTimeStep, FLOAT TotalDuration)
    {
        AnimNodeSeq = NodeSeq;
        CurrentTime = AnimCurrentTime;
        TimeStep = AnimTimeStep;
        
        HandleNotify(NodeSeq, TrailNotifyType_Tick);
    } 
    
    virtual void NotifyEnd(class UAnimNodeSequence* NodeSeq, FLOAT AnimCurrentTime)
    {
        AnimNodeSeq = NodeSeq;
        TimeStep = CurrentTime - EndTime;
        CurrentTime = AnimCurrentTime;
        
        HandleNotify(NodeSeq, TrailNotifyType_End);
    }        
}
主要要改寫的函式是HandleNotify()和GetPSysComponent():
UMyTrailParticleComponent* UMyAnimNotify_Trails::GetPSysComponent(USkeletalMeshComponent* SkelComp)
{
    if( SkelComp )
    {
        for(INT i = 0; i < SkelComp->Attachments.Num(); i++)
        {
            UMyTrailParticleComponent* PSysComp = Cast<UMyTrailParticleComponent>( SkelComp->Attachments(i).Component );
            if( PSysComp && PSysComp->TrailNotify == this )
            {
                return PSysComp;
            }            
        }
    }
}

void UMyAnimNotify_Trails::HandleNotify(UAnimNodeSequence* NodeSeq, ETrailNotifyType NotifyType)
{
...
    UMyTrailParticleComponent* PSysComp = GetPSysComponent( NodeSeq->SkelComponent );
    if( ! PSysComp && NotifyType == TrailNotifyType_Start )
    {
        PSysComp = ConstructObject<UMyTrailParticleComponent>( UMyTrailParticleComponent::StaticClass(), NodeSeq->SkelComponent );
        PSysComp->TrailNotify = this;
...
}

不檢查ControlEdgeName


建立AnimTrial用的粒子系統時,需要指定ControlEdgeName欄位,這個名稱必須和AnimNotify_Trails的ControlPointSocketName一樣。所以為了給不同socket用,必須複製同樣的粒子系統來修改這個欄位。這個限制顯然相當不便,幸好把它拿掉並不難。

主要是把FParticleAnimTrailEmitterInstance類別的TrailsNotify()和TrailsNotify_UpdateData()這兩個函式在開頭的檢查拿掉即可。以下程式碼示範如何修改,為了不直接變更引擎程式,另外自訂粒子發射器實例和對應的粒子模組。

為了使用自訂的粒子發射器實例,需要自訂一個粒子模組,才能在粒子編輯器中使用:
class MyParticleModuleTypeDataAnimTrail extends ParticleModuleTypeDataAnimTrail
    native(Particle);
    
cpptext
{
    virtual class FParticleEmitterInstance* CreateInstance( UParticleEmitter* Emitter, UParticleSystemComponent* Component ); 
}
自訂的粒子模組只是用來建立自訂的粒子發射器實例:
FParticleEmitterInstance* UMyParticleModuleTypeDataAnimTrail::CreateInstance( UParticleEmitter* Emitter, UParticleSystemComponent* Component )
{
    FMyParticleAnimTrailEmitterInstance* Instance = new FMyParticleAnimTrailEmitterInstance;
    check(Instance);
    Instance->InitParameters( Emitter, Component );
    return Instance;
}
粒子發射器實例並非繼承自UObject,完全用C++來寫,所以不用寫相關的UnrealScript。自訂的粒子發射器實例要修改TrailsNotify()和TrailsNotify_UpdateData()函式,拿掉一開始檢查ControlEdgeName的if區塊。
class FMyParticleAnimTrailEmitterInstance : public FParticleAnimTrailEmitterInstance
{
public:

    DECLARE_PARTICLEEMITTERINSTANCE_TYPE( FMyParticleAnimTrailEmitterInstance, FParticleAnimTrailEmitterInstance );
    
    virtual void TrailsNotify( const UAnimNotify_Trails* AnimNotifyData );
    virtual void TrailsNotify_UpdateData( const UAnimNotify_Trails* AnimNotifyData );
};

IMPLEMENT_PARTICLEEMITTERINSTANCE_TYPE( FMyParticleAnimTrailEmitterInstance )

void FMyParticleAnimTrailEmitterInstance::TrailsNotify( const UAnimNotify_Trails* AnimNotifyData )
{
    ...
    //if( AnimNotifyData->ControlPointSocketName != TrailTypeData->ControlEdgeName )
    //{
    //    return;
    //}
    ...
}

void FMyParticleAnimTrailEmitterInstance::TrailsNotify_UpdateData( const UAnimNotify_Trails* AnimNotifyData )
{
    ...
    //if( AnimNotifyData->ControlPointSocketName != TrailTypeData->ControlEdgeName )
    //{
    //    return;
    //}
    ...
}
記得還要在Descriptions.int設定MyParticleModuleTypeDataAnimTrail在編輯器的名稱,不然就會顯示這個很長的類別名稱。

沒有留言:

張貼留言