【UE】EnhancedInputについて:Trigger編

UE5.1から標準InputとなったEnhancedInputについて軽く調べてたので、標準のTriggerについてまとめようと思います。

細かい部分に関してはソースコードまで追えてないので、調べたら随時更新します。

 


EnhancedInputについては公式のドキュメントを参考にしてください。

docs.unrealengine.com

 

ChordedAction

指定したInputActionと同時に入力されているときにTriggerイベントが発火する。

float値のInputActionで行った際には同時入力でないときにもTriggerが発火してたので要調査。使うときは基本的にはBool値のもので行ったほうが安全そう

パラメータ

  • ChordAction:同時に入力が必要なInputActionの配列
  • ActionThreshold:入力の閾値

Combo

指定したInputActionが指定した時間以内に順番に連続で入力されたときにTriggerイベントが発火される。このTriggerだとMappingでの入力キーの設定がなくなる。

パラメータ

  • ComboActions:設定されたInputActionが上から順番に下記のTimeToPressKeyの時間以内に入力があり、すべての入力が終わったときにTriggerイベントが発火する。
    • ComboStepAction:設定するInputAction 
    • TimeToPressKey:次のInputAction入力猶予時間。超えても入力がない場合はCancelイベントが発火する。
  • CancelActions:コンボを強制的にキャンセルするInputActionの配列

Down

押してる間Triggerを発火。Noneと同じ挙動をする

パラメータ

  • ActionThreshold:入力の閾値

Hold

入力を設定時間以上押し続けた場合Triggerイベントを発火する

パラメータ

  • HoldTimeThreshold:押し続ける時間
  • IsOneShot
    • True:一度だけTriggerを発火して即Completeイベントを発火
    • False:押し続けている間毎フレームTriggerを発火し離したときにCompleteイベントを発火
  • Affected by TimeDilation:?
  • ActionThreshold:入力の閾値

HoldAndRelease

入力を設定時間以上押し続けた後に離したタイミングでTriggerイベントを発火

パラメータ

  • HoldTimeThreshold:押し続ける時間
  • Affected by TimeDilation:?
  • ActionThreshold:入力の閾値

Pressed

入力があったとき一度だけTriggerイベントを発火

パラメータ

  • ActionThreshold:入力の閾値

Pulse

入力を押し続けている間IntervalのタイミングごとにTriggerイベントを発火。Trigger以外はOngoingイベントにいく

パラメータ

  • TriggerOnStart
    • True:入力開始時にもTriggerイベントを発火
    • False:入力開始時にはTriggerイベントなし
  • Interval:Triggerイベントを発火する秒数
  • TriggerLimit:1以上が設定されている場合、指定回数Triggerイベントを発火するとCompleteイベントを発火し、以降はイベントなし。0の場合は入力を離すまでイベントを発火する
  • Affected by TimeDilation:?
  • ActionThreshold:入力の閾値

Released

入力を離したタイミングでTriggerイベントを発火。押し続けている間はOngoingイベント

パラメータ

  • ActionThreshold:入力の閾値

Tap

指定した時間以内に入力の押下が行われた際にTrigger発火

パラメータ

  • TapReleasedTimeThreshold:押して離すまでの猶予時間
  • Affected by TimeDilation:?
  • ActionThreshold:入力の閾値

 

【UE4】OnlineSubsystemEOSの使い方まとめ

今回は、4.27から導入されたOnlineSubsystemEOSを使ってP2PNATで動かせる環境を作るまでの情報をまとめた記事になります。

 

以下参考にさせていただいた情報

OnlineSubsystemEOSの導入について(エディタでローカルで試す方法)

qiita.com

 

公式のドキュメント

docs.unrealengine.com

 

公式のOnlineSubsystemEOSのドキュメント

docs.unrealengine.com

 

RPCに関して説明は下記の記事を参考にしてください

マルチプレイヤーゲーム実装について説明してくれているUE4アドカレ2021の神記事

qiita.com

 

項目 内容
エンジンバージョン 4.27.2
テンプレート ThirdPersonCPP

 

また以下の内容は今回の記事では扱いません。

  • ほかのプラットフォームと併用(EOSPlus)の設定
  • ボイスチャット
  • DedicatedServerでの利用
  • RPCを使ったゲーム制作の実装

 

基本的な導入は公式ドキュメントを参考にしてください。

プラグイン

f:id:shiratori00:20211221223756p:plain

プロジェクト設定

f:id:shiratori00:20211221223214p:plain

ArtifactsにはEOSのDeveloperPortalで製品登録をして各情報を作成して、設定をしてください。

 

DefaultEngine.ini(追加部分)

[OnlineSubsystemEOS]
bEnabled=true

[OnlineSubsystem]
DefaultPlatformService=EOS

[/Script/Engine.GameEngine]
!NetDriverDefinitions=ClearArray
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemEOS.NetDriverEOS",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")

[/Script/OnlineSubsystemEOS.NetDriverEOS]
bIsUsingP2PSockets=true

 

ここからはC++での実装になります。基本的には冒頭で紹介したOnlineSubsystemEOSの使い方を説明してくださっている記事のものと同じです。

 

クラスはGameInstanceで行っていますが、PlayerControllerでも構いません。

その場合、PlayerControllerを取得してくる処理が必要なくなるので適宜読み替えをお願いします。

ログイン処理

void UMyGameInstance::LoginEOS()
{
    IOnlineSubsystem* OnlineSub = Online::GetSubsystem(GetWorld());
    if (OnlineSub)
    {
        IOnlineIdentityPtr Identity = OnlineSub->GetIdentityInterface();
        if (Identity.IsValid())
        {
            //ローカルプレイヤーの取得
            ULocalPlayer* LPC = GetWorld()->GetFirstPlayerController()->GetLocalPlayer();

            if (LPC != NULL)
            {
                //コントローラIDの取得
                int ControllerId = LPC->GetControllerId();
                if (Identity->GetLoginStatus(ControllerId) != ELoginStatus::LoggedIn)
                {
                    UE_LOG_ONLINE(Warning, TEXT("ComandLine: %s"), FCommandLine::Get());

                    //OnlineSubsystemのLoginCompleteデリゲートを登録
                    Identity->AddOnLoginCompleteDelegate_Handle(ControllerId, FOnLoginCompleteDelegate::CreateUObject(this, &UMyGameInstance::OnLoginCompleteDelegate));

                    //ログインの情報を作成
                    FOnlineAccountCredentials acountCredentials;
                    //ログインの種類を設定
                    acountCredentials.Type = "accountportal";
Identity->Login(ControllerId, acountCredentials); } ELoginStatus::Type status = Identity->GetLoginStatus(ControllerId); } } } }

EOSでログインする場合FOnlineAccountCredentialsに設定する項目はログイン方法の種類によって変わります。

設定は

FOnlineAccountCredentials.Type

で行います。

 

exchangecode

EpicGamesLauncherを利用してログインする場合にこれを使います。

追加で

FOnlineAccountCredentials.Token

を設定する必要があります。

調査不足でここのトークンの取得方法はわかってません。

developer

EOSの開発ツールを使ってログインする方法になります。冒頭で紹介したOnlineSubsystemEOSの使用方法を説明してくださってる記事の中に詳細があるので割愛します。

基本的には起動時引数を設定しておいて自動でログインする方法になります。

accountportal

Webブラウザを介して自動で認証を通してくれるやり方で、特に情報を設定せずに使えるので今回はこれで行います。

 

ログイン成功時デリゲート

void UMyGameInstance::OnLoginCompleteDelegate(int32 LocalUserNum, bool bWasSuccessful, const FUniqueNetId& UserId, const FString& Error)
{
    IOnlineIdentityPtr Identity = Online::GetIdentityInterface();
    if (Identity.IsValid())
    {
        //ローカルプレイヤーコントローラを取得
        APlayerController* LPC = GetWorld()->GetFirstPlayerController();
        //ローカルプレイヤーを取得
        ULocalPlayer* LP = Cast<ULocalPlayer>(LPC->GetLocalPlayer());
        if (LP != NULL)
        {
            int ControllerId = LP->GetControllerId();
            //現在のユニークIDの取得
            FUniqueNetIdRepl uniqueId = LPC->PlayerState->GetUniqueId();
            //通信用UniqueNetIDを設定
            uniqueId.SetUniqueNetId(FUniqueNetIdWrapper(UserId).GetUniqueNetId());
            //UniqueIDを更新
            LPC->PlayerState->SetUniqueId(uniqueId);
        }
    }
}

 

セッションの作成

bool UMyGameInstance::HostSession()
{
    IOnlineSubsystem* const OnlineSub = Online::GetSubsystem(GetWorld());
    if (OnlineSub)
    {
        IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface();
        if (Sessions.IsValid())
        {
            APlayerController* LPC = GetWorld()->GetFirstPlayerController();

            TSharedPtr<class FOnlineSessionSettings> SessionSettings = MakeShareable(new FOnlineSessionSettings());
            //制限なしで参加できる人数
            SessionSettings->NumPublicConnections = 4;
            
            SessionSettings->NumPrivateConnections = 0;
            //セッションの検索を許可する
            SessionSettings->bShouldAdvertise = true;
            //Session開始後でも参加を許可する
            SessionSettings->bAllowJoinInProgress = true;
            //招待を許可する
            SessionSettings->bAllowInvites = true;
            //プレゼンスにロビー情報をのせる
            SessionSettings->bUsesPresence = true;
            //SessionIDを知っているユーザーであれば参加可能にする
            SessionSettings->bAllowJoinViaPresence = true;
            //EOSロビーサービスを利用するように設定
            SessionSettings->bUseLobbiesIfAvailable = true;
            //ロビーによる音声通話の設定
            SessionSettings->bUseLobbiesVoiceChatIfAvailable = false;

            //検索ワードとしてSearchKeywordを設定
            SessionSettings->Set(SEARCH_KEYWORDS, SearchKeyword, EOnlineDataAdvertisementType::ViaOnlineService);

            //セッション作成終了時デリゲートの登録
            Sessions->AddOnCreateSessionCompleteDelegate_Handle(FOnCreateSessionCompleteDelegate::CreateUObject(this, &UMyGameInstance::OnCreateSessionCompleteDelegate));

            TSharedPtr<const FUniqueNetId> UniqueNetIdptr = LPC->GetLocalPlayer()->GetPreferredUniqueNetId().GetUniqueNetId();
            //セッションの作成
            bool bResult = Sessions->CreateSession(*UniqueNetIdptr, HostSessionName, *SessionSettings);

            if (bResult) {
                return true;
            }
        }
    }
    return false;
}

セッション検索用のSearchKeywordにはCustomという文字列を設定してます。

 

セッション作成成功時デリゲート

void UMyGameInstance::OnCreateSessionCompleteDelegate(FName InSessionName, bool bWasSuccessful)
{
    if (bWasSuccessful)
    {
        FString path = MapRootPath + TargetMapName;
        UGameplayStatics::OpenLevel(this, FName(path), true, "listen");
    }

}

セッション作成が成功したらゲーム用レベルに移動します。

今回はThirdPersonテンプレートなので

 

MapRootPath : /Game/ThirdPersonBP/Maps/

TargetMapName : ThirdPersonExampleMap

 

を設定しています。

 

セッション検索

void UMyGameInstance::FindSession()
{
    IOnlineSubsystem* const OnlineSub = Online::GetSubsystem(GetWorld());
    if (OnlineSub)
    {
        IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface();
        if (Sessions.IsValid())
        {
            APlayerController* LP = GetWorld()->GetFirstPlayerController();

            FOnlineSessionSearch* search = new FOnlineSessionSearch();

            search->MaxSearchResults = 10;
            search->PingBucketSize = 50;
            search->bIsLanQuery = false;

            SearchSettings = MakeShareable(search);

            SearchSettings->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals);
            SearchSettings->QuerySettings.Set(SEARCH_LOBBIES, true, EOnlineComparisonOp::Equals);
            SearchSettings->QuerySettings.Set(SEARCH_KEYWORDS, SearchKeyword, EOnlineComparisonOp::Equals);

            Sessions->AddOnFindSessionsCompleteDelegate_Handle(FOnFindSessionsCompleteDelegate::CreateUObject(this, &UMyGameInstance::OnFindSessionsCompleteDelegate));

            TSharedRef<FOnlineSessionSearch> SearchSettingsRef = SearchSettings.ToSharedRef();
            TSharedPtr<const FUniqueNetId> UniqueNetIdptr = LP->GetLocalPlayer()->GetPreferredUniqueNetId().GetUniqueNetId();
            if (UniqueNetIdptr != NULL)
            {
                bool bIsSuccess = Sessions->FindSessions(*UniqueNetIdptr, SearchSettingsRef);
            }
        }
    }
}

セッション検索成功時デリゲート

void UMyGameInstance::OnFindSessionsCompleteDelegate(bool bWasSuccessful)
{
    if (bWasSuccessful)
    {
        if (SearchSettings->SearchResults.Num() == 0)
        {
        }
        else
        {
            TArray<FBlueprintSessionResult> resultList;

            for (auto result : SearchSettings->SearchResults)
            {
                FBlueprintSessionResult val;
                val.OnlineResult = result;
                resultList.Add(val);
            }

            FindSessionsCompleteDelegate.Broadcast(resultList);
            //const TCHAR* SessionId = *SearchSettings->SearchResults[0].GetSessionIdStr();

            //JoinSessionEOS(SearchSettings->SearchResults[0]);
        }
    }
}

セッション検索結果をUIに投げるためにデリゲートを用意しBP側で受け取れるようにしています。

軽く動かすだけならコメントアウトしている部分のようにリザルトの0番目で後述するセッション参加関数を呼べば動きます。

 

セッション参加

void UMyGameInstance::JoinSessionEOS(FOnlineSessionSearchResult SearchResult)
{
    IOnlineSubsystem* const OnlineSub = Online::GetSubsystem(GetWorld());
    if (OnlineSub)
    {
        IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface();
        if (Sessions.IsValid())
        {
            if (SearchResult.IsValid())
            {
                APlayerController* LPC = GetWorld()->GetFirstPlayerController();

                Sessions->AddOnJoinSessionCompleteDelegate_Handle(FOnJoinSessionCompleteDelegate::CreateUObject(this, &UMyGameInstance::OnJoinSessionCompleteDelegate));

                TSharedPtr<const FUniqueNetId> UniqueNetIdptr = LPC->GetLocalPlayer()->GetPreferredUniqueNetId().GetUniqueNetId();

                Sessions->JoinSession(*UniqueNetIdptr, CurrentSessionName, SearchResult);
            }

        }
    }

}

セッション参加成功時デリゲート

void UMyGameInstance::OnJoinSessionCompleteDelegate(FName SessionName, EOnJoinSessionCompleteResult::Type Result)
{
    IOnlineSubsystem* const OnlineSub = Online::GetSubsystem(GetWorld());
    if (OnlineSub)
    {
        IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface();
        if (Sessions.IsValid())
        {
            if (Result == EOnJoinSessionCompleteResult::Success)
            {
                FString ConnectString;
                if (Sessions->GetResolvedConnectString(CurrentSessionName, ConnectString))
                {
                    APlayerController* LPC = GetWorld()->GetFirstPlayerController();
                    //Sessions->ClearOnJoinSessionCompleteDelegate_Handle(&AMyPlayerController::OnJoinSessionCompleteDelegate);
                    UE_LOG_ONLINE_SESSION(Log, TEXT("JoinSessionComplete : Join Session: traveling to %s"), *ConnectString);
                    LPC->ClientTravel(ConnectString, TRAVEL_Absolute);
                }
            }
        }
    }
}

以上の用意した関数をBPから呼びます。

 

f:id:shiratori00:20211221223008p:plain

f:id:shiratori00:20211221223024p:plain

こちらの画像ではデリゲートを用意し、セッションを検索して出てきた結果をUIに表示しています。

 

実行手順としては

  1. ログインを行う
  2. ホスト側がセッションを作成
  3. クライアント側がセッションを検索→参加

で動作します。

 

 

 

NAT超えて動作することは確認済みです。みんなも気軽にオンラインゲーム作ろう!

 

【UE4】MRTK-Unrealを触ってみた

この記事はHoloLens Advent Calendar 2020の15日目の記事となります。

qiita.com

 

内容は自作アプリを作りながら、先月行われたUnrealFestにて紹介されていたMRTK-Unrealについてまとめておこうと思います。

 

www.youtube.com

触ってない機能については後日加筆修正を加えていく予定です。

 

機能の一覧に関しては下記の公式ドキュメントをご覧ください。

github.com

 

今回の環境はUE4.25+MRTK for Unreal 0.10.0になります。

 

MRTKforUnrealの導入方法ですが、EpicGamesLauncherのマーケットプレイスから、Mixed Reality UX Toolsを探して、エンジンにインストールするだけです。

f:id:shiratori00:20201214192302p:plain

 

一応Hololens2の開発導入やコアモジュールの解説を過去に記事にしているので、そちらもご覧ください。

shiratori1221.hatenablog.com

 

マニピュレータに関しては上記の記事で説明しているので省略します。

 

ハンドメニュー

一番汎用的で使いやすいメニューだと思います。

まず、親クラスを作成します。名前はBP_HandMenuActorとします。

コンポーネントにUxtPalmUpConstraintを追加します。これは、手の平などを検知し、追従する機能をアクターに追加するコンポーネントです。

f:id:shiratori00:20201214195150p:plain

プロパティに関して軽く羅列すると、

MaxPalmAngle : 手のひらとして認識する円錐の最大角度

RequireFlatHand : 手の平坦度条件を使用する?

Hand:どちらの手(もしくは両手)を認識した際に追従するか

Zone:追従する場所。手を中心として上下左右どこか

OffsetMode:位置の補正。手の平の回転を優先するのか、カメラの向きを優先するのか

RotationMode:向きの補正。何もしないか、手のひらの回転に追従するか、カメラへ常に向き続けるか。基本的にLook at Camera

GoalMargin:Zoneの位置からのマージン

MoveOwingActor:手にくっついて自身のアクターの位置を動かすか。基本的にTrue

LocationLerpTime:追従する際のアニメーションの位置補正にかかるディレイ時間。大きくすればするほど追従する動きが鈍く遅くなる

RotationLerpTime:追従する際のアニメーションの回転補正にかかるディレイ時間。大きくすればするほど追従する動きが鈍く遅くなる

 

使えるイベントは以下の通りです。ただ、基本的にはOnConstraintActivatedとOnConstraintDeactivatedの二つだけでいいと思います。

OnConstraintActivated : 手のひらを認識したとき

OnConstraintDeactivated : 手のひらをロストしたとき

OnBeginTracking : 手のひらのトラッキングを開始したとき。何が違うのかはよくわかりませんでした。

OnEndTracking : 手のひらのトラッキングが終了したとき。何が違うのかはよくわかりませんでした。

残りの二つはUE4コンポーネントの標準的なイベントなので割愛します。

 

実際ハンドメニューで押すことのできるボタンに関しては、UxtUIElementとUxtPressableButtonActorで実装します。

UxtUIElementは子要素のUIのVisbleの管理を行います。

UxtPressableButtonActorはMRTKが用意しているUI要素の一つで、標準的なボタンの実装がされているクラスになります。

f:id:shiratori00:20201215052132p:plain

使用する際にはUxtUIElementのchildActorで設定をします。

設定できる要素を軽くまとめます。

Millimeter Size : ボタンの大きさの設定

IsPlated : 背景的な青い部分を表示するかどうか

Icon Brush : アイコンの設定。あらかじめ用意されたアイコンに加えて独自のアイコンを追加することも可能

Label : アイコンの下に表示する文字列。標準で設定されているフォントだと、日本語は使用不可

LabelTextBrush : Labelの設定。文字色やサイズ、使用するフォントなどはここから変更をする。

ButtonBrush : ボタンの見た目に関しての設定。スタティックメッシュやマテリアルの設定。押したときのアニメーションや音に関しての設定がまとめられています。

 

これ以下の部分はあんまり触らないので割愛。

 

次はボタンへのイベントのバインドについて説明します。

それぞれのボタンのイベントに関してですが、今回の実装の仕方であると、childActorからUxtPressableButtonComponentを取得し、ButtonPressedイベントにバインドします。

f:id:shiratori00:20201215060639p:plain

もう一つの方法としては、MRTKのボタンクラスを継承したブループリントクラスを作成し、それぞれのクラス内でイベント処理を実装して、それをChildActorとする方法です。

ボタンの種類に関しては通常の押下ボタン・トグル・チェックボックスラジオボタン・スイッチが用意されています。

f:id:shiratori00:20201215061004p:plain

UxtPressableButtonActorの場合は以下の通りになります。

f:id:shiratori00:20201215061135p:plain

コンポーネントに関しては特に先ほどの説明と変わりがないので割愛します。

イベント実装に関しては、ルートコンポーネントであるButtonComponentにイベントがあるのでそれぞれ必要なイベントを実装してください。

基本的にはPressedかReleasedあたりのみの利用になると思います。

 

次は手のひらを認識/ロストしたときにUI要素の表示/非表示を切り替える処理を実装します。

f:id:shiratori00:20201215061621p:plain

ここではロスト時に非表示にしていますが、複雑なハンドメニューの場合は、非表示にせずにするとよいと思います。

あとはレベル上にハンドメニューのActorを配置orプレイ開始時などにSpawnActorなどをすればハンドメニューが機能します。

 

UMGの利用

UE4のUI機能であるUMGを利用することも可能です。利用方法は3DWidgetの実装にUxtWidgetコンポーネントを追加するだけです。

f:id:shiratori00:20201215062359p:plain

基本的に2DのUIを触るので、タッチやファービームで動作します。スライダーなどは少し操作に手間取る部分がありました。

また、汎用マニピュレータのアクターなどとの干渉もあったりで使う際には注意が必要です。

使う利点としては、UMGアニメーションが利用できるところなどでしょうか。

基本的にはUI要素はMRTK側で大体用意されているので、UMGを積極的に利用する必要性は薄い気はします。

 

 

明日は@t-shoさんの記事となります。

【UE4】GameplayAbilityとAnimNotify/AnimNotifyStateを使ってコンボを作る

この記事はUnrealEngine 4(UE4)AdventCalender 2020の15日目の投稿記事です。

 

qiita.com

 

やることはタイトルの通りコンボをUE4で実装する方法の一つの紹介みたいな感じです。

今回のアドベントカレンダーでネタ被りをしてしまって申し訳ない・・・。

qiita.com

 

GameplayAbilityの概要やら導入について説明してくださっている先駆者の方々や公式のドキュメント

docs.unrealengine.com

okawari-hakumai.hatenablog.com

historia.co.jp

 

 GameplayAbilitySystemの概要と導入は上記の記事を参考にしてください。

 

アニメーションのアセットは下記のものを使用しました。

Dynamic Sword Animset:アニメーション - UE マーケットプレイス

 

また、このコンボ実装は公式のActionRPGプロジェクトを参考にさせてもらいました。

Action RPG:Epic コンテンツ - UE マーケットプレイス

 

AnimNotifyとAnimNotifyStateが存在するので、それについて軽く説明と使い方をまとめます。

docs.unrealengine.com

AnimNotifyとAnimNotifyStateはアニメーションでの通知イベントのクラスです。両者の違いは1フレームか複数フレームにまたがるかです。

 

AnimNotifyは挿入したフレームの際にフックするReceiveNotifyイベントが、

AnimNotifyStateはBegin、Tick、Endイベントが実装されています。

 

これを利用して入力受付の時間の調整などの処理を実装していきます。

 

プロジェクトはUE4.26のC++のBlankで作成しました。C++のThirdPersonでもいいと思います。

プラグインを有効化したあと、プロジェクトのBuild.csを編集します。

// Copyright Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;

public class MyProject4 : ModuleRules
{
    public MyProject4(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
    
        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "GameplayAbilities", "GameplayTags", "GameplayTasks" });

        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");

        // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
    }
}

GameplayAbilitiesとGameplayTags、GameplayTasksをPublicDependencyModuleNamesに追加しました。 

 

次にキャラクターの実装をします。冒頭の記事を参考にC++で作成します。適宜自身のプロジェクトに置き換えてください。

 

MyCharacter.h

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

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "AbilitySystemInterface.h"
#include "MyAbilitySystemComponent.h"
#include "MyCharacter.generated.h"

class UMyGameplayAbility;

UCLASS()
class MYPROJECT4_API AMyCharacter : public ACharacter, public IAbilitySystemInterface
{
    GENERATED_BODY()

    /** Camera boom positioning the camera behind the character */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
    class USpringArmComponent* CameraBoom;

    /** Follow camera */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
    class UCameraComponent* FollowCamera;

public:
    // Sets default values for this character's properties
    AMyCharacter();

    /** Base turn rate, in deg/sec. Other scaling may affect final turn rate. */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
    float BaseTurnRate;

    /** Base look up/down rate, in deg/sec. Other scaling may affect final rate. */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
    float BaseLookUpRate;

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

    /** Called for forwards/backward input */
    void MoveForward(float Value);

    /** Called for side to side input */
    void MoveRight(float Value);

    /**
    * Called via input to turn at a given rate.
    * @param Rate  This is a normalized rate, i.e. 1.0 means 100% of desired turn rate
    */
    void TurnAtRate(float Rate);

    /**
    * Called via input to turn look up/down at a given rate.
    * @param Rate  This is a normalized rate, i.e. 1.0 means 100% of desired turn rate
    */
    void LookUpAtRate(float Rate);

    /** Handler for when a touch input begins. */
    void TouchStarted(ETouchIndex::Type FingerIndex, FVector Location);

    /** Handler for when a touch input stops. */
    void TouchStopped(ETouchIndex::Type FingerIndex, FVector Location);


public:   
    // Called every frame
    virtual void Tick(float DeltaTime) override;

    // Called to bind functionality to input
    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

public:
    /** Returns CameraBoom subobject **/
    FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
    /** Returns FollowCamera subobject **/
    FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }

public:
    /*Ability System*/
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Abilities", meta = (AllowPrivateAccess = "true"))
        class UMyAbilitySystemComponent* AbilitySystem;

    UAbilitySystemComponent* GetAbilitySystemComponent() const
    {
        return Cast<UAbilitySystemComponent>(AbilitySystem);
    };


    /*Ability List*/
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Abilities")
        TArray<TSubclassOf<class UGameplayAbility>> AbilityList;

    virtual void PossessedBy(AController* NewController) override;

public:
    UFUNCTION(BlueprintCallable, Category = "Abilities")
        bool ActivateAbilitiesWithTags(FGameplayTagContainer AbilityTags, bool bAllowRemoteActivation = true);

    UFUNCTION(BlueprintCallable, Category = "Abilities")
        void GetActiveAbilitiesWithTags(FGameplayTagContainer AbilityTags, TArray<UMyGameplayAbility*>& ActiveAbilities);
};

MyCharacter.cpp

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


#include "MyCharacter.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/InputComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/Controller.h"
#include "GameFramework/SpringArmComponent.h"
//#include <AbilitySystemComponent.h>

// Sets default values
AMyCharacter::AMyCharacter()
{
    GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);

    // set our turn rates for input
    BaseTurnRate = 45.f;
    BaseLookUpRate = 45.f;

    // Don't rotate when the controller rotates. Let that just affect the camera.
    bUseControllerRotationPitch = false;
    bUseControllerRotationYaw = false;
    bUseControllerRotationRoll = false;

    // Configure character movement
    GetCharacterMovement()->bOrientRotationToMovement = true; // Character moves in the direction of input...  
    GetCharacterMovement()->RotationRate = FRotator(0.0f, 540.0f, 0.0f); // ...at this rotation rate
    GetCharacterMovement()->JumpZVelocity = 600.f;
    GetCharacterMovement()->AirControl = 0.2f;

    // Create a camera boom (pulls in towards the player if there is a collision)
    CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
    CameraBoom->SetupAttachment(RootComponent);
    CameraBoom->TargetArmLength = 300.0f; // The camera follows at this distance behind the character  
    CameraBoom->bUsePawnControlRotation = true; // Rotate the arm based on the controller

    // Create a follow camera
    FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
    FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName); // Attach the camera to the end of the boom and let the boom adjust to match the controller orientation
    FollowCamera->bUsePawnControlRotation = false; // Camera does not rotate relative to arm

    // Note: The skeletal mesh and anim blueprint references on the Mesh component (inherited from Character) 
    // are set in the derived blueprint asset named MyCharacter (to avoid direct content references in C++)

    AbilitySystem = CreateDefaultSubobject<UMyAbilitySystemComponent>(TEXT("AbilitySystem"));

}

// Called when the game starts or when spawned
void AMyCharacter::BeginPlay()
{
    Super::BeginPlay();
    
    if (AbilitySystem)
    {
        int32 inputID(0);
        if (HasAuthority() && AbilityList.Num() > 0)
        {
            for (auto Ability : AbilityList)
            {
                if (Ability)
                {
                    AbilitySystem->GiveAbility(FGameplayAbilitySpec(Ability.GetDefaultObject(), 1, inputID++));
                }
            }
        }
        AbilitySystem->InitAbilityActorInfo(this, this);
    }
}

void AMyCharacter::MoveForward(float Value)
{
    if ((Controller != nullptr) && (Value != 0.0f))
    {
        // find out which way is forward
        const FRotator Rotation = Controller->GetControlRotation();
        const FRotator YawRotation(0, Rotation.Yaw, 0);

        // get forward vector
        const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
        AddMovementInput(Direction, Value);
    }
}

void AMyCharacter::MoveRight(float Value)
{
    if ((Controller != nullptr) && (Value != 0.0f))
    {
        // find out which way is right
        const FRotator Rotation = Controller->GetControlRotation();
        const FRotator YawRotation(0, Rotation.Yaw, 0);

        // get right vector 
        const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
        // add movement in that direction
        AddMovementInput(Direction, Value);
    }
}

void AMyCharacter::TurnAtRate(float Rate)
{
    // calculate delta for this frame from the rate information
    AddControllerYawInput(Rate * BaseTurnRate * GetWorld()->GetDeltaSeconds());
}

void AMyCharacter::LookUpAtRate(float Rate)
{
    // calculate delta for this frame from the rate information
    AddControllerPitchInput(Rate * BaseLookUpRate * GetWorld()->GetDeltaSeconds());
}

void AMyCharacter::TouchStarted(ETouchIndex::Type FingerIndex, FVector Location)
{
}

void AMyCharacter::TouchStopped(ETouchIndex::Type FingerIndex, FVector Location)
{
}

// Called every frame
void AMyCharacter::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

}

// Called to bind functionality to input
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    // Set up gameplay key bindings
    check(PlayerInputComponent);
    PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
    PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);

    PlayerInputComponent->BindAxis("MoveForward", this, &AMyCharacter::MoveForward);
    PlayerInputComponent->BindAxis("MoveRight", this, &AMyCharacter::MoveRight);

    // We have 2 versions of the rotation bindings to handle different kinds of devices differently
    // "turn" handles devices that provide an absolute delta, such as a mouse.
    // "turnrate" is for devices that we choose to treat as a rate of change, such as an analog joystick
    PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput);
    PlayerInputComponent->BindAxis("TurnRate", this, &AMyCharacter::TurnAtRate);
    PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput);
    PlayerInputComponent->BindAxis("LookUpRate", this, &AMyCharacter::LookUpAtRate);
}

void AMyCharacter::PossessedBy(AController* NewController)
{
    Super::PossessedBy(NewController);

    AbilitySystem->RefreshAbilityActorInfo();
}

bool AMyCharacter::ActivateAbilitiesWithTags(FGameplayTagContainer AbilityTags, bool bAllowRemoteActivation)
{
    if (AbilitySystem)
    {
        return AbilitySystem->TryActivateAbilitiesByTag(AbilityTags, bAllowRemoteActivation);
    }
    return false;
}

void AMyCharacter::GetActiveAbilitiesWithTags(FGameplayTagContainer AbilityTags, TArray<UMyGameplayAbility*>& ActiveAbilities)
{
    if (AbilitySystem)
    {
        AbilitySystem->GetActiveAbilitiesWithTags(AbilityTags, ActiveAbilities);
    }
}

基本的にはThirdpersonのC++からパクってきてます。一部いらなさそうなやつは削除したりしてます。

 

キャラクターで使っている独自AbilitySystemComponentとGameplayAbilityを追加で作ります。

MyGameplayAbility.h

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

#pragma once

#include "CoreMinimal.h"
#include "Abilities/GameplayAbility.h"
#include "MyGameplayAbility.generated.h"

/**
 * 
 */
UCLASS()
class MYPROJECT4_API UMyGameplayAbility : public UGameplayAbility
{
    GENERATED_BODY()
    
    UFUNCTION(BlueprintCallable, Category = "GameplayAbility")
    virtual void AddGameplayTags(const FGameplayTagContainer GameplayTags);

    UFUNCTION(BlueprintCallable, Category = "GameplayAbility")
    virtual void RemoveGameplayTags(const FGameplayTagContainer GameplayTags);

};

MyGameplayAbility.cpp

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


#include "MyGameplayAbility.h"
#include "AbilitySystemComponent.h"

void UMyGameplayAbility::AddGameplayTags(const FGameplayTagContainer GameplayTags)
{
    UAbilitySystemComponent* Comp = GetAbilitySystemComponentFromActorInfo();

    Comp->AddLooseGameplayTags(GameplayTags);
}

void UMyGameplayAbility::RemoveGameplayTags(const FGameplayTagContainer GameplayTags)
{
    UAbilitySystemComponent* Comp = GetAbilitySystemComponentFromActorInfo();
    Comp->RemoveLooseGameplayTags(GameplayTags);
}

MyAbilitySystemComponent.h

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

#pragma once

#include "CoreMinimal.h"
#include "AbilitySystemComponent.h"
#include "MyAbilitySystemComponent.generated.h"

/**
 * 
 */

class UMyGameplayAbility;
class UGameplayAbility;

UCLASS()
class MYPROJECT4_API UMyAbilitySystemComponent : public UAbilitySystemComponent
{
    GENERATED_BODY()
public:
    UMyAbilitySystemComponent();

    void GetActiveAbilitiesWithTags(const FGameplayTagContainer& GameplayTagContainer, TArray<UMyGameplayAbility*>& ActiveAbilities);
    static UMyAbilitySystemComponent* GetAbilitySystemComponentFromActor(const AActor* Actor, bool LookForComponent = false);
};

MyAbilitySystemComponent.cpp

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


#include "MyAbilitySystemComponent.h"
#include "MyGameplayAbility.h"
#include "AbilitySystemGlobals.h"

UMyAbilitySystemComponent::UMyAbilitySystemComponent() {}

void UMyAbilitySystemComponent::GetActiveAbilitiesWithTags(const FGameplayTagContainer& GameplayTagContainer, TArray<UMyGameplayAbility*>& ActiveAbilities)
{
    TArray<FGameplayAbilitySpec*> AbilitiesToActivate;
    GetActivatableGameplayAbilitySpecsByAllMatchingTags(GameplayTagContainer, AbilitiesToActivate, false);

    for (FGameplayAbilitySpec* spec : AbilitiesToActivate)
    {
        TArray<UGameplayAbility*> AbilityInstances = spec->GetAbilityInstances();

        for (UGameplayAbility* ActiveAbility : AbilityInstances)
        {
            ActiveAbilities.Add(Cast<UMyGameplayAbility>(ActiveAbility));
        }
    }
}

UMyAbilitySystemComponent* UMyAbilitySystemComponent::GetAbilitySystemComponentFromActor(const AActor* Actor, bool LookForComponent)
{
    return Cast<UMyAbilitySystemComponent>(UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(Actor, LookForComponent));
}

 

VisualStudioのビルドが通れば、C++側の実装は終わりになります。

 

UE4Editorに移り、AnimNotifyStateを作ります。

f:id:shiratori00:20201214112305p:plain

名前はNS_JumpSectionにしておきます。

グラフが開いたら、FunctionsでReceivedNotifyBegin/EndをOverrideで作ります。

また、VariablesにName型の変数を一つ追加します。ここでは名前をNextSectionNameとしました。

f:id:shiratori00:20201214183721p:plain

End側ではコンボ用のプロパティを初期化しています。

f:id:shiratori00:20201214183726p:plain

Start側ではMontageの次のセクションを初期化し、後述で作るプレイヤークラスのコンボ入力の受付可能フラグを立て、現在実行中のAnimNotifyStateクラスである自身をプレイヤークラスのプロパティにセットしています。

 

次に再生するアニメーションモンタージュを作成します。

名前はAM_Swordとします。

いい感じに攻撃モーションを並べて、先ほど作成したAnimNotifyStateを配置します。

f:id:shiratori00:20201214184335p:plain

f:id:shiratori00:20201214184341p:plain

今回はループで延々と攻撃が続けられるように二つの配置とNS_JumpSectionをそれぞれに配置していますが、ループせずに終わる場合は、最後のモーションには通知を置かないようにします。

 

AnimationBlueprintでは作成したアニメーションモンタージュを再生できるようにしておきます。

f:id:shiratori00:20201214184631p:plain

 

GameplayAbilityを作ります。名前はGA_Testとしておきます。

f:id:shiratori00:20201214185405p:plain

変数にAnimationMontageを追加します。名前はanimMontageで、デフォルトの値には先ほど作成したアニメーションモンタージュをセットしておきます。

やってることはAnimationMontageを再生開始しています。

 

最後に操作するキャラクタークラスを作成します。MyCharacterクラスを基底クラスとしたBlueprintクラスを作成します。名前はBP_Characterにしておきます。

VariablesのAbilities/AbilityListに先ほど作成したGameplayAbilityを追加します。これで実行時にAbilitySystemComponentに自動で登録されます。

また、変数にコンボ受付可能フラグのEnableComboPeriod、現在実行中のAnimNotifyStateクラスを格納するCurrentNotifyを追加します。

再生用にDoMeleeという関数を作成します。

f:id:shiratori00:20201214185811p:plain

 

DoMelee関数の中身です。

f:id:shiratori00:20201214190019p:plain

現在アニメーション通知が存在するなら後述するコンボ処理関数に処理を移します。

存在しない場合はニュートラル状態なので、新規でGameplayAbilityを実行します。

 

コンボ処理のJumpSectionForCombo関数の中身です。

f:id:shiratori00:20201214190719p:plain

処理としてはコンボ受付可能状態であれば、次のセクションにアニメーション通知に設定されたセクションを次のセクションとしてMontageに設定しています。

そして、コンボ入力の受付フラグを折ります。

 

以上のものを実装した結果、下記の動画のような動作をします。

 

 

 

明日は@dgtanakaさんの記事になります。

 

 

【UE4】プラグインから別プラグインを参照する方法

汎用的な処理をプラグイン化して外だししたとき、プラグインで別のプラグインに存在する処理を使いたい場合の設定方法について知ったので、忘備録替わりに記事に残しておきます。

 

参考にした情報元はこちらのフォーラムにありました。

Plugin dependent on another plugin - UE4 AnswerHub

こちらでは4.17での情報になりますが、4.25での動作を確認しています。

 

使いたい処理のあるプラグイン:AdditionalPlugin 使う側のプラグイン:MyPlugin

と表記します。

MyPlugin.uplugin

"Modules" : [
    {
        "Name" : "MyPlugin",
        "Type""Runtime",
        "LoadingPhase" : "Default"
    }
],
//ここから追加
"Plugins": [
    {
        "Name" : "AdditionalPlugin",
        "Enabled" : true
    }
]
//ここまで
MyPlugin.Build.cs
PublicDependencyModuleNames.AddRange(
    new string[]
    {
        "Core",
        "CoreUObject",
        "Engine",
        "AdditionalPlugin",//追加
    }
);

AdditionalPluginの部分は適切にエクスポートされている名前を使用してください。基本的に.upluginファイルの名前と一致するはずなので、それを参考にすればいいと思います。

【UE4】Hololens2開発まとめ

Hololens2買いました。

 

なので、UE4での開発について公式のドキュメントの内容をまとめておきます。

 

 

UE4のバージョンは4.25.1で試してます。4.24以前だと使えないプラグインがあるので気を付けて下さい。

 

参考元は以下にまとめます。

 

Microsoftの公式ドキュメント。基本的にHololens2の機能の実装の仕方自体はこっちにしか載ってないので注意。

docs.microsoft.com

 

Epicの公式ドキュメント。ストリーミングとパッケージングの仕方だけ載ってる。

 

docs.unrealengine.com

環境構築ですが、EpicGamesLauncherで4.25.1をダウンロードして、オプションでHololens2をダウンロードします。

f:id:shiratori00:20200706225926p:plain

 

インストールが終わったら、日本語環境限定でエンジンソースに手を入れる必要があります。

Epic Games/UE_4.25/Engine/Source/Runtime/Core/Public/Misc/Char.h

の中身のコメントを全部消して上書き保存してください。

 

VisualStudioInstallerでUniversalWindowsプラットフォーム開発、C++デスクトップ開発、.NETデスクトップ開発をインストールしておきます。

f:id:shiratori00:20200713015538p:plain

 

以下の設定でプロジェクトを作成する

f:id:shiratori00:20200706231830p:plain

 

プロジェクトを作成したら、下記のプラグインをダウンロードして、Pluginsフォルダを作ってそこに入れます。

github.com

立ち上げる前に、uprojectファイルを右クリックからGenerateVisualStudioProjectFilesをやっておきます。

 

起動したらAugumentedRealityのHololens、VirtualRealityのMicrosoftWindowsMixedRealityにチェックを入れて再起動する。

f:id:shiratori00:20200706231605p:plain

f:id:shiratori00:20200706231609p:plain

レベルをEmptyLevelで作ります。

ARSessionConfigを作ります。

f:id:shiratori00:20200711003919p:plain

 

f:id:shiratori00:20200711003937p:plain

ブループリントでGameModeとPawnを作成して、GameModeのBeginPlayとEndPlayでARSessionの開始と終了処理を追加します。

 

f:id:shiratori00:20200711004114p:plain

 

作ったプレイヤークラスをデフォルトプレイヤーに設定します。

WorldSettingで作成したGameModeを設定します。

以上で必要な構築は終わりです。

 

次にデバッグの方法です。実機デバッグの仕方は主に2つです。

・パッケージ化して、DevicePortalから実機へデプロイして動かす

・HolographicRemotingを利用してPCから遠隔で起動する。

 

それぞれのメリットデメリットを記載します。

パッケージ化

メリット

・実際のアプリケーションの動作と同じ動作をする(当然ですけど

 

デメリット

・パッケージ化→デプロイの工程にそこそこ時間がかかる上に、Editor単体でどうにもならないので面倒くさい

 

HolographicRemoting

メリット

・簡単。VRPlayボタンを押すだけで速攻で起動できる。

 

デメリット

・そこそこ重い処理をしたりするとHololens2側で描画が消える。(空間マッピングワイヤーフレーム表示とかするとほとんど描画できなかったり)

 

それぞれの使い方としては、

パッケージ化:アプリの全体を通してのデバッグ等。HolographicRemotingでは描画されなくてどうしようもない場合

HolographicRemoting:テスト的に実装した機能をぱぱっと試したいとき

 

基本的にはHolographicRemotingでデバッグしてある程度まとまってきたらパッケージ化の流れで問題ないと思います。

 

やり方は下記にまとめてある公式ページがあるのでそちらを参照してください。

docs.unrealengine.com

 

docs.unrealengine.com

 

ここから各種機能の使い方をまとめます。

 

手の追跡

UXToolsプラグインのUxtHandInteractionActorを利用します。

 

これを使えば、対象のアクターにGenericManipurateコンポーネントをつけるだけで、物をつかんで移動、大きさ・回転の変更などのアクションを楽に行えます。 

f:id:shiratori00:20200710211739p:plain

UxtHandInteractionActorを生成する

 

BlueprintのInputAction/Axisを使う場合、MotionControllerを追加することと、CaptureGesturesであらかじめ取得するジェスチャーを設定しておく必要がある。

f:id:shiratori00:20200710232705p:plain

コンポーネントにMotionControllerの追加とCaptureDevicesの実行

f:id:shiratori00:20200711101934p:plain

Windows Spatial Input系列がHololens2のジェスチャーイベント

TapイベントはRelease時のみ反応

DoubleTapイベントはRelease時のみ反応。また、一回目のタップでTapイベントが反応

HoldはPress・Release両方に対応

ManipurationとNavigationは違いがよくわかんなかったです。Holdっぽい状態から手を動かすと反応しました。詳細がわかる人はだれか教えてください。もしくはわかったら追記します。

 

GenericManipurateコンポーネントを付けるとそのアクターの移動や回転ができます。

また、片手と両手でのアクターの操作方法も決められます。

f:id:shiratori00:20200711102348p:plain

GenericManipurateコンポーネントの詳細画面

ManipurationModeが片手か両手かその両方かの操作方法を選択します。

OneHandRotationModeは片手でつかんだ際のアクターの回転についてです。片手の場合確実に手にくっついてきます。

下記に軽く動作させてみた感じの結果をまとめました。

-Maintain Original Rotation : 回転はつかんだ時のそのオブジェクトの回転を保持したまま動きます

-Rotate About Object Center : 手の回転に合わせてアクター。アクターの中心を軸として回転します。

-Rotate About Grab Point : 手の回転に合わせてアクターが、つかんだ位置を中心軸として回転する?(正直よくわからんかったですけど

-Maintain Rotation to User : ユーザーの回転に合わせてアクターが回ります。ここのユーザーの回転は頭の向きのことです。ロール回転は特に変化なかったですが、首を縦横に動かしたら動きました。

-Gravity Aligned Maintain Rotation to User : 上記との違いはよく分からんかったです。

-Face User : 必ずユーザーの方を正面にアクターが回転します

-Face Away From User : アクターがユーザーの位置から逆向きにの方向に回転します。

TwoHandTransformModesは、両手でつかんだ際の挙動に関してです。これに関しては、移動・回転・拡縮のそれぞれを有効にするかどうかを選びます。デフォでは全部動きます。

Smoothingはどれだけなめらかに手にくっついてきたり、回転などの変化がされるかの値です。ただし、0にすると動かなくなります。1以上にしておくととりあえずは動きますが、標準の100で問題ないでしょう。

 

簡単なボタンも用意されています。SimpleButtonというクラスを継承したBlueprintを作るとすぐに使用できます。

f:id:shiratori00:20200711013520p:plain

 

イベントについて。また、下記の表記で、

ポインタは人指し指が近づいたときと遠い時に出るレーザーポインタの両方を指してます。

指は、人差し指のことを指してます。

BeginFocus:ポインタがボタンに当たったとき

UpdateFocus:ポインタがボタンに当たり続けてるとき

EndFocus:ポインタがボタンがから離れたとき

BeginPoke:指がボタンに触れたとき

UpdatePoke:指がボタンに触れ続けているとき。UpdateFocusと重複して発生する

EndPoke:指がボタンから離れたとき

ButtonPressed:ボタンが押されたとき

ButtonReleased:ボタンが離されたとき

 

になります。見た目の変更はBaseが土台、MovingVisualsがピンク色のボタン部分になります。StaticMeshで用意してください。

 

パラメータに関してですが、

MaxPushDistance:指でボタンの押せる距離。長いほどボタンの押せる距離が長くなる。土台を突き抜けることも可能

Pressed Fraction:遠い時のポインタでボタンをタップして押した際にボタンがどこまで移動するか。PushDistanceの遠いポインタ版

Released Fraction:ちょっとこれよくわかんないです。リリース時になんか起きるのかなと思っていろいろ値を変えましたが特に結果が変わらなかったので・・・。

Recovery Speed:Push時にボタンが戻るスピード

 

Handデータを直接取りたい場合は下記の記事に詳細が載ってます。自分でも触ったら別途なんか記事書くと思います。

docs.microsoft.com

 

視線入力

設定としてProjectSettingsからPlatforms>Hololens>CapabilitiesのGaze Inputにチェックを入れてください。

f:id:shiratori00:20200711115312p:plain

アイトラッキングの権限の設定

 

実際にアイトラッキングの使用はGetGazeData関数を使います。

f:id:shiratori00:20200711115517p:plain

OutGazeDataはアイトラッキングの結果が入っている構造体のEyeTrackerGazeDataです

ReturnValueはアイトラッキングが正常にデータを取得できたかの返り値です。目を閉じていたり、ちゃんとかぶれていない場合にはfalseを返します。

 

EyeTrackerGazeDataの中身についてですが、

・GazeOrigin : 目線の開始位置

・GazeDirection : 目線の向き

・FlaxionPoint : 常に0

・ConfidenceValue : データの信頼性。基本1

 

空間マッピング

空間マッピングを利用するには、ARSessionConfigでGenerateMeshDatafromTrackedGeometryとGenerateCollisionforMeshDataにチェックを入れる。

f:id:shiratori00:20200712173025p:plain

空間とあたり判定をとるには、LineTracebyChannelやSphereTracebyChannelなどのTraceのあたり判定をとることができます。

f:id:shiratori00:20200713014805p:plain

あとは認識した空間のメッシュ等の情報が入っているTrackingGeometoryの取得などが公式ドキュメントにはまとめてあります。

docs.microsoft.com

空間オーディオ

空間オーディオを利用するには、PluginのMicrosoftSpatialSoundを有効化して再起動。

f:id:shiratori00:20200708232357p:plain

ProjectSettings>Platforms>Hololens>AudioのSpatializationPluginにMicrosoftSpatialSoundを設定

f:id:shiratori00:20200711193855p:plain

次に減衰オブジェクトを作ります。

f:id:shiratori00:20200711203959p:plain

作成した減衰オブジェクトの設定のAttenuationSpatialization>SpatializationMethodをBinauralに設定してください。

f:id:shiratori00:20200712154537p:plain

その他の減衰設定は公式のドキュメントを参照してください。ここでは詳細の解説は省きます。

docs.unrealengine.com

 

AudioComponentがついたActorを用意します。AudioComponentの設定から先ほど作成した減衰オブジェクトを設定します。

f:id:shiratori00:20200712172233p:plain

また、公式のドキュメントに書いてあるのですが、空間オーディオで鳴らす音はモノラルである必要があるので注意してください。

 

あとは、適当な位置に置くとその方向から聞こえるようになるはずです。

音声入力

ProjectSettings>Engine>Input>SpeechMappingsにキーワードとアクション名を設定することで普通にInputActionと同じように使えるようになります。

f:id:shiratori00:20200713012727p:plain

SpeechMappingsに名前と反応するキーワードを設定

f:id:shiratori00:20200713012807p:plain

設定した名前のInputActionが使えるようになる

適当にキーワードを入れて試してみましたが、日本語の単語は反応せず、英語の単語のみ反応しました。英語の単語であれば、キーワードがひらがなで入力してあっても反応しました。

ただ、自分の発音がダメダメすぎて反応率が悪かったので、あんまり検証しきれてないです。

 

 

 

残り、空間アンカーとカメラとQRコードは検証不足なので、後日追記するか別記事でまとめるなりします。

 

【UE4】OculusQuestのHandtracking導入手順(Android開発環境構築も含む)※Quest2の動作確認済み

※10/15日追記 Quest2用ではプロジェクト設定のPlatform > Android でPackage For  Oculus Mobile devicesをQuest2に設定すれば動きます。

EpicGamesLauncherのUE4では現時点ではQuest2での動作はできませんでした。

※10/15日追記 UE4.26Preview3でのビルドを確認しました。プロジェクト設定をこの記事と同じようにすれば問題なく動きます。

ハンドトラッキングも使えるようになってたので、ハンドトラッキングやりたい場合でもエンジンビルドの必要がなくなりました。

f:id:shiratori00:20201015014252p:plain

 

現時点でUE4でハンドトラッキングを使用するためには、エンジンソースコードをビルドする必要があります。

 

公式のハンドトラッキング導入に関してはこちら

developer.oculus.com

 

developer.oculus.com

 

まずハンドトラッキングの機能が入ったUE4のエンジンソースコードをOculusのGithubからCloneしてきます。

エンジンソースコードは下記のリンクの場所にあります。

https://github.com/Oculus-VR/UnrealEngine/tree/4.25

 

Cloneした後は普通にUE4のエンジンソースコードのビルド手順をとります。

また、ビルドする際にVisualStudio2017を使用します。2019でできるかは未検証です。

手順自体はReadmeに書いてあるので簡単に。

・Setup.batを実行

・GenerateProjectFiles.batを実行

UE4.slnをVisualStudioで開く。

 -ソリューション構成:DevelopmentEditor

 -ソリューションプラットフォーム:Win64

・に設定しUE4ターゲットをビルド

 

ソースコードのビルド自体はいろんな人が開設してるので、詳しい内容などは適宜調べてください。

 

UE4が開くことができたら適当にプロジェクトを作ってください。

Blankでモバイルターゲットのプロジェクトで構いません。

 

作り終わったら一旦閉じます。

 

Androidの開発環境が必要になるので、そちらを構築します。

 

公式のAndroid開発環境構築に関しては下記のリンクの場所にあります。

docs.unrealengine.com

 

まずAndroidStudioをダウンロードします。バージョン指定がありますが、別に最新のバージョンでも問題なかったので、気にせず下記の場所からダウンロードします。

https://developer.android.com/studio

 

インストールが終わったらAndroidStudioの初期設定を行います。この時完了時にPCの再起動が必要になります。

再起動してきたらもう一度AndroidStudioを開きます。必要なバージョンのSDKやNDKをダウンロードしてきます。

起動時に右下のConfigureからSDKManagerを選択して開きます。

f:id:shiratori00:20200618070426p:plain

 

開いたら必要なSDKやらツール等をインストールしていきます。

まずQuestは25らしいので7.1.1が必要になります。

f:id:shiratori00:20200618071713p:plain

 

また、ツールはNDK、Google USB Driver、Android SDK Platform-Toolsあたりがあるといいと思います。

自分はとりあえずこの状態で動いたので参考にしてもらえると。

f:id:shiratori00:20200618071710p:plain

チェックボックスにチェックを入れ終えたら右下のApplyを押してください。するとインストール画面前に承認画面が入るので、中央下あたりにあるラジオボタンでAcceptを選択してからインストールを開始してください。

 

インストールが終わったら閉じてください。

 

そして、再度先ほど作ったUE4プロジェクトを開きます。

一応ランチャーにもその他というバージョンでプロジェクトがしれっと入ってるので、そちらからでもいいですし、直接.uprojectファイルをダブルクリックで起動しても構いません。

 

Oculus公式のQuestのQuickスタートは下記の場所にあります。

developer.oculus.com

 

プロジェクト設定を開きます。まずPlatformsのAndroidのカテゴリを選択してください。

SDKライセンスを承認してください。

Targetバージョンと最低Targetバージョンを25に設定します。

Enable Fullscreen Immersive on KitKat and above devices.にチェックを入れます。

f:id:shiratori00:20200618221304p:plain

 

Package for Oculus Mobile devices でプラスを押してOculusQuestを選択します。

その下のRemove Oculus Signature Files from Distribution APKにチェックを入れます。

f:id:shiratori00:20200618215322p:plain

次にPlatformsのAndroidSDKカテゴリに移動します。

ここではダウンロードしたSDK・NDK・JDKのパスを指定します。

スクショのパスは独自で決めたもので、初期設定では

C:\Users\ユーザー名\AppData\Local\Android\ あたりだったはずです。

JDKは初期設定なので参考にどうぞ。

SDKAPIレベルには android-25

NDKのAPIレベルにはandroid-21を指定します。

初期ではlatestですが、SDKを複数入れている場合、ビルドに失敗したので指定しています。一つならビルドが通るかは未検証です。

f:id:shiratori00:20200618215805p:plain

PluginsのOculusVRカテゴリに移動します。

MobileのHand Tracking SupportをControllers and Hands か HandsOnlyに設定します。

f:id:shiratori00:20200618221256p:plain

 

プロジェクト設定は以上です。

次はPawnを作成します。

Sceneコンポーネント一つにCamera、MotionControllerComponentを二つ、OculusHandComponentを二つです。詳しい階層構造は下のスクショを参考ください。

f:id:shiratori00:20200618225628p:plain

MotionControllerとOculusHandCompoentは右手と左手用に設定をそれぞれ変更してください。

下のスクショは右手版になります。SkeletonTypeとMeshTypeを親のMotionControllerの手と同じ側にすればいいです。

あとQuestは6DoFなので、FloorLevelでとりあえず初期化しておく必要があります。

していないと、アプリ起動時の位置が初期位置になるので、立ったまま地面に埋まるとかそういうことが起こります。

f:id:shiratori00:20200619001016p:plain

 

あとはGameModeを作ってDefaultPlayerに設定するか、直接レベル上に配置してInputを受け取れるようにしてください。

 

PCにQuestをつなげて起動をすればハンドトラッキングができると思います。

 

 

ちなみに、OculusHandComponentのInitializePhysicsにチェックを入れるとBlock系のあたり判定を生成してくれるようになり、物を殴り飛ばしたりできるようになります。