johnnyGameStudio’s blog

無能なゲームプログラマのぼやき ぎーくになりたい Twitter: https://twitter.com/JGS_Developer

【UE4】ガンビットAI made in UE4を公開しました【自作ライブラリOSS】

はじめに

元々僕はFF12のGambitAIがめちゃくちゃ好きで、UnityでGambitAIを作っていました
その時の記事はこちら
johnnygamestudio.hatenablog.com

で、それなりに出来あがって自己満足していたんですがこのブログのアクセス解析を見てみるとこの記事をGoogle検索からみている人がめちゃくちゃ多くてこれはちゃんとライブラリ化して公開したほうがいいなと思いました

f:id:johnnygamestudio:20200901001754p:plain

しかもGoogleで「ガンビットAI」と検索するとなんとこの記事がTOP2にヒットするんですよね
ビックリする

f:id:johnnygamestudio:20200901002111p:plain

そしてOSSとして公開したライブラリはこちら MITライセンスです

github.com

解説

Unityで作ったときはbehaviortreeとステートマシンで無駄に冗長性を持った構造にしていましたが、今回作った構造はとても単純です
ざっくりとした構造は以下のようになっています f:id:johnnygamestudio:20200901012730p:plain

Initialize時にプライオリティごとにソートされているので、ThinkActionを実行するとFF12ガンビットのように上から順繰り条件判定処理を行います
条件判定の方法としてはUAIConditionクラスを継承したBlueprintを複数用意しておいてそれを一つのActionにつき好きなだけ設定することができます

これにより、一つ一つの条件は簡単なモノでも複数の条件判定を重ねることで複雑な条件判定を設定できる且つ組み合わせ次第で新しい条件判定処理を追加実装することなく作成できます つまりは運用上はUAIConditionを少しずつ用意していくだけというのを想定しています

例えば、
「自身のHPが50%以上の場合のみTRUEのUAICondition」
「敵の中に特定の属性を持つ敵がいたらTRUEのUAICondition」
という二つのUAIConditionを組み合わせれば「自身のHPが50%以上の時、敵の中に特定属性を持つモノがいたら実行する」といった処理が出来上がります

UGambitAIComponent::ThinkActionを実行するとUGambitAIComponent::GetCurrentActionでHITしたFGambitActionInfoが取得できます
その中のActionIDを元にしてそこからはいい感じにゲーム側でアクションを実行してください

その他持っている機能

  • プライオリティ機能
  • 重み判定により同じプライオリティなら重みで判定される

HOW TO

1. JohnnyGambitLibraryをC++プラグインとしてプロジェクトに追加する

2. GambitAIComponentを持つキャラクターアクターを用意する

f:id:johnnygamestudio:20200901014726p:plain

3. UAIConditionを継承したBlueprintを作成する

3-2. 作成したBlueprintでDoAICondition関数の中身を作成する

※一応サンプルで「HPが50%以上の時にTRUE」などのCondtionは用意してあります
f:id:johnnygamestudio:20200901015409p:plain

4. FDataTableGambitAISheetを継承したDataTableを作成する

4-2. 作成したDataTableにUAIConditionなどの設定を行う

f:id:johnnygamestudio:20200901015554p:plain

4-3. キャラクターアクター内でUGambitAIComponent::InitializeAIを実行し作成したDataTableを渡す

4-4. 各キャラクターアクターが持つUGambitAIParameterのSetParameterでAIの判定に必要な条件を詰めてあげる

5. いい感じにThinkActionを実行してからGetCurrentActionでヒットしたActionIDを使ってゲーム側のアクション処理を実行する(雑)

こんな感じ(DoActionはゲーム側の関数)
f:id:johnnygamestudio:20200901020630p:plain f:id:johnnygamestudio:20200901020422p:plain

今後の課題

  • ターゲッティング周りが弱い
  • 敵と自身以外で条件を入れられない(環境情報も欲しくない?)
  • ちゃんとしたドキュメントを用意する
  • 採用タイトルをリリースする(自作のヤツ)

【UE4】各要素の重さを考慮してランダムに取得するアルゴリズム

はじめに

ゲームを作っていると「敵のステータスが瀕死状態の時は「行動A」と「行動B」と「行動C」を以下の表のように動いてほしい」といったことが頻発します

行動A 行動B 行動C
確率 10% 50% 40%

しかも厄介なのが「一定条件下だと行動Cは抽選から外す」といった仕様が追加されたりしますよね
都度都度条件式を書いているとバグの元だし、要素数が変わっても同じコードで動くようにしたいです

アルゴリズム解説

  1. TPair<重み,抽選要素>の形をした配列を作成
  2. 重み0を防ぐため調整
  3. Totalの重みを算出
  4. 0 ~ Totalの範囲で乱数を取得しそれをkeyWeightとする
  5. 配列をforで回し、重みの値で順番にtotalから減算していく
  6. total値がkeyWeightを下回ったらその要素をreturn
  7. forの中でreturnしなかったら最後の要素をreturn

実装

使いまわせるようにUtilityとして実装しました.

UCLASS()
class JOHNNYLIBRARY_API UJohnnyUtility : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()
public:

    template<typename T>
    static void AdjustWeight(TArray<TPair<int, T>>& weightList)
    {
        for (TPair<int, T>& pair : weightList)
        {
            pair.Key++;
        }
    }

    template<typename T>
    static int GetTotalWeight(TArray<TPair<int, T>>& weightList)
    {
        int total = 0;
        for (TPair<int, T>& pair : weightList)
        {
            total += pair.Key;
        }
        return total;
    }

    template<typename T>
    static T CalcWeightRate(TArray<TPair<int, T>>& weightList,int totalWeight)
    {
        int total = totalWeight;
        T ret = T();
        int keyWeight = FMath::RandRange(0,totalWeight);
        for (TPair<int, T>& pair : weightList)
        {
            int weight = pair.Key;
            T value = pair.Value;
            total -= weight;
            if (total <= keyWeight)
                return value;
            ret = value;
        }
        return ret;
    }
};

How to

  TArray<TPair<int, FName>> weightList;
    {
        TPair<int, FName> pair;
        pair.Key = 10;
        pair.Value = TEXT("Action A");
        weightList.Add(pair);
    }
    {
        TPair<int, FName> pair;
        pair.Key = 50;
        pair.Value = TEXT("Action B");
        weightList.Add(pair);
    }
    {
        TPair<int, FName> pair;
        pair.Key = 40;
        pair.Value = TEXT("Action C");
        weightList.Add(pair);
    }
    UJohnnyUtility::AdjustWeight(weightList);
    int totalWeitght = UJohnnyUtility::GetTotalWeight(weightList);
    FName hitName = UJohnnyUtility::CalcWeightRate(weightList, totalWeitght);
    GLog->Log("#####Weight Random");
    GLog->Log(hitName.ToString());

動作確認

上記のコードを面倒なのでTickでぶん回してみました
以下ログです

#####Weight Random
Action B
#####Weight Random
Action C
#####Weight Random
Action B
#####Weight Random
Action C
#####Weight Random
Action C
#####Weight Random
Action B
#####Weight Random
Action B
#####Weight Random
Action B
#####Weight Random
Action B
#####Weight Random
Action C
#####Weight Random
Action C
#####Weight Random
Action B
#####Weight Random
Action C
#####Weight Random
Action C
#####Weight Random
Action B
#####Weight Random
Action B
#####Weight Random
Action B
#####Weight Random
Action A
#####Weight Random
Action B
#####Weight Random
Action B
#####Weight Random
Action B
#####Weight Random
Action C
#####Weight Random
Action C
#####Weight Random
Action A
#####Weight Random
Action A
#####Weight Random
Action C
#####Weight Random
Action B
#####Weight Random
Action B
#####Weight Random
Action B
#####Weight Random
Action B

計30回分のログをコピペしています。割合を見てみましょう

Action A Action B Action C
回数 3回 17回 10回

それぞれ、10%、50%、40%の期待値なのでかなり期待通りの結果が出ていることがわかります
仮にAction Cが抽選から外されたとしても分母が「10+50=60」となり
Action Aは10/60で約16.6%の確率となり、Action Bは50/60で約83.3%の確率となります
(まぁこれを赦すかどうかは仕様次第ですが)

【UE4】いい感じにばらけさせた座標値を取得する試み【C++】

はじめに

中心地点からランダムになにかをポップさせたい時って結構ありますよね?
でも、いい感じにばらけさせようとするとちょっと面倒だったりします
ちゃんとやろうと思ったらコリジョン判定などで行うことはできますが、そこまでやるつもりはない…めんどくさい…

ということで大体実装するとこんな感じになるとは思うんですが
こういうプログラムをみんな車輪を再開発している気がしたので僕が書いた場合のコードをまとめておきます
もっといいやり方ないかな~と思ってるんで定番のアルゴリズムとかあったら教えてください
(こういうよく使う系の処理ってEngine側で用意してあると助かるけど、これは求めすぎかなぁ)

実行環境

UE4.24.3
Microsoft Visual Studio Community 2017 Version 15.9.21

実装

確認方法

中心地点から一定範囲以内(250以内)に10個の椅子を発生させます

「いい感じ」とは

  • 偏りが少ない
  • 座標同士はある程度離れている

ランダムに取得する際にバイアスをかける

実装したコード

// Fill out your copyright notice in the Description page of Project Settings.


#include "JGSUtility.h"
#include "Math/UnrealMathUtility.h"

FVector UJGSUtility::GetNonOverlapVector(float maxRange,int index, float degree, float repossessionRange,const TArray<FVector>& vectorArray)
{
    auto getRandomVector = [maxRange](FVector biasVector)
    {
        float xMin = FMath::Clamp((-maxRange) + biasVector.X, -maxRange, maxRange);
        float xMax = FMath::Clamp(maxRange + biasVector.X, -maxRange, maxRange);
        float yMin = FMath::Clamp((-maxRange) + biasVector.Y, -maxRange, maxRange);
        float yMax = FMath::Clamp(maxRange + biasVector.Y, -maxRange, maxRange);
        float xRange = FMath::RandRange(xMin, xMax);
        float yRange = FMath::RandRange(yMin, yMax);
        return FVector(xRange, yRange, 0);
    };
    float theta = FMath::DegreesToRadians(index * degree);
    int r = maxRange / 2;//中心点から最大範囲の中間の長さ
    FVector biasVector = FVector(FMath::Cos(theta) * r,FMath::Sin(theta) * r,0);
    FVector retVector = getRandomVector(biasVector);
    const int safeTryCount = 10;
    int count = 0;
    bool isLoopEnd = false;
    //とり続けると設定値次第では無限LOOPしかねないのでセーフラインを設けておく
    while (!isLoopEnd && count < safeTryCount)
    {
        isLoopEnd = true;
        for (const auto& vec : vectorArray)
        {
            //再取得範囲内だったら再取得
            float xMax = vec.X + repossessionRange;
            float xMin = vec.X - repossessionRange;
            if (xMin <= retVector.X && retVector.X <= xMax)
            {
                retVector = getRandomVector(biasVector);
                isLoopEnd = false;
                break;
            }
            float yMax = vec.Y + repossessionRange;
            float yMin = vec.Y - repossessionRange;
            if (yMin <= retVector.Y && retVector.Y <= yMax)
            {
                retVector = getRandomVector(biasVector);
                isLoopEnd = false;
                break;
            }
        }
        count++;
    }
    return retVector;
}

void UJGSUtility::GetNonOverlapVectorArray(float maxRange, int degree, float repossessionRange,int requestCount ,TArray<FVector>& OutVecArray)
{
    OutVecArray.Empty();
    for (int i = 0; i < requestCount; i++)
    {
        FVector vector = GetNonOverlapVector(maxRange,i,degree,repossessionRange,OutVecArray);
        OutVecArray.Add(vector);
    }
}

解説

今回実装したコードは単純で、ランダムで座標を取得する際にバイアスを足してあげて既存の確定済みの座標にある程度被っていたら再取得しているだけです
問題はこのバイアスをどう求めるかですが、これも単純に以下の画像のように求められると思います

r : 座標取得の最大範囲
θ : 取得回数 × 任意の角度
α : 最大範囲座標
α’: αから半分の距離にある座標

image.png

実行した際の設定値とBlueprint

Max Range Degree Repossesion Range Request Count
250 36度 35 10回

image.png

結果

なにも考えずにランダムにした場合

image.png image.png

「いい感じ」とは程遠いですね

今回のアルゴリズムを試した結果

image.png

image.png わりといい感じにバラすことができた気がします

おわりに

まぁ実際このコードで繰り返し試してみればわかるんですが、これでもわりと椅子と被るときあります
それはまぁsafeCount以内に取得できないケースもあるので、ある程度は許容かなと…
こういうコード、色んなゲームでみんな車輪の再開発しまくってると思うんだよね…
定番化してほしい…

【ネタバレ注意】The Last of Us part2をクリアしたので語ってみる【ラスアス2考察】

はじめに

今回、はじめてゲームのプレイ後記事を書いてみました
なぜ書いたかというと、今作は世界のゲーム業界において大きな転換となる作品なのではないかと感じたからです
ちなみに今回の記事では以下のことにはたくさんの人が触れているのでこの記事では触れないのでご留意ください \

ちなみに僕は前作であるThe Last of Usは今作をプレイするために発売前にクリアした新規?勢です
なので、ある程度フラットな立場で今作を見れているのかな?という気もします

またこの記事に特に他意はなく、The Last of us part2を作った開発チームに最大の敬意を表します
ということは最初に言っておきます

クリア直後の感想

今作をクリアした時に最初に思ったのは

「このゲームは一体なにをプレイヤーに訴えたかったんだろう?」

ということでした
たいていのゲームではゲームをクリアした時点で「プレイヤーにはこういう感情を抱いていてほしい/メッセージを受け取ってほしいんだな」というのが伝わるものが多い
しかし、今作においてはそれがなかった
というのも、ラストが基本的に一見虚無感しか得られず「復讐をするのではなく赦すことが大事である」と言いたい…?でもそれだとありきたりというか、あそこまで虚無感を演出する必要性が感じられなかったのでエンディングのクレジットを見ながらしばらく考え込んでしまいました

色々議論を呼んでいるアビー編ですが、アビー編に入るのはアビーへのヘイトが高まった状態で入るわけですからアビーを操作するのは苦痛でしたし、しばらくは「アビーとか興味ないからさっさとエリー編に戻れ」としか思ってませんでした
しかし、ラストのシーンではアビーにもそれなりに感情移入できてしまっていたのでなんとも言えないモヤモヤを抱えたままエンディングへと突入することになりました

そこから、色々考えたり再度プレイしたり調べたりして考えたことなどをまとめていきます

考察(ここからネタバレ有)

なぜNaughty Dogは今作を「復讐」というテーマにしたのか

f:id:johnnygamestudio:20200706034434p:plain

そもそもなぜ今作は復讐をテーマにしたのかと考えたら、前作の焼き直しになるのを避けたからだろうなと思いました
前作の評価が高かったのは

  • 娘を失ったジョエルがエリーと関わる中で心を取り戻していく物語
  • ジョエルの善悪を超越した魅力
  • 残酷な世界の中で絆を紡いでいく二人の関係性の美しさ(尊さ)

ざっくりこんな感じだと思いますが、つまりは前作は「ジョエルの物語」であり、ジョエルの魅力は前作で描き切っているわけです
その続演で無理やりまた「ジョエルの物語」をやろうとすると前作の焼き直しになってしまう上、おそらく前作を超えるのは難しいでしょう

そうなると必然的に物語の主役はジョエルからエリーに移るのは自然な流れです
しかし、前作がヒットした最大の要因である絆や残酷な世界の中での美しさというのはあくまでもジョエルだから成り立った話であって成長したとはいえエリーにジョエルのような役は合っていないと感じます
つまり今作で必要な最低限の構成としては

  • 前作の焼き直しにならないストーリー
  • エリーならではのストーリー

という構成になっていなくてはなりません
そういった条件があった中で新作を作ろうということでテーマが「復讐」になるのはまぁ仕方ないのかなという気はしました

アビー編とはなんだったのか

https://stat.ameba.jp/user_images/20200624/23/gamezoon/d3/61/j/o1920108014779264111.jpg

結論から言うと「アビーは今作におけるジョエルの役割」であることをプレイヤーに理解させるためのストーリーなのは間違いないでしょう
ジョエルを殺した宿敵としてアビーというキャラクターは登場するわけですが、アビー編で明らかになったアビーという人物は仲間想いであり、身体を張って見ず知らずの相手を守ろうとする人物だったわけです
さらには、ジョエルのように自らの意思とは関係なくレブという子供を守る立場となり、レブとの間に絆を築いていくことになります
また、エリーをジョエルを殺した時と劇場で戦った時とで2度見逃している点からみても今回の裏テーマである「赦し」を体現しているキャラクターといえます
アビー単体で考えれば、せっかく見逃してあげたエリーに好きな人や愛犬、友人が皆殺しにされてしまうという見方によってはかなり被害者側とも見える存在です

今作におけるエリーの立ち位置

https://livedoor.blogimg.jp/games084/imgs/a/d/ad176ac6.png

こちらも結論から言うと「今作におけるエリーは”打ち倒すべき復讐鬼”」として描かれていたのは間違いないと感じました
今作では敵同士が雑談をしていたり敵を殺すと名前を呼んで叫んだりして
「敵として存在するNPCもれっきとした人間であり、誰かの大切な人である」
ということを繰り返し見せられるゲームデザインになっています
いくつかのレビュー記事で
「敵キャラの殺害という手段を避ける方法(麻酔銃とか)がないにもかかわらず、プレイヤーに悲惨さを見せつけるのは単純に不快」
といった内容を見ましたが

エリーは復讐に取りつかれて周りが見えなくなっている(関係のない人間も殺してしまう)存在である

ということを印象づけたいのであり、確信的にそういう構図にしたと感じました

ではアビーはどうなのでしょうか?
アビー編でも敵キャラも人間であることは変わらず描写されています
しかし、僕はアビーとエリーでは行動は同じでもその根本の部分は全く異なると考えます
そもそもアビーが実行する暴力は前作のジョエルと同様に大切な守るべき存在(レブ)を守るための暴力であることに対し、エリーはあくまでも復讐という利己的な理由によって実行しています
アビーは前述の通り、自身の復讐の際には顔を見られたはずのトミーやエリーは復讐とは無関係であることから見逃しています
ここからもアビーとエリーの暴力は全く異なるものであるということが描写されています

https://value-kaden.com/wp-content/uploads/2020/06/8018.jpg

そう思ったのはそれだけではありません、エリーはアビーを見つけるためにアビーの仲間を見つけ出し次々に殺したり拷問したりするわけですが、ノラを拷問した辺りから様子が変わってきます
手が震えたり、躊躇する素振りを見せたりして「自分がやってきたことは正しいのか?」と思い始める描写が増え始めます
そしてその感情はオーウェン達を殺し、メルが妊娠していたことを知った時に爆発します

極めつけはエリー対アビーの戦いです
やっとエリーの操作に戻るのかと思ったらまさかのアビー操作でエリーを倒すという構図になります
そしてその戦闘のゲームデザインは明らかに前作でのエリーと人食いをしてでも生き残ろうとしているデビットとの戦闘と酷似しています
ここから汲み取れる開発側の意図とは

エリーこそ真の敵である

ということではないのでしょうか

なぜエリーはアビーを赦したのか

https://assets.st-note.com/production/uploads/images/28721921/picture_pc_0f9a7485397ae9d2f622daa1057c6b9e.jpg

ここもかなり賛否を呼んでいる点ですが、エリーはラストにアビーにとどめをさすことを止めてしまいます
人によっては「あれほど殺してやる!と息巻いて、絆を結んだジョエルを殺したアビーを赦すなら今までの行いはなんだったのか」となってしまうのは無理ないと思います
重要な点はアビーに止めをさそうとしているときにエリーが思い出すジョエルとの思い出です
エリーは、自分に了承なくファイアフライから救ったジョエルに対して「赦せないけど赦したいとは思っている」と言ったことを思い出します

そもそもこの決闘にしたってアビーは望んでいませんが、エリーがレブにナイフを突きつけ「戦え」と言ったから了承して戦うことになりました
そんなアビーとレブの関係性、レブのために戦うアビーを見てジョエルとアビーを重ねたのではないのでしょうか?そうすることで

アビーを赦すことで同時に生前和解することが出来なかったジョエルを赦すことができた

ということではないのでしょうか

そう考えるとエンディングのシーンが少し意味が変わってくるように思います
最初は復讐も果たせず、指を失ってジョエルとの思い出のギターも弾くこともできなくなり、家族にも見放されたという虚無感しかないエンディングですが
ジョエルという存在を”思い出”にして復讐から解放されたということを演出しているようにみえます
つまり

The Last of us part2は”エリーが父親(ジョエル)から離れて新たな人生を歩む物語”である

というのが真のThe Last of us part2の訴えたかったことなのではないでしょうか?

これで前述した今作の物語の条件である

  • 前作の焼き直しにならないストーリー
    → 前作は「絆の美しさ」「父親(ジョエル)とエリーの絆」などがテーマだったが、今作は「娘(エリー)が大人として独り立ちする」というテーマ
  • エリーならではのストーリー
    → 娘が大人になるというテーマはエリーにしかできない

ということでクリアしていることになります

https://i2.wp.com/otakaranet.com/wp-content/uploads/2020/06/d3088bf82f6a8e31f55898db919ebc06.jpg?fit=1920%2C1080&ssl=1

総評

The Last of us part2はビデオゲームを一介の娯楽作品から文化作品へ飛躍させようとした作品である

なぜそう思うかというと、通常多くのビデオゲームは言うまでもなく娯楽作品であり、プレイヤーには「買ってよかったな」「プレイしてよかったな」と思わせるものです
しかし、文芸界、例えば小説や映画では必ずしもそういった作品ばかりではありません
こちらになにかを訴えかけたり、望んでいない描写をすることで時には社会全体に問いかけるものが多く存在します

今作「The Last of us part2」はまさにそういったことを狙った作品なのではないでしょうか?
控えめに言って今作は繰り返しプレイしたいと積極的に思えるほど綺麗な余韻を残してくれるタイプの作品ではありません
しかし開発側の込めようとしたメッセージはいたるところに見え隠れしています

でも個人的評価は「7/10」

https://dic.nicovideo.jp/oekaki/507989.png

色々考察を書きましたが、肝心なことは

ユーザーはそんなこと望んじゃいない

やるにしたってもっとストーリー構成を練ったほうが良かっただろ

という点につきるといえます
多くのユーザが指摘している通り、アビー編は冷静な目でみれば価値のあるものだったといえますが普通にプレイしていればなぜ敵であるアビーを操作させられるんだという不快感のほうが大きいです
そのため、多くのユーザがアビーに感情移入できずによくわからないままエンディングへ突入するという結果になっているようにしか見えません
この内容でやるなら、アビー編はエリー編より前に持ってきてアビーへの先入観をなくしてあげたりしたほうが良かったでしょう
開発側が伝えたかった事が伝わりやすい最適なストーリー構成だったかというと疑問です

というかそもそもユーザが見たかったのは「復讐」だの「赦し」みたいな展開は望んじゃいません
ユーザが期待していたのはジョエルとエリーの物語であり、その中でエリーの成長を描けば良かったと思わずにはいられません
この物語を描くためにジョエルという前作の主人公をあっさり殺しすぎでは?と思ってしまいます

それらの点を考えるとゲームとして、作品として非常によくできていたしとても面白かったですが総合すると海外メディアのように手放しで満点を出せるとは思えず、7点というラインがちょうどいいかなというのが個人的な肌感です

ただ、一部で言われているような 「このストーリーを考えた人は前作が嫌いなのか?」 というのは絶対にないなぁ

【UE4】ランライムに生成されるActorComponent内のMesh情報をエディタ上で表示する

はじめに

開発していると「このMeshは動的に入れ替わるからエディタ上ではまだ生成されていないし、デフォルトで余計なMeshは表示させたくない」といったクラス設計にすることが多々あると思います
しかしまだ動的に生成されるオブジェクトはそのままだとエディタ上で確認できず「このMeshはどれくらい大きいのか?比較できない」といった状況になります
その際の対処法をまとめておきます

結論:OnRegisterを使おう

実装

今回のサンプルの構造としてはUTestComponentを持つATestActorを継承したBPクラスを作成し、ATestCharactorをB;ueprintエディタ上で設定します

ATestActorとATestCharactorは特筆することはないのでてきとーに用意してください

ざっくり以下のような図になります
image.png

確認

エディタで確認すると表示しているのはATestActorを継承したBlueprintですが、設定してATestCharacotrが表示されていることがわかるとおもいます(椅子のMeshがATestCharacotr)

image.png
image.png

Sample Code

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Components/SceneComponent.h"
#include "TestComponent.generated.h"


UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class SANDBOX_API UTestComponent : public USceneComponent
{
    GENERATED_BODY()

public: 
    // Sets default values for this component's properties
    UTestComponent();

protected:
    // Called when the game starts
    virtual void BeginPlay() override;
    virtual void OnRegister() override;
    virtual void OnUnregister() override;

public: 
    // Called every frame
    virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

    void CreateCharactor();
    void DestroyCharactor();

private:
    UPROPERTY(EditAnywhere)
    TSubclassOf<class ATestCharactor> charactorClass;
    UPROPERTY()
    class ATestCharactor* meshActor = nullptr;
    UPROPERTY()
    bool bCreatedCharactor = false;
        
};
// Fill out your copyright notice in the Description page of Project Settings.


#include "TestComponent.h"
#include "Engine/World.h"
#include "TestCharactor.h"

// Sets default values for this component's properties
UTestComponent::UTestComponent()
{
    // Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these features
    // off to improve performance if you don't need them.
    PrimaryComponentTick.bCanEverTick = true;

    // ...
}
void UTestComponent::OnRegister()
{
    Super::OnRegister();
    CreateCharactor();
}
void UTestComponent::OnUnregister()
{
    DestroyCharactor();
    Super::OnUnregister();
}
void UTestComponent::CreateCharactor()
{
    if (charactorClass != nullptr)
    {
        DestroyCharactor();

        FActorSpawnParameters spawnParameters;
        spawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
        meshActor = GetWorld()->SpawnActor<ATestCharactor>(charactorClass,FTransform(), spawnParameters);
        //自身の子オブジェクトとしてぶら下げる
        meshActor->AttachToComponent(this,FAttachmentTransformRules::KeepRelativeTransform);
    }
}
void UTestComponent::DestroyCharactor()
{
    if (meshActor != nullptr)
    {
        meshActor->Destroy();
        meshActor = nullptr;
    }
}
...

注意点

OnUnregisterを忘れないように

OnRegisterはPlay時に呼ばれたり、PlayInから停止して非実行状態に戻ってきたときにも再度呼ばれたりするので生成するクラスが増えてしまったりするのでOnUnregisterで生成しておいたMeshはDestroyしておくのを忘れないようにしておきましょう

【UE4】LevelSequenceでActorComponentを継承したComponentのプロパティを扱いたい

はじめに

C++で作成したActorに自分で追加してあるActorComponentのメンバ変数をLevelSequenceで扱いたい場合の手順がまとまっている情報がなかったので備忘録を兼ねてまとめておきます

環境

UE4.24.2
VisualStudioCommunity2017
Windows10

HowTo

ActorのコンストラクタでActorComponentを追加する

たとえ、メンバ変数にActorComponentを持っていたとしてもコンストラクタでCreateDefaultSubobjectで追加しておかないとLevelSequenceで認識してくれません
検証用に以下のサンプルでは二つのActorComponentを

という二つの方法でやってみます

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "TestActor.generated.h"

UCLASS()
class TESTPRJ_API ATestActor : public AActor
{
    GENERATED_BODY()
    
public: 
    // Sets default values for this actor's properties
    ATestActor();

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

private:
    class UTestComponent* testComp;//コンストラクタで生成

    class UTestComponent* testComp2;//BeginPlayで生成
};

// Fill out your copyright notice in the Description page of Project Settings.


#include "TestActor.h"
#include "TestComponent.h"

// Sets default values
ATestActor::ATestActor()
{
    // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = true;
    testComp = CreateDefaultSubobject<UTestComponent>(TEXT("testComp"));
}

// Called when the game starts or when spawned
void ATestActor::BeginPlay()
{
    Super::BeginPlay();
    testComp2 = NewObject<UTestComponent>(this);
    testComp2->RegisterComponent();
}

LevelSequenceで扱いたいメンバ変数にUPROPERTY(Interp)を付与する

LevelSequenceでメンバ変数を直接変更させる場合、UPROPERTY(Interp)をつけてあげる必要があります
なので今回はてきとーにBoolの変数をActorComponentに追加しました

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "TestComponent.generated.h"


UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class TESTPRJ_API UTestComponent : public UActorComponent
{
    GENERATED_BODY()

public: 
    // Sets default values for this component's properties
    UTestComponent();

protected:
    // Called when the game starts
    virtual void BeginPlay() override;

public: 
    // Called every frame
    virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

private:
    UPROPERTY(Interp)//追加
    bool bFlag;
        
};

確認

てきとーなLevelSequenceを作成し、TestActorのトラックを追加してみるとコンストラクタで追加した「testComp」だけが表示されて「testComp2」は表示されないことがわかります

image.png

また、TestComponentのメンバ変数がLevelSequenceのトラックとして追加できるようになっていることが確認できます

image.png

なぜコンストラクタで追加しないとLevelSequenceで扱えないのか

コンストラクタ内にブレイクポイントを仕込むとわかるのですが、UObjectを継承しているクラスのコンストラクタは以下の二つのタイミングで実行されます

  • ゲームが実行されたとき
  • エディタを起動するとき

つまり、エディタを起動する際にUObjectのリフレクション情報を生成しておいて、その情報を以ってLevelSequenceで扱えるようにしているわけです
C++そのものにリフレクションはないので考えてみれば当たり前ですね

【UE4】BPに公開している関数の引数に使う構造体にデフォルト引数を使いたい【C++】

はじめに

開発をしていてBPに公開されている関数に構造体を追加する必要が出たときにこの現象にぶつかりました
色々調べましたがエラー内容で検索してもドキュメントにも原因が明記されているところは見当たらなかったので解決方法をまとめておきます

デフォルト引数が使えない

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "TestActor.generated.h"

USTRUCT(BlueprintType)
struct FActorInfo
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "hoge")
    int32 num;

};


UCLASS()
class SANDBOX_API ATestActor : public AActor
{
    GENERATED_BODY()
    
public: 
    // Sets default values for this actor's properties
    ATestActor();

    UFUNCTION(BlueprintCallable, Category = "TestActor")
    void TestFunc(const FActorInfo& info = FActorInfo());//コンパイル時にエラーが出る

};

上記のように書くと以下のようなコンパイルエラーが発生します

重大度レベル   コード   説明  プロジェクト  ファイル    行 抑制状態
エラー       C++ Default parameter not parsed: info "FActorInfo()"   Sandbox C:\UE4_Prj\Sandbox\Source\Sandbox\TestActor.h   30  

原因の推察

C++のルール的には記述の問題ではない(UFUNCTIONコメントアウトするとコンパイルが通る)ことから考えてもUE4ならではの状況なのは間違いないです
ということは,BPとして公開することに原因がありそうです
BPのデフォルト引数はmetaデータとして埋め込むことが可能であることを考えると構造体のデフォルト値をコンパイル時に取得できていない?

解決方法

metaデータにデフォルト引数を持たせることで解決できます
関数の定義を以下のように書き換えましょう
デフォルト引数が設定されているのでBPでピンを繋げていなくてもエラーは出ません

UFUNCTION(BlueprintCallable, Category = "TestActor", meta = (AutoCreateRefTerm = "info"))
void TestFunc(const FActorInfo& info);

image.png

試しに以下のようにmetaデータを削除してみるとエラーがでることが確認できる

UFUNCTION(BlueprintCallable, Category = "TestActor")
void TestFunc(const FActorInfo& info);

image.png

所感

根本の理由はちゃんと追っていないので、詳細な理由がわかる人がいたら教えてください