本文へスキップ
バージョン: 3.1

ランタイムに配置されたコンテンツを永続化のためにシリアライズする方法

このハウツーは以下を含みます:

  • 実行時にスポーンするプレハブの登録
  • 将来のセッションでコンテンツを復元するために、トランスフォームとステートフル・データを保存する。
  • 以前にシリアライズされたデータの更新

このハウツーでは、手動で作成した冗長文字列を使って、人間が読める形式でのシリアライゼーションを実演する。 適切なシリアライゼーションを行うには、 [Serializable] 構造体を使用し、 具象構造体にはJSONシリアライザを、コンパクトなストレージにはバイナリー・シリアライザを使用することを推奨します。

前提条件

Lightship ARを有効にしたUnityのプロジェクトが必要です。 詳細については、 ARDK 3.0 のインストールを参照してください。

ランタイム・コンテンツのシリアライゼーションの基本概念

リモートコンテンツオーサリング(Remote Content Authoring)とは対照的に、Unity のシーンは ARLocation ごとに 静的に配置されたオブジェクトで構築されますが、ランタイムコンテンツシリアライゼーション(Runtime Content serialization)では、プレイヤーが任意にコンテンツ を配置し、配置されたコンテンツを既知のフォーマットにシリアライズして、将来的に再スポーンできるようにします。

プレイヤーが構築したシーンをシリアライズするには、以下の情報が必要です:

  1. コンテンツがアンカーされるルート変換(ARLocation、Image Targetなど)。
    1. これにより、将来デシリアライズされたコンテンツが同じ場所に置かれることが保証される。
  2. プレイヤーが配置したすべてのGameObject
  3. それぞれのGameObjectの種類と状態(この例ではtransformとcolor)。

このデータは、文字列またはバイナリブロブに圧縮され、将来の検索に使用されます。

データをデシリアライズし、シーンを再構築する:

  1. 各GameObjectは、シリアライズされたデータから取得し、適切なGameObjectをインスタンス化する必要がある。
    1. 各GameObjectは同じルートtransfrom(ARLocation)の下に配置され、同じ場所に表示されます。
  2. 各GameObjectの状態データ(トランスフォーム、色)が適用される。

サンプルシーン "VpsSceneSerialization "は、このハウツーにあるすべてのスクリプトをフォローするために用意されています。

シリアライズのためにプレハブを登録する

解決すべき最初の問題は、プレイヤーが配置可能なすべてのプレハブのリストを集めることです。 これは 将来、現場を再現するために必要なことである。

スクリプト PersistentObject.cs はこの目的を達成し、スポーン可能なプレハブは GetComponentで見つけることができ、一意のプレハブは PrefabIdで識別されます。

using UnityEngine;

public class PersistentObject : MonoBehaviour
{
[SerializeField]
private long _prefabId;

public long PrefabId
{
get { return _prefabId; }
internal set
{
_prefabId = value;
}.
}

...// シリアライズコード
}.

VpsSceneSerialization」サンプルでは、 PersistentObject Prefabsが2つあり、立方体と球体で、 、それぞれ PrefabIds 0と1を持っています。

これらの PersistentObjects は、 PersistentObjectManagerに登録され、スポーン可能なオブジェクトの完全なリスト を持つ。

永続オブジェクト・マネージャ・コンポーネント
using System.Collections.Generic;
using System.Collections.ObjectModel;


public class PersistentObjectManager : MonoBehaviour
{
[SerializeField]
private List<PersistentObject> _persistentObjects = new List<PersistentObject>();

public ReadOnlyCollection<PersistentObject> PersistentObjects => _persistentObjects.AsReadOnly();

...// シリアライズおよび検証コード
}.

ランタイム・コンテンツの配置

VpsSceneSerialization」サンプルには、 PersistentContentDemoManager.csPersistentObjectPlacementBehaviour.cs という2つのスクリプトがあり、プレイヤーがプレーンや既存のGameObjectをタップするたびに、キューブやスフィアの生成/破棄を管理します。

実際のゲームには独自のスポーン/デストロイ・ルールがあり、このハウツーではこれらのスクリプトがどのように機能するかについては深く触れない。 唯一関連する実装の詳細は、インスタンス化後、すべてのスポーンされたオブジェクトが ARLocationにチャイルドされるということです:

// _rootは、現在追跡されているARLocationへの参照
spawned.transform.SetParent(_root.transform, true)

生成オブジェクトのシリアライズと保存

プレイヤーがシーンを構築し、それをシリアライズして保存する準備ができたら、 GameObjectがスポーンされたすべてのリストを収集する必要があります。 例えば、各GameObjectをListに保持する、 、あるいはこのサンプルのように、ルートGameObjectに GetComponent<PersistentObject> を使うなど、いくつかの方法がある:

public class PersistentObjectManager : MonoBehaviour
{
[SerializeField]
private List<PersistentObject> _persistentObjects = new List<PersistentObject>();
private const char Delimiter = ';';

private readonly Dictionary<long, PersistentObject> _validObjects =
new Dictionary<long, PersistentObject>();

public string SerializeObjectsToString(GameObject rootGo = null)
{
var root = rootGo ? rootGo.transform : this.transform;
var serializedObjects = new List<string>();
ValidateAndRefreshPersistentObjectList();

foreach (var child in root)
{
var persistentObject = ((Transform)child).GetComponent<PersistentObject>();
if (persistentObject)
{
if (!_validObjects.Keys.ToList().Contains(persistentObject.PrefabId))
{
continue;
}

serializedObjects.Add(persistentObject.SerializeObjectToString());
}.
}

return string.Join(Delimiter, serializedObjects.ToArray());
}.

...// デシリアライズと検証コード
} ...

簡単のため、このサンプルではすべてのPrefabsが同じルートにチャイルドされ、 Prefabsがネストされていないことを想定しています。 実際のゲームでは、階層構造におけるプレハブのトラッキングとハンドリングを改善する必要があるかもしれない。

PersistentObjectsリスト を収集した後、各 PersistentObject は、独自のシリアライズ を処理して文字列に変換し、それらを1つの文字列表現に結合して格納します。

public class PersistentObject : MonoBehaviour
{
[SerializeField]
private long _prefabId;

public long PrefabId
{
get { return _prefabId; }
internal set
{
_prefabId = value;
}.
}

public virtual string SerializeObjectToString()
{
return SimpleObjectToString();
}

protected string SimpleObjectToString()
{
var stringFormat = $"PrefabId: {_prefabId}位置:{SerializeVector3(transform.localPosition)}, " +
$"Rotation:{SerializeQuaternion(transform.localRotation)}、Scale:{SerializeVector3(transform.localScale)}";

return stringFormat;
}

private string SerializeVector3(Vector3 vector)
{
return string.Format("{0},{1},{2}", vector.x, vector.y, vector.z);
}

private string SerializeQuaternion(Quaternion quat)
{
return string.Format("{0},{1},{2},{3}", quat.x, quat.y, quat.z, quat.w);
}.

...// デシリアライズコード
}.

シリアライズすると、 PrefabId:0, 位置:0,0,0, 回転:0,-0.2323422,0,0.9726341, Scale:1,1,1;PrefabId:1, Position:0,0,-1.689, Rotation:0,0,0,1, Scale:1,1,1 が返される。 この文字列には、将来のセッションでシーンを再構築するための関連情報( )がすべて含まれている。

シリアライズされた文字列は、将来の検索のために、バックエンドやサービスに保存することができる。 このサンプルでは、 、ARLocationのAnchorの名前をキーにして、PlayerPrefsにローカルに保存されているだけです:

// コンテンツをシリアライズして格納する
var serializedString = PersistentObjectManager.SerializeObjectsToString(_root);

// _locationStringはpersistentAnchor.nameだけ
PlayerPrefs.SetString(_locationString, serializedString)

より多くの状態を格納するためのPersistentObjectの拡張

VpsSceneSerialization」サンプルでは、CubeとSphereは、 base PersistentObjectsではなく、 ColorfulPersistentObjects です。 この区別は、基本的なトランスフォームの上に、よりカスタムなステートフル・データ 。

using UnityEngine;

public class ColorfulPersistentObject : PersistentObject
{
public override string SerializeObjectToString()
{
Color color;

if (!Application.isPlaying)
{
// マテリアルを漏らさないためには、編集モードでsharedMaterialを使用する必要があります。
// これは、マテリアル(デフォルト)を共有するすべてのゲームオブジェクトが同じ色を持つことを意味します
color = GetComponent<Renderer>().sharedMaterial.color;
}
else
{
color = GetComponent<Renderer>().material.color;
}

var colorStr = SerializeColor(color);
return $"{base.SerializeObjectToString()}, Color: {colorStr}";
}

private string SerializeColor(Color color)
{
return $"{color.r},{color.g},{color.b},{color.a}";
}.

...// デシリアライズコード
}.

各シリアライズされたGameObjectのデータには「Color」というフィールドが追加され、 未来に復元することができる。 完全なデータ文字列は次のようになる:

PrefabId:0, Position:0,0,0, 回転:0,-0.2323422,0,0.9726341, Scale:1,1,1, Color: 1,1,1,1;PrefabId:1, Position:0,0,-1.689, 回転:0,0,0,1, Scale:1,1,1, 色: 1,1,1,1

シリアライズされたコンテンツの復元(デシリアライズ)

以前にシリアライズされたデータ文字列は、 PersistentObjectManager.csによって将来のセッションで復元されます。

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text.RegularExpressions;

using UnityEngine;

public class PersistentObjectManager : MonoBehaviour
{
[SerializeField]
private List<PersistentObject> _persistentObjects = new List<PersistentObject>();

public ReadOnlyCollection<PersistentObject> PersistentObjects => _persistentObjects.AsReadOnly();

private const char Delimiter = ';';
private const string PrefabIdReg = "PrefabId:(?<pid>˶*)";
private Regex _prefabIdRegex = new Regex(PrefabIdReg);

private readonly Dictionary<long, PersistentObject> _validObjects =
new Dictionary<long, PersistentObject>();

...// シリアライズとバリデーションコード

public List<GameObject> DeserializeObjectsFromString(string str, GameObject rootGo = null)
{
var spawnedObjects = new List<GameObject>();
var root = rootGo ? rootGo.transform : this.transform;
ValidateAndRefreshPersistentObjectList();

var objs = str.Split(Delimiter);
foreach (var serializedObj in objs)
{
var match = _prefabIdRegex.Match(serializedObj);
if (!match.Success)
{
Debug.LogError("Could not parse out prefab id");
continue;
}

var prefabId = long.Parse(match.Groups["pid"].Value);
if (!_validObjects.ContainsKey(prefabId))
{
Debug.LogError($"Could not find prefab id {prefabId}");
continue;
}

var persistentObject = _validObjects[prefabId];
var newObject = Instantiate(persistentObject.gameObject, root);
newObject.GetComponent<PersistentObject>().ApplyStringToObject(serializedObj);
spawnedObjects.Add(newObject);
}

return spawnedObjects;
}.
}

DeserializeObjectsFromString メソッドは、以前にシリアライズされた文字列を受け取り、それを 個々のGameObjectに分割し、 記録された PrefabIdsに従って、登録された PersistentObjects をスポーンしようとする。

_peristentObjects リストのリストと内容が、シリアライズとデシリアライズの間で 一貫性を保つことが重要です。そうでないと、無意味なオブジェクトが復元される可能性があります。 実際のゲームでは、 _persistentObjects リストの一貫性を確保するためのバージョン管理スキームが推奨されます。 。

各GameObjectの文字列は、 PersistentObject (または拡張 ColorfulPersistentObject) に渡され、各GameObjectが個別に処理する。

using System.Text.RegularExpressions;

using UnityEngine;

public class ColorfulPersistentObject : PersistentObject
{
private const string ColourReg = "Color: (?<color>.*,.*,.*,.*,.*)";
private static Regex _colourRegex = new Regex(ColourReg);

...// シリアル化コード

public override void ApplyStringToObject(string str)
{
base.ApplyStringToObject(str);

var match = _colourRegex.Match(str);
if (!match.Success)
{
Debug.Log("No match");
return;
}

var colorString = match.Groups["color"].Value;
var color = DeserializeColor(colorString);
ApplyColorToObject(color);
}

public void ApplyColorToObject(Color color)
{
Material mat;
if (!Application.isPlaying)
{
// マテリアルを漏らさないためには、編集モードで sharedMaterial を使用しなければならない
mat = GetComponent<Renderer>().sharedMaterial;
}
else
{
mat = GetComponent<Renderer>().material;
}

mat.color = color;
}

private Color DeserializeColor(string str)
{
// 文字列から色をデシリアライズ
var split = str.Split(',');
return new Color(float.Parse(split[0]), float.Parse(split[1]), float.Parse(split[2]),
float.Parse(split[3]));
}.
}

拡張 ColorfulPersistentObject は、シリアライズされた文字列のColor部分のみを処理し、 、変換データを処理するためにベース PersistentObject に文字列を渡す。 このような複数のマッチは、 非効率的である。

using System.Text.RegularExpressions;

using UnityEngine;

public class PersistentObject : MonoBehaviour
{
[SerializeField]
private long _prefabId;

public long PrefabId
{
get { return _prefabId; }
internal set
{
_prefabId = value;
}.
}

private static string regStr =
"PrefabId:(?<pid>\d*), Position: (?<pos>.*,.*,.*), Rotation: (?<rot>.*,.*,.*,.*), Scale: (?<scale>.*,.*,.*)";
private static Regex regex = new Regex(regStr);

public virtual void ApplyStringToObject(string str)
{
// 文字列を解析してオブジェクトに適用
SimpleApplyStringToObject(str);
}

protected void SimpleApplyStringToObject(string str)
{
var match = regex.Match(str);
if (!match.Success)
{
Debug.Log("No match");
return;
}

var prefabId = match.Groups["pid"].Value;
var position = match.Groups["pos"].Value;
var rotation = match.Groups["rot"].Value;
var scale = match.Groups["scale"].Value;

_prefabId = long.Parse(prefabId);
transform.localPosition = DeserializeVector3(position);
transform.localRotation = DeserializeQuaternion(rotation);
transform.localScale = DeserializeVector3(scale);
}

private Vector3 DeserializeVector3(string str)
{
var split = str.Split(',');
return new Vector3(float.Parse(split[0]), float.Parse(split[1]), float.Parse(split[2]));
}

private Quaternion DeserializeQuaternion(string str)
{
var split = str.Split(',');
return new Quaternion(float.Parse(split[0]), float.Parse(split[1]), float.Parse(split[2]), float.Parse(split[3]));
}.
}

以前にシリアライズされたデータの更新

以前にシリアライズされたデータは、データを復元することで更新することができ、プレイヤーは 、シーンを変更(GameObjectの削除や移動)することができます。その後、再シリアライズし、 、以前に保存されたデータを上書きします。

非常に大きなシーンの場合、常にシーン全体を読み込んで書き換えるのは非効率的なので、インクリメンタルアップデート システムが推奨される。