現役GDが考えるUE4ブループリントのきれいな書き方

Blueprint

はじめに

UE4のブループリント(以下、BP)はビジュアルスクリプティング言語1と呼ばれるグラフィカルなプログラミング言語である。

ビジュアルスクリプティング言語はテキストベースのスクリプティング言語に比べて歴史が浅く、言語ごとに特性も大きく変わることなどから「こう書くべき」というノウハウがあまり溜まっていない。また、UE4のBPはゲームデザイナーやアーティストなどのノンプログラマ向けに作られた経緯もあることから、使用者が書き方・スタイルガイドへの意識やノウハウが少ない。

結果以下のような問題をBPは抱えている;

  • 👎ノードをつなぐワイヤーのスパゲッティ化
    • 見て理解するのが困難になり生産性が落ちる
    • 問題のある処理を見逃しがちになる
  • 👎ワイヤー繋げなおしの手間がかかることによる処理の移植のしにくさ
  • 👎一画面に処理が収まりきらず処理を理解するために画面スクロール・ズームを多用せざるを得ない
    • UE4のエディタは一般的にDual Displayだと足りないといわれる一因

以下紹介する書き方は上記問題を解決し、不具合を防止し生産性を上げるBPの書き方のノウハウになる。

もし、一度もUE4スタイルガイドを見たことがない場合は一度目を通しておくべき。分量がかなりあるのでBPの項目だけでも良い

基本単位は実行ピンのあるノード単位でまとめ、まっすぐに並べる

ルール 基本単位は実行ピンのあるノード単位でまとめる

まず、実行ピン2 が入出力ピンとしてついているノードを基本の単位として考える。

該当するのはフロー制御ノード、イベントノード、普通の関数ノード、変数のSetノード、マクロノード、サブグラフ(折りたたみグラフ)ノード等。

「基本単位」の基準になる実行ピン付ノードの例

BPの処理は実行ピン同時をつなぐ 実行ワイヤー3 をたどって処理が行われる。実行ピンの付かないノードはその意味では「脇道」なので基本的な流れを追いかけるのはこの 基本単位4 を追いかければよい。

ルール 基本単位同士をまっすぐに並べる

実行ワイヤーをまっすぐになるっように実行ピン付ノードを並べる

次に、『Gamemakin UE4スタイルガイド』5 を参考に実行ピン付ノード同士を直線状に並べてゆく。

前述のように、BPの処理は実行ピン同時をつなぐ 実行ワイヤー3 をたどって処理が行われるので、これらのノードがわかりやすく整列して並んでいないと人間の理解が困難になってゆく。仮に迷路のような並び方をしていた場合、遅かれ早かれバグの温床になってしまう。

実行ピン付ノードをまっすぐに並べる事で、 どこを目で追いかければよいかが一発で見分けられるようにする のが目的。
また、以降に記載するルールの恩恵を得るための基本的な前提となる。

実行ピン付ノードの下に実行ピン無しのノードを積み重ねる

ルール 実行ピン付ノードの下に実行ピン無しのノードを積み重ねる

実行ピン付のノードには「実行ピンではない入力ピン」(プログラミング用語でいえば関数の引数に当たる)があることがある。
そのピンへワイヤーを繋ぐ元のノードが実行ピン付でない場合は、実行ピン付ノードから位置を離すのではなくすぐ下に重ねるように隣接して設置する。

ノードを重ねる

実行ピン無しノードからさらに実行ピン無しノードが繋がっている場合(例:コンポーネントの変数をGetするときなど)は、さらにその下に配置して、先がなくなるまで下に重ねてゆく。

実行ピンなしノードはどんどん下に重ねる

該当するのは、数値の四則演算ノードや変数のGetノード、pureに指定された関数(純粋関数6)ノードなど。

実行ピン付のノードとそれ同士の繋がりを「木の幹」とすると、実行ピンの付かないノードは「木の枝葉」に当たる。
木の幹と枝葉(本筋と脇道)が一目で分かるように、並べ方で区別がつくようにする のが一つ目の目的である。

もしこのルールに従わない場合、ワイヤーの色に差があるとはいえメインの処理の流れにみえるように配置することも可能であり、人間の理解が困難になる。

幹と枝葉の区別の付きにくい悪い例の図

二つ目の目的としては、 基本単位(実行ピン付きノード+それから参照される実行ピン無しノード)をブロック状に一定範囲にまとめておくことで矩形選択しやすくする ことである。矩形選択しやすいということは処理の単位で動かしやすく整理しやすい、コメントノードでくくりやすい7、あるいはコピーやカットなど移植がしやすいということにつながる。

もしこのルールに従わない場合、グラフを見やすく整理したり、リファクタリングしたり、処理を移動する際に、マウスクリックやコピーペーストなどの作業が何度も発生し、「書くのが大変」という状況になってしまう。

矩形選択しにくい悪い例の図

BPに対してテキストベースのほうが書くスピードが速くてマシ、と思っている人は、この方法を試してもらうと大分マシになるだろう。テキストにも書き方のコツがあるように、BPなどのヴィジュアルノードベースにも「書き方」(描き方?)のコツがある。

同じ基本単位のノード群は左端でそろえる

ルール 同じ基本単位(実行ピン付きノード+それから参照される実行ピン無しノード群)のノードはその左端でそろえる

そろえることで 基本単位4を視覚的に認識しやすくする のが目的。
ただし、四端どこでもよいわけではなく左端でないといけない。

BPのノードは大きさが可変である。UE4エディタの表示言語を切り替えたり8、ノードの入力欄の内容の変化、ノードの対象や属するもの(例:関数ノードの属するBPクラス名)の名称の変更などによって、いくらでも大きさが変わる。

そのため右端でそろえたとしてもいつでもそろわなくなる可能性があり、あまり意味がない(かけた手間が無駄になる可能性が高い)。
左上が原点となるので、下に重ねるノード群も同じ左端でそろえるようにすることで ノードサイズの可変にも耐えられるようにする

実行ピン付ノード引数の順番とノードの積み重ね順を合わせる

ルール 引数(入力ピン)が複数あるノードの場合には、そのピンにつながる先のノード群の配置も入力ピンの順番に合わせる

順番を合わせることで理解をしやすく し、また 矩形選択しやすくする のが目的。

一つの基本単位に関連するノードを重ねた場合など、縦に大きくなることがしばしばある。この場合、仮に入力ピンの順番と繋がった先のノード群の並びの順番がバラバラであると、どのようにつながっているのかわかりにくい。また矩形選択も関係ないノードが混ざりこむためにしにくくなる。

ノードの入力ピンが多い場合はノード2つ分の幅を使って並べる

ルール ノードの入力ピンが多い場合はノード2つ分の幅を使って並べる

入力ピンが多いノードの並び方のルールの例

過剰に縦に長い基本単位ができるのを防ぐ のが目的。

なお、入力ピンの先のノードが単純でなく数珠つなぎになる場合はまとめてpureな関数(純粋関数)ノードにまとめることを検討する。

pureな関数ノードに入力ピンをまとめる

入力ピンが多くない場合でも検討してみるとよい。

入出力ピンから伸びるワイヤーを一本に絞る

ルール 入出力ピンから伸びるワイヤーを一本に絞る

1つ目の目的は スパゲッティ化の防止

特にあるノードの出力ピンから複数のノードでワイヤーが繋がっている場合、まるでタコ足配線のようにあちこちにワイヤーが繋がることになる。このような場合ワイヤーがノードと重なる可能性が高く、ワイヤーを目で追うときに邪魔になって理解がしにくくなる。また、遠く離れたノードからワイヤーを伸ばすことにもなり、画面をスクロールしないと処理が追えないという読みにくさにもつながる。

BPにおいては出力ピンに複数のワイヤーをつなげることはスパゲッティ化を招く第一歩であり、避けなければならない。

2つ目の目的は 基本単位の移植時にワイヤーの接続情報を維持する ことである。

具体的には次のルールに従う。

基本単位内で使うものは基本単位内で完結するようにする

ルール 基本単位内で繋ぐノードは他から繋ぐのではなく基本単位内で完結するようにそれぞれ配置して繋ぐ

たとえば 基本単位A と 基本単位B の2つの基本単位があり、ローカル変数Xの値を参照する共通の処理があるとする。

この時何も考えずに思わず、Get Xノードからそれぞれの基本単位内のノードにつなげたくなるが、そうしてしまうとスパゲッティ化の第一歩(2本のワイヤー だけ で済むならよいが本当に将来も2本だけなのかは 誰にも保障できない 。気が付いたらスパゲッティ化している。)につながるうえに、矩形選択時に一つだけGet Xノードが漏れるため選択しにくくなってしまう。

この例ではGet Xノードを2つ配置し、それぞれ 基本単位A と 基本単位B のブロックの中に納まるように配置する。

こうすることで 将来にわたってもスパゲッティ化を起こさず矩形選択しやすさを維持できる

複数本の実行ワイヤーは入力ピンにつなぐ前にRerouteノードでまとめる

ルール 複数本の実行ワイヤーは入力ピンにつなぐ前にRerouteノードでまとめる

ワイヤーを直接一つのピンにつなぐのではなく、一旦 Rerouteノード9 で束ねたうえで入力ピンには1本だけ繋ぐようにする。

コピー&ペーストやカットなどで処理を移植する際に、Rerouteノード込みで選択しておくことで、 ワイヤーのつながりが維持される のが目的。繋ぎなおす手間、覚えておく手間を減らし、つなぎ間違いによるケアレスミスを防止する。

仮に、いきなりつながったままだと移植後にワイヤーが切れてしまい、繋ぎなおす手間がかかる。また、ちゃんと覚えていられれば良いものの、つなぐ先を間違ってしまい、処理を移植したことでエンバグする恐れがある。

原則、処理の結果は変数に入れ、他の処理単位にワイヤーで繋がない

ルール 原則、処理の結果は変数に入れ、他の処理単位にワイヤーで繋がない

基本単位内で使うものは基本単位内で完結するようにする」のメリット、 スパゲッティ化防止・移植性の維持 を得るために、原則処理結果は変数に確保し、直接ワイヤーで繋がないようにする。

以下は例外
  • 例外
    • 1回しか使用しない場合
    • 実行ノード同士が隣接する
1回しか使用しないかどうか分からないからといってむやみやたらに変数に入れておく必要はない。1回しか使用せず、ワイヤーがスパゲッティ化する恐れがほとんどないなら問題ないだろう。

実行ワイヤーで繋いだ”行”はSequenceノードを用いて繋ぐ

ルール 意味のある実行ピン付ノードの繋がりを”行”とみなし適宜”改行”する

基本単位4 をつないでいくとき、処理の意味として区切りを作ることができる。これを”行”とみなす。
そうとらえることで、”改行”することができる。

“改行”をして”行”単位で分離することで、コメントノードで分かりやすく区別できるようにする ことが目的。また 行単位での移植性も確保 できる。

ルール “行”同士を`Sequence`ノードで繋ぐ

“改行”の実現方法としてはZの文字のようにワイヤーをつないでいくことができるが、行内の処理を変更するたびにつなぎなおしたり、斜めにワイヤーが張られることでノードにかぶったりとあまり好ましくならない。

そこで Sequenceノード10 を使って複数の”行”をつなぐことで、行末端のノードからワイヤーを排除できる。例えば変数の初期化処理をまとめた行があった場合、Z字につないでいる場合は変数が増えるたびにつなぎなおしが必要になってしまうが、Sequenceノードを使えば単に後ろに初期化のノードを繋いでいくだけでよくなる。

Sequenceノードを使った”改行”は、 行単位での移植性をより高めておく のが目的。

なお、Sequenceノードはいくつか注意点がある。

: Delayノードは想定の挙動にならない

もんしょの巣穴blog [UE4] SequenceとDelayの関係

: 途中の出力ピンのワイヤー末端でReturnノードが来るとSequenceの処理は中断される。

このような点に注意して使用する。

処理が莫大になって大きな画面スペースが必要になってしまった場合の解決方法として、この縦に伸びるようにするルール以外にも関数化・マクロ化・サブグラフ化という手段もある。

同じ処理があった場合はコピペするのではなく、関数化しよう。ほんの少しの差は引数で吸収できる。

同じ処理があり、かつ実行ピンが入力・出力どちらかで複数になる場合はマクロ化する。ただしBPのマクロは遅いので可能な限り使用しないようにする。

上記以外はサブグラフ化してまとめられるが、サブグラフを開いて読む手間というのも存在するのでそのコストは考えたほうが良い。個人的にはよほど大規模でない限り不要、大規模になってしまう場合はそもそも複数人の作業でバッティングする可能性が高いのでファイルを分けることを検討したほうが良いと思う。

関数化・マクロ化・サブグラフ化のオーバーヘッドに関してはきちんと計測をしたほうが良い。

“行”の途中の折り返しはRerouteノードを使ってノードに被らないよう直線的に引く

ルール “行”の途中の折り返しはRerouteノードを使ってノードに被らないよう直線的に引く

ワイヤーが被らないようにする

ワイヤーがノードなどに被ることを防止する ことが目的。

“行”があまりにも横に伸びてしまった場合や、ForEachLoopWithBreakノード等があった場合、”行”の折り返しが必要になる。何も考えずにそのままつなぐと、ノードや基本単位の中にワイヤーがかぶることで処理が追いにくくなってしまう。

ワイヤーが一つのノードにループして戻ってくる場合は上を通す

ルール ワイヤーが一つのノードにループして戻ってくる場合は上を通す

読みやすさを維持しながら更新のコストを下げる のが目的。

そのままつなぐと確実にワイヤーとノードが重なって処理が追いにくくなるのでRerouteノードで整理する。

下をぐるっと通すこともできるが、以下の問題がある。

  • これまでのルールでノードを下に重ねるようにしたためかなり大回りになってしまう
  • 処理を追加した場合など繋ぎなおし・Rerouteノードの配置しなおしが発生する
  • ForEachLoopWithBreakノードのCompletedピンのように下に続きの処理が伸びる時に交差する

上を通すことで最小限のメンテコストで見やすさを維持できる。

良い例
https://blueprintue.com/blueprint/k21kb8k4/

余談:BP以外での活用方法

AnimBPのAnimノードグラフやマテリアルのノードグラフの場合、BPのグラフに比べて実行ワイヤーが少ない。その点注意すればこのルールを活用することもできる。

BP以外のUE4のノードのきれいな書き方のルールは別途考えていかないといけないだろう。

まとめ

ルールと目的まとめリスト

  • ルール 基本単位は実行ピンのあるノード単位でまとめる
  • ルール 基本単位同士をまっすぐに並べる
    • どこを目で追いかければよいかが一発で見分けられるようにする
  • ルール 実行ピン付ノードの下に実行ピン無しのノードを積み重ねる
    1. 木の幹と枝葉(本筋と脇道)が一目で分かるように、並べ方で区別がつくようにする
    2. 基本単位(実行ピン付きノード+それから参照される実行ピン無しノード)をブロック状に一定範囲にまとめておくことで矩形選択しやすくする
    • ルール 同じ基本単位(実行ピン付きノード+それから参照される実行ピン無しノード群)のノードはその左端でそろえる
      • 基本単位4を視覚的に認識しやすくする
      • ノードサイズの可変にも耐えられるようにする
    • ルール 引数(入力ピン)が複数あるノードの場合には、そのピンにつながる先のノード群の配置も入力ピンの順番に合わせる
      • 順番を合わせることで理解をしやすく
      • 矩形選択しやすくする
    • ルール ノードの入力ピンが多い場合はノード2つ分の幅を使って並べる
      • 過剰に縦に長い基本単位ができるのを防ぐ
  • 入出力ピンから伸びるワイヤーを一本に絞る
    • スパゲッティ化の防止
    • 基本単位の移植時にワイヤーの接続情報を維持する
    • ルール 基本単位内で繋ぐノードは他から繋ぐのではなく基本単位内で完結するようにそれぞれ配置して繋ぐ
      • 将来にわたってもスパゲッティ化を起こさず
      • 矩形選択しやすさを維持できる
    • ルール 複数本の実行ワイヤーは入力ピンにつなぐ前にRerouteノードでまとめる
      • ワイヤーのつながりが維持される
  • ルール 原則、処理の結果は変数に入れ、他の処理単位にワイヤーで繋がない
    • スパゲッティ化防止・移植性の維持
  • 実行ワイヤーで繋いだ”行”はSequenceノードを用いて繋ぐ
    • ルール 意味のある実行ピン付ノードの繋がりを”行”とみなし適宜”改行”する
      • コメントノードで分かりやすく区別できるようにする
      • 行単位での移植性も確保
    • ルール “行”同士をSequenceノードで繋ぐ
      • 行単位での移植性をより高めておく
  • ルール “行”の途中の折り返しはRetouteノードを使ってノードに被らないよう直線的に引く
    • ワイヤーがノードなどに被ることを防止する
    • ルール ワイヤーが一つのノードにループして戻ってくる場合は上を通す
      • 読みやすさを維持しながら更新のコストを下げる

ルールの方針まとめ

これまでのルールは大きく以下の判断基準に沿ったもの。

  1. 読みやすさの維持
  2. メンテコストの削減
  3. 選択しやすさ・移植性の確保

上記に見合うこれら以外に良いルールがあればどんどん採用して「きれいな」BPを書くべし。


  1. または「ヴィジュアルプログラミング言語」とも。 ↩︎
  2. デフォルトで白いピン。公式 実行ピン ↩︎
  3. デフォルトで白いワイヤー。パルスが流れる。公式 実行ワイヤー ↩︎ ↩︎
  4. 説明の便宜上の名前。実行ピン付ノードとそこに付随するノードをまとめたグラフ上でのノード群・並べるときの単位。 ↩︎ ↩︎ ↩︎ ↩︎
  5. 出典 Gamemakin UE4スタイルガイド ↩︎
  6. 公式純粋関数と非純粋関数 ↩︎
  7. コメントノードは複数のノードを選択した後に「C」キーを押すことでまとめて括って作ることができる。 公式 ノードのアクション ↩︎
  8. ノードの表示言語を日本語化すると検索しにくくなる、という問題もあるため英語で表示をおすすめする。 ↩︎
  9. 公式 接続の再ルーティング ↩︎
  10. 公式 Sequence ↩︎
タイトルとURLをコピーしました