HeliScriptでアイテムを反復して動かす【コピペで使えるサンプルコード付き】

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

VR法人HIKKYのorganization下のQiita/Zenn両方に投稿しております。
:::
## はじめに
次にアイテムを反復して動かすためのcomponentを作っていきます。
このcomponentは上下に反復して動く床や、横に押し出されるブロックのために使用することを想定しています。
heliscriptでは、[hsMathSine(float radian)](https://vrhikky.github.io/VketCloudSDK_Documents/12.3/hs/hs_system_function_math.html#hsmathsinfloat)という組み込み関数が用意されているため、こちらを使用して反復して動く床を作っていきます。

また、今回は反復が折り返す地点で、一時停止を行い動きにメリハリをつけられるように設定します。
プロパティとして、反復する方向と大きさ、反復する幅、反復の開始地点、一時停止する時間を設定できるようにしましょう。

![2024-03-29 11-12-34.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629942/c6dc39d2-3ec1-1af4-1322-a901443665e3.gif)

**Unity設定画面**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629942/2f901635-c164-da64-21c1-e8347c7198c1.png)

**コード全文**
```C#:Utility.hs
class Utility
{
    //String型をVector3型に変換する関数
    public Vector3 StrToVector3(string str)
    {
        Vector3 result;

        list<string> strVec = str.Split(",");
        result = makeVector3(strVec[0].ToFloat(), strVec[1].ToFloat(), strVec[2].ToFloat());
        return result;
    }

    //Vector3型とfloat型の掛け算を行う関数
    public Vector3 Vector3MulFloat(Vector3 vec, float f)
    {
        Vector3 result;
        result = makeVector3(vec.x * f, vec.y * f, vec.z * f);
        return result;
    }
}
```

```C#:ItemRepetitionMove.hs
component ItemRepetitionMove
{
    Utility m_UT; //Utilityクラスを宣言する
	Item  m_item; //コンポーネントがセットされたItemを取得する

	Vector3 m_oneRoundDuration; //反復する方向と大きさ
	float m_amplitude; //反復する幅
	float m_offset; //反復の開始地点
    float m_pauseTime; //一時停止の時間
	
	Vector3 m_itemInitialPos; //アイテムが配置された初期地点
    Vector3 m_timeCounter; //時間ごとの位置を格納する
    int m_changeDirectionTime; //進行方向が変わったときの時間
    Vector3 m_resultPos; //移動結果の位置
    float m_prevDistance; //1フレーム前の移動距離
    float m_prevSign; //1フレーム前の移動方向
    bool m_isPaused; //ポーズを行うかを格納する。
    

    public ItemRepetitionMove()
    {
        m_UT = new Utility(); //Utilityクラスを初期化する
        m_item = hsItemGetSelf();

		Vector3 beforOneRoundDuration = m_UT.StrToVector3(m_item.GetProperty("oneRoundDuration(Vector3)"));
        m_oneRoundDuration = m_UT.Vector3MulFloat(beforOneRoundDuration, 2 * PI);
		m_amplitude = m_item.GetProperty("amplitude(float)").ToFloat();
		m_offset = m_item.GetProperty("offset(float)").ToFloat() * 2 * PI;
		m_pauseTime = m_item.GetProperty("pauseTime(float)").ToFloat() * 1000f; //単位をミリ秒にする

		m_itemInitialPos = m_item.GetPos();
        m_timeCounter = new Vector3();
        m_resultPos = new Vector3();
	}

    //毎フレーム呼びだされる。
	public void Update()
	{
        if(m_isPaused)
		{
            //フラグが経った時間と、m_pauseTimeに設定した秒数を加えた時間待つ
			if(m_changeDirectionTime + (m_pauseTime) < hsSystemGetTime())
			{
				m_isPaused = false;
			}
		} 
        else
        {
            RepetitionMoveItem();
            CheckConversion();
        }
	}

    //ItemをSinカーブに沿って反復させる関数。経過時間とパラメータを基に新しい位置を計算し、Itemに適用する。
	void RepetitionMoveItem()
	{
        //反復する方向と大きさに経過時間をかけて、Sinカーブに設定する値を求める
        m_timeCounter.Add(m_UT.Vector3MulFloat(m_oneRoundDuration, hsSystemGetDeltaTime()));

        //ベクトルの要素ごとにオフセットを加えたSinカーブを求める
        Vector3 sinPos = makeVector3(hsMathSin(m_offset + m_timeCounter.x), hsMathSin(m_offset + m_timeCounter.y), hsMathSin(m_offset + m_timeCounter.z));

        //振幅の大きさを求める
        m_resultPos = m_UT.Vector3MulFloat(sinPos, m_amplitude);

        //反復した位置に対して、最初の配置位置を加える
        m_resultPos.Add(m_itemInitialPos);

        //ポジションを設定する
        m_item.SetPos(m_resultPos);
	} 

    // 1フレーム前と現在のポジションの変化を検出し、進行方向が変わったかどうかをチェックする関数
	void CheckConversion()
	{
        // m_itemInitialPos（初期ポジション）とm_resultPos（現在のポジション）間の距離を計算
        float distance = Distance(m_itemInitialPos, m_resultPos);
        // 今フレームから前フレームの距離距離を引いた値の符号を計算（進行方向の変化を検出）
        float sign = Sign(distance - m_prevDistance);

        // 前フレームの符号と今フレームの符号が異なり、かつ、その符号が-1である場合
        if (m_prevSign != sign && sign == -1f)
        {
            m_isPaused = true;
            m_changeDirectionTime = hsSystemGetTime();
        }

        // 現在の距離と符号を次のフレームの比較のために保持
        m_prevDistance = distance;
        m_prevSign = sign;
	}

    //二つのVector3の距離を返す関数
    float Distance(Vector3 vec1, Vector3 vec2)
    {
        float result;

        float deltaX = vec2.x - vec1.x;
        float deltaY = vec2.y - vec1.y;
        float deltaZ = vec2.z - vec1.z;
        
        float delta = deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ;
        result = hsMathSqrt(delta);

        return result;
    }

    //floatの符号を返す関数
	int Sign(float value)
	{
		int result;

		if (value < 0)
		{
			result = -1;
		}
		else
		{
			result =  1;
		}

		return result;
	}
}
```
## コード解説
```C#:Utility.hs
class Utility
{
    //String型をVector3型に変換する関数
    public Vector3 StrToVector3(string str)
    {
        Vector3 result;

        list<string> strVec = str.Split(",");
        result = makeVector3(strVec[0].ToFloat(), strVec[1].ToFloat(), strVec[2].ToFloat());
        return result;
    }

    //Vector3型とfloat型の掛け算を行う関数
    public Vector3 Vector3MulFloat(Vector3 vec, float f)
    {
        Vector3 result;
        result = makeVector3(vec.x * f, vec.y * f, vec.z * f);
        return result;
    }
}
```

Utility.hsは、[Heliscriptでアイテムをまっすぐ動かす【コピペで使えるサンプルコード付き】](https://qiita.com/IwahanaFuku/private/f171b7af2a9e15dc2139)で作成したItemMove componentに含まれていた、`public Vector3 StrToVector3(string str)`と、`public Vector3 Vector3MulFloat(Vector3 vec, float f)`を格納しています。
Propertyの宣言や各種計算で頻出するため、一つのclassにまとめておきます。

componentではなく、classとして定義したクラスオブジェクトをVket Cloudに読みこませるためには、VketCloudSettings > BaseSettingのHeliScriptにHeliScriptファイルを登録しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629942/2e5a97c3-272a-5d2f-cdea-4382f7b81736.png)

```C#
    Utility m_UT; //Utilityクラスを宣言する

    public ItemRepetitionMove()
    {
        m_UT = new Utility(); //Utilityクラスを初期化する
    }
```
HeliScriptには、staticの概念がない（12.3時点）ため、使用する際はクラスを生成して使用します。

### メンバ変数の宣言とコンストラクタ
```C#
    Utility m_UT; //Utilityクラスを宣言する
	Item  m_item; //コンポーネントがセットされたItemを取得する

	Vector3 m_oneRoundDuration; //反復する方向と大きさ
	float m_amplitude; //反復する幅
	float m_offset; //反復の開始地点
    float m_pauseTime; //一時停止の時間
	
	Vector3 m_itemInitialPos; //アイテムが配置された初期地点
    Vector3 m_timeCounter; //時間ごとの位置を格納する
    int m_changeDirectionTime; //進行方向が変わったときの時間
    Vector3 m_resultPos; //移動結果の位置
    float m_prevDistance; //1フレーム前の移動距離
    float m_prevSign; //1フレーム前の移動方向
    bool m_isPaused; //ポーズを行うかを格納する。
    

    public ItemRepetitionMove()
    {
        m_UT = new Utility(); //Utilityクラスを初期化する
        m_item = hsItemGetSelf();

		Vector3 beforOneRoundDuration = m_UT.StrToVector3(m_item.GetProperty("oneRoundDuration(Vector3)"));
        m_oneRoundDuration = m_UT.Vector3MulFloat(beforOneRoundDuration, 2 * PI);
		m_amplitude = m_item.GetProperty("amplitude(float)").ToFloat();
		m_offset = m_item.GetProperty("offset(float)").ToFloat() * 2 * PI;
		m_pauseTime = m_item.GetProperty("pauseTime(float)").ToFloat() * 1000f;

		m_itemInitialPos = m_item.GetPos();
        m_timeCounter = new Vector3();
        m_resultPos = new Vector3();
	}
 ```
 
コンストラクタの設定に関しては、ItemMove componentと同じように設定を行っています。
ただ、Propertyの設定時に1を設定したら1秒に1往復したほうが調整しやすいため、2 * πをm_oneRoundDurationと、m_offsetにかけます。
πは、HeliScript上では、`PI`として、定数に登録されているため、こちらを使用して、2 * πの計算を行います。

詳しくは後述しますが、m_pauseTimeは、heliscriptで取得できる秒数が、ミリ秒なため、単位を合わせるために1000倍しています。

また、反復が折り返す地点で、一時停止を行うため、その計算に必要な変数を格納するためのm_prevDistance、m_prevSignという変数を用意しています。

### 反復した動きを作る
```C#
    //ItemをSinカーブに沿って反復させる関数。経過時間とパラメータを基に新しい位置を計算し、Itemに適用する。
	void RepetitionMoveItem()
	{
        //反復する方向と大きさに経過時間をかけて、Sinカーブに設定する値を求める
        m_timeCounter.Add(m_UT.Vector3MulFloat(m_oneRoundDuration, hsSystemGetDeltaTime()));

        //ベクトルの要素ごとにオフセットを加えたSinカーブを求める
        Vector3 sinPos = makeVector3(hsMathSin(m_offset + m_timeCounter.x), hsMathSin(m_offset + m_timeCounter.y), hsMathSin(m_offset + m_timeCounter.z));

        //振幅の大きさを求める
        m_resultPos = m_UT.Vector3MulFloat(sinPos, m_amplitude);

        //反復した位置に対して、最初の配置位置を加える
        m_resultPos.Add(m_itemInitialPos);

        //ポジションを設定する
        m_item.SetPos(m_resultPos);
	} 
```

`RepetitionMoveItem()`関数は、[hsMathSin(float radian)](https://vrhikky.github.io/VketCloudSDK_Documents/12.3/hs/hs_system_function_math.html#hsmathsinfloat)関数をもとにした反復する動きを実現するための関数です、hsMathSin関数は、Unity C#だと[Mathf-Sin](https://docs.unity3d.com/ja/current/ScriptReference/Mathf.Sin.html)関数と同じ挙動を行う関数であり、引数として渡した値をラジアンの角度とした、円周のx座標位置を返す関数です。入力に対して反復するような値を取得することが出来るため、この性質を利用して反復する動きを作ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629942/1c6d460a-bb99-0b20-5e58-1a10f7cdc127.png)

Sin関数について馴染みのない方は、UnityでSin関数についてより詳しく解説されている記事などをご参照ください。
[【Unity】Sin関数の使い方](https://qiita.com/Nekomasu/items/f526b9392fd16f2bd243)
[Unity C#で学ぶ数学 #三角関数](https://qiita.com/miya-z/items/e5134687712ecff9104b#%E4%B8%89%E8%A7%92%E9%96%A2%E6%95%B0)

### 最大最小地点で折り返したことを検知する
```C#
    // 1フレーム前と現在のポジションの変化を検出し、進行方向が変わったかどうかをチェックする関数
	void CheckConversion()
	{
        // m_itemInitialPos（初期ポジション）とm_resultPos（現在のポジション）間の距離を計算
        float distance = Distance(m_itemInitialPos, m_resultPos);
        // 今フレームから前フレームの距離距離を引いた値の符号を計算（進行方向の変化を検出）
        float sign = Sign(distance - m_prevDistance);

        // 前フレームの符号と今フレームの符号が異なり、かつ、その符号が-1である場合
        if (m_prevSign != sign && sign == -1f)
        {
            m_isPaused = true;
            m_changeDirectionTime = hsSystemGetTime();
        }

        // 現在の距離と符号を次のフレームの比較のために保持
        m_prevDistance = distance;
        m_prevSign = sign;
	}

    //二つのVector3の距離を返す関数
    float Distance(Vector3 vec1, Vector3 vec2)
    {
        float result;

        float deltaX = vec2.x - vec1.x;
        float deltaY = vec2.y - vec1.y;
        float deltaZ = vec2.z - vec1.z;
        
        float delta = deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ;
        result = hsMathSqrt(delta);

        return result;
    }

    //floatの符号を返す関数
	int Sign(float value)
	{
		int result;

		if (value < 0)
		{
			result = -1;
		}
		else
		{
			result =  1;
		}

		return result;
	}
```
アイテムの往復が折り返したことを検知する方法として、
* 赤丸：今回と前回のフレームが初期位置から移動している方向が違っていること
* 青四角：今回のフレームが初期位置から移動した距離と、前回のフレームの初期位置から移動した距離を引いた時、結果がマイナスになる。つまり初期位置に向かって移動していること

を条件としています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629942/235c739f-5aee-234d-2c05-0fb5fb984565.png)

Distance関数は、初期位置から移動した距離を計算するため、Sign関数は、移動方向を計算するために作成しています。

条件が達成されると[int hsSystemGetTime()](https://vrhikky.github.io/VketCloudSDK_Documents/12.3/hs/hs_system_function.html#hssystemgettime)を使って、m_isPausedをtrueにして、現在の経過時間を取得するようにします。

### 設定した秒数待機する
```C#
    //毎フレーム呼びだされる。
	public void Update()
	{
        if(m_isPaused)
		{
            //フラグが経った時間と、m_pauseTimeに設定した秒数を加えた時間待つ
			if(m_changeDirectionTime + (m_pauseTime) < hsSystemGetTime())
			{
				m_isPaused = false;
			}
		} 
        else
        {
            RepetitionMoveItem();
            CheckConversion();
        }
	}
```
HeliScriptは、Unity C#のInvoke()や、コールチンといった指定秒数待つ仕組みを持ちません。
なので、Update関数内で、開始時点の秒数＋待ちたい秒数が、現在の秒数よりも小さくなったかどうかで、指定秒待つ仕組みを作ります。
Update関数内で、m_isPausedがTrueになったら計測を初めて、条件が達成されたら、m_isPausedをfalseに戻します。
[int hsSystemGetTime()](https://vrhikky.github.io/VketCloudSDK_Documents/12.3/hs/hs_system_function.html#hssystemgettime)は、単位がミリ秒なため、コンストラクタ内で、Propertyとして取得した待機時間を1000倍にしています
```C#
    m_pauseTime = m_item.GetProperty("pauseTime(float)").ToFloat() * 1000f;
```

なかなか複雑な動きが実装できるようになってきました！

 **前 :** [HeliScriptでアイテムをまっすぐ動かす【コピペで使えるサンプルコード付き】](https://qiita.com/IwahanaFuku/private/f171b7af2a9e15dc2139) 
 **次 :** [HeliScriptで中間ポイント、ゲームオーバー、ゲームクリアを実装する【コピペで使えるサンプルコード付き】](https://qiita.com/IwahanaFuku/private/a5479a1a13f6267e558d)