Unityをアップデートしたら、iOS版バイナリをアップロード時にITMS-90109の警告を頂くようになった

Unityを2018から2019に更新したタイミングからだと思うのだけれど、App Store Connectにバイナリをアップロードすると以下のような警告を頂くようになってしまった。

ITMS-90109: This bundle is invalid - The key UIRequiredDeviceCapabilities in the Info.plist may not contain values that would prevent this application from running on devices that were supported by previous versions.

ネットで調べると、info.plistからUIRequiredDeviceCapabilitiesを削除すると回避できたとのことだったので、ビルドのポストプロセスで行うことに。

using System.IO;
using UnityEditor;
using UnityEditor.Callbacks;

#if UNITY_IOS
using UnityEditor.iOS.Xcode;

public static class XcodePostProcessBuild
{
    [PostProcessBuild]
    public static void OnPostProcessBuild(BuildTarget target, string path)
    {
        var plistPath = Path.Combine(path, "Info.plist");
        var plist = new PlistDocument();
        plist.ReadFromFile(plistPath);

        plist.root.values.Remove("UIRequiredDeviceCapabilities");

        plist.WriteToFile(plistPath);
        
    }
}
#endif

これで、警告が出ないようにできました。

Unity2018からUnity2019にアップデートしたら、Android版ビルド時にKeystore 周りでエラーが出た

あるプロジェクトをUnity2018からUnity2019にアップデートしたら、Android版のビルド時に

Keystore file '/Users/wakepon/projects/2357/Temp/gradleOut/Divide.keystore' not found for signing config 'release'. See the Console for details.

というエラーが出るようになってしまった。

Player Settings -> Publishing SettingsからKeystore Managerを選んでkeystoreファイルを指定し直し、それ以下も全て再度設定しなおしたらビルドが通るようになった。

Galaxy Noteでのみ、しかもタッチペンを使ったときのみ描けなくなるというバグ

私のリリースしているお絵かきアプリのユーザーさんから、タッチペンを使うときだけ絵が描けないという報告を受けました。調べていくと、奇妙な現象でして、

  • 指では描ける
  • タッチペンを使った場合でもボタン等は反応する
  • 自分の手元にあるiPadや他のタブレットでは問題ない

調べてみると、以下のような投稿をForumで見かけました。

https://forum.unity.com/threads/android-pen-not-working.766508/?fbclid=IwAR2on-iHh1uE4cLJLd9Q1QKJfkXfDjCCDRP1arC3r__geryCtpbCVhw9UiE

どうもGalaxy Note特有の現象なようです。

Unity側で対処すべき問題なのかGalaxy Note側の問題なのかは分からないのですが、とにかく不具合なことは確かです。

issuetracker.unity3d.com


こちらを見ると1月中旬に修正が入ったそうですが、自分の使っているUnity2018.4.15fでビルドしたバージョンでは未だに発生するようでした。

仕方がないので先のフォーラムに書いてあったワークアラウンドを試してみたところ解決できました。

ワークアラウンドというのは見えないUIレイヤーを一つ挟んでそこでタッチとタッチポジションを検知するというもの。

ざっくりこういう感じのコンポーネントを作って、このisPointerDownやpointerPositionをInput.GetMouseButton( 0 )やInput.mousePositionの代わりに使うことで解決できました。

2020/3/6 修正
以前のIDragHandlerを使うバージョンだと僅かな移動の場合にポジションが更新されなかったのでpositionは、Input.mousePositionで取ることにして、downしたかどうかだけをOnPointer関数で取得することにしました。

public class UILayer : IPointerDownHandler, IPointerUpHandler {
    public bool isPointerDown;
 
    //------------------------------------------------------------------------------
    public void OnPointerDown(PointerEventData pointerEventData)
    {
        isPointerDown = true;
    }

    //------------------------------------------------------------------------------
    public void OnPointerUp(PointerEventData pointerEventData)
    {
        isPointerDown = false;
    }
}

GoogleSpreadSheetからデータを取ってきてScriptableObjectに値を突っ込む

基本的にはこちらを参考にしてます
7081.hatenablog.com

まずは上記のサイトを参考にしつつGoogle API Consoleでの設定を行います。

次に認証コードからアクセストークンとリフレッシュトークンを取得します。

WWWForm form = new WWWForm();
if (!form.headers.ContainsKey("Content-Type"))
{
    form.headers.Add("Content-Type", "application/x-www-form-urlencoded");
}
else
{
    form.headers["Content-Type"] = "application/x-www-form-urlencoded";
}
form.AddField("code", "OAuthの認証コード");
form.AddField("client_id", "OAuthのクライアントID");
form.AddField("client_secret", "クライアントシークレット");
form.AddField("redirect_uri", "http://localhost");
form.AddField("grant_type", "authorization_code");
form.AddField("access_type", "offline");
Dictionary<string, string> headers = form.headers;
byte[] rawData = form.data;
WWW www = new WWW("https://www.googleapis.com/oauth2/v4/token", rawData, headers);
while (!www.isDone) ;
Debug.Log("GetAccessJsonData, www.text : " + www.text);

アクセストークンさえ手に入れば、

//とってくるセルの範囲
string start_cell = "A1";
string end_cell = "Z100";//範囲は大きめにとっておいても大丈夫

string send_text = "https://sheets.googleapis.com/v4/spreadsheets/スプレッドシートのID/values/"
    + start_cell + ":" + end_cell + "?access_token=" + access_token;

WWW www = new WWW(send_text);
while (!www.isDone) ;

Debug.Log( "json data : " + www.text );

っという感じで、セルのデータをjsonデータとしてとってこれます。

ちなみにここで「スプレッドシートのID」とは、スプレッドシートのページのURLが

https://docs.google.com/spreadsheets/d/ほげほげ/edit#gid=0

だとしたら、この「ほげほげ」の部分です。

アクセストークンは寿命があるので、PlayerPrefsに覚えておいて、使えなくなったらまた発行してもらうっていうふうにしてみます。

//アクセストークンを取得
//覚えていたら、それを返す。
//それが無効になってたらリフレッシュして再取得したものを返す
public static string getAccessToken()
{
    const string KEY_ACCESS_TOKEN = "KEY_ACCESS_TOKEN";

    string access_token = PlayerPrefs.GetString( KEY_ACCESS_TOKEN, "" );
    //アクセストークンがまだ有効かどうか調べる
    string access_token_status = CheckAccessTokenStatus( (string)access_token );
    if( access_token_status == "Invalid Value" || access_token == "" )
    {
        //アクセストークンが無効だったので取り直す
        Debug.Log( "Refresh Access Token!!!" );
        access_token = RefreshAccessToken();
    }

    //アクセストークンを覚えておく
    Debug.Log( "access_token : " + access_token );
    PlayerPrefs.SetString( KEY_ACCESS_TOKEN, access_token );

    return access_token;
}

//Access Tokenが有効かどうかチェック
static string CheckAccessTokenStatus( string access_token )
{
    WWW www = new WWW("https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=" + access_token );
    while (!www.isDone) ;

    JsonData jsonData = JsonMapper.ToObject(www.text);
    IDictionary idict = jsonData;
    if( idict.Contains("error_description") )
        return (string)jsonData["error_description"];
    else
        return "";
}

static string RefreshAccessToken()
{
    WWWForm form = new WWWForm();
    form.AddField("refresh_token", "リフレッシュトークン" );
    form.AddField("client_id", "クライアントID");
    form.AddField("client_secret", "クライアントシークレット");
    form.AddField("grant_type", "refresh_token");

    Dictionary<string, string> headers = form.headers;
    byte[] rawData = form.data;
    WWW www = new WWW("https://www.googleapis.com/oauth2/v4/token", rawData);

    while (!www.isDone) ;

    JsonData jsonData = JsonMapper.ToObject(www.text);
    return (string)jsonData["access_token"];
}

ちなみにJsonのパースにはLitJsonを使っています。
GitHub - Mervill/UnityLitJson: JSON library for Unity3D

今回はゲーム内テキストの日本文と英語文をMessageDatabaseというScriptableObjectに持たせることを想定し、そこにデータをぶち込もうと思います。

[System.Serializable]
public class TranslateText {
    public string key;
    public string jp;
    public string eng;
    public TranslateText( string key_, string jp_, string eng_ )
    {
        key = key_;
        jp  = jp_;
        eng = eng_;
    }
}

[CreateAssetMenu(menuName = "CreateMessageDatabase")]
public class MessageDatabase : ScriptableObject {
    public TranslateText[] translateTexts = new TranslateText[0];

#if UNITY_EDITOR
    //google spreadsheet から読み込んで登録
    public void RegisterData( string[][] data, int num_data )
    {
        translateTexts = new TranslateText[num_data];
        for( int i = 0; i < num_data; ++i )
        {
            string key = data[i][0];
            string jp  = data[i][1];
            string eng = data[i][2];
            translateTexts[i] = new TranslateText( key, jp, eng );
        }
    }
#endif
}


データをぶち込んでいるところも含めて、全体は以下のような感じ。

#if UNITY_EDITOR
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using LitJson;

public class SpreadsheetDataLoader
{
    private const int MAX_COLUMN = 5;//多めに取る。足りなかったら増やす
    private const int MAX_ROW = 1000;//多めに取る。足りなかったら増やす。

    [System.Serializable]
    public class SpreadsheetData
    {
        public string range;
        public string majorDimension;
        public SpreadsheetData()
        {
            for (int i = 0; i < values.Length; ++i)
            {
                values[i] = new string[MAX_COLUMN];
            }
        }

        //多めにとっててもToObjectするときにshrinkされるっぽい
        public string[][] values = new string[MAX_ROW][];

        public int NumColumns { get { return values.Length; } }
    }

    [MenuItem("Assets/Load Spreadsheet")]
    static void LoadSpreadsheet()
    {
        string access_token = getAccessToken();

        //とってくるセルの範囲
        string start_cell = "A1";
        string end_cell = "Z100";//範囲は大きめにとっておいても大丈夫

        string send_text = "https://sheets.googleapis.com/v4/spreadsheets/スプレッドシートのID/values/"
            + start_cell + ":" + end_cell + "?access_token=" + access_token;

        WWW www = new WWW(send_text);
        while (!www.isDone) ;

        SpreadsheetData spreadsheet = null;
        spreadsheet = JsonMapper.ToObject<SpreadsheetData>(www.text);

        int num_columns = spreadsheet.NumColumns;
        //Scriptable Objectのインスタンスを作る
        MessageDatabase messages = AssetDatabase.LoadAssetAtPath<MessageDatabase>("Assets/MessageDatabase.asset");
        messages.RegisterData(spreadsheet.values, num_columns);//データを登録

        //セーブ
        UnityEditor.EditorUtility.SetDirty(messages);
        UnityEditor.AssetDatabase.SaveAssets();
    }

    //アクセストークンを取得
    //覚えていたら、それを返す。
    //それが無効になってたらリフレッシュして再取得したものを返す
    public static string getAccessToken()
    {
        const string KEY_ACCESS_TOKEN = "KEY_ACCESS_TOKEN";

        string access_token = PlayerPrefs.GetString( KEY_ACCESS_TOKEN, "" );
        //アクセストークンがまだ有効かどうか調べる
        string access_token_status = CheckAccessTokenStatus( (string)access_token );
        if( access_token_status == "Invalid Value" || access_token == "" )
        {
            //アクセストークンが無効だったので取り直す
            Debug.Log( "Refresh Access Token!!!" );
            access_token = RefreshAccessToken();
        }

        //アクセストークンを覚えておく
        Debug.Log( "access_token : " + access_token );
        PlayerPrefs.SetString( KEY_ACCESS_TOKEN, access_token );

        return access_token;
    }

    //Access Tokenが有効かどうかチェック
    static string CheckAccessTokenStatus( string access_token )
    {
        WWW www = new WWW("https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=" + access_token );
        while (!www.isDone) ;

        JsonData jsonData = JsonMapper.ToObject(www.text);
        IDictionary idict = jsonData;
        if( idict.Contains("error_description") )
            return (string)jsonData["error_description"];
        else
            return "";
    }

    static string RefreshAccessToken()
    {
        WWWForm form = new WWWForm();
        form.AddField("refresh_token", "リフレッシュトークン" );
        form.AddField("client_id", "クライアントID");
        form.AddField("client_secret", "クライアントシークレット");
        form.AddField("grant_type", "refresh_token");

        Dictionary<string, string> headers = form.headers;
        byte[] rawData = form.data;
        WWW www = new WWW("https://www.googleapis.com/oauth2/v4/token", rawData);

        while (!www.isDone) ;

        JsonData jsonData = JsonMapper.ToObject(www.text);
        return (string)jsonData["access_token"];
    }

    //一番最初にRefleshTokenとかを調べるときに使う
    [MenuItem("Assets/GetRefleshToken")]
    static void GetRefleshToken()
    {
        WWWForm form = new WWWForm();
        if (!form.headers.ContainsKey("Content-Type"))
        {
            form.headers.Add("Content-Type", "application/x-www-form-urlencoded");
        }
        else
        {
            form.headers["Content-Type"] = "application/x-www-form-urlencoded";
        }
        form.AddField("code", "OAuthの認証コード");
        form.AddField("client_id", "OAuthのクライアントID");
        form.AddField("client_secret", "クライアントシークレット");
        form.AddField("redirect_uri", "http://localhost");
        form.AddField("grant_type", "authorization_code");
        form.AddField("access_type", "offline");
        Dictionary<string, string> headers = form.headers;
        byte[] rawData = form.data;
        WWW www = new WWW("https://www.googleapis.com/oauth2/v4/token", rawData, headers);
        while (!www.isDone) ;
        Debug.Log("GetAccessJsonData, www.text : " + www.text);
    }
}
#endif

これで、
f:id:wkpn:20191025224641p:plain
こーいうデータから
f:id:wkpn:20191025224707p:plain
こんな感じのScriptableObjectにできます。

自分のゲームでは、例えばゲーム内ショップのラインナップとか、スコア計算用の係数郡とかをGoogleSpreadSheetで管理して、それをScriptableObjectに流し込んで使っています。

Google Play Service系のプラグインを更新したら ClassNotFoundException: Didn't find class “android.support.v4.content.FileProvider”というエラーがでてハングした

PlayServiceResolverでプラグインを最新にしたら、アプリがクラッシュするようになっちゃいました。
どうもSocialConnector関係のところで止まっているようでした。

たぶん、android.support.v4.aarとかが無くなったからのようです。

最新では、androidx.core.content.FileProviderが代わりのものっぽいので、AndroidManifest.xml内の

android:name="android.support.v4.content.FileProvider"

ってところを

android:name="androidx.core.content.FileProvider"

こんな感じに修正して、

Asset/SocialConnector/SocialConnector.cs内の中の51行目

var fileProvider = new AndroidJavaClass("android.support.v4.content.FileProvider");

var fileProvider = new AndroidJavaClass("androidx.core.content.FileProvider");

と修正したら動くようになりました。

ビッグローブ光にて、何故かMacでだけIPv6接続できなかった。

うちのネットワーク環境、ビッグローブ光でiPv6対応したのに、なぜかIPv4でしかアクセスできない状態だった。
ちなみに、所有機Macbook Pro

この問題、ずーっと放置していたのだけど、今日Windows機を導入したので試してみたらIPv6でアクセスできた。
なんでMacIPv6接続できずWindowsでだけ接続できるんやー!っと思ったので、重い腰を上げてビッグローブに問い合わせてみた。

しかし、分からないのでAppleに聞いてくれと言われてしまう。オーマイガッ。

Appleに聞くのも腰が重いなぁと思って、プチプチっとネットワーク設定をいじっていたら、意外と簡単にIPv6アクセスできるようになった。

System Preference -> Network -> WiFi -> Advanced -> TCP/IP

Configure IPv6でLink-local onlyにしてたのをAutomaticallyにするだけだった。

ちなみにIPv6になって爆速になったかと言われるとそうでもなかった。

UnityのRandom.Range(min,max)はfloatとintで挙動が違う

Random.Range(min,max)は、引数がfloatの場合、maxはinclusiveになりますが、intの場合はmaxがexclusiveになります。

つまり、Random.Range(0.0f,10.0f)の場合、10.0fが返ってくるときがありますが、Random.Range(0,10)では10は返ってきません。最大でも9です。

なんでやねん!

知ったとき驚いたんですが、プログラミング界では普通なんですかね?

参考:
docs.unity3d.com