C++幼女先輩

プログラミング成分多め

Unreal4.13 リプレイ機能調査(キルカメラ) Part 1

はじめに

今回は調査。不確定な要素もあるかもしれない 有識者のツッコミ欲しい所。

UnrealEngineにはリプレイ機能がある。 これは 終了したゲームを例えばファイルから再現をさせる機能である ムービーと違って、後から視点を変更したり出来る

docs.unrealengine.com

ドキュメントを読むかぎりでは UE4.13ではリプレイ機能が3種類存在する

Null Streamer

NULL Streamer はホスト マシンからイベントを直接ディスクに記録します。シングル プレイヤー ゲームや、リプレイをホスト プレイヤー自身のマシンにローカルに保持するゲームに最適です

とあるように、ディスクに保存し、ゲーム終了後にファイルを読み込んで リプレイを見る事が可能のようだ 機能的に、リプレイの記録終了後にしか読み込めない

Memory Streamer

Memory Streamer はクライアント マシンで実行し、メモリにデータを保存します。スポーツ作品のビデオ判定機能やシューティング ゲームのキルカメラに最適です。

モリー上にリプレイを保存し、任意のタイミングで再生可能なので スポーツゲームの得点シーンのリプレイや、FPSのキルカメラで 倒された瞬間の相手のカメラを再生したり リアルタイムで任意の状況を再生できそう

HTTP Sreamer

最後に HTTP Streamer はリプレイ データを LAN またはインターネット上の 2 台めのマシンに送ります。専用サーバーのゲームやプレイヤーに反応し続けながら大勢の観戦者に向けてストリーミングする必要があるゲームに適しています。

おそらく、他のサーバにリプレイデータを送信し、複数のサーバが リプレイをリレー出来るものだと思う。 ゲーム大会で観戦者が大勢いる際に、リプレイサーバを増やす事で容易にスケーリング可能になると思われる

気になる所

リプレイ システムは C++ コードから使用できます。主に、UGameInstance クラスおよび UWorld クラス経由で、またはコンソール コマンドやコマンドライン引数を使って使用できます。C++/Blueprint API を統合したものを構築中であり、エンジンの将来のバージョンでリリース予定です。

つまり、今のリプレイコードは プレビュー版で、今後 ちゃんとしたものをリリースする、作成途中の可能性がある 少なくともBluePrintのインタフェースは存在しない

よろしい、ならば開発だ

と勇んだものの、公式ドキュメントは上記ページのみで、ネットにサンプルもみつけられなかった ので、1から調査しなければならない おそらく今回は、コードを追っかけて色々な無駄をすることになるが ドキュメント読めない私の 問題解決方法をメモがわりに

Unreal Reference

リプレイに直接関連しそうなReferenceは下記

NetworkReplayStreaming NetworkReplayStreaming | Unreal Engine API Reference

NullNetworkReplayStreaming NullNetworkReplayStreaming | Unreal Engine API Reference

HttpNetworkReplayStreaming HttpNetworkReplayStreaming | Unreal Engine API Reference

NullとHttpはわかるが Memory Replayが無い・・・ のでコードをみよう

コードを書きましょう

Source/Runtime/NetworkReplayStreaming以下に NetworkReplayStreaming、NullNetworkReplayStreaming、InMemoryNetworkReplayStreaming、HttpNetworkReplayStreaming が存在する。 InMemoryNetworkReplayStreamingはReferenceにもまだ のってないようだ

また、コードをみると いずれもUPROPERTYがついておらず、BluePrintから使われる想定になっていないことが見て取れる

どこに何を書けばいいか手探り状態なので、とりあえず Characterに書いてみようとおもう

モジュール追加

NetworkReplayStreaming はモジュールなので、まずはビルドスクリプトに追加しよう

プロジェクト名.Build.csのPublicDependencyModuleNamesに "NetworkReplayStreaming" を追加ですね。

色々叩いてみよう

新規プロジェクトを作り、とりあえず Characterクラスを派生し AMyCharacterをつくり 調査しよう 最も機能の少なそうな NullNetworkReplayStreamingを。

BeginPlay あたりにとりあえず Factoryする

 FNullNetworkReplayStreamingFactory factory;
    TSharedPtr<  INetworkReplayStreamer > networkReplayStreamer_ = factory.CreateReplayStreamer();

動くかは知らないがビルド通った そして おそらくリプレイ録画開始メソッドに近いと思われる StartStreamingを呼ぶことを考える しかし引数が多く、最後にコールバックが必要。 コールバックも関数オブジェクトでもなく FOnStreamReadyDelegate型

エンジンコードを検索し StartStreamingを呼ぶ手本がないか探す

DemoNetDriver.cpp

DemoNetDriver.cppにそれはあった

const TCHAR* const StreamerOverride = URL.GetOption(TEXT("ReplayStreamerOverride="), nullptr);
        ReplayStreamer = FNetworkReplayStreaming::Get().GetFactory(StreamerOverride).CreateReplayStreamer();

GetFactoryの引数が不気味だ。Engine.iniの設定により 使うクラスがかわりそうだ 追っていくと

INetworkReplayStreamingFactory& FNetworkReplayStreaming::GetFactory(const TCHAR* FactoryNameOverride)
{
    FString FactoryName = TEXT( "NullNetworkReplayStreaming" );

    if (FactoryNameOverride == nullptr)
    {
        GConfig->GetString( TEXT( "NetworkReplayStreaming" ), TEXT( "DefaultFactoryName" ), FactoryName, GEngineIni );
    }
    else
    {
        FactoryName = FactoryNameOverride;
    }

    // See if we need to forcefully fallback to the null streamer
    if ( !FModuleManager::Get().IsModuleLoaded( *FactoryName ) )
    {
        FModuleManager::Get().LoadModule( *FactoryName );
    
        if ( !FModuleManager::Get().IsModuleLoaded( *FactoryName ) )
        {
            FactoryName = TEXT( "NullNetworkReplayStreaming" );
        }
    }

    return FModuleManager::Get().LoadModuleChecked< INetworkReplayStreamingFactory >( *FactoryName );
}

iniで値をかえているようだ デフォルトでは NullNetworkReplayStreaming を使うようになっているので、このあたりを変更すれば InMemoryやHTTPを使うことが出来そう Iniファイルで変更するか、

bool UDemoNetDriver::InitConnect( FNetworkNotify* InNotify, const FURL& ConnectURL, FString& Error )

を呼び出す時に ConnectURLに ReplayStreamerOverride=好きなStreamingクラス名  を指定すれば出来ると期待

 ReplayStreamer->StartStreaming( 
        DemoFilename, 
        FString(),      // Friendly name isn't important for loading an existing replay.
        UserNames, 
        false, 
        FNetworkVersion::GetReplayVersion(), 
        FOnStreamReadyDelegate::CreateUObject( this, &UDemoNetDriver::ReplayStreamingReady ) );

これを参考にして呼んでみようかと思うも、このDemoNetDriverが何かを軽くしらべる。 ソースを軽く読んだところ、サーバとの通信の間?にたち、そこでリプレイデータを保存したりなんたらしてそうで NetworkReplayStreaming を直接叩かず、DemoNetDriverを操作する方が正しいのではないかとおもい調べる

UDemoNetDriver::InitConnect を呼んでいる奴を検索

UGameInstance

なんとなく 真犯人な雰囲気がする

void UGameInstance::PlayReplay(const FString& Name, UWorld* WorldOverride, const TArray<FString>& AdditionalOptions)
{
    UWorld* CurrentWorld = WorldOverride != nullptr ? WorldOverride : GetWorld();

    if ( CurrentWorld == nullptr )
    {
        UE_LOG( LogDemo, Warning, TEXT( "UGameInstance::PlayReplay: GetWorld() is null" ) );
        return;
    }

    if ( CurrentWorld->WorldType == EWorldType::PIE )
    {
        UE_LOG( LogDemo, Warning, TEXT( "UGameInstance::PlayReplay: Function called while running a PIE instance, this is disabled." ) );
        return;
    }

    CurrentWorld->DestroyDemoNetDriver();

    FURL DemoURL;
    UE_LOG( LogDemo, Log, TEXT( "PlayReplay: Attempting to play demo %s" ), *Name );

    DemoURL.Map = Name;
    
    for ( const FString& Option : AdditionalOptions )
    {
        DemoURL.AddOption(*Option);
    }

    const FName NAME_DemoNetDriver( TEXT( "DemoNetDriver" ) );

    if ( !GEngine->CreateNamedNetDriver( CurrentWorld, NAME_DemoNetDriver, NAME_DemoNetDriver ) )
    {
        UE_LOG(LogDemo, Warning, TEXT( "PlayReplay: failed to create demo net driver!" ) );
        return;
    }

    CurrentWorld->DemoNetDriver = Cast< UDemoNetDriver >( GEngine->FindNamedNetDriver( CurrentWorld, NAME_DemoNetDriver ) );

    check( CurrentWorld->DemoNetDriver != NULL );

    CurrentWorld->DemoNetDriver->SetWorld( CurrentWorld );

    FString Error;

    if ( !CurrentWorld->DemoNetDriver->InitConnect( CurrentWorld, DemoURL, Error ) )
    {
        UE_LOG(LogDemo, Warning, TEXT( "Demo playback failed: %s" ), *Error );
        CurrentWorld->DestroyDemoNetDriver();
    }
    else
    {
        FCoreUObjectDelegates::PostDemoPlay.Broadcast();
    }
}

AdditionalOptionsに先程の ReplayStreamerOverride=好きなStreamingクラス名

で動きそうな雰囲気

GameInstance.cpp

ほぼ絞れた UGameInstance::PlayReplayの呼び口を探すと2個あった

UGameInstance::StartGameInstance

 // Parse replay name if specified on cmdline
    FString ReplayCommand;
    if ( FParse::Value( Tmp, TEXT( "-REPLAY=" ), ReplayCommand ) )
    {
        PlayReplay( ReplayCommand );
        return;
    }

なんと、プロセスを起動する時に -REPLAY= のコマンドライン引数を使って リプレイファイルを指定して再生可能である

仕様上 NullNetworkReplayStreaming しか使えない そして 今まで調べていたのは リプレイ再生であったことがわかる

UWorld::HandleDemoPlayCommand

本命はこちらである

bool UWorld::Exec( UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar )
{
    if( FParse::Command( &Cmd, TEXT("TRACETAG") ) )
    {
        return HandleTraceTagCommand( Cmd, Ar );
    }
    else if( FParse::Command( &Cmd, TEXT("FLUSHPERSISTENTDEBUGLINES") ) )
    {       
        return HandleFlushPersistentDebugLinesCommand( Cmd, Ar );
    }
    else if (FParse::Command(&Cmd, TEXT("LOGACTORCOUNTS")))
    {       
        return HandleLogActorCountsCommand( Cmd, Ar, InWorld );
    }
    else if (FParse::Command(&Cmd, TEXT("DEMOREC")))
    {       
        return HandleDemoRecordCommand( Cmd, Ar, InWorld );
    }
    else if( FParse::Command( &Cmd, TEXT("DEMOPLAY") ) )
    {       
        return HandleDemoPlayCommand( Cmd, Ar, InWorld );
    }
    else if( FParse::Command( &Cmd, TEXT("DEMOSTOP") ) )
    {       
        return HandleDemoStopCommand( Cmd, Ar, InWorld );
    }
    else if (FParse::Command(&Cmd, TEXT("DEMOSCRUB")))
    {
        return HandleDemoScrubCommand(Cmd, Ar, InWorld);
    }
    else if (FParse::Command(&Cmd, TEXT("DEMOPAUSE")))
    {
        return HandleDemoPauseCommand(Cmd, Ar, InWorld);
    }
    else if (FParse::Command(&Cmd, TEXT("DEMOSPEED")))
    {
        return HandleDemoSpeedCommand(Cmd, Ar, InWorld);
    }
    else if( ExecPhysCommands( Cmd, &Ar, InWorld ) )
    {
        return HandleLogActorCountsCommand( Cmd, Ar, InWorld );
    }
    else 
    {
        return 0;
    }
}

bool UWorld::HandleDemoPlayCommand( const TCHAR* Cmd, FOutputDevice& Ar, UWorld* InWorld )
{
    FString Temp;
    const TCHAR* ErrorString = nullptr;

    if ( !FParse::Token( Cmd, Temp, 0 ) )
    {
        ErrorString = TEXT( "You must specify a filename" );
    }
    else if ( InWorld == nullptr )
    {
        ErrorString = TEXT( "InWorld is null" );
    }
    else if ( InWorld->GetGameInstance() == nullptr )
    {
        ErrorString = TEXT( "InWorld->GetGameInstance() is null" );
    }
    
    if (ErrorString != nullptr)
    {
        Ar.Log(ErrorString);

        if (GetGameInstance() != nullptr)
        {
            GetGameInstance()->HandleDemoPlaybackFailure(EDemoPlayFailure::Generic, FString(ErrorString));
        }
    }
    else
    {
        InWorld->GetGameInstance()->PlayReplay(Temp);
    }

    return true;
}

非常に簡単だった・・・・・

デバッグコマンド DEMOPLAY にファイル名を入れると、リプレイを再生してくれる

つまり、このデバッグコマンドのコードを見れば リプレイの実装が出来そうな予感

エンジンコード色々読んだけど、単純だった予感