[UE5] UE初心者のグレーテーマへの挑戦

## 概要
このページでは、Unreal Engine 5のEditorの見た目をグレーテーマにするために挑戦した結果を紹介しています。
また、グレーテーマを実現するために開発したStarship Editor Style Overriderのプラグイン開発で起きた問題・解決を軽く紹介しています。


## Starship Editor Style Overrider プラグインとは
https://www.unrealengine.com/marketplace/ja/product/starship-editor-style-overrider

BPの右下のテキスト色を変更したり
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/48fc78b8-79ea-f545-1e2a-7e624d7e9f57.png)

見え無くしたり
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/34998eb2-d8bb-396a-6b1f-411d82fa5b2c.png)

ノードの色を調整するという事が可能なプラグインです。
![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/511538ce-49f7-0289-64be-d28ad67b6896.gif)

どんな感じの調整方法をしているかは、GithubのReadmeを参考にすることが出来ます。
https://github.com/aquasilver/StarshipEditorStyleOverrider/blob/main/README.md

Starship Editor Style Overriderプラグインは、簡単に言えばUE Editorの基本的なStyleとBrushを全て収集し、Editorから上書き出来る（脳筋）エンジニア向けプラグインです。

プリセットみたいなのがあって、いい感じに調整できるプラグインというわけではありません。
[UE4に存在したプラグイン](https://www.unrealengine.com/marketplace/ja/product/ue4editor-customize, "UE4Customize")のように特定の箇所だけ変更できるようなプラグインではありません。
(UE4は触っていないので、間違っているかもしれません）

UE使用者のニーズや機能は多岐に渡るため
全てのStyleやBrushに対して調整を行えるようにして、後は各自、頑張ってくださいという設計になっています。

エンジンのコード次第では特殊な扱われ方をしているStyleなどがあり、このプラグインを使用しても、正しく反映させることは出来ない可能性があります。
そのため、以下の知識やスキルがあれば、このプラグインをより便利に扱えると思っております。


#### プラグインを扱うために必要な知識やスキル

- UE5 のEditor Style知識
- エンジンコードを検索して、自分が知りたいコードの場所を発見することが出来ること。

### 自己紹介 + プラグインの開発に至った経緯(スキップ化）
<details><summary>自己紹介 + プラグインの開発に至った経緯</summary><dev>

初めまして、しるばーです。
今は転職活動中ですが、退職前は10年ほどゲームプレイエンジニアをしていました。
内製のゲームエンジンを使用してエンジン拡張やゲーム開発を携わっておりました。
2023年9月頃から初めてUnreal Engine 5を触り始め、最初にUEの感想はリフレクションシステムすごい！でした。
(BPでEditorを拡張できるのは、いい意味でカオス）
次に出た感想としては、Editorが黒基調で見にくい事でした。
黒と白は私の眼にはダメージが大きく、ゲーム開発をするためには良いスタイルとは思えませんでした。
私は、長時間作業で疲れないため（目が大事）にハードウェア側のディスプレイの設定の照度を下げ、さらにソフトウェア側からも明るさを調整しています。（会社の人には私のディスプレイは見にくくて評判でした？）
つまり、まぁまぁ暗いです。

閑話休題
UE5にはカラーテーマという機能があり、大雑把な項目に対して色調整を行う事が出来ますね
私は長年グレーテーマを愛用していまして、Visual Studioや内製エンジンもグレーテーマで作業をしていました。
グレーテーマにした場合は、理想は、背景色が白ベースになり、文字の色が黒ベースです。
つまり、グレーテーマにするためには、白基調のテーマに変更するのが理想的です。
しかし、UEのカラーテーマはそこまで調整することを想定していませんでした。（たぶん）
UE素人が、いきなり、このエンジン改善に手を出すべきかという思いもあったわけですが・・・
エンジンのコードを直接編集すれば、改善できるという事は早めに分かりました。
さらに調査を進めるとプラグインとして実装することが出来ることがなんとなーく想像できたためプラグイン開発に移行しました。

</details>

## 今回のゴールはUE Editorをグレーテーマを実現する


### "まずは" カラーテーマを使ってみよう
基本的な使い方は、割愛しますが・・・以下のURLを参考にしてください。
https://forums.unrealengine.com/t/change-the-color-of-the-unreal-editor-ui-in-ue5/543843

#### カラーテーマでグレーテーマにしてみる
まずは、先駆者に頼ろう。
https://forums.unrealengine.com/t/i-really-need-a-light-editor-theme/228610
https://www.reddit.com/r/unrealengine/comments/nllds5/the_theme_editor_is_too_powerful/

今回は、↓を使用しました。
https://pastebin.com/raw/mVBq7bpv)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/4bcd1614-3f60-8f7f-425a-53b6bcfcf2fd.png)

 :angel: ( 眩しい! )

他にも共有されているテーマを使用してみましたが、どれもいまいちでした。
先駆者の力を借りつつ、後は自分で色を調整してみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/17da582b-4544-72f3-0016-3fcf8fe67555.png)

グレーテーマ愛用者は、ここで手を打つかもしれません。
しかし、微妙な所がいくつかあります。

なんで、検索するときの文字は白なの？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/36e120e7-2631-7dab-2153-4ea5cb1057f0.png)

SearchBoxに対するフォーカスが解除されれば、黒に戻ります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/97cc4d64-81e2-0a9f-b884-189c3040d6d6.png)

Q. なぜ、Search Boxのフォーカス時は白色になるの？
A. Whiteでハードコードされているから。
(Content BrowserのSearch BoxのStyleはFilterBar.SearchBoxですが、Box StyleはNormalEditalbeTextBoxを使用しています。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/5b117de3-34af-89b2-f735-1dbdaf05f116.png)

つまり、あの眩しかったThemeのSearch Boxは入力時は何も見えません。

Q. カラーテーマのWhiteの色を黒に設定し、Blackを白色に設定すれば解決するのでは？
A. 残念ながら、しません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/a18872f4-5819-f021-c5f6-bd506ea1c488.png)

まるで、見せてはいけない所を隠すかのようなStyleの出来上がりです。

ハードコードされているWhiteをBlackにすれば、Search Boxの白色問題は回避することが出来ます。
折角？なので、UEの拡張性の限界値を知るために、プラグイン開発に乗り出しました。

## UE Editor Styleの基本的な仕組みについて (スキップ化）
<details><summary>UE Editor Styleの基本的な仕組みについて</summary><dev>

これより先は、プラグイン開発のお話に移ります。
そして、FAppStyleやFStarshipCoreStyleなどの仕組みを知っている前提で説明をしております。
軽く Styleについて説明します。

##### Styleについて

例えば、SButtonには二つのStyleを設定することが出来ます。
- ButtonStyle : ボタンの見た目のStyle
- TextStyle : ボタン内に表示されるTextのStyle
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/1b180726-6aeb-c943-60f2-fa3745be2346.png)

TextStyleのデフォルトで設定されている"ButtonText"のStyleを見てみましょう。
"ButtonText"は、NormalTextというStyleとして設定されていますね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/d32929c4-aa4a-03e6-8580-38ef30f00fcd.png)

NormalTextの設定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/0fd7eec2-7a01-7085-52b4-12bd584a3d94.png)

SetColorAndOpacityに、FSlateColor::UseForeground()が設定されていますね。
UEのColor ThemeのForegroundの色を変えることで、確認してみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/1d0d161d-e1a0-9fa9-1810-4b9a4b3982e4.png)

FSlateColor::UseForeground()は、NormalText以外の様々なStyleで使用されているため、影響範囲が大きいですね
様々なWidgetは、何かしらのStyleが設定されているという事が、分かりました。


#### FStarshipCoreStyleとFStarshipEditorStyleについて
SButtonのStyleは、FCoreStyleから取得していますが、最終的に先ほどのスクリーンショットではFStarshipCoreStyleというクラスに定義されていました。

FStarshipCoreStyleというクラスは、EditorやEngineが使用するためのStyleを定義しているクラスです。
さらに、FStarshipEditorStyleというクラスが存在し、Editor向けのStyleを定義しているクラスがあります。

StyleやBrushを定義するクラスは、独自に実装が可能で、エンジン内に複数存在しています。
FStarshipCoreStyleとFStarshipEditorStyleは、そのうちの二つとなります。
しかし、この二つのクラスは、StyleやBrushを取得する際のデフォルトのStyleとして扱うように設定されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/fc8d3bb2-eab4-1940-2274-c6ee216f9fdc.png)
(FStarshipEditorStyleは、FStarshipCoreStyleを親設定にしているため、FStarshipCoreStyleを、このタイミングで設定する必要性は無い）

##### FAppStyleについて
FAppStyleは、かなりコンパクトなクラスです。
SetAppStyleSetNameで、設定したAppStyleの名前を取得するクラスです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/b3702116-9724-2d32-d397-2154fed96ad7.png)

SButtonがStyleを取得していた先は、FCoreStyleです
ただFCoreStyleは、FAppStyle::Get()を使用しているだけです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/3fecc187-756d-a9bf-1d3c-8dfdcff98539.png)

FAppStyle::SetAppStyleSetNameを使用して、別のAppStyleを基準にしてStyleやBrushを読み込ませることで
エンジン内のStyleやBrushを別の値に変化させることが出来ます。

</details>

## Starship Editor Style Overrider プラグインの開発
(略：Starshipプラグイン）


このプラグインを使用すると、SearchBoxの色も変えれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/cb98af6c-7d82-cc0b-23a1-c5102eca91fa.png)



Starship Editor Style Overriderは、以下のようなフローでStyleを上書きしています。
1. **FStarshipEditorStyle**がFAppStyleに登録後を検出する。(Engine/Source/Editor/EditorStyle/Private/StarshipStyle.h)
1. Starshipプラグインから、FAppStyle::SetAppStyleSetNameを実行して、Starshipプラグイン用に実装したFStarshipOverrideStyleにすり替えます。
1. 上書きしたいStyle情報をFStarshipOverrideStyleに設定する

ざっくり説明すると、このような形で実装しました。
Editorの実装はStyleを上書きするためには、マニュアルで書き込めば不必要なのですが、さすがにEditorは必須でした。
掻い摘んで説明したいのですが、説明が長くなるため、コード見てくれ・・・ということで
開発中に起きた問題を抜粋し、説明していきたいと思います。

### Case 1 : USTRUCTのオブジェクトをProperty Viewに表示したい
多くのStyleはFSlateWidgetStyleで構成されています。(FSlateWidgetStyleはUSTRUCTです。）
手っ取り早く、BPのProperty Viewを紐解いてSKismetInspectorを使用していることが判明しました。
(今となっては、FPropertyEditorModuleのいくつかの関数を使えば、おそらく似たような事は出来ると判明しました。）
SKismetInspector::ShowSingleStructという便利なメソッドが用意されており、こちらを使用することでStyleのStructのパラメーターを表示・調整することが出来ました。

### Case 2 : Editor上から、変更箇所をリセットさせたい。
なんだかんだ、コードをかみ砕きながらStarshipプラグインのEditorを実装中に、調整した個所をリセットしたいという普通の発想が出てきました。

↓のスクショは実装後ですが、赤いチェックマークが変更箇所を表しています。
チェックマークが無ければ、一体どこを調整したのか、分からないです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/068d25ba-0a6c-f57a-073b-08700898f35d.png)

Unreal Engineを使っている方々はProperty Viewでリセットボタンって見たことあると思います。
（"Reset this property to its default value"の所です。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/b9a9d827-619d-0aa5-5937-4b591e244611.png)

Starshipプラグインでは、二つのリセット機能が必要になりました。
1. StarshipプラグインのEditorで編集したセーブ前のデータリセット
1. セーブされているパラメーターを初期値に戻すためのデータリセット

UEが用意してくれているProperty Viewでのリセットボタンは前者で補いました。
後者は、自前で実装する必要がありました。

私は、BrushやリセットボタンのようにWidgetを実装するのではなく、右クリックのContext Menuでリセットしたいというわがままがありました。
拡張性に優れているUEなら何でも出来るんだよね？という淡い期待を信じて、実装に手を付けました。

Property Viewの項目に対してContext Menuのカスタマイズは、通常の手段では許可していません。
デフォルトのContext Menuが基本的に表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/a8257360-9591-f455-95e9-59814d34c930.png)

エンジンの設計思想なのか、なんとなくこうなっているのかは、分かりませんが
Brushのプロパティを見ると分かりやすいです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/0b9daa8b-b90f-ae6d-d153-4affd10a420f.png)
ブラシの設定やリセットなどは、ドロップダウンから行うようにしています。
Property Viewの右クリックによるContext Menuでは、アクセスすることが出来ません。
PropertyViewに関しては、Viewのカスタマイズは容易という事が伝わってきました。

それでも・・・拡張性に優れているUEなら**何かやり方あるでしょ？** と、私は期待しました
(Context Menuの拡張性はFExtenderを使用したやり方が主流？っぽいですが、Property ViewのContext MenuはFExtenderを扱っていないため、出来ませんでした。)

紆余曲折を経て、たどり着いた答えが**IDetailPropertyExtensionHandler**を使用する事でした。
IDetailPropertyExtensionHandlerは、Property Viewの行をカスタマイズすることが出来ます。
Property ViewのBrushのように専用のインターフェースを提供することが出来るイメージです。
（Brushは、IDetailPropertyExtensionHandlerを使っていませんが・・・）

調整用のStyleを表示しようとした際に、FDetailWidgetRowに対して、AddCustomContextMenuActionでContext Menuを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/2d654d4f-4b15-512e-31d5-c271db2106b1.png)

わーい、これでContext Menuが出来たぞ！

### Case 3 : IDetailPropertyExtensionHandlerが動作しない
原因は、USTRUCTにあります。
Case 1で、SKismetInspector::ShowSingleStructでProperty Viewを作成していましたが、IDetailPropertyExtensionHandlerはUCLASSのみにしか作用してくれませんでした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/27e2a0a5-b34f-72f7-836c-b3d8a62ee98a.png)

なぜこのような設計になっているのかは不明ですが、USTRUCTからUCLASSに変更対応をしないといけないわけです
しかし、StyleやBrushはUSTRUCTなわけです。
（この時点で、私はプラグイン無理かもと思いました）


UCLASSに変更するにしても、UOBJECTを継承したクラスでは対応することは出来ないです。
```
UCLASS()
class MyClass : public UObject
{
    GENERATED_UCLASS_BODY()

    UPROPERTY()
    FSlateWidgetStyle* Style; // Build Error
};
```
UPROPERTYはPointerに対応していません。
また、TObjectPtrはUCLASSしかサポートしていないです。
各StyleのクラスごとにUCLASSを作成すれば一応は出来るのですが、まぁまぁ数があるためやりたくありません。

私が目を付けたのはBPのProperty Viewです。
BPの変数はUStructの変数やUClassの変数を取り扱う事が出来ています。
また、UClass経由からUStructの生成を行っていることが分かりました。

Case 4に続きます。

### Case 4 : UClassのクラスを作成する方法
UObjectを継承したクラスを作成するのではなく、UClass自体を作成するという意味です。

例えば、このような感じの処理をイメージします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/5f5903e6-a4d7-d5f3-b27d-adb5731bf96a.png)

AddCppPropertyを使用することで、任意のFPropertyを追加することが出来ます。
このテクニックを利用して、任意のUStructのプロパティをUClassに追加して、SKismetInspectorにObjectを渡してあげることでUClassのオブジェクトとしてProperty Viewで表示してくれるようになりました。

ここら辺の挙動がネットやエンジン内で溢れているわけではないため、大雑把に仕組みは理解していますが細かい箇所は、意味が分かっていない所もあります。
（例えば、AssembleReferenceTokenStreamってなんだよとかね）

長い道のりでしたが、Context Menuをカスタマイズすることが出来ました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20924/572d6335-6395-77cb-0367-2c4c80c9975b.png)

### Case 5: Brushの数が3000個以上でクラッシュしてしまう
- FStarshipCoreStyle
- FStarshipEditorStyle

Starshipプラグインは上記二つのStyleから合法的に情報を抜き取り、調整可能にしています。
二つのStarshipStyleからBrushの数を合わせると、3000個以上登録されていました。

またクラッシュの原因は、当時の設計ではStarshipプラグインのEditorを立ち上げた際に
一つのUClassに対して、大量のFPropertyを登録する形で実装を行っていました。
（回避方法あったかもですが）あっけなくFPropertyの登録の限界値を迎えたのが原因だったようです。

- Property Viewが表示されるタイミングでUClassを作成する
- UClassに登録するFPropertyは一つに変更する。（複数のUClassを作成する）

という形にすることで問題を回避しました。

## まとめ
他にも、:angel_tone2:みたいな状態になったケースもあったのですが、長くなってしまうので
またの機会にしたいと思います。

Starship Editor Style Overriderは、完璧ではありません。
変更できない箇所や再起動が必要な個所、リアルタイムに反映されないなどのパラメーターが存在します。
また、変更したい箇所が一体何のStyleを使用しているのかは、エンジンのコードを見ない限りは辿り着けないデザインになっています。
（Widget Refrectorみたいに、Style判別ツールとかあるといいよね、たぶん、大変。）

マーケットプレイスでDL出来るのはWindowsのみですが、プラットフォーム専用の処理はしていないと思いますので
恐らく、絶対、たぶん、他のPlatformでも動作するはずです。
もし、ご興味があればGitHubからご利用していただけると幸いです。

### どうでもいい感想
UE5を本格的に触る前から、果たしてプラグインなんか開発出来るのか？という気持ちが最初にありました。
調査時間のほうが実装よりも大量に消費させられました。（よくある奴）
基本的な事はネットから知ることが出来るので、ある程度は楽でいいですね
後は、この関数って何？とかこの機能はどういうことが出来るの？などをChat GPTに質問して教えてもらっていました。
ただ、まぁまぁな確率で適当な答えを出してくるので、参考程度に抑えて後は自分で調べるというような扱い方をしていました。
（ちょっとポンコツな先輩みたいな感じ）

ちょっと長くなってしまいましたが、最後までお読みいただきありがとうざいました。