Addressables.CheckForCatalogUpdates() で返ってくるリストが常に空になる問題

発生した問題

サーバーに置いてある Addressables のコンテンツカタログをアップデートしてから Addressables.CheckForCatalogUpdates() を実行しても、常に空のリストしか返ってこないという問題に遭遇しました。

解決方法

先に Addressables.InitializeAsync() を明示的に実行して、初期化が完了するのを待ってから Addressables.CheckForCatalogUpdates() を呼ぶ。

説明

Addressables の初期化は、Addressables API を初めて叩いたときに自動的に行われるので、普通はわざわざ Addressables.InitializeAsync() を実行する必要はありません。

なので、いきなり Addressables.CheckForCatalogUpdates() を呼んでもエラーにはなりません。

しかし、この時点では Addressables の初期化はまだ「完了」はしていないので、カタログの更新のチェックを行うことができないようです。

エラーにはならないが、カタログ更新のチェックも行えないので、結果として空のリストが返ってくる……ということのようです。

普通にエラーを吐いてくれたほうがありがたいんですが、どうしてこういう仕様になっているんですかね……。

補遺

なお、Addressables の設定の「Disable Catalog Update on Startup」が OFF の場合*1、Addressables の初期化時に自動でカタログのアップデートを行うようになるので、この場合も Addressables.CheckForCatalogUpdates() の返り値は常に空のリストになります。

f:id:Gigacee:20211222175947p:plain

参考

forum.unity.com

*1:デフォルトでは OFF になっています。

パッケージを一括アップデートする

Unity の Package Manager でインストールしたパッケージを一括アップデートする方法です。

  1. manifest.json から、アップデートしたいパッケージの行を削除する。
  2. Unity Editor に戻る。
    1. おそらくコンソールにエラーが出ると思いますが気にせず次へ。
  3. manifest.json の削除した行を元に戻す。
  4. 再び Unity Editor に戻る。

これで、一旦削除したパッケージが最新のものになります。

※ 元々バージョンを指定してインストールしていた場合はアップデートされません。

Unity でサービスロケーターを使う

2023.02.20 追記

本記事で紹介しているスクリプトを発展させ、Package Manager から導入することができるサービスロケーターのパッケージを公開しています。

blog.gigacreation.jp

はじめに

例えば GameManager のような、どこからでもアクセスしたい・一つだけ存在していてほしいクラスを Unity で実装する場合、よく使われるのは【シングルトン】というデザインパターンです。

シングルトンは非常にシンプルでお手軽に実装でき、小規模なプロジェクトでは大変便利なものではありますが、LevelManager とか SaveLoadManager とか LanguageManager とかといった感じでマネージャークラスが増えてくると、いろんなシングルトンがいろんなクラスから好き勝手にアクセスされるようになり、クラス間の依存関係がぐちゃぐちゃになってしまいがちです。*1

そこで登場するのが【サービスロケーター】で、簡単に言うと、

  • どこからでもアクセスできる「格納場所*2」を一つだけ用意し、
  • GameManager や LevelManager などをそこに登録し、
  • 他のクラスからは GameManager などに直接アクセスするのではなく、「格納場所」を経由してアクセスするようにする

という代物です。こうすることで、クラス間の依存を「格納場所」に集約でき、依存関係が追いやすくなるというわけです。

(👇この解説動画が分かりやすかったです)

www.youtube.com

実装

それでは実装です。

サービスロケーター本体のクラス

まず、サービスロケーター本体のスクリプトは以下のようになります。

using System;
using System.Collections.Generic;
using UnityEngine;

public static class ServiceLocator
{
    /// <summary>
    /// インスタンスを登録する辞書。
    /// </summary>
    static readonly Dictionary<Type, object> instances = new Dictionary<Type, object>();

    /// <summary>
    /// インスタンスの登録をすべて解除します。
    /// </summary>
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    static void Initialize()
    {
        instances.Clear();
    }

    /// <summary>
    /// インスタンスを登録します。すでに同じ型のインスタンスが登録されている場合は登録できませんので、先に Unregister を行ってください。
    /// </summary>
    /// <param name="instance">登録するインスタンス。</param>
    /// <typeparam name="T">登録するインスタンスの型。</typeparam>
    public static void Register<T>(T instance) where T : class
    {
        var type = typeof(T);

        if (instances.ContainsKey(type))
        {
            Debug.LogWarning($"すでに同じ型のインスタンスが登録されています:{type.Name}");
            return;
        }

        instances[type] = instance;
    }

    /// <summary>
    /// インスタンスの登録を解除します。インスタンスが登録されていなかった場合は警告が出ます。
    /// </summary>
    /// <param name="instance">登録を解除するインスタンス。</param>
    /// <typeparam name="T">登録を解除するインスタンスの型。</typeparam>
    public static void Unregister<T>(T instance) where T : class
    {
        var type = typeof(T);

        if (!instances.ContainsKey(type))
        {
            Debug.LogWarning($"要求された型のインスタンスが登録されていません:{type.Name}");
            return;
        }

        if (!Equals(instances[type], instance))
        {
            Debug.LogWarning($"登録されている要求された型のインスタンスと渡されたインスタンスが一致しません:{type.Name}");
            return;
        }

        instances.Remove(type);
    }

    /// <summary>
    /// 指定された型のインスタンスがすでに登録されているかをチェックします。
    /// </summary>
    /// <typeparam name="T">登録を確認するインスタンスの型。</typeparam>
    /// <returns>指定された型のインスタンスがすでに登録されている場合は true を返します。</returns>
    public static bool IsRegistered<T>() where T : class
    {
        return instances.ContainsKey(typeof(T));
    }

    /// <summary>
    /// 渡されたインスタンスがすでに登録されているかをチェックします。
    /// </summary>
    /// <param name="instance">登録を確認するインスタンス。</param>
    /// <typeparam name="T">登録を確認するインスタンスの型。</typeparam>
    /// <returns>渡されたインスタンスが既に登録されている場合は true を返します。</returns>
    public static bool IsRegistered<T>(T instance) where T : class
    {
        var type = typeof(T);

        return instances.ContainsKey(type) && Equals(instances[type], instance);
    }

    /// <summary>
    /// インスタンスを取得します。取得できなかった場合はエラーになります。
    /// </summary>
    /// <typeparam name="T">取得したいインスタンスの型。</typeparam>
    /// <returns>取得したインスタンスを返します。取得できなかった場合は null を返します。</returns>
    public static T GetInstance<T>() where T : class
    {
        var type = typeof(T);

        if (instances.ContainsKey(type))
        {
            return instances[type] as T;
        }

        Debug.LogError($"要求された型のインスタンスが登録されていません:{type.Name}");
        return null;
    }

    /// <summary>
    /// インスタンスを取得し、渡された引数に代入します。取得できなかった場合は null が入ります。
    /// </summary>
    /// <param name="instance">取得したインスタンスを入れる変数。</param>
    /// <typeparam name="T">取得したいインスタンスの型。</typeparam>
    /// <returns>取得が成功したら true を返します。</returns>
    public static bool TryGetInstance<T>(out T instance) where T : class
    {
        var type = typeof(T);

        instance = instances.ContainsKey(type) ? instances[type] as T : null;

        return instance != null;
    }
}

Dictionary<Type, object> instances が「格納場所」で、ここに GameManager などを詰めていきます。

サービスロケーターに格納するクラス(各種マネージャーなど)

お次は、GameManager などといった外部からアクセスしたいクラスを、サービスロケーターに登録する手順です。

using UnityEngine;

/// <summary>
/// マネージャークラス。
/// </summary>
public class GameManager : MonoBehaviour, IGameManager
{
    /// <summary>
    /// 外部から使いたいメソッド。
    /// </summary>
    public void DoSomething()
    {
        Debug.Log("I am GameManager!");
    }

    void Awake()
    {
        // サービスロケーターに自身を登録
        ServiceLocator.Register<IGameManager>(this);
    }

    void OnDestroy()
    {
        // サービスロケーターから自身の登録を解除
        ServiceLocator.Unregister<IGameManager>(this);
    }
}

/// <summary>
/// マネージャークラスのインターフェイス。
/// </summary>
public interface IGameManager
{
    /// <summary>
    /// 外部から使いたいメソッド。
    /// </summary>
    void DoSomething();
}

登録する型が GameManager ではなく IGameManager というインターフェイスになっていますが、こうすることで外部からアクセスできるメソッドを限定することができるので、なるべくインターフェイスを定義して実装するのをおすすめします。

この例だと DoSomething() しかないので恩恵を感じられませんが、実際にはいろいろなメソッドやプロパティが実装されるはずなので、アクセス可能な対象を限定しておくことは決して悪いことではありません。

外部からサービスロケーターにアクセスするクラス

最後に、サービスロケーターへアクセスする外部のクラスです。

using UnityEngine;

public class Foo : MonoBehaviour
{
    void Start()
    {
        var gameManager = ServiceLocator.GetInstance<IGameManager>();

        gameManager.DoSomething();
    }
}

ServiceLocator.GetInstance<IGameManager>() で、格納された GameManager を取得できます。取得できたら、あとは普通にメソッドを実行することができます✨

いちいち GetInstance でインスタンスを取得しないといけないのが面倒に感じるかもしれませんが、逆に言うと GetInstance が無い場合はマネージャークラスとは無関係だということが保証されるので、スクリプトの理解がしやすくなります。

なお、GameManager の登録は Awake() で行うので、他のクラスからは Start() 以降にアクセスする必要があります。

補遺

インスタンスを一つのみに制限する

さて、サービスロケーターを使うことで「どこからでもアクセスしたい」は実現できましたが、「一つだけ存在していてほしい」は実現できていません。

これは、「サービスロケーターへの登録時に、すでに同じ型のインスタンスが登録されていたら自身を破棄する」という処理を追加することで実現できます。

具体的にはこのように書きます。

public class GameManager : MonoBehaviour, IGameManager
{
    public void DoSomething()
    {
        Debug.Log("I am GameManager!");
    }

    void Awake()
    {
        // すでに IGameManager がサービスロケーターに登録されていたら、自身を破棄して終了
        if (ServiceLocator.IsRegistered<IGameManager>())
        {
            Destroy(gameObject);
            return;
        }

        ServiceLocator.Register<IGameManager>(this);
    }

    void OnDestroy()
    {
        ServiceLocator.Unregister<IGameManager>(this);
    }
}

GetInstance と TryGetInstance

今回紹介したスクリプトでは、 GetInstance でインスタンスを取得できなかった場合はエラーになるようにしています。

Unity でよく使われる GetComponent などは取得に失敗してもエラーにならないので、それに倣ったほうが良いという意見もあるとは思うのですが、自分のコーディングだと、

  • 基本的に GetComponent は必ずそのコンポーネントを取得できる想定の時しか使わない
  • 取得できなかった場合も処理を続行したい場合は TryGetComponent を使う

という理由から、今回はエラーになる仕様を採用しました。なお、TryGetComponent に相当するメソッドとして TryGetInstance を用意しています。

Awake と Start

記事中にも書きましたが、サービスロケーターへの登録は Awake() で行っているので、アクセスは Start() 以降でないとできません。

これが嫌だという方もいらっしゃるかもしれませんが、 Awake() の時点では他のオブジェクトの初期化が完了していない場合があるので、今回の例に限らず、そもそも Awake() 内で他のオブジェクトに働きかけるのは避けたほうが良いでしょう。

おわりに

サービスロケーターを使えば、依存関係の複雑化を回避しつつ、どこからでもアクセスできるクラスを作ることができます。サービスロケーターを使いこなして、シングルトンを駆逐していきましょう!

*1:そもそもこんなに「○○マネージャー」を作るの自体が良くない? それはそう🙂

*2:Dictionary 型が使われることが多い気がします。

ルートに変更された Prefab があるかどうかを調べるエディター拡張

Prefab の Apply や Revert をし忘れていないかいつも心配になるので、それをチェックするエディター拡張を作成しました。

自分のワークフローだとルートに存在している Prefab だけをチェックするようにしたほうが都合が良かったのでそうしていますが、子孫含めたすべての Prefab をチェックしたい場合は、rootGameObjects = ...の部分を適宜書き換えてください。

using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.Experimental.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;

public static class GcTools
{
    [MenuItem("GcTools/Check if Root Prefabs have Changed")]
    public static void CheckIfRootPrefabsHaveChanged()
    {
        IEnumerable<GameObject> rootGameObjects;

        var currentPrefabStage = PrefabStageUtility.GetCurrentPrefabStage();

        if (currentPrefabStage == null)
        {
            // Prefab Mode でない場合、シーンのルートにある GameObject を取得
            rootGameObjects = SceneManager.GetActiveScene().GetRootGameObjects();
        }
        else
        {
            // Prefab Mode の場合、ルートの Prefab の 1 階層下にある GameObject を取得 
            rootGameObjects = currentPrefabStage.prefabContentsRoot.transform.Cast<Transform>()
                .Select(child => child.gameObject);
        }

        // 変更されている Prefab Instance を抽出
        var overriddenPrefabInstances = rootGameObjects
            .Where(PrefabUtility.IsAnyPrefabInstanceRoot)
            .Where(x => PrefabUtility.HasPrefabInstanceAnyOverrides(x, false))
            .Select(x => (Object)x)
            .ToArray();

        foreach (var instance in overriddenPrefabInstances)
        {
            Debug.Log($"ルートの Prefab が変更されています:{instance}");
        }

        if (!overriddenPrefabInstances.Any())
        {
            Debug.Log("ルートに変更された Prefab はありませんでした。");
        }

        // 変更されている Prefab Instance が存在していたら、それらを選択
        Selection.objects = overriddenPrefabInstances;
    }
}

Unity WebGL の実行環境が PC かモバイル端末かを判別するスクリプトを公開しました

PC モバイル

はじめに

  • PC では Post Processing を有効にしたいけど、モバイル端末では重たいので無効にしたい。
  • モバイル端末の場合はOnPointer*を切ってInput.Touchesを使いたい。

……というように、PC とモバイル端末で処理を分けたいという場合がたまにあります。そんな時、このスクリプトを導入することで、実行環境が PC かモバイル端末かを判別することができます。

github.com

使い方

CheckIfMobileForUnityWebGL.jslib内のIsMobile()を呼ぶと、モバイル端末ならtrue、PC ならfalseが返ってきます。以下のように呼び出してください。

#if !UNITY_EDITOR && UNITY_WEBGL
    [System.Runtime.InteropServices.DllImport("__Internal")]
    private static extern bool IsMobile();
#endif

    private void CheckIfMobile()
    {
        var isMobile = false;

#if !UNITY_EDITOR && UNITY_WEBGL
        isMobile = IsMobile();
#endif

        GetComponent<Text>().text = isMobile ? "Mobile" : "PC";
    }

インストール

Package Manager

https://github.com/gigacee/CheckIfMobileForUnityWebGL.git?path=Assets/Plugins/CheckIfMobileForUnityWebGL

手動

Assets/Plugins/CheckIfMobileForUnityWebGL/CheckIfMobileForUnityWebGL.jslibを、自分のプロジェクトにコピーしてください。

※ 必ずAssets/Plugins/に配置してください。でないと機能しません。

Unity のビルド時刻を表示するスクリプトを公開しました

github.com

f:id:Gigacee:20210108003920p:plain

使い方

  1. このパッケージをインポートした状態でゲームをビルドすると、ビルドが開始された時刻を int のフィールドで保持した ScriptableObject がAssets/BuildTimestampDisplay/BuildTimestamp.assetに生成されます。 f:id:Gigacee:20210108004009p:plain
  2. uGUI の Text にBuild Timestamp Displayをアタッチし*1BuildTimestamp.assetをセットします。 f:id:Gigacee:20210430001446p:plain
    • 表示フォーマットや時差を設定することもできます。日本時間の場合は「Utc Offset Hours」に「9」を入力します。
  3. そしてシーンを再生すると、Text がビルド日時に上書きされ、画面に表示されます。

インストール

Package Manager

https://github.com/Gigacee/build-timestamp-display-for-unity.git?path=Assets/BuildTimestampDisplay

手動

Assets/BuildTimestampDisplay/ を、自分のプロジェクトにコピーしてください。

*1: Add Component > Gigacee > Build Timestamp Display とクリックしてアタッチできます。

モバイルにも対応した、Unity WebGL からツイートができるスクリプトを公開しました

はじめに

先日リリースした『ねねちーのお昼ご飯大作戦ある!』では、スクリーンショット付きで結果をツイートすることができます。

f:id:Gigacee:20201004223441p:plain
こんな感じ。

そしてこのたび、ここで使用したスクリプトを切り出して、GitHub にて公開いたしました。

github.com

デモはこちら。

www.gigacreation.jp

使い方

テキストのみツイートする

TweetFromUnityWebGL.jslib 内の TweetFromUnity() を実行することでツイートができます。以下のように呼び出してください。

using UnityEngine;
#if !UNITY_EDITOR && UNITY_WEBGL
using System.Runtime.InteropServices;
#endif

public class Demo1 : MonoBehaviour
{
#if !UNITY_EDITOR && UNITY_WEBGL
    [DllImport("__Internal")]
    private static extern string TweetFromUnity(string rawMessage);
#endif

    public void Tweet()
    {
#if !UNITY_EDITOR && UNITY_WEBGL
        TweetFromUnity("Tweet Message");
#endif
    }
}

PC 環境で実行するとブラウザで twitter.com の投稿画面が開かれ、モバイル環境だと Twitter アプリが起動します。

Sample1_Tweet シーンにこの例がありますので、ご参照ください。

スクリーンショット付きでツイートする

ゲームのスクリーンショット付きでツイートすることもできます。Imgur を使用した例を以下に示します。

Imgur のクライアント ID が必要です。取得手順はこちらを参照してください。

// Original code from https://github.com/ttyyamada/TweetWithScreenShotInWebGL
// Licensed under https://github.com/ttyyamada/TweetWithScreenShotInWebGL/blob/master/LICENSE

using System;
using System.Collections;
using System.Xml.Linq;
using UnityEngine;
using UnityEngine.Networking;
#if !UNITY_EDITOR && UNITY_WEBGL
using System.Runtime.InteropServices;
#endif

public class Demo2 : MonoBehaviour
{
    [SerializeField] private string _imgurClientId;

#if !UNITY_EDITOR && UNITY_WEBGL
    [DllImport("__Internal")]
    private static extern string TweetFromUnity(string rawMessage);
#endif

    public void TweetWithScreenshot()
    {
        StartCoroutine(TweetWithScreenshotCo());
    }

    private IEnumerator TweetWithScreenshotCo()
    {
        yield return new WaitForEndOfFrame();

        Texture2D tex = ScreenCapture.CaptureScreenshotAsTexture();

        var wwwForm = new WWWForm();
        wwwForm.AddField("image", Convert.ToBase64String(tex.EncodeToJPG()));
        wwwForm.AddField("type", "base64");

        // Upload to Imgur
        UnityWebRequest www = UnityWebRequest.Post("https://api.imgur.com/3/image.xml", wwwForm);
        www.SetRequestHeader("AUTHORIZATION", "Client-ID " + _imgurClientId);

        yield return www.SendWebRequest();

        var uri = "";

        if (!www.isNetworkError)
        {
            XDocument xDoc = XDocument.Parse(www.downloadHandler.text);
            uri = xDoc.Element("data")?.Element("link")?.Value;

            // Remove Ext
            uri = uri?.Remove(uri.Length - 4, 4);
        }

#if !UNITY_EDITOR && UNITY_WEBGL
        TweetFromUnity($"Tweet Message%0a{uri}");
#endif
    }
}

Sample2_TweetWithScreenshot シーンにこの例がありますので、ご参照ください。

なお、この Imgur を使用するコードは、やまださん制作のサンプルを参考にさせていただきました。ありがとうございます😊

特殊文字

改行やハッシュタグをツイート文に含めたい場合は、以下のようにしてください。

  • 改行 : %0a
  • ハッシュタグ (#) : %23

例:

TweetFromUnity("ツイートメッセージに、改行や%0aハッシュタグを含めることもできます!%0a%0a%23TweetFromUnityWebGL");

ツイートメッセージに、改行や
ハッシュタグを含めることもできます!

#TweetFromUnityWebGL

インストール

Package Manager

https://github.com/gigacee/TweetFromUnityWebGL.git?path=Assets/Plugins/TweetFromUnityWebGL

手動

Assets/Plugins/TweetFromUnityWebGL/TweetFromUnityWebGL.jslib を、自分のプロジェクトにコピーしてください。

※ 必ず Assets/Plugins/ に配置してください。でないと機能しません。

余談:このスクリプトができるまで

Unity WebGL からツイートするための方法は、ネットで調べるといくつかの情報がヒットします。ですがその多くが、すでに廃止されている Application.ExternalEval() を使用したもので、最新の Unity で動作するようなスクリプトは見つけることができませんでした。

とはいえ、Unity WebGL から JavaScript の関数を呼び出すこと自体は、前作の『かえちゃんジャンプ!!』でやったことがあります。その応用で、Unity 内で生成した文字列を JavaScript に渡し、twitter.com を開けばツイートできるのでは? と考え、最初に書いたコードが以下のようなものです。

// TweetFromUnityWebGL.jslib

mergeInto(LibraryManager.library, {
    TweetFromUnity: function (rawMessage) {
        var message = Pointer_stringify(rawMessage);
        window.open("https://twitter.com/intent/tweet?text=" + message, "_blank");
    },
});

Pointer_stringify() は、Unity 内の文字列を JavaScript 文字列に変換するためのものらしいです。

あとは Unity 内から TweetFromUnity("ツイート文だよ~") という感じで呼び出せば、 window.open(uri) で twitter.com の投稿画面が開き、ツイートができる! 完璧!! ……と思っていたのですが、iOS の Safari でゲームを実行すると、ポップアップブロック機能が働いてしまい、投稿画面が開いてくれませんでした。かと言って、 location.href = uri で遷移すると、ゲームを実行しているタブと同じタブで投稿画面が開いてしまい、ゲームが終了してしまいます。

どうしたものか……としばらく考え、モバイル端末なら大抵 Twitter アプリが入っているはずだから、そのアプリを直接起動すれば良いのでは??? と考えました。つまり、こんなコードです。

// TweetFromUnityWebGL.jslib

mergeInto(LibraryManager.library, {
    TweetFromUnity: function (rawMessage) {
        var message = Pointer_stringify(rawMessage);
        location.href = "twitter://post?message=" + message;
    },
});

モバイル環境で twitter://post?message= を実行すると、Twitter アプリが開きます。この URI (?) を調べるのに、だいぶ苦労しました……。

ですがこれだと、今度は PC でツイート画面が開けませんから、どうにかして実行環境が PC かモバイルかを判別しなくてはなりません。これに関してはネットに豊富な情報がありましたので、苦労せず実装することができました。

// TweetFromUnityWebGL.jslib

mergeInto(LibraryManager.library, {
    TweetFromUnity: function (rawMessage) {
        var message = Pointer_stringify(rawMessage);
        var mobilePattern = /Android|iPhone|iPad|iPod/i;

        if (window.navigator.userAgent.search(mobilePattern) !== -1) {
            // Mobile
            location.href = "twitter://post?message=" + message;
        } else {
            // PC
            window.open("https://twitter.com/intent/tweet?text=" + message, "_blank");
        }
    },
});

ユーザーエージェントを取得し、その中に「Android」「iPhone」「iPad」「iPod」のいずれかが含まれていたらモバイル端末、というように判別します。これで、すべての環境*1でツイートをすることができるようになりました!🎉 めでたしめでたし😊


追記(2020.10.18)

あとから知ったのですが、iOS 13.0 以降の iPad*2だと、ユーザーエージェントに「iPad」の文字列が入ってないんですね……。そのため、上記のコードだと iPad で Twitter アプリが開きません。iPad でも Twitter アプリが開くよう、以下のように修正しました。

// TweetFromUnityWebGL.jslib

<feff>mergeInto(LibraryManager.library, {
    TweetFromUnity: function (rawMessage) {
        var message = Pointer_stringify(rawMessage);
        var mobilePattern = /android|iphone|ipad|ipod/i;

        var ua = window.navigator.userAgent.toLowerCase();

        if (ua.search(mobilePattern) !== -1 || (ua.indexOf("macintosh") !== -1 && "ontouchend" in document)) {
            // Mobile
            location.href = "twitter://post?message=" + message;
        } else {
            // PC
            window.open("https://twitter.com/intent/tweet?text=" + message, "_blank");
        }
    },
});

ua.indexOf("macintosh") !== -1 で「Macintosh」の文字列を見つけ、なおかつ "ontouchend" in document でタッチ操作がサポートされているかを調べます。タッチ操作ができる Macintosh => iPad というわけですね。

なお、この PC かモバイルかを判別するスクリプトをさらに切り出したものを公開しているので、もしよければそちらもご覧になってください。

追記(2021.09.18)

Imgur のクライアント ID を取得する手順です。

  1. Imgur のアカウントを作成します。
  2. https://api.imgur.com/oauth2/addclient にアクセスします。
  3. 「Application name」に、任意のアプリ名(ゲームタイトルなど)を入力します。
  4. 「Authorization type」は、「Anonymous usage without user authorization」に設定します。
  5. 「Authorization callback URL」に、適当な URL を入力します。
    1. 特に使用しないので何でもいいです。自分は「https://imgur.com」を入力しました。
  6. 「Email」に、任意のメールアドレスを入力します。
    1. こちらも何でも大丈夫です。ユーザーに公開されたりすることもありません。
  7. 最後に、「submit」ボタンを押せば、クライアント ID を取得できます。

*1:モバイル端末で Twitter アプリがインストールされていない場合? そんなものは知らん🙄

*2:mini を除く。

The coloring of this site is Dracula PRO🧛🏻‍♂️
This website uses the FontAwesome icons licensed under CC BY 4.0.

2020 GIGA CREATION