HeliScriptでアイテムを揺らす【コピペで使えるサンプルコード付き】

:::note info
本記事は、[HeliScriptでリッチなアスレチックワールドを作ろう！【コピペで使えるサンプルコード付き】](https://qiita.com/IwahanaFuku/private/63e6bbca2bc1f13ed57e)の記事の一部です

VR法人HIKKYのorganization下のQiita/Zenn両方に投稿しております。
:::

# はじめに
HeliScriptでアイテムを揺らす`ItemShake compornent`を作成してみます。[hsMathRandom(int)](https://vrhikky.github.io/VketCloudSDK_Documents/5.4/ja/hs/hs_system_function_math.html#hsmathrandomint)を利用することで、現在の位置から、フレーム毎にランダムな位置へとずらして、ぶるぶると動くような動作を作ることが出来ます。
Propertyとして、揺れの強さを設定できるようにします。

また、記事の後半で、アイテムを揺らすコンポーネントと、[HeliScriptでアイテムをまっすぐ動かす【コピペで使えるサンプルコード付き】](https://qiita.com/IwahanaFuku/items/f171b7af2a9e15dc2139)で作った、アイテムを動かすコンポーネントを利用して、プレイヤーが床の上に乗ると、アイテムが揺れて、数秒後に床が落ち始めるギミックを作ります。

`ItemMove` compornentで設定できる、velocity(Vector3)で落下速度を、
今回作成する`ItemShake` compornentで設定できる、strength(float)で揺れの強さに加えて、
揺れ始めるまでの時間と、落下が始まるまでの時間を設定できるようにします。

Propertyを利用したギミック制作を行うことで、今まで作ってきた小さな機能を持つコンポーネントを利用して、複雑なギミックを持つ機能を再利用しやすい形で作ることが出来るようになります。

# アイテムを揺らす
![2024-03-26 18-25-06_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629942/b74ed2ad-3af5-3f70-42b3-c16d160c2aa2.gif)

**Unity設定画面**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629942/e93fb0e7-eae7-f7bf-16a0-c16766f702eb.png)

**コード全文**
```C#:ItemShake.hs
component ItemShake
{
    Item m_item;
	Vector3 m_itemInitialPos;
	float m_strength;

	bool m_isShake;
	Vector3 m_beforeRandomPos; //1フレーム前の移動距離

    public ItemShake()
    {
        m_item = hsItemGetSelf();
		m_itemInitialPos = m_item.GetPos();

        m_strength = m_item.GetProperty("strength(float)").ToFloat();

		m_beforeRandomPos = new Vector3();
    }

	public void Update()
	{
		ShakeItem();
	}

	void ShakeItem()
	{
		//ランダムな値の粒度
		float resolution = 10f;

		//ランダムなVector3の値を取得する
        Vector3 randomPos = RandomVector(m_strength, resolution);

		//今回のフレームで移動するポジションを格納するための変数を用意して、現在の位置を格納する
		Vector3 resultPos = m_item.GetPos();

		//ランダムの値を加算し続けて、意図しない位置に移動しないように前回のフレームの移動量を戻す
		resultPos.Sub(m_beforeRandomPos);

		//現在の位置にランダムなVector3の値を足してランダムに移動させる
        resultPos.Add(randomPos);

		//ランダムなポジションを設定する
        m_item.SetPos(resultPos);

		//次回フレームで元のポジションに戻すために今回のフレームのランダムな値を保持しておく。
		m_beforeRandomPos = makeVector3(randomPos.x, randomPos.y, randomPos.z);
	}

	//strengthの大きさと、resolutionの粒度の細かさに応じてランダムな値を取得する
	Vector3 RandomVector(float strength, float resolution)
	{
		// resolutionの粒度でランダムな値を生成し、strengthでスケールを調整した後、中心を0にシフトする
        Vector3 result = makeVector3((hsMathRandom(resolution) / resolution - 0.5f) * strength,
                                     (hsMathRandom(resolution) / resolution - 0.5f) * strength,
									 (hsMathRandom(resolution) / resolution - 0.5f) * strength); 
		return result;
	}

    //プロパティの変更を検知して、設定する
    public void OnChangedProperty(string key, string value)
    {
        if(key == "strength(float)")
        {
            m_strength = m_item.GetProperty("strength(float)").ToFloat();
        }
    }
}
```

## コード解説
### Vector3でランダムな値を取得する
```C#
	//strengthの大きさと、resolutionの粒度の細かさに応じてランダムな値を取得する
	Vector3 RandomVector(float strength, float resolution)
	{
		// resolutionの粒度でランダムな値を生成し、strengthでスケールを調整した後、中心を0にシフトする
        Vector3 result = makeVector3((hsMathRandom(resolution) / resolution - 0.5f) * strength,
                                     (hsMathRandom(resolution) / resolution - 0.5f) * strength,
									 (hsMathRandom(resolution) / resolution - 0.5f) * strength); 
		return result;
	}
```
ランダムの値の範囲を設定するための`strength`と、ランダムで選ばれる範囲の値の細かさを設定するための`resolution`が設定できる関数を作成します。

[hsMathRandom(int)](https://vrhikky.github.io/VketCloudSDK_Documents/5.4/ja/hs/hs_system_function_math.html#hsmathrandomint)は、int型またはfloat型の値を設定することで、0から入力値未満の間のランダムな値を返す関数です。
hsMathRandom()関数にはfloat型変数resolutionを渡していますが、例えばresolutionに10を渡すことで、0≦n＜10となるランダムな値が設定されます。
その値に対して、resolutionで割ることによって、ランダムな値が0～1の間に正規化されます。

ただ、このままだと、Vecotor3の値が、0地点を中心としてランダムな値が返されるのではなく、0.5の地点を中心としてランダムな値が返されてしまうので、-0.5fを引くことによって、-0.5～0.5の間にランダムな値が設定されるようにします。

最後に、strengthをかけることによって任意の幅にrandomな値が収まるようになります。

### アイテムの現在の位置にランダムな値が設定されるようにする。
```C#
	public void Update()
	{
		ShakeItem();
	}

	void ShakeItem()
	{
		//ランダムな値の粒度
		float resolution = 10f;

		//ランダムなVector3の値を取得する
        Vector3 randomPos = RandomVector(m_strength, resolution);

		//今回のフレームで移動するポジションを格納するための変数を用意して、現在の位置を格納する
		Vector3 resultPos = m_item.GetPos();

		//ランダムの値を加算し続けて、意図しない位置に移動しないように前回のフレームの移動量を戻す
		resultPos.Sub(m_beforeRandomPos);

		//現在の位置にランダムなVector3の値を足してランダムに移動させる
        resultPos.Add(randomPos);

		//ランダムなポジションを設定する
        m_item.SetPos(resultPos);

		//次回フレームで元のポジションに戻すために今回のフレームのランダムな値を保持しておく。
		m_beforeRandomPos = makeVector3(randomPos.x, randomPos.y, randomPos.z);
	}
```
`RandomVector(float strength, float resolution)`で作成したランダムな値をもとに、現在のポジションに対してランダムな値を足し合わせます。
ただ、そのまま足すだけだと、ランダムに移動し続けて最初に配置された場所からどんどんとずれて行ってしまうので、1フレーム前のランダムの値を`m_beforeRandomPos`に格納して、前回のフレームの移動量を元に戻すことによって、現在のポジションからずれすぎないようにしておきます。

`m_beforeRandomPos`に値を格納するときは、参照渡しとならないように、`makeVector3(randomPos.x, randomPos.y, randomPos.z)`で値を設定するようにします。

### `strength(float)`を後々変更できるようにする。
```C#
    //プロパティの変更を検知して、設定する
    public void OnChangedProperty(string key, string value)
    {
        if(key == "strength(float)")
        {
            m_strength = m_item.GetProperty("strength(float)").ToFloat();
        }
    }
```
床に触れたらアイテムが揺れ始めるような実装を行いたいので、`strength(float)`は、スクリプトから再変更が行えるようにします。

[OnChangedProperty(string Key, string Value)](https://vrhikky.github.io/VketCloudSDK_Documents/12.3/hs/hs_component.html#-_14)で、変更されたPropertyを検知して、`m_strength`を更新できるようにします。


# 床のアイテムに触れたら床を揺らして、n秒後に落とす

![2024-05-08 23-01-27.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629942/b1fc0a8c-4673-498e-3c1a-07ae0bf8a6cf.gif)


**Unity設定画面**

**床と不安定な床の設定を行っているHEO Object**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629942/dec5d932-a567-32a8-8818-566649764ce0.png)


**プレイヤーが床に触れたことを検知するArea Collider**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629942/ebf346e7-d121-a3b7-1b18-e34ca9ff377a.png)



**コード全文**
```C#:UnstableFooting.hs
component UnstableFooting
{
    Item m_item;
    string m_velocityValue;
    string m_strengthValue;

    int m_stableTime;
    int m_unstableTime;

    string m_state;
    int m_onFootingTime;
    bool m_isOnFooting;

    public UnstableFooting()
    {
        m_item = hsItemGetSelf();
        
        m_velocityValue = m_item.GetProperty("velocity(Vector3)");
        m_strengthValue = m_item.GetProperty("strength(float)");

        m_stableTime = m_item.GetProperty("stableTime(float)").ToFloat() * 1000;
        m_unstableTime = m_item.GetProperty("unstableTime(float)").ToFloat() * 1000;

        InitializeFooting();
    }

    void StartUnstableFooting()
    {
        m_isOnFooting = true;
        m_onFootingTime = hsSystemGetTime();
    }

    // 初期化時にステートをstableに戻して、位置も初期状態に戻す。
    public void InitializeFooting()
    {
        m_isOnFooting = false;
        m_state = "stable";
        m_item.CallComponentMethod("ItemMove", "ResetToStartPosition", "");
    }
    
    public void Update()
    {
        // 足場の上のコライダーに侵入したら足場のステートを監視する
        if(m_isOnFooting)
        {
            ChackState();
        }

        // 足場の上のステートを更新
        FootingState();
    }

    void ChackState()
    {
        // 足場の上のコライダーに侵入した時からstabeleステートを経過した場合、unstableステートに移行
        if(m_onFootingTime + (m_stableTime) < hsSystemGetTime())
        {
            m_state = "unstable";
        }

        // unstableステートも経過した場合、fallステートにに移行
        if(m_onFootingTime + (m_stableTime + m_unstableTime) < hsSystemGetTime())
        {
            m_state = "fall";
        }
    }

    // 足場のステートの更新
    public void FootingState()
    {
        switch (m_state)
        {
            //足場を揺らさず、移動もさせない
            case "stable":
                m_item.SetProperty("strength(float)", "0");
                m_item.SetProperty("velocity(Vector3)", "0,0,0");
                break;

            //足場を揺らして不安定な感じを演出する
            case "unstable":
                m_item.SetProperty("strength(float)", m_strengthValue);
                break;
            
            //足場を落とす
            case "fall":
                m_item.SetProperty("velocity(Vector3)", m_velocityValue);
                break;

            default:
                break;
        }
    }
}
```
## コード解説
### 床の状態をstateとして管理できるようにする
```C#
    public UnstableFooting()
    {
        InitializeFooting();
    }

    // 初期化時にステートをstableに戻して、位置も初期状態に戻す。
    public void InitializeFooting()
    {
        m_isOnFooting = false;
        m_state = "stable";
        m_item.CallComponentMethod("ItemMove", "ResetToStartPosition", "");
    }

    // 足場のステートの更新
    public void FootingState()
    {
        switch (m_state)
        {
            //足場を揺らさず、移動もさせない
            case "stable":
                m_item.SetProperty("strength(float)", "0");
                m_item.SetProperty("velocity(Vector3)", "0,0,0");
                break;

            //足場を揺らして不安定な感じを演出する
            case "unstable":
                m_item.SetProperty("strength(float)", m_strengthValue);
                break;
            
            //足場を落とす
            case "fall":
                m_item.SetProperty("velocity(Vector3)", m_velocityValue);
                break;

            default:
                break;
        }
    }
```
プレイヤーが床の上に乗ると、アイテムが揺れて、数秒後に床が落ち始めるといった機能を作るためにそれぞれの状態をstate（状態）として管理できるようにします。
必要そうなstateとして、

* stable : プレイヤーが上に乗っていない、もしくは接触してから時間がたっておらず床が揺れていない状態
* unstable : プレイヤーが上に乗ってから時間が経って、床が揺れ始めた状態
* fall : さらに時間がたって、床が揺れながら床が落ち始めた状態

を用意しておきます。
HeliScriptにはEnum型は用意されていないため、文字列でstateの管理を行います。

それぞれのstateごとの条件分岐を行うために、[Switch文](https://vrhikky.github.io/SceneBuilder_Documents/latest/hs/hs_statement_control.html#switch-)で、条件分岐を行います。
それぞれのstateで、各componentに対して、HEO Propertyで設定された値を設定します。

`InitializeFooting()`で、初期化用の関数を用意しておきます。


### stateを更新するための処理を作る

```C#    
    public void Update()
    {
        // 足場の上のコライダーに侵入したら足場のステートを監視する
        if(m_isOnFooting)
        {
            ChackState();
        }

        // 足場の上のステートを更新
        FootingState();
    }

    void ChackState()
    {
        // 足場の上のコライダーに侵入した時からstabeleステートを経過した場合、unstableステートに移行
        if(m_onFootingTime + (m_stableTime) < hsSystemGetTime())
        {
            m_state = "unstable";
        }

        // unstableステートも経過した場合、fallステートにに移行
        if(m_onFootingTime + (m_stableTime + m_unstableTime) < hsSystemGetTime())
        {
            m_state = "fall";
        }
    }
```
`CheckState()`関数は、[HeliScriptでアイテムを反復して動かす【コピペで使えるサンプルコード付き】](https://qiita.com/IwahanaFuku/items/dfc38371ad0ed7cc034d) で作ったような、指定秒待つ仕組みを実装しています。
HeliScriptは、Unity C#のInvoke()や、コールチンといった指定秒数待つ仕組みを持ちません。
なので、Update関数内で、開始時点の秒数＋待ちたい秒数が、現在の秒数よりも小さくなったかどうかで、指定秒待つ仕組みを作ります。

`CheckState()`関数を`Update()`関数内で毎フレーム呼び出すことによって、指定した秒数が経過したら、指定したstateに遷移するといった仕組みを作ることが出来ます

### 不安定な足場ギミックの状態を変更し始める

```C#    
    void StartUnstableFooting()
    {
        m_isOnFooting = true;
        m_onFootingTime = hsSystemGetTime();
    }
```
`StartUnstableFooting()`関数は、Unity側のエリアコライダーからプレイヤーが足場の上に乗ったことを検出します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629942/63d438aa-cff1-c5e7-b160-04121880da0d.png)

エリアコライダーに侵入時した際、`StartUnstableFooting()`関数を呼びたいため、

アイテム名：UnstableFooting
コンポーネント名：UnstableFooting
ファンクション名：StartUnstableFooting
String型引数：なし

と指定します。
それぞれ、
アイテム名は、UnstableFooting componentがアタッチされた対象となるHEO Object。
コンポーネント名は、StartUnstableFootingが含まれているcomponent
ファンクション名は、実際に呼びたい関数名。
今回は引数が必要ないため何も設定していません。

StartUnstableFooting()関数により、m_isOnFootingがtrueになったら計測を始めます。
[int hsSystemGetTime()](https://vrhikky.github.io/VketCloudSDK_Documents/12.3/hs/hs_system_function.html#hssystemgettime)は、単位がミリ秒なため、コンストラクタ内で、Propertyとして取得した待機時間を1000倍にしています。


:::note info
今シリーズで作成したcomponentはすべて一つのItemだけで完結していましたが、今回作成した不安定足場を複数個設置したい場合、UnstableFooting、UnstableFootingField、UnstableFootingAreaの名前を変更し、エリアコライダーに記載するCallScriptアクションの**アイテム名**は、対象となるUnstableFooting componentがアタッチされたHEO Object（今回であればUnstableFooting）の名前に設定する必要があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629942/499187bf-2b49-f596-7677-34d32ad010d2.png)
:::
 
 **前 :** [HeliScriptで振り子を動かす【コピペで使えるサンプルコード付き】](https://qiita.com/IwahanaFuku/items/886ff5e61282bf7b611d) 
 **次 :** [プレイヤーの名前とクリアタイムを表示できるようにする【コピペで使えるサンプルコード付き】](https://qiita.com/IwahanaFuku/private/f43127bea0cbd6447acb)