20200525のUnityに関する記事は10件です。

スマートデバイスプログラミング④(上からランダムに降ってくるボールを拾ってスコアを競うゲーム)

UnityとGameCanvasのダウンロード

1:UnityHubをDLしてインストール。(一部のwindows環境では拡張子を.exeに変更する必要があります)

2:UnityHubを起動して、Unity2019.3.11f1をインストール。

3:GameCanvasのダウンロード
https://github.com/sfc-sdp/GameCanvas-Unity/
でGameCanvasをダウンロード

4:追加画像をダウンロードしてAssetsに追加
http://web.sfc.keio.ac.jp/~wadari/sdp/k04_res.zip

Game.csの編集

Assets内のGame.csを別エディタで編集

using Sequence = System.Collections.IEnumerator;

/// <summary>
/// ゲームクラス。
/// 学生が編集すべきソースコードです。
/// </summary>
public sealed class Game : GameBase
{
  const int BALL_NUM = 30;
  int[] ball_x = new int [BALL_NUM];
  int[] ball_y = new int [BALL_NUM];
  int[] ball_col = new int [BALL_NUM];
  int[] ball_speed = new int [BALL_NUM];
  int ball_w = 24;
  int ball_h = 24;
  int player_x = 304;
  int player_y = 448;
  int player_speed = 3;
  int player_w = 32;
  int player_h = 32;
  int player_img = 4;
  int score = 0;
  int time = 1800;
  int player_col = 4;
  int combo = 0;

    /// <summary>
    /// 初期化処理
    /// </summary>
    public override void InitGame()
    {
      gc.SetResolution(640, 480);
      for(int i =0 ; i < BALL_NUM ; i ++ )
      {
        resetBall(i);
      }
    }

    /// <summary>
    /// 動きなどの更新処理
    /// </summary>
    public override void UpdateGame()
    {
      time = time - 1;

      for(int i =0 ; i < BALL_NUM ; i ++ )
      {
        ball_y[i] = ball_y[i] + ball_speed[i];
        if(ball_y[i]> 480){
          resetBall(i);
        }

        if(gc.CheckHitRect(ball_x[i],ball_y[i],ball_w,ball_h,player_x,player_y,player_w,player_h)){
          if(time>=0){
            score=score+ball_col[i];//ballごとに違った点数を加える
            if(player_col == ball_col[i]) {
              combo++;
              score+= combo;
            }
            else {
              combo = 0;
            }

            player_col = ball_col[i];
          }
          resetBall(i);
        }

      }
      if(gc.GetPointerFrameCount(0) > 0 ){
        if(gc.GetPointerX(0) > 320) {
          player_x += player_speed;
          player_img = 4;
        }
        else {
          player_x -= player_speed;
          player_img = 5;
        }
      }
      if(player_x<=0){
        player_x=0;
      }
      if(player_x>=616){
        player_x=616;
      }


    }

    /// <summary>
    /// 描画の処理
    /// </summary>
    public override void DrawGame()
    {
      gc.ClearScreen();

      for(int i =0 ; i < BALL_NUM ; i ++ ){
        gc.DrawImage(ball_col[i],ball_x[i],ball_y[i]);
      }

      gc.SetColor(0,0,0);
      if(time>=0){
        gc.DrawString("time:"+time,0,0);
      }
      else {
        gc.DrawString("finished!!",0,0);
      }
      gc.DrawString("score:"+score,0,24);
      gc.DrawString("combo:"+combo,0,48);

      //gc.DrawClipImage(player_img,player_x,player_y,0,64,32,32);
      if(time>=0){
        int u = 32+ ((time%60)/30)*32;//0.5秒ごとにユーザー画像を切り替えx
        int v = (player_col - 1) *32;//最初4 色に応じてユーザーの色の画像を指定y
        gc.DrawClipImage(player_img,player_x,player_y, u,v,32,32);
      }
      else {
        gc.DrawClipImage(player_img,player_x,player_y, 96,(player_col - 1) *32,32,32);
      }


    }
    void resetBall(int id){
      ball_x[id] = gc.Random(0,616);
      ball_y[id] = -gc.Random(24,480);
      ball_speed[id] = gc.Random(3,6);
      ball_col[id] = gc.Random(1,3);
    }
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

スマートデバイスプログラミング③(制限時間内にボールを反射させてブロックを壊すゲーム)

UnityとGameCanvasのダウンロード

1:UnityHubをDLしてインストール。(一部のwindows環境では拡張子を.exeに変更する必要があります)

2:UnityHubを起動して、Unity2019.3.11f1をインストール。

3:GameCanvasのダウンロード
https://github.com/sfc-sdp/GameCanvas-Unity/
でGameCanvasをダウンロード

Game.csの編集

Assets内のGame.csを別エディタで編集

using Sequence = System.Collections.IEnumerator;

/// <summary>
/// ゲームクラス。
/// 学生が編集すべきソースコードです。
/// </summary>
public sealed class Game : GameBase
{
  int ball_x;
  int ball_y;
  int ball_speed_x;
  int ball_speed_y;
  int player_x;
  int player_y;
  int player_w;
  int player_h;

  const int BLOCK_NUM = 50;
  int[] block_x = new int [BLOCK_NUM];
  int[] block_y = new int [BLOCK_NUM];
  bool[] block_alive_flag = new bool [BLOCK_NUM];
  int block_w = 64;
  int block_h = 20;
  int time ;

    /// <summary>
    /// 初期化処理
    /// </summary>
    public override void InitGame()
    {
      gc.SetResolution(640, 480);
      ball_x = 0;
      ball_y = 0;
      ball_speed_x = 3;
      ball_speed_y = 3;
      player_x = 270;
      player_y = 460;
      player_w = 100;
      player_h = 20;

      for(int i =0 ; i < BLOCK_NUM ; i ++ )
      {
        block_x[i] = (i % 10 ) * block_w;
        block_y[i] = (i / 10 ) * block_h;
        block_alive_flag[i] = true;
      }
      time = 0;
    }

    /// <summary>
    /// 動きなどの更新処理
    /// </summary>
    public override void UpdateGame()
    {
      if(countBlock()!=0){
        time++;
      }
      player_y = gc.GetPointerY(0) - player_h/2;

      ball_x = ball_x + ball_speed_x;
      ball_y = ball_y + ball_speed_y;

      if( ball_x < 0 ) {
        ball_x = 0;
        ball_speed_x = -ball_speed_x;
      }

      if( ball_y < 0 ) {
        ball_y = 0;
        ball_speed_y = -ball_speed_y;
      }

      if( ball_x > 616 ) {
        ball_x = 616;
        ball_speed_x = -ball_speed_x;
      }

      if( ball_y > 456 ) {
        ball_y = 456;
        ball_speed_y = -ball_speed_y;
      }
      if(gc.GetPointerFrameCount(0) > 0 ){
        player_x = gc.GetPointerX(0) - player_w/2;
      }

      if(gc.CheckHitRect(ball_x,ball_y,24,24,player_x,player_y,player_w,player_h)){
        if(ball_speed_y>0){
          ball_speed_y = -ball_speed_y;
        }
      }

      for(int i = 0; i<BLOCK_NUM; i++){
        if(gc.CheckHitRect(ball_x,ball_y,24,24,block_x[i],block_y[i],block_w,block_h)){
          block_alive_flag[i]=false;
        }
      }
    }

    /// <summary>
    /// 描画の処理
    /// </summary>
    public override void DrawGame()
    {
      // 画面を白で塗りつぶします
        gc.ClearScreen();

        // 0番の画像を描画します
        gc.DrawImage(0, 0, 0);

          gc.DrawImage(1,ball_x,ball_y);

        gc.SetColor(0, 0, 255);
        gc.FillRect(player_x,player_y,player_w,player_h);



        for(int i = 0; i<BLOCK_NUM; i++){
          if(block_alive_flag[i]){
            gc.FillRect(block_x[i],block_y[i],block_w,block_h);
          }
        }


        gc.DrawString("time:"+time,60,0);
        if(countBlock()==0){
          gc.DrawString("clear",60,30);
        }


    }
    int countBlock(){
      int num = 0;
      for(int i =0 ; i < BLOCK_NUM ; i ++ ){
        if(block_alive_flag[i]){
          num++;
        }
      }
      return num;
    }
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

スマートデバイスプログラミング②(ランダムにカードを出して得点を競うゲーム)

UnityとGameCanvasのダウンロード

1:UnityHubをDLしてインストール。(一部のwindows環境では拡張子を.exeに変更する必要があります)

2:UnityHubを起動して、Unity2019.3.11f1をインストール。

3:GameCanvasのダウンロード
https://github.com/sfc-sdp/GameCanvas-Unity/
でGameCanvasをダウンロード

Game.csの編集

Assets内のGame.csを別エディタで編集

using Sequence = System.Collections.IEnumerator;

/// <summary>
/// ゲームクラス。
/// 学生が編集すべきソースコードです。
/// </summary>
public sealed class Game : GameBase
{
    // 変数の宣言
    int money;
    const int CARD_TYPE = 10;
    int[] card_count = new int [CARD_TYPE];
    string[] card_name =
         {"A","B","C","D","E","F","G","H","I","J"};
    bool isComplete;
    int new_card ;

    /// <summary>
    /// 初期化処理
    /// </summary>

    ///起動時に一回だけ呼ばれる・・初期化設定用
    public override void InitGame()
    {
      resetValue();
    }

    /// <summary>
    /// 動きなどの更新処理
    /// </summary>

    //1フレームごとに呼ばれる・動きの処理を入れる
    public override void UpdateGame()
    {
        //タップした時の処理
        if (gc.GetPointerFrameCount(0)==1 && ! isComplete) {
             money -= 100;
             if (gc.Random(0,3)==0){
             new_card = gc.Random(0,4);
           }else{
             new_card = gc.Random(5,9);
           }
             card_count[new_card]++;


           for (int i = 0; i < 5; i++) {
             if (card_count[i] > 4){
               isComplete = true;
             }
           }
        }
        //長押しした時の処理
        if(gc.GetPointerFrameCount(0) >= 120){
          resetValue();
        }
    }

    /// <summary>
    /// 描画の処理
    /// </summary>

    //1フレームごとに呼ばれる・描画の処理
    public override void DrawGame()
    {
      gc.ClearScreen();
      gc.SetColor(255,0,0);
      gc.SetFontSize(36);
      gc.DrawString("money:"+money,60, 40);

      if(new_card >= 0){
        gc.DrawString("new:"+card_name[new_card],60, 80);
      }

      for(int i=0 ; i< CARD_TYPE ; i++){
        gc.DrawString(card_name[i] + ":" + card_count[i],60, 120+i*80);
      }

      if(isComplete ){
        gc.DrawString("complete!!",60, 920);
      }

    }
    void resetValue(){
      money = 10000;
      for (int i = 0; i < CARD_TYPE; i++) {
        card_count[i] = 0;
      }
      isComplete = false;
      new_card = -1;
    }
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

スマートデバイスプログラミング①(制限時間内にクリックした回数を競争するゲーム)

UnityとGameCanvasのダウンロード

1:UnityHubをDLしてインストール。(一部のwindows環境では拡張子を.exeに変更する必要があります)

2:UnityHubを起動して、Unity2019.3.11f1をインストール。

3:GameCanvasのダウンロード
https://github.com/sfc-sdp/GameCanvas-Unity/
でGameCanvasをダウンロード

Game.csの編集

Assets内のGame.csを別エディタで編集

using Sequence = System.Collections.IEnumerator;

/// <summary>
/// ゲームクラス。
/// </summary>
public sealed class Game : GameBase
{
    // 変数の宣言
    int sec = 0;

    int time = 600;
    int score = 0;

    /// <summary>
    /// 初期化処理
    /// </summary>
    public override void InitGame()
    {
        // キャンバスの大きさを設定します
        gc.SetResolution(720, 1280);
    }

    /// <summary>
    /// 動きなどの更新処理
    /// </summary>
    public override void UpdateGame()
    {
        // 起動からの経過時間を取得します
        sec = (int)gc.TimeSinceStartup;

        time = time - 1;
        if(gc.GetPointerFrameCount(0)==1){
          if(time >= 0){
            score = score + 1;
          }
        }
        if(gc.GetPointerDuration(0) >= 2.0f){
          time =600;
          score =0;
        }
    }

    /// <summary>
    /// 描画の処理
    /// </summary>
    public override void DrawGame()
    {
        // 画面を白で塗りつぶします
        gc.ClearScreen();

        // 0番の画像を描画します
        gc.DrawImage(0, 0, 0);

        // 黒の文字を描画します
        gc.SetColor(0, 0, 0);
        gc.SetFontSize(48);
        gc.DrawString("この文字と青空の画像が", 40, 160);
        gc.DrawString("見えていれば成功です", 40, 270);
        gc.DrawRightString($"{sec}s", 630, 10);

        if(time >= 0 ){
          gc.DrawString("time:"+time,60,0);
        }
        else {
          gc.DrawString("finished!!",60,0);
        }
        gc.DrawString("score:"+score,60,60);
    }
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityど素人の奮闘記

Unityでドミノ倒し作ってみた.

今回はUnityの物理エンジンを使ってドミノ倒しを作ってみました.
ただ同じ大きさのドミノを倒しても面白くないと思いn番煎じながら1.5倍の大きさのドミノを倒していくものにしました.
参考:https://estorypost.com/social-network/youtube/domino-can-topple-1-5-times-its-size/
もはやリアルで示されているものをわざわざやる必要もないのですが笑

Step1 地面をつくる

Createから[3D Object]の[Plane]もしくは[Terrain]を選択します.
今回は自分自身の復習の意味もあってTerrainを選択しました.
スクリーンショット 2020-05-24 21.12.57.png
Terrainを使う際の注意はTerrainの中央がシーンビューの中央とずれていることです.
一辺が500の正方形なのでtransformでPositionをx=-250,y=0,z=-250にしておきましょう.

Step2 自然をつくる

AssetStoreからStandardAssetsをインストールしてください.
驚くほど簡単に山や木,湖がつくれてびっくりします.
スクリーンショット 2020-05-24 21.23.44.png
今回は一応全てインポートしておきましょう.
スクリーンショット 2020-05-24 21.31.00.png
山や木を作ることは非常に簡単でTerrainから[Raise or Lower]を選択して左クリックで山ができます.
山をへこませたい時はシフトキーを押しながら左クリックをすればいいです.
木も同様に木のマークをクリックしインポートしたStandardAssetsの素材をEditから加えてタッチすれば出来ます(適当)
山の表面,地面の表面の素材も同様にもはやドラッグするだけです.

Step3 ドミノ作成

ドミノの作成で注意することは一つだけです.
それは各辺の長さをr倍した時,体積はr^3倍となることです.そのことにだけ注意すれば問題ないと思います.
あとはAdd ComponentでRigidBodyを追加してあげましょう.
Cubeからドミノを作成した場合,Box Colliderは最初から追加されています.
あとは一番小さなドミノを傾けて...
と言いたいところですがまだ何もコードを書いていないので書きましょう.
すべてのドミノが倒れた時に歓声が起こるコードを書きます.

Step4 歓声作成

スクリーンショット 2020-05-24 21.58.51.png
上のようなソースコードでpublic変数としてAudioを追加できるようにしました.
Audioは効果音ラボさんからダウンロードさせていただきました.
効果音ラボ(https://soundeffect-lab.info/)
あとはCreateから[CreateEmpty]を選択し,[Timekeep]と名前を変更しました.
オブジェクトに先ほどのTimekeepスクリプトを追加し,音声も追加しました.
スクリーンショット 2020-05-25 11.08.26.png

完成!

これからも勉強していきます...

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【xcodeエラー】Unity-iPhone has conflicting provisioning settings.が出た時はこうして下さい

今回のエラー

Unityでスマホアプリを作る際にxcodeでビルドを行いました。
ところが下記のようなエラーが出ました。
(私は1度AppStoreにアプリの審査を出しました。1回目は特に何もせずできましたが2回目に下のようなエラーになりました。)

エラー内容

●出現場所
こちらの写真はエラーが終わったあとなのでエラーコードは表示されていません。
一番下の行にエラーが出ていました。
xcodeerror2.png

●エラーの内容
Unity-iPhone has conflicting provisioning settings.
(その下に英文が何行か書いてあり、code signing identif を "iPhone Developer"にしなさい。みたいな英文が書いてありました。)

対処方法

よくわからないですが
TARGETS > Build Settings > Signing
の値を下の画像のように変えました。
XcodeError.png
そうしたらできました。
なぜできるようになったかは分かりませんが、
下の画像の項目を1つかえて、チェックしてと繰り返し行いました。
無駄に時間を費やしてしまったなと思ったので、私と同じことが起こらないようにとQiitaにまとめておきました。

どなたかのお役に立てれば幸いです。
わからないことがあれば
twitterまで連絡ください。
@e_san_desuyo

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityメモ

JavaScript

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse


    ws.on('message',function(message){
        const obj = JSON.parse(message);

        console.log("command: ", obj.command);
        console.log("level: ", obj.level);
        if( obj.command == 'move'){
          console.log('timeElapsed : '+ obj.timeElapsed)
        }
    });

Unity

    https://docs.unity3d.com/ja/2018.4/Manual/JSONSerialization.html
    [Serializable]
    public class MyClass
    {
        public string command;
        public int level;
        public float timeElapsed;
    }
    void Update() {

        MyClass myObject = new MyClass();
            myObject.command = "move";
            myObject.level = 1;
            myObject.timeElapsed = 47.5f;
            string json = JsonUtility.ToJson(myObject);

            byte[] data = System.Text.Encoding.ASCII.GetBytes(json);

            ws.Send(data);
        }

unity > stringからbyte[]への変換

byte[] data = System.Text.Encoding.ASCII.GetBytes(text);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】非同期処理を理解する〜async/await編〜

async/await とは

  • C# の機能として用意されている
    • C# 5.0の新機能
    • Unity 2018 から C# 6.0 に対応した(それまでは C# 4.0 でした。)
    • Task クラスに対して使うものという軽い認識(本当は INotifyCompletion インターフェースの IsCompletedGetResult
  • Unity で .NET 4.x を選択すれば使用可能
  • async = asynchronous = 非同期
    • await を使うメソッドにつけなければならないキーワード
    • async をつけただけでは普通のメソッドと挙動は変わらない
  • await
    • 非同期メソッドを呼び出し、完了するまで実行を中断するキーワード
  • 戻り値を取得できる(コルーチンは無理)
  • 並列処理だけど、普通のメソッドの呼び出しと同じようにかける = 可読性が上がる

基本的な構文は、

async 戻り値の型 メソッド名(引数)
{
    await ~~~~
}    

戻り値は、基本的に Task / Task<T> 型と考えて問題ないかと思います。
(といいながら、サンプルは void で書いたりしています…)

機能

Task.Delay(Int32 millisecondsDelay)

引数で指定されたミリ秒待機します。

using System.Threading.Tasks;
using UnityEngine;


public class Test : MonoBehaviour
{
    void Start()
    {
        AsyncSample1();
    }

    async void AsyncSample1()
    {
        Debug.Log("AsyncSample1 Start.");
        await Task.Delay(1000);
        Debug.Log("AsyncSample1 End.");
    }
}

今回のサンプルでは、1000ミリ秒 = 1秒 待機するようにしたので、実行結果は以下のようになります。
image.png

ContinueWith

直列処理できます。
非同期処理のメソッドも、ContinueWith でつなぐことで、直列化することができます。

ちなみに ContinueWith使わずに、普通に並列的に処理にしたければ、下のようになイメージになります。

using System.Threading.Tasks;
using UnityEngine;


public class Test : MonoBehaviour
{
    void Start()
    {
        AsyncSample1();
        AsyncSample2();
    }

    async void AsyncSample1()
    {
        Debug.Log("AsyncSample1 Start.");
        await Task.Delay(1000);
        Debug.Log("AsyncSample1 End.");
    }

    async void AsyncSample2()
    {
        Debug.Log("AsyncSample2 Start.");
        await Task.Delay(1000);
        Debug.Log("AsyncSample2 End.");
    }
}

並列なので、
1. AsyncSample1 Start.
2. AsyncSample2 Start.
3. AsyncSample1 End.
4. AsyncSample2 End.
という結果になります。

image.png

ContinueWith を使って直列処理(AsyncSample1 を実行完了後、 AsyncSample2 を実行)するサンプルは、

using System.Threading.Tasks;
using UnityEngine;


public class Test : MonoBehaviour
{
    void Start()
    {
        AsyncSample1().ContinueWith(_ => AsyncSample2());
    }

    async Task AsyncSample1()
    {
        Debug.Log("AsyncSample1 Start.");
        await Task.Delay(1000);
        Debug.Log("AsyncSample1 End.");
    }

    async Task AsyncSample2()
    {
        Debug.Log("AsyncSample2 Start.");
        await Task.Delay(1000);
        Debug.Log("AsyncSample2 End.");
    }
}

注意点は、メソッドの返り値の型が Task になっていることくらいです。

  1. AsyncSample1 Start.
  2. AsyncSample1 End.
  3. AsyncSample2 Start.
  4. AsyncSample2 End. という結果になります。

image.png

Task.Run()

引数として与えられた処理を別スレッドで実行します。
用途として、重い処理を非同期にしたいときに使います。

ちょっと良い例が思いつかないので、超簡単なサンプルだけ書きます。

Task.Run(()  =>  Debug.Log("重い処理..."));

Task.WhenAll()

指定された Task が全て完了してから Task を実行することができます。

using System.Threading.Tasks;
using UnityEngine;


public class Test : MonoBehaviour
{
    async void Start()
    {
        await Task.WhenAll(AsyncSample1(), AsyncSample2());
        Debug.Log("All Completed.");
    }

    async Task AsyncSample1()
    {
        Debug.Log("AsyncSample1 Start.");
        await Task.Delay(1000);
        Debug.Log("AsyncSample1 End.");
    }

    async Task AsyncSample2()
    {
        Debug.Log("AsyncSample2 Start.");
        await Task.Delay(1000);
        Debug.Log("AsyncSample2 End.");
    }
}

AsyncSample1(), AsyncSample2() の実行が完了したら、 All Completed. を出力するようなサンプルです。
実行結果は、
image.png

Task<T>

戻り値が欲しい場合は Task<T> を使います。

using System.Threading.Tasks;
using UnityEngine;


public class Test : MonoBehaviour
{
    async void Start()
    {
        var str = await AsyncSample1();
        Debug.Log(str);
    }

    async Task<string> AsyncSample1()
    {
        Debug.Log("AsyncSample1 Start.");
        await Task.Delay(1000);
        return "AsyncSample1 End.";
    }
}

AsyncSample1() の結果が返ってくるまで、Debug.Log されると困るので、 await する必要があります。
(待ち受けするイメージです。)
結果は、
image.png

終わりに

Unity 初心者なので、間違いがあったら教えてくれるとありがたいです。
Unity で Task 使うなら、 UniTask 使おう。(理由はいまいち知らない…)
理由は UniTask の記事を書くときに、調べたいと思います。

参考文献

https://docs.microsoft.com/ja-jp/dotnet/api/system.threading.tasks.task?view=netcore-3.1
https://torikasyu.com/?p=1554
https://docs.microsoft.com/ja-jp/dotnet/api/system.threading.tasks.task.whenall?view=netcore-3.1
https://www.slideshare.net/UnityTechnologiesJapan/unite-tokyo-2018asyncawait

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FinalIK.VRIK.GuessHandOrientations()でアバターの手首の回転を調整する

概要

ハンドコントローラの動きに合わせてIKでアバターを動かそうとするとき、ランタイムでアバターを切り替える場合は特に、各アバターによって手のボーンの初期姿勢が異なるために両手があらぬ方向を向くという問題がある気がします。

今回は、VRoomというHMDを被りながら作業通話やゲームをするツールを作っている過程で、上記の問題にどのように対処したのか、書きながら整理しようと思います。
かなり手探りで進めているので、いろいろ教えていただければ幸いです…

GitHub : Ytomi4/VRoom

環境

  • Unity 2019.3.10
  • FinalIK ver1.9

目指すところ

アプリ内でアバターを自由に切り替えることを前提に、プレイヤーの手にアバターの手が重なるよう、ボーンの初期姿勢が異なる様々なアバターに対応可能な調整方法を考える。

解決したい問題の特定

便宜的に、アバターによって手首のボーンの初期姿勢が異なることから起こる問題と表現しましたが、具体的には下画像のような状況を指しています。
FinalIK_HandTransform.png

画像上段は、各アバターの左手にあたるボーン(VRIK.referencesのLeftHandに格納されるGameObject)の姿勢をGizmoで表示したものです。
左手ボーンのローカル空間において、赤が右方向(1,0,0)、緑が上方向(0,1,0)、青が前方向(0,0,1)であることを示しています。

上の例が示す通り、アバターの左手(掌)が左手ボーンのローカル空間でどの方向を向いているかは様々です(モデラーの方の出力による)。

Unitychan(画像左上)1では、左手が左手ボーンのローカル空間における左方向(-1,0,0)を向いており、Cygnet(画像中央上)2では左手ボーンのローカル空間における上方向(0,1,0)を向いています。Unitychan(画像左上)とVita(画像右上)3は、手がそれぞれのボーンのローカル空間で左方向(-1,0,0)を向いているという点では同じですが、x軸(赤)を中心に90°回転させたようになっており、たとえば手のひらに対する親指の方向の表現が異なります。

これにより、読み込んだアバターに単純にVRIKコンポーネントをアタッチするだけでは手首があらぬ方向を向きます(画像下段)。
Weightを1にして左手のボーンの回転RotationをTarget(画像ではVIVEコントローラーのモデルの先端)に完全に追従させると、「左手ボーンのローカル空間における右・上・前方向」と「コントローラーのローカル空間における右・上・前方向」とがぴったり重なるように左手ボーンが回転するため、アバターの手があらぬ方向を向いてしまう、と理解しました。

上記の理由からアバターによって種々の方向を向く手首を、なんとかコントローラーの前方向を向くようにしたいというのが今回の問題となります。

解決の指針

たとえば、VRIKの公式チュートリアル動画(1:39~)では、コントローラーの位置と回転を反映させたシーン内のGameObjectの子オブジェクトをTargetに設定し、Transformを直接Inspectorからいじるという形で調整しています。

ただし、今回のケースでは、プレイ中のアバターの切り替えを前提としているのでInspectorから直接調整するという手法はとれず、アバター読み込み時にアバターの手のボーンの状態から適当な回転を計算し、親(コントローラー)に対してTargetを回転させる必要があります。

ここでかなり役に立ったのがGuessHandOrientations()でした(リファレンス)。これによって、回転を計算するのに必要な情報である、手首から手のひら方向手のひらから親指方向とみなせる手のボーンのローカル空間における軸を求められます。内容は決して複雑ではないのですが、言及している記事を自分が見つけられなかった、かつ公式のリファレンスがスカスカだったので、次の話に入る前に少しまとめておきます。

VRIK.GuessHandOrientations()

VRIK.cs
public void GuessHandOrientations() {
            solver.GuessHandOrientations(references, false);
}

まず一次的な処理としては、IKSolverVRクラスのインスタンスsolverからGuessHandOrientations (VRIK.References references, bool onlyIfZero)を呼び出し、次にsolver.rightHandとsolver.leftHandのwristToPalmAxispalmToThumbAxisに代入する値(Vector3)を計算しています。それぞれ、手首から手のひら方向手のひらから親指方向とみなせる手のボーンのローカル空間における軸を意味します。
FinalIK_HandTransform_cyg.png
上の画像の例では、wristToPalmAxisが(0,1,0)、palmToThumbAxisが(0,0,1)です。

もう少し詳しく見ると、wristToPalmAxisは手のボーンから肘にあたる場所のボーンへのベクトルtoForearmと、手のボーンのローカル空間におけるXYZ軸それぞれとの内積を取り、内積が最も小さくなる軸をVector3で返した後で、肘とは反対側を指すようひっくり返した(-1倍した)ものであると説明できます。Final IKのスクリプトから一部抜粋して引用します。

IKSolverVR.cs
leftArm.wristToPalmAxis = GuessWristToPalmAxis(references.leftHand, references.leftForearm);
IKSolverVR.cs
private Vector3 GuessWristToPalmAxis(Transform hand, Transform forearm) {
            Vector3 toForearm = forearm.position -hand.position;
            Vector3 axis = AxisTools.ToVector3(AxisTools.GetAxisToDirection(hand, toForearm));
            if (Vector3.Dot(toForearm, hand.rotation * axis) > 0f) axis = -axis;
            return axis;
        }

余談ですが、先の説明と引用したコードが示す通り、wristToPalmAxisは肘と手首の関係のみから求められ、手のひらと手首の位置関係は関係ないため、たとえば手首が小さく丸まっているようなアバターを想定した場合でも、wristToPalmAxisがコントローラーの前方向を向くように回転させれば、手首の丸まりを保持したままコントローラーに沿った手のかたちになるといえそうです。
モデラ―の方が出力してくれた肘から手にかけてのニュアンスを(極力)維持したまま、向きをコントローラーにあわせてそれらしく調整できるようになっているかというのはかなり重要な部分だと思います。

具体的な解決方法

VRoomのCharacterControl.csでは、アバター読み込み時に以下のような処理を行うことにしました。

  • 読み込んだアバターにVRIKコンポーネントをアタッチする
  • AutoDetectReferences()でリファレンスを埋める
  • AdjustIKTargetsToAvatarsHands()でアバターに合わせてIKTargetの位置・回転を変える
    • GuessHandOrientations()で、solver.rightHandsolver.leftHandそれぞれのwristToPalmAxispalmToThumbAxisを求める
    • wristToPalmAxisがコントローラーの前方向(0,0,1)を向くような回転leftWristRotを求め、IKTargetを回転させる
      • このとき、あくまで回転させるのはコントローラーの子オブジェクトであるIKTargetのlocalRotationであるため、wristToPalmAxis(あるいは回転後のpalmToThumbAxis)と比較するコントローラーの前方向は常に(0.0,1)と表現できる 
    • コントローラーの前方向(0,0,1)を軸として、leftWristRotで回転したpalmToThumbAxisが、コントローラーの右方向(1,0,0)となす角だけ回る回転leftPalmRotAngleを求め、IKTargetを回転させる(左手の場合)
    • さらに回転したpalmToThumbAxisが左方向(-1,0,0)を向いている場合は、前方向(0,0,1)を軸に180°回転(左手の場合)
      • Vector3.Angleの返すleftPalmRotAngleはあくまで角度差であるため、90°反対に回転してpalmToThumbAxisと向くべき方向との間で180°の開きが生まれるパターンがある
    • 手の位置にオフセットを加える
      • オフセットを加えない場合、コントローラーの先端がアバターの手首の位置にあり、不自然に感じる
      • オフセットを加える方向はアバターによって異なるので、毎回TargetのlocalPositionはVector3.zeroに戻しておく必要がある
  • IKTargetの代入やstretchCurveの初期化など…
CharacterControl.cs
private void AdjustIKTargetsToAvatarsHands()
{
        _vrik.GuessHandOrientations();

        //Targetのローカルポジションを(0,0,0)に戻しておく
        _leftHandIKTarget.transform.localPosition = Vector3.zero;
        _rightHandIKTarget.transform.localPosition = Vector3.zero;

        //左手
        //wristToPalmAxisがコントローラーの前方向(0,0,1)を向くよう回転
        Quaternion leftWristRot = Quaternion.FromToRotation(_vrik.solver.leftArm.wristToPalmAxis, Vector3.forward);
        _leftHandIKTarget.transform.localRotation = leftWristRot * Quaternion.identity;

        //palmToThumbAxisがコントローラーの右方向(1,0,0)となす角だけ前方向(0,0,1)を軸に回転
        float leftPalmRotAngle = Vector3.Angle(leftWristRot * _vrik.solver.leftArm.palmToThumbAxis, Vector3.right);
        Quaternion leftPalmRot = Quaternion.AngleAxis(leftPalmRotAngle, Vector3.forward);
        _leftHandIKTarget.transform.localRotation = leftPalmRot * _leftHandIKTarget.transform.localRotation;

        //palmToThumbAxisが左方向(-1,0,0)を向いている場合は、前方向(0,0,1)を軸に180°回転
        Vector3 leftThumbDirRotated = leftPalmRot * (leftWristRot * _vrik.solver.leftArm.palmToThumbAxis);
        if (Vector3.Dot(leftThumbDirRotated, Vector3.right) < 0)
        {
            _leftHandIKTarget.transform.localRotation = Quaternion.AngleAxis(180, Vector3.forward) * _leftHandIKTarget.transform.localRotation;
        }

        //手の位置にオフセットを加える
        _leftHandIKTarget.transform.localPosition -= _handOffset * Vector3.forward;


        //右手
        //wristToPalmAxisがコントローラーの前方向(0,0,1)を向くよう回転
        Quaternion rightWristRot = Quaternion.FromToRotation(_vrik.solver.rightArm.wristToPalmAxis, Vector3.forward);
        _rightHandIKTarget.transform.localRotation = rightWristRot * Quaternion.identity;

        //palmToThumbAxisがコントローラーの左方向(1,0,0)となす角だけ前方向(0,0,1)を軸に回転
        float rightPalmAngle = Vector3.Angle(rightWristRot * _vrik.solver.rightArm.palmToThumbAxis, Vector3.left);
        Quaternion rightPalmRot = Quaternion.AngleAxis(rightPalmAngle, Vector3.forward);
        _rightHandIKTarget.transform.localRotation = rightPalmRot * _rightHandIKTarget.transform.localRotation;

        //palmToThumbAxisが右方向(1,0,0)を向いている場合は、前方向(0,0,1)を軸に180°回転
        Vector3 rightThumbDirRoted = rightPalmRot * (rightWristRot * _vrik.solver.rightArm.palmToThumbAxis);
        if (Vector3.Dot(rightThumbDirRoted, Vector3.left) < 0)
        {
            _rightHandIKTarget.transform.localRotation = Quaternion.AngleAxis(180, Vector3.forward) * _rightHandIKTarget.transform.localRotation;
        }

        //手の位置にオフセットを加える
        _rightHandIKTarget.transform.localPosition -= _handOffset * Vector3.forward;
}

実際の動き

FinalIK_HandTransform_performance.png

例に挙げた3体のアバターで確認したところ、プレイヤー側に操作を強いることなく、どのアバターの手もプレイヤーの手と違和感なく重なるようになっていました。

課題

  • 確実にベストプラクティスがどこかにあるはずなので、調べる&読む
  • 例外をつくらないleftPalmRot
  • 他のボーンとの位置関係から、特定の部位の向いている方向をとるという考え方は応用が利きそう(たとえば指先前方向にRayを飛ばすなど)
  • そもそものアバター間の違いが、作られたモデリングソフトの違いによって発生していると思っているが、どうなのだろう
  • Quaternionの理解を深めたい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ccacheを導入してUnityのiOSビルドを高速化出来ないか検証してみた

ccache1がUnityのiOSビルド2でも利用できるのか?」「また、出来るとした場合にはどれくらい効果があるのか?」について調べてみました。

私自身、ccacheの導入そのものが初だったこともあるので、前半は主にccacheのインストール方法や設定項目」などについて記載していき、後半から「Unity上での導入手順及び効果」について記載していければと思います。
(予めccacheを知っている方は前半部分読み飛ばしても良いかも)

もし間違いや不足などあればコメントにて教えて頂けると幸いです! :bow:

※まだWIP

書いておいてなんですが...後半の「Unity iOSビルドで導入した際の効果」についてはまだ未調査なところがチラホラと有り、完全に調べきれているとは言い切れないところもあるので、一先ずは「雰囲気で導入してみた際の途中経過までのメモ書き」的な感じで読んで頂ければと思います。。
(未調査項目は必要に駆られたタイミングで調査を再開し、随時追記していく予定..)

TL;DR

先にUnity iOSビルドで導入した際の効果について、今の時点での検証結果から纏めます。
(導入関連は特に纏めることが無いので割愛)

※Unity iOSビルドで出力される一式を記事中ではxcodeprojと表記してます。以降の解説でも同様の表記となります。

  • Unityから出力したxcodeprojにて、一度ビルドした後にCleanしてからリビルドしたらキャッシュヒットしてビルド時間が短くなった
  • 「Build」と「Archive」は別のキャッシュが作られる
    • → 「Build」後 に「Archive」を行っても後者のビルド時間は短くならない
  • Unity iOSビルド時に「Append」でxcodeprojを更新してもキャッシュヒットは維持される
  • Unity iOSビルド時に「Replace」でxcodeprojを置き換えたらキャッシュヒットしなくなった
    • ※「既存のxcodeprojを手動で削除 → ビルド」も同様

結果だけで見ると「Appendでの更新」や「一度ビルドした後のClean/Build」ではキャッシュヒットして高速化出来るものの、Replace相当の手順でxcodeprojそのものを置き換えてしまうとキャッシュヒットしなくなるように思われました。

とはいえまだ雰囲気で使っているところはある上に、原因については詳細まで追いきれていないので、気が向いたら調査を再開していきたいところ...。
→ 幾つかの未調査項目などはこちらに纏めている。

ccacheの導入

macOS上でのccacheの導入方法や各種設定項目についてメモしていきます。
(導入時のOSはmacOS Catalina 10.15.3)

インストール

brewコマンドからインストールできます。

$ brew install ccache

ccacheの設定について

幾つか必要そうな情報だけメモします。
詳細はccache -hでヘルプを参照してください。

設定の確認

ccache -pを叩くことで現在の構成を確認できます。
各項目の概要についてはドキュメントにある「Configuration settings」の章をご確認ください。

ccache -pの出力結果(クリックで展開)

出力結果のプレフィックスに(default)と付いている物はデフォルト設定であり、(/Users/<user>/.ccache/ccache.conf)と付いているものはユーザーが任意で設定変更した項目となります。
※以下で言うとmax_size = 5.0Gが変更されている。

# -p, --show-config         show current configuration options in
#                           human-readable format
$ ccache -p
(default) base_dir =
(default) cache_dir = /Users/<user>/.ccache
(default) cache_dir_levels = 2
(default) compiler =
(default) compiler_check = mtime
(default) compression = false
(default) compression_level = 6
(default) cpp_extension =
(default) debug = false
(default) depend_mode = false
(default) direct_mode = true
(default) disable = false
(default) extra_files_to_hash =
(default) hard_link = false
(default) hash_dir = true
(default) ignore_headers_in_manifest =
(default) keep_comments_cpp = false
(default) limit_multiple = 0.8
(default) log_file =
(default) max_files = 0
(/Users/<user>/.ccache/ccache.conf) max_size = 5.0G
(default) path =
(default) pch_external_checksum = false
(default) prefix_command =
(default) prefix_command_cpp =
(default) read_only = false
(default) read_only_direct = false
(default) recache = false
(default) run_second_cpp = true
(default) sloppiness =
(default) stats = true
(default) temporary_dir =
(default) umask =

キャッシュサイズの変更

キャッシュサイズはccache -Mから変更できます。
単位はGMなど付ければ指定可能です。

# -M, --max-size=SIZE       set maximum size of cache to SIZE (use 0 for no
#                           limit); available suffixes: k, M, G, T (decimal)
#                           and Ki, Mi, Gi, Ti (binary); default suffix: G
$ ccache -M 1G
Set cache size limit to 1.0 GB

※設定ファイルから変更する場合

コマンドなどで設定した情報はccache.confと言うファイルに保存されます。
こちらは幾つか存在しますが、今回は以下のパスにある設定ファイルに変更を加えてます。(詳細は「ドキュメント -> Configuration」を参照)

/Users/<user>/.ccache/ccache.conf

こちらを直接編集することでも設定変更が可能です。

ccache.conf
max_size = 1G

※環境変数から設定を変更する場合

設定ファイル以外にも環境変数からも設定を変更することが出来ます。

環境変数名は「ドキュメント -> Configuration settings」にある項目名横のカッコ内に記載されてます。(e.g. max_size (CCACHE_MAXSIZE))

# 最大キャッシュサイズの変更
$ export CCACHE_MAXSIZE=5G

キャッシュディレクトリの変更

キャッシュディレクトリの変更は以下のコマンドから変更できます。(外部ストレージを指定する例)
こちらもccache.confを書き換えたり、環境変数から指定して変更することも可能です。

# -o, --set-config=K=V      set configuration item K to value V
$ ccache -o cache_dir=/Volumes/<外部ストレージ>/ccache

ccache.confの書き換え

ccache.conf
max_size = 5G
cache_dir = /Volumes/<外部ストレージ>/ccache

環境変数から指定

# 最大キャッシュサイズの変更
$ export CCACHE_DIR=/Volumes/<外部ストレージ>/ccache
-oコマンドについて

-oコマンドはKey(設定項目名)Value(設定値)を指定することで変更を適用できるコマンドです。
(イメージ的にはコマンドからccache.confを書き換えることが出来る機能)

ただし、中にはccache -Mの様にデフォルトで設定変更がコマンド化されているものもあります。

// これら2つは同じ機能
$ ccache -o max_size=1G
$ ccache -M 1G

キャッシュデータについて

統計情報の確認

ccache -sを叩くとキャッシュの統計情報を確認することができます。
各項目の概要についてはドキュメントにある「Cache statistics」の章をご確認ください。

# -s, --show-stats          show summary of configuration and statistics
#                           counters in human-readable format
$ ccache -s
cache directory                     /Users/<user>/.ccache
primary config                      /Users/<user>/.ccache/ccache.conf
secondary config      (readonly)    /usr/local/Cellar/ccache/3.7.9/etc/ccache.conf
cache hit (direct)                     0
cache hit (preprocessed)               0
cache miss                             0
cache hit rate                      0.00 %
cleanups performed                     0
files in cache                         0
cache size                           0.0 kB
max cache size                       5.0 GB

キャッシュの削除

キャッシュはccache -Cで削除できます。

# -C, --clear               clear the cache completely (except configuration)
$ ccache -C
Cleared cache

※ちなみに、大文字の-Cと小文字の-cは別コマンドになっているので注意。

# -C, --clear               clear the cache completely (except configuration)
$ ccache -C

# -c, --cleanup             delete old files and recalculate size counters
#                           (normally not needed as this is done
#                           automatically)
$ ccache -c

統計情報のクリア

ccache -sで出力できる統計情報はccache -zを叩くことで初期化する事が出来ます。

# -z, --zero-stats          zero statistics counters
$ ccache -z
Statistics zeroed

UnityのiOSビルドで導入してみた

ここからはUnityのiOSビルドにて導入する際の手順や結果などについて纏めていきます。

導入手順

導入手順については以下の記事を参考にさせていただきました。
順に解説していきます。

ccacheを導入してC++のコンパイルを最適化

ちなみに、iOSビルドでは主にUnityから出力されたxcodeprojに対してccacheを適用し、Xcodeビルドの高速化を行っていく形となります。
→ 逆に言うとUnity側のプロセス(xcodeprojの出力など)自体はccacheの適用外 と言う認識

Xcode側でccacheを呼び出すためのラッパースクリプトを用意

xctoolchain内にccacheを呼び出すためのラッパースクリプトを用意します。
参考記事に倣ってshell内で完結させてますが、予めccache_wrapperを別途生成して該当パスに保存 → パーミッションの変更で用意することも可能です。

$ sudo vi /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ccache_wrapper

----------------------------------
#!/bin/bash

which ccache
if [ "$?" -eq 0 ]; then
    # ※ `CCACHE_SLOPPINESS(sloppiness)`はccache.confで設定することも可能
    export CCACHE_SLOPPINESS=pch_defines,time_macros,clang_index_store
    exec `which ccache` /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -Qunused-arguments "$@"
else
    exec /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -Qunused-arguments "$@"
fi
----------------------------------

$ sudo chmod 755 /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ccache_wrapper

Editor拡張でxcodeprojに対してラッパースクリプトを自動設定

後はUnity側で以下のEditor拡張を用意します。
こちらを導入することで、iOSビルド時に必要なUser-Defined等が自動で設定されます。
(上記で用意したccache_wrapperの設定など)

#if UNITY_IOS
using System.IO;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;

namespace Samples
{
    static class CcacheSettings
    {
        /// <summary>
        /// xcodeprojにccache向けの設定を適用するサンプル
        /// </summary>
        [PostProcessBuild]
        public static void OnPostprocessBuild(BuildTarget buildTarget, string path)
        {
            if (buildTarget == BuildTarget.iOS)
            {
                var pbxProjectPath = PBXProject.GetPBXProjectPath(path);
                var pbxProject = new PBXProject();
                pbxProject.ReadFromString(File.ReadAllText(pbxProjectPath));

                // refered to:
                //     https://qiita.com/tani-shi/items/e1493e63a02966ef1bac
                var target = pbxProject.ProjectGuid();
                pbxProject.AddBuildProperty(target, "CC", "$(DT_TOOLCHAIN_DIR)/usr/bin/ccache_wrapper");
                pbxProject.SetBuildProperty(target, "LDPLUSPLUS", "$(DT_TOOLCHAIN_DIR)/usr/bin/clang++");

                File.WriteAllText(pbxProjectPath, pbxProject.WriteToString());
            }
        }
    }
}
#endif

ビルド時間の計測

ビルドに用いる検証用プロジェクトには、以前書いたこちらの記事にあるゲームプロジェクトを利用します。
(実案件レベルとは行かずとも、コンパイル時間を検証するにあたって多少のコード量は欲しかったので)

mao-test-h/DOTS-Jungle

環境/スペック

  • macOS Catalina 10.15.3
    • MacBook Pro (15-inch, 2018)
      • CPU: 2.2 GHz 6コア Intel Core i7
      • メモリ 16GB 2400 MHz DDR4
  • Unity2019.3.11f1
  • Xcode 11.4.1

検証内容

以下に検証項目と期待する動作を定義します。
こちらをccache -zCでキャッシュ及び統計情報を消した状態で上から順にビルドしていき、ビルド時間やキャッシュヒット率などの変動を見ていきます。

  • ①: Unityから出力されたxcodeprojで「Build → Clean」を3回繰り返す
    • → そもそもccacheは機能するのか?
  • ②: ①から継続して「Clean → Archive」を行う
    • → ①で得たキャッシュはアーカイブ時にもヒットするのか?
  • ③: ①, ②でビルドしたxcodeprojに対し、Unity側でAppendビルド
    • → Unity側でAppendした際にキャッシュは維持されるのか?
  • ④: ③のxcodeprojに対し、Unity側でReplaceビルド
    • xcodeprojを丸々置き換えた際にもキャッシュは維持されるのか?

ちなみに今回は「そもそもとして機能するのか?」に注目しているので、(検証としては微妙かもしれませんが...)ビルド毎にコード変更などは加えてません。
記載しているビルド時間はそれを踏まえた結果として見てください。

ビルド設定

デフォルトから変更した項目のみ記載します。
各項目の設定についてはドキュメントをご覧ください。

compiler_check = content
sloppiness = pch_defines,time_macros,clang_index_store

①の結果

2回目からキャッシュヒット率が上がっており、ビルド時間が大幅に短くなっていることが確認できました。
この事からccache自体は正しく機能しているみたいです。

ccache -s 1回目 2回目 3回目
ビルド時間(秒) 202.0sec 25.4sec 24.1sec
cache hit (direct) 0 404 808
cache hit (preprocessed) 0 5 5
cache miss 409 409 414
cache hit rate 0.00 % 50.00 % 66.26 %
cleanups performed 0 0 0
files in cache 1913 1913 1937
cache size 583.7 MB 583.7 MB 584.2 MB
max cache size 5.0 GB 5.0 GB 5.0 GB

②の結果

①の状態から継続してアーカイブを行ってますが、1回目の結果を見る分にはキャッシュヒットが上昇していない上にキャッシュミスが目立っているので、恐らくは①のキャッシュが参照されていないように思われました。 (キャッシュミスの理由についてはまだ未調査...)

2回目からは新たに生成されたキャッシュを見に行っているためか、再びヒット率が上昇してビルド時間が短くなってます。

ccache -s 1回目 2回目 3回目
ビルド時間(秒) 308.7sec 140.6sec 132.5sec
cache hit (direct) 808 (prev:808) 1211 1620
cache hit (preprocessed) 5 5 5
cache miss 823 (prev:414) 829 829
cache hit rate 49.69 % 59.46 % 66.22 %
cleanups performed 0 0 0
files in cache 3849 3877 3877
cache size 1.8 GB 1.8 GB 1.8 GB
max cache size 5.0 GB 5.0 GB 5.0 GB

③の結果

Unity上からAppendしてxcodeprojを更新した際の結果です。
ビルド時間やキャッシュヒット/ミスの数値を見た感じだと、①と②で生成されたキャッシュがちゃんとヒットしているように思われます。

ccache -s 1回目(Clean → Build) 2回目(Clean → Archive)
ビルド時間(秒) 25.0sec 142.1sec
cache hit (direct) 2029 (prev:1620) 2438
cache hit (preprocessed) 5 5
cache miss 829 (prev:829) 829
cache hit rate 71.04 % 74.66 %
cleanups performed 0 0
files in cache 3877 3877
cache size 1.8 GB 1.8 GB
max cache size 5.0 GB 5.0 GB

④の結果

最後にUnity上からReplaceしてxcodeprojを置き換えた際の結果です。
(ちなみに「既存のxcodeprojを手動で削除 → ビルド」の手動Replaceも同様の結果です)

こちらはBuild/Archive共に殆どキャッシュヒットしていない様に見受けられました。

ccacheの設定は以下の様になっており、更新チェックに関する条件自体は問題無さそうに思われますが...なぜ適用されていないのかまでは追いきれておらず...。

  • base_dirは設定していないので、対象は絶対パスで制御される
    • → Unityから出力されるxcodeprojは常に同名で出力しており、パス自体は変わっていないので適用されるはず...
  • 更新ファイルのチェック方法はcontent(ファイル内容を基にチェック)で設定
    • → チェック方法をデフォルトのmtime(mtimeとsizeのhash)に戻しても変わらず...
    • 出力されたIL2CPPのコードにも目立った差分は無い
ccache -s 1回目(Clean → Build) 2回目(Clean → Archive)
ビルド時間(秒) 195.6sec 301.8sec
cache hit (direct) 2439 (prev:2438) 2440
cache hit (preprocessed) 10 10
cache miss 1232 (prev:829) 1640
cache hit rate 66.53 % 59.90 %
cleanups performed 0 2
files in cache 5350 6576
cache size 2.4 GB 3.4 GB
max cache size 5.0 GB 5.0 GB

結果を踏まえて

個人的には「Replace相当のビルドを行った際にもキャッシュヒットしてくれると嬉しい3」と言うモチベーションから調査を開始したこともあったので、今の時点での結果としては少し微妙だった...と言ったところです。

原因については特定しきれていないので、気が向いたら調査を再開していきたい...。

未調査項目

幾つか挙げます。

  • Xcode側のビルド設定の詳細把握
  • Xcode/ccache共に設定に抜けが無いか確認
  • ドキュメントにある「Troubleshooting -> Performance」を読む分にはマクロ周りが影響を及ぼしている可能性も考えられる...?
    • 後は「揮発性の情報を含む自動生成コード」と言うのも引っ掛かってそうな感じもあったり... :thinking:
  • 「Cache debugging」にあるデバッグモードを有効にすることで原因調査を進められそう?
    • まだ未検証...

補足: Androidは?

「内部的にNDK使ってそうだし、一部のビルドプロセスは高速化出来るかな?」と仮説立てて調べてみましたが...結論から言えばダメそう...?(調査不足の可能性あり)

環境変数のNDK_CCACHEなどを設定した状態でAndroidビルドを回してみましたが、ccache -sで統計情報を見る分には何も動いていないように思われました。

後はこの状態で[Export Project]をON/OFFにしてビルドを行い、双方のビルドレポートを確認した感じだと、どちらも支配的なのはIL2CPPと言うプロセスでした。
こちらが文字通り「ILからCPPに変換するツール側のプロセス」でコンパイラが一体関与しない類のものであれば、そもそもとして適用自体でき無さそうとも考えられそうです。(IL2CPPをコードレベルで追ったわけではないので予想にはなりますが...)

EXTuVImUYAA6TjB.png

参考/関連サイト


  1. 簡単に言えばC/C++に於けるビルド高速化ツール。ざっくりとした概要はWikipediaの記事参照 

  2. 正確に言うとUnityが出力したxcodeprojに対するXcodeビルド 

  3. 例えばビルドプロセスを自動化する際に、ビルド毎に影響が残らないように敢えてReplace相当でビルドすることがあるので、ここでキャッシュヒットして高速化されると嬉しかったりする。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む