PaperSloth’s diary

主にゲーム開発関連についての記事を書きます。

Unity serializeについてのまとめ

目次

環境

Unity 2019.3.13f1

概要

組み込み型、Property、struct/classの Inspectorへの公開方法をまとめた記事になります

Unityのシリアル化については下記のドキュメントに記載があります
スクリプトのシリアル化 - Unity マニュアル

C#の型情報についてのドキュメントはこちら
型 - C# プログラミング ガイド | Microsoft Docs

[SerializeFiled]について

主に変数をInspectorに公開するために使用することが多いです

public class ExampleBehaviour : MonoBehaviour
{
    // 書き方は2通りあり、改行してもしなくてもよい
    [SerializeField]
    int ExampleValue = 10;

    [SerializeField] int Value = 10;
}

f:id:PaperSloth:20200525113603p:plain

public / private を書かなくてもよいのか、publicとの違い

先ずはpublic / privateを書かなくてもよいのかについてですが
C#のclassはアクセス修飾子がない場合に明示的にprivateになります
そのため以下の2つは同じ意味を持ちます

[SerializeField]
int ExampleValue = 10;
// どちらの書き方でも同じ意味をもつ
[SerializeField]
private int Value = 10;


次にpublicとの違いについて
public の場合[SerializeField]を付けなくてもInspectorに変数を公開できます
そこまでは同じです
ですが、アクセス修飾子が異なるので他のクラスから変更できてしまいます

[SerializeField]
int ExampleValue = 10;

public int Value = 10;

このクラス単体に限っては何の問題もありません
ただし、他のクラスから変更できてしまうため以下のような問題が発生します

先ずはExampleBehaviour にアクセスするためのクラスを用意します

public class TestBehaviour : MonoBehaviour
{
    [SerializeField]
    ExampleBehaviour Example;

    void Start()
    {
        Example.Value = 100;
    }
}

f:id:PaperSloth:20200525114818p:plain


実行するとInspector上で"10"だったExampleValueが"100"に上書きされてしまいます
f:id:PaperSloth:20200525114939p:plain

この書き方の場合には意図しない値の変更が行われる可能性があり
バグを生み出す原因となり得ます
Unityに限らずアクセス修飾子には気を付けましょう

PropertyをInspectorに公開する方法

Propertyは以下のようにSerializeFieldを付けてもInspectorに公開できません

[SerializeField]
int ExampleValue { get; set; }

f:id:PaperSloth:20200525120820p:plain

解決方法として[field: ] を付与することでInspectorに公開することができます

[field: SerializeField]
int ExampleValue { get; set; }

f:id:PaperSloth:20200525120905p:plain

backing field の表記になりパッと見では分かりにくくなってしまいます
(Propertyを公開していることを意図的に表現したい場合はこのままでよいですが)

解決策として以下のForumにある「RenameFieldAttribute」のようなclassを用意することで解決できます
https://forum.unity.com/threads/c-7-3-field-serializefield-support.573988/

public class ExampleBehaviour : MonoBehaviour
{
    [field: SerializeField]
    [field: RenameField(nameof(ExampleProperty))]
    int ExampleProperty { get; set; }

    [field: SerializeField]
    [field: RenameField("RenameValue")]
    int Value { get; set; }
}

f:id:PaperSloth:20200525121344p:plain

class, structをInspectorに公開する方法

classもstructもそのままではInspectorに公開することができません

using UnityEngine;

public class ExampleBehaviour : MonoBehaviour
{
    public struct StructData
    {
        public int Value;
        public string Name;
    }
    [SerializeField]
    StructData structData;

    public class ClassData
    {
        [SerializeField]
        string Tag;
    }
    [SerializeField]
    ClassData classData;
}

f:id:PaperSloth:20200525122809p:plain

どちらの場合でもInspectorに公開する場合は[System.Serializable] を使用することでInspectorに公開できます

using UnityEngine;

public class ExampleBehaviour : MonoBehaviour
{
    [System.Serializable]
    public struct StructData
    {
        public int Value;
        public string Name;
    }
    [SerializeField]
    StructData structData;

    [System.Serializable]
    public class ClassData
    {
        [SerializeField]
        string Tag;
    }
    [SerializeField]
    ClassData classData;
}

f:id:PaperSloth:20200525123019p:plain

まとめ

ここまでに紹介してきた書き方をざざっとまとめました。

using System;
using UnityEngine;

public class ExampleBehaviour : MonoBehaviour
{
    [SerializeField]
    int ExampleValue;
    [field: SerializeField]
    int PropertyValue { get; set; }
    // ※RenameFieldの拡張用のclassを記述した場合
    [field: SerializeField]
    [field: RenameField("RenameProperty")]
    int RenameValue { get; set; }

    // struct, class どちらの場合でも[System.Serializable]で対応可能
    // ここではusing System;を定義しているためSystemを省略
    [Serializable]
    public class ClassData
    {
        public string Name;
        [SerializeField]
        string Tag;
    }
    [SerializeField]
    ClassData classData;
}

f:id:PaperSloth:20200525124828p:plain

おまけ Inspectorで上書きした値が保存される場所

Inspector上で書き換えた値はScene fileに紐付いて保存されています

例えば以下のように設定しておいた場合
f:id:PaperSloth:20200525123612p:plain

scene.unity ファイルをエディターで開いてgrepすると
以下のように保存されていることが確認できます

MonoBehaviour:
  ... (省略)
  m_EditorClassIdentifier:
  structData:
    Value: 2000
    Name: Enemy
  classData:
    Tag: Player

普段中身を見ることは少ないとは思いますが
デカいSceneを開かないでgrepして値を追いたい時など何かしら使うケースがあるかもしれません

UE4 KeyConfig作成用のPluginを公開しました

目次

概要

UE4でのKeyConfigの作成の参考になるPluginを公開しました。
鋭意開発中でまだまだ機能不足が目立ちます。

ProjectDirのPlugins以下に以下のPluginsを入れれば動作します。
github.com

ProjectSettingsのAction Mapping / Axis Mappingで設定した名前を元に変更します。

初期のバージョンでの実装

現在用意しているノードについて
f:id:PaperSloth:20200511131004p:plain

- ChangeMapping
Action/Axisどちらかのキーの変更用ノード
  BindName : Action/Axis Mappingで設定した変更したいキーの名前
  Key : FKeyの構造体、AnyKey等で取得するのが楽です
  BindType : ActionかAxisのどちらか
  PlayerIndex : 変更を適用したいPlayerControllerの番号(デフォルトは0)
- ChangeActionMapping
Action Mappingで設定したキーの変更用ノード
- ChangeAxisMapping
Axis Mappingで設定したキーの変更用ノード
  Scale : 基本的には-1.0 - 1.0 で指定 ProjectSettingsのAxis Mappingと同様
- GetAllActionMappingNames
Action Mappingで設定したキーの名前一覧を取得するノード
  ActionNames : Action Mappingで設定したキーの名前一覧
- GetAllAxisMappingNames
Axis Mappingで設定したキーの名前一覧を取得するノード
  AxisNames : Axis Mappingで設定したキーの名前一覧
実装概要

GitHubに公開してるので、実装をそのまま見てもらえれば早いんですが、簡単に概要だけ書きます。
まず、FKey構造体を使用するためBuild.csのPublicDependencyModuleNamesにInputCoreを追加しておく必要があります。

PublicDependencyModuleNames.AddRange(
    new string[]
    {
        "Core",
        "InputCore", // 追加
    }
);

Mapping系について
PlayerInputからActionMappings配列内の同名のAction/Axisを削除し、新しくKeyを設定して追加しています。
Axisに関しても同様の実装になっています。

const APlayerController* const PC = UGameplayStatics::GetPlayerController(WorldContextObject, PlayerIndex);
UPlayerInput* const PlayerInput = PC->PlayerInput;
PlayerInput->ActionMappings.RemoveAll([&ActionName](const FInputActionKeyMapping& NowMapping)
{
    return (NowMapping.ActionName == ActionName);
});

const FInputActionKeyMapping AddMapping(ActionName, Key);
PlayerInput->AddActionMapping(AddMapping);

GetAllXX系について
こちらはUInputSettingを取得してそこに登録されているAction/Axis名を取得して
配列に格納して返しているだけです。

ActionNames.Empty();

const UInputSettings* Settings = GetDefault<UInputSettings>();
for (const FInputActionKeyMapping& Action : Settings->GetActionMappings())
{
	ActionNames.Emplace(Action.ActionName);
}


特に細かく語るほどのことがないシンプルな実装なので、C++を書ける人はPluginを導入せずに自分で書いた方がなにかと都合がいいと思います。

追記: 大幅に仕様を変更し、サンプルプロジェクトを追加

以前の実装との違い
更新内容
1. Gamepadでのマウスカーソル移動が可能になった
2. Sampleを用意したため、より具体的な使い方のイメージができるようになった
3. iniへの書き込みを行うようにしたため、変更内容が保存されるようになった
4. Keyboard, Gamepadそれぞれでの変更が可能となった

なくなった機能
1. Axis対応の破棄(機能不十分のため一旦見送り)
2. 複数PlayerControllerに対応できなくなった
対応するにはコードの書き換えが必要です

まずは最小構成のNodeのサンプルです。
登録したAction名に対して任意のKeyで上書きするというものです。
一番シンプルな使い方です。
f:id:PaperSloth:20200516004514p:plain

続いてNodeの一覧です。
f:id:PaperSloth:20200516004506p:plain
ざっくり分けると以下の3カテゴリに分かれています。

  • Keyの更新
  • Gamepadでのマウスカーソル有効化/無効化
  • Key情報の検索

サンプルの画面がこちらです。
Keyboard, Gamepadそれぞれで任意のKeyの割当ができるようなサンプルを用意しました。
f:id:PaperSloth:20200516004458p:plain

参考資料

スライド内のp.48
UE4を用いたTPS制作事例 EDF:IR 地球を衛る兵士の作り方

こちらのPluginにRebind系とGamepadのUMGでの使用するための機能が一通り揃っていますので、こちらを使用されるのも良いかと思います。
GitHub - EverNewJoy/VictoryPlugin: Rama's Victory BP Plugin
https://github.com/EverNewJoy/UE4GamepadUMG

UE4 HoudiniEngine for UE4を使ってHDAをUE4で使う方法

目次

環境

UE4.23.1
Houdini Indie 18.0.287
Houdini Engine for Unreal 4.22.3/4.23.1

前準備

UE4とHoudiniはインストール済の前提で進めます。
Houdiniのインストール時にHoudini Engine for Unrealにチェックを入れておくと良いでしょう。
f:id:PaperSloth:20200503145953p:plain

また、Houdini Engineの機能はHoudini Apprenticeでは使用できません。
HDA(Houdini Digital Assetの作成までは行えたと思いますが、UE4側で使用できないはずです。

実際に制作してみる

今回は説明のためにSphereをHDA化してUE4に持っていきます。
パラメーターを吐き出す流れさえ理解すれば、あとは自由に応用が効くと思いますので非常に簡単な例としました。

少し凝ったものを作成したい方は以下のSideFXのサイトにUE4向けのチュートリアル動画が参考になると思います。
動画ではHoudini FX 15.0.299を使用しており、バージョンがかなり古くなっていますが大きく困るポイントは少ないと思います。
バージョン違いでは色々と問題が発生するため慣れていない場合は極力同じバージョンか近いバージョンのものを使用する方がよいでしょう。
https://www.sidefx.com/ja/products/houdini-engine/plug-ins/unreal-plug-in/

Houdini 側の作業

大まかな流れ
1. geometry作成
2. sphere作成
3. subnet化
4. exportするパラメーターの設定
5. HDAとして保存

1. geometry作成
Houdiniのいつもの流れですね。
Network Viewでgeometryを作成します。
f:id:PaperSloth:20200503151832p:plain

2. sphere作成
1.で作成したgeometryをダブルクリックか i キーで中に入ります。
中に入るとsphereを作成します。
f:id:PaperSloth:20200503152035p:plain

sphereのPrimitive TypeをPolygonに変更しておきます。
f:id:PaperSloth:20200503152555p:plain

3. subnet化
subnet化は複数のノードの塊を1つのパッケージにする機能ですが
HDAとして出力する際にも役立ちます。

sphereノードを選択してNetwork View右上の箱のアイコンからsubnet化できます。
ショートカットキーはShift + Cです。
f:id:PaperSloth:20200503152933p:plain

以下のようにsubnetノードが作成されます。
f:id:PaperSloth:20200503153048p:plain


4. exportするパラメーターの設定
subnetノードを選択してGearのアイコンのEdit Parameter InterfaceからexportするParameterを設定します。
f:id:PaperSloth:20200503153614p:plain

Edit Parameter Interfaceのwindowが開いたら
From Nodesタブに切り替え、sphereのFrequency (freq)を右のExisting Parametersにドラッグアンドドロップします。
パラメーターを選択してして -> ボタン押下でも可能です。
f:id:PaperSloth:20200503154015p:plain

パラメーターを反映できたことを確認したらAcceptボタンを押下して閉じます。
f:id:PaperSloth:20200503154114p:plain

subnetの中に先程設定した Frequencyが追加されていれば成功です
f:id:PaperSloth:20200503154236p:plain


5. HDAとして保存
subnetノードを選択して右クリック -> Create Digital Asset... からHDA化します。
f:id:PaperSloth:20200503154356p:plain

Operator Nameにsphereと入力します。
するとOperatar Labelも自動で変更されます。
Save to Libraryには保存したいパスにsphere.hdalcとしてAccpetを押下します。
f:id:PaperSloth:20200503154506p:plain

すると以下のようにEdit Operator Type Propertiesが開きます。
ここでは特に変更を行わなずSave to Libraryのpathが正しいことを確認したらAcceptを押下します。
f:id:PaperSloth:20200503154925p:plain

以上でHoudini 側の作業は終了です。

HDAをUE4にimportする

UE4側では4.23.1で新規にBlankのTemplateでプロジェクトを作成しました。
既存のプロジェクトでも特に問題ありません。

先ずプロジェクトを起動したらHoudini Engineがインストールされていることを確認しましょう。
Edit -> Plugins のInstalled 以下にHoudini Engineが存在し、Enabledになっていることを確認します。
f:id:PaperSloth:20200503155252p:plain
f:id:PaperSloth:20200503155313p:plain

有効になっていれば、特に設定することはありません。
先ほど作成したsphereのHDAをContent BrowserにドラッグアンドドロップするかImportボタンを押下してImportします。
f:id:PaperSloth:20200503155451p:plain
f:id:PaperSloth:20200503155615p:plain

ImportしたHDAをLevel上に配置したら完了です。
DetailsからFrequencyを変更してみて反映されることを確認しましょう。
f:id:PaperSloth:20200503160303g:plain

また、Houdini側で設定をしていない場合でもUE4側からMaterialを変更したりStatic MeshとしてBake(保存)することができます。
importしたHDAを選択してDetails内のHoudini Generated Meshes以下から可能です。
Bakeを押下すると現在の設定のMeshが別アセットとして保存されます。
Materialを変更すればもちろん見た目に反映されます。
f:id:PaperSloth:20200503163223p:plain

それぞれパラメーターを変更してStatic MeshとしてBakeしたものです。
f:id:PaperSloth:20200503163528p:plain

Bake後は通常のStaticMeshと同様ですので、UE4側の設定でLODを作成したりすることももちろん可能です。

Materialを変更した例
f:id:PaperSloth:20200503163854p:plain

おまけ
importかexportの設定に失敗していたり、Houdini Apprenticeだと以下のようにロゴのみの表示になってしまいます。
f:id:PaperSloth:20200503162513p:plain

追記 Unreal Materialの適用方法

Houdini 側でUE4のMaterialへの参照を持たせた状態でのHDAの作成が可能です。
HoudiniにはUnreal Materialというノードが用意されています。
f:id:PaperSloth:20200503171641p:plain
f:id:PaperSloth:20200503171711p:plain
f:id:PaperSloth:20200503171733p:plain

Stringの説明文にあるように、UE4のプロジェクト内での相対パスを設定してやればよいようです。
UE4側で使用するためのMaterialを用意しておきましょう。
Content直下にM_TestHDAというBaseColorに赤を割り当てたMaterialを用意しました。
f:id:PaperSloth:20200503171913p:plain
f:id:PaperSloth:20200503172018p:plain

再びHoudiniの作業に戻ります。
UE4のパスのContentはGameと解釈されます。
そのため、Material'/Game/(Materialへのpath)/(MaterialName.MaterialName)'
といったパスを設定してやります。
今回の例ではContent直下のため、以下のようになります。
「Material'/Game/M_TestHDA.M_TestHDA'」
f:id:PaperSloth:20200503172115p:plain

あとは先程同様にHDAとして保存し、UE4にImportします。
これで設定したMaterialが反映されていることが確認できました。
f:id:PaperSloth:20200503172640p:plain
因みにEngineContent以下のMaterialを適用したい場合のパスは/Game/ を/Engine/ と置換してやることで使用可能です。

追記 作成したHDAの更新方法

Houdini 側の作業
Assets -> Edit Assets Properties... -> subnet Name
これでEdit Operator Type Propertiesが開かれます。
そこで、Apply またはAcceptを押下することでHDAが更新されます。
f:id:PaperSloth:20200504173033p:plain

UE4側の作業
上記の更新を行った後にHDAを選択してDetails内からRebuild Assetを押下することで更新されます。
f:id:PaperSloth:20200504173254p:plain

参考資料

https://www.sidefx.com/ja/products/houdini-engine/plug-ins/unreal-plug-in/

https://www.sidefx.com/forum/topic/49570/


また、今回は紹介できませんでしたがSideFX Labs内に
NiagaraやTerrain関連のノードも用意されており、機会があれば紹介させていただきます。
f:id:PaperSloth:20200503173906p:plain

UE4 SceneCaptureComponent2DでTextureStreamingが走らない場合の対処法

目次

環境

UE4.25.0 Preview7
Visual Studio Community 2019

概要

SceneCaptureComponent2Dを使用してCapture Every Frameフラグを有効化しているにも関わらず
Capture先でTextureStreamingが走らないという経験をした方向けの記事です。

具体的な現象としては遠方でのCapture時に発生します。
さらにめんどうなことに再現するのに少し手間がかかります。
一度正常にStreamingが走った後はEditorを再起動しないと再現ができません。
Streamingした結果がCacheされているからでしょうね。

問題の再現

まずはどういった状態になるのかを見ていきましょう。
こちらがCaptureしたRenderTargetをPlaneに貼ったものです。
TextureStreamingが走っていないため、Textureが粗いものになっています。
CaptureするActorはSubLevelに分けて(1000000.0, 1000000.0, 0.0)の遠方を基準に配置しました。
f:id:PaperSloth:20200502171242p:plain

そしてこちらがTextureStreamingが走った後の期待値です。
f:id:PaperSloth:20200502171310p:plain

岩のMeshが一番違いが分かりやすいと思います。
原因としてはMainCamera以外ではStreamingManagerが走らないことが原因と思われます。
そのため、MainCameraに映らない遠方のCaptureしたActor等は手動で走らせてやる必要があります。

回避方法

こちらのAnswerHubでの回答にあるように手動でUpdateSceneCaptureContents() を呼び出して更新してやる必要があります。
このアプローチのためにはC++コードの記述が必要になります。
追記に書きましたがBlueprint側から利用可能なノードも用意されています。
ただし、挙動が違うものになりますので注意が必要です。
https://answers.unrealengine.com/questions/841147/view.html

ざっくりと書くとStereamingManagerに位置情報を送り
SceneInterfaceからUpdateSceneCaptureContents() を呼び出すだけです。

IStreamingManager::Get().AddViewSlaveLocation(CaptureLocation);
World->Scene->UpdateSceneCaptureContents(CaptureComponent);

USceneCaptureComponent2Dを継承してコードを書くのが手っ取り早いですが
なんとなくUBlueprintFunctionLibraryを継承してコードを用意しました。
USceneCaptureComponent2D版はAnswerHubのリンク先にコード例があります。

今回追加したのはUpdateCapture() で位置とSceneCaptureComponent2Dを渡し、boolで成功判定を返す関数です

// BlueprintFunctionLibrary.h
#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Engine/SceneCapture2D.h"
#include "SandboxFunctionLibrary.generated.h"

UCLASS()
class SANDBOX425_API USandboxFunctionLibrary : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()

    UFUNCTION(BlueprintCallable, Category = "Sandbox", meta = (WorldContext = "WorldContextObject"))
    static bool UpdateCapture(UObject* WorldContextObject, FVector CaptureLocation, USceneCaptureComponent2D* CaptureComponent);
};

// BlueprintFunctionLibrary.cpp
#include "SandboxFunctionLibrary.h"

bool USandboxFunctionLibrary::UpdateCapture(UObject* WorldContextObject, FVector CaptureLocation, USceneCaptureComponent2D* CaptureComponent)
{
    UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject);
    if (!World || !World->Scene)
    {
        return false;
    }

    IStreamingManager::Get().AddViewSlaveLocation(CaptureLocation);
    World->Scene->UpdateSceneCaptureContents(CaptureComponent);
    return true;
}

あとはこの関数を呼び出すだけでStreamingManagerが期待通りに動作し、Textureの粗さは改善されます。
Level Blueprintから以下の呼び出しをしました。
(確認用に作成したBlueprintで処理としてはかなり悪いのでここは参考にしない方が良いです)
f:id:PaperSloth:20200502172506p:plain


この解決方法の問題点として、Textureの粗さが改善された後もMainCamera以外で無駄にTextureStreamingを走らせてしまうことです。
なにかしらの方法で終了判定を取得して更新を止めた方が良いです。

ひとまず以上で無事にTextureの粗さは改善されます。
同様の問題を踏んだ方はこちらの方法を検討されてみてもよいかもしれません。
f:id:PaperSloth:20200502171310p:plain



最後に余談ですがSceneCaptureComponentでは
Actorを選択した時にViewport右下にCapture先がPreviewされないため
確認用にCameraComponentを追加するのもアリかもしれません。
完全に無駄なComponentのため、リリース時には必ず削除する必要がありますがテスト用としてはオススメです。
f:id:PaperSloth:20200502173209p:plain

[追記] Blueprintで動作させる

カニパンチさんに教えていただきました。
情報を提供いただき、ありがとうございました。

Prestream Textures ノードを使用することでActor内の全StaticMeshComponent, StaticMeshComponentごとにStreamingを走らせることが可能なようです。
Actor側のPrestreamTexturesの引数Secondsに0.0fを設定していると60.0f*60.0f*24.0f*30.0fの30日分走ります。
そのため、実際に運用される場合は適切な数値を入れたほうがよいでしょう。
f:id:PaperSloth:20200502183524p:plain

例えば岩のActorのみを更新したい場合は下記のようになります。
f:id:PaperSloth:20200502182151p:plain

動作させると岩のActorのみが更新されていることが確認できました。
f:id:PaperSloth:20200502182255p:plain

UE4 ノードの整理(ルートを直線にする方法)

目次

環境

UE4.25.0 Preview2

概要

以前にもBlueprint TipsのスライドでStraighten Connectionsの紹介をしましたが
他にも便利な機能があるのでそちらを紹介していこうと思います。

www.slideshare.net

Blueprint編

上記のSlideでも紹介したStraighten Connectionsのおさらいから。
複数のノードを選択して「Q」キーでしたね。
f:id:PaperSloth:20200314203131g:plain

もしくは複数のノードを選択して
ノード上で右クリック -> Alignment -> Straighten Connection(s)です。
f:id:PaperSloth:20200314203238p:plain


ですが、これだと意図した通りに真っ直ぐになってくれない場合もあります。
そういった場合に便利なのが「Straighten Connection to (node name)」です。
ピンの上で右クリック -> Straighten Connections to (node name)で使用可能です。
f:id:PaperSloth:20200314203555p:plain

これを実行するとそのピンを基準に接続先のノードが真っ直ぐになります。
具体的にはsin node等微妙に真っ直ぐにならないノードが真っ直ぐになって便利です。

before
f:id:PaperSloth:20200314203944g:plain
after
f:id:PaperSloth:20200314204003g:plain

ここまで紹介した機能は全てBlueprint Classに限らず
Level BlueprintやAnimation Blueprintでも共通の操作です。

Material Editor編

続いてはMaterial です。
Materialでは「Straighten Connection(s)」が使用可能で、「Straighten Connection to (node name)」が使用不可です。
Materialの方が高さが合わないノードが多いので特に欲しい機能ですが、ないのであれば仕方ありません。

まずは「Straighten Connection(s)」のおさらいから。
Blueprint同様に複数ノードを選択して「Q」キーで真っ直ぐにしてくれます。
f:id:PaperSloth:20200314204702g:plain

Blueprint同様にsin nodeなどは相変わらずズレます。
「Straighten Connection to (node name)」が使えればいいのですが、別の手段で回避します。
人によってはEditor背景のGridに合わせたいという要望もあると思います。
そういった時に「Straighten Connection(s)」ではズレてしまうため
少し手間ですが手動でGridに合わせることでルートを真っ直ぐにしつつGridに合った並びにできます。

Niagara Editor編

いきなり脱線しますが、UE4.25 PreviewからNiagaraの BETAが外れて正式リリースになりそうですね。
先ずはアイコンが変わっていました。

UE4.24
f:id:PaperSloth:20200314210745p:plain
UE4.25
f:id:PaperSloth:20200314210836p:plain

Effectツール感があっていい感じのアイコンになりましたね。

本題に戻ります。
NiagaraのModule ScriptとFunction Scriptのノードの整列についてはMaterialと同じです。
「Straighten Connection(s)」が使用可能で、「Straighten Connection to (node name)」が使用不可です。

複数のノードを選択して「Q」キーで整列が可能です。
f:id:PaperSloth:20200314211816g:plain


まとめ

今回はひたすらルートを真っ直ぐにすることについて書きました。
BlueprintはPinごとに制御可能なので便利ですがMaterial とNiagaraも対応してほしいなと思っています。
大きく困ることはないので、好みの問題ですが。

さて、仕事でここまでやる必要があるかという正直過剰だと思います。
個々人の意識の問題として整理するのは大変良いことですが
プロジェクトのルールに組み込む際にここまで強要するのは過剰だと思っています。

また、これだけ一生懸命整理してもエンジンのアップデート等の原因で簡単に破綻してしまいます。
何事も程々がいいということですね。
個人プロジェクトで意識的にこういったことをやる分には良いことだと思います。

以上!

日記 第4回UE4何でも勉強会のLT に登壇してきました

イベントページはこちら
ue4allstudy.connpass.com


登壇内容は「Blueprint Tips 30選」です
資料はslidesharegithubにアップしました。

www.slideshare.net

GitHub - PaperSloth/Presentation: 登壇資料


公開した資料はslideshareでは標準でgifが動作しないため、githubにgif版をアップしました。
gif版の方がより正確に内容が伝わりやすくなっていると思います。


色々と登壇内容を考えたのですが、初心者向けにBlueprintの操作についてまとめた資料を目標に作成しました。
登壇時間が5分しかないということがすっぽり抜けており、当日は10個も紹介できませんでしたが。

登壇の0-5分前にgithubslideshareに公開できると勉強会が終わってからの作業もスムーズだと感じました。
また機会があればどこかで何かを話そうと思います。

UE4 外部ライブラリの使用方法について

この記事はUnreal Engine 4(UE4) Advent Calendar 2019の22日目の投稿記事です。
Unreal Engine 4 (UE4)のカレンダー | Advent Calendar 2019 - Qiita


目次

環境

UE4.23.1
Visual Studio 2017 Community

結論

最初に結論だけを書きます。
Static Library, Dynamic-link Libraryどちらの場合も編集するのは .Build.cs です。
・共通(Static Libraryはこれで設定完了)

// インクルードファイルへのpath 情報を追加
// .h/.hpp ファイルを配置したディレクトリへのpathを記述
PublicSystemIncludePaths.Add(Path.Combine(LibraryPath, "include"));
// ライブラリへのpath 情報を追加
// .lib ファイルを格納したディレクトリへのpath を記述
PublicLibraryPaths.Add(Path.Combine(LibraryPath, "lib", "x64"));
// ライブラリをプロジェクトに追加
// .lib ファイルの名前を記述
PublicAdditionalLibraries.Add("LirbraryName.lib");


・Dynamic-link Library

// ライブラリをプロジェクトに追加
// .dll ファイルの名前を記述
 PublicDelayLoadDLLs.Add("LibraryName.dll");
// プロジェクトのパッケージング情報に.dll の情報を追加し、ファイルコピー
// .dll ファイルを格納したディレクトリへのpath を記述
 RuntimeDependencies.Add(Path.Combine(LibraryPath, "lib", "x64");

概要

今回はUE4で外部のライブラリ(Static Library, Dynamic-link library)の使用方法について書きます。
使用するライブラリは自作した簡単なライブラリを使用します。
Static Libraryはint の足し算を行うだけのもの : ExampleLib
Dynamic-link Libraryはint の引き算を行うだけのもの : ExampleDll
この2つを用意しました。
わざわざライブラリ化する意味は全くありませんが、使用方法の参考例として用意しました。

ライブラリの作成方法についてはUE4の話から逸れるため、作成方法などについては省略します。
下記ドキュメントを参考にしてください。
チュートリアル: スタティック ライブラリを作成して使用する (C++) | Microsoft Learn
Visual Studio で C/C++ DLL を作成する | Microsoft Learn

Static Libraryの作成

設定等は大幅に省いてざっくりとコードだけ載せます。

こちらも設定等は省いてコードだけ。

UE4でのLibraryの使用方法

ようやくUE4の話に戻ります。
まずは新規にUE4プロジェクトを作成します。


続いて使用するライブラリをそれぞれプロジェクトに紐付けるために適切なディレクトリに移します。
(ProjectRoot)\ThirdParty\ 以下にファイルを置くのが一般的でしょう。
ThirdPartyというフォルダは存在しないため、新規作成します。

例 :
(ProjectRoot)\ThirdParty\(LibraryName)\include\
(ProjectRoot)\ThirdParty\(LibraryName)\lib\(PlatformName)\

今回は以下のように配置しました。
・ Static Library
(ProjectRoot)\ThirdParty\ExampleLib\include\
(ProjectRoot)\ThirdParty\ExampleLib\lib\x64\

・Dynamic-link Library
(ProjectRoot)\ThirdParty\ExampleDll\include\
(ProjectRoot)\ThirdParty\ExampleDll\lib\x64

ライブラリに更新が入った時には
Generate Visual Studio project filesでプロジェクトを再生成しています。
これが正しいフローなのかどうかは疑問が残りますが、これでライブラリの更新は反映されます。

ライブラリのリンクについて編集するのは基本的に下記のファイルのみです。
(ProjectRoot)\Source\(ProjectName)\(ProjectName).Build.cs
今回の場合だと (ProjectRoot)\Source\LibrarySandbox\LibrarySandbox.Build.cs
コメント等を削除して、初期の記述状態は下記になります。

using UnrealBuildTool;

public class LibrarySandbox : ModuleRules
{
    public LibrarySandbox(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });

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

ライブラリの使用準備にあたってThirdPartyへのFullPath取得のPropertyを追加します。

// Path使用のため追加
using System.IO;

public class LibrarySandbox : ModuleRules
{
    // 追加
    private string ThirdPartyPath
    {
        get { return Path.GetFullPath(Path.Combine(ModuleDirectory, "../../ThirdParty/")); }
    }
...
}

Static Libraryの使用方法

Static Libraryを使用するにあたっては下記へのファイル追加が必要です
・PublicSystemIncludePaths
・PublicLibraryPaths
・PublicAdditionalLibraries

includeファイルのディレクトリの追加、ライブラリへのpath の追加、ライブラリの追加です
今回の例では下記のようになります。

public class LibrarySandbox : ModuleRules
{
    ...
    public LibrarySandbox(ReadOnlyTargetRules Target) : base(Target)
    {
        ...
        PrivateDependencyModuleNames.AddRange(new string[] { });

        // 下記を追加
        string ExampleLibPath = Path.Combine(ThirdPartyPath, "ExampleLib");
        PublicSystemIncludePaths.Add(Path.Combine(ExampleLibPath, "include"));

        PublicLibraryPaths.Add(Path.Combine(ExampleLibPath, "lib", "x64"));
        PublicAdditionalLibraries.Add("ExampleLib.lib");
...
}

では実際に使用できるかコードを書いて確かめてみます。
GameModeのctorから呼び出してみます。

#include "LibrarySandboxGameModeBase.h"
#if PLATFORM_WINDOWS
#include "ExampleLib.h"
#include <memory>
#endif

ALibrarySandboxGameModeBase::ALibrarySandboxGameModeBase()
{
#if PLATFORM_WINDOWS
    const auto helloLib(std::make_shared<ExampleLib::HelloLib>());
    UE_LOG(LogTemp, Log, TEXT("Hello Static Library World : 1 + 2 = %d"), helloLib->Add(1, 2));
#endif
}

実行してみると無事に動作することが確認できました。

LogInit: FAudioDevice initialized.
LogLoad: Game class is 'LibrarySandboxGameModeBase'
LogTemp: Hello Static Library World : 1 + 2 = 3

手順としてはPublicSystemIncludePaths, PublicLibraryPaths, PublicAdditionalLibrariesに設定を記述するだけでプロジェクトから使用できるため比較的お手軽です。

Dynamic-link Libraryを使用するにあたっては下記へのファイル追加が必要です
・PublicSystemIncludePaths
・PublicLibraryPaths
・PublicAdditionalLibraries
・PublicDelayLoadDLLs
・RuntimeDependencies

includeファイルのディレクトリの追加、ライブラリへのpath の追加、ライブラリの追加、ライブラリのパッケージング時のコピー

今回の例では下記のようになります。

public class LibrarySandbox : ModuleRules
{
    ...
    public LibrarySandbox(ReadOnlyTargetRules Target) : base(Target)
    {
        ...
        PrivateDependencyModuleNames.AddRange(new string[] { });

        // 下記を追加
        string exampleDllRootPath = Path.Combine(ThirdPartyPath, "ExampleDll");
        PublicSystemIncludePaths.Add(Path.Combine(exampleDllRootPath, "include"));

        string exampleDllPath = Path.Combine(exampleDllRootPath, "lib", "x64");
        PublicLibraryPaths.Add(exampleDllPath);
        PublicAdditionalLibraries.Add("ExampleDll.lib");

        string exampleDllName = "ExampleDll.dll";
        string exampleDllFullPath = Path.Combine(exampleDllPath, exampleDllName);
        PublicDelayLoadDLLs.Add(exampleDllName);
        RuntimeDependencies.Add(exampleDllFullPath);
...
}

実際に使用できるかコードを書いて確かめてみます。
GameModeのctorから呼び出してみます。

#include "LibrarySandboxGameModeBase.h"
#if PLATFORM_WINDOWS
#include "ExampleDll.h"
#include <memory>
#endif

ALibrarySandboxGameModeBase::ALibrarySandboxGameModeBase()
{
#if PLATFORM_WINDOWS
    const auto helloDll(std::make_shared<ExampleDll::HelloDll>());
    UE_LOG(LogTemp, Log, TEXT("Hello Dynamic-link Library World : 2 - 1 = %d"), helloDll->Sub(2, 1));
#endif
}

実行してみるとエラーが出ます。
delayhlp.cpp の中でKernelBase.dllで例外が発生したと表示されます。
これはBinaries/Win64/ 以下にdll が存在せずEditorから実行できません。
ですのでdll をBinaries/(Platforms)/以下にコピーするコードを追加します。

using System;
using System.IO;

public class LibrarySandbox : ModuleRules
{
    ...
    public LibrarySandbox(ReadOnlyTargetRules Target) : base(Target)
    {
        ...
        string exampleDllName = "ExampleDll.dll";
        string exampleDllFullPath = Path.Combine(exampleDllPath, exampleDllName);
        PublicDelayLoadDLLs.Add(exampleDllName);
        RuntimeDependencies.Add(exampleDllFullPath);
        CopyDll(exampleDllName, exampleDllFullPath);
    }

    // Binaries以下にdll をコピーする
    private void CopyDll(string dllName, string dllFullPath)
    {
        if (!File.Exists(dllFullPath))
        {
            Console.WriteLine("file {0} does not exist", dllName);
            return;
        }
        string binariesDir = Path.Combine(ModuleDirectory, "../../Binaries/Win64/");
        if (!Directory.Exists(binariesDir))
        {
            Directory.CreateDirectory(binariesDir);
        }
        string binariesDllFullPath = Path.Combine(binariesDir, dllName);
        if (File.Exists(binariesDllFullPath))
        {
            File.SetAttributes(binariesDllFullPath, File.GetAttributes(binariesDllFullPath) & ~FileAttributes.ReadOnly);
        }
        // try catchでくくっていないとパッケージ作成時にこけます
        try
        {
            File.Copy(dllFullPath, binariesDllFullPath, true);
        }
        catch (Exception ex)
        {
            Console.WriteLine("failed to copy file: {0}", dllName);
        }
    }
}


これで無事に動作することが確認できました。

LogInit: FAudioDevice initialized.
LogLoad: Game class is 'LibrarySandboxGameModeBase'
LogTemp: Hello Dynamic-link Library : 2 - 1 = 1

手順としてはPublicSystemIncludePaths, PublicLibraryPaths, PublicAdditionalLibraries, に設定を記述することに加えて
PublicDelayLoadDLLs, RuntimeDependenciesが追加されました。
RuntimeDependenciesは記述がなくとも動作しますが
これを設定していないとプロジェクトのパッケージング時にそのフォルダにDLLがコピーされないため
DLLがなくて動作しないという問題を避けることができます。

パッケージング後のディレクトリはWindows PC向けパッケージでは下記のようになります。
WindowsNoEditor\LibraySandbox.exe
WindowsNoEditor\LibrarySandbox\ThirdParty\ExampleDll\lib\x64\ExampleDll.dll


最後にbuild.cs と使用例を全文載せておきます。