エンジニアっぽくなりたい

UnityやUnrealEngine 4などでなにか役に立ちそうなことを発信していく。

UE4 シーケンサーで実行中に対象を置き換える

シーケンサーで設定したものを、ランタイムのキャラクターに置き換えなきゃいけないことって多々ありますよね。
着せ替えとかができるゲームだと、その時設定している衣装でカットシーンにでてきてほしい、みたいな。
一応公式のドキュメントにもやり方が書いてありましたが、「GetSequenceBinding」ノードを使う方法がなかなか納得できなかったので別の方法を検討してみました。


目標


シーケンサーで指定したキャラクターを実行時のキャラクターで置き換えてシーケンサーで指定した動作をさせる。

環境

・UE 4.27.0 (Gitのコミット:99b6e203a15d04fc7bbbf554c421a985c1ccb8f1 をビルド)
・VisualStudio 2019 16.11.2


準備

置き換え対応をするための準備をします。
今回はUE4のテンプレートから「サードパーソン」を選択した前提で進行します。(いつもサードパーソンですが。)

まずは以下の必要なデータを作成します。
・LevelSequence上で指定するキャラクター
・置き換え対象となるキャラクター
・置き換えの対象となるLevelSequence

LevelSequence上で指定するキャラクター

「ThirdPersonCharacter」とします。

置き換え対象となるキャラクター

「ThirdPersonCharacter」を継承したBPを作成します。
名前は「BP_PlayerCharacter」とし、今回は以下のような変更を行います。
・SkeletalMeshを「SK_Mannequin_Female」に変更。
・マテリアルを赤く設定。
こんな感じ
f:id:naoxgames:20211016143457p:plain

その後、ゲームモードのDefaultPawnを「BP_PlayerCharacter」にします。

置き換えの対象となるLevelSequence

今回は以下のようにただ右から左へ走るモーションをするだけのものを作りました。
名前は「LS_Run」とします。
元のままだとAnimBPにスロットの影響がないので、以下のように「DefaultSlot」を通るようにします。
f:id:naoxgames:20211016143657p:plain

シーケンサーで指定するキャラクターは「ThirdPersonCharacter」で、「Spawnable」で設定します。
f:id:naoxgames:20211016141550p:plain
f:id:naoxgames:20211016141626p:plain


簡単にやってみる(Level上で完結パターン)

まずは簡単にやってみる。
作戦としてはレベルに配置した「LevelSequenceActor」の詳細にある「BindingOverrides」を使用します。
f:id:naoxgames:20211016144616p:plain

メリットとしては
 簡単、詳細をいじる程度で完結する。

デメリットとしては
 レベル上にあらかじめすべての要素が揃っている必要があり、動的な変化に適用できない。
 シーケンサーで変更(追加削除など)をかけた場合関連個所すべてを修正する必要がある。
という感じ。


Level上に配置&設定をする

レベルに以下を設定して配置します。

BP_PlayerCharacter

レベルに配置し、詳細から「AutProcessPlayer」を「Player0」にする。(実行時にプレイヤーのキャラクターとして扱われる。※さっきまで知らなかった)
PlayerStartがあるなら消しておきます。

LS_Run

レベルに配置し、詳細から「自動プレイ」にチェックを入れます。(実行時即再生されます。)
詳細から「BindingOverrides」に以下のように設定します。
f:id:naoxgames:20211016145651p:plain
「ObjectBindingId」がシーケンサー上のBinding名で、複数置き換えたい対象がある場合は「+」でそれぞれ指定します。
「BoundActor」はレベルに配置されているActorです。


実行!!!!


f:id:naoxgames:20211022002952p:plain
※画像でも動画でも伝わらない気がしますが置き換えできてるんです。

という具合で置き換えできました!
アニメーションはスケルトンが一致していないといけないとか、コンポーネントシーケンサーで設定している場合はそれが付いていないといけないので、気を付ける必要はあります。

できたけど、もはやこのやり方が通用するシチュエーションがいまいち浮かびません。
「GetSequenceBinding」ノード使うパターンのほうがまだましな気がする。



これだと全然ダメなのでもう少し良い方法を検討。


やってみる(BPで処理するパターン)

作戦としては同じく「LevelSequenceActor」の詳細にある「BindingOverrides」を使用します。
f:id:naoxgames:20211016144616p:plain

今回は各「LevelSequence」をBP化し、あらかじめBP側で必要な設定をしておくパターンです。
また、動的に生成されたオブジェクトに対する置き換えも検討した対応になります。

メリットとしては
 シーケンサーの担当者はBPの詳細をいじるだけ。
 動的な変化にも対応できる。

デメリットとしては
 LevelSequence毎にBPを作成する必要があるため、アセットが増える。
 シーケンサーで変更(追加削除など)をかけた場合関連個所すべてを修正する必要がある。

という感じ。


LevelSequenceActorのBPを作成、設定する

コンテンツブラウザから「LevelSequenceActor」のBPを作成します。
ここでは「BP_LS_Run」とします。
f:id:naoxgames:20211020005540p:plain
作成したBPを開き、詳細から「LevelSequence」の項目に対象の「LevelSequence」を指定します。
f:id:naoxgames:20211020005635p:plain
ここでは「LS_Run」とします。

BindingOverridesの対象を指定する

置き換え対象の「ObjectBindingID」を「BindingOverrides」に設定する必要があります。
「LevelSequenceActor」では「MovieSceneObjectBindingID」型の変数を作ると、設定されている「LevelSequence」の「ObjectBinding」の情報を指定できるようになります。
f:id:naoxgames:20211020010345p:plain
f:id:naoxgames:20211020010847p:plain

シーケンサーが設定されている場合もそれぞれの中から指定することができます
f:id:naoxgames:20211020011425p:plain
これは便利!!!!

※「FMovieSceneObjectBindingIDCustomization」というクラスでエディター拡張されているようで、親クラスである「FMovieSceneObjectBindingIDPicker」あたりで関連する処理が行われているようです。
このあたりは調べて後ほど書こうと思います。


ここでは「PawnBindingID」という変数を作り、シーケンサーの置き換えたいキャラクターの「ObjectBindingID」(この場合は「ThirdPersonCharacter」)を指定しましょう。
f:id:naoxgames:20211021225543p:plain


次に、公式のドキュメントでも使ってる「AddBinding」ノードで、「BindingOverrides」に置き換えたい対象を設定します。

LevelSequenceActor.cpp
void ALevelSequenceActor::AddBinding

を見るとわかりますが、「AddBinding」の中で「BindingOverrides」に置き換え対象の設定を行っています。
なので、「AddBinding」を使うことで動的に置き換え対象を指定できます。ヤッタゼ!

ここでは試しに以下のようなBPノードを設定してみます。
f:id:naoxgames:20211021231733p:plain
これは現在レベルに存在する「BP_PlayerCharacter」クラスのBPを探し、一つ目に見つけたものを「PawnBindingID」で指定した「ObjectBindingID」に置き換える、というような内容になります。


実行!!!!

上で設定した「BP_LS_Run」をレベルに配置し、「自動プレイ」にチェックを入れて実行してみましょう。

f:id:naoxgames:20211022003137p:plain
※「BP_LS_Run」はレベルに配置時「BindingOverrides」は未設定ですが、実行されるとBPで処理された置き換え設定により「BindingOverrides」が設定されます。

置き換わった!!!!

すべてのシーケンサーをBP化(サブシーケンサーは不要)する必要がありますが、こうすることで、実行前とかで置き換え対象を取得し、あらかじめ設定しておいた「ObjectBindingID」を使って置き換える設定、というのが可能になります。
普通に作ってると複数のサブシーケンサーがあって、各カット毎に置き換えたいキャラクターが設定されているので、今回の「PawnBindingID」みたいなものは配列で持ち、その分設定する必要が出てくると思います。
また、その時の会話対象や敵とかも置き換えたいとかあれば、それも別変数で設定する必要がありそうです。


まだまだ課題はありますが、ひとまず状況に応じた置き換えをしたい場合に対応可能で、「GetSequenceBinding」ノードを使うパターンよりはいいのかなと思っています。


Tagという男

別の方法として、

LevelSequenceActor.cpp
void ALevelSequenceActor::AddBindingByTag

f:id:naoxgames:20211021234231p:plain
という、シーケンサーで「ObjectBinding」に対してタグを設定し、そのタグに対して一括置き換え設定をするパターンもあります。
こんな感じで設定して
f:id:naoxgames:20211021235939p:plain

こんな感じで表示される
f:id:naoxgames:20211022000006p:plain


今回こっちを使わなかったのは、タグは基本的に文字列を指定をする必要があり、打ち間違いなどのリスクがあるにはあります。
実際の業務で文字列はあんまり打たせたくないな~という個人的な気持ちから、今回は使いませんでした。
「MovieSceneObjectBindingID」は「LevelSequence」に設定されているものがプルダウンで表示されてその中から選ぶ形式のため、間違えにくいと考えています。

「AddBindingByTag」を使う場合は、私としては
・タグは特定の種類から選択式にする改造
・特定のアクターを設定すると自動で適切なタグが付く改造
・特定の種類以外のタグが指定されていないか検出、置き換えなどをするツールを作る
などでケアが必要かなと思います。
逆にそういうケアさえすれば「AddBindingByTag」のほうがとても便利かと思います!!
・・・・もしかしたら後々そういうのを記事で書くかも・・・・?わかりませんが。


思ったこと

という感じでやってみたわけですが。
エンジン改造なしだとこんな感じかなぁという印象です。
やはり使い勝手の悪さは残る気がしているので、近々エンジン改造ありで、どれくらい便利にできるか検討してみます。多分。
改造したところで、ある程度の条件下で自動的に置き換えるって感じになると思うので、逆に置き換えたくない場合にトラブルになったりする可能性もありそう・・・・?

ここはPlus Ultraの精神でさらに向こうへ行こうと思います。





正しい対応方法やより良い方法があればご指摘お願いいたします!!!


UE4 LevelSequenceDirectorを使いまわす~with ちょっとエンジン改造~

最近ヒロアカにハマっています。PlusUltraって言葉がとても好きです。
さらに向こうへ!!って感じの意味らしい。
私もエンジン改造から逃げずにさらに向こうへ行きたいところです。


前回の記事
naoxgames.hatenablog.jp

「LevelSequenceDirectorを使いまわす」の内容で、ちょっと改造するとちょっと面倒が減らせるなぁ、というところがあったので対応してみようと思います。


目標


「LevelSequenceDirector」が生成される瞬間から「BP_MyLevelSequenceDirector」に置き換える。
都度親クラスの変更を行う必要をなくしたい!!


環境

・UE 4.27.0 (Gitのコミット:99b6e203a15d04fc7bbbf554c421a985c1ccb8f1 をビルド)
・VisualStudio 2019 16.11.2


やってみる

作戦としてはC++でのクラス「ULevelSequenceDirector」を検索し、生成される瞬間の対象クラスを入れ替える。
入れ替える方法はiniファイルを使用する。

置き換え準備

プロジェクトのConfigフォルダにある「DefaultEditor.ini」に以下を追記します。

[EngineOverride]
LevelSequenceDirector="BlueprintGeneratedClass'/Game/LevelSequenceTest/LevelSequenceDirectorTest/BP_MyLevelSequenceDirector.BP_MyLevelSequenceDirector_C'"

※[EngineOverride]や「LevelSequenceDirector」の文字列はなんでも問題ありません。
クラスパスは前回の記事で作成した「BP_MyLevelSequenceDirector」のパス+「_C」です。

ここに記述した情報を使い、エンジン内で自作プロジェクト側のクラス(BPクラス?)を呼びます。


ULevelSequenceDirector生成場所を探す

「ULevelSequenceDirector」でエンジンを検索してみると MovieSceneSequenceEditor_LevelSequence.h
CreateBlueprintForSequence 関数内で

Blueprint = FKismetEditorUtilities::CreateBlueprint(ULevelSequenceDirector::StaticClass(), InSequence, BlueprintName, BPTYPE_Normal, UBlueprint::StaticClass(), UBlueprintGeneratedClass::StaticClass());

このような処理が行われており、ここで生成されているようです。
この「ULevelSequenceDirector::StaticClass()」を置き換えたいです。


エンジン改造

該当箇所のクラスを置き換える処理を行います。
以下、MovieSceneSequenceEditor_LevelSequence.hのCreateBlueprintForSequence改造後の内容になります。
コメントで挟まれている箇所が追加、変更箇所です。

  virtual UBlueprint* CreateBlueprintForSequence(UMovieSceneSequence* InSequence) const override
    {
        UBlueprint* Blueprint = GetBlueprintForSequence(InSequence);
        if (!ensureMsgf(!Blueprint, TEXT("Should not call CreateBlueprintForSequence when one already exists")))
        {
            return Blueprint;
        }

        ULevelSequence* LevelSequence = CastChecked<ULevelSequence>(InSequence);
        //***********************************************
        //改造:LevelSequenceDirectorのClassを置き換える追加
        FString OverrideClassName;
        GConfig->GetString(TEXT("EngineOverride"), TEXT("LevelSequenceDirector"), OverrideClassName, GEditorIni);
        TSubclassOf<class UObject> LevelSequenceDirectorClass = LoadClass<ULevelSequenceDirector>(NULL, *OverrideClassName, NULL, LOAD_NoWarn, NULL);

        if (LevelSequenceDirectorClass == nullptr)
        {
            LevelSequenceDirectorClass = ULevelSequenceDirector::StaticClass();
        }
        //ここまで
        //***********************************************
        
        FName BlueprintName = "SequenceDirector";
        
        //***********************************************
        //改造:LevelSequenceDirectorClassを指定するよう変更
        Blueprint = FKismetEditorUtilities::CreateBlueprint(LevelSequenceDirectorClass, InSequence, BlueprintName, BPTYPE_Normal, UBlueprint::StaticClass(), UBlueprintGeneratedClass::StaticClass());
        //Blueprint = FKismetEditorUtilities::CreateBlueprint(ULevelSequenceDirector::StaticClass(), InSequence, BlueprintName, BPTYPE_Normal, UBlueprint::StaticClass(), UBlueprintGeneratedClass::StaticClass());
        //ここまで
        //***********************************************
        
        Blueprint->ClearFlags(RF_Standalone);

        LevelSequence->SetDirectorBlueprint(Blueprint);
        return Blueprint;
    }



Plus ultra !!!!

見える形で示すのが少し難しいですが、以下のような感じです。

このように、作成したばかりのシーケンサーの「LevelSequenceDirector」の初期の親クラスがすでに置き換わっている状態にできました。
こうすることでいくらか楽できるようになったのかなと思います。
一応これでも問題はあり、すでに作成済みの「LevelSequenceDirector」は置き換わらないです。


思ったこと

ちょっとしたエンジン改造でワークフローの改善につながるものは多いと思います。
データ作成をする人のミスが減る環境を作れたら結果エンジニアが楽な未来にたどり着くこともありますよね。
エンジン改造がすべてではありませんが、適切な対応をできるようになっていきたいものです。




正しい対応方法やより良い方法があればご指摘お願いいたします!!!


UE4 LevelSequenceDirectorを使いまわす

シーケンサーでイベントトラックを使ってBPで処理を作る、とかやりますよね。
プロジェクトによってはそれをデザイナーやプランナーに設定してもらう、という場合もあると思います。
シーケンサー毎に毎回イベントトラックで〇〇ノードを呼び出して、このピンをつなげて・・・・・なんて説明するのも面倒ですよね。
要はシーケンサーが作られた段階でイベントトラックに必要な処理が準備されていて、イベントトラックを作成したらBPいじらずに目的のイベントを仕込めたら、きっと楽になるはずだ!


目標


シーケンサーのイベントトラックでイベントを設定するのを楽にしたい!
汎用的なイベントは共通化して処理は一か所にしたい。

※今回はエンジン改造はしません。


環境

今回からエンジンビルドします。
・UE 4.27.0 (Gitのコミット:99b6e203a15d04fc7bbbf554c421a985c1ccb8f1 をビルド)
・VisualStudio 2019 16.11.2 ※バージョンアップしました。


やってみる

作戦としてはLevelSequenceDirectorを継承したクラス&BPを作ってそれに置き換えます。
LevelSequenceDirectorは、シーケンサーでイベントトラックを使った際に処理を書くBPのことです。

LevelSequenceDirectorを開く

開き方は
シーケンサーでイベントトラックを作成し、キーを打ちます。
f:id:naoxgames:20210913212608p:plain
そのキーをダブルクリックすると以下のようなBPが開きます。
f:id:naoxgames:20210913212728p:plain
これの右上を見るとわかりますが、こいつが噂の「LevelSequenceDirector」です。


LevelSequenceDirector継承したクラス&BPを作成

C++クラスを追加で「LevelSequenceDirector」を選択します。
ここでは「MyLevelSequenceDirector」とします。
f:id:naoxgames:20210913213840p:plain

次にブループリントを作成します。
「MyLevelSequenceDirector」を選択します。
ここでは「BP_MyLevelSequenceDirector」とします。
f:id:naoxgames:20210913220448p:plain

これでクラスとBPを作成できました。

汎用処理を作成

カットシーンでよくありそうな汎用処理を作成します。
ここでは
・一時停止
・再開
Widget生成(クリック待ち)
を作成してみます。


まずは「一時停止」と「再開」を作成します。
これらはC++クラスに記述しようと思います。
以下のようにします。

・MovieSceneモジュール追加
Project.Build.cs

~
    PublicDependencyModuleNames.AddRange(new string[] { "MovieScene" });
~


・処理作成
MyLevelSequenceDirector.h

#pragma once

#include "CoreMinimal.h"
#include "LevelSequenceDirector.h"
#include "MyLevelSequenceDirector.generated.h"

UCLASS()
class PROJECT_API UMyLevelSequenceDirector : public ULevelSequenceDirector
{
    GENERATED_BODY()
    
public:
    UFUNCTION(BlueprintCallable)
        void Pause();

    UFUNCTION(BlueprintCallable)
        void Resume();

};


MyLevelSequenceDirector.cpp

#include "MyLevelSequenceDirector.h"
#include "LevelSequencePlayer.h"

void UMyLevelSequenceDirector::Pause()
{
    Player->Pause();
}

void UMyLevelSequenceDirector::Resume()
{
    Player->Play();
}


これで作成できました。
「LevelSequenceDirector」では「ULevelSequencePlayer」型の「Player」が使えるので、シーケンサーを操作する処理がかなりやりやすいです。

次にWidget生成(クリック待ち)を作成します。
これはBP側で作成します。
ウィジェットブループリントを作成します。
ここでは「WBP_ClickWait」とします。
f:id:naoxgames:20210913222051p:plain

パレットから「Button」を探し、配置します。
そうすることで「階層ビュー」にButtonが表示されます。
f:id:naoxgames:20210913222321p:plain
ボタンは好きなように整えてください。
私は以下のようにしました。
f:id:naoxgames:20210913235759p:plain

次にボタンに押されたときのイベントを設定します。

「階層ビュー」から「Button」を選択し、「詳細ビュー」のイベントから、「OnClicked」を選びます。
f:id:naoxgames:20210914000650p:plain

以下のようなイベントが作成されます。
f:id:naoxgames:20210914000854p:plain

次にイベントディスパッチャーを作成し、呼び出すようにします。
「WBP_ClickWait」の「マイブループリントビュー」から「イベントディスパッチャー」の右にある「+」を押して追加します。
ここでは「OnClickedDelegate」とします。
f:id:naoxgames:20210914001139p:plain

先ほど作成した「Button」の「OnClicked」のイベントから呼び出すようにします。
ドラッグアンドドロップで「呼び出し」を選択する
f:id:naoxgames:20210914001417p:plain
 ↓
f:id:naoxgames:20210914001434p:plain

これでWidgetの対応はできました。


次に「BP_MyLevelSequenceDirector」でクリック待ちのイベントを作成します。

ノードは以下のようにします。
f:id:naoxgames:20210914002044p:plain
簡単に説明します。
上記では「WaitClick」というカスタムイベントを作成。
「UMyLevelSequenceDirector」で作成した「Pause」を呼び、「WBP_ClickWait」を生成して表示。
「WBP_ClickWait」の「OnClickedDelegate」に、ボタンが押されたとき用のカスタムイベントを新たに作成して設定。
「UMyLevelSequenceDirector」で作成した「Resume」を呼び、Widgetを削除しています。


これで汎用処理の準備ができました!!!
(「Pause」とか「Resume」とか作りましたけど、結局使うのは「WaitClick」だけですね。。。)



LevelSequenceDirectorを置き換え

「LevelSequenceDirector」を汎用処理を設定したBPに差し替えます。

まずはシーケンサーのイベントトラックからキーをダブルクリックして「LevelSequenceDirector」に入ります。
この画面です。
f:id:naoxgames:20210913212728p:plain

次に「クラス設定」を押し、「詳細ビュー」から「親クラス」の項目を選択し、「BP_MyLevelSequenceDirector」を選択します。
f:id:naoxgames:20210914002845p:plain

そしてイベントグラフから「WaitClick」を検索すると・・・・
f:id:naoxgames:20210914003119p:plain
ある!!!!!


イベントの設定の仕方

シーケンサーの「LevelSequenceDirector」に入ってからイベントにつなげるのでも問題ありませんが、せっかくなのでシーケンサーから直接設定してみましょう。

シーケンサーのイベントトラックから直接指定することができます。
f:id:naoxgames:20210914003442p:plain
キーを右クリックし、プロパティのイベントからクイックバインドを選択すると上記のように先ほど作成したイベントが呼べるようになっています。
※イベントにすでに設定されている場合はクリアを選択すると選べるようになります。
※引数があるようなイベントの場合、上記のプロパティ内に設定する値が表示され、指定することができるようになります。(ObjectBindingIDとかActorとかも指定できちゃう)

新しいLevelSequenceを作成しても、「LevelSequenceDirector」の親クラスを今回作成したものに変えることで同じ機能が揃った状態になります。

便利!!!!!!!!!!

ちなみに、残念なことに、ここでイベント選んでも、直接そのイベントになるのではなく、都度カスタムイベントが作られる形になります。
こんな感じ
f:id:naoxgames:20210914004213p:plain

シーケンサーで今設定しているイベントが何かを確認しようとすると以下のように、カスタムイベント名が表示されるため、中身がわかりません
f:id:naoxgames:20210914004246p:plain

カスタムイベントが作成されることは避けられませんが、カスタムイベントの名前は変えられます。
カスタムイベント名をわかりやすいものにすることで、シーケンサーからの視認性もよくなると思います。
f:id:naoxgames:20210914004814p:plain


これ、結構便利じゃないですか?各シーケンサーで書かされるより、一つのBPで管理できるほうが楽ですよね。

実行!!!!

ただキャラが左に行くだけのシーケンサーを作ってみました。
途中で「WaitClick」が処理され、一時停止され、ボタンを押すと再開する、というものです。
f:id:naoxgames:20210914005351p:plain
これが以下のようになります。


ちゃんと一時停止して、ボタンを押すと再開していますね。
シーケンサー作成のたびにそれ用の処理を組むという手間が省けるので、かなり使いやすいのではないかと思います。


Plus ultra !!!!

以下の記事で少しエンジン改造をすることで親クラスを始めから任意のクラスに置き換える対応をしてみました。
興味がありましたら見てみてください。
naoxgames.hatenablog.jp



思ったこと

LevelSequenceDirectorについて調べてみるとほとんど情報が出てきませんでした。
今回の対応は内容は全く難しくないですが、恩恵は大きいと思っています。
もしあまり知られていないのであれば是非使っていただきたい!
(もしかしてみんな普通にやってることなのかな・・・・?おれはこの前まで知らなかったよ!!)




正しい対応方法やより良い方法があればご指摘お願いいたします!!!


UE4 AnimBPを使いまわす方法

UE4でアニメーションを制御するときにAnimationBlueprint(以後AnimBP)を使うことがありますよね。
私の認識では、スケルトンが異なる場合は別のAnimBPを作成する必要があり、AnimGraphなどの処理を別のスケルトンに使い回す方法がないと思っていました。
このままでは、同じような動作をするキャラクターに、追加で処理を加えるとき、C++でできない変更の場合はキャラ数分変更をする必要があるということになりますよね。
流石に不便だよなーーと思っていたのですが、いい感じの方法があったので紹介してみます。


※この記事の続きとしてより便利に使える方法も記事にしています。合わせて見ていただけると良いかと思います。

naoxgames.hatenablog.jp




目標


AnimBPを異なるスケルトンで使い回す!
今回は、LookAtノードを設定したAnimBPを別スケルトンでも使い回し、同じように腰や首が対象物の方を向くようにしてみます。
※汎用性を高めるためにCustomAnimNodeとかにも手を出します。


環境

・UE 4.26.1 (エンジンビルドはしていません)
・VisualStudio 2019 16.7.2


やってみる

作戦、というかもはや本質ですが、AnimBPを継承します。
まずは継承元となるAnimBPを作成し、LookAtの設定をします。

AnimBP作成して視線制御

コンテンツブラウザから右クリックでAnimBPを作成しましょう。
ケルトンの指定は「UE4_Mannequin_Skeleton」とします。
f:id:naoxgames:20210811231212p:plain
f:id:naoxgames:20210811231445p:plain

注視の設定します。
以下の画像のように設定してみます。
f:id:naoxgames:20210811232021p:plain
内容としては、
・適当なアニメーションを指定(ここではThirdPersonIdle)
・注視点を示すVector3の変数を作成(ここではLookAtPos)
・「注視点(LookAt)」ノードを3つ配置して、「Bone to Modify」に上から「head」「neck1」「spine_03」を指定し、「LookAtLocation」のピンを公開します。
f:id:naoxgames:20210811232352p:plain
・LookAtPosを各注視点ノードの「LookAtLocation」ピンにつなげる。
・「注視点(LookAt)」ノード3つの「Alpha」ピンの値を合計1.0になるように設定する(注視点を見るときにその骨が動く割合を示します。)
・出力ポーズにつなげる。

これで注視の設定ができました。
設定がうまくいっていると真下を向いているはずです。
f:id:naoxgames:20210811232708p:plain


継承してAnimBP作成

コンテンツブラウザ上で、前の手順で作成したAnimBPを右クリックして「子ブループリントクラスを作成します」をクリック。
f:id:naoxgames:20210811232814p:plain
これで子AnimBPが作成されました。


AnimBPの対象スケルトンを変更

別のスケルトンをAnimBPに設定します。
ここでは、以下のアセットが無料だったのでこれを利用して動物2匹ほど動かしてみます。
marketplace-website-node-launcher-prod.ol.epicgames.com

AnimBPのスケルトンですが、継承できるのは知っていましたが、スケルトンに依存していると認識していたため使うことがありませんでした。
がしかし!!!!
継承したAnimBPで対象のスケルトンを変更することができます。
AnimBPの「クラス設定」から設定できます。
f:id:naoxgames:20210811233413p:plain

ここでこの継承したAnimBPに別のスケルトンを設定しましょう。
ここでは「SK_DeerStag_Skeleton」と「SK_Pig_Skeleton」を設定してみます。

ケルトンを変更して、一度そのAnimBPを閉じて開きなおすと、スケルトンに応じたスケルタルメッシュがプレビューに表示されます。
f:id:naoxgames:20210811233658p:plain

LookAtを拡張したAnimNodeを作成する

AnimBPを継承してスケルトンを変更することで異なる骨でもロジックを使いまわすことはできました。
しかし、LookAtノードのように、詳細ビューから直接骨を指定しているパターンもあり、その値を継承先で上書きできなければ大きな問題になります。
そのため、今回は「AnimInstance」を継承したクラスと、LookAtノード一式を継承・コピーしたCustomAnimNodeを作成してみます。
LookAtノードのクラスは「AnimGraphNode_LookAt」「AnimNode_LookAt」となり、これらを継承、もしくはコピーした新クラスを作成します。
それに合わせてモジュールの設定もお願いします。(今回はランタイムのモジュールに書いてしまいますが、実際はEditorと分けないとパッケージ化するときにエラーになります。)


ここでは
MyAnimInstance      → AnimInstanceを継承したクラス
MyAnimGraphNode_LookAt  → AnimGraphNode_LookAtをコピーしたクラス
MyAnimNode_LookAt    → AnimNode_LookAtを継承した構造体
とします。


作戦としては、MyAnimInstanceに設定された値をMyAnimNode_LookAtで受け取って処理を行うようにします。
MyAnimInstanceに設定された変数は、AnimBPの「クラスのデフォルト」からAnimBP毎に設定ができるため、これで継承先で別の設定ができます。
※本当はキャラによってLookAtの対象にする骨の数が違う可能性があるため、それも考慮した実装にしたいが、今回は単純にAnimNode_LookAtの機能を改造するだけにします。

それぞれコードは以下のようにします。

Build.cs

~
        PublicDependencyModuleNames.AddRange(new string[] { "AnimGraphRuntime" });

        //Editor用
        PublicDependencyModuleNames.AddRange(new string[] { "BlueprintGraph" });
~



MyAnimInstance.cpp

#pragma once

#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "BoneContainer.h"

#include "MyAnimInstance.generated.h"


USTRUCT(BlueprintType)
struct FBoneInfo
{
    GENERATED_USTRUCT_BODY()
    UPROPERTY(EditAnywhere)
    FBoneReference BoneRef;

    UPROPERTY(EditAnywhere)
    FVector RotAxis;

};

UCLASS()
class PROJECT_API UMyAnimInstance : public UAnimInstance
{
    GENERATED_BODY()
    
public:
    UPROPERTY(EditAnywhere, Category = SkeletalControl)
        TMap<FString, FBoneInfo> BoneMap;
};


MyAnimGraphNode_LookAt.h
※基本はAnimGraphNode_LookAt.hのコピーとなるため、変更した箇所のみ記述します。Nodeの型を変えただけです。

~
#include "MyAnimNode_LookAt.h"
#include "MyAnimGraphNode_LookAt.generated.h"


UCLASS(meta = (Keywords = "Look At, Follow, Trace, Track"))
class PROJECT_API UMyAnimGraphNode_LookAt : public UAnimGraphNode_SkeletalControlBase
{
    GENERATED_UCLASS_BODY()

    UPROPERTY(EditAnywhere, Category = Settings)
        FMyAnimNode_LookAt Node;
~
};


MyAnimGraphNode_LookAt.cpp
※基本はAnimGraphNode_LookAt.cppのコピーとなるため、変更した箇所のみ記述します。Nodeの名前変えただけです。

~
#include "MyAnimGraphNode_LookAt.h"
#include "AnimGraphNode_LookAt.h"
#include "Animation/AnimInstance.h"
#include "UObject/AnimPhysObjectVersion.h"
~
#define LOCTEXT_NAMESPACE "MyAnimGraph_LookAt"
~
FText UMyAnimGraphNode_LookAt::GetControllerDescription() const
{
    return LOCTEXT("LookAtNode", "My Look At");
}
~
FText UMyAnimGraphNode_LookAt::GetNodeTitle(ENodeTitleType::Type TitleType) const
{
~
        if (TitleType == ENodeTitleType::ListView || TitleType == ENodeTitleType::MenuTitle)
        {
            CachedNodeTitles.SetCachedTitle(TitleType, FText::Format(LOCTEXT("MyAnimGraphNode_LookAt_ListTitle", "MyAnimLookAt{ControllerDescription} - Bone: {BoneName}"), Args), this);
        }
        else
        {
            CachedNodeTitles.SetCachedTitle(TitleType, FText::Format(LOCTEXT("MyAnimGraphNode_LookAt_Title", "MyAnimLookAt{ControllerDescription}\nBone: {BoneName}"), Args), this);
        }
~
}
~


MyAnimNode_LookAt.h

~
#pragma once

#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "BoneIndices.h"
#include "BoneContainer.h"
#include "BonePose.h"
#include "BoneControllers/AnimNode_SkeletalControlBase.h"
#include "BoneControllers/AnimNode_LookAt.h"
#include "CommonAnimTypes.h"
#include "MyAnimNode_LookAt.generated.h"

USTRUCT()
struct PROJECT_API FMyAnimNode_LookAt : public FAnimNode_LookAt
{
    GENERATED_USTRUCT_BODY()

    UPROPERTY(EditAnywhere)
        FString BoneKey;

    virtual void Initialize_AnyThread(const FAnimationInitializeContext& Context) override;
};


MyAnimNode_LookAt.cpp

#include "MyAnimNode_LookAt.h"
#include "MyAnimInstance.h"
#include "Animation/AnimInstanceProxy.h"

void FMyAnimNode_LookAt::Initialize_AnyThread(const FAnimationInitializeContext& Context)
{
    UObject* animInstanceObj = Context.AnimInstanceProxy->GetAnimInstanceObject();
    UMyAnimInstance* animInstance = Cast<UMyAnimInstance>(animInstanceObj);
    if (animInstance != nullptr && animInstance->BoneMap.Find(BoneKey) != nullptr)
    {
        BoneToModify = animInstance->BoneMap[BoneKey].BoneRef;
        LookAt_Axis.Axis = animInstance->BoneMap[BoneKey].RotAxis;
    }

    FAnimNode_LookAt::Initialize_AnyThread(Context);
}



これでAnimBPを使い回すための新LookAtノードが作成できました。


キャラ毎に設定する

キャラ毎に設定をして、違うスケルトンだけど同じロジックが動いている状態を作ります。


まずは継承元の注視ノードを先ほど作成したMyLookAtに差し替えます。
ノードの詳細から、「BoneKey」を設定します。
f:id:naoxgames:20210812002855p:plain
上から順に「head」「neck」「spine」とします。

AnimBPのクラスのデフォルトの設定は以下のようにします。
UE4_Mannequin_Skeleton
f:id:naoxgames:20210812002615p:plain 「head」「neck」「spine」それぞれに適切な骨を設定します。
「RotAxis」は(0,1,0)にします。

先ほど継承して作成した動物用のAnimBPのクラスのデフォルトにも設定を行います。
動物は骨の軸が「UE4_Mannequin_Skeleton」と違うようなので合わせて「RotAxis」を設定します。
f:id:naoxgames:20210812011520p:plain

SK_DeerStag_Skeleton
f:id:naoxgames:20210812003235p:plain

SK_Pig_Skeleton
f:id:naoxgames:20210812003400p:plain


あとはAnimBPの注視点を示す「LookAtPos」に注視する座標を設定します。
BPとかで対象を受け取ってAnimBPの「LookAtPos」に代入したりしましょう。

これで準備はできた!!!


実行!!!!!

f:id:naoxgames:20210812012323p:plain


こんな感じになった!みんないい感じにこっちみてる。
骨に制限かかってないから、すんごいこっちみてる。(制限をかけましょう。)


思ったこと

これみんな知ってるのかな?
私は人に教えてもらうまでこの方法を知らず、知った上で検索してもそれっぽい情報をなかなか得られませんでした。
あまり知られていないのか?問題がある手法なのか?分かりませんが少なからず上記で要件は達成できそうに思えます。
CustomAnimNodeやAnimInstanceの実装がこうするのが適切かはわかりません、ノードのInitializeThread時取得する方法がこれしか浮かばず、複数の骨指定のためにTMapにしてみました。より良い実装があればご指摘いただきたいです。

また、この記事の続きとしてAnimLayerを使いより便利に使える方法も記事にしています。合わせて見ていただけると良いかと思います。
naoxgames.hatenablog.jp




正しい対応方法やより良い方法があればご指摘お願いいたします!!!


UE4 JOJOのザ・ワールド風表現α版(1)

JOJOのザ・ワールドの表現に挑戦してみました。
※今回は必要なパラメーターを取得するまでの対応となります。

やってみたはいいものの、どうも納得のいくレベルに到達せず、詰めていくのに時間がかかりそうなのでここまでの内容を書いておこうと思います。
内容としては、表現に必要となるであろう最低限の要素の実装について記述します。
※実用性は現状低いため、今後改善していきます。

ちなみに現段階の完成形は以下になります。

f:id:naoxgames:20210712035257p:plain

目標


JOJOのザ・ワールド風の表現を作ってみる!
今回は必要なパラメーターを取得できるようにする対応までになります。


環境

・UE 4.26.1 (エンジンビルドはしていません)

やってみる

まずは要素を考えてみる。
・特定位置から球状にザ・ワールドのエリアが広がっていく
・ザ・ワールドのエリア外の表現
・ザ・ワールドのエリア越しの背景の表現
・ザ・ワールドのエリア内の表現
・ザ・ワールドのエリア内の特定のオブジェクト用(キャラクターなど)の表現
・それぞれの表現で色を調整できるようにする

という感じでしょうか。
ちなみに今回はグラフィック的な表現はすべてPostProcessMaterialで行います。


エリア内、エリア外、エリア越しを取得。

※この表現が一番問題があり、要検討内容になります。
ザ・ワールドのエリアを示す球を作成しエリア内、エリア外、エリア越しをPostProcessMaterialで取得できるよう対応します。
いろいろと迷った挙句、半透明を使い取得できるようにしてみました。
こうすることで、他で半透明が使えなくなる問題があるので、要検討です。。。

具体的に対応したいイメージは以下です。

f:id:naoxgames:20210712022451p:plain
という具合でSphereを配置したものから
f:id:naoxgames:20210712022721p:plain のようなパラメーターを取得したいわけです。
上記の場合
赤:エリア内のオブジェクト
緑:エリア越しの背景
青:エリア外
を表しています。

Sphereに適用するマテリアル

f:id:naoxgames:20210712024405p:plain
Sphereのマテリアル
のようにします。
Sphereの手前側、奥側を取得するために両面描画にし、それぞれ違う値を適用します。
値はなんでもいいですが、見た目に影響しない程度に薄くしています。※とても無理やりな対応となっています。

PostProcessMaterial

f:id:naoxgames:20210712033746p:plain という具合で、Sphereの外側のみか内側と外側が重なっているかをPostProcessInput1のAから取得して処理することで可能です。

エリア内の特定のオブジェクト用(キャラクターなど)の表現

エリア内の背景オブジェクトとキャラクターは違う処理をしたくなったりします。
現にアニメでキャラクターと背景で違う表現です。
ここで取得したいのは以下のようなパラメーターになります。

f:id:naoxgames:20210712031429p:plain
エリア内外オブジェクト
上記画像では、選択している4つのキューブすべてが違う処理をされています。
※ここではキャラクターと背景を想定します。
赤:エリア内の背景オブジェクト想定
緑:エリア越しの背景(一部エリア内)にあるキャラクター想定
青:エリアの手前に存在するオブジェクト(キャラクターも同様)想定
黄:エリア内のキャラクター想定
となります。

この対応では、キャラクターとして扱うオブジェクトにCustomDepthを使用しています。以下のような感じ
f:id:naoxgames:20210712032030p:plain
CustomDepth設定

それによってキャラクターのことを取得できるようになったのでPostProcessMaterialを少々改造し、以下のようにします
f:id:naoxgames:20210712033932p:plain
CustomDepthを取得

少し見にくいので追加部分をアップで
f:id:naoxgames:20210712032339p:plain
追加部分をアップに



これで必要なパラメーターを取得できました。
次はこれらを踏まえて色の設定やちょっとした演出の内容になります。



正しい対応方法やより良い方法があればご指摘お願いいたします!!!

UE4 NaviMeshを任意の形に動的に変化させたい

ナビメッシュを動的に変更かけたくなることってありますよね。
ゲームの進行状況だったり、同じレベルだけど別の用途で使いたいときだったり、特定のものがあるときは通れない(もしくは別の道を優先するようになる)とか。
いろいろ調べてみると、設定を「Dynamic」にすることで解消できる!というのを多く見かけました。
確かに求めていることを実現することはできますが、処理負荷が非常に心配です。
今回は「Dynamic」設定にしないで対応する方法を検討してみます。


目標


NavModifierを使ってナビメッシュの動的変更を行う。
ナビメッシュの「RuntimeGeneration」は「Dynamic Modifiers Only」を使用する。

環境

・UE 4.26.2 (エンジンビルドはしていません)

前準備

ナビメッシュをレベルに設定して可視化

とりあえず何もないレベルにナビメッシュを設定して、表示するようにします。
f:id:naoxgames:20210530213603p:plain
こちらは新規レベルを作成してナビメッシュバウンズボリュームを配置してサイズを変更しただけのレベルです。
※手順は以下
f:id:naoxgames:20210530213205p:plain
でレベル作成。
f:id:naoxgames:20210530213430p:plain
でナビメッシュを張り
f:id:naoxgames:20210530213624p:plain
で可視化しています。※もし表示されない場合は「ビルド」から「パスをビルド」を行ってください。
f:id:naoxgames:20210530213828p:plain


ランタイム設定を変更

ナビメッシュの更新設定を変更します。
プロジェクト設定→「エンジン」→「ナビゲーションメッシュ」→「ランタイム」→「Runtime Generation」
ここを「Dynamic Modifiers Only」に設定します。
f:id:naoxgames:20210530214045p:plain


これで必要な設定は完了しました。


やってみる

アクターを作成

作戦としては「NavModifier」コンポーネントを使ったアクターでNaviMeshを動的に変更します。
まずはアクターを作成します。
ここでは「BP_NavModTest」とします。
今回は「Cube」を使うこととします。
f:id:naoxgames:20210530224724p:plain

キューブの「Can Ever Affect Navigation」のチェックをオフにします。
f:id:naoxgames:20210530224825p:plain

「NavModifier」コンポーネントを追加します。
f:id:naoxgames:20210530225327p:plain

これでアクターの設定は完了です。


ナビメッシュを変化させる(Editor時)

先ほどのレベルに配置してみましょう。
f:id:naoxgames:20210530231432p:plain
キューブの形にナビメッシュが切り取られました。

試しに回転やスケールを編集してみましょう。
f:id:naoxgames:20210530232537p:plain
アクターの形に合わせて切り取られているのがわかります。


ナビメッシュを変化させる(実行時)

実行時用の対応をします。
まずは先ほど作成した「BP_NavModTest」に以下のようなイベントを作成します。
f:id:naoxgames:20210531002346p:plain
処理の内容はNaviMeshに対して行う処理と見た目の表示/非表示、コリジョンの有効/無効を設定しています。

次に、レベルブループリントで「BP_NavModTest」のリファレンスを作成し、キーで操作できるようにします。
ここでは以下のように設定しました。
f:id:naoxgames:20210531001949p:plain
「1」を押したときにナビメッシュを切り抜く、表示、コリジョン有効。
「2」を押したときにナビメッシュはそのまま、非表示、コリジョン無効。
となります。

これで実行してみると以下のようになります。
f:id:naoxgames:20210531002810g:plain

また、回転をさせ続けてみると以下のようになります。

f:id:naoxgames:20210531004059g:plain


このように、動的に変化をつけることができました。
※ランタイムのナビメッシュ表示は以下
@キーを押して「show Navigation」
f:id:naoxgames:20210531005427p:plain


思ったこと

「Dynamic Modifiers Only」の設定で実現できました。
とりあえず、「Dynamic」にするのは規模感にもよりますがもう少し他の手を探したほうがいいかと思います。
実際にゲームで使用するレベルでその負荷に耐えられるのか、各アクターの設定(「Can Ever Affect Navigation」とか)を適切に設定する必要があるなど、いろいろと考えることがあると思われます。
今回は単純な形で動的な変形をさせましたが、複雑なメッシュの形に合わせた対応方法もあるので、また別の記事で記述させていただきます。
※といってもエンジニア側の対応は変わらず、デザイナーのモデルの設定によるものになります。


正しい対応方法やより良い方法があればご指摘お願いいたします!!!


UE4シーケンサーのメニューに項目を追加したい(2)

目標


UE4シーケンサーのエディター拡張を行い、メニューの項目を追加してシーケンサーに対して独自の処理を行うための入り口を作りたい。
※エンジン改造はしません。
今回はシーケンサーのトラックメニューに追加する方法になります。
具体的には以下の画像の箇所

f:id:naoxgames:20210508013755p:plain
トラックメニュー

前回の続きで対応するため、以下もご一読ください。
naoxgames.hatenablog.jp

環境

・UE 4.26.1 (エンジンビルドはしていません)
・VisualStudio 2019 16.7.2

やってみる

作戦としては前回同様UI拡張点からコードを検索して、似た処理を真似てみます。
前回のプラグインに処理を追加して対応します。

UIの拡張点の表示

前回同様UIの拡張点の表示を行いましょう。
UE4エディターの[編集]→[エディタの環境設定]→[UIの拡張点の表示]にチェックを入れます。
該当箇所を確認すると

f:id:naoxgames:20210508015105p:plain
拡張点
「ObjectBindings」ということがわかりました。

エンジンから類似処理を探す

「ObjectBindings」でエンジンのコードを検索すると、SSequencer.cppSSequencer::GetContextMenuContentで使われていることがわかります。
FSequencer::BuildAddObjectBindingsMenuを見ると、ObjectBindings[i]->BuildSequencerAddMenuでメニューの登録がされています。
トラックメニューに表示されるにはこのObjectBindingsに登録されている必要があるようです。

ObjectBindingsに登録する処理を探すとFSequencer::InitSequencerObjectBindings.Addされているのが確認できます。
EditorObjectBindingDelegatesの要素数分登録されているので、FSequencer::InitSequencerの引数のEditorObjectBindingDelegatesがどこでどう設定されているか確認します。

FSequencer::InitSequencerを検索するとFSequencerModuleのCreateSequencerで呼び出しています。
EditorObjectBindingDelegatesに登録する処理はFSequencerModuleのRegisterEditorObjectBindingで行っています。


っということで、必要な情報は揃いました。

モジュール設定

SequenceExtensionEd.Build.csに以下を追加します。

~
        PublicDependencyModuleNames.AddRange(
            new string[]
            {
                ~
                "Sequencer",
                ~
            }
            );
~


シーケンサーモジュールに拡張メニュー追加

トラックメニューに追加する処理を作成します。

ObjectBindingExtension.h

#include "CoreMinimal.h"

#include "ISequencerEditorObjectBinding.h"
/**
 * 
 */
class ISequencer;
class AActor;

class FObjectBindingExtension : public ISequencerEditorObjectBinding
{
public:

    FObjectBindingExtension(TSharedRef<ISequencer> InSequencer);

    static TSharedRef<ISequencerEditorObjectBinding> OnCreateActorBinding(TSharedRef<ISequencer> inSequencer);

    // ISequencerEditorObjectBinding interface
    virtual void BuildSequencerAddMenu(FMenuBuilder& MenuBuilder) override;
    virtual bool SupportsSequence(UMovieSceneSequence* InSequence) const override;

private:

    void AddObjectBindingMenuExtension();

private:
    TWeakPtr<ISequencer> Sequencer;
};



ObjectBindingExtension.cpp

#include "ObjectBindingExtension.h"

#include "Misc/Guid.h"
#include "EditorStyleSet.h"
#include "LevelSequence.h"

#define LOCTEXT_NAMESPACE "FObjectBindingExtension"

FObjectBindingExtension::FObjectBindingExtension(TSharedRef<ISequencer> InSequencer)
    : Sequencer(InSequencer)
{ }

TSharedRef<ISequencerEditorObjectBinding> FObjectBindingExtension::OnCreateActorBinding(TSharedRef<ISequencer> inSequencer)
{
    return MakeShareable(new FObjectBindingExtension(inSequencer));
}

void FObjectBindingExtension::BuildSequencerAddMenu(FMenuBuilder& MenuBuilder)
{
    MenuBuilder.AddMenuEntry(
        LOCTEXT("ObjectBindingEx", "ObjectBindingEx"),
        LOCTEXT("ObjectBindingToolTip", "ObjectBindingToolTip"),
        FSlateIcon(FEditorStyle::GetStyleSetName(), "ContentBrowser.ColumnViewAssetIcon"),
        FUIAction(FExecuteAction::CreateRaw(this, &FObjectBindingExtension::AddObjectBindingMenuExtension)));
}

bool FObjectBindingExtension::SupportsSequence(UMovieSceneSequence* InSequence) const
{
    return InSequence->GetClass() == ULevelSequence::StaticClass();
}

void FObjectBindingExtension::AddObjectBindingMenuExtension()
{
    FText DialogText = FText::FromString("ObjectBindingMenuExtension");
    FMessageDialog::Open(EAppMsgType::Ok, DialogText);
}

#undef LOCTEXT_NAMESPACE



SequencerExtensionEd.h

~
private:
~
    FDelegateHandle ObjectBindingDelegateHandle;
~



SequencerExtensionEd.cpp

~
#include "ObjectBindingExtension.h"
~
void FSequencerExtensionEdModule::StartupModule()
{
~
    ObjectBindingDelegateHandle = SequencerModule.RegisterEditorObjectBinding(FOnCreateEditorObjectBinding::CreateStatic(&FObjectBindingExtension::OnCreateActorBinding));
~
}
~
void FSequencerExtensionEdModule::ShutdownModule()
{
~
        SequencerModule.UnRegisterEditorObjectBinding(ObjectBindingDelegateHandle);
~
}



実行!!!!

f:id:naoxgames:20210508031343p:plain
トラックメニュー
f:id:naoxgames:20210508031502p:plain
メニュー押したとき

シーケンサーのトラックメニューに項目が指定した名前で追加され、ダイアログが表示されました。


思ったこと

前回とは違った追加の仕方で、メニューごとに違うとやりにくいですね。
以前に書いた記事で、ISequencerを取得する対応を紹介しています。
naoxgames.hatenablog.jp これと組み合わせることでメニューからできることの幅が広がると思います。


ここまでの内容は以下のリポジトリに上げてあります。 github.com

この記事の続き naoxgames.hatenablog.jp

正しい対応方法やより良い方法があればご指摘お願いいたします!!!