LoginSignup
0

posted at

UnityでARスタンプラリーアプリを作ってみた

ARスタンプラリーアプリとは何か?

ARスタンプラリーアプリとはAR機能を使ったスタンプラリーを行うアプリです。
典型的なスタンプラリーの形式は

  1. スタンプカードを持ったユーザーが特定の場所へ行く
  2. その場所にあるスタンプを自身のスタンプカードに押すことでポイントを取得する
  3. スタンプカードにスタンプを集めることでポイントを貯める

だと思います。それに対してARスタンプラリーアプリでは、

  1. スマホを持ったユーザーが特定の画像をカメラで写す
  2. カメラが画像を認識することで特定の3Dモデルが表示され、そのモデルをタッチすることでポイントを取得する
  3. 取得したポイントはユーザーごとにWebサーバーで貯められ、スマホから確認することができる

という機能になっています。
両者の比較を図にするとこんな感じです。
AR Stamp Rally Image.png

作ったもの

下の映像は今回私が作成したARスタンプラリーアプリのデモです。
デモでは以下の3つを実行しています。(デモが分かりにくくてすいません)

  1. ユーザーを新規登録する
  2. PCの画像をカメラに写すことで3Dモデルが表示される(左の桃のようなキャラクターは自分の大学のマスコットキャラクターを模して作りました)
  3. 3Dモデルをタッチすることで、モデルのアニメーションと共に右上のユーザーのポイントを表すPointが0から1に加算される

この記事で知れること

この記事では以下の二つについて紹介していきます。

  • Unityでスマートフォンからオブジェクトを表示するスクリプトの書き方
  • UnityでAPI連携をするスクリプトの書き方

バックエンドの処理やモデルのアニメーションについてはWebサイトなどで簡単に知れることであり、この記事で紹介すると記事自体が長くなってしまうため省きます。

実装について

開発&動作環境

ツール  バージョン
Unity 2021.3.6f1
Android 10

構成図

アプリの簡単な構成図は以下の通りです。
AR production image.png

前提条件

UnityでARを行うのに必要なAR FoundationのインストールやHierarchyにAR SessionAR Session Originの追加の方法はネットで調べればたくさん出てきますのでこの記事では触れません。もし探すのが面倒な方は以下のサイトを参考にしてみてください。また、現在Unity Hubからプロジェクトを作る際にAR テンプレートを選択することですぐにARが始められるようになっていますので、今回私は使用していませんがAR テンプレートを使ってAR環境を整えるのもアリです。

ARでオブジェクトを表示する機能のスクリプト

スクリプトを書く前準備

  1. Asset内で左クリック→Create→XR→Reference Image Libraryを選択して、Reference Image Libraryを作成する
  2. Hierarchy内のAR Session Originを右クリックし、先ほど作成したReference Image Libraryadd component
  3. Reference Image Libraryに認識したい画像を追加(画像によっては使用できないものもあります。使用できる画像の条件についてはこちらの記事を参考にしてみてください)
  4. ARでオブジェクトを表示する機能のスクリプトが書かれたファイルをAR Session Originadd component
  5. 追加したコンポーネントにはPlaceable Prefabsというプロパティがあるので、そこに認識したい画像と同じ名前を持つ3Dモデルを追加
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

[RequireComponent(typeof(ARTrackedImageManager))]
public class ImageTracking : MonoBehaviour // 名前は何でも良いよ
{
    [SerializeField]
    private GameObject[] placeablePrefabs;

    private Dictionary<string, GameObject> spawnedPrefabs = new Dictionary<string, GameObject>();
    private ARTrackedImageManager trackedImageManager;

    // すべての前準備
    private void Awake()
    {
        trackedImageManager = FindObjectOfType<ARTrackedImageManager>();

        foreach (GameObject prefab in placeablePrefabs)
        {
            GameObject newPrefab = Instantiate(prefab);
            spawnedPrefabs.Add(prefab.name, newPrefab);
            newPrefab.SetActive(false);
        }

        trackedImageManager.trackedImagesChanged += ImageChanged;
    }

    // 3Dモデルを表示するイベントを解除
    private void OnDisable()
    {
        trackedImageManager.trackedImagesChanged -= ImageChanged;
    }

    // 3Dモデルの表示
    private void ImageChanged(ARTrackedImagesChangedEventArgs eventArgs)
    {
        foreach (ARTrackedImage trackedImage in eventArgs.added)
        {
            UpdateImage(trackedImage);
        }

        foreach (ARTrackedImage trackedImage in eventArgs.updated)
        {
            UpdateImage(trackedImage);
        }

        foreach (ARTrackedImage trackedImage in eventArgs.removed)
        {
            spawnedPrefabs[trackedImage.name].SetActive(false);
        }
    }

    // 表示される画像によって表示するオブジェクトを変更させる
    private void UpdateImage(ARTrackedImage trackedImage)
    {
        string name = trackedImage.referenceImage.name;
        var imageMarkerTransform = trackedImage.transform;

        GameObject prefab = spawnedPrefabs[name];
        var markerRotation = imageMarkerTransform.rotation * Quaternion.Euler(0f, 0f, 180f);
        prefab.transform.SetPositionAndRotation(imageMarkerTransform.position, markerRotation);
        prefab.transform.SetParent(imageMarkerTransform);
    
        prefab.SetActive(trackedImage.trackingState == TrackingState.Tracking);
    }
}

スクリプトを書く上で参考にした記事

どのような処理が行われているのかをメソッドごとに見ていきます。

宣言部分

placeablePrefabsにはスクリプトを書く前準備の5で追加した3Dモデルが格納されています。
spawnedPrefabsは表示する3Dモデルの名前をキー、3Dモデルのオブジェクトを値とする配列で、Awakeメソッドで使用します。

Awake

このメソッドでは3Dモデルを表示する準備を行っています。
行っていることは以下の2つです。

  • PlacePrefabsで格納した3Dモデルをコピーしたオブジェクトを生成し、spawnedPrefabsに3Dモデルの名前をキーとして格納します。
  • trackedImageManagerにあらかじめセットされているtrackedImagesChangedイベントに後に紹介するImageChangedメソッドを登録します。trackedImagesChangedイベントとはスマホに画像が新たに写る時・別の画像が映る時・画像が映らなくなった時に発火し、イベントの結果をImageChangedメソッドに引数として渡します。

参考

OnDisable

AwakeメソッドでtrackedImagesChangedイベントに登録したImageChangedメソッドを解除します。今回は一つのイベントに一つのメソッドしか登録していないので解除をしなくても動作するのですが、もし一つのイベントに複数のメソッドを登録しなくてはいけない時、解除をしないと参照されないメソッドが出てきてしまうので、本番を意識してイベントの解除処理を書いています。
ちなみにOnDisableメソッドはUnityのスクリプトのライフサイクルにおける最後の方に実行されるメソッドです。

参考

ImageChanged

このメソッドでは3Dモデルの表示の実行を行っています。
trackedImagesChangedイベントの登録時に受け取ったARTrackedImagesChangedEventArgsの値によって実行内容を切り替えています。ARTrackedImagesChangedEventArgsの値は以下の三つがあり、発火するイベント内容によってそれぞれの値はList<ARTrackedImage>を保持します。(ARTrackedImageオブジェクトは認識する画像の情報を含んでいます。)

1 2
added 画面上で画像の追加があった時にその画像の情報を保持する
updated 画面上で画像の変更があった時にその画像の情報を保持する
removed 画面上で映らなくなった画像があった時にその画像の情報を保持する

addedupdatedの場合はtrackedImagesChangedイベントの切り替えによるARTrackedImagesChangedEventArgsの値の要素(ARTrackedImageオブジェクト)をUpdateImageメソッドに渡し、removedの場合は3Dモデルを見えなくします。(このアプリではupdatedの場合の処理しか実行されなかったので、addedremovedがどのときに画像の情報を保持するタイミングは公式資料などから自分なりに読み取った内容をただ載せているだけで、確証はありません。)

参考

UpdateImage

このメソッドでは3Dモデルの表示の実装をしています。
行っていることは以下の3つです。

  • ARTrackedImageオブジェクトに含まれる画像の名前・位置・向きを取得します。
  • 画像の情報をもとに表示する3Dモデルの表示位置を調整します。
  • 画像が追跡可能な範囲まで画面にはっきりと表示されている場合、3Dモデルを表示する。

参考

ユーザーの動作をバックエンドに伝えるスクリプト

今回紹介するのは画像を認識することによって表示された3Dモデルをユーザーがタッチした時にユーザーのポイントを追加するスクリプトです。

スクリプトを書く前準備

  • このスクリプトが書かれたファイルを表示する3Dモデル(プレハブ)にadd component
  • 追加したコンポーネントにTarget Rendererというプロパティが表示されるので、3Dモデルのパーツの一つをそこへ追加
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;

    public class TouchTranstion : MonoBehaviour // 名前は何でも良いよ
{
    [SerializeField] Renderer targetRenderer;
    
    // オブジェクトが表示時の処理の実行
    void Update()
    {
        if (targetRenderer.isVisible && Input.touchCount > 0)
        {
           StartCoroutine(IncrementPointApi()); 
        }
    }

// ポイントの追加
    IEnumerator IncrementPointApi()
    {
        string inputText = PlayerPrefs.GetString("Name");
        WWWForm formData = new WWWForm();
        formData.AddField("name", inputText);

        // usingを使用する理由はメモリリーク対策
        using UnityWebRequest www = UnityWebRequest.Post("API(URL)", formData);
        yield return www.SendWebRequest();

        if (www.result == UnityWebRequest.Result.ConnectionError || www.result == UnityWebRequest.Result.ProtocolError)
        {
            // API通信が失敗した時の処理
        }
        else
        {
            // API通信が成功した時の処理
        }
    }
}

宣言部分

3Dモデルが表示されているかの判定用に使用するtargetRendererを用意しています。targetRendererには前準備で追加した3Dモデルのパーツの一部(今回は胴体パーツ)がMesh Renderとして格納されています。

Update

このメソッドでは、targetRendererで格納したパーツが表示されるかつユーザーが画面にタッチした時にユーザーのポイントを追加するIncrementPointApiメソッドを実行しています。StartCoroutineを使用してIncrementPointApiメソッドを実行する理由は、StartCoroutineがないとUnityWebRequestクラスを利用したAPI通信が伴うメソッドの実行ができないからです。
targetRendererが表示されているかの判断はRendererクラスの変数isVisibleを使用します。

参考

IncrementPointApi

このメソッドで行っていることは以下の三つです

  • WWWFormクラスを使用してバックエンドへ送るデータの作成します。WWWFormAddFieldメソッドの第一引数にフィールド名、第二引数に値を入れることでフォームデータの追加ができます。
  • UnityWebRequestクラスを使用し、APIを通してバックエンドへデータを送信します。UnityWebRequestクラスのPostメソッドの第一引数にAPI、第二引数にフォームデータを入れることでUnityWebRequestオブジェクトをHTTPのPOSTが行える状態にし、SendWebRequestメソッドを実行することでバックエンドとの通信を行っています。UnityWebRequestオブジェクト生成時にusingを書いている理由は、これがないとメモリリークでエラーが生じてしまうからです。また、SendWebRequestメソッドにyieldを書く理由は、バックエンドこれがないとバックエンドの結果を取得できないからです。
  • API通信が成功or失敗した際の処理の実行をします。今回の処理ではバックエンドとの通信エラーが起きるか、バックエンドから4xxor5xxエラーが返されたら失敗の処理が行われます。

参考

終わりに

UnityとC#について筆者が初心者であることから、処理の説明がところどころいい加減になってしまいました。
今後UnityとC#の学習を深めることで、両者の理解を深めるとともにARスタンプラリーアプリの完成度を高めていきたいです!

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
0