おもちゃラボ

Unityで遊びを作ってます

【Unity】プログレスバーを作るときにハマりがちな3つの誤り

Unityでプログレスバー(progressbar)を作るときは、uGUIのImageを使うことが多いと思います。Imageの設定をFillに設定することで簡単にプログレスバーのようなものを作ることができます。

f:id:nn_hokuson:20170405192003p:plain

スクリプトからプログレスバーを更新する場合は、次のようにImageオブジェクトのfillAmountの項目を0.0〜1.0の間で設定することで表示の割合を変えることができます。

GetComponent<Image>().fillAmount = 0.5;

やることはこれだけなのですが、重い処理に合わせてプログレスバーを進めようとするとなかなかうまく行きません。次の3つが失敗例。最後に成功例を載せています。

誤り実装① コルーチンを使う

IEnumerator Progress()
{
    for(int i = 0; i <= 100; i++){
        GetComponent<Image>().fillAmount = i / 100.0f;
        yield return null;
    }
}

void HeavyProcess()
{
    StartCoroutine(Progress());
    
    //以下重たい処理
    // ・・・・
}

重たい処理をはじめる前にコルーチンを生成し、そちらでなんちゃってアニメーションしようという案です。

Unityのコルーチンはスレッドっぽくてスレッドではない(並列動作しない)ため、この実装は正常に動きません。重たい処理に引きずられて、Progressコルーチンは重たい処理が終わったあとにようやく実行されます。

誤り実装② 処理の間に挟み込む

void HeavyProcess()
{
    //重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 0.2f;
    //重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 0.4f;
    //超重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 0.9f;
    //重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 1.0f;
}

コルーチンがだめなら、直接重たい処理の中にプログレスバーの更新プログラムを挟み込めばいいじゃん案です。

なかなか良さそうですが、残念ながらこれも正常には動作せず、いきなりプログレスバーの進捗が100%になってしまいます。

これは、fillAmountに値を代入したあとも重たい処理が続くため、uGUIの画面更新が行われないのが原因です。当たり前といえば当たり前か・・・・

誤り実装③ Threadを使う

private Thread _thread;
IEnumerator Progress()
{
    for(int i = 0; i <= 100; i++){
        GetComponent<Image>().fillAmount = i / 100.0f;
        Thread.Sleep(10);
    }
}

void HeavyProcess()
{
    _thread = new Thread(Progress());
    _thread.Start();
    
    //以下重たい処理
    // ・・・・
}

コルーチンがだめなら非同期スレッドだ、案です。そもそもUnityの設計思想がシングルスレッドなので、Threadを使う時点でイケてないのですが・・・Threadの中ではuGUIの更新ができずにエラーが出ます。

ちゃんとプログレスバーが伸びる実装

IEnumerator HeavyProcess()
{
    //重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 0.2f;
    yield return null;
    //重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 0.4f;
    yield return null;
    //超重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 0.9f;
    yield return null;
    //重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 1.0f;
}

void Hoge()
{
    StartCoroutine(HeavyProcess());
}

さて、いよいよ正しく動く実装です。結局何をしたかというと、コルーチンと挟み込みの折衷案です。重たい関数自体をコルーチンにして、プログレスバーをすすめるたびに、一旦処理をメインスレッドに返します。これにより描画処理が実行され、プログレスバーも進む、という仕組みです。

これにて一件落着!?

f:id:nn_hokuson:20170405192018g:plain

【Unityシェーダ入門】シェーダで旗や水面をなびかせる

ゲーム中で、旗や水面は静止していることはあまりなく、風にあおられてなびいていることが多いですね。

f:id:nn_hokuson:20170404200530j:plain

リアルになびかせようとすると、シミュレーションやボーンアニメーションをする必要があります。この記事ではUnityの頂点シェーダを使ってお手軽に旗や波などを動かす方法を紹介したいと思います。

今回の記事の内容は次のとおりです。

旗や水面をなびかせるアルゴリズム

旗をなびかせるためには、平面オブジェクトの頂点を移動させることで実現します。3Dで考えると難しいので、2Dで考えてみましょう。

平面オブジェクトを横から見ると次のように一直線に頂点が並んでいます。

f:id:nn_hokuson:20170404194735p:plain:w350

この頂点を波の形に移動させることで、平面オブジェクトがなびいているように見せかけることができます。

f:id:nn_hokuson:20170404194741p:plain:w350

全体的には波打っているように見えますが、それぞれの頂点は単に上下運動を繰り返しているだけです。ただし、隣の頂点とは少しタイミングをずらして上下運動をさせます。

f:id:nn_hokuson:20170404194747p:plain:w350

水面uvスクロールで動かす方法はこちらの記事に書いていますので参考にしてみてください。

nn-hokuson.hatenablog.com

3Dモデルの配置とシェーダファイルの作成

まずはUnityのシーンビューに平面を配置します。ヒエラルキービューから「Create」→「3D Object」→「Plane」を選択してください。

f:id:nn_hokuson:20170404194753p:plain:w300

UnityのPlaneは10x10のメッシュなので、旗のようになびかせると少々ぎこちない動きになってしまいます。もう少し細かいメッシュのものを使いたい方は、Blenderなどで作成して下さい。

f:id:nn_hokuson:20170404195658j:plain

いつもと同じようにシェーダファイルとマテリアルを作成します。プロジェクトビューで「Create」→「Shader」→「Standard Surface Shader」を選択し、ファイル名は「Flag」で保存します。
このファイルを選択した状態で、「Create」→「Material」を選択し、Custom_Flagマテリアルを作成してください。作成したマテリアルにFlagシェーダが使われていればOKです。

f:id:nn_hokuson:20170404194805p:plain:w150

マテリアルは平面オブジェクトにアタッチして、必要でしたらマテリアルにテクスチャを設定しておきます。

f:id:nn_hokuson:20170404194813p:plain

シェーダプログラム

今回作成した旗のシェーダプログラムは次のとおりです。

Shader "Custom/Flag" {
    Properties {
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200
        
        CGPROGRAM
        #pragma surface surf Lambert vertex:vert
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input {
            float2 uv_MainTex;
        };

        void vert(inout appdata_full v, out Input o )
        {
            UNITY_INITIALIZE_OUTPUT(Input, o);
            float amp = 0.5*sin(_Time*100 + v.vertex.x * 100);
            v.vertex.xyz = float3(v.vertex.x, v.vertex.y+amp, v.vertex.z);            
            //v.normal = normalize(float3(v.normal.x+offset_, v.normal.y, v.normal.z));
        }

        void surf (Input IN, inout SurfaceOutput o) {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

頂点の座標データを操作するので、独自のvertexシェーダをカスタムで作る必要があります。

f:id:nn_hokuson:20170401080509p:plain

頂点シェーダのカスタムについては、頂点カラーの回で詳しく説明しているので合わせて参考にしてください。

nn-hokuson.hatenablog.com

#pragmaの中で「vertex:vert」とすることで、カスタムしたバーテックスシェーダ(vert)を使うことを宣言しています。頂点シェーダ(vert)のなかでは、各頂点を上下運動させるシェーダプログラムを書いています。

sin関数を使って時間とともに頂点のy座標を変化させています。隣り合う頂点とは少し周期をずらすために、sin関数の引数に頂点のx座標を足しています。

実行結果

実行結果は次のようになります。このように、頂点シェーダを使うことで簡単にアニメーションを作ることができます。

f:id:nn_hokuson:20170404194843g:plain

【Arduino】カラーセンサ(TCS230 / TCS3200)を使って色を調べる

Arduinoとカラーセンサを使って色を調べる方法を紹介します。カラーセンサはAmazonで販売されているTCS230 TCS3200を使用しました。

f:id:nn_hokuson:20170403192557j:plain

カラーセンサの仕組み

カラーセンサは赤色を検知するセンサと緑色を検知するセンサと青色を検知するセンサの3つから構成されています。この3つのセンサを順番に調べることで、RGBの値を取得します。

f:id:nn_hokuson:20170401215154p:plain:w500

RGBのセンサのうちどのセンサを有効にするかは、S2とS3のピンの組み合わせで指定します。

S2 S3 Color
L L Red
L H Blue
H H Green

センサから得られるRGBデータはOUTピンから出力され、そのデータは周波数として取得できます。ArduinoではpulseIn関数を使って簡単に周波数を調べることができます。

S0とS1ピンは色情報を周波数に変換するときのスケーリングの割合を指定するのに使用します。下のプログラムでは2%の変調をしています。

S0 S1 Scaling
L L Power Down
L H 2%
H L 20%
H H 100%

カラーセンサとArduinoをつなぐ回路図

カラーセンサとArduinoを接続する回路図は次のようになります。特に注意することはありませんが、S0〜S4をArduinoのポート4〜7、OUTピンをポート8に接続しています。

f:id:nn_hokuson:20170402165213p:plain:w400

Arduinoのプログラム

カラーセンサからの値を取得してRGBの情報を表示するプログラムは次のとおりです。

#define S0 4
#define S1 5
#define S2 6
#define S3 7
#define sensorOut 8

int whiteR = 180;
int whiteG = 240;
int whiteB = 170;
int blackR = 20;
int blackG = 20;
int blackB = 16;

void setup() {
  pinMode(S0, OUTPUT);
  pinMode(S1, OUTPUT);
  pinMode(S2, OUTPUT);
  pinMode(S3, OUTPUT);
  pinMode(sensorOut, INPUT);
  
  digitalWrite(S0,HIGH);
  digitalWrite(S1,LOW);
  
  Serial.begin(9600);
}

void loop() {
  digitalWrite(S2,LOW);
  digitalWrite(S3,LOW);
  int r = pulseIn(sensorOut, LOW);
  //r = map(r, whiteR, blackR,0,255);
  delay(100); 

  digitalWrite(S2,HIGH);
  digitalWrite(S3,HIGH);
  int g = pulseIn(sensorOut, LOW);
  //g = map(g, whiteG, blackG,0,255);
  delay(100);
  
  digitalWrite(S2,LOW);
  digitalWrite(S3,HIGH);
  int b = pulseIn(sensorOut, LOW);
  //b = map(b, whiteB, blackB,0,255);
  delay(100);

  Serial.println(String(r) + "," + String(g) + "," + String(b) );
}

このプログラムではS2/S3でRGBのセンサを切り替えながら、各センサから得られる周波数をRGBの輝度に変換しています。

Arduinoで周波数を調べるにはplusIn関数を使います。plusIn関数はピンに入力されるパルスの時間を計測する関数で、入力がHIGHに変わると同時に時間の計測を始め、LOWに戻るまでの時間をマイクロ秒単位で返します。

pulseIn() - Arduino Reference

f:id:nn_hokuson:20170401230231p:plain:w250

キャリブレーション

センサから得られる値はお使いの環境で多少変化すると思うので、キャリブレーションする必要があります。

白色の紙をセンシングした時のRGB値と、黒色の紙をセンシングした時のRGB値を調べて、whiteRGB、blackRGBの値に代入して下さい。

キャリブレーション値が得られたら、上のプログラムでコメントアウトしているmap関数の行のコメントを外して下さい。map関数はキャリブレーションした値を使って、センサからの値を0〜255の輝度値に変換します。

参考

カラーセンサTCS230 TCS3200の使い方に関しては、こちらのサイトを参考にさせていただきました。
howtomechatronics.com
circuitdigest.com