C++幼女先輩

プログラミング成分多め

何番煎じか不明だが Unreal4.14で Unitychanを読み込む

はじめに

いろいろな人が記事書いてくれているので基本それを見てください たとえば

UE4でゼロからユニティちゃんを表示させるまで - Let's Enjoy Unreal Engine

今回は自分のメモと UE4.14で変更点があると思われるのでチェックです

データのコンバート

上記Blogの通りにします。

UnityChanデータをダウンロード FBXConverterでFBX2013にコンバート

プロジェクト作成

テンプレートより ThirdPartyゲームを選ぶ、BluePrintで。

モデルデータのインポート

コンバートした unitychan.fbx をimportします 上記Blogのとおりに・・

f:id:murasame-labo:20161204150907p:plain

4.14ではメニュー変わってますがとりあえずデフォルトでよさそうな気がします

Warningでましたね・・

f:id:murasame-labo:20161204151211p:plain

マテリアル設定

上記Blogのとおりに・・ テクスチャをimportし、Materialを作成し モデルデータのMaterialを差し替えます チークと目が透過はいってなくおかしいので チークと目の透過設定を行う

f:id:murasame-labo:20161204151235p:plain

ちゃんと表示できましたね!

モーション読み込み

以前読み込んだ時は 寝転んでしまいましたが・・ 果たして大丈夫でしょうか

デフォルトで読み込みすると やはり地面に寝てしまいましたので import時に X軸回転させます

f:id:murasame-labo:20161204152149p:plain

Warningでました・・ f:id:murasame-labo:20161204152210p:plain

が、とりあえずちゃんと表示されたようです

f:id:murasame-labo:20161204152242p:plain

プレイヤーをUnityChanに

デフォルトだと グレーマンですが、UnityChanに変更します

WorldOutlinerの ThirdPersonCharacterを選び、Meshを unitychanに変更

f:id:murasame-labo:20161204152818p:plain

実行すると、Tポーズのままですが UnityChanが動きました

次回は、待機、Jumpあたりのアニメーション (ステートマシン)作ります

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 にファイル名を入れると、リプレイを再生してくれる

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

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

UnrealEngine モジュールを使うときのメモ

必要になった背景

モジュール使ってますか? とりあえず今回 リプレイ機能を調査したかったので

#include "Runtime/NetworkReplayStreaming/NullNetworkReplayStreaming/Public/NullNetworkReplayStreaming.h"

を行いたかったのですが、NullNetworkReplayStreaming.h 内部でコンパイルエラーが出ます

#pragma once

#include "NetworkReplayStreaming.h"
#include "Core.h"
#include "Engine.h"
#include "ModuleManager.h"
#include "UniquePtr.h"
#include "OnlineJsonSerializer.h"
#include "Tickable.h"

当然 内部ヘッダにはパス通ってませんね

パスを通さなければ。。。。

おそらく、プロジェクトパスに Runtime/NetworkReplayStreaming/NullNetworkReplayStreaming/

追加すればビルド通るとおもうけど、それは Unrealの流儀じゃないので 正しい方法で行う

ビルドシステム

UnrealC#コードを使ったビルドシステムがある

プロジェクト名.Build.cs ってのがあるので そこを開く

// Fill out your copyright notice in the Description page of Project Settings.

using UnrealBuildTool;

public class プロジェクト名 : ModuleRules
{
    public プロジェクト名(TargetInfo Target)
    {
        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });

        PrivateDependencyModuleNames.AddRange(new string[] {  });

        // Uncomment if you are using Slate UI
        // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
        
        // Uncomment if you are using online features
        // PrivateDependencyModuleNames.Add("OnlineSubsystem");
        // if ((Target.Platform == UnrealTargetPlatform.Win32) || (Target.Platform == UnrealTargetPlatform.Win64))
        // {
        //      if (UEBuildConfiguration.bCompileSteamOSS == true)
        //      {
        //          DynamicallyLoadedModuleNames.Add("OnlineSubsystemSteam");
        //      }
        // }
    }
}

この PublicDependencyModuleNames.AddRange  の配列が、それなので "NetworkReplayStreaming" を追加してビルドすると OK

さくらVPSで Google G Suiteを使いメールアカウントを作成

メモなのでさらっと

まず、今までは GMailアドレスで仕事をしていたが、ビジネスも大きくしなきゃいけないので murasame-lab.com の、独自ドメインのメールアドレスを作ることにした

さくらVPSを使っているので、メールサーバを建てればよいし、知人と一緒に管理してもいいんだけど とりあえずは便利な外部メールサーバを使うことに

候補はいっぱいあった

Google G Suite

今回これを試用することにした。機能的には豊富で、ノーマルプランが 一人あたり 月500円と少しお高い 容量無制限だし 十分なんだけど。

Office365

Office365入っているので独自ドメインを使う事が可能

BizSpark

調べてないけどおそらく可能ではないか? 有り難いことに 申請は即通った! ただ、向こうのトラブルでBizSparkがまだ使える状態にないので今回は対象外

ロリポップ

暫く使ってない ロリポップアカウントあるので、独自ドメインでメール作れそうだけど そもそも 使ってないから削除してもいいよねと・・

お名前メール

お名前.com でドメイン取ったし、便利 なんと月42円で メンバー無制限。。 容量10Gプランでも77円・・ 安すぎる

Google G Suiteにした

理由は 1ヶ月試用可能なので 期間限定で試す

murasame-lab.com あてのメールを、Googleサーバに流すため、DNSの設定が必要だが その前段階として 認証用に、 http://murasame-lab.com のWebページの metaタグに、認証コードを入れる必要がある ので まずは Webページのmetaを書き換える

このあたりの手順は G Suiteがステップ・バイ・ステップで教えてくれるので便利

最後の関門がDNS設定。

G Suiteの設定ヘルプに さくらVPSがないので 色々と試して結果下記のように設定した

GoogleメールのDNSに転送

エントリ名 @ 種別 MX 値 1 ASPMX.L.GOOGLE.COM.

このような形式で指定されたMXを追加していく 5レコードあった。 さくらVPSの場合は 値の最初の文字が優先度。その後 半角スペースの後に 転送ドメインFQDN。最後にドット忘れずに!

元あったMXを削除

私の場合は 値 10 @ のMXがあったので削除した。優先度下げるだけでも動くと思うが一応。

TXTレコード追加

TXTレコードは本来 コメントだったと思うけど、今ではコメントに設定を書くので必要かもしれない

エントリ @ タイプ TXT 値 "@ v=spf1 mx ~all"

以上でデータを反映させて暫く待つと G Suiteでメール送受信が出来るようになる。 運が悪ければ 反映に1日ぐらいかかったりするけど たいていは数時間で反映される

会社のホームページリニューアル&現在の仕事状況

会社ホームページリニューアル

友達のWebデザイナに会社のページを作ってもらった

株式会社ムラサメ研究所

POPで可愛い感じにというオーダーだったと思う

まだ日付やメニューなど修正していくんだけど

あとはゲーム会社らしく、HTML5でページに遊び要素入れたい。

お仕事状況

現在稼働中ライン

現在 7-8人で、コンシュマータイトルを作成中 Unrealを使った対戦ゲーム 今のところ長期予定

今後

1ラインだけではリスクヘッジ出来ないため、あと2ラインほど確保できるようにしたい コンシュマーは技術力も必要で楽しいけど、リスクが高いし要求技術も高いので 新人育成として、スマホゲームやWebゲームのラインが欲しい

また、Web関連の仕事も欲しい。理由は周りにWeb関係多いので仕事を依頼しやすいから

また、自社案件も 高速ゲームサーバ等行いたいし、研修の意味でもゲームエンジンオープンソースで作りたい

でも基本的には、プログラマは案件のほうが多く 人が足りない。

インドとパキスタンにオフショアする計画もある 中国、ベトナム、エジプト等も 少し話はすすめてるけど

そういえば、お友達から仕事依頼も来ているので、ラインさえ確保すればなんとか・・・ 今はプログラマのみだけど 今後はアートディレクタや営業、プランナいれて ゲーム1本まるっと作れる体制作る!

IT以外

お茶のビジネスが、開始寸前。今 有識者の意見をもらい、ロードマップ作成中

洋服のセレクトショップは、アイディア段階だが、決まれば出資する

釣りビジネスは、まだスタートするには厳しい状態。もう少しアイディア練らないと・・

シェアハウス(ギークハウス)作成は、色々とありストップしている 

Unreal 4.13でWorldViewProjectionを探す旅

調査中内容でも書かないよりはましかとおもって

自分用のメモついでに。

UnrealEngineは基本的にMatrixを扱わずに開発出来るように作られているように思う BluePrintでもほとんどMatrix関係の関数がないし

その代わりに、RotaterやTranslator等を作って Applyする World変換もViewPort変換も専用の関数があり Matrixを使う必要性がない

そのあたりの調査をする

WorldViewProjection関連

APlayerControllerに色々と入っている PlayerControllerは色々な機能があり レベル(マップ)遷移、Spectator(観戦モード)、FOV、2D 3D座標変換、マウス座標のオブジェクト取得、(ボイス)チャット機能 カメラアニメ(手ブレ表現等)、音声再生、フォースフィードバック、キー入力・・・ 書ききれない機能がある

今回は 座標変換関連を調査

Projection(3D->2D)

いきなり主目的。 ProjectWorldLocationToScreen ProjectWorldLocationToScreenWithDistance の2関数がそれにあたる

PlayerController.cpp

bool APlayerController::ProjectWorldLocationToScreen(FVector WorldLocation, FVector2D& ScreenLocation) const
{
    return UGameplayStatics::ProjectWorldToScreen(this, WorldLocation, ScreenLocation);
}

bool APlayerController::ProjectWorldLocationToScreenWithDistance(FVector WorldLocation, FVector& ScreenLocation) const
{
    FVector2D ScreenLoc2D;
    if (UGameplayStatics::ProjectWorldToScreen(this, WorldLocation, ScreenLoc2D))
    {
        // find distance
        ULocalPlayer const* const LP = GetLocalPlayer();
        if (LP && LP->ViewportClient)
        {
            // get the projection data
            FSceneViewProjectionData ProjectionData;
            if (LP->GetProjectionData(LP->ViewportClient->Viewport, eSSP_FULL, /*out*/ ProjectionData))
            {
                ScreenLocation = FVector(ScreenLoc2D.X, ScreenLoc2D.Y, FVector::Dist(ProjectionData.ViewOrigin, WorldLocation));

                return true;
            }
        }
    }

    return false;
}

GameplayStatics.cpp

bool UGameplayStatics::ProjectWorldToScreen(APlayerController const* Player, const FVector& WorldPosition, FVector2D& ScreenPosition)
{
    ULocalPlayer* const LP = Player ? Player->GetLocalPlayer() : nullptr;
    if (LP && LP->ViewportClient)
    {
        // get the projection data
        FSceneViewProjectionData ProjectionData;
        if (LP->GetProjectionData(LP->ViewportClient->Viewport, eSSP_FULL, /*out*/ ProjectionData))
        {
            FMatrix const ViewProjectionMatrix = ProjectionData.ComputeViewProjectionMatrix();
            return FSceneView::ProjectWorldToScreen(WorldPosition, ProjectionData.GetConstrainedViewRect(), ViewProjectionMatrix, ScreenPosition);
        }
    }

    ScreenPosition = FVector2D::ZeroVector;
    return false;
}

ScreenView.cpp

bool FSceneView::ProjectWorldToScreen(const FVector& WorldPosition, const FIntRect& ViewRect, const FMatrix& ViewProjectionMatrix, FVector2D& out_ScreenPos)
{
    FPlane Result = ViewProjectionMatrix.TransformFVector4(FVector4(WorldPosition, 1.f));
    if ( Result.W > 0.0f )
    {
        // the result of this will be x and y coords in -1..1 projection space
        const float RHW = 1.0f / Result.W;
        FPlane PosInScreenSpace = FPlane(Result.X * RHW, Result.Y * RHW, Result.Z * RHW, Result.W);

        // Move from projection space to normalized 0..1 UI space
        const float NormalizedX = ( PosInScreenSpace.X / 2.f ) + 0.5f;
        const float NormalizedY = 1.f - ( PosInScreenSpace.Y / 2.f ) - 0.5f;

        FVector2D RayStartViewRectSpace(
            (float)ViewRect.Min.X + ( NormalizedX * (float)ViewRect.Width() ),
            (float)ViewRect.Min.Y + ( NormalizedY * (float)ViewRect.Height() )
            );

        out_ScreenPos = RayStartViewRectSpace;

        return true;
    }
    
    return false;
}

細かい部分はおいておいて

ProjectWorldLocationToScreen はViewPort変換までおこない3D座標からスクリーン座標に変換 ProjectWorldLocationToScreenWithDistance は、上記に加えZにターゲットまでの距離が入る ようだ

残念ながら ViewPort変換の入らない、WorldViewProjectionは直接取得出来ない雰囲気

GetProjectionDataでProjectionを取得し、TransformFVector4で Vectorに適用している。 この時の座標系は同次座標ではないので wで除算すれば、-1..1 のWVP座標になるようだ そこに Screenサイズを適用し最終的なScreen座標を取得している

ただし W<0 の時は計算されないので、プレイヤーの背後は計算されない。 今回はプレイヤーの背後も必要だったので、これらの関数を W<0でも計算できるように改良したり -1..1 の座標系が欲しかったので

上記関数を自作した

DeprojectMousePositionToWorld

PlayerController.cpp

bool APlayerController::DeprojectMousePositionToWorld(FVector& WorldLocation, FVector& WorldDirection) const
{
    ULocalPlayer* const LocalPlayer = GetLocalPlayer();
    if (LocalPlayer && LocalPlayer->ViewportClient)
    {
        FVector2D ScreenPosition;
        if (LocalPlayer->ViewportClient->GetMousePosition(ScreenPosition))
        {
            return UGameplayStatics::DeprojectScreenToWorld(this, ScreenPosition, WorldLocation, WorldDirection);
        }
    }

    return false;
}

GameplayStatics.cpp

bool UGameplayStatics::DeprojectScreenToWorld(APlayerController const* Player, const FVector2D& ScreenPosition, FVector& WorldPosition, FVector& WorldDirection)
{
    ULocalPlayer* const LP = Player ? Player->GetLocalPlayer() : nullptr;
    if (LP && LP->ViewportClient)
    {
        // get the projection data
        FSceneViewProjectionData ProjectionData;
        if (LP->GetProjectionData(LP->ViewportClient->Viewport, eSSP_FULL, /*out*/ ProjectionData))
        {
            FMatrix const InvViewProjMatrix = ProjectionData.ComputeViewProjectionMatrix().InverseFast();
            FSceneView::DeprojectScreenToWorld(ScreenPosition, ProjectionData.GetConstrainedViewRect(), InvViewProjMatrix, /*out*/ WorldPosition, /*out*/ WorldDirection);
            return true;
        }
    }

    // something went wrong, zero things and return false
    WorldPosition = FVector::ZeroVector;
    WorldDirection = FVector::ZeroVector;
    return false;
}

GameplayStatics.cpp

void FSceneView::DeprojectScreenToWorld(const FVector2D& ScreenPos, const FIntRect& ViewRect, const FMatrix& InvViewProjMatrix, FVector& out_WorldOrigin, FVector& out_WorldDirection)
{
    float PixelX = FMath::TruncToFloat(ScreenPos.X);
    float PixelY = FMath::TruncToFloat(ScreenPos.Y);

    // Get the eye position and direction of the mouse cursor in two stages (inverse transform projection, then inverse transform view).
    // This avoids the numerical instability that occurs when a view matrix with large translation is composed with a projection matrix

    // Get the pixel coordinates into 0..1 normalized coordinates within the constrained view rectangle
    const float NormalizedX = (PixelX - ViewRect.Min.X) / ((float)ViewRect.Width());
    const float NormalizedY = (PixelY - ViewRect.Min.Y) / ((float)ViewRect.Height());

    // Get the pixel coordinates into -1..1 projection space
    const float ScreenSpaceX = (NormalizedX - 0.5f) * 2.0f;
    const float ScreenSpaceY = ((1.0f - NormalizedY) - 0.5f) * 2.0f;

    // The start of the raytrace is defined to be at mousex,mousey,1 in projection space (z=1 is near, z=0 is far - this gives us better precision)
    // To get the direction of the raytrace we need to use any z between the near and the far plane, so let's use (mousex, mousey, 0.5)
    const FVector4 RayStartProjectionSpace = FVector4(ScreenSpaceX, ScreenSpaceY, 1.0f, 1.0f);
    const FVector4 RayEndProjectionSpace = FVector4(ScreenSpaceX, ScreenSpaceY, 0.5f, 1.0f);

    // Projection (changing the W coordinate) is not handled by the FMatrix transforms that work with vectors, so multiplications
    // by the projection matrix should use homogeneous coordinates (i.e. FPlane).
    const FVector4 HGRayStartWorldSpace = InvViewProjMatrix.TransformFVector4(RayStartProjectionSpace);
    const FVector4 HGRayEndWorldSpace = InvViewProjMatrix.TransformFVector4(RayEndProjectionSpace);
    FVector RayStartWorldSpace(HGRayStartWorldSpace.X, HGRayStartWorldSpace.Y, HGRayStartWorldSpace.Z);
    FVector RayEndWorldSpace(HGRayEndWorldSpace.X, HGRayEndWorldSpace.Y, HGRayEndWorldSpace.Z);
    // divide vectors by W to undo any projection and get the 3-space coordinate 
    if (HGRayStartWorldSpace.W != 0.0f)
    {
        RayStartWorldSpace /= HGRayStartWorldSpace.W;
    }
    if (HGRayEndWorldSpace.W != 0.0f)
    {
        RayEndWorldSpace /= HGRayEndWorldSpace.W;
    }
    const FVector RayDirWorldSpace = (RayEndWorldSpace - RayStartWorldSpace).GetSafeNormal();

    // Finally, store the results in the outputs
    out_WorldOrigin = RayStartWorldSpace;
    out_WorldDirection = RayDirWorldSpace;
}

スクリーン座標からワールド座標に変換。 やってることは単純で、先程の WVPVpの逆行列を掛けているだけ ただ 2D座標から3D座標への変換は距離により無限に座標があるので Locationと方向をかえす。 要はRayを返すから Distanceを計算してポジションは取ってね って事

オブジェクトとの当たり判定 (2D->3D)

GetHitResultUnderCursor GetHitResultUnderCursorByChannel GetHitResultUnderCursorForObjects GetHitResultUnderFinger GetHitResultUnderFingerByChannel GetHitResultUnderFingerForObjects GetHitResultAtScreenPosition GetHitResultAtScreenPosition

マウスクリック、タッチ 等でスクリーン座標をとり 上記のようにRayを作り オブジェクトと衝突したらその3D座標を返す

ボタンやオブジェクトのクリックに使える

チャンネル指定や、オブジェクトタイプで衝突を決めれる このあたりは RayTraceと同じ仕様

まとめ

APlayerController には、プロジェクション、デプロジェクションがあり、ワールド座標とスクリーン座標の変換は容易である ただし 後ろが取れなかったり、ViewPort座標でしか取れなかったりと 多少不便だが 普通の人は直接Matrixを使わないので問題がない

Matrixを直接触る上級者は、上記関数をさんこうにして 独自で関数を作れ!

以上

ブループリント de Enumを作る

かるいTipsなので説明なしに

C++EnumやStruct作るのは簡単。 C++の構文使えばいい

それをBluePrintからも使えるようにするには、UPROPERTYをつける

ってのは以前やったが

ブループリント上で Enumも構造体も作れる

新規ブループリントから Enumを選ぶ

f:id:murasame-labo:20160926172304p:plain

そして Enumのプロパティより値を追加する

f:id:murasame-labo:20160926172449p:plain

同じ操作で構造体も作れる そして それらは当然 ブループリントから使えます

おそらく ブループリントで定義された構造体やenumは C++からは使えません。