PaperSloth’s diary

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

UE5 ちょっと便利かもしれないプログラミングの書き方

環境

・UE5.3.2
・Rider 2023.3.2

概要

対象読者の想定としてはプロの現役プログラマーというよりは、そこを目指して勉学に励んでいる方のレベルを想定しています。
プログラマーのアーティストの方にも役立ち部分はあるかもしれません。
現在プログラマーの方には真新しい情報はないと思われます。

UE4/5向けにBlueprintやC++で記述していますが、C#やその他の言語でも活用できるちょっとしたTips程度のものをいくつ書いてみました。
BlueprintとUnreal C++のどちらでも利用可能なものを紹介していきます。

if文 / branchノードの削減または単純化 (Min / Max / Clampの利用)

ここではBlueprintやC++向けに紹介していますが、Min / Max / ClampはMaterialでも使用可能です。
また、0.0 ~ 1.0のClampに関してはMaterialではsaturateノードがあり
個人的にはそちらの方が0 - 1のClampであることが伝わりやすくて好みです。

アイテムや魔法での回復処理を組むケース

まずは以下のようにゲームで回復を行った時に
現在HP + 回復量の値が最大HPを超えたら最大HPとして
そうでない場合は回復後のHPを割り当てるような処理があった場合
通常は以下のように組むことがあると思います。

こういったケースではif文は不要でMinノードを使用することで簡潔に記述できます。

さらに、Blueprintの場合はMathExpressionでより一層簡潔に記述が可能です。

ダメージを受けてHPが減るケース

実際にはAny | Point | Radial Damage Event等を使用しますが、ここでは簡潔に説明するためCustomEventで定義しています。

先程同様にダメージ処理を組んでいきます。
HP - ダメージが0以下ならHPを0とし、そうでない場合はHP - ダメージが現在のHPとなります。

今度はMaxノードを使用します。

Minのケースと同様にMath Expression化可能です。

Clampについて

Clampは上記のMinとMaxを組み合わせたものです。
Value, Min, Maxの3つのPinがあり、ValueがMin以上でMax以下となるように制限をかけてくれます。

最後にC++での記述を載せておきます。

// 実際にはMaxHealth等のパラメーターはDataTableやDataAsset等にして管理するのが望ましいです。
void OnHeal(const float Heal)
{
    Health = FMath::Min(MaxHealth, Health + Heal);
}
void OnDamage(const float Damage)
{
    Health = FMath::Max(0.0f, Health - Damage);
}

配列のwrap around(上限に達したら最初に戻る)

先ずは、以下のように手持ちの武器を次の武器に切り替えるような処理を組んだとします。

このままでも現在選択している武器のindex情報は正しく
剣 -> 銃 -> 拡張武器 -> 剣... といった形で次の武器に切り替えることが可能です。

こういった処理をより簡潔に組むには、index % Max といった形で配列の最大数で剰余してやればよいです。

wrap aroundをよく使う場面としては
ゲームの設定画面やアイテム選択画面なんかが特に多いと思います。
設定内の画面品質のUIで
クオリティー低 -> 中 -> 高 -> 最高 -> 低... といった場面ですね。

C++で組むと以下のようになります。

TArray<FString> WeaponArray{"Sword", "Gun", "ExtraWeapon"};
void ChangeNextWeapon()
{
    CurrentWeanponIndex = (WeaponArray.Num() % ++CurrentWeanponIndex);
}

マイナス方向の配列のwrap around(下限に達したら最大数に戻る)

残念ながらマイナス方向のwrap aroundは剰余で求めることができません。

Xにて親切なお犬様がマイナス方向のwrap aroundについて教えてくださいました。

まずは愚直に組むと以下のような処理になるかと思います。

先程教えていただいた内容を元にノードを組むと以下のようになります。
Blueprintの配列操作にはLastIndexというノードがあり、配列の末尾のindexを取得可能です。
この場合は要素数が3つなので、Lengthは3となり
LastIndexは0, 1, 2と0始まりでカウントされるため末尾の2が取得できます。
内容としてはLength - 1と同義です。

最後にC++で組むと以下のようになります。

void ChangePrevWeapon()
{
    const int WeaponNum = WeaponArray.Num();
    CurrentWeanponIndex = (CurrentWeanponIndex + (WeaponNum - 1)) % WeaponNum;
}


先程のプラス方向のwrap aroundと組み合わせて
プラスマイナスの双方向を考慮したWrap関数を作っておくと便利かもしれません。

小数点の比較 (floatの比較)

例えば以下のように何かしらのfloat値に計算をして、==ノードで0.0 かどうかを比較したいといったケースがあると思います。

この場合に、良くも悪くもBlueprintではわりと動いてtrue になることがあるのですが
基本的に小数点の比較というのはプログラミングでは御八度です。
小数点の誤差でfalse になるケースがほとんどだったりします。

こういった誤差を考慮してだいたい同じ値という比較用のノードが用意されています。
それがNearly Equalノードです。
原則として、このノードを使うようにするとよいでしょう。

C++で記述すると以下です。

bool IsZeroFloat(const float Value)
{
    // C++の場合は0との比較用にIsNearlyZero()があります
    return FMath::IsNearlyZero(Value);
    // return FMath::IsNearlyEqual(Value, 0.0f);
}

余談ですが、UnityではMathf.Approximatelyを使用することで比較が可能です。

早期returnの活用

正直なところ、Blueprintでは特にシンプルになった感じがなくあまり恩恵を感じられないかもですが・・・
攻撃用の関数を以下のように組んだとします。
HPが0以上で攻撃のクールタイムが0になっていたら攻撃をするといった関数です。

条件式として
HPが0以上 かつ
クールタイムが0
であれば攻撃となります。

こういったケースでは早期returnを使用すると良いとされています。
HPが0であれば攻撃しない
クールタイムが残っていれば攻撃しない
といった条件式に置き換えることができます。

Blueprintでは恩恵を受けれている感じがないですね。
C++で記述してみます。
先ずは早期return なしver

void Attack()
{
    if (Health > 0.0f)
    {
        if (FMath::IsNearlyZero(CoolTime))
        {
            // 何かしらの攻撃処理
        }
    }
}

このようにネストが深くなって処理が読みにくいです。
早期returnで書き換えると以下のようにスッキリします。

void Attack()
{
    if (FMath::IsNearlyZero(Health))
        return;
    if (0.0f < CoolTime)
        return;
    // 何かしらの攻撃処理
}

まとめ

C++に絞ればもっとたくさんのネタがありますが、BlueprintとC++で考えてみるとあまり浮かびませんでした。
いずれも学生時代の頃に知って便利だと感じた小ネタになります。

早期return や説明用のlocal 変数定義等については、C++やその他の言語では読みやすくなって便利なのですが
いかんせんノードエディタではその恩恵があまり受けられず
むしろlocal変数を増やしたりすることで余計にノード郡が大きくなってしまったりと逆効果かな?と感じることがあります。

こういった浅いネタでなく、もう少しちゃんとしたネタが浮かべばまた書いていこうと思います。