C++ ときどき ごはん、わりとてぃーぶれいく☆

Wonder Rabbit Projectのなかのひとのブログ。主にC++。

Real Unreal Engine C++ 2017-12 (part-5/5)

はてなブログの記事あたりの容量制限のため前の部分 §1.10. までは前の記事でどうぞ→Real Unreal Engine C++ 2017-12 (part-4/5) - C++ ときどき ごはん、わりとてぃーぶれいく☆

1.11. UE4/C++ における入門的な「スレッド」ライブラリーの std 互換性

std UE4
promise TPromise
future TFuture
thread FRunnableThread

PRIMISE-FUTURE-THERAD の std と UE4 の 比較は以前書いた記事がちょうどこのセクションに相当する。

本節ではこれを基にそれぞれを項として整理する。繋げるとどうなるかは上記の記事を参照されたい。

1.11.1. TPromise<T>std::promise<T>

promise の扱いについては std と主な使い方についてはメンバー関数名も snake か Camel か程度の違いなので std 慣れした C++er が UE4 版の実装を使おうとして混乱する事は無い。

// std
#include <future>
void std_promise()
{
  std::promise< int > p;
  auto f = p.get_future();
  p.set_value( 123 );
}
// UE4
#include "Runtime/Core/Public/Async/Future.h"
void ue4_promise()
{
  TPromise< int > p;
  auto f = p.GetFuture();
  p.SetValue( 123 );
}

1.11.2. TFuture<T>std::future<T>

// std
#include <future>
void std_future( std::future< int >& f )
{
  // せっかくなので参考記事には無い処理完了をブロッキングせずに待つパターンの
  // 基礎的な実装要素を加えて紹介する。
  if ( f.wait_for( std::chrono::seconds( 0 ) ) == std::future_status::ready )
    auto result = f.get();
}
// UE4
#include "Runtime/Core/Public/Async/Future.h"
void ue4_future( TFuture< int >& f )
{
  if ( f.IsReady() )
    auto result = f.Get();
}

wait_for, wait_until ほど高機能ではないが、一般的に IsReady だけでも事足りる。

1.11.3. FRunnableThreadstd::thread

// std
void std_thread( std::promise< int >&& p )
{
  auto t = thread( [ =, p = move(p) ] () mutable { p.set_value( 123 ); } );
  t.join(); // or detach()
}
// UE4
void ue4_thread( TPromise< int >&& p )
{
  struct r_type: public FRunnable
  { TPromise<int> p;
    r_type( TPromise<int>&& p_ ) : p( MoveTemp( p_ ) ) { }
    bool Init() override { return true; }
    uint32 Run() override { p.SetValue( 123 ); return 0; }
    void Stop() override { }
  } r( MoveTemp( p ) );
  constexpr auto bAutoDeleteSelf      = false;
  constexpr auto bAutoDeleteRunnable  = true;
  auto t = FRunnableThread::Create
  ( &r
  , TEXT( "promise-future tester")
  , bAutoDeleteSelf
  , bAutoDeleteRunnable
  , 0
  );
  t.WaitForCompletion();
}

スレッドを生成する事だけを考えると UE4 の生スレッドの生成は少々面倒くさい。しかし、 FRunnableThreadSuspend()Kill() をメンバー関数として持っているので少しだけ高機能。

以上のようにして "野生のスレッド" を放つ事は UE4 のライブラリー機能としてもサポートされているが、実用上はそのような用途では std 実装を使った方が楽で future の待ち方にも工夫でき面倒も無くて良いだろう。もちろん、 std::promiseFRunnableThread を組み合わせても問題は無い。

UE4 では "野生のスレッド" ではなく、フレームワークが管理するメインスレッドとワーカースレッド群へ FAutoDeleteAsyncTask を用いてフレームワークの制御下で非同期タスクを実行もできる。それについては以下の記事が参考になるだろう。FAsyncTaskFAutoDelegateAsyncTask についてはそちらを参照すると良いだろう。

1.12. UE4/C++ における「アトミック」ライブラリーの std 互換性

所属 要素型 Header
std atomic<T> <T> <atomic>
UE4 FThreadSafeBool bool Runtime/Core/Public/HAL/ThreadSafeBool.h
UE4 FThreadSafeCounter int32 Runtime/Core/Public/HAL/ThreadSafeCounter.h
UE4 FThreadSafeCounter64 int64 Runtime/Core/Public/HAL/ThreadSafeCounter64.h

1.12.1. 主なメンバー関数の対応

std UE4 効果
Reset 0 にする
store
operator=
Set 値を放り込む
load 値を取り出す
exchange 値を取り替える
operator++ Increment 1 増やす
operator-- Decrement 1 減らす
fetch_addo
perator+=
Add 任意の数を加算する
fetch_sub
operator-=
Subtract 任意の数を減算する
fetch_and
operator&=
fetch_or
`operator
=` |
fetch_xor
operator^=
  • std:

  • UE4:

    • FThreadSafeBoolFThreadSafeCounter を継承して定義され、内部的には int3201 のみを扱うよう制限して実現している。
    • FThreadSafeCounterFThreadSafeCounter64 は内部的には FPlatformAtomics::InterlockedAdd などに実装を依存している。
      • 実装詳細は HAL/PlatformAtomics.h で定義され、もし気になるならば、 Runtime/Core/Public/Windows/WindowsPlatformAtomics.h などを参照すると善い。
        • 例えば Windows の場合は最終的には <intrin.h> から Win32 API::_InterlockedExchangeAdd 等が使われる。(MSDN)
    • int32, int64 に対応しているので、一応実質的にはこれらでも uint16void* も扱えないことは無い…

UE4 のアトミックを用いても UPROPERTY()UFUNCTION() では使用できず特にメリットも無いので、ツールチェインの std 実装が信用できる場合は std のアトミックを使えば良い。

参考:

  1. atomic - cpprefjp C++日本語リファレンス
  2. FThreadSafeBool | Unreal Engine
  3. FThreadSafeCounter | Unreal Engine
  4. FThreadSafeCounter64 | Unreal Engine

1.13. UE4/C++ における「並列処理」ライブラリーと std / OpenMP / Intel-TBB / Microsoft-PPL

所属 関数・プラグマ Header
std for_each <algorithm> <execution>
OpenMP #pragma omp parallel for
Microsoft PPL parallel_for <ppl.h>
IntelTBB parallel_for <tbb/parallel_for.h>
UE4 ParallelFor Runtime/Core/Public/Async/ParallelFor.h

モダンな C++er は既に C++17 で <algorithm><execution> による並列処理機能が導入される事は熟知し、開発環境がこれに対応可能となる日を心待ちにしている。

とは言え、現状、現実には多くの C++er は古風でやや癖はあるものの簡単に使用できる OpenMP や、 Microsoft 環境向けだけであれば PPL 、 PC 向けのクロスプラットフォームであれば intel TBB 、 GCC だけであれば Parallel Mode など開発環境に合わせて並列化や並行処理のための機能を使用している。

UE4 でもそれらを使用する事は可能だが、 簡単なループ並列化については UE4ParallelFor を用意している。本節では簡単で擬似的なソースコードC++17 Parallelism, OpenMP, Intel TBB, Microsoft PPL について同等のコードを例示的に整理する。

1.13.1. ParallelForfor_each ( C++17 Parallelism ) #pragma omp parallel for / parallel_for ( TBB ) / parallel_for ( PPL )

// Note: このコードはほとんど実際に動きそうなコードを書いているが、擬似コードである。
// 実際には例示する example 群は同時に1つのソースコードではそのまま動作しない。
// それぞれの違いを理解しやすいよう1つにまとめたが、実行したい場合は
// それぞれのライブラリーや環境に合わせて1つ1つ試すと良い。

#include <iostream>
#include <cmath>
#include <numeric>
#include <vector>

constexpr size_t n = 256;
using value_type = double;
using values_type = std::vector< value_type >;

void process_value( const value_type v )
{
  std::cout << ( std::to_string( std::sqrt( v ) ) + '\n' );
}

// std::for_each_n ( with C++17 Parallelism )
#include <algorithm>
#include <execution>

void example_std_for_each_cpp17_parallelism( const values_type& values )
{
  std::cout << "\n[ std::for_each with C++17 Parallelism ]\n";
  
  // https://github.com/cplusplus/parallelism-ts
  // http://www.modernescpp.com/index.php/c-17-new-algorithm-of-the-standard-template-library
  // http://www.bfilipek.com/2017/08/cpp17-details-parallel.html
  std::for_each
  ( std::execution::par
  , values.begin()
  , values.end()
  , &process_value
  );
}

void example_openmp( const values_type& values )
{
  std::cout << "\n[ OpenMP ]\n";
  
  // official spec. http://www.openmp.org/specifications/
  // ref 1 https://en.wikipedia.org/wiki/OpenMP
  // ref 2 http://bisqwit.iki.fi/story/howto/openmp/
  // ref 3 (ja) http://tech.ckme.co.jp/openmp.shtml
  #pragma omp parallel for
  for ( auto i = values.cbegin(); i < values.cend(); ++i )
    process_value( *i );
}

#include <ppl.h>

void example_microsoft_ppl( const values_type& values )
{
  std::cout << "\n[ Microsoft PPL ]\n";
  
  // PPL https://msdn.microsoft.com/en-us/library/dd492418.aspx
  // parallel_for https://msdn.microsoft.com/en-us/library/dd728073.aspx
  parallel_for
  ( size_t( 0 )
  , values.size()
  , [&]( auto index )
    {
      process_value( values[ index ] );
    }
  );
}

#include <tbb/parallel_for.h>

void example_intel_tbb( const values_type& values )
{
  std::cout << "\n[ Intel TBB ]\n";
  
  // https://software.intel.com/en-us/node/506153
  parallel_for
  ( size_t( 0 )
  , values.size()
  , [&]( auto index )
    {
      process_value( values[ index ] );
    }
  );
}

#include "Runtime/Core/Public/Async/ParallelFor.h"

void example_ue4_parallel_for( const values_type& values )
{
  std::cout << "\n[ UE4 ParallelFor ]\n";
  
  // https://docs.unrealengine.com/latest/INT/API/Runtime/Core/Async/ParallelFor/
  ParallelFor
  ( values.size()
  , [&]( auto index )
    {
      process_value( values[ index ] );
    }
  );
}

int main()
{
  std::vector< double > values( n );
  std::iota( p, p + n, 0 );
  
  example_std_for_each_cpp17_parallelism( values );
  example_openmp( values );
  example_microsoft_ppl( values );
  example_intel_tbb( values );
  example_ue4_parallel_for( values );
}

UE4C++ で一般的に用いられる事の多い多くのライブラリー等に比べてより単純な ParallelFor しか高レベル並列化機能を提供していないが、基本的には [ 0 .. N ) のレンジの並列化そのままで多くの場合には十分かつ必要に応じて少し与えるレンジやファンクターのキャプチャーに工夫をすればたいていの用途には簡単に応用できるため多くの場合には必要十分となる。

それでは必要不十分という場合には UE4 のタスク制御機能を応用したり、必要に応じてライブラリーを導入すると良い。また、恐らくそう遠く無く C++17 Parallelism 対応の開発環境も UE4 での開発環境一般としても、特に PC 向けであれば使用可能となるので、近い未来にはアルゴリズムについてはそちらも必要に応じて使用すると良い。

なお、 ParallelFor には例では省略した最後の bool 型の引数が1つあり、デフォルトの false では並列実行されるが、 true を与えると逐次実行に動作が切り替わる機能もあり、これがデバッグに有用な事もしばしばあり便利である。

但し、 UE4ParallelFor については扱われる index が int32 型である点に気をつける必要がある。 C++ 界の一般的な並行処理ライブラリーでは index は size_t 型である。

参考:

  1. C++ 標準の promise / future / thread に対応する UE4 標準の TPromise / TFuture / FRunnableThread の使い方 - C++ ときどき ごはん、わりとてぃーぶれいく☆
  2. future - cpprefjp C++日本語リファレンス
  3. thread - cpprefjp C++日本語リファレンス
  4. Using AsyncTasks - Epic Wiki

1.14. UE4/C++ における「ファンクター」ライブラリーと std 互換性

std::function に相当する機能が UE4 ライブラリーにも実装されている。本節ではこれらについて std 慣れした C++er 向けに整理する。

1.14.1. TFunctionRef<T>std::function<T>

所属 関数 Header
std function <functional>
UE4 TFunction Runtime/Core/Public/Templates/Function.h
UE4 TFunctionRef Runtime/Core/Public/GenericPlatform/GenericPlatformProcess.h
特徴 std::function TFunction TFunctionRef
has operator= yes yes no
has operator(bool) yes yes no
has ownership of a functor yes yes no
a speed prioritized no no yes

ソースコードでの具体的な比較を以下に示す:

// ふつうの関数 -> ポインターは C++ ファンクターの一種
void something_function() { }

void example()
{
  using signature_type = void();
  // ラムダエクスプレッション -> C++ ファンクターの一種
  auto lambda = []{};
  
  std::function< signature_type > f = lambda;
  if ( f ) // operator(bool) による呼び出し可能の確認に対応
    f();
  f = &something_function; // 代入によりファンクターの挿げ替えが可能
  if ( f )
    f();
  
  // UE4: TFunction
  TFunction< signature_type > g = lambda;
  if ( g ) // operator(bool) による呼び出し可能の確認に対応
    g();
  g = &something_function; // 代入によりファンクターの挿げ替えが可能
  if ( g )
    g();
  
  // UE4: TFunctionRef
  TFunctionRef< signature_type > h = lambda;
  h(); // とりあえず呼び出す事しかできないが、軽量な速度重視の実装になっている
  
  TFunctionRef< signature_type > h = lambda;
}

std::function 互換相当の TFunction については特に UE4 ならこちらを使うべきという程の強い動機はない。 TFunctionRefUE4 にしかない機能だが、ユーザーにとってはこちらもこれという程の使い所も無いだろう。

2. プロフェッショナル C++er のための UE4/C++ リアル・ベーシック

予め断っておかねばならない事がある。§1を書き終えた時点で11月23日も終わろうとしており、著者には時間的猶予が…無い。そこで§2は内容を大幅に予定よりも削減してしまった。

アドベンカレンダーにかぎらず、著者はこのブログで毎月数本の技術ブログ記事を執筆しているので、ここで書かれる予定だった記事も興味が失われない限りはいつか記事になると思う。さしあたり、気が向いたならブログを購読しておいて頂けると読者数のカウントが私にも見えて嬉しい。

2.1. UE4/C++ におけるガベージコレクターの操作

UE4 のガベージコレクターも UHT の力を借りるとは言えライブラリーとしての実装であり、 C++ コードからある程度は制御が可能になっている。本節ではありがちな要求について UE4/C++ での実現方法について整理する。

2.1.1. 任意の UObject* の参照カウントを確認する

// o の参照カウントを取得
inline static int32 GetReferenceCount( UObject* o )
{
  check( o )
  
  constexpr bool bInRequireDirectOuter     = false;
  constexpr bool bInShouldIgnoreArchetype  = true;
  constexpr bool bInSerializeRecursively   = true;
  constexpr bool bInShouldIgnoreTransient  = false;
  
  TArray< UObject* > refs;
  
  FReferenceFinder finder
  ( refs
  , o
  , bInRequireDirectOuter
  , bInShouldIgnoreArchetype
  , bInSerializeRecursively
  , bInShouldIgnoreTransient
  );
  
  finder.FindReferences( o );
  
  return refs.Num();
}

参考:

  1. FReferenceFinder | Unreal Engine
  2. [https://wiki.unrealengine.com/Garbage_Collection~Count_References_To_Any_Object:title]

2.1.2. 強制的にガベージコレクターを動作させる

GetWorld()->ForceGarbageCollection( true );

UWorld::ForceGarbageCollecion は「強制的にガベージコレクターを動作させる」が、厳密な意味で「直ちに」ではない。 GC の監視タイマーは通常 60 秒ごとにガベージコレクトを行うが、このタイマーの残りを 0 にして、次回の(次フレームの前の) GC のタイマー判定タイミングでガベージコレクトを行わせる効果がある。

なお、当然ながら、この機能では参照カウントが有効なオブジェクトは削除されない。 UObject::ConditionalBeginDestroy を呼びポインターを保持している UPROPERTY() の値を nullptr にしておけば削除される事になる。但し、 AActor については ConditionBeginDestroy の前に Destroy を呼び忘れないよう注意されたい。

また、複数のオブジェクトから参照するが GC の参照カウントに影響する必要が無い場合は TWeakObjectPtr<T>, TAutoWeakObjectPtr<T> を用いておけば面倒が無い。

参考:

  1. TWeakObjectPtr | Unreal Engine
  2. TAutoWeakObjectPtr | Unreal Engine
  3. UWorld::ForceGarbageCollection | Unreal Engine
  4. https://wiki.unrealengine.com/Garbage_Collection_%26_Dynamic_Memory_Allocation#Counting_UPROPERTY.28.29_References_To_Any_Object

2.2. UE4/C++ における「相互排除と同期」ライブラリーと std 互換性

UE4 には 2 種類の相互排除と同期のためのライブラリー実装があるので std の同様の実装と使用例を挙げて整理する。

2.2.1. FCriticalSection クリティカルセクション型と std::mutex

// std
#include <mutex>
{
  std::mutex m;
  // scoped lock; C++17 からは template 型が省略可能になる
  { std::lock_guard< decltype( m ) > lock( m ); }
  // manual operation
  m.lock();
  m.unlock();
}
// UE4
#include "Runtime/Core/Public/Misc/ScopeLock.h"
{
  FCriticalSection m;
  // scoped lock
  { FScopeLock lock( &m ); }
  // manual operation
  m.Lock();
  m.Unlock();
}

std 版では lock_guard, adopt_lock, unique_lock など実装が UE4 よりも充実しているので、特別な事情が無く、 std 版を使用可能な場合は UE4/C++ でも std 版を使えば良い。

2.2.2. FRWLock 読み書きロック型と std::shared_mutex

既に C++17er となる準備のできている一般的な多くの C++er 、あるいは C++14er はプラットフォーム依存の API に頼らずとも shared_mutex, shared_timed_mutex を用いて Read / Write レベルのロック制御を実装している。

UE4 でもそれらに相当… "いちおう" は相当する FRWLock がライブラリーで提供されている。

// std ( C++17 )
#include <mutex>
{
  std::shared_mutex m;
  { std::shared_lock< decltype( m ) > read_lock( m ); }
  { std::lock_guard< decltype( m ) > write_lock( m ); }
}
// std ( C++14 )
#include <mutex>
{
  std::shared_timed_mutex m;
  { std::shared_lock< decltype( m ) > read_lock( m ); }
  { std::lock_guard< decltype( m ) > write_lock( m ); }
}
// UE4
#include "Runtime/Core/Public/Misc/ScopeRWLock.h"
{
  FRWLock m;
  // scoped lock
  { FRWScopeLock read_lock( m, FRWScopeLockType::SLT_ReadOnly ); }
  { FRWScopeLock write_lock( m, FRWScopeLockType::SLT_Write ); }
  // manual operation
  m.ReadLock();
  m.ReadUnlock();
  m.WriteLock();
  m.WriteUnlock();
}

FRWLock の挙動は次の通り:

  1. ReadLock -> ReadLock: 可能
    • ReadLock した回数だけ ReadUnlock されるとロック状態が解除される
  2. ReadLock -> WriteLock: 解錠待ち(=ブロッキングデッドロック注意)
  3. WriteLock -> WriteLock 解錠待ち(=ブロッキングデッドロック注意)
  4. WriteLock -> ReadLock 解錠待ち(=ブロッキングデッドロック注意)
  5. 過解錠の挙動はプラットフォーム依存(≃落ちる)

参考:

  1. shared_mutex - cpprefjp C++日本語リファレンス
  2. shared_timed_mutex - cpprefjp C++日本語リファレンス
  3. shared_lock - cpprefjp C++日本語リファレンス
  4. lock_guard - cpprefjp C++日本語リファレンス
  5. C++17, C++14, C++11 に併せた std::shared_mutex, std::shared_timed_mutex, std::mutex から mutex_type, read_lock_type, write_lock_type を扱う例 - Qiita
  6. https://github.com/EpicGames/UnrealEngine/blob/4.18/Engine/Source/Runtime/Core/Public/Windows/WindowsCriticalSection.h#L114

2.3. UE4/C++ における基礎的な「HTTP」ライブラリー

UE4 には HTTP Client のライブラリー実装があるので紹介する。

2.3.1. FHttpModule

そのまま紹介してもあまり面白くないので、少し工夫して実用的にラップした例を紹介する:

#pragma once

#include "Runtime/Online/HTTP/Public/Http.h"
#include "Runtime/Core/Public/Async/Future.h"
#include "Async.h"

namespace usagi
{
  template < typename T >
  inline static TFuture< T >
  GetHTTP
  ( const FString& url
  , std::function< T ( FHttpRequestPtr, FHttpResponsePtr, bool ) >
      decoder = [] ( auto, auto, auto ) { return T(); }
  , const FString& method = TEXT( "GET" )
  , const FString& user_agent = TEXT( "X-UE4-Agent" )
  )
  {
    TSharedPtr< TPromise< T > > p( new TPromise< T >() );
    auto f = p->GetFuture();
    
    auto http = &FHttpModule::Get();
    auto request = http->CreateRequest();
    
    request->OnProcessRequestComplete().
      BindLambda
      ( [ = ]
        ( FHttpRequestPtr   request
        , FHttpResponsePtr  response
        , bool              successful
        )
        {
          UE_LOG( LogTemp, Log, TEXT( "result of http-request url: %s => %s" ), *url, successful ? TEXT( "Succeeded" ) : TEXT( "Failed" ) );
          p->SetValue( decoder( request, response, successful ) );
        }
      );
    
    request->SetURL( url );
    request->SetVerb( method );
    request->SetHeader( TEXT( "User-Agent" ), user_agent );
    
    request->ProcessRequest();
    
    return f;
  }
}

ここまで読み進んだ C++er ならこの程度のコードは難しくなく読めると思う。一応簡単に解説する。

  1. UE4 の HTTP Client 機能は 7 段階でリクエスト:
    1. FHttpModule::Get // Note: FHttpModule は singleton
    2. FHttpModule::CreateRequest
    3. IHttpRequest::OnProcessRequestComplete
    4. IHttpRequest::SetURL
    5. IHttpRequest::SetVerb
    6. IHttpRequest::SetHeader
    7. IHttpRequest::ProcessRequest
  2. 実装上の工夫として usagi::GetHTTP では:
    1. url と decoder を実引数で渡す
      • decoder は Get 結果(テキストとは限らない)を望むオブジェクトへ変換するファンクター
    2. 関数自体は Future を返す
      • 呼び出し元は Future::IsReady を確認して Future::Get する

改良の余地はあるが、あまり複雑化しても FHttpModule の紹介から遠ざかる(&わたしの執筆時間がもうない)こともあり、この程度の実装で紹介した。

C++er は大きなフレームワーク(Qt, POCO など)を使う場合で無ければ libcurl に頼るか boost::asio でソケットから組み立てるなど何れ何かと "めんどう" な HTTP 処理だが、 UE4(大きなフレームワークだが) では Client 処理についてはこのように簡単に扱えるようになっている。

なお、 HTTP Server を簡単に組み込みたい場合は Marketplace で有償販売されている UnrealWebServer など使うと実装の苦労が無く C++ に加えて Blueprint でも簡単に扱えて良い。

もちろん、必要に応じて boost::asio や nghttp2 などから組み込んでもよいだろう。特に blueprint 対応しなくて構わないならばそれほど "めんどう" でもない。

なお、 UE4 の HTTP ライブラリーはモジュール化されているため、 "{your-project-name}.Build.cs" の PublicDependencyModuleNames.AddRange, "Http" を追加しておく必要がある点には注意されたい。

2.4. UE4/C++ における「JSON」ライブラリー

UE4 には JSON を扱うためのライブラリーが実装されている。少々癖があるが、 UE4JSON を扱うならば一度試す価値はある。

2.4.1. FJsonValue 抽象型

UE4JSON ライブラリーは、コード例を示す前に少し解説が必要と思われる。

全てはここから始まる。

  1. FJsonValue は抽象型なのでそのオブジェクトは存在しない。
  2. FJsonValue から派生した -Array, -Boolean, -Null, -Number, -Object, -String の 6 つの型が具体的な JSON 値を保持するオブジェクトとして存在可能。
  3. TSharedPtr< FJsonValue > value = MakeSharable( new FJsonValueNull() ); の様に使う。
    • Null 以外は ctor で値を放り込める。
    • 但し、 Array は TArray< TSharedPtr< FJsonValue > > 、 Object は TSharedPtr< FJsonObject > を放り込む必要がある。
      • FJsonObject は public メンバーに TMap< FString, TSharedPtr< FJsonValue > > を持ち、 key-valueJSON の値として便利にキャストする機能を持った特別の型

理屈としては以上で JSON のオブジェクトを UE4/C++ 上のオブジェクトとして表現する。加えて、 FJsonValueFJsonObject は値をキャストして取得する機能があり、これによって具体的な値の取得も便利に可能となっている。

但し、 { "a": { "b": [ { "c": 123 } ] } }` に対して "a.b[0].c" のようにパスでアクセスする機能などハイカラなものは無いので、地味にこつこつオブジェクトの組み立てと戦いながら扱う必要がある。

また、 UE4JSON ライブラリーはモジュール化されているため、 "{your-project-name}.Build.cs" の PublicDependencyModuleNames.AddRange, "Json", "JsonUtilities" を追加しておく必要がある点にも注意されたい。

2.4.2. UE4/C++ JSON ライブラリーによる読み書き

C++er には旧くは picojson や json11 などの JSON ライブラリーが、また最近では nlohman-json ライブラリーの利用も増えている。実際問題、 C++ のオブジェクトとして JSON を扱いたいだけならばそれらの方が扱いやすくて楽だが、本記事の趣旨の都合、 UE4JSON ライブラリーを使った JSON の読み書きを紹介する。

慎重な C++er は使い方を誤る事は無いと思うものの、 TSharedPtr を介した、また値の取得についてはその const* (スマートポインターの const ポインターを扱う設計なのだ…)を扱わなければならないので、少々の覚悟をしてから挑まないと混乱してしまう。

次の JSON を読んでログ出力する例を挙げる:

{ "aaa": 1.23
, "bbb": true
, "ccc": "hoge"
, "ddd": [ 1.23, true, "hoge" ]
, "eee": { "ppp": "qqq" }
}
// read a JSON
/// @param in JSON string
#include "Runtime/Json/Public/Serialization/JsonSerializer.h"
void ue4_json_read( const FString in )
{
  // reader
  TSharedRef< TJsonReader<> > reader = TJsonReaderFactory<>::Create( *in );
  // string --> a value of the root
  TSharedPtr< FJsonValue > root_value;
  if ( ! FJsonSerializer::Deserialize( reader, root_value ) || ! root_value.IsValid() )
    return;
  // root: value --> root object
  const TSharedPtr< FJsonObject >* root_object;
  if ( ! root_value->TryGetObject( root_object ) )
    return;
  // root: object --> a number value of the "aaa" field
  double value_of_aaa = 0;
  if ( ! (*root_object)->TryGetNumberField( TEXT( "aaa" ), value_of_aaa ) )
    return;
  UE_LOG( LogTemp, Log, TEXT( "value_of_aaa=%f" ), value_of_aaa )
  // root: object --> a bool value of the "bbb" field
  bool value_of_bbb = false;
  if ( ! (*root_object)->TryGetBoolField( TEXT( "bbb" ), value_of_bbb ) )
    return;
  UE_LOG( LogTemp, Log, TEXT( "value_of_bbb=%s" ), ( value_of_bbb ? TEXT( "true" ) : TEXT( "false" ) ) )
  // root: object --> a string value of the "ccc" field
  FString value_of_ccc;
  if ( ! (*root_object)->TryGetStringField( TEXT( "ccc" ), value_of_ccc ) )
    return;
  UE_LOG( LogTemp, Log, TEXT( "value_of_ccc=%s" ), *value_of_ccc )
  // root: object --> a array value of the "ddd" field
  const TArray< TSharedPtr< FJsonValue > >* value_of_ddd = 0;
  if ( ! (*root_object)->TryGetArrayField( TEXT( "ddd" ), value_of_ddd ) )
    return;
  // foreach value_of_ddd ( array ) --> value --> { number | bool | string }
  for ( auto&& element_of_ddd : *value_of_ddd )
  {
    double  number_buffer = 0;
    bool    bool_buffer   = false;
    FString string_buffer;
    // value --> number
    if ( element_of_ddd->TryGetNumber( number_buffer ) )
      UE_LOG( LogTemp, Log, TEXT( "number value in ddd=%f" ), number_buffer )
    if ( element_of_ddd->TryGetBool( bool_buffer ) )
      UE_LOG( LogTemp, Log, TEXT( "bool value in ddd=%f" ), bool_buffer )
    if ( element_of_ddd->TryGetString( string_buffer ) )
      UE_LOG( LogTemp, Log, TEXT( "string value in ddd=%f" ), *string_buffer )
  }
  // root: object --> an object value of the "eee" field
  const TSharedPtr< FJsonObject >* value_of_eee;
  if ( ! (*root_object)->TryGetObjectField( TEXT( "eee" ), value_of_eee ) )
    return;
  // Note: これまでの root object のように Field 指定して取得もできるが、
  //       TMap< FString, TSharedPtr< FJsonValue > > を直接操作もできる
  for ( auto&& pair_of_eee: (*value_of_eee)->Values )
  {
    FString string_buffer;
    if ( pair_of_eee.Value->TryGetString( string_buffer ) )
      UE_LOG( LogTemp, Log, TEXT( "string:string value in eee={%s:%s}" ), *pair_of_eee.Key, *string_buffer )
  }
}

扱っている対象が抽象型の Value なのか、あるいは Array または Object なのかを中心に、オブジェクトに対しては対象はポインターか、スマートポインターか、その中の何かか、気を使って組み立てる必要がある。幸い、型のエラーは翻訳時に検出されるが、コードの実装者は少々疲れることになるだろう。

さておき、次は同じ JSON オブジェクトを構築する例を示す:

// create a JSON
/// @param in JSON string
TSharedPtr< FJsonValue > ue4_json_create()
{
  // Note: auto で受けたいが、 MakeShareable の return は
  // SharedPointerInternals::FRawPtrProxy< FJsonObject > で
  // そうなると operator-> も使えないのでだるい。
  TSharedPtr< FJsonObject > root_object = MakeShareable( new FJsonObject );
  root_object->SetNumberField( TEXT( "aaa" ), 1.23 );
  root_object->SetBoolField( TEXT( "bbb" ), true );
  root_object->SetStringField( TEXT( "ccc" ), TEXT( "hoge" ) );
  TArray< TSharedPtr< FJsonValue > > ddd_array;
  ddd_array.Reserve( 3 );
  ddd_array.Emplace( MakeShareable( new FJsonValueNumber( 1.23 ) ) );
  ddd_array.Emplace( MakeShareable( new FJsonValueBoolean( true ) ) );
  ddd_array.Emplace( MakeShareable( new FJsonValueString( TEXT( "hoge" ) ) ) );
  // SetXXXField 系では右辺値参照はサポートされていない
  root_object->SetArrayField( TEXT( "ddd" ), ddd_array );
  TSharedPtr< FJsonObject > eee_object = MakeShareable( new FJsonObject );
  eee_object->SetStringField( TEXT( "ppp" ), TEXT( "qqq" ) );
  root_object->SetObjectField( TEXT( "eee" ), eee_object );
  return MakeShareable( new FJsonValueObject( root_object ) );
}

参考:

  1. Dom | Unreal Engine

2.5. UE4/C++ における「ファイルシステム」ライブラリー

実行時にファイルシステムを覗きたい需要はままあるかと思う。一般に C++er は C++14er である現在まで長きに渡り filesystem について標準ライブラリーで十分には扱えない事に嘆いていた。しかし C++17er がその嘆きを過去のものにする日は既にそう遠くない。

UE4 にもファイルシステムライブラリーが実装されている。もう数日、あるいはもう少し、 UE4 の release ラインが公式に C++17 ビルドに対応するその日まで有用かもしれない簡単な例を掲載する事とした。 ".json" 拡張子を持つファイル群を特定のディレクトリーから実行時に根こそぎ読み出す UE4ファイルシステムライブラリーでの実装例を紹介する。

2.5.1. UE4ファイルシステムライブラリーで .json ファイルを根こそぎ読み出す

実装例兼参考リンク:

// UE4
{
  const FString my_json_dir = TEXT( "my_json_dir" );
  
  TArray< FString > directories_to_skip;
  // https://docs.unrealengine.com/latest/INT/API/Runtime/Core/HAL/FPlatformFileManager/
  IPlatformFile& platform_file = FPlatformFileManager::Get().GetPlatformFile();
  // https://docs.unrealengine.com/latest/INT/API/Runtime/Core/Misc/FLocalTimestampDirectoryVisitor/
  FLocalTimestampDirectoryVisitor Visitor( platform_file, directories_to_skip, directories_to_skip, false );
  // https://docs.unrealengine.com/latest/INT/API/Runtime/Core/Misc/FPaths/
  // https://docs.unrealengine.com/latest/INT/API/Runtime/Core/Misc/FPaths/ProjectDir/index.html
  platform_file.IterateDirectory( *( FPaths::ProjectDir() / my_json_dir ), Visitor );
  
  for ( TMap< FString, FDateTime >::TIterator TimestampIt( Visitor.FileTimes ); TimestampIt; ++TimestampIt )
  {
    const FString file_path = TimestampIt.Key();
    const FString file_name = FPaths::GetCleanFilename( file_path );
    
    if ( FPaths::GetExtension( file_name, false ).Equals( TEXT( "json" ), ESearchCase::IgnoreCase) )
    {
      FString json_text;
      
      if ( FFileHelper::LoadFileToString( json_text, *file_path, FFileHelper::EHashOptions::None ) )
        // 前節で紹介した JSON の reader へ投げるなどお好み処理
        ue4_json_read( json_text );
    }
  }
}

2.6. エンジンプラグインのリビルド

時折、 Marketplace で購入したエンジンプラグインのローカル環境での手修正、そして手リビルドをしたい場合があるかもしれない。私は最近もあった。

エンジン本体のローカル環境でのリビルドは公式にわかりやすい解説もあるし、 Linux でも Windows でも困らないが、エンジンプラグインのリビルドは方法を知らないとどうしたもんかと少々悩むかもしれないので最後に紹介する事とした。

2.6.1. エンジンプラグインのリビルド

  1. エンジンプラグインディレクトリーごと 7z でもしてバックアップしてからソースの改変作業を開始する。
  2. ソースを改変する。
  3. リビルドしてパッケージを適当な dir へ吐き出す。
    • "C:\Program Files\Epic Games\UE_4.18\Engine\Build\BatchFiles\RunUAT.bat" BuildPlugin -plugin="C:\Program Files\Epic Games\UE_4.18\Engine\Plugins\Marketplace\SomethingPlugin\Something.uplugin" -package="C:\Users\usagi\tmp\xxxxx"
  4. エンジンプラグインディレクトリーの中身をリビルドした中身へ挿げ替える。

おわりに

§2 はお察しのように大幅に削る事になってしまった。しかしこれでも11月の任意に行動可能なプライベート時間は98%をこの執筆に費やした気分でいる。実際のところもっと時間をとれるかとも思ったが、現実に一日は24時間、仕事はきっちりやる(というかザンギョーもする)なかではなかなか他の趣味の時間をゼロにしても完全な内容には程遠い完成度の文書しか残せなかったことが悔やまれる。しかし、12月1日はもうすぐそこまで来ている。それにこの後わたしは C++ アドベンカレンダーの記事も書くことにしている。時間が、足りない。悲しい。

そして、そんなことをしてこの記事を書いていた11月は趣味で好き勝手に作る楽しいゲーム開発の時間はゼロにしていた。これもまたつらい。数時間前に STEAM からのメールを見て「フィリスのアトリエ」も DLC コンプリートで買ってしまい、詰みゲーも増えた。

私はもともと毎月何本か適当にこのブログで技術記事も執筆しているし、他に趣味のお茶の話と食べ物の話のブログもちらほら書いている。

日頃から技術ブログを書いている事もあり、せっかくアドベンカレンダーに参加するならば、といつもより頑張った記事でも書こうとしてしまい、正直これはたいへん疲れた。アドベンカレンダーは日頃情報発信する機会の少ない方にも情報発信する機会として有用という要素もあるし、私の場合は放って置いても技術ブログを書いているので、やっぱりアドベンカレンダーは今回を最後に引退しようかな、なんて思っています😅

ともあれ、今回書ききれなかった§2にあるべきような節に相当する内容なども含め、今後もちまちまこのブログでは記事を書きますので興味があれば、あるいはググった拍子になど適当に、購読やはてなスターSNSでのいいねなど頂ければ嬉しいです。

では、みなさん良いクリスマスまで残り24日間と数時間、楽しんで参りましょう😃 私はひとあし先に、一仕事終えた気分で続く記事を楽しみに待つ事とします。