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

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

この入門ガイドは以下を含みます:

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

この入門ガイドでは、人間が読みやすい形式でシリアライズのデモを行うために、手動で構築した冗長な文字列を使用しています。 適切なシリアライズを行うには、 [Serializable] 構造体を使用し、具象構造にはJSONシリアライザー、コンパクトな保存にはバイナリーシリアライザーを使用することをお勧めします。

前提条件

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

ランタイム・コンテンツのシリアライズに関する基本概念

リモートコンテンツオーサリング では、Unityシーンにおいて各 ARLocation 内に静的に配置されたオブジェクトが含まれますが、ランタイムコンテンツのシリアライズでは、プレイヤーが任意にコンテンツを配置し、その配置されたコンテンツを既知の形式にシリアライズすることで、後で再スポーンできるようにします。

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

  1. コンテンツをアンカーするルートトランスフォーム(ARロケーション、イメージターゲットなど)
    • これにより、将来的にデシリアライズされたコンテンツが同じ場所に正しく配置されます。
  2. プレイヤーが配置したすべての GameObject
  3. GameObjects の種類と状態(この例では、トランスフォームと色)。

このデータは、次回プレイヤーが同じ ARLocation をトラッキングする際に取得できるように、文字列またはバイナリブロブに圧縮されます。

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

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

サンプルシーン「VpsSceneSerialization」が提供されており、この入門ガイドのスクリプトに沿って進めることができます。

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

まずはじめに、プレイヤーが配置可能なすべてのプレハブのリストを収集することが重要です。 これは 将来、現場を再現するために必要なことである。

スクリプト PersistentObject.cs では、 GetComponent を使用してスポーン可能なプレハブを見つけ、各プレハブに PrefabId を割り当てて識別します。

クリックしてPersistentObjectクラスを展開
using UnityEngine;

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

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

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

「VpsSceneSerialization」サンプルでは、キューブと球体の2つの PersistentObject プレハブがあり、それぞれに PrefabId 0と1が割り当てられています。 その後、これらの PersistentObjects は、スポーン可能なオブジェクトの完全なリストを作成するために PersistentObjectManager に登録されます。

永続オブジェクト・マネージャ・コンポーネント
クリックして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 をタップするたびに、 CubeSphere のスポーンと破壊を管理します。 各ゲームにはオブジェクトのスポーンや破壊に関する独自のルールがあるため、この入門ガイドではこれらのスクリプトの詳細については説明しません。

注意

次のコード行は重要な点を実装しています。生成されたすべてのオブジェクトは、インスタンス化後に必ず ARLocation の子オブジェクトにしなければなりません。 独自のアプリケーションを作成する際には、必ずこの点を守ってください。

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

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

プレイヤーがシーンを構築し、それをシリアライズして保存する準備ができたら、まずスポーンされたすべての GameObject のリストを作成する必要があります。 これを行う方法はいくつかあり、各 GameObjectList に保持する方法や、このサンプルで用いられているように、ルート GameObject に対して GetComponent<PersistentObject> を使用する方法が考えられます。

クリックして、更新後のPersistentObjectManagerスクリプトを表示
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());
}.

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

簡潔さを重視して、このサンプルでは、すべてのプレハブが同じルートの子であり、入れ子になったプレハブがないことを前提としています。 実際のアプリケーションでは、Hierarchy内でのプレハブのトラッキングと処理がさらに高度になる場合があります。

PersistentObjectsList を収集した後、各 PersistentObject は自分自身のシリアライズを文字列に変換します。 その後、これらの文字列は一つの文字列に結合され、保存されます。

クリックして、更新後のPersistentObjectスクリプトを表示
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}, Position: {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, Position: 0,0,0, Rotation: 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

シリアライズされた文字列は、将来の取得に備えてバックエンドやサービスに保存することができます。 このサンプルでは、文字列はローカルに PlayerPrefs に保存され、 ARLocation アンカーの名前でインデックスが付けられています。

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

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

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

「VpsSceneSerialization」サンプルでは、キューブと球体は基本の PersistentObject ではなく、 ColorfulPersistentObjects です。 ここでは、基本的なトランスフォームに加えて、さらにカスタムのステートデータをシリアライズする方法を示すために、基本クラスを拡張しています。

クリックしてColorfulPersistentObjectクラスを表示
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}";
}.

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

この拡張機能は、将来的に復元できるように、シリアライズされた GameObjectColor フィールドを追加します。 これにより、サンプルのシリアライズデータ文字列は次のようになります。

PrefabId: 0, Position: 0,0,0, Rotation: 0,-0.2323422,0,0.9726341, Scale: 1,1,1, Color: 1,1,1,1;PrefabId: 1, Position: 0,0,-1.689, Rotation: 0,0,0,1, Scale: 1,1,1, Color: 1,1,1,1

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

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

クリックして、更新後のPersistentObjectManagerスクリプトを表示
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 メソッドは、シリアライズされた文字列を受け取り、それを個々の GameObjects に分割します。次に、記録された PrefabIds に従い、登録された PersistentObjects のスポーンを試みます。 各 GameObject の文字列は、各 GameObject が個別に処理できるように、 PersistentObject(またはこの例では ColorfulPersistentObject)に渡されます。

注意

シリアライズとデシリアライズの間で、 _persistentObjects リストとその内容が一致していることを確認してください。 一致していない場合、意味不明なオブジェクトがスポーンする可能性があります。 完全なゲームを作成する際には、リストの整合性を確保するためにバージョニングスキームを使用することをお勧めします。

クリックして、更新後のColorfulPersistentObjectのデシアライズのスクリプトを表示
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 は、シリアライズされた文字列の色部分を処理し、残りのデータは基本の PersistentObject クラスに渡してトランスフォームデータを処理します。 これは非効率であるため、次のように整理するとよいでしょう。

クリックして、調整後の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]));
}.
}

シリアライズデータを更新する

シリアライズデータを更新するには、データを復元し、プレイヤーにシーンを変更させてから(GameObjects の削除や移動など)、再シリアライズして以前に保存されたデータを上書きします。 (非常に大きなシーンの場合、常にシーン全体を読み書きするのは効率が悪いため、インクリメンタルな更新システムを推奨します。)

詳細情報

Location ARの詳細な使用方法については、 Creating Location AR Experiences with VPS(VPSを使ってロケーションAR体験を作成する) を参照してください。