Unityで行動分析 ～その2：比率スケジュール編～

# 1. はじめに
　[ 準備編 ](https://qiita.com/TeRa_YUKI/items/b9433da24af552d6b3e3)で作成した *Operandum1* の Script を編集して、比率スケジュールを作成します。基本的なことは準備編で一通り解説しているので、本記事では *Operandum1* の Script の解説のみとなります。また、今回も「オペランダムへの反応」→「得点上昇」と「強化オペランダムへの反応」→「得点上昇」の2つの場合を考慮して解説したいと思います。

# 2. 比率スケジュールとは
　比率スケジュール(Ratio schedule)とは、「強化子提示後に一定回数自発された反応に強化子が随伴する(坂上・井上, 2018, pp.173)」強化スケジュールです。一定回数 ( スケジュール値 ) が固定である場合は**固定比率スケジュール** ( Fixed Ratio schedule; **FR** ) 、平均するとスケジュール値になる場合は**変動比率スケジュール** ( Variable Ratio schedule; **VR** ) と呼びます。
　Unityで作成する場合は、弁別刺激点灯時に限り反応はいつでも受けつけるけれど、スケジュール値 ( *x times* ) に到達しなければ強化子を得られない ( あるいは *Ramp* が点灯しない ) ようにすれば良いです。比率スケジュールのイメージ図を下に示します。下図の左は「オペランダムへの反応」→「得点上昇」を、下図の右は「強化オペランダムへの反応」→「得点上昇」を示しています。FRであれば *x times* が常に一定となり、VRであれば *x times* が毎回変動します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555056/4c5754bc-04f4-e04a-deae-78a16010a128.png" width=90%>

# 3. FR
## 3.1. 「オペランダムへの反応」→「得点上昇」
　Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の *Operandum1* の Script にはなかったコードです。

```C#:Operandum1
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Operandum1_Script : MonoBehaviour
{
    int Point = 1;
    public GameObject Sd1_off;
    public GameObject Sd1_on;
    public Text CountText;
    public AudioClip PointSE;
    AudioSource audioSource;

    //New
    int Counter = 0;
    public int FRTimes;
    public AudioClip Operandum1SE;


    void Start()
    {
        audioSource = GetComponent<AudioSource>();
    }


    // New
    void Update()
    {
        if (Sd1_on.activeSelf)
        {
            if (Input.GetKeyDown(KeyCode.F))
            {
                Counter += 1;
                audioSource.PlayOneShot(Operandum1SE);
                if (Counter == FRTimes)
                {
                    audioSource.PlayOneShot(PointSE);
                    CountText.text = "Point : " + Point.ToString();
                    Point += 1;
                    Counter = 0;
                }
            }
        }

        if (Sd1_off.activeSelf)
        {
            Counter = 0;
        }
    }
}
```
　このScriptの流れをざっくり書くと、変数の宣言 → Start() → Update()となります。解説は「// New」と書かれている箇所のみ行います。

**変数の宣言**

* int Counter; ... int型の変数 Counter を宣言
* public int FRTimes; ... public な float型の変数 FRTimes を宣言<br>
 → Editor上ではFRのスケジュール値を入れてください
* public AudioClip Operandum1SE; ... public な AudioClip として Operandum1SE を宣言<br>
 → Editor上では *Operandum1* に反応したときに鳴るSEを入れてください

**Update()**

* **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
    * **「if (Input.GetKeyDown(KeyCode.F))」**の処理 ... キーボードのFキーが押されたときの処理
        * 効果音( Operandum1SE )が鳴る
        * Counterのカウントが1増加
        * **「if (Counter == FR)」**の処理 ... CounterがFRのスケジュール値に到達したときの処理
            * 効果音( PointSE )が鳴る
            * 得点が1点上昇( Point += 1; )
            * Counter を0にリセット<br>
             → 得点が上昇すると回数がリセットされ、もう一度時間を計測しはじめる<br>
             → 弁別刺激が点灯している間、FRが走り続ける

## 3.2. 「強化オペランダムへの反応」→「得点上昇」
　Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の *Operandum1* の Script にはなかったコードです。

```C#:Operandum1
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Operandum1_Script : MonoBehaviour
{
    public GameObject Sd1_off;
    public GameObject Sd1_on;
    public GameObject Ramp_off;
    public GameObject Ramp_on;
    public AudioClip Operandum1SE;
    AudioSource audioSource;
    GameObject Ramp;
    Ramp_Script Ramp_Script;

    //New
    int Counter = 0;
    public int FRTimes;

    void ResetTimes()
    {
        Counter = 0;
    }


    void Start()
    {
        audioSource = GetComponent<AudioSource>();
        Ramp = GameObject.Find("Ramp");
        Ramp_Script = Ramp.GetComponent<Ramp_Script>();
    }

    
    void Update()
    {
        // New_1
        if (Sd1_on.activeSelf)
        {
            if (Input.GetKeyDown(KeyCode.F))
            {
                audioSource.PlayOneShot(Operandum1SE);
                Counter += 1;

                // New_2
                if (Counter == FRTimes)
                {
                    Ramp_off.SetActive(false);
                    Ramp_on.SetActive(true);
                    Invoke("ResetTimes", Ramp_Script.ReinforceableTime);
                }
            }
        }

        if (Sd1_off.activeSelf)
        {
            Counter = 0;
            Ramp_off.SetActive(true);
            Ramp_on.SetActive(false);
        }
    }
}
```
　このScriptの流れをざっくり書くと、変数の宣言 → Start() → Update()となります。解説は「// New」と書かれている箇所のみ行います。

**変数の宣言**

* int Counter; ... int型の変数 Counter を宣言
* public int FRTimes; ... public な float型の変数 FRTimes を宣言<br>
 → Editor上ではFRのスケジュール値を入れてください

**Update()**

* New_1
    * **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
        * 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
        * **「if (Input.GetKeyDown(KeyCode.F))」**の処理 ... キーボードのFキーが押されたときの処理
            * 効果音( Operandum1SE )が鳴る
            * Counterのカウントが1増加
* New_2
    * **「if (Counter == FR)」**の処理 ... CounterがFRのスケジュール値に到達したときの処理
        * Ramp_off を非アクティブ化、Ramp_on をアクティブ化<br>
         → 疑似的に強化可能ランプの点灯を表現
        * Invoke("ResetTimes", Ramp_Script.ReinforceableTime)<br>
         → 強化可能ランプ点灯時から強化可能時間が経過すると、Counter を0にリセット

# 4. VR
## 4.1. Pythonで *x sec* のリストを作成する
　VRでは、FRとは異なり、*x times* が一定ではなく変動します。この変動した値をUnity上で作成しても良いのですが、先にPythonで *x times* のリストを作成してCsv形式で出力しておきます。その後、作成したCsv形式の *x times * のリストをUnityで読み込みます。

### 4.1.1.  *x times* のリストを作成する関数
　 *x times * のリストを作成する関数は下記のとおりです。環境について、Pythonのバージョンは「Python 3.7.1」で、Jupyter Notebookを使用しています。

```Python:Python
import numpy as np

def variable(value, value_min, value_max, reinforcement):
    for i in range(100**100):
        random_ = np.random.randint(value_min, value_max, reinforcement)
        if random_.mean()==value:
            variable = random_
            break
    return variable
```

* forの中の処理
    * スケジュール値の範囲 ( value_min から value_max まで ) の乱数(一様分布)を reinforcement 分作成して1次元の行列にする
        * 乱数生成については[ **こちら** ](https://note.nkmk.me/python-numpy-random/)を参照してください
    * ifの中の処理
        * random_ の平均値がスケジュール値と同じになった場合、variable に random_ を格納
        * variable に random_ を格納したらforループを中断<br>
 → スケジュール値の範囲がよほど無茶なものでない限り、100の100乗回のforループは行われない

### 4.1.2.  *x times * のリストをCsvファイルに出力

```Python:Python
# 「_」には、value, range_min, range_max, reinforcementの値を入れてください
value, range_min, range_max, reinforcement = _, _, _, _
variable = variable(value, range_min, range_max, reinforcement)

# 「/」の前にデータの出力先を入れてください
np.savetxt('/Variable.csv', variable, delimiter=',') 
```
　作成したCsvファイルは、Assetの中のResourcesというファイルを作成して、その中に入れます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555056/0fe03efe-1f59-6140-1500-d061444a66ad.png" width=35%>



## 4.2. 「オペランダムへの反応」→「得点上昇」
　Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の *Operandum1* の Script にはなかったコードです。

```C#:Operandum1
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.IO;

public class Operandum1_Script : MonoBehaviour
{
    int Point = 1;
    public GameObject Sd1_off;
    public GameObject Sd1_on;
    public Text CountText;
    public AudioClip PointSE;
    AudioSource audioSource;

    //New
    bool first = true;
    int Counter = 0;
    int i;
    int CsvCounter = 0;
    private List<string> CsvVariable = new List<string>();
    public AudioClip Operandum1SE;


    void Start()
    {
        audioSource = GetComponent<AudioSource>();

        //New_1
        TextAsset Csv = Resources.Load("Variable") as TextAsset;
        StringReader reader = new StringReader(Csv.text);
        while (reader.Peek() != -1)
        {
            string line = reader.ReadLine();
            string[] values = line.Split(',');

            // New_2
            for (i = 0; i < values.Length; i++)
            {
                CsvVariable.Add(values[i]);
            }
        }
    }


    void Update()
    {
        // New_3
        if (Sd1_on.activeSelf)
        {
            if (Input.GetKeyDown(KeyCode.F))
            {
                audioSource.PlayOneShot(Operandum1SE);
                Counter += 1;

                // New_4
                if (first)
                {
                    if (Counter == int.Parse(CsvVariable[CsvCounter]))
                    {
                        audioSource.PlayOneShot(PointSE);
                        CountText.text = "Point : " + Point.ToString();
                        Point += 1;
                        CsvCounter += 1;
                        Counter = 0;
                    }
                }
            }
        }

        if (Sd1_off.activeSelf)
        {
            Counter = 0;
        }
    }
}
```
　このScriptの流れをざっくり書くと、変数の宣言 → Start() → Update()となります。5行目に「using System.IO;」が追加されているので注意してください。解説は「// New」と書かれている箇所のみ行います。

**変数の宣言**

* bool first = true; ... bool型の変数 first が true であることを宣言
* int Counter; ... int型の変数 Counter を宣言
* int i; ... int型の変数 i を宣言
* int CsvCounter = 0; ... int型の変数 CsvCounter が 0 であることを宣言
* private List<string> CsvVariable = new List<string>(); ... string型の List として CsvVariable を宣言
* public AudioClip Operandum1SE; ... public な AudioClip として Operandum1SE を宣言<br>
 → Editor上では *Operandum1* に反応したときに鳴るSEを入れてください

**Start()**

* New_1
    * CsvファイルをUnityに読み込ませる
    * [ **こちらの記事** ](https://note.com/macgyverthink/n/n83943f3bad60)とやっていることは全く同じで、詳しい解説も載っているのでここでは割愛します
* New_2
    * **「for (i = 0; i < values.Length; i++)」**の処理 ... 取得したCsvファイルの値を List の中に格納する処理
        * C# のforループの書き方については[ **こちら** ](http://kimama-up.net/unity-for/)を参照してください

**Update()**

* New_3
    * **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
        * 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
        * **「if (Input.GetKeyDown(KeyCode.F))」**の処理 ... キーボードのFキーが押されたときの処理
            * 効果音( Operandum1SE )が鳴る
            * Counterのカウントが1増加
* New_4
    * **「if (first)」**の処理 ... 取得するList内の要素の順番についての処理
        * **「if (Counter == int.Parse(CsvVariable[CsvCounter]))」**の処理 ... CounterがVRのスケジュール値に到達したときの処理
            * 効果音( PointSE )が鳴る
            * 得点が1点上昇( Point += 1; )
            * CsvCounterが1つ増加<br>
            → 取得するList内の要素の順番を1つずらす
            * Coutner を0にリセット<br>
             → 得点が上昇すると回数がリセットされ、もう一度時間を計測しはじめる<br>
             → 弁別刺激が点灯している間、VRが走り続ける

## 4.3. 「強化オペランダムへの反応」→「得点上昇」
　Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の *Operandum1* の Script にはなかったコードです。

```C#:Operandum1
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;

public class Operandum1_Script : MonoBehaviour
{
    public GameObject Sd1_off;
    public GameObject Sd1_on;
    public GameObject Ramp_off;
    public GameObject Ramp_on;
    public AudioClip Operandum1SE;
    AudioSource audioSource;
    GameObject Ramp;
    Ramp_Script Ramp_Script;

    //New
    bool first = true;
    int Counter = 0;
    int i;
    int CsvCounter = 0;
    private List<string> CsvVariable = new List<string>();


    void ResetTimes()
    {
        Counter = 0;
        first = true;
    }


    void Start()
    {
        audioSource = GetComponent<AudioSource>();
        Ramp = GameObject.Find("Ramp");
        Ramp_Script = Ramp.GetComponent<Ramp_Script>();

        //New_1
        TextAsset Csv = Resources.Load("Variable") as TextAsset;
        StringReader reader = new StringReader(Csv.text);
        while (reader.Peek() != -1)
        {
            string line = reader.ReadLine();
            string[] values = line.Split(',');

            // New_2
            for (i = 0; i < values.Length; i++)
            {
                CsvVariable.Add(values[i]);
            }
        }
    }


    void Update()
    {
        // New_3
        if (Sd1_on.activeSelf)
        {
            if (Input.GetKeyDown(KeyCode.F))
            {
                audioSource.PlayOneShot(Operandum1SE);
                Counter += 1;

                // New_4
                if (first)
                {
                    if (Counter == int.Parse(CsvVariable[CsvCounter]))
                    {
                        Ramp_off.SetActive(false);
                        Ramp_on.SetActive(true);
                        CsvCounter += 1;
                        first = false;
                        Invoke("ResetTimes", Ramp_Script.ReinforceableTime);
                    }
                }
            }

            if (Sd1_off.activeSelf)
            {
                Counter = 0;
                Ramp_off.SetActive(true);
                Ramp_on.SetActive(false);
            }
        }
    }
}
```
　このScriptの流れをざっくり書くと、変数の宣言 → Start() → Update() → ResetTime()となります。5行目に「using System.IO;」が追加されているので注意してください。また、ResetTime()では「time = 0;」だけではなく「first = true;」も書かれてあるので注意してください。解説は「// New」と書かれている箇所のみ行います。

**変数の宣言**

* bool first = true; ... bool型の変数 first が true であることを宣言
* int Counter; ... int型の変数 Counter を宣言
* int i; ... int型の変数 i を宣言
* int CsvCounter = 0; ... int型の変数 CsvCounter が 0 であることを宣言
* private List<string> CsvVariable = new List<string>(); ... string型の List として CsvVariable を宣言

**Start()**

* New_1
    * CsvファイルをUnityに読み込ませる
    * [ **こちらの記事** ](https://note.com/macgyverthink/n/n83943f3bad60)とやっていることは全く同じで、詳しい解説も載っているのでここでは割愛します
* New_2
    * **「for (i = 0; i < values.Length; i++)」**の処理 ... 取得したCsvファイルの値を List の中に格納する処理
        * C# のforループの書き方については[ **こちら** ](http://kimama-up.net/unity-for/)を参照してください

**Update()**

* New_3
    * **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
        * 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
        * **「if (Input.GetKeyDown(KeyCode.F))」**の処理 ... キーボードのFキーが押されたときの処理
            * 効果音( Operandum1SE )が鳴る
            * Counterのカウントが1増加
* New_4
    * **「if (first)」**の処理 ... 取得するList内の要素の順番についての処理
        * **「if (Counter == int.Parse(CsvVariable[CsvCounter]))」**の処理 ... CounterがVRのスケジュール値に到達したときの処理
            * Ramp_off を非アクティブ化、Ramp_on をアクティブ化<br>
             → 疑似的に強化可能ランプの点灯を表現
            * CsvCounterが1つ増加<br>
            → 取得するList内の要素の順番を1つずらす
            * first を false にする
            → 取得するList内の要素の順番が2つ以上ずれないようにする
            * Invoke("ResetTimes", Ramp_Script.ReinforceableTime)<br>
         → 強化可能ランプ点灯時から強化可能時間が経過すると、Counter を0にリセットして、first を true に戻す

# 5. 最後に
　「比率スケジュール(Ratio schedule)」の中の、「固定比率スケジュール(Fixed Ratio schedule)」と「変動比率スケジュール(Variable Ratio schedule)」をUnityで作る方法の解説を行いました。コードや用語等で間違っている点があれば、ご指摘いただけると幸いです。

# 参考URL

・NumPy, randomで様々な種類の乱数の配列を生成
https://note.nkmk.me/python-numpy-random/

・Unity で CSV ファイルを読み込む方法
https://note.com/macgyverthink/n/n83943f3bad60

・【Unity】C#の基本構文『for』
http://kimama-up.net/unity-for/


# 引用文献
坂上 貴之・井上 雅彦 (2018). 行動分析学──行動の科学的理解をめざして── 有斐閣