3
Help us understand the problem. What are the problem?

posted at

updated at

【Unity】Shaderを用いて斜めHPバーを作ってみた

はじめに

皆さんは、HPゲージの表示のさせ方について悩んだことがあるでしょうか?
Unityではゲージを表現するための機能がいくつか用意されていますが、元の機能のみではどうも思ったような表現が出来ないことがあります。
例えば、斜めのHPバーをImageのfillAmountで作ろうとすると、以下の画像のように現在値が増減させるときに縦に真っすぐな線になってしまいます。
  
こうではなく、右の画像のように傾きを持たせたいですね。
今回は、このような斜めになった、少しスタイリッシュなHPゲージをShaderを用いて作成していきます。

環境

  • Unityバージョン :Unity2020.3.18f1
  • テキストエディタ :Microsoft Visual Studio 2019

目次

  1. よく使われるゲージの増減手法
  2. Shaderを使う準備をする
  3. Shaderを書く
  4. 中身を見る
  5. C#スクリプトを書いて動かしてみる
  6. おわりに

1. よく使われるゲージの増減手法

1-1. Sliderを使う

斜めのHPゲージを作る前に、他にどのような手法が使われているか簡単に見ていきましょう。

Slider(スライダー)はツマミを掴み、ドラッグすることでゲージの値を変化させることが出来るものです。よくゲームの音量調整や明るさ調整などに使用されています。
このツマミを削除し、ゲージの変化をスクリプト側から行うことでHPゲージのようなふるまいをさせることも可能です。
エディタ上のタブのGameObject/UI/Sliderから作成できます。

1-2. ImageのfillAmountを使う

fillAmountは、様々な手法でImageの表示面積を削ることができる機能です。縦にゲージを減らしたり、円形に減らすなど、Sliderより様々な表現を作り出すことが出来ます。
まず、GameObject/UI/ImageからImageを作成し、InspectorからImageコンポーネントのSource Imageに変化させるSpriteをアタッチします。その後、Image TypeからFilledを選択するとFill Amountのプロパティが表示されます。
3.PNG
実際にこのスライダーを動かしてみると、Imageが削られているのが確認できます。
Fill MethodやFill Originの値も変更して遊んでみましょう。

2. Shaderを使う準備をする

では、すぐに斜めに傾けるShaderを書く...といきたいところですが、いくつか準備をする必要があります。

  • Spriteを用意する
  • Shaderファイルを作成する
  • ShaderをアタッチするMaterialを作成する
  • MaterialをGameObjectにアタッチする

2-1. Spriteを用意する

Rectangle

今回はこちらの白色の画像を使用します。
Unityを3Dで起動し、この画像のTexture TypeをDefault → Sprite (2D and UI)へ変更し、Applyを押して変更を確定します。

2-2. Shaderファイルを作成する

Projectウィンドウから、右クリックし、Create/Shader/Image Effect Shaderを選択します。

2-3. ShaderをアタッチするMaterialを作成する

2-2と同様にして、Create/Materialから新しいMaterialを作成します。
その後、先ほど作成したShaderファイルをドラッグ&ドロップでこのMaterialにアタッチします。
4.PNG
これで、このMaterialに先ほど作成したShaderが適用されました。
画像のように、Materialのプレビューが真っ黒になっていれば成功です。

2-4. MaterialをGameObjectにアタッチする

最後にこのMaterialを適用するGameObjectを作成します。
Hierarchyウィンドウから、右クリックし、UI/Imageから作成します。
すると、Canvasの中にImageコンポーネントを持つゲームオブジェクトが生成されます。

次に、Imageを選択し、先ほどの横長の画像をSource Imageに、作成したマテリアルをMaterialにアタッチします。
以下のような画像になっていれば大丈夫です。
5.PNG
最後に、一番下にあるSet Native Sizeをクリックし、Imageのサイズを整えます。
これによって、Imageにも真っ黒なMaterialが適用されました。

3. Shaderを書く

では準備もできましたし、Shaderファイルを開いてみましょう。
いきなり大量にコードが出てきましたが、ご安心ください。今回はこのうちの一部を変更・追加するのみですので。
以下のように書き換えてみましょう。

SlantHPShader.shader
Shader "Hidden/SlantHPShader"
{
    Properties // プロパティを置ける
    {
        // 追加
        _FillAmount("FillAmount", Range(0.0, 1.0)) = 1.0 // 値を増減させるためのプロパティ
        _Color ("Color", Color) = (1, 1, 1, 1) // 色を変更するためのプロパティ
    }
    SubShader
    {
        // No culling or depth
        Cull Off ZWrite Off ZTest Always
        Blend SrcAlpha OneMinusSrcAlpha // 追加

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            // 追加
            float4 _MainTex_TexelSize; // テクスチャのサイズに関する情報を持つ
            float _FillAmount;
            fixed4 _Color;

            fixed4 frag(v2f i) : SV_Target
            {
                // 追加
                fixed4 color = _Color; // 処理中の座標の色を_Colorに
                float h = _MainTex_TexelSize.w; // テクスチャの縦のピクセル数
                float w = _MainTex_TexelSize.z; // テクスチャの横のピクセル数

                // 左端を斜めにする処理
                if (i.uv.y > i.uv.x * w / h) {
                    color.a = 0.0; // 色のアルファ値を0(透明)にする
                }
                // 右端を斜めにする処理
                else if (i.uv.y < i.uv.x * w / h - (w - h) / h * _FillAmount) {
                    color.a = 0.0; // 色のアルファ値を0(透明)にする
                }

                return color;
            }
            ENDCG
        }
    }
}

1行目の名前は"Hidden/(このシェーダーの名前)"となるようにしましょう。それ以外の部分であればコピペしても正しく動作するかと思います。

4. 中身について

さてこのスクリプトの中ではどのような動作が行われているのでしょうか。変更した部分について、詳しく見ていきましょう。

4-1. Propertiesブロック内

ShaderLab: Properties
シェーダーを使って Unity の マテリアルインスペクター でアーティストが設定するパラメーターを定義できます。

ここで宣言した変数が、MaterialのInspector画面から調整できるようになります。
作成したMaterialのInspectorを見てみると、下画像のようにFillAmountColorの2つの値が調整できるようになっていることがわかります。
6.PNG
実際にこの値を動かしてみましょう。
FillAmountの値を動かすと、0から1の範囲で、それに合わせて用意していたImageのゲージが変化していきます。しっかり傾いた状態を維持していますね。
Colorは今回、Imageコンポーネントの色を取得するつもりなので、ここでは白色のままにしておきます。

4-2. fragブロック内

このブロック内では、Imageの両端が斜めになるような数学的な処理と色変更を行っています。
斜めの表現を作るために、描画する範囲を領域の考え方を用います。
8.png
上の画像のように、まず両端の傾け度合いを考えます。今回は一番単純な45度の傾きを考えました。
用意したSpriteの横の長さをw、縦の長さをhとすると、このような式を作ることができます。

次にこれを直接プログラムに書き記したいところですが、xとyを表現する際に使用するi.uvという変数はそれぞれ0~1の範囲しか持たないため、上の式を下図のように書き換える必要があります。
9.png
xy方向の長さをそれぞれ1とする場合、それぞれの値にwh逆数を掛けることで割合を求めることが出来ます。
さらに、この右辺の切片の部分にPropertiesブロック内で宣言したfillAmountの値を掛けることでゲージの増減を考えることが出来ます。つまり、このようになります。

y > \frac{w}{h} x - \frac{w - h}{h} \times fillAmount

そして最後に、これをプログラムに落とし込みます。
今回は領域内を描画するのではなく、反対に領域外を描画しない(不透明度を0にする)方法を取ります。そのため、図に表示されている不等号を反対向きにする必要がありますね。

// 左端を斜めにする処理
if (i.uv.y > i.uv.x * w / h)
{
    color.a = 0.0; // 色のアルファ値を0(透明)にする
}
// 右端を斜めにする処理
else if (i.uv.y < i.uv.x * w / h - (w - h) / h * _FillAmount)
{
    color.a = 0.0; // 色のアルファ値を0(透明)にする
}

そして、この条件に一致する場合にcolor.a = 0.0と不透明度を0にすることで、範囲外を見せないようにしています。

5. C#スクリプトを書いて操作してみる

では最後に、このFillAmountの値をC#スクリプトから変更してみましょう。

GaugeController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class GaugeController : MonoBehaviour
{

    private Material mat; // Materialのインスタンス
    private Image img; // アタッチされたゲームオブジェクトのImageコンポーネント
    [SerializeField, Range(0.0f, 1.0f)] private float fillAmount = 1.0f; // ゲージ量

    // Start is called before the first frame update
    void Start()
    {
        img = GetComponent<Image>(); // Imageコンポーネント取得
        mat = Instantiate(img.material); // Materialのインスタンス生成
        mat.SetColor("_Color", img.color); // Materialの色指定
        img.material = mat; // ImageのMaterialをインスタンス化したMaterialに変更
    }

    // Update is called once per frame
    void Update()
    {
        mat.SetFloat("_FillAmount", fillAmount);
    }

    // ゲームオブジェクトが破棄されたときに呼び出される
    private void OnDestroy()
    {
        // インスタンスを持っていたら破棄する
        if (mat != null)
        {
            Destroy(mat);
        }
    }
}

5-1. Materialのインスタンスを作成する

現在ImageコンポーネントにアタッチしているMaterialは、Assetsフォルダ内のMaterialを直接指定しています。そのため、このままFillAmountの値を変更しようとすると、他のゲームオブジェクトまで値が変更されてしまいます。
それを回避するために、スクリプト内でMaterialのインスタンスを生成し、アタッチしなおしています。

5-2. 色や値を変化させる

実際にこのスクリプトをゲームオブジェクトにアタッチしてみましょう。
色も実際のHPバーっぽく黄緑色にしてみました。
10.PNG
そして実行し、スクリプトのFillAmountのスライダーを動かすと、実際に動くのが確認できます。
これで、斜めに傾いたHPバーが完成しました。
この値を別スクリプトから参照したり、色を変えたりと様々な場面で使うことが出来そうです。

6. おわりに

今回は、斜めに傾いたHPバーをShaderを用いて作成しました。
まさか、数学の領域の知識を使うことになるとは思わず、Unityで使う場面があったことに喜びを隠せませんでした。
ただ、Shaderの中にif文を書くことは、パフォーマンスの低下に繋がるためあまり良くないという話をよく耳にします。そのため、これを如何にして改善するのか、今後の課題にしたいと思っています。

ありがとうございました!

参考文献

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
3
Help us understand the problem. What are the problem?