johnnyGameStudio’s blog

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

【UE4】CommandletでTextureのSizeなどのMeta情報を取得出来ない場合の解決策とその原因

開発環境

UE4.26.2 Windows10 Visual Studio 2019

はじめに

この記事の内容はとてもシンプルだが、いざCommandletでAssetの詳細データを取得しようとすると上手くいかず、解決策を探すのに手間取ったので備忘録として記録する
おそらく同じような同士がいると思うので誰かの助けになれば幸い

発生した現象

参考先(※1)を元に以下のようなCommandletを追加し

int32 UMyCommandlet::Main(const FString& CmdLineParams)
{
    TArray<FAssetData>  _AssetList;
    GetAssetList(_AssetList);

    for (auto Asset : _AssetList)
    {
        if (UTexture2D* TmpTexture = Cast<UTexture2D>(Asset.GetAsset())) {
            
            UE_LOG(LogSandbox, Log, TEXT("Texture Size x:%d y:%d"), TmpTexture->GetSizeX(), TmpTexture->GetSizeY());
        }
    }

    return(1);
}

そして※1を参考に以下のコマンドで実行してみる

{EnginePath}\Engine\Binaries\Win64\UE4Editor-Cmd.exe {Pj Path}Sandbox.uproject -run=My -param=2.0 -stdout -UTF8Output
pause

実行した際のLogである"\Saved\Logs{PJ Name}.log"を開いてみると以下のように、テクスチャサイズが取れないことがわかる

[2022.03.21-21.51.02:162][  0]LogSandbox: Texture Size x:0 y:0
[2022.03.21-21.51.02:163][  0]LogSandbox: Texture Size x:0 y:0
[2022.03.21-21.51.02:165][  0]LogSandbox: Texture Size x:0 y:0
[2022.03.21-21.51.02:167][  0]LogSandbox: Texture Size x:0 y:0
[2022.03.21-21.51.02:168][  0]LogSandbox: Texture Size x:0 y:0
[2022.03.21-21.51.02:170][  0]LogSandbox: Texture Size x:0 y:0
...

発生原因

GetSizeXの中身を確認するとEngineのソースでは以下のようになっており、このPlatformDataがNullであることがわかる

 FORCEINLINE int32 GetSizeX() const
    {
        if (PlatformData)
        {
            return PlatformData->SizeX;
        }
        return 0;
    }

PlatformDataの生成場所

ではなぜNullか?通常のエディタ実行時ではこのPlatformDataが生成されるのはどこなのかをブレイクポイントで追ってみると以下の処理順でCallされるCreateTransient関数内で生成されることが分かった

UThumbnailManager::SetupCheckerboardTexture > FImageUtils::CreateCheckerboardTexture > UTexture2D::CreateTransient

Commandletではサムネイルを必要としないのでこのままでは生成する大本であるUThumbnailManagerがCallされず、各テクスチャのPlatformDataも生成されないことがわかった

解決方法

Commandlet実行時に引数「-allowcommandletrendering」を追加する 以上

補足

解決策はとてもシンプル
ヒストリア様の参考リンク(※2)ではデフォルトで記述されているのでそのままコピペして使っていると気づかないが、このallowcommandletrenderingはあらゆるレンダリング系の情報を取得する際に必須になる引数なので常につけておいて良いことがわかった

動作確認

前述したコマンドの最後に引数として「-allowcommandletrendering」を追加して実行してみると、以下のLogのようにテクスチャサイズが取得できることが分かった

{EnginePath}\Engine\Binaries\Win64\UE4Editor-Cmd.exe {Pj Path}Sandbox.uproject -run=My -param=2.0 -stdout -UTF8Output -allowcommandletrendering
pause
[2022.03.21-21.54.13:272][  0]LogSandbox: Texture Size x:2048 y:2048
[2022.03.21-21.54.14:390][  0]LogTexture: Display: テクスチャをビルドしています:T_Brick_Clay_Beveled_M (AutoDXT、2048X2048)
[2022.03.21-21.54.14:390][  0]LogSandbox: Texture Size x:2048 y:2048
[2022.03.21-21.54.14:616][  0]LogTexture: Display: テクスチャをビルドしています:T_Brick_Clay_Beveled_N (BC5、2048X2048)
[2022.03.21-21.54.14:616][  0]LogSandbox: Texture Size x:2048 y:2048
[2022.03.21-21.54.15:732][  0]LogTexture: Display: テクスチャをビルドしています:T_Brick_Clay_New_D (AutoDXT、2048X2048)

参考リンク

(※1) https://qiita.com/unknown_ds/items/e49aa05e142a04a1ee59
(※2) https://historia.co.jp/archives/7343/

【UE4】完全に独立したカスタムクラッシュレポーターを実装する

はじめに

クラッシュレポーターに関する日本語情報はとても少ない
というか英語でも少ないし、UE4.24前後からは仕様が微妙に変化しているらしくggってもわからない部分が多い
なので「かっこいいクラッシュレポーターを作りたい!」と思ったときにとても苦労するので
自分がわかっている範囲で共有

完全に独立したカスタムクラッシュレポーターとは?

標準のクラッシュレポーターはこちら

image.png

見てわかる通り、めちゃくちゃ「UE4!!!!!」という印象だし出てほしくない情報とかも出ている
まぁ標準のクラッシュレポーターじたいもEngineのソースを弄れば変更が可能なのでレイアウトを変更したり見た目を変えることも可能ではあるけれど、色々要件がある場合自前でクラッシュレポーターを1から作成してそちらを使用したいというケースが出てくる
しかしそうなるととたんに情報が少なくなるのでそこらへんの情報をまとめる

想定しているゴール

  • 完全に独立したカスタムクラッシュレポーターを呼び出すことが出来る
  • エラー情報を取得することができる

クラッシュレポーターとは

UE4のエディタやパッケージ化したバイナリがクラッシュした際に出てくるエラー情報などを表示したうえで事前に設定したサーバへエラー内容を送信してくれるクライアント
詳細は公式ドキュメントを参照

開発環境

UE4.26系
Windows10
Visual Studio Community 2017

前準備

Githubから持ってきたEngineでビルドする

EpicgameLancherでDLした通常のUE4ではカスタムのクラッシュレポーターは使用できないので必ず行ってください

  1. GithubからEngineのソースを手に入れる方法についての詳細は公式ドキュメントへ
  2. Engineのソースが用意出来たらuprojectファイルを右クリックでSwitch Unreal Engine version image.png
  3. 用意したEngineを指定してslnファイルを再生成
  4. VisualStudioでエディタを再ビルドする

プロジェクト設定

クラッシュレポーターをパッケージに含めるためとエラー情報を取得できるようにするために以下の二つをプロジェクト設定からONにする

image.png

image.png

独自のクラッシュレポーターを実装する

クラッシュレポーターの実装形式は何でもよいのだが、今回は標準的なWPFアプリケーションで実装

※ここのコードそのものは重要ではない
image.png

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.IO;

namespace CrashRepoter
{
    /// <summary>
    /// App.xaml の相互作用ロジック
    /// </summary>
    public partial class App : Application
    {
        private string windowMessage = "";
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            List<string> argList = new List<string>(e.Args);
            string path = argList.Find(n => n.Contains("UE4CC-") && n.Contains("Crashes"));
            string logText = null, xmlText = null;
            byte[] miniDump = null;
            if (string.IsNullOrEmpty(path))
                return;

            GetErrorInfos(path, out logText, out xmlText, out miniDump);
            windowMessage = xmlText;
        }

        protected override void OnActivated(EventArgs e)
        {
            base.OnActivated(e);

            MainWindow mainWindow = MainWindow as MainWindow;
            mainWindow.SetDescriptionText(windowMessage);
        }

        private void GetErrorInfos(string crashDirPath, out string OutLog, out string OutXml, out byte[] OutMiniDump)
        {
            OutLog = "";
            OutXml = "";
            OutMiniDump = null;
            DirectoryInfo crashDir = new DirectoryInfo(crashDirPath);
            if (crashDir.Exists)
            {
                crashDir.Refresh();

                FileInfo[] fileBuffer = null;

                fileBuffer = crashDir.GetFiles("*.log");
                if (fileBuffer.Length > 0)
                {
                    FileInfo logFile = fileBuffer.First();
                    OutLog = File.ReadAllText(logFile.FullName);
                }

                fileBuffer = crashDir.GetFiles("*.runtime-xml");
                if (fileBuffer.Length > 0)
                {
                    FileInfo logFile = fileBuffer.First();
                    OutXml = File.ReadAllText(logFile.FullName);
                }

                fileBuffer = crashDir.GetFiles("*.dmp");
                if (fileBuffer.Length > 0)
                {
                    FileInfo dumpFile = fileBuffer.First();
                    OutMiniDump = File.ReadAllBytes(dumpFile.FullName);
                }
            }
        }
    }
}

重要な部分のコード解説

エラー情報の吐き出し先のPathを取得する

引数に起動時のパス情報などが渡されるので文字列検索で取得可能

List<string> argList = new List<string>(e.Args);
string path = argList.Find(n => n.Contains("UE4CC-") && n.Contains("Crashes"));

EventArgs引数が取得できない場合はEnvironmentクラスで取得可能なのでこちらでもOK

string[] args = Environment.GetCommandLineArgs();

備考

独自のクラッシュレポーターを作成するときにあまりネットに情報ないのがUE4側から吐き出されたエラー情報のファイルがどこにあるのかという点
ネットにある資料(参考リンク)では以下のPathにあるとあるのだが現在は仕様が変わったせいなのかここには存在しない

~\AppData\Local\[Game Name]\Saved\Crashes\UE4CC-Windows-#################################

現在は以下のパスに出力される

~\[Packaged folder]\[project name]\Saved\Crashes\UE4CC-Windows-#################################

なお、引数として渡される情報は以下の通りである

第1引数 第2引数 第3引数 第4引数
エラーファイルの出力Path gameのAppName 今回のCrashGUID DebugSymbolsのPath

パッケージされた成果物のクラッシュレポーターをカスタムクラッシュレポーターにする

実行ファイルを置き換えるだけ

UE4のクラッシュレポーターはゲームとは完全に独立していて別プロセスとなっているので置き換えるだけで動作するようにできている
クラッシュレポーターは以下のPathに配置されているのでそのディレクトリのファイルを全て削除してカスタムクラッシュレポーターの成果物へ置き換えよう
※この時カスタムクラッシュレポーターのファイル名は全て「CrashReportClient」にしておこう

~\[Packaged folder]\Engine\Binaries\Win64

置き換えた後のクラッシュレポーターディレクト

image.png

これで全ての作業が完了
ゲームを起動させてコンソールコマンドなどで意図的にクラッシュさせて作成したクラッシュレポーターが起動することを確認しよう

おまけ

いやー完全独自のカスタムクラッシュレポーターとか要らないから標準のクラッシュレポーターをパパっといじりたいですわ~
という人は以下のbugsplatさま(参考リンク)の記事を見ればどうにかなるので参考にしてほしい

参考リンク

teal-gameさま
bugsplatさま
Sentry.IOさま

現在のゲーム業界を取り巻く地獄~なぜゲームは完成しないのか~

現在のゲーム業界を取り巻く地獄~なぜゲームは完成しないのか~

はじめに

ここ最近知り合いや様々なゲーム会社から「ゲームが完成しない」という話をよく聞く これはなぜゲームが完成しないのか?という話をブームにのってダラダラと書いていくポエム記事です
ちょっと長いので「なぜゲームが完成しないのか?」が気になる人は前半は読み飛ばしても構いません

お前は誰

7,8年くらいの間主にスマホゲームを作っているゲームプレイプログラマーやってます。
なぜこのポエムを書こうと思ったかというと、私は人よりプロジェクトが死ぬ様(リリース出来ずプロジェクトクローズorサービス終了)を人より多く観測しているからです。
具体的に言うと今まで7本のプロジェクトが死ぬ様子をこの目で見てきており、知り合いの業界歴20年とかのゲームプログラマーと話しても精々1,2本しか経験したことある人がいないので人より多少熱を込めて現場目線込みで語れるかなと思ってます。

現在のゲーム業界

現在の世界のゲーム業界市場規模は前年比19.6%増で約18兆円(※1:グローバルゲームマーケットレポート2020より)といわれており、2021年には20兆円を超えると予想されています
2020年の世界ゲーム市場規模

また、ゲーム業界の市場規模は近年急上昇しており、10年前と比較すると倍以上の市場規模となっています (※2:ファミ通ゲーム白書2020より)
2010-2019の推移

さらに、ゲーム業界は2025年には約28兆円以上(※3:BCC Researchより) になると予想されており今後益々巨大な市場になるであることがほぼ確実となっている成長産業、それがゲーム業界です

GAFAでもゲーム開発は難しい?

そんな成長産業である市場に巨大企業の筆頭であるGAFAも例外なく参入してきています
しかし先日、立て続けて2つの残念なニュースが流れてきました

開発中止が続くAmazonのゲーム開発部門「Amazon Game Studios」はなぜ失敗したのか?
Google、Stadia向けの自社ゲームスタジオ閉鎖を発表。これ以上は開発コストをかけられず、今後はサービス提供に専念

Amazonのほうは去年か一昨年に大規模のレイオフが行われたときに話題になりましたが、GoogleのStadia向けAAAタイトルはクラウドゲームの目玉といわれていただけに業界内での反響も大きいように見えます。

AmazonGameStudioがリリースできたゲームは2018年発売の「The Grand Tour Game」のみ、それ以外は残念ながら開発中止に追い込まれているようです 驚くことにAmazonGameStudio単体で年間5億ドル(約520億円)もの予算を費やしていることがニュースで明らかになり、数年間で費やした金額は一体どれほどの金額になるのか想像もできません
その金額を踏まえてGoogleのニュースを見ると、記事内で「ロから完成度の高いゲームを作るには長い歳月と多くの投資が必要で、急激にコストが増大していった」と言及されているのも頷けます

AmazonGoogleは優秀な人材を集めることができなかったのでしょうか?お金が足りなかったのでしょうか?
いいえ、そうとは思えません

優秀な人を集めてお金をつぎ込んでもゲームが完成するわけじゃない

AmazonGoogleは言わずと知れた世界有数の巨大IT企業です。お金も人材も多くのゲーム会社より圧倒的に豊富で優れているでしょう。
しかし、それだけではゲームは完成させることはできないことはこの2つの企業が証明してくれたと言っていいと思います。
こういった話は別にAmazonGoogleでしか起きていないことではありません、日本の様々な企業でも起きていることです。

日本のゲーム会社の事例

例えば「グランブルーファンタジー」「アイドルマスターシンデレラガールズ」「プリンセスコネクトRe:Dive」など数多くの人気スマホゲームを持つCygamesは代表的です

Cygamesの「ウマ娘」は事前登録から3年を経てついに今月リリースされる見込とされており、記憶をたどるとたしか初出は2016か2017年前後に初お披露目があった気がします。そして2018年に事前登録まで行っているにもかかわらず、約3年も経過…その過程で担当PがCygamesを退職するなど波乱万丈な開発だったことは想像に難くありません
開発にかけた期間は予想ですが、企画段階も含めて5,6年でしょうか?期間を考えると開発費は数十億はいっているはずです。

他にも同社はプラチナゲームズと共同開発で「LOST ORDER」を発表していますが、クローズドβテストを行った後の続報が全く聞かないのでおそらく開発中止になっているとみていいのではないでしょうか(確か女性向けゲームも近いタイミングで発表していたはずだけど気づいたら消えていた)
他にも「GRANBLUE FANTASY Relink」は初出であるプレスリリースの2016年から6年経て2022年にリリース予定とされています。

Cygamesに限った話ではありません、スクウェアエニックスの「インフィニティ ストラッシュ ドラゴンクエスト ダイの大冒険」はプレスリリースしてから約半年後の2020年末に発売日はだいぶ先であることを発表しています
ダイの大冒険は今アニメ放送をしていることを考えても、放送期間中に発売したかったことは間違いないでしょう。にもかかわらず「だいぶ先」と明言していることからもアニメ放送に間に合わず、しばらくリリースされることはないのは確実だと思われます。

Cygamesもスクウェアエニックス(開発はゲームスタジオ)も非常に人材が豊富でノウハウもあり、開発資金も潤沢な大きなゲーム会社です
これらのタイトルを開発するのにお金を投入して新たに人材を集めて開発に注力したことでしょう。しかし全てが上手くいくとは限らない結果なのは明らかです。

ここまでを踏まえて考えても、ゲームを完成させることが今まで以上に難しくなっているというのは目を逸らしてはならない現実といっていいでしょう、ノウハウがないAmazonGoogleが苦労するのは必然といえます

気になった方は上場ゲーム企業(特にスマホ)のここ数年のタイトルリリース数を比較してみてください。明らかにリリース頻度が落ちていることが観測できます。

ゲームが完成しない理由(主観)

ではお金もあってノウハウもあるゲーム会社がリリースできない理由とはなんなのでしょうか?
正直、デスプロジェクトソムリエの僕でも理由を完全に理解することは出来ていません。しかし、その中でいくつか共通のパターンがあるように思えるのでそれらを紹介します。

スタープレイヤーだけいても仕方がない

お金があって成長中の会社では特に陥りがちなのですが「あの有名タイトルを手がけていた〇〇さんが入社!」といった話を聞いたことがないでしょうか?
たしかに優秀な人材を集めることは重要なことですし必要です。しかし、人間は万能ではないので実はそのスタープレイヤーである有名クリエイターが活躍できていたのは無名の副官がいたり誰かが阿吽の呼吸で本人の自覚さえなく手助けされていて初めて発揮できる優秀さであることが多々あります。

また、人を一気にたくさんかき集めると会社やチームの文化や手法が全く統一されておらず、ゲーム開発とは全く関係のない事柄に時間を取られることになります(Version管理ツールは何を使う?チャットアプリは何を使う?プロジェクト管理ツールは?それの予算はどうする?稟議は誰が出す?などなどなど)

考えうる解決法

このケースで躓く場合、往々にしてチームビルディングを甘く見ているのが原因と言っていいと思います。
スタープレイヤーを支える副官も一緒に引き抜くことが出来なかったのなら、誰かがそれの変わりをすればいいのですがそれは一朝一夕で上手くいくものではありません。当然それを見込んだ開発期間を取れるように動きましょう。
それが出来ないなら、プロジェクトのリーダーが強いリーダーシップを発揮して(曖昧)チーム文化を根付くように声を発し続け「このプロジェクトはなにを大事にしているのか」「このプロジェクトはなにを目指しているのか」などを強く訴えて一つの文化を持ったチームを作っていくことを意識していくしかありません。
これを甘く見ていると経営層からは「人とお金は渡したのに成果が出ない、なぜだ」といわれたり、チームメンバー間で「あの人は駄目だ」的な陰口が横行します。

〇〇億を扱う経験のあるPやDの不在

前述したとおり、ゲーム業界の市場規模はこの10年で倍以上に膨れ上がっており、それは今後も加速していきます。
そうなると同然のことながら扱う金額も大きくなるということです。しかし、10年前は精々が開発費1億~2,3億の世界だったのに、今ではコンシューマもスマホも10億~30億のような規模になってきています。

今のゲーム業界は高齢化していますので、主にお金を握っているPやDの多くは40~50代の方が多いです。そうなると、キャリアの大半の期間である3,40代まではそのくらいの予算規模で仕事をしていた人が大多数です。10年前に数十億の予算規模を動かしていた経験のある人はAAAタイトル経験者しかいませんので当たり前ですね。
扱う予算が増えれば当然ですがお金の使い方やプロジェクトの動かし方もかわってきます。今の大半のPやDの目線からすると、気がついたら急に渡される予算が増えて今までのやり方が通用しなくなってきてしまっているのではないでしょうか?(本人じゃないとわかりませんが)

人数が多くなれば情報伝達のやり方も変わってきます、噂話も横行し始めます、予算配分も考えなくてはなりません、経営側も判断を間違えてしまうかもしれません

考えうる解決法

どうすればいいんだろう?本当にわからない
すぐどうにかするというよりは小さいプロジェクトでPやDの経験を積み、チャレンジできる母数を増やせばどうにかなるかもしれない(ほんまか?)

ゲーム開発が好きな人間が指揮権を握っているとは限らない

もっとも悲しいケースです。AmazonGameStudioの記事を読んでいる限り、Amazonではこのケースに近いことが起こったことが推測されます。
急成長した企業だったり、他業種から参入してきた組織で発生しがちです。ゲーム開発の最大の難点はゴールが明確ではないことだと思います。
AmazonGoogleがいままで開発してきたソフトウェアやサービスは何らかの課題がありそれらを解決するのがゴールですし大半の開発現場ではそれが正しいゴールだと思います。
しかし、ゲームは「面白いものを作る」のがゴールであり、その面白さは千差万別で正解は何一つありませんしトレンドもすごい勢いで変化していきます。
ゲーム開発はそれが前提なので結局はPやDに「このゲームはこれが面白い、誰が何と言おうと面白いんだ」という狂気にも近い信念がないと必ずぶれることになります。

会社設立からわずか4年弱という短期間で「デスストランディング」をリリースしたコジマプロダクションの小島監督デスストランディングの開発時で開発メンバーに”いいね”を理解してもらうことが難しかったと語っています
しかし、仕上がってみれば見事"いいね"の仕組みは上手くゲームデザインとうまくかみ合い、新しいゲーム体験を生み出しています。しかし、プロジェクトのお金を握っている人がゲーム開発に理解がない人間だとどれだけ説明したところで「他社はどうなの?流行のゲームは?」といわれるのは想像に難くありません。
Amazonでも似たような話が記事に書かれています

さらにフラジーニ氏が、さまざまなメディアで取り上げられている「今最も人気があり儲かっているゲーム」についての情報を集め、毎月の会議でその流行を追いかけるように指示するため、会議が脱線して現場が混乱することも日常茶飯事だった

考えうる解決法

解決方法はありません、指揮権を持っている人の認識が変わらないことにはどうしようもないのではやくそのチームを離れてください。

あとがき

いかがだったでしょうか。今回書いたことはあくまでも個人的見解であり、これらを回避すれば必ずゲームを完成させられるといったわけではありません。
しかし、間違いなくどのゲーム会社でも普遍的に起きている現象だと思うのでなにかの参考になれば幸いです。
ここまで見てくださった方ありがとうございました。

参考文献

※1: https://www.4gamer.net/games/999/G999905/20201118033/
※2: https://prtimes.jp/main/html/rd/p/000007188.000007006.html
※3: https://www.value-press.com/pressrelease/258892

【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点というラインがちょうどいいかなというのが個人的な肌感です

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