Nobollel開発者ブログ

Nobollelのエンジニアが、UnityやCocos2d-xの旬な情報・技術を紹介します。

GitLab(Omnibus)のバックアップデータをGoogle Cloud Storageに保存する方法

こんにちは。清水です。
GitLabにはバックアップデータをリモートストレージに保存する仕組みが備わっています。ググってみると、Amazon S3の情報はよく見かけますが、Google Cloud Storage (GCS) に保存する方法がピンポイントでなかったので、今回この方法を紹介します。

1. アクセスキーを作成する

(1) Google Cloud Platform のコンソール画面より、メニューから 「Storage」を選択
(2) 「設定」を選択
(3) 「相互運用」を選択
(4) 画面下部にある「新しいキーを作成」ボタンをクリック
(5) 表示された「アクセスキー」と「非表示」をメモする

f:id:nobollel:20170203221402p:plain

2. バケットを作成する

(1) Google Cloud Platform のコンソール画面より、メニューから 「Storage」を選択
(2) 「ブラウザ」を選択
(3) 画面上部にある「バケットを作成」ボタンをクリック
(4) 適切なバケット名を設定し、「名前」と「場所」をメモする

f:id:nobollel:20170203214026p:plain

3. GitLabを設定する

(1) GitLabの設定ファイル gitlab.rb を開く

$ sudo vi /etc/gitlab/gitlab.rb

(2) バックアップ用の設定「backup_upload_connection」と「backup_upload_remote_directory」を追加する

gitlab_rails['backup_upload_connection'] = {
  'provider' => 'Google',
  'region' => 'asia-northeast1', # 2.(4)の「場所」項目
  'google_storage_access_key_id' => 'xxxxxxxxxxxxxxxxxxxx', # 1.(5)の「アクセスキー」項目
  'google_storage_secret_access_key' => 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' # 1.(5)の「非表示」項目
}
gitlab_rails['backup_upload_remote_directory'] = 'nobollel-gitlab' # 2.(4)の「名前」項目

4. GitLabの設定を反映する

$ sudo gitlab-ctl reconfigure

以上で設定は完了です。 GitLabのバックアップを実行し、GCSにバックアップデータが保存されているか確認しましょう。

Unityでサーバーを使わずにアプリの最新バージョンがあるかどうかチェックし、ストアに誘導する

どうも、エンジニアの水津です
今回はUnityにおいて、アプリの更新がある場合にストアに誘導するポップアップを表示する実装になります

バージョンアップを促すポップアップの実装は、大規模なスマホゲームアプリの場合にサーバーとの整合性を図るためによく実装されています
ただ、そういったアプリはサーバー側でクライアントのバージョンも管理することが多く、その場合アプリの最新バージョンがストア上がったのを確認してからサーバー側のクライアントバージョンを書き換えるという手間がかかります
また、カジュアルゲームなどではそもそもサーバーを使用しないことがあるため、上記方法ではバージョンアップを通知することができません
今回はカジュアルゲームの実装でバージョンアップ通知をしたかったため、サーバーを使わないバージョンアップ通知を実装してみました

使用するライブラリ

以下の2つのライブラリを使用します
1.Html Agility Pack
htmlagilitypack.codeplex.com

2.Mobile Native PopUps
https://www.assetstore.unity3d.com/jp/#!/content/18479

1についてはAndroidにおいてHTMLのパースに使用します
SGMLReaderというライブラリも試してみたのですが、Play Storeのwebページのパースが正常に行えなかったので、こちらのライブラリを使うことを推奨します
サイトからダウンロードして解凍したら、Net20と書かれているディレクトリのHtmlAgilityPack.dllをUnityのスクリプトのあるディレクトリに入れてください

2のアセットについては更新を促すポップアップの表示に使用します
ポップアップの表示とボタン選択のコールバックが受け取れれば、なんでも構いません

ソースコード

全体のソースコードは以下のようになります

using UnityEngine;
using System;
using System.Collections;
using System.Xml;
using HtmlAgilityPack;

public class VersionChecker : MonoBehaviour
{
    string _storeUrl = "";

    void Start() {
        CurrentVersionCheck ();

        if (LoadInvalidVersionUpCheck ()) {
            return;
        }
       #if UNITY_IOS
        StartCoroutine(VersionCheckIOS());
       #elif UNITY_ANDROID
        StartCoroutine(VersionCheckAndroid());
       #endif
    }

    const string INVALID_VERSION_UP_CHECK_KEY = "InvalidVersionUpCheck";

    bool LoadInvalidVersionUpCheck() {
        return PlayerPrefs.GetInt (INVALID_VERSION_UP_CHECK_KEY, 0) != 0;
    }

    void SaveInvalidVersionUpCheck(bool invalid) {
        PlayerPrefs.SetInt(INVALID_VERSION_UP_CHECK_KEY, invalid ? 1 : 0);
    }

    const string CURRENT_VERSION_CHECK_KEY = "CurrentVersionCheck";

    void CurrentVersionCheck() {
        var version = PlayerPrefs.GetString(CURRENT_VERSION_CHECK_KEY, "");
        if (version != Application.version) {
            PlayerPrefs.SetString (CURRENT_VERSION_CHECK_KEY, Application.version);
            SaveInvalidVersionUpCheck (false);
        }
    }

    IEnumerator VersionCheckIOS() {
        var url = string.Format("https://itunes.apple.com/lookup?bundleId={0}", Application.bundleIdentifier);
        WWW www = new WWW(url);
        yield return www;

        if (string.IsNullOrEmpty(www.error) && !string.IsNullOrEmpty (www.text)) {
            var lookupData = JsonUtility.FromJson<AppLookupData> (www.text);
            if (lookupData.resultCount > 0 && lookupData.results.Length > 0) {
                var result = lookupData.results [0];
                if (VersionComparative (result.version)) {
                    ShowUpdatePopup (result.trackViewUrl);
                }
            }
        }
    }

    IEnumerator VersionCheckAndroid() {
        var url = string.Format("https://play.google.com/store/apps/details?id={0}", Application.bundleIdentifier);

        WWW www = new WWW(url);
        yield return www;
        if (string.IsNullOrEmpty(www.error) && !string.IsNullOrEmpty (www.text)) {
            var htmlDoc = new HtmlDocument ();
            htmlDoc.LoadHtml (www.text);
            var node = htmlDoc.DocumentNode.SelectSingleNode ("//div[@itemprop=\"softwareVersion\"]");
            if (node != null) {
                if (VersionComparative (node.InnerText)) {
                    ShowUpdatePopup (url);
                }
            }
        }
    }

    bool VersionComparative(string storeVersionText) {
        if (string.IsNullOrEmpty (storeVersionText)) {
            return false;
        }
        try {
            var storeVersion = new Version (storeVersionText);
            var currentVersion = new Version (Application.version);

            if (storeVersion.CompareTo(currentVersion) > 0) {
                return true;
            }
        } catch (Exception e) {
            Debug.LogErrorFormat("{0} VersionComparative Exception caught.", e);
        }

        return false;
    }

    void ShowUpdatePopup(string url) {
        if (string.IsNullOrEmpty (url)) {
            return;
        }
        _storeUrl = url;
       #if !UNITY_EDITOR
        if (Application.systemLanguage == SystemLanguage.Japanese)
        {
            string title = "アプリの更新があります";
            string message = "更新しますか?";
            string yes = "はい";
            string no = "いいえ";
            MobileNativeDialog dialog = new MobileNativeDialog(title, message, yes, no);
            dialog.OnComplete += OnPopUpClose;
        }
        else {
            string title = "There is an update of the application";
            string message = "Do you want to update the application?";
            string yes = "Yes";
            string no = "No";
            MobileNativeDialog dialog = new MobileNativeDialog(title, message, yes, no);

            dialog.OnComplete += OnPopUpClose;
        }
       #endif
    }


    private void OnPopUpClose(MNDialogResult result)
    {
        switch (result)
        {
        case MNDialogResult.YES:
            Application.OpenURL (_storeUrl);
            break;
        case MNDialogResult.NO:
            SaveInvalidVersionUpCheck (true);
            break;
        }
    }
}


[Serializable]
public class AppLookupData {
    public int resultCount;
    public AppLookupResult[] results;

}

[Serializable]
public class AppLookupResult {
    public string version;
    public string trackViewUrl;
}

これをシーン上のオブジェクトにAddComponentすれば自動的にバージョンチェックが行われます
ただ、通信中やポップアップ表示中にシーンが遷移すると処理が中断したり挙動がおかしくなるので注意してください

各部分解説

    void Start() {
        CurrentVersionCheck ();

        if (LoadInvalidVersionUpCheck ()) {
            return;
        }
       #if UNITY_IOS
        StartCoroutine(VersionCheckIOS());
       #elif UNITY_ANDROID
        StartCoroutine(VersionCheckAndroid());
       #endif

    }

    const string INVALID_VERSION_UP_CHECK_KEY = "InvalidVersionUpCheck";

    bool LoadInvalidVersionUpCheck() {
        return PlayerPrefs.GetInt (INVALID_VERSION_UP_CHECK_KEY, 0) != 0;
    }

    void SaveInvalidVersionUpCheck(bool invalid) {
        PlayerPrefs.SetInt(INVALID_VERSION_UP_CHECK_KEY, invalid ? 1 : 0);
    }

    const string CURRENT_VERSION_CHECK_KEY = "CurrentVersionCheck";

    void CurrentVersionCheck() {
        var version = PlayerPrefs.GetString(CURRENT_VERSION_CHECK_KEY, "");
        if (version != Application.version) {
            PlayerPrefs.SetString (CURRENT_VERSION_CHECK_KEY, Application.version);
            SaveInvalidVersionUpCheck (false);
        }
    }

この部分はバージョンアップチェックを行うかどうかを確認しています
バージョンアップ確認ポップアップで一度でもいいえを選択している場合、以後バージョンアップチェックを行わないようにしています
ただ、いいえを選択した後にアプリのバージョンが変わっている場合、再度バージョンアップチェックを有効にしています
バージョンアップチェックを行う場合、IOSAndroidで分岐します

   IEnumerator VersionCheckIOS() {
        var url = string.Format("https://itunes.apple.com/lookup?bundleId={0}", Application.bundleIdentifier);
        WWW www = new WWW(url);
        yield return www;

        if (string.IsNullOrEmpty(www.error) && !string.IsNullOrEmpty (www.text)) {
            var lookupData = JsonUtility.FromJson<AppLookupData> (www.text);
            if (lookupData.resultCount > 0 && lookupData.results.Length > 0) {
                var result = lookupData.results [0];
                if (VersionComparative (result.version)) {
                    ShowUpdatePopup (result.trackViewUrl);
                }
            }
        }
    }

iOSにおいてApp Storeのアプリのバージョンを取得しています
https://itunes.apple.com/lookup?bundleId=バンドル名"でアプリの詳細なデータをJSONで取得できるので、JSONをデシリアライズしアプリバージョンを取得しています
ストアのURLもJSONに含まれているので、もしバージョンアップしていたらそのストアのURLをポップアップに渡しています

   IEnumerator VersionCheckAndroid() {
        var url = string.Format("https://play.google.com/store/apps/details?id={0}", Application.bundleIdentifier);

        WWW www = new WWW(url);
        yield return www;
        if (string.IsNullOrEmpty(www.error) && !string.IsNullOrEmpty (www.text)) {
            var htmlDoc = new HtmlDocument ();
            htmlDoc.LoadHtml (www.text);
            var node = htmlDoc.DocumentNode.SelectSingleNode ("//div[@itemprop=\"softwareVersion\"]");
            if (node != null) {
                if (VersionComparative (node.InnerText)) {
                    ShowUpdatePopup (url);
                }
            }
        }
    }

AndroidにおいてPlay Storeのアプリのバージョンを取得しています
Androidはストア上の情報を取得する手段がないため、ストアのwebページのHTMLをパースしてバージョンを取得しています
ページのHTMLを全探索してitemprop属性がsoftwareVersionのものを探しているので、処理的には結構重い処理になっていると思います
もしPlay Storeのアプリページの構造が変わった場合、上記の方法では動かなくなるので注意してください

   bool VersionComparative(string storeVersionText) {
        if (string.IsNullOrEmpty (storeVersionText)) {
            return false;
        }
        try {
            var storeVersion = new Version (storeVersionText);
            var currentVersion = new Version (Application.version);

            if (storeVersion.CompareTo(currentVersion) > 0) {
                return true;
            }
        } catch (Exception e) {
            Debug.LogErrorFormat("{0} VersionComparative Exception caught.", e);
        }

        return false;
    }

ストアのバージョンとアプリのバージョンを比較して、ストアのバージョンが新しければtrueを返します

   void ShowUpdatePopup(string url) {
        if (string.IsNullOrEmpty (url)) {
            return;
        }
        _storeUrl = url;
       #if !UNITY_EDITOR
        if (Application.systemLanguage == SystemLanguage.Japanese)
        {
            string title = "アプリの更新があります";
            string message = "更新しますか?";
            string yes = "はい";
            string no = "いいえ";
            MobileNativeDialog dialog = new MobileNativeDialog(title, message, yes, no);
            dialog.OnComplete += OnPopUpClose;
        }
        else {
            string title = "There is an update of the application";
            string message = "Do you want to update the application?";
            string yes = "Yes";
            string no = "No";
            MobileNativeDialog dialog = new MobileNativeDialog(title, message, yes, no);

            dialog.OnComplete += OnPopUpClose;
        }
       #endif
    }


    private void OnPopUpClose(MNDialogResult result)
    {
        switch (result)
        {
        case MNDialogResult.YES:
            Application.OpenURL (_storeUrl);
            break;
        case MNDialogResult.NO:
            SaveInvalidVersionUpCheck (true);
            break;
        }
    }

ポップアップを表示して、YESが押されたらアプリのURLへ飛ばしています
文言などは適時適当に変更してください

【Reactive Extensions】 IObservableの合成と分岐入門その5

【Reactive Extensions】 IObservableの合成と分岐入門その5

おはこんばんにちは、tsuchimotoです。

今回は前回の第4回に引き続き、UniRxを使ったIObservableの合成と分岐入門の5回目になります。

前回までのリンク

Ambメソッド

Ambメソッドについて説明します。

Ambメソッドは複数のIObservableのシーケンスの中から一番最初に値を発行したIObservableのシーケンスの値を後続に流すメソッドです。

このメソッドのオーバーロードを以下に示します。

// 引数で渡した全てのIObservable<T>から最初に値を発行したIObservable<T>の値を後ろに流す
public static IObservable<T> Amb<T>(params IObservable<T>[] sources);
// sources内の全てのIObservable<T>から最初に値を発行したIObservable<T>の値を後ろに流す
public static IObservable<T> Amb<T>(this IEnumerable<IObservable<T>> sources);
// firstとsecondのうち最初に値を発行したものの値を後ろに流す
public static IObservable<T> Amb<T>(this IObservable<T> first, IObservable<T> second);

他の合成系メソッドと同様に可変長引数やIEnumerable>の拡張メソッドや2つの合成に特化したオーバーロードが用意されています。

Ambメソッドのコード例を以下に示します。

Observable.Amb(
    // 3秒後に値を発行するIO<T>
    Observable.Timer(TimeSpan.FromSeconds(3)).Select(_ => "3sec"),
    // 10秒後に値を発行するIO<T>
    Observable.Timer(TimeSpan.FromSeconds(10)).Select(_ => "10sec"),
    // 2秒後に値を発行するIO<T>
    Observable.Timer(TimeSpan.FromSeconds(2)).Select(_ => "2sec"),
    // 30秒後に値を発行するIO<T>
    Observable.Timer(TimeSpan.FromSeconds(22)).Select(_ => "30sec"),
    // 5秒後に値を発行するIO<T>
    Observable.Timer(TimeSpan.FromSeconds(6)).Select(_ => "5sec"))
    .Subscribe(
        s => Debug.LogFormat("OnNext: {0}", s),
        () => Debug.Log("OnCompleted"));

Timerメソッドを使って、指定した時間が経過した後に、値を発行するIObservableのシーケンスをAmbメソッドで合成しています。

実行結果を以下に示します。

OnNext: 2sec
OnCompleted

引数で渡した中で一番早く値を発行する Observable.Timer(TimeSpan.FromSecond(2)).Select(_ => "2sec")の結果が OnNextやOnCompletedに渡っていることが確認できます。

Switchメソッド

Switchメソッドについて説明します。

Switchメソッドは複数のIObservableのシーケンスの中から値が発行される度に、一つ前のシーケンスの値を流すのを辞めて、 後から発行されるシーケンスの値を流し始めます。

要するに、後から発行されたIObservableシーケンスにどんどん切り替えていくのです。

メソッドの定義は以下のようになります。

public static IObservable<T> Switch<T>(this IObservable<IObservable<T>> sources);

Switchメソッドのコード例を以下に示します。

// 1から4の値を発行するシーケンス
IObservable<string> source = Observable.Range(1, 4)
    // 1から4の値をそれぞれ1ミリ秒毎に文字列を発行する4つのシーケンスに変換
    .Select(i => 
        Observable.Interval(TimeSpan.FromMilliseconds(i)).Take(3).Select(sec =>
            string.Format("i:{0} {1}MSec", i, sec)
        )
    )
    // Switchメソッドで合成
    .Switch();

source.Subscribe(
    s => Debug.LogFormat("OnNext: {0}", s),
    () => Debug.Log("OnCompleted"));

実行結果は以下になります。

OnNext: i:4 0MSec
OnNext: i:4 1MSec
OnNext: i:4 2MSec
OnCompleted

ここで比較のためにSwitchをMergeに変えて実行してみましょう。

コードは以下になります。

// 1から4の値を発行するシーケンス
IObservable<string> source = Observable.Range(1, 4)
    // 1から4の値をそれぞれ1ミリ秒毎に文字列を発行する4つのシーケンスに変換
    .Select(i => 
        Observable.Interval(TimeSpan.FromMilliseconds(i)).Take(3).Select(sec =>
            string.Format("i:{0} {1}MSec", i, sec)
        )
    )
    // Mergeメソッドで合成
    .Merge();

実行結果は以下になります。

OnNext: i:1 0MSec
OnNext: i:2 0MSec
OnNext: i:3 0MSec
OnNext: i:4 0MSec
OnNext: i:1 1MSec
OnNext: i:2 1MSec
OnNext: i:3 1MSec
OnNext: i:4 1MSec
OnNext: i:1 2MSec
OnNext: i:2 2MSec
OnNext: i:3 2MSec
OnNext: i:4 2MSec
OnCompleted

Mergeメソッドでは4つのIObservableシーケンスから発行された値はすべて流されました。 対して、Switchメソッドでは4番目のシーケンスから発行された値しか流れていません。

Switchメソッドでは新たなシーケンスにどんどん切り替わって行き、最後の4番目のシーケンスの値しか流れて来ないことが分かります。

Unityのシーン初期時のリソース(Audio)生成速度の比較

初めまして。NobollelUnityエンジニアの山田です。

Unityを使っていると自分で毎回課題に上がるのがリソースの読み込み方法です。 どこまでAssetBundleにするのか、ResourcesLoadを使うのか、SceneのHierarchyに非アクティブで置いておきアクティブ状態で切り替えるのか。

特に個人的に気を使うのがAudioの扱いです。 物にもよりますがAudioは重たいので、単純にResources.Loadで読み込むと音が途切れやすいです。 また、設定によりますが一気にメモリを食うのでアプリがクラッシュしやすい場面でもあります。

なのでAssetBundleを使わないような場合、よく使うAudioはキャッシュしておくパターンも使われると思います。

そこで今回はシーン初期時に行うAudioのキャッシュ方法の速度について調べてみました。

調査環境は以下の通りです ・Unity5.5.0p3 ・Android XperiaZ3Compact ・AudioのPreloadAudioDataのチェックは外しておく

今回比較するのは以下の3つのパターンです。

1.ヒエラルキー上にAudioを置いておく 2.Awake時にResources.LoadAllを使い指定フォルダのAudioを全て読み込み、GameObjectを生成していく 3.Inspecter上にAudioを設定しておき、Awake時にループでGameObjectを生成していく

メインとなるシーンが読み込み終わって、最初のStartが呼ばれるまでにかかった時間(Time.realtimeSinceStartup)を計ります。

これらをそれぞれ10回実行し、中央値を取りました。

使用するAudioはCasual Music Pack

https://www.assetstore.unity3d.com/jp/#!/content/21086

の全20ファイル(376.2MB)を使用しました。

1.ヒエラルキー上にAudioを置いておく ヒエラルキー上に20ファイルを置いておき、ビルドして実行しました。 ResourcesフォルダにはAudioファイルは置いていません。 (Resourcesフォルダにファイルがあると起動時間が若干伸びてしまうため)

結果: 7.216597秒

2.Awake時にResources.LoadAllを使い指定フォルダのAudioを全て読み込み、GameObjectを生成していく こちらはこのようなコードでテストをしました void Awake() { AudioClip[] clips = Resources.LoadAll("Music"); foreach (AudioClip clip in clips) { GameObject go = new GameObject(); AudioSource source = go.AddComponent(); source.clip = clip; source.Play(); //一応Playもさせておく } }

結果: 6.130289秒

3.Inspecter上にAudioを設定しておき、Awake時にループでGameObjectを生成していく こちらは2.のコードのclipsをヒエラルキーであらかじめ全て入れておいたものになります。 Resourcesフォルダからも移動をしております。

結果: 6.853645秒

というわけで今回は以下の結果になりました。 ヒエラルキー  7.216597秒 LoadAll  6.130289秒 Inspecter  6.853645秒

意外にもResources.LoadAllが一番速度が出ました。

今回試していないパターン(Application.LoadLevelAdditiveなど)でどのぐらい速度が出るのか、 またAudioでなくUIなどではどうなるのかなども調査が必要そうです。

Spine Bounding Boxes (Cocos2d-x)

Last time i wrote a quick introduction on how to use Spine with Cocos2d-x, today i thought i would continue on the same path and show you how use Spine bounding boxes to achieve collision in Cocos2d-x.

The easiest way to get collision detecting for Spine models is to use the built in Bounding Box attachment. You could make one box covering your whole model, or make a more precise ones for each of your bones. Bounding boxes can also be switched on and off at any point in the timeline of your animations, for example in the middle of a sword swinging animation, or right as your hero draws his shield, for more exact collision timings.

f:id:nobollel:20170111201053p:plain

Bounding boxes are created the same way as other attachments in Spine, select the bone you want to use as parent for your bounding box, press "New..." in the bottom right of the screen, and a dialog box will pop up asking you for a name. After naming your box, a plus sign will appear next to the cursor and we can now start putting down the bounds of our "box" by clicking. Once you are happy, complete the shape by clicking on the first node you put down. The shape of the bounding box can be edited at any time from the hierarchy.

After exporting, we can start using the bounding box in Cocos2d-x. For this example i will use the mouse position to see if we hit our target.

    // First we create a skeletonBounds object
    auto bounds = spSkeletonBounds_create();

    // Listener to get out click position
    auto listener = EventListenerTouchOneByOne::create();
    listener->onTouchBegan = [bounds](Touch* touch, Event* event) {

        // We then need to update the bounds to get their current position
        spSkeletonBounds_update(bounds, _skeletonNode->getSkeleton(), true);
       
        // The location of the click
        auto target = event->getCurrentTarget();
        auto location = target->convertToNodeSpace(touch->getLocation());

        // Then all we have to do is check if the click was within the bounds!
        if (spSkeletonBounds_containsPoint(bounds, location.x, location.y)) {
            // We hit something!
            CCLOG("Hit!");
        }
        return true;
    };

There is of course a lot more fancy stuff you can do, spSkeletonBounds_containsPoint returns the first object to get hit, which you could then use to detect which body part it was. Or if you already know what bounding box you are looking for, you can use spPolygon_containsPoint instead.

submoduleのあるGitリポジトリで複数のssh公開鍵を使う方法

どうもはじめまして、先月からNobollelでお世話になっているエンジニアの水津です
今回は初めてなので軽い内容で、submoduleのあるGitリポジトリで複数のssh公開鍵を使う方法についてです

割と特殊な条件の時に必要となるので普段は必要ない設定かと思いますが、例えばgithubのユーザーをプライベート用と会社用で持っていて、それらを同じPCで使おうと思った時に、同じ公開鍵を登録しようとするとKey is already in useと言われて登録できません
その場合、別の鍵を作成して、~/.ssh/configを設定するのですが、そちらの手順については以下の記事が綺麗にまとまっています

【メモ】githubの複数アカウントにSSH接続するための設定手順 | Developers.IO

しかし、この方法で問題になるのは、チェックアウトしたGitリポジトリにsubmoduleがある場合です
チェックアウトしたGitリポジトリのsubmoduleを、新しく作った公開鍵で使えるように.gitmodulesのurlを~/.ssh/configで設定した自分のホスト名に書き換えてプッシュしたりなんかすると、今度は他の全てのユーザーがsubmoduleが使えなくなってしまいます

この場合の対応方法については以下のStackOverflowに適切な回答がありました

github - Updating a submodule when multiple git accounts are configured - Stack Overflow

ここの回答にあるように.git/configの中にあるurlのホスト名部分を、~/.ssh/configで設定したホスト名に変更すれば、ローカルのurlのみを変更することができます
しかし、この状態でgit submodule syncを実行したりすると再びローカル設定が.gitmodulesのurlに上書きされてしまうため、注意が必要です
SourceTreeで作業をしていたりするといつの間にか.git/configが書き換わっていたということもあるので、ローカル設定を変更するには.git/configを書き換えるということを覚えておくと良いと思います

Introduction to Spine

Spine is a 2d based animation software, what makes it stand out is its ability to use bones and meshes, just like any 3d animation tool, to effortlessly create awesome 2d animation for your games. Another great feature is being able to use "skins" and "attachment", allowing you to reuse the same skeleton and animations for while switching out things like clothing and weapons for your character.

f:id:nobollel:20161207150629p:plain

Importing and rigging

Before starting to animate, we need to import some images. All we have to do is finding the Images tab in the hierarchy and press the Browse button at the bottom, then select the folder with the images we want to import. We can now use the images by simply dragging them onto our stage, and arranging them how we want our model to look.

If you already have your model assembled in Photoshop or Gimp, you can use the export script to generate a JSON file that can be imported directly into Spine.

f:id:nobollel:20161207160752p:plain

To create bones for your character, press the "Create new bones" at the bottom left of the screen, hold ctrl and click on the image you want to attach the bone to, then click and drag from where you want the actual bone. Remember to always select the bone you want as parent before creating new bones.

Animations

To create a new animation, find the Animations tab in the hierarchy and then press the "New Animation" button at the bottom of the screen. Once you have given your new Animation a name, press SETUP at the top left to go into animation mode.

f:id:nobollel:20161207150642p:plain

The basics of the Dropsheet is to create one or more keyframe at different positions in the timeline, and Spine will tween between the two, making a smooth transition. To create a keyframe, press anywhere in the timeline where you want the new frame to be, then either move or rotate the body part you want to animate, or manually put in the values in the transform window above the Dropsheet.

You can also put "Events" in the timeline that can be used for things like footstep effects or to let your game know the exact timing to spawn the bullet in your shooting animation. Creating an event is just like creating animations, find the Event tab and press New Event. To use the event in your animation, go to the frame you want the event to occur, and press the small key next to the event you want to use.

f:id:nobollel:20161207151637p:plain

Runtimes and scripting

To get our animations working in our game we need one the Spine runtime. Today i will be using the Cocos2d-x runtime, but spine is supported by a wide range of popular engines and languages, you can find a full list here. http://esotericsoftware.com/spine-runtimes

Getting the character to show up in Cocos2d-x is super easy, if you have Cocos2d-x installed, you should already have the runtime too, but we need to add the header file to our class. I will also use the Spine namespace for convenience.

HelloWorld.h

#include <spine/spine-cocos2dx.h>
using namespace spine;

And with that we can add our character to our game!

HelloWorld.ccp

// creates a node holding our character
auto skeletonNode = SkeletonAnimation::createWithFile("res/koko.json", "res/koko00.atlas", 1.0f);

// the position we want our character to appear
skeletonNode->setPosition(Vec2(600, 10));

// (optional) called once a new animation starts
skeletonNode->setStartListener([skeletonNode] (int trackIndex) {
    spTrackEntry* entry = spAnimationState_getCurrent(skeletonNode->getState(), trackIndex);
    CCLOG("start: %s", entry->animation->name);
});

// (optional) called every time an animation is completed
skeletonNode->setCompleteListener([skeletonNode] (int trackIndex, int loopCount) {
    CCLOG("loop count: %d", loopCount);
});

// (optional) this is called each time the animation runs into the events we added in spine
skeletonNode->setEventListener([skeletonNode] (int trackIndex, spEvent* event) {
    CCLOG("event: %s", event->data->name);
});

// the most basic way to play an animation is to use setAnimation(
// int trackIndex, const std::string &animationName, bool loop)
skeletonNode->setAnimation(0, "walk", true);

// finally we can add the node to the stage
addChild(skeletonNode);

And thats it! We now have our character animated on the screen!