Simple Persistent AR: Object spawning and data persistence

After setting up the project, we spawn and persist objects inside the domain.

Object spawning

  1. Create serialized fields for the Raycast Manager and Create Cube button:
[SerializeField] private ARRaycastManager raycastManager;
[SerializeField] private Button createCubeButton;
  1. Declare a list to store AR raycast hits:
private List<ARRaycastHit> _arRaycastHits = new List<ARRaycastHit>();
  1. In the ^Update()^ method, raycast from the center of the screen to an AR plane and place the cube marker where the raycast hits a plane.
private void Update()
{
    // Make a raycast from the center of the screen to an AR plane (floor, wall, or any other surface detected by ARFoundation)
    var ray = arCamera.ViewportPointToRay(Vector3.one * 0.5f);
    if (raycastManager.Raycast(ray, _arRaycastHits, TrackableType.PlaneWithinPolygon))
    {
        // Place the cube where the raycast hits a plane. Move it half the cube size along the hit normal (up if on the ground, forward if on the wall)
        cube.transform.position = _arRaycastHits[0].pose.position + _arRaycastHits[0].pose.up * cube.transform.localScale.x / 2f;
        // Rotate the cube only around y axis to always face the camera
        cube.transform.rotation = Quaternion.Euler(Vector3.Scale(arCamera.transform.rotation.eulerAngles, Vector3.up));
    }
}
  1. Define a ^PlaceCube()^ method to be called when the Create Cube button is clicked. This method will instantiate a cube with the specified position, rotation, and color parameters.
private void PlaceCube(Vector3 position, Quaternion rotation, Color color)
{
    var placedCube = Instantiate(cube, position, rotation);
    placedCube.GetComponent<Renderer>().material.color = color;
    placedCube.gameObject.SetActive(true);
}
  1. Now create the method OnCubeButtonClick() that gets a random color and places the cube where the cube marker is.
private void OnCubeButtonClick()
{
    var color = Random.ColorHSV();
    // Place the cube where the cube marker is 
    PlaceCube(cube.transform.position, cube.transform.rotation, color);
}
  1. Import the ^Random^ function used above:
using Random = UnityEngine.Random;
  1. In the ^Start()^ method, attach an event listener to ^createCubeButton^ that triggers the ^OnCubeButtonClick()^ method when the button is clicked:
private void Start()
{
    ...
    _manna.OnLighthouseTracked += OnLighthouseTracked;
    ~~
    createCubeButton.onClick.AddListener(OnCubeButtonClick);
    ~~
    _conjureKit.Connect();
}

Data persistence

In this sample project we'll save data about the spawned cubes in the device's local storage in JSON format. Because Unity-specific data types like ^Vector3^, ^Quaternion^, and ^Color^ are not inherently serializable into JSON format, we'll introduce the custom serializable classes ^SerializableVector3^, ^SerializableQuaternion^, and ^SerializableColor^, to be used when converting to and from JSON format.

  1. Create a new C# script named ^SaveData.cs^:
using System;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class SaveData
{
    public List<CubeData> cubes = new List<CubeData>();
}

// Because Unity's Vector3, Quaternion and Color structs are not marked as [Serializable] they can't be serialized into JSON.
// For that we create serializable versions of each one. There can be other approaches depending on how you serialize/deserialize the data.
[Serializable]
public class CubeData
{
    public SerializableVector3 position;
    public SerializableQuaternion rotation;
    public SerializableColor color;

    public CubeData() {}

    public CubeData(Vector3 position, Quaternion rotation, Color color)
    {
        this.position = new SerializableVector3(position);
        this.rotation = new SerializableQuaternion(rotation);
        this.color = new SerializableColor(color);
    }
}

[Serializable]
public class SerializableVector3
{
    public float x, y, z;
    
    public SerializableVector3() {}

    public SerializableVector3(Vector3 sourceVector)
    {
        x = sourceVector.x;
        y = sourceVector.y;
        z = sourceVector.z;
    }

    public Vector3 ToVector3() => new Vector3(x, y, z);
}

[Serializable]
public class SerializableQuaternion
{
    public float x, y, z, w;
    
    public SerializableQuaternion() {}

    public SerializableQuaternion(Quaternion sourceQuaternion)
    {
        x = sourceQuaternion.x;
        y = sourceQuaternion.y;
        z = sourceQuaternion.z;
        w = sourceQuaternion.w;
    }
    
    public Quaternion ToQuaternion() => new Quaternion(x, y, z, w);
}

[Serializable]
public class SerializableColor
{
    public float r, g, b, a;
    
    public SerializableColor() {}

    public SerializableColor(Color sourceColor)
    {
        r = sourceColor.r;
        g = sourceColor.g;
        b = sourceColor.b;
        a = sourceColor.a;
    }
    
    public Color ToColor() => new Color(r, g, b, a);
}
  1. Back in ^PersistentARinDomain.cs^, create a field for the ^SaveData^ object:
private SaveData _saveData = new SaveData();
  1. Define a ^SaveLocally()^ method to serialize ^_saveData^ to JSON save it to the device's local storage using ^PlayerPrefs^:
private void SaveLocally()
{
    var json = JsonUtility.ToJson(_saveData);
    PlayerPrefs.SetString("_saveData", json);
    PlayerPrefs.Save();
}
  1. Define another method ^LoadLocally()^ to load the JSON data from the device's local storage, deserialize it into ^_saveData^, and place the cubes in the scene:
private void LoadLocally()
{
    if(!PlayerPrefs.HasKey("_saveData"))
        return;
    
    var json = PlayerPrefs.GetString("_saveData");
    _saveData = JsonUtility.FromJson<SaveData>(json);

    foreach (var savedCube in _saveData.cubes)
    {
        PlaceCube(savedCube.position.ToVector3(), savedCube.rotation.ToQuaternion(), savedCube.color.ToColor());
    }
}
  1. Save cube data when a new cube is placed by the ^OnCubeButtonClick()^ method:
private void OnCubeButtonClick()
{
    ...
    PlaceCube(cube.transform.position, cube.transform.rotation, color);
    ~~ 
    // Save the position and rotation information locally
    _saveData.cubes.Add(new CubeData(cube.transform.position, cube.transform.rotation, color));
    SaveLocally(); 
    ~~
}
  1. Finally, call ^LoadLocally()^ in the ^OnLighthouseTracked()^ method after the user calibrates into a domain:
private void OnLighthouseTracked(Lighthouse lighthouse, Pose qrPose, bool isCalibrationGood)
{
    ...
        if(!_calibrated)
        {
            _calibrated = true;
            calibrateUI.SetActive(false);
            cube.SetActive(true);
            ~~
            LoadLocally(); 
            ~~
        }
    }
}

Assign references in Unity

The last step is to assign the references to the AR Camera, Cube, Calibrate UI, Raycast Manager, and Create Cube button in the Unity Editor by dragging and dropping the GameObjects into the script's corresponding serialized fields.

Now when you build and run the project, you should be able to calibrate into a domain, place cubes in it, and have them persist across sessions.

Need a refresher on the essentials?

Check out more lessons, DIY kits and essentials reading material at the developer learning centre homepage.

ポーズメッシュを構築するのにサポートが必要ですか?

プロジェクトをスタートさせるためにAUKIトークンの助成金を申請し、Auki Labsチームと直接連携して、あなたのクリエイションをマーケットへ。選ばれた申請者は最大10万米ドル相当のAUKIトークンの助成を受け、アウキラボチームによる開発、マーケティング支援を受けることができます。