クイック エンジニアリングブログ

株式会社クイック Web事業企画開発本部のエンジニアリングチームが運営する技術ブログです。

Unity初心者がiPhone用の花粉チェッカー作ってみた

どうも!じゃがいもです!  最近花粉がひどいですね、、、鼻水がノンストップです。 今回はそんな憎き花粉情報が見れるアプリを「Unity」で作ってみようと思います。 せっかくなので、最近流行りのChatGPTの力も借りながら、1日でどこまで作れるかチャレンジしてみます。

環境

Macbook Pro 2023(Mac OS 13.6)
Unity Hub 3.7.0
Unity Editor 2022.3.20f1

目次

  • アプリの概要、使用技術
  • Unityのインストール(本記事では説明を省きます)
  • Projectの設定
  • ゲーム作成
  • 最後に

アプリの概要、使用技術

「概要」 アプリを起動し、中央にいるペンギンがタップされるのをトリガーとして、現在地の取得を行い、Pollen APIを実行し、花粉の種類と強さの結果を表示します。

「使用技術」 ・Unity ・Google Maps Platform Pollen API Pollen API を使用すると、特定の場所の花粉データをリクエストできます。花粉データには、地域の植物種と花粉の種類、花粉飛散量指数と健康に関する推奨事項が含まれます。Pollen API は 65 か国以上をカバーしています(公式ガイドから抜粋) https://developers.google.com/maps/documentation/pollen/overview

Projectの設定

1, New projectから2D Mobileを選択する

2, Build SettingからiOSを選択し、「Install with Unity Hub」を選択する

3, iOS Build Supportを選択してInstallを行う

4, Unityを起動し直し、Build SettingからiOSを選択し、「Switch Platform」を選択する

5, Gameウィンドウの上部から、適当なiPhoneを選ぶ 今回は縦なので「iPhone 12 Pro Max」の「Portrait」を選択する

6, Canvasをシーン内に追加し、Inspector内の「Canvas Scaler」を以下のように設定する

UI Scale Mode: Scale With Screen Size
Reference Resolution X: 1920
Reference Resolution Y: 1080
Match: Height(1)

ゲーム作成

ここまででiPhoneゲームの作成が行える環境が整いました。 ここからゲーム本体の作成を行なっていきます。

1, ゲームに登場するキャラクターを選択する 今回は「2D Character Sprite Animation - Penguin」を選択しました。アニメーションが付いていてとてもかわいいです

2, ペンギンをゲーム内に追加する

3, 「アプリのタイトル」と「TapMe!」というテキストを追加する これらは最初に作成したCanvasの中に入れる 適当に位置を調整し、見た目を以下のように整える (花粉っぽさを出すために、タイトルの横に画像を入れてみました)

4, スクリプトの適用 ペンギンがタップされたら、「現在地の取得」「Pollen APIの実行」「キャラクターのアニメーション変更」「テキストに結果の表示」を行うことができるスクリプトを追加する

今回スクリプトの名前は「LocationBasedInfoFetcher」とします

GPTの力も借りて以下のスクリプトを作成しました

using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using UnityEngine.Networking;
public class LocationBasedInfoFetcher : MonoBehaviour
{
    public RuntimeAnimatorController walkAnimatorController;
    private Animator animator; 

    void Start()
    {
        animator = GetComponent<Animator>();
    }

    void Update()
    {
        // PCの場合のマウスクリックを検出
        if (Input.GetMouseButtonDown(0)) // 0は左クリック
        {
            CheckTouch(Input.mousePosition);
        }

        // スマートフォンのタッチを検出
        if (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Began)
        {
            CheckTouch(Input.GetTouch(0).position);
        }
    }

    void CheckTouch(Vector2 position)
    {
        // スクリーン座標からレイキャストを行い、タッチまたはクリックされたオブジェクトを検出
        RaycastHit2D hit = Physics2D.Raycast(Camera.main.ScreenToWorldPoint(position), Vector2.zero);

        // ヒットしたオブジェクトがこのスクリプトを持つオブジェクトであるか確認
        if (hit.collider != null && hit.collider.gameObject == this.gameObject)
        {
            Debug.Log("Object tapped!");
            StartCoroutine(FetchLocation()); // 位置情報を取得するコルーチンを開始
            UpdateText(); // タグが"TapMeTitle"のテキストオブジェクトのテキストを更新
        }
    }

    IEnumerator FetchLocation()
    {
        float latitude;
        float longitude;

        ChangeAnimatorControllerToWalk();

        #if UNITY_EDITOR || UNITY_STANDALONE
        // PCまたはUnityエディターで実行されている場合、固定の位置情報を使用(アナハイムディズニー)
        latitude = 33.814037796759294f;
        longitude = -117.91901816207327f;

        Debug.Log($"Using fixed location: {latitude}, {longitude}");
        #else
        // 位置情報サービスが有効になっているか確認
        if (!Input.location.isEnabledByUser)
        {
            Debug.Log("Location services are not enabled by user.");
            yield break;
        }

        // 位置情報サービスを開始
        Input.location.Start();

        // 初期化が完了するまで待機
        int maxWait = 20; // 最大待機時間(秒)
        while (Input.location.status == LocationServiceStatus.Initializing && maxWait > 0)
        {
            yield return new WaitForSeconds(1);
            maxWait--;
        }

        // 初期化が20秒以内に完了しない場合
        if (maxWait < 1)
        {
            Debug.Log("Timed out");
            yield break;
        }

        // 位置情報サービスが失敗した場合
        if (Input.location.status == LocationServiceStatus.Failed)
        {
            Debug.Log("Unable to determine device location");
            yield break;
        }
        else
        {
            // 位置情報サービスが成功し、位置情報を取得
            latitude = Input.location.lastData.latitude;
            longitude = Input.location.lastData.longitude;
            Debug.Log($"Location: {latitude}, {longitude}");
        }

        // 位置情報サービスを停止
        Input.location.Stop();
        #endif

        // APIリクエストのURLを構築
        string apiKey = ""; // 実際のAPIキーに置き換える
        string url = $"https://pollen.googleapis.com/v1/forecast:lookup?key={apiKey}&location.latitude={latitude}&location.longitude={longitude}&days=1";

        // コルーチンを開始してAPIリクエストを送信
        yield return StartCoroutine(SendApiRequest(url));
    }

    IEnumerator SendApiRequest(string url)
    {
        using (UnityWebRequest webRequest = UnityWebRequest.Get(url))
        {
            yield return webRequest.SendWebRequest();

            if (webRequest.isNetworkError || webRequest.isHttpError)
            {
                Debug.LogError("Error: " + webRequest.error);
                UpdateText(true); // 失敗を示すために引数にtrueを渡す
            }
            else
            {
                // レスポンスをログに出力
                Debug.Log("Response: " + webRequest.downloadHandler.text);

                // APIレスポンスを解析してテキストを更新
                UpdateTextWithPollenInfo(webRequest.downloadHandler.text);

                // アニメーターコントローラーを「penguin_idle_01」に変更
                ChangeAnimatorControllerToIdle();
            }
        }
    }

    void UpdateText(bool failed = false)
    {
        // タグが"TapMeTitle"のテキストオブジェクトを検索
        GameObject textObject = GameObject.FindGameObjectWithTag("TapMeTitle");
        if (textObject != null)
        {
            // テキストコンポーネントを取得
            Text textComponent = textObject.GetComponent<Text>();
            if (textComponent != null)
            {
                // 失敗した場合は失敗メッセージを表示、そうでなければ通常のメッセージを表示
                textComponent.text = failed ? "取得に失敗しました" : "現在地の花粉情報\n取得中だよ";
            }
            else
            {
                Debug.LogWarning("Text component not found on the object with tag 'TapMeTitle'.");
            }
        }
        else
        {
            Debug.LogWarning("Object with tag 'TapMeTitle' not found.");
        }
    }

    void UpdateTextWithPollenInfo(string json)
    {
        var pollenData = JsonUtility.FromJson<PollenData>(json);
        bool hasCategoryInfo = false;
        string displayText = "";

        foreach (var dailyInfo in pollenData.dailyInfo)
        {
            foreach (var pollenInfo in dailyInfo.pollenTypeInfo)
            {
                if (!string.IsNullOrEmpty(pollenInfo.indexInfo.category))
                {
                    hasCategoryInfo = true;
                    displayText += $"{pollenInfo.displayName}: {pollenInfo.indexInfo.category}\n";
                }
            }
        }

        if (!hasCategoryInfo)
        {
            displayText = "現在地に取得できる花粉情報がありませんでした";
        }

        // テキストオブジェクトを更新
        GameObject textObject = GameObject.FindGameObjectWithTag("TapMeTitle");
        if (textObject != null)
        {
            Text textComponent = textObject.GetComponent<Text>();
            if (textComponent != null)
            {
                textComponent.text = displayText.TrimEnd('\n'); // 末尾の改行を削除
            }
        }
    }


    void ChangeAnimatorControllerToIdle()
    {
        RuntimeAnimatorController idleController = Resources.Load<RuntimeAnimatorController>("penguin_idle_01");
        if (animator != null && idleController != null)
        {
            animator.runtimeAnimatorController = idleController;
        }
    }

    void ChangeAnimatorControllerToWalk()
    {
        if (walkAnimatorController != null)
        {
            animator.runtimeAnimatorController = walkAnimatorController;
        }
        else
        {
            Debug.LogWarning("Walk animator controller not set or not found.");
        }
    }
}

[System.Serializable]
public class PollenData
{
    public DailyInfo[] dailyInfo;
}

[System.Serializable]
public class DailyInfo
{
    public PollenTypeInfo[] pollenTypeInfo;
}

[System.Serializable]
public class PollenTypeInfo
{
    public string displayName;
    public IndexInfo indexInfo;
}

[System.Serializable]
public class IndexInfo
{
    public string category;
}

5, スクリプトに合わせて各々設定 ・タップ後にデフォルトのアニメーションをScriptに設定

・ペンギンをタップできるようにするため「Box Collider 2D」を追加

・TapMeのテキストに対して、「Tag」を設定。名前は「TapMeTitle」 →コード内から参照できるようにするため

6, Google Maps Platform設定 Google Maps Platformにログインし、「Pollen API」を有効にし、API Keyを発行します API Keyはコード内の「apiKey」変数に代入します

7, いざ実行 ここまで行うと実行できる状態になります

PCの場合、現在地の取得が行えないため、アナハイムにあるディズニーの緯度経度を固定値として入れています (日本の緯度経度にしなかったのは、Pollen APIが日本の花粉情報を芝生しかカバーできていないため、、、)

出力する情報は「花粉名: 強さ」としています。複数出力にも対応させています

エラーが発生した場合は「取得に失敗しました」と表示されるようにしました

最後に

Pollen APIが日本の花粉にほとんど対応できていないのは予想外でした(芝生の花粉にしか対応していない。芝生って花粉あったんだ・・・)

そして、半日程度でここまで作成できたのは「GPT」の力が大きいです。生成してくれるコードは完璧ではなく、エラーが発生したり求めるものとはズレたものを出力してきたりもしますが、プロトタイプ程度であればほぼほぼ修正の必要がないものを出してくれます(完成したコードは綺麗ではありませんが、ChatGptを使えばリファクタリングも一瞬で行うことができます)

クイックのエンジニア組織では「Github Copilot」の導入を進めている最中で、業務でもAIの力を使って効率よく開発していけたらと考えています。とてもワクワクしますね!

以上ここまでお読みいただきありがとうございました。


\\『真のユーザーファーストでマーケットを創造する』仲間を募集中です!! //

919.jp