1. はじめに
準備編 で作成した 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 が毎回変動します。
3. FR
3.1. 「オペランダムへの反応」→「得点上昇」
Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の Operandum1 の Script にはなかったコードです。
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 を宣言
→ Editor上ではFRのスケジュール値を入れてください - public AudioClip Operandum1SE; ... public な AudioClip として Operandum1SE を宣言
→ 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にリセット
→ 得点が上昇すると回数がリセットされ、もう一度時間を計測しはじめる
→ 弁別刺激が点灯している間、FRが走り続ける
- **「if (Input.GetKeyDown(KeyCode.F))」**の処理 ... キーボードのFキーが押されたときの処理
3.2. 「強化オペランダムへの反応」→「得点上昇」
Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の Operandum1 の Script にはなかったコードです。
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 を宣言
→ Editor上ではFRのスケジュール値を入れてください
Update()
- New_1
- **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
- 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
- **「if (Input.GetKeyDown(KeyCode.F))」**の処理 ... キーボードのFキーが押されたときの処理
- 効果音( Operandum1SE )が鳴る
- Counterのカウントが1増加
- **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
- New_2
- **「if (Counter == FR)」**の処理 ... CounterがFRのスケジュール値に到達したときの処理
- Ramp_off を非アクティブ化、Ramp_on をアクティブ化
→ 疑似的に強化可能ランプの点灯を表現 - Invoke("ResetTimes", Ramp_Script.ReinforceableTime)
→ 強化可能ランプ点灯時から強化可能時間が経過すると、Counter を0にリセット
- Ramp_off を非アクティブ化、Ramp_on をアクティブ化
- **「if (Counter == FR)」**の処理 ... CounterがFRのスケジュール値に到達したときの処理
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を使用しています。
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次元の行列にする
- 乱数生成については こちら を参照してください
- ifの中の処理
- random_ の平均値がスケジュール値と同じになった場合、variable に random_ を格納
- variable に random_ を格納したらforループを中断
→ スケジュール値の範囲がよほど無茶なものでない限り、100の100乗回のforループは行われない
- スケジュール値の範囲 ( value_min から value_max まで ) の乱数(一様分布)を reinforcement 分作成して1次元の行列にする
4.1.2. *x times * のリストをCsvファイルに出力
# 「_」には、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というファイルを作成して、その中に入れます。
4.2. 「オペランダムへの反応」→「得点上昇」
Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の Operandum1 の Script にはなかったコードです。
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 CsvVariable = new List(); ... string型の List として CsvVariable を宣言
- public AudioClip Operandum1SE; ... public な AudioClip として Operandum1SE を宣言
→ Editor上では Operandum1 に反応したときに鳴るSEを入れてください
Start()
- New_1
- CsvファイルをUnityに読み込ませる
- こちらの記事 とやっていることは全く同じで、詳しい解説も載っているのでここでは割愛します
- New_2
- **「for (i = 0; i < values.Length; i++)」**の処理 ... 取得したCsvファイルの値を List の中に格納する処理
- C# のforループの書き方については こちら を参照してください
- **「for (i = 0; i < values.Length; i++)」**の処理 ... 取得したCsvファイルの値を List の中に格納する処理
Update()
- New_3
- **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
- 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
- **「if (Input.GetKeyDown(KeyCode.F))」**の処理 ... キーボードのFキーが押されたときの処理
- 効果音( Operandum1SE )が鳴る
- Counterのカウントが1増加
- **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
- New_4
- **「if (first)」**の処理 ... 取得するList内の要素の順番についての処理
- **「if (Counter == int.Parse(CsvVariable[CsvCounter]))」**の処理 ... CounterがVRのスケジュール値に到達したときの処理
- 効果音( PointSE )が鳴る
- 得点が1点上昇( Point += 1; )
- CsvCounterが1つ増加
→ 取得するList内の要素の順番を1つずらす - Coutner を0にリセット
→ 得点が上昇すると回数がリセットされ、もう一度時間を計測しはじめる
→ 弁別刺激が点灯している間、VRが走り続ける
- **「if (Counter == int.Parse(CsvVariable[CsvCounter]))」**の処理 ... CounterがVRのスケジュール値に到達したときの処理
- **「if (first)」**の処理 ... 取得するList内の要素の順番についての処理
4.3. 「強化オペランダムへの反応」→「得点上昇」
Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の Operandum1 の Script にはなかったコードです。
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 CsvVariable = new List(); ... string型の List として CsvVariable を宣言
Start()
- New_1
- CsvファイルをUnityに読み込ませる
- こちらの記事 とやっていることは全く同じで、詳しい解説も載っているのでここでは割愛します
- New_2
- **「for (i = 0; i < values.Length; i++)」**の処理 ... 取得したCsvファイルの値を List の中に格納する処理
- C# のforループの書き方については こちら を参照してください
- **「for (i = 0; i < values.Length; i++)」**の処理 ... 取得したCsvファイルの値を List の中に格納する処理
Update()
- New_3
- **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
- 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
- **「if (Input.GetKeyDown(KeyCode.F))」**の処理 ... キーボードのFキーが押されたときの処理
- 効果音( Operandum1SE )が鳴る
- Counterのカウントが1増加
- **「if (Sd1_on.activeSelf)」**の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
- New_4
- **「if (first)」**の処理 ... 取得するList内の要素の順番についての処理
- **「if (Counter == int.Parse(CsvVariable[CsvCounter]))」**の処理 ... CounterがVRのスケジュール値に到達したときの処理
- Ramp_off を非アクティブ化、Ramp_on をアクティブ化
→ 疑似的に強化可能ランプの点灯を表現 - CsvCounterが1つ増加
→ 取得するList内の要素の順番を1つずらす - first を false にする
→ 取得するList内の要素の順番が2つ以上ずれないようにする - Invoke("ResetTimes", Ramp_Script.ReinforceableTime)
→ 強化可能ランプ点灯時から強化可能時間が経過すると、Counter を0にリセットして、first を true に戻す
- Ramp_off を非アクティブ化、Ramp_on をアクティブ化
- **「if (Counter == int.Parse(CsvVariable[CsvCounter]))」**の処理 ... CounterがVRのスケジュール値に到達したときの処理
- **「if (first)」**の処理 ... 取得するList内の要素の順番についての処理
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). 行動分析学──行動の科学的理解をめざして── 有斐閣