Kismet

Kismet

kismet 은 언리얼엔진3의 기능을 graph 형태로 쉽게 에디팅할 수 있게 함으로써, 디자이너 또는 아티스트가 손쉽게 게임 컨텐츠를 생산해낼 수 있도록 해주는 도구이다.

Actor 의 생성/소멸/이동은 물론이고 변수를 활용하여 조건/분기를 graph 의 node 조합으로 구현 가능하고, 컨텐츠 특화적인 node 또한 손쉽게 추가할 수 있다.

특징

  • kismet 은 레벨map에 귀속(저장)되는 리소스이다. 1)
  • kismet 은 크게 Action/Variable/Event 으로 구분되며 각각은 그에 걸맞는 하위 카테고리를 가지고 있다.
  • kismet 으로 가급적 게임중 여러 인스턴스가 생성될 수 있는 시퀀스는 만들지 말자. kismet 은 이렇게 여러개의 인스턴스가 생성되어 프로세싱되는 것에대해 취약한것 같다.

종류

위에 설명했듯이 kismet 을 이루는 노드는 크게 Action/Variable/Event 로 구분되며 각각에 대해 살펴본다.

Action

Actor -> Stream In Textures

특정 Texture 를 미리 로드해 두고, 일정 시간동안 Stream out 되는 것을 방지한다.

특히 Cinematic 모드에서 유용하다. 연출씬에 사용되는 Texture 를 미리 로드해 둠으로써, 실제 로드 시점에 갑작스럽게 로딩되어 Mipmap 이 미리보이는 등의 엉성한 연출을 방지할 수 있다.

Non block 노드이므로 활성화 된 후, 바로 다음 노드로 진행된다.

크게 다음과 같은 기준으로 스트리밍을 해준다.

  1. 특정 Actor 와의 거리에 따른 스트리밍. ( Actor 를 Location 핀에 연결 )
  2. 특정 Actor 가 사용하고 있는 모든 Texture 를 미리 로드. ( Actor 를 Target 핀에 연결 )
  3. 특정 MaterialInstance 를 미리 로드 ( 대상 MaterialInstance 를 Force Materials 배열에 추가 )
SeqAct_StreamInTexture.uc
class SeqAct_StreamInTextures extends SeqAct_Latent
    native( Sequence );
 
...
 
var() float    Seconds;                        // 몇 초동안 강제로 Texture 로드 상태를 유지할 것인가?
 
...
 
var() array<Object>  LocationActors;           // 거리 기반 스트리밍 할 타겟 Actor 리스트. 상기 1번에 해당
var() array<MaterialInstance> ForceMaterials;  // 강제로 로드할 MaterialInstance 객체. 상기 3번에 해당
var() array<Object>  Targets;                  // SequenceAction 클래스의 맴버변수. 이 Actor 가 사용하는 MeshComponent 의 Texture 가 미리 로드된다. 상기 2번에 해당
 
... 
 
var( CinematicMipLevels ) const TextureGroupContainer  CinematicTextureGroups;  // 미리 로드할 Texture 그룹.
 
...
UnSequence.cpp
...
UBOOL USeqAct_StreamInTextures::UpdateOp( FLOAT DeltaTime )
{
    ...
 
    // ForceMaterials 배열에 지정된 것 미리로드 수행. 상기 3번에 해당
    if ( InputLinks( 0 ).bHasImpulse )
    {
        // ForceMaterials 배열에 지정된 것들을 미리로드. 클라이언트 복제도 같이 수행된다.
    }
    else if ( InputLinks( 1 ).bHasImpulse )
    {
        // ForceMaterials 배열에 지정된 것들 미리로드 해제. 역시 이것도 클라복제
    }
 
    // 거리 기반 스트리밍 수행. 상기 1번에 해당
    if ( StreamingActive )
    {
        ...
 
        GStreamingManager->AddViewSlaveLocation( Target->Location );
 
        ...
    }
 
    // 대상 객체가 사용하고 있는 Texture 미리 로드 수행. 클라 복제도 수행. 상기 2번에 해당
 
    ...
 
    AActor* Target = Cast<AActor>( Targets(TargetIndex) );
    if ( Target )
    {
        ...
 
        Target->PrestreamTextures( Seconds, bStreamingActive, SelectedCinematicTextureGroups );
 
        ... // 클라 복제도 수행
    }
}

Event -> Attach To Event

특정 Event를 특정 Actor 에 귀속시킨다.

Event → Level Loaded 나 Event → Level Startup 과 같이 글로벌하게 발생하는 Event 를 제외하고 특정 Actor 를 대상으로 하여 발생하는 Event 들2)은 Event → Attach To Event 를 사용하여 귀속시켜주지 않으면 그 대상에게 발생하지 않는다.

따라서 명시적으로 Event → Attach To Event 를 이용하여 대상에게 붙여줘야 한다.

Event 를 특정 Actor 에 귀속시키는 코드
// UnSequence.cpp
void USeqAct_AttachToEvent::Activated()
{
    ...
 
    // 대상 중 Actor 타입을 찾는다.
    TArray<UObject**> ObjVars;
    GetObjectVars( ObjVars, TEXT("Attachee") );
 
    TArray<AActor*> targets;
    for ( INT Idx = 0; Idx < ObjVars.Num(); Idx++ )
    {
        AActor* actor = Cast<AActor>( *(ObjVars( Idx ) ) );
        if ( actor != NULL )
        {
            ...
            targets.AddUniqueItem( actor );
            ...
        }
    }
 
    // 귀속시킬 Event 를 구한다.
    TArray<USequenceEvent*> Events;
    for ( INT Idx = 0; Idx < EventLinks( 0 ).LinkedEvents.Num(); Idx++ )
    {
        USequenceEvent* Event = EventLinks( 0 ).LinkedEvents( Idx );
        if ( Event != NULL )
        {
            Events.AddUniqueItem( Event );
        }
    }
 
    // 모든 대상 Actor 를 돌며 모든 Event 를 귀속시켜준다.
    if (
        targets.Num() > 0 &&
        Events.Num() > 0
    )
    {
        ...
        Events( EventIdx )->DuplicateEvts.AddItem( evt );    // 프로토타입에 복제된 자신의 인스턴스를 넣고
        evt->Originator = targets( Idx );                    // Event 인스턴스의 Actor(소유자)를 넣고
        targets( Idx )->GeneratedEvents.AddItem( evt );      // Actor 에 Event 를 귀속시키고
        evt->Originator->eventReceivedNewEvent( evt );       // Actor 에 Event 를 받았다는 신호를 호출한다.
        ...
    }
}

Sound

Set Sound Mode

SoundMode 를 적용한다. SoundMode 에 대한 더 자세한 설명은 여기서 참조.

이 노드가 하는 일은 심플하다. 노드가 활성화될 때 PlayerController 의 OnSetSoundMode 를 호출한다.

SeqAct_SetSoundMode.uc
...
 
var() SoundMode SoundMode;    // 이 노드가 활성화될 때 적용할 SoundMode 객체. PlayerController 에서 참조된다.
 
...
 
event Activated()
{
    local PlayerController PC;
 
    PC = GetWorldInfo().GetALocalPlayerController();
    if ( PC != None )
    {
        PC.OnSetSoundMode( self );
    }
}
 
...

그리고 PlayerController 는 AudioDevice 에 접근하여 SoundMode 를 세팅한다.

PlayerController.uc
function OnSetSoundMode( SeqAct_SetSoundMode Action )
{
    local AudioDevice Audio;
 
    Audio = class'Engine'.static.GetAudioDevice();
    if ( Audio != None )
    {
        if ( Action.InputLinks[0].bHasImpulse && ( Action.SoundMode != None ) )
        {
            Audio.SetSoundMode( Action.SoundMode.Name );   // Start 핀에 신호가 왔다면 SoundMode 세팅
        }
        else
        {
            Audio.SetSoundMode( 'Default' );               // Stop 핀에 신호가 왔다면 Default 모드로 세팅
        }
    }
}

Matinee

Matinee 를 재생한다.

이와 관련된 스크립트 코드는 SeqAct_Interp.uc 에 정의되어 있다.

SeqAct_Interp.uc
...
 
cpptext
{
    ...
 
    void Play();              // 재생
    void Reverse();           // 역 재생
    void Pause();             // 정지
    void ChangeDirection();   // 현재 재생방향을 바꿈
 
    ...
}
 
...

위를 보면 알겠지만 재생에 필요한 기본 함수가 있고, 그 나머지는 이를 위한 도움함수들이다.

노드를 더블클릭하면 Matinee 에디터 화면이 나오는데, 여기서 Group 을 편집하면 Kismet의 Matinee노드에 그 Group 에 해당하는 Value 핀이 생성이 된다. 이 Value 핀은 Target 의 의미로써, 해당 Group 에 있는 Track 을 누구에 적용할 것이지를 정하는 기능을 한다.

글고 주의할 점은 Float Material Param Track 을 제대로 사용하려면 대상이 MaterialInstanceActor 이어야 한다. 3)

이 노드가 Matinee 와 연관지어져 초기화 되는 로직은 아래와 같다.

// UnInterpolation.cpp
void USeqAct_Interp::Activated()
{
    ...
 
    // 현재 노드 초기화. 여기서 Matinee 재생에 필요한 Data 와 InterpGroup 및 InterpTrack 을 초기화 하고 Instance 를 생성한다.
    InitInterp();
 
    // 각 핀에서 신호가 왔다면 적절하게 Matinee 운동을 수행한다.
    if ( InputLinks(0).bHasImpulse )
    {
        Play();
    }
    else if ( InputLinks(1).bHasImpulse )
    {
        Reverse();
    }
    else if ( InputLinks(4).bHasImpulse )
    {
        ChangeDirection();
    }
 
    ...
 
    // 서버라면, 이를 client 측으로 복제할 Actor 를 생성한다.
    ...
}

대략 위의 내용으로 초기화

Condition

Variable

Event

쉽게 말해 kismet 노드의 스타팅 포인트라고 말할 수 있다. 일종의 Trigger 역할을 한다.

Level Loaded

레벨이 모두 로드되고 나서 발생하는 이벤트

세가지 Output Link 가 있다.

  • Loaded and Visible
  • Beginning of Level
  • Level Reset
Loaded and Visible

맵이 로드되고 발생하는 이벤트

// UnGame.cpp
UBOOL UGameEngine::LoadMap( const FURL& URL, UPendingLevel* Pending, FString& Error )
{
    ...
    GWorld->BeginPlay( URL );  // 맵 로딩 시 게임플레이를 위한 요소 초기화
    ...
}
 
// UnWorld.cpp
void UWorld::BeginPlay( const FURL& InURL, UBOOL bResetTime )
{
    ...
    GetGameSequence()->BeginPlay();  // 게임 시퀀스 초기화
    ...
}
 
// UnSequence.cpp
void USequence::BeginPlay()
{
    ...
    USeqEvent_LevelLoaded* Evt = Cast<USeqEvent_LevelLoaded>( SequenceObjects( Idx ) );
    if ( Evt != NULL &&
         Evt->OutputLinks.Num() > 0 &&
         Evt->OutputLinks( 0 ).Links.Num() > 0 )
    {
        TArray<INT> ActivateIndices;
        ActivateIndices.AddItem( 0 );
        Evt->CheckActivate( GWorld->GetWorldInfo(), NULL, 0, &ActivateIndices );
    }
    ...
}
 
// UnSequence.cpp
void USequenceEvent::CheckActivate( ... )
{
    ...
    ActivateEvent( ... );
    ...
}
Beginning of Level

게임이 시작될 때 발생하는 이벤트

정확히는 GameInfo.StartMatch() 안에서 WorldInfo.NotifyMatchStarted() 를 호출함으로써 발생한다.

:!: 단 GameInfo.StartMatch() 는 중간에 접속한 클라이언트에서는 호출되지 않는 듯 하니 주의하여 사용하도록 한다.

// UnLevAct.cpp
void AWorldInfo::NotifyMatchStarted( UBOOL bShouldActivateLevelStartupEvents, UBOOL bShouldActivateLevelBeginningEvents, UBOOL bShouldActivateLovelLoadedEvents )
{
    ...
    Seq->NotifyMatchStarted( bShouldActivateLevelStartupEvents, bShouldActivateLevelBeginningEvents, bShouldActivateLovelLoadedEvents );
    ...
}
 
// UnSequence.cpp
void USequence::NotifyMatchStarted( UBOOL bShouldActivateLevelStartupEvents, UBOOL bShouldActivateLevelBeginningEvents, UBOOL bShouldActivateLovelLoadedEvents )
{
    ...
    USeqEvent_LevelLoaded* LoadedEvt = Cast<USeqEvent_LevelLoaded>( SequenceObjects( Idx ) );
    if ( LoadedEvt != NULL &&
         LoadedEvt->OutputLinks.Num() > 1 &&
         LoadedEvt->OutputLinks( 1 ).Links.Num() > 0 )
    {
        TArray<INT> ActivateIndices;
        ActivateIndices.AddItem( 1 );
        LoadedEvt->CheckActivate( GWorld->GetWorldInfo(), NULL, 0, &ActivateIndices );
    }
    ...
}
 
// UnSequence.cpp
void USequenceEvent::CheckActivate( ... )
{
    ...
    ActivateEvent( ... );
    ...
}
Level Reset

엔진이 기본적으로 호출을 지원해주지는 않는 듯 하다. (코드상 확인)

필요에 따라 직접 구현해주면 될듯

Level Startup

상기 Level Loaded 섹션의 Beginning of Level 와 쓰임새가 동일하다. 당연히 이벤트 호출시점도 동일

Game Ended

Player -> Player Spawned

플레이어가 스폰될 때 발생하는 이벤트.

상기 Level Loaded 의 Beginning of Level 과 마찬가지로 :!: GameInfo.StartMatch() 에 의해 발생하므로 클라이언트에서는 호출되지 않는것 같다.

// GameInfo.uc
function RestartPlayer( Controller NewPlayer )
{
    ...
 
    if ( WorldInfo.GetGameSequence() != None )
    {
        // SeqEvent_PlayerSpawned 이벤트를 모두 찾아서 발동시킨다.
        WorldInfo.GetGameSequence().FindSeqObjectsByclass( class'SeqEvent_PlayerSpawned', TRUE, Events );
        for ( Idx = 0; Idx < Events.Length; Idx++ )
        {
            SpawnedEvent = SeqEvent_PlayerSpawned( Events[Idx] );
            if (
                SpawnedEvent != None &&
                SpawnedEvent.CheckActivate( NewPlayer, NewPlayer )
            )
            {
                SpawnedEvent.SpawnPoint = startSpot;
                SpawnedEvent.PopulateLinkedVariableValues();
            }
        }
    }
}

코드

공통

  • kismet 을 아우르는 메인로직 시작
    // UnLevTic.cpp
    void UWorld::Tick( ... )
    {
        ...
        if ( ... )
        {
            ...
            if ( !Info->bPlayersOnly && GIsGame )
            {
                ...
                for ( INT SeqIdx = 0; SeqIdx < CurrentLevel->GameSequences.Num(); SeqIdx++ )
                {
                    SCOPE_CYCLE_COUNTER( STAT_KismetTime );
     
                    if ( CurrentLevel->GameSequences( SeqIdx ) != NULL )
                    {
                        CurrentLevel->GameSequences( SeqIdx )->UpdateOp( DeltaSeconds );
                    }
                }
            }
            ...
        }
        ...
    }
  • 런타임에서 graph 의 pin 에 연결되어 있는 node 를 실제 native variable 에 대입하는 로직
    // UnSequence.cpp
    void USequenceOp::GetBoolVars( TArray<UBOOL*> &outBools, const TCHAR *InDesc ) const
    {
        GetOpVars<UBOOL, USeqVar_Bool>( outBools, inDesc );
    }
     
    void USequenceOp::GetIntVars( TArray<INT*> &outInts, const TCHAR *inDesc ) const
    {
        GetOpVars<INT, USeqVar_Int>( outInts, inDesc );
    }
     
    void USequenceOp::GetFloatVars( TArray<FLOAT*> &outFloats, const TCHAR *inDesc ) const
    {
        GetOpVars<FLOAT, USeqVar_Float>( outFloats, inDesc );
    }
     
    void USequenceOp::GetVectorVars( TArray<FVector*> &outVectors, const TCHAR *inDesc ) const
    {
        GetOpVars<FVector, USeqVar_Vector>( outVectors, inDesc );
        GetOpVars<FVector, USeqVar_Object>( outVectors, inDesc );
    }
     
    void USequenceOp::GetStringVars( TArray<FString*> &outStrings, const TCHAR *inDesc ) const
    {
        GetOpVars<FString, USeqVar_String>( outStrings, inDesc );
    }
     
    void USequenceOp::GetObjectVars( TArray<UObject**> &outObjects, const TCHAR *inDesc ) const
    {
        ...
    }

Event

  • 발동

참조

1) Sub Sequence 로 제작된 내용은 Sequence 형태의 리소스로 패키지 안에 export 할 수 있다. 여기참조
2) Pawn → Death, Player → Player Spawned 등이 이에 해당한다.
3) UnInterpolation.cpp 의 UInterpTrackInstFloatMaterialParam::InitTrackInst 함수 참조