kismet 은 언리얼엔진3의 기능을 graph 형태로 쉽게 에디팅할 수 있게 함으로써, 디자이너 또는 아티스트가 손쉽게 게임 컨텐츠를 생산해낼 수 있도록 해주는 도구이다.
Actor 의 생성/소멸/이동은 물론이고 변수를 활용하여 조건/분기를 graph 의 node 조합으로 구현 가능하고, 컨텐츠 특화적인 node 또한 손쉽게 추가할 수 있다.
위에 설명했듯이 kismet 을 이루는 노드는 크게 Action/Variable/Event 로 구분되며 각각에 대해 살펴본다.
특정 Texture 를 미리 로드해 두고, 일정 시간동안 Stream out 되는 것을 방지한다.
특히 Cinematic 모드에서 유용하다. 연출씬에 사용되는 Texture 를 미리 로드해 둠으로써, 실제 로드 시점에 갑작스럽게 로딩되어 Mipmap 이 미리보이는 등의 엉성한 연출을 방지할 수 있다.
Non block 노드이므로 활성화 된 후, 바로 다음 노드로 진행된다.
크게 다음과 같은 기준으로 스트리밍을 해준다.
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 그룹. ...
... 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를 특정 Actor 에 귀속시킨다.
Event → Level Loaded 나 Event → Level Startup 과 같이 글로벌하게 발생하는 Event 를 제외하고 특정 Actor 를 대상으로 하여 발생하는 Event 들2)은 Event → Attach To Event 를 사용하여 귀속시켜주지 않으면 그 대상에게 발생하지 않는다.
따라서 명시적으로 Event → Attach To Event 를 이용하여 대상에게 붙여줘야 한다.
// 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 를 받았다는 신호를 호출한다. ... } }
SoundMode 를 적용한다. SoundMode 에 대한 더 자세한 설명은 여기서 참조.
이 노드가 하는 일은 심플하다. 노드가 활성화될 때 PlayerController 의 OnSetSoundMode 를 호출한다.
... var() SoundMode SoundMode; // 이 노드가 활성화될 때 적용할 SoundMode 객체. PlayerController 에서 참조된다. ... event Activated() { local PlayerController PC; PC = GetWorldInfo().GetALocalPlayerController(); if ( PC != None ) { PC.OnSetSoundMode( self ); } } ...
그리고 PlayerController 는 AudioDevice 에 접근하여 SoundMode 를 세팅한다.
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 를 재생한다.
이와 관련된 스크립트 코드는 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 를 생성한다. ... }
대략 위의 내용으로 초기화
쉽게 말해 kismet 노드의 스타팅 포인트라고 말할 수 있다. 일종의 Trigger 역할을 한다.
레벨이 모두 로드되고 나서 발생하는 이벤트
세가지 Output Link 가 있다.
맵이 로드되고 발생하는 이벤트
// 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( ... ); ... }
게임이 시작될 때 발생하는 이벤트
정확히는 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 Loaded 섹션의 Beginning of Level 와 쓰임새가 동일하다. 당연히 이벤트 호출시점도 동일
플레이어가 스폰될 때 발생하는 이벤트.
상기 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(); } } } }
// 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 ); } } } ... } ... }
// 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 { ... }