20190526のUnityに関する記事は7件です。

ジャイロで視差効果 (uGUI、2D)

できること

  • Unityの2Dプロジェクト、または、3Dプロジェクトでも、常に最前面に張り付くタイプ(Screen Space - Overlay)のuGUIで、スクリプトなしで簡易的な視差効果を実現します。

アセットの入手 (GitHub)

ダウンロード ⇒ ParallaxEffect.unitypackage
ソースはこちらです。

挙動と例

デバイスを傾けると、向きと傾きに応じて、オブジェクトがスライドします。
同時に、複数のオブジェクトに対して、個別の設定で効果を付与できます。
ScreenRecord-_10_46午後_-5月-26_-2019__2.gif   inspecter.png

解説

この例では、大きさの異なる4枚の矩形を中心を合わせて配置し、それぞれにコンポーネントをアタッチして、大きいものから順に「Deepness=1~4」で設定しています。(他のパラメータはデフォルトのままです。)
※この例のシーンがアセットに付属しています。

使い方

  • プロジェクトにアセットをインポートしてください。
  • 対象のオブジェクト(Imageやスプライト)に、"ParallaxEffect"スクリプトをアタッチしてください。
  • インスペクタでパラメータを調整してください。
  • 通常、Editor上ではジャイロが機能しないので、動作の確認には実機が必要です。
パラメータ 意味
ShiftRatio 傾きに反応する感度です。逆向きにスライドさせる場合は負数を指定します。
Deepness 感度に乗じられます。
Movable Radius スライドを制限する距離を指定します。0を指定すると制限されません。
Return Ratio 中点に復帰する速度を指定します。0を指定すると自動復帰しなくなります。
Horizontal 水平方向のスライドについて有効/無効を切り替えます。
Vertical 垂直方向のスライドについて有効/無効を切り替えます。

ParallaxEffect.Reactable = ture; // or false … 全体の有効/無効を切り替えます。

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

Unity+Node.jsで1対1の簡単なオンラインシューティングゲームを作った話 Unity編

この記事に関わる記事一覧

導入編
Node.js編
Unity編(現在の記事)

はじめに

Node.jsを使ってみたく、実際に手を動かして何か作ってみるのが一番だと思い、
自分なりにプレイヤーのマッチングや同期の取り方を考えてやってみようと思いました。

Node.js(サーバー側)よりもUnity(クライアント側)で苦戦しました...

私はNode.js,リアルタイム通信の知識がそこまであるわけではないので、
素人なりにどう考えて実装していったかの記録を残していきたいと思い記事を書き始めました。

コードの書き方に正解はないと思うので、これから書いていく記事を通して
今回作成したゲームの作り方の概念だけ書いていこうかなと思います。

成果物

今回作成したアプリはAndroid,iosでリリースしました。
AppStore
PlayStore
Mac版
Windows版
Android版は実機で動きを確認できていないので、動かなかったら申し訳ございません。

動きを確認するためにはデバイスが二つ必要になります。(PCとスマホでも確認できます)
2019/5/11現在サーバー稼働中ですのでマッチング開始ボタンを押すと同時に押した人(5秒以内)とマッチングします。

操作方法

ドラッグ:移動
タップ :自機が向いている方向に弾を発射

スクリーンショット

IMG_5512.PNG

IMG_5514.PNG

UDPManager

まず、通信するクラスが必要だったので作成しました。
作成したクラスはJSONで送受信データを扱います。
シングルトンです。

UDPManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using MiniJSON;

public class UDPManager : MonoBehaviour
{
    private static UDPManager instance = null;
    public static UDPManager Instance
    {
        get
        {
            if (instance)
            {
                return instance;
            }
            else
            {
                instance = GameObject.FindObjectOfType<UDPManager>();
                if (!instance)
                {
                    var newObj = new GameObject();
                    newObj.name = "UDPManager";
                    newObj.AddComponent<UDPManager>();
                    Instantiate(newObj);
                    instance = newObj.GetComponent<UDPManager>();
                }
                return instance;
            }
        }
    }

    [SerializeField]
    string host = "localhost";
    [SerializeField]
    int port = 33333;

    private UdpClient client;
    private Thread thread;

    public delegate void MessageReceived(JsonNode jsonNode, string jsonStr);
    public event MessageReceived messageReceived;

    private void Awake()
    {
        if (this != Instance)
        {
            Destroy(this.gameObject);
            return;
        }

        DontDestroyOnLoad(this.gameObject);
    }

    void Start()
    {
        client = new UdpClient();
        client.Connect(host, port);

        thread = new Thread(new ThreadStart(ThreadMethod));
        thread.Start();
    }

    void Update()
    {

    }

    private void OnApplicationFocus(bool focus)
    {
        if (client == null)
            return;

        if(focus)
        {
            client.Connect(host, port);
        }
    }

    void OnApplicationQuit()
    {
        client.Close();
        thread.Abort();

        if (this == Instance) instance = null;
    }

    private void ThreadMethod()
    {
        while (true)
        {
            if (!client.Client.Connected)
                client.Connect(host, port);

            IPEndPoint remoteEP = null;
            byte[] data = client.Receive(ref remoteEP);
            string text = Encoding.UTF8.GetString(data);

            if (messageReceived != null)
            {
                JsonNode jsonNode = JsonNode.Parse(text);
                messageReceived(jsonNode, text);
            }

            Debug.Log("GET:" + text);
        }
    }

    public void SendJson(string jsonStr)
    {
        if(!client.Client.Connected)
            client.Connect(host, port);

        byte[] dgram = Encoding.UTF8.GetBytes(jsonStr);
        client.Send(dgram, dgram.Length);

        Debug.Log("SEND:" + jsonStr);
    }
}

hostにはサーバーのアドレスを入れます。

JSONを扱うためにMiniJsonとそのパラメーターを扱いやすくする、
Koki IbukuroさんのJsonNodeを使用させていただきました。

シングルトンなので

UDPManager udpManager = UDPManager.Instance;

でどこからでも取得できています。
オブジェクトとして生成されていなかったとしても、自動的に生成されるように実装されています。

以下使い方です。

JSONの送信

string jsonStr = "{\"type\":\"greet\",\"msg\":\"hello\"}";
UDPManager.Instance.SendJson(jsonStr);

JSONの受信

//1.受信処理をするクラスを作成
void OnReceiveMessage(JsonNode jsonNode, string jsonStr)
{
 //処理
}

//2.関数をUDPManagerに登録
UDPManager.Instance.messageReceived += OnReceiveMessage;

//3.オブジェクトが破棄されたときに関数の登録を解除するようにしておく
//OnDestroyはMonoBehaviourが破棄されるときに自動的に呼ばれる関数
void OnDestroy()
{
 UDPManager.Instance.messageReceived -= OnReceiveMessage;
}

あとはUDPManagerがメッセージを受信するたびに、OnReceiveMessageを呼び出します。
注意しなければならないことが一つありまして、
OnReceiveMessage内はUnityのメインスレッドで実行されないので、
オブジェクトの生成や位置の変更などオブジェクトの操作ができなくなります。

なので今回私は以下のようにして実行しました。

//処理を入れておくキューを生成しておく
Queue<Action> mainThreadQueue = new Queue<Action>();

//処理を追加
Action action = () => {
 //処理
};
mainThreadQueue.Enqueue(action);

//Update内で実行
void Update()
{
  while(mainThreadQueue.Count > 0)
  {
    Action action = mainThreadQueue.Dequeue();
    action();
  }
}

UDPManager.csは自己責任でご自由に使っていただいて大丈夫です。

マッチング

マッチングは以下のようなシーケンスで実装しました。
Matching.png

draw.ioを使って書いてみました。
UML詳しくないので、書き方が間違っているかもしれませんが、
今回のゲームのマッチングの流れが伝われば幸いです。

Unityで行なっている処理としては以下の流れです
1.マッチング開始したことをサーバーに伝える
2.サーバーから自分のユーザー情報を受け取る
3.マッチングが成立情報をサーバーから受け取りシーン遷移
3.マッチングが成立しなかった情報をサーバーから受け取る

基本的にサーバーがマッチングの処理を行なってくれるので、
クライアント側ではメッセージの送受信ができれば実装できると思います。
サーバー側の処理についてはNode.js編をご参照ください。

入力情報・位置・回転同期

この記事を書くために自分の書いたコードを見直してみましたが、
コードが汚い状態になっておりまとめるのが難しい状態であると感じました...

自分の考えが整理されていることと、コードの綺麗さは比例するのかもしれませんね...
入力の同期だけでサンプルを作り、別の記事で詳しく説明したいと思います。

最後に

肝心な部分が説明できない記事になってしまい申し訳ございません。
導入編
Node.js編
はスムーズに記事を書くことができたのですが、今回は何を書いて良いか分からなくなってしまいました。
ソースコードを全て載せたとしても説明が難しいです...
恐らくこれは自分の考えが整理されていないからだと思います。

Node.jsとUnityを使ったチュートリアル的なサンプルを次回から作成して説明していきたいと思います。

今回の記事でUDPManagerの内容は参考になると思います。
それ以外の内容はあまり参考にならないと思いますが...

これからも記事を書くことを習慣にしていきたいと思いますので、よろしくお願いいたします!!

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

Unityで画像をソース上で動的に切り替える方法を色々試したけど、縮尺が変になる挙動が直らない

Unity2019.3.0.a2と2018.4.0f1

2Dの話です。

画像が歪む

まずは画像を見てもらえると分かるのですが盾の画像がおかしなことになっています。
4つの盾は本来みんな同じ画像です。

2019.3.0.a2
unity_tate_2019.png

仕様

盾を表示するロジックとしては画像下部の装備欄をクリックするとアイテム欄(CardBoardという名前をつけています)が出てきて、選択すると装備欄に反映されます。
selected_card.png

ですが、この装備を何度か繰り返すと装備欄の画像の縮尺が変になってしまう不具合です。

原因調査

当然、ロジックがおかしいのか暗黙的に数値が変わっているのかと思いましたのでそのあたりを色々と試しました。

階層

動的に画像を切り替えるために以下のような階層にしました。

CanvasUI
  EquipmentCardField
    EquipmentCardBox0
      ImageInvisibleSprite
    EquipmentCardBox1
      ImageInvisibleSprite
    EquipmentCardBox2
      ImageInvisibleSprite
    EquipmentCardBox3
      ImageInvisibleSprite

CanvasUIが画面全体のCanvasです。
EquipmentCardFieldが画像下部の装備欄です。
EquipmentCardBox0,1,2,3が画像の下地になっているImageです。
ImageInvisibleSpriteはImageですがアルファ値をいじって透明になっています。画像はemptyという真っ白なものを初期でセットしています。

基本的にはImageInvisibleSpriteの画像emptyを別な画像に切り替えてアルファ値を1.0にして透明じゃなくす方法を取っています。

動的に画像を変更する

直接画像ファイルを読み込んで画像を変更する

EquipmentCardBox equipmentCardBox = _equipmentCardBoxs[index];
equipmentCardBox.Equip(actionCard);
GameObject childGameObject = _equipmentCardBoxs[index].transform.Find("ImageInvisibleSprite").gameObject;
Sprite sprite = Resources.Load<Sprite>(equipmentCardBox.GetImageFilePath());
Image childImage = childGameObject.GetComponent<Image>();
childImage.sprite = sprite;
childImage.SetAlpha( 1.0f );

_equipmentCardBoxsは長さ4のEquipmentCardBoxの配列です。
EquipmentCardBoxはEquipmentCardBox0~3に一つずつ関連付けています。
equipmentCardBoxが装備を行う欄の一つになります。
.Equip()で装備を行っています。
画面に表示されている拳や盾などはCardという名称をつけています。
ActionCardはCardBoardでクリックしたCardです。

childGameObjectがImageInvisibleSpriteのGameObjectになります。
Resources.Loadメソッドで画像ファイルのパスを指定してSpriteとして読み込んでいます。
childImage.sprite = sprite で画像の切り替えを行っています。
その下のchildImage.SetAlpha( 1.0f );でアルファ値を切り替えています。SetAlphaは拡張メソッドです。
名前の通りアルファ値を変えるためのメソッドです。

この方式で試したところ装備欄の画像は歪みました。

Texture2Dから新規にSpriteを生成して画像を変更する

EquipmentCardBox equipmentCardBox = _equipmentCardBoxs[index];
equipmentCardBox.Equip(actionCard);
GameObject childGameObject = _equipmentCardBoxs[index].transform.Find("ImageInvisibleSprite").gameObject;
Texture2D tex2d = Resources.Load(equipmentCardBox.GetImageFilePath()) as Texture2D;
Image childImage = childGameObject.GetComponent<Image>();
childImage.sprite =  Sprite.Create(tex2d, new Rect(0,0,tex2d.width,tex2d.height), Vector2.zero);
childImage.SetAlpha( 1.0f );

Resources.Load()でTexture2Dに変換してspriteの代入箇所でSprite.create()で新しいスプライトを生成しています。

この方式で試したところ装備欄の画像は歪みました。

GameObjectのコンポーネントのspriteを代入する

EquipmentCardBox equipmentCardBox = _equipmentCardBoxs[index];
equipmentCardBox.Equip(actionCard);
GameObject childGameObject = _equipmentCardBoxs[index].transform.Find("ImageInvisibleSprite").gameObject;
Image childImage = childGameObject.GetComponent<Image>();
childImage.sprite =  actionCard.gameObject.GetComponent<Image>().sprite;
childImage.SetAlpha( 1.0f );

直接Resourcesフォルダの画像を読むのを止めてクリックしたCardのGameObjectからspriteを取り出して代入させてます。

この方式で試したところ装備欄の画像は歪みました。

ScaleやSetNativeSizeを使う

childImage.rectTransform.localScale = new Vector3(1,1,1);
childImage.transform.localScale = new Vector3(1,1,1);
childImage.SetNativeSize();

上記の画像を切り替える方法でこれらのソースを組み合わせたパターンを試しましたがどれも同じ結果になりました。
装備欄の画像は歪みます。

数値を確認する

暗黙的にwidthやheight,scaleの数値が変わっているのかと思いPlayer再生中に値を確認したところ、何の変化もありませんでした。
見えてる値と実際の値が食い違っている可能性も考えてDebug.Log()で出力してみたところ、何の変化もありませんでした。

その他

他にも色々と試しましたが割愛します。

LTSを使ってみることに

UnityのLTSを調べてみました。一番新し目なLTSは2018.4.0f1でしたのでそこにパッケージをインポートして再作成。

何だか出来た。

2018.4.0f1
unity_tate_2018.png

色々装備を変更してこねくり回しても期待どおりの動作をしました。
満足。

左のウィンドウの色が変わっているのは単純に変えたものをキャプチャしただけです

まとめ

最新版を使うときは予期せぬ不具合があるかもしれないので気をつけたほうが良いかもしれないです。
本当は私の知らない仕様があってそういう挙動になっていしまっているのかもしれません。
私が分かったのはUnity 2018.4.0f1にしたら求めていた挙動になったということだけです。

分かりきったことではありますが、自分が詳しくないものを利用するときはLTSか安定版を使うのが無難ですね。

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

Unity+MagicOnionのサンプルプログラムをコンテナ化してAzureContainerInstancesで簡単に動かす方法

まえがき

MagicOnion は C# による .NET Core/Unity 用のリアルタイム通信フレームワークです。

この記事では公式のサンプルプログラムのサーバーサイド部分をコンテナ化して、MicrosoftAzure上で簡単に動かす方法を紹介します。

MagicOnion や Docker や Azure に不慣れな方でも手順通りにぽちぽちすれば 30 分程度で一通り試すことができると思います。

Docker 関連の作業は DockerHub 上で行いますので、Local 環境で Docker 関連の事前準備は不要です。

事前準備

この記事では GitHub 上に作業用リポジトリを用意して、そちらを DockerHub の AutomatedBuilds の対象に指定することで Docker イメージを生成しています。

そのため事前に以下のアカウントを用意してください。

  • GitHubアカウント
  • DockerHubアカウント
  • MicrosoftAzureアカウント

1. 作業用 GitHub リポジトリの作成、サンプルコードの修正、Dockerfileの追加

  1. GitHub に新規リポジトリを作成し、MagicOnion のリポジトリから MagicOnion\samples\ChatApp 配下の3つのフォルダをリポジトリのルートにコピーします。

    • ChatApp.Server
    • ChatApp.Unity
    • GeneratorTools
  2. ChatApp.Server\Program.cs の ServerPort の IP アドレスを localhost から 0.0.0.0 に変更します。
    ※localhost のままでは Azure 上で正常に動作しません。
    image.png

  3. リポジトリのルートへ Dockerfile を追加します。
    Dockfile の中身はこちらをコピペしてください。

  4. 作業内容を Commit して GitHub へ Push します。
    ここまでの作業が終わったリポジトリはこちらです。
    image.png
    フォルダ構成などが一致していることを確認してください。

2. DockerHub で Docker イメージを生成

DockerHub にログインしてリポジトリを作成します。
image.png
設定項目がいくつかありますが直感的に設定できるので割愛します。
最後は Create&Build を押してください。
image.png

Build は15分ほどかかるのでのんびり待ちます。
Build が終わったらいよいよ Azure で動かしてみます。

3. AzureContainerInstances でコンテナを実行

AzurePortal にログインして ContainerInstances を作成します。
image.png
image.png
ContainerName は任意の名前を設定します。
ImageName は DockerHub の ID と RepositoryName を / で繋いで入力します。
e.g. minami/magiconion-chat-app-server

他の項目も入力したら Next:Networking を押します。
image.png
MagicOnion が使用する TCP の 12345 Port を開放します。
DNSNameLabel は必須ではありませんが、設定しておくと便利なときもあります。
それぞれ設定したら Review + create を押します。
image.png
Create を押します。
image.png
30 秒程で ContainerInstances が起動します。
image.png
IP アドレスか FQDN をメモしておきます。
※ContainerInstances を Stop すると IP アドレスが変わるので注意してください。Restart なら変化しません。
image.png
Azure の作業はここまでです。

4. Unity で動作確認

  1. ChatApp.Unity\Assets\Scripts\ChatComponent.cs で定義されている接続先を localhost から ContainerInstances の IP アドレスか FQDN に書き換えます。
    image.png

  2. ChatScene を開いて動作確認します。
    image.png

以上です、お疲れ様でした。

感想

最近は簡単で便利なものがたくさんあってすごいなと思いました(こなみ)
自分の備忘として記事にしてみましたがどなたかの参考になることがあれば幸いです。

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

[Unity] カスタムTextコンポーネントで省略表示をする

はじめに

Unityでテキストが表示範囲内に収まらない場合に文字をトリミングして省略文字を表示するカスタムTextコンポーネントを作成しました。
Image01_20190526.png

環境

Unity 2018.2.18.f1

ソースコード

using System;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;

public class TrimmingText : Text
{
    // 省略時に表示する文字列
    private string ellipse = "...";

    // テキスト
    private string _text;

    public new string text
    {
        get { return _text; }
        set
        {
            _text = value;
            UpdateText(_text);
        }
    }

    // Text の RectTransform
    private RectTransform _rectTransform;

    public RectTransform _RectTransform
    {
        get
        {
            if (_rectTransform == null)
                _rectTransform = GetComponent<RectTransform>();
            return _rectTransform;
        }
    }

    // RectTransformのサイズ変更時に呼び出される
    protected override void OnRectTransformDimensionsChange()
    {
        base.OnRectTransformDimensionsChange();
        UpdateText(_text);
    }

    // ロードされた時やインスペクターの値が変更されたときに呼び出される
    protected override void OnValidate()
    {
        base.OnValidate();
        _text = m_Text;
        UpdateText(_text);
    }

    // テキストの更新
    private void UpdateText(string str)
    {

        // RichTextはタグの解析をしていないため未対応(タグがトリムされてしまう)
        if (supportRichText)
        {
            throw new Exception("Richtext is not supported");
        }
        else
        {
            // 文字列がない場合は以降の処理をしない
            if (string.IsNullOrWhiteSpace(str))
            {
                return;
            }

            // 設定されたテキスト情報を元にジェネレータを作成する
            var generator = new TextGenerator();
            var settings = this.GetGenerationSettings(_RectTransform.rect.size);
            generator.Populate(str, settings);

            // 0行の場合は、以降の処理を行わない
            if (generator.lineCount <= 0)
            {
                return;
            }

            var updatedText = str;

            // horizontalOverflow = warp
            if (HorizontalWrapMode.Wrap.Equals(horizontalOverflow))
            {
                var height = generator.GetPreferredHeight(updatedText, settings) / settings.scaleFactor;

                // generatorで設定されたテキスト領域高が、RectTransform高を超える場合はトリムを行う
                if (_RectTransform.rect.size.y < height)
                {
                    updatedText += ellipse;
                    height = generator.GetPreferredHeight(updatedText, settings) / settings.scaleFactor;

                    // generatorで設定されたテキスト領域高が、RectTransform高を超えないようになるまで文字を削除する
                    while (_RectTransform.rect.size.y < height && ellipse.Length < updatedText.Length)
                    {
                        updatedText = updatedText.Remove(updatedText.Length - ellipse.Length - 1, 1);
                        height = generator.GetPreferredHeight(updatedText, settings) / settings.scaleFactor;
                    }
                }
            }

            // horizontalOverflow = overflow
            else
            {
                var width = generator.GetPreferredWidth(updatedText, settings) / settings.scaleFactor;

                // generatorで設定されたテキスト領域幅が、RectTransform幅を超える場合はトリムを行う
                if (_RectTransform.rect.size.x < width)
                {
                    updatedText += ellipse;
                    width = generator.GetPreferredWidth(updatedText, settings) / settings.scaleFactor;

                    // generatorで設定されたテキスト領域幅が、RectTransform幅を超えないようになるまで文字を削除する
                    while (_RectTransform.rect.size.x < width && ellipse.Length < updatedText.Length)
                    {
                        updatedText = updatedText.Remove(updatedText.Length - ellipse.Length - 1, 1);
                        width = generator.GetPreferredWidth(updatedText, settings) / settings.scaleFactor;
                    }
                }
            }
            m_Text = updatedText;
        }
    }
}

使い方

Textコンポーネントの代わりに「TrimmingText」をアタッチしてください。
Image02_20190526.png

解説

  • 「TextGenerator」で生成された文字列の表示範囲がRectTransformの範囲を超える場合、文字列をトリミングして「...」を表示します。
  • 「HorizontalOverflow」の「Overflow」「Warp」の両方に対応させています。「Overflow」の場合は、文字列の幅を監視し、「Warp」の場合は高さを監視してトリミング位置を判定します。
  • RichTextなどの一部の機能が正常動作しないためご注意ください。また表示幅が非常に狭いなど特定の条件下では正しく動作しません。

免責事項

  • 本記事に掲載された内容によって生じた損害等の一切の責任を負いかねますのでご了承ください。また本記事に記載されているリンクなどによって他のサイトに移動した場合、移動先サイトで提供される情報、サービス等について一切の責任を負いません。
  • 本記事に記載されているソースコードの利用は自己責任でお願い致します。プログラムの利用で生じるすべての損害に関して、利用者がその一切の責任を負うものとし、当方は一切の責任を負いません。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity(C#)】Animationとパラメータを連動させる方法

デモ

タイトルの内容について書きますが、百聞は一見に如かずということでご覧ください。

Gif_Squat.gif

膝の角度に合わせて数値及び、青い角度計が変化しています。

これは膝の角度を読み取っているわけではないです。

アニメーション(スクワット)の再生時間に応じてパラメータを変化させています。

GetCurrentAnimatorStateInfo

Animatorを作ると複数のステートを作成できます。

ステートはアニメーションを遷移させるときに便利です。

今回は再生中のアニメーションを調べる必要があるので、
GetCurrentAnimatorStateInfo(0)を使って調べます。

   [SerializeField]
   Animator squatAnimeController;

   void Update()
    {
       AnimatorStateInfo animeStateInfo = squatAnimeController.GetCurrentAnimatorStateInfo(0);
    }

GetCurrentAnimatorStateInfo(0)を使うことで、再生中のアニメーションを取得できます。
引数はアニメーションのLayerですが、複雑なアニメーションを作成してLayerを使用した場合を除いて、
基本的にデフォルトの0で大丈夫です。

AnimatorStateInfo.normalizedTime

normalizedTimeを使用すればアニメーションの再生時間を0~1の値に正規化することができます。

正規化された時間を利用して他のパラメータの値を操作すれば
デモのようにアニメーションに合わせて数値を変化させることができます。

Mathf.Lerp

先程の正規化された時間とMathf.Lerpを利用して数値を連動して変化させます。
Mathf.Lerpの引数は3つ用意されており、Lerp(float a, float b, float t)となっています。

それぞれの引数は
a:開始値
b:終了値
t:補完値(0~1)
となっています。aの値からbの値に向けてtで補完します。
と言われてもいまいちわからないと思います。(私は使ってみるまでさっぱりでした)

なので一例を示します。

まずはコードです。

   [SerializeField]
   Animator squatAnimeController;

   [SerializeField]
   Text degreesText;

   void Update()
    {
       AnimatorStateInfo animeStateInfo = squatAnimeController.GetCurrentAnimatorStateInfo(0);

       float degrees = Mathf.Lerp(180, 0, animeStateInfo.normalizedTime); 
       degreesText.text = ((int)degrees).ToString() + "°";
    }

このように、アニメーションの再生時間(正規化されているので0~1)に合わせて
角度が180~0°で変化しているのがわかるかと思います。(GIFが汚いのはご容赦ください)
Gif_Squat_Explanation.gif

これが先程のaの値からbの値に向けてtで補完するという意味です。

a:開始値(180)
b:終了値(0)
t:補完値(正規化したアニメーションの再生時間0~1)

Mathf.Repeat

先程のGIF画像ではステートを二つ用意して、同じアニメーションを交互に遷移させていました。
その理由としてはアニメーションのループ設定がうまくいかなかったからです。

ループ設定してしまうと、アニメーションの正規化がうまくできないようです。
0~1収まるはずの正規化した値が1を超えて無限に増えていきます。(ループに終了時間という概念が存在しないから?)

もし、なんらかの事情でアニメーションのループ設定を外せない場合は、
Mathf.Repeatで正規化した値が1を超えないようにします。

Mathf.Repeat(float t, float length)
指定値(length)を超えると0に戻り、再度 指定値まで循環します。

   [SerializeField]
   Animator squatAnimeController;

   [SerializeField]
   Text degreesText;

   void Update()
    {
       AnimatorStateInfo animeStateInfo = squatAnimeController.GetCurrentAnimatorStateInfo(0);

       float degrees = Mathf.Lerp(180, 0, Mathf.Repeat(animeStateInfo.normalizedTime, 1)); 
       degreesText.text = ((int)degrees).ToString() + "°";
    }

これでループ設定を外せないとしても安心です。
Gif_Squat_Explanation_2.gif

fillAmount

角度計のアニメーションはこちらのサイトを参考にしました。

アニメーションに応じて、FillAmountの値を変化させています。
ImageFillAmount.PNG

最終的なコード

当方、マジックナンバーおじさんですが、ご容赦ください。

作成した画像(色付き)にアタッチ
using UnityEngine.UI;
using UnityEngine;

public class AngleVisualController : MonoBehaviour
{
    [SerializeField]
    Animator squatAnimeController;

    AnimatorStateInfo animeStateInfo;

    [SerializeField]
    Text degreesText;

    Image circle_Image;

    void Start()
    {
        circle_Image = this.gameObject.GetComponent<Image>();
    }

    void Update()
    {
        animeStateInfo = squatAnimeController.GetCurrentAnimatorStateInfo(0);

        float squatVertical = 25 / 60.0f;
        float degrees = 0;

        if (animeStateInfo.normalizedTime < squatVertical)
        {
            circle_Image.fillAmount = Mathf.Lerp(0.48f, 0.25f, animeStateInfo.normalizedTime * (60.0f / 25));

            degrees = Mathf.Lerp(170, 90, animeStateInfo.normalizedTime * (60.0f / 25));
            degreesText.text = ((int)degrees).ToString() + "°";
        }
        else
        {
            circle_Image.fillAmount = Mathf.Lerp(0.25f, 0.48f, (animeStateInfo.normalizedTime-(25.0f/60)) * (60.0f / 25));

            degrees = Mathf.Lerp(90, 170, (animeStateInfo.normalizedTime - (25.0f / 60)) * (60.0f / 25));
            degreesText.text = ((int)degrees).ToString() + "°";
        }
    }
}

なぜこんなわけのわからない計算をLerpの中に仕込ませているかというと、
・スクワットのアニメーションの折り返し地点(正規化した値でいうと0.5)がしゃがんだ状態ではない
・角度の値が、直立(170)→しゃがむ(90)→直立(170)を取る
・アニメーションの再生フレームを取得できない
からです。

なので、もっとシンプルに使うには
・しゃがむ、立ち上がる の二つのアニメーションを用意する
なのかな~と思います。そうすれば、変な計算しなくても正規化した値をそのまま使えるかと思います。

本当は、アニメーションの再生フレームを取得して
0~25フレームはこの処理をして、26~60フレームは別の処理を...
みたいなことができれば便利なのですが、見つかりませんでした。(ありましたら教えてください)

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

Oculus Quest に Unity で開発したアプリをいれる方法(Mac/Win)

やったこと

最低限これだけやれば Oculus Quest に Unity で作ったアプリをいれられるという手順。
最初なので、Unity で新規プロジェクトを作ってそのまま入れてみます。

使ったもの

Oculus Quest 64GB
MacPro(2013) MacOS Mojave 10.14.5
GALLERIA SG VR Ready Windows 10 Home
Unity 2018.4.0f1
Mac/Win と Oculus Quest は USB-C <-> USB-A ケーブルを使用(こういうの

Oculus Quest 用に Unity ビルドをする

  1. 3Dで新規プロジェクトを作成
  2. Build Settings から Platform を Android に Switch Platform する。
  3. Player Settings... から以下の項目を変更する
    1. Other Settings -> Identification の Package Name を適当に変更、同じく Minimum API Level を API level 19 にする
    2. XR Settings -> Virtual Reality Supported にチェックをいれ、Virtual Reality SDKs の + を押して、Oculus を選択する

スクリーンショット 2019-05-26 14.47.15.png
スクリーンショット 2019-05-26 14.45.55.png

これで Build するだけ。
とりあえずは Build が通ることだけを確認する。

Android SDK をインストールしていない場合は、Android Studio をダウンロードし、インストールしてください。
インストール後に Unity を立ち上げ直して、Unity の Preferences -> External Tools の Android SDK の Browse を押すと、インストールされたパスを案内してくれるので、そのまま選択する。

Oculus Quest と Mac/Windows を開発者モードで接続する

1. スマホの Oculus アプリから Oculus Quest を開発者モードにする

Oculusの開発者登録をしていない場合は
https://dashboard.oculus.com/ ここに飛ばされるので、団体を登録する。
IMG_5161.PNG

2. (Windows のみ) Oculus Go ADB Drivers をインストールする

Oculus Go ADB Drivers
ダウンロードし解凍したら、中にはいっている android_winusb.inf を右クリックしてインストール。

3. Oculus Quest と Mac/Windows を接続する

接続すると Oculus Quest 側に、PCからの接続を許可するかのダイアログがでるので、許可する。

Unity でビルドしたアプリを Oculus Quest で実行する

Unity でさきほど Build が成功している状態で
今度は、Build And Run します。
これで、Oculus Quest 上に、Unity のデフォルトの空間が表示されれば成功です。

実行したアプリの削除

Oculus Quest 内から、アプリの削除ができなかったので、adb を使う
adb の場所は、以下にあります(Android Studio をインストールしたときに入っている)
Win : C:¥Users¥ユーザー名¥AppData¥Local¥Android¥sdk¥platform-tools
Mac : /Users/ユーザー名/Library/Android/sdk/platform-tools

パッケージ名は、Unity の Player Settings で設定した Package Name です。

# インストール済みのアプリの一覧を表示
$ adb shell pm list package

# 削除
$ adb uninstall パッケージ名

Unity 2019 で実行できなかった

現時点で最新の Unity 2019.1.4f1 で実行できず(起動しようとしてすぐホーム画面に戻る)
こちら に記載されている対応バージョンの 2019.1.2f1 でも同様にダメでした。よく理由はわからず。

さいごに

Oculus Quest と Mac を開発者モードでつなぐ部分が、少し手こずりました。
相性なのかタイミングなのかわからないが、Mac の方では何度やっても認識してもらえず
Oculus アプリ側で開発者モードのON/OFFを何度か繰り返したり、USBをさし直したり SMC/PRAMリセットしたりいろいろ試行錯誤しました。
一度つながって許可さえしてしまえば、それ以降は問題ないです。

つぎは、Oculus Integration の Assets をいれて、コントローラの操作をしたいと思います。

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