- 投稿日:2021-02-27T22:23:54+09:00
FinalIK for VRChat [アイテム追従・持ち替え]
はじめに
VRChat(以下VRC)での使用を想定したFinalIKの設定方法についてまとめました.
今回はFilanIKの基本的な使い方として,
- LimbIKを使ってアイテムをアバターの手に追従させる
- 二つのLimbIKを使ってアイテムの追従先を切り替える(手<->頭)
について説明したいと思います.
概要
FinalIKとはUnityのアセットで,IKを使ってアバターやオブジェクトの動きを制御できるツールです.
また,VRChat内に持ち込むことができる外部アセットの一つです.
一例として,FinalIKを使うと多関節アームを柔軟に操作でき,動画のようなアバター改変をすることができます.
#FinalIK for #VRChat
— Chootana (@choo_zap) February 23, 2021
現時点で理解できてることを全て詰め込んだスチームパンク・メカこんちゃんです
1フルトラ
2両足のフルトラを止めて空中浮遊
3サブアームの先端が足として動くモード.歩行アニメーションも追従します
4サブアームの床固定モード.体が上下移動しても柔軟に支えてくれます pic.twitter.com/RzNv0lsmev対象読者
VRCでFinalIKを使いたい方とします.
また今回はAvatar3.0でアバターをアップロードしたことある方を対象とします.
Avatar3.0についてはVRChat 続・初心者向けUnity備忘録で一通りの設定方法を学ぶことをおすすめします.環境
注意:
VRCのバージョンによってVRC内でのFinalIKの挙動が変更される可能性があります.動作確認した環境は以下の通りです.
- Unity: 2018.4.20f1
- VRC: 2021.1.2
- FinalIK
- $90. セールで安くなるタイミングでの購入がおススメです!
IK(Inverse Kinematics)について
アバター改変時に右腕の動きをチェックしたいとすると,Armatureの肩関節(Shoulder)を回転させて,次に肘関節(Elbow)を回転させて...とするかと思います.
このように各関節を一つずつ動かすことで全体の動きを作っていく仕組みをFK(Forward Kinematics)と呼びます.
反対に,アバターの手の位置や姿勢を特定のターゲット位置・姿勢に合わせて,残りの関節の回転を手に合わせて計算する仕組みをIK(Inverse Kinematics)と呼びます.
VRCでもHMDと2つのコントローラ(+トラッカー)の位置・姿勢からIKを解くことで体全体の動きを制御しているので,VRCをプレイする方には身近な存在だと言えます.実際にIK制御するために必要なものは,以下の三つになります.
複数の関節を持つモデル(実際に動かしたいキャラクターや多関節アームなど)
ターゲット(モデルの部位を合わせたい位置・姿勢)
IKコントローラー(VRIK, FBBIK, FABRIK, CCDIKなど)
FinalIKでは様々なIKソルバーがコンポーネントとして用意されています
全体図
今回はオブジェクト(cube)をIKで制御することで,腕や頭に追従させたいと思います.
全体の階層構造は以下の通りです.
- Avatar(VRC Avatar Descriptorある階層)
- Armature(HMDやコントローラで操作するスケルトン)
- Body(スキニングされたモデル)
- IKTargets
- IKControllers
- IKModel
用意したアバター直下に3つの空オブジェクトを追加して,それぞれ分かりやすいようにIKTargets/IKControllers/IKModelとします.
それぞれのオブジェクトについて実際に説明します.Point
VRCのバージョン変更によってFilanIKの挙動が変更されることがあります.
またVRCでのIKとFinalIKが衝突し,Unityでは上手く動作していてもVRCに持ち込むとおかしくなるケースを確認しています.
そこで,VRC Avatar Descriptorがある親オブジェクトとArmatureにFilanIK由来のコンポーネントを追加しないことで,VRCとの衝突を防ぎます.IKModel
IKで動かすためのモデルです.
IKModelの階層に,2つの関節に相当する空オブジェクト(Bone1, Bone2)と実際に動かしたいオブジェクト(Cube)を親子関係で追加します.
Bone1, Bone2を可視化してみると以下のようになります.
このようにBone1, Bone2,CubeのPositionをそれぞれ重ならないように,「く」の字に配置しておきます.
今回はBone2のxを1だけずらし,Cubeは好きな位置に設定しました.IKTargets
次にターゲットとなるゲームオブジェクトを追加します.
IKTargetsの階層に2つの空オブジェクトを用意し,それぞれTarget2Hand, Target2Headとします.右手と頭の位置に対応させます.ターゲットをアバターに追従させるためには,Armatureのある階層にターゲットを置く必要は必ずしもありません.
下図のようにParentConstraintを用意して,
1. Is Activeにチェックを入れる
2. Constraint Settingsで対応する関節を指定する(今回はhand.RとHead)
すると,アバター直下にあるターゲットが右手や頭に追従するようになります.
IKControllers
今回の主役となるIKコンポーネントを追加します.
IKControllersの階層に2つの空オブジェクトを用意し,それぞれController2Hand, Controller2Headとします.今回はLimb IKを使用します.
Limb IKとは腕や足などの3つの関節をもつオブジェクトを制御するためのソルバーです.
今回のIKModelは2つの関節+cubeなのでこのコンポーネントが適しています.下図のようにController2Hand/Controller2HeadにそれぞれLimbIKコンポネントを追加し,
- Fix Transformsにチェック
- Bone1, Bone2, Bone3にIKModelのBone1, Bone2, Cubeをそれぞれ追加
- Targetに対応するTarget2Hand/Target2Headを追加
- Position Weightを1に
- AvatarIKGoalは今回はLeftFootに
と設定します.
Position Weightを1にするとIKModelのCubeをTargetの位置にぴったり合うように計算されます.
Rotation Weightを1にするとTargetの回転姿勢にも合うように計算されます.これでCubeを追従させる準備がひとまず整ったので,動作確認してみます.
Controller2Handに追加したLimbIKコンポーネントにチェックを入れて,Controller2Headの方はチェックを外しておきます.プロジェクトを実行してみて,アバターの肩関節を回転させて
Cubeが右手に追従していれば成功です.(動画ではTargetを可視化させています.)
IKControllerの入れ替え
Cubeは基本的に右手に追従させて,切り替えで頭に追従させるとします.
今回はそのためにLimbIKを2つ用意しましたが,どちらのコンポーネントもONにしておくと両方についてのIkが解かれて,Cube位置が決定します.
この時Position Weightを1にしているので,必ずどちらかのTargetの方に移動しています.
このままVRCに持ち込むと,どちらのIKが解かれるかが未定なので,挙動が不安定になります.(特に多くのIKコンポーネントを連動させる時に注意)そこでIKControllersにIK Execution Orderを追加し,
- IK Componentsのsizeを1にし,
- Element 0 にデフォルトとするController2Handを追加
- IK Execution Order自体にチェックをいれる
- Controller2Hand内のLimb IKのチェックを外しておく
と設定します.
IK Execution OrderはIKコンポーネントの実行順序を明示するもので,ここに追加されたIKはチェックが外れていても実行されます.
この状態でプロジェクトを実行してみると,デフォルトではController2Handが実行されており,Controller2HeadをON/OFFすることでCubeの位置を入れ替わることが確認できます.
最後にController2HeadのLimbIKをON/OFFするアニメーションを作ればVRC内でも再現できます.
bool変数とON/OFFアニメーションで作れるので非常にシンプルです.最後に
Unity上では上手く動作しても,VRCの仕様によって挙動がおかしいことがあります.
必ずアバターをアップロードしてVRC内で動作確認しましょう.
- 投稿日:2021-02-27T22:23:54+09:00
FinalIK for VRChat [アイテム持ち替え]
はじめに
VRChat(以下VRC)での使用を想定したFinalIKの設定方法についてまとめました.
今回はFilanIKの基本的な使い方として,
- LimbIKを使ってアイテムをアバターの手に追従させる
- 二つのLimbIKを使ってアイテムの追従先を切り替える(手<->頭)
について説明したいと思います.
概要
FinalIKとはUnityのアセットで,IKを使ってアバターやオブジェクトの動きを制御できるツールです.
また,VRChat内に持ち込むことができる外部アセットの一つです.
FinalIKを使うと多関節アームを柔軟に操作でき,例として動画のようなアバター改変をすることができます.
#FinalIK for #VRChat
— Chootana (@choo_zap) February 23, 2021
現時点で理解できてることを全て詰め込んだスチームパンク・メカこんちゃんです
1フルトラ
2両足のフルトラを止めて空中浮遊
3サブアームの先端が足として動くモード.歩行アニメーションも追従します
4サブアームの床固定モード.体が上下移動しても柔軟に支えてくれます pic.twitter.com/RzNv0lsmev対象読者
VRCでFinalIKを使いたい方とします.
また今回はAvatar3.0でアバターをアップロードしたことある方を対象とします.
Avatar3.0についてはVRChat 続・初心者向けUnity備忘録で一通りの設定方法を学ぶことをおすすめします.環境
注意:
VRCのバージョンによってVRC内でのFinalIKの挙動が変更される可能性があります.動作確認した環境は以下の通りです.
- Unity: 2018.4.20f1
- VRC: 2021.1.2
- FinalIK
- $90. セールで安くなるタイミングでの購入がおススメです!
IK(Inverse Kinematics)について
アバター改変時に右腕の動きをチェックしたいとすると,Armatureの肩関節(Shoulder)を回転させて,次に肘関節(Elbow)を回転させて...とするかと思います.
このように各関節を一つずつ動かすことで全体の動きを作っていく仕組みをFK(Forward Kinematics)と呼びます.
肩関節から肘関節の順に回転させることで手の位置を求めるイメージです.反対に,アバターの手の位置や姿勢が特定の位置・姿勢に合うように,他の関節の回転を計算する仕組みをIK(Inverse Kinematics)と呼びます.
手の位置が先に与えられて,肘関節から肩関節の逆順に必要な回転を求めるイメージです.VRCでもHMDと2つのコントローラ(+トラッカー)の位置・姿勢からIKを解くことで体全体の動きを制御しているので,VRCをプレイする方には身近な存在だと言えます.
実際にIK制御するために必要なものは,以下の3つになります.
複数の関節を持つモデル(実際に動かしたいキャラクターや多関節アームなど)
ターゲット(モデルの部位を合わせたい位置・姿勢)
IKコントローラー(VRIK, FBBIK, FABRIK, CCDIKなど)
FinalIKでは様々なIKソルバーがコンポーネントとして用意されています.
全体図
今回はIK制御を用いてオブジェクト(cube)を腕や頭に追従させたいと思います.
全体の階層構造は以下の通りです.
- Avatar(VRC Avatar Descriptorある階層)
- Armature(HMDやコントローラで操作するスケルトン)
- Body(スキニングされたモデル)
- IKTargets
- IKControllers
- IKModel
用意したアバター直下に3つの空オブジェクトを追加して,それぞれ分かりやすいようにIKTargets/IKControllers/IKModelとします.
それぞれのオブジェクトについて実際に説明します.Point
VRCのバージョン変更によってFilanIKの挙動が変更されることがあります.
またVRCでのIKとFinalIKが衝突し,Unityでは上手く動作していてもVRCに持ち込むとおかしくなるケースを確認しています.
そこで,VRC Avatar Descriptorがある親オブジェクトとArmatureにFilanIK由来のコンポーネントを追加しないことで,VRCとの衝突を防ぎます.IKModel
IKで動かすためのモデルです.
IKModelの階層に,2つの関節に相当する空オブジェクト(Bone1, Bone2)と実際に動かしたいオブジェクト(Cube)を親子関係で追加します.
Bone1, Bone2を可視化してみると以下のようになります.
このようにBone1, Bone2,CubeのPositionをそれぞれ重ならないように,「く」の字に配置しておきます.
今回はBone2のxを1だけずらし,Cubeは好きな位置に設定しました.IKTargets
次にターゲットとなるゲームオブジェクトを追加します.
IKTargetsの階層に2つの空オブジェクトを用意し,それぞれTarget2Hand, Target2Headとします.右手と頭の位置に対応させます.ターゲットをアバターに追従させるためには,Armatureのある階層にターゲットを置く必要は必ずしもありません.
下図のようにParentConstraintを用意して,
1. Is Activeにチェックを入れる
2. Constraint Settingsで対応する関節を指定する(今回はhand.RとHead)
すると,アバター直下にあるターゲットが右手や頭に追従するようになります.
IKControllers
今回の主役となるIKコンポーネントを追加します.
IKControllersの階層に2つの空オブジェクトを用意し,それぞれController2Hand, Controller2Headとします.今回はLimb IKを使用します.
Limb IKとは腕や足などの,3つの関節を持つモデルを制御するためのソルバーです.
今回のIKModelは2つの関節+cubeなのでこのコンポーネントが適しています.下図のようにController2Hand/Controller2HeadにそれぞれLimbIKコンポネントを追加し,
- Fix Transformsにチェック
- Bone1, Bone2, Bone3にIKModelのBone1, Bone2, Cubeをそれぞれ追加
- Targetに対応するTarget2Hand/Target2Headを追加
- Position Weightを1に
- AvatarIKGoalは今回はLeftFootに
と設定します.
Position Weightを1にするとIKModelのCubeをTargetの位置にぴったり合うように計算されます.
Rotation Weightを1にするとTargetの回転姿勢にも合うように計算されます.これでCubeを追従させる準備がひとまず整ったので,動作確認してみます.
Controller2Handに追加したLimbIKコンポーネントにチェックを入れて,Controller2Headの方はチェックを外しておきます.プロジェクトを実行後,アバターの肩関節を回転させて,
Cubeが右手に追従していれば成功です.(動画ではTargetを可視化させています.)
IKControllerの入れ替え
Cubeは基本的に右手に追従させて,切り替えで頭に追従させるとします.
今回はそのためにLimbIKを2つ用意しましたが,どちらのコンポーネントもONにしておくと両方についてのIkが解かれて,Cube位置が決定します.
この時Position Weightを1にしているので,必ずどちらかのTargetの方に移動しています.
このままVRCに持ち込むと,どちらのIKが解かれるかが未定なので,挙動が不安定になります.(特に多くのIKコンポーネントを連動させる時に注意)そこでIKControllersにIK Execution Orderを追加し,
- IK Componentsのsizeを1にし,
- Element 0 にデフォルトとするController2Handを追加
- IK Execution Order自体にチェックをいれる
- Controller2Hand内のLimb IKのチェックを外しておく
と設定します.
IK Execution OrderはIKコンポーネントの実行順序を明示するもので,ここに追加されたIKはチェックが外れていても実行されます.
この状態でプロジェクトを実行してみると,デフォルトではController2Handが実行されており,Controller2HeadをON/OFFすることでCubeの位置を入れ替わります.
最後にController2HeadのLimbIKをON/OFFするアニメーションを作ればVRC内でも再現できます.
bool変数とON/OFFアニメーションで作れるので非常にシンプルです.最後に
Unity上では上手く動作しても,VRCの仕様によって挙動がおかしいことがあります.
必ずアバターをアップロードしてVRC内で動作確認しましょう.
- 投稿日:2021-02-27T21:09:17+09:00
Unityで行動分析 ~その4:反応率分化強化スケジュール編~
1. はじめに
準備編 で作成した Operandum1 の Script を編集して、時間スケジュールを作成します。基本的なことは準備編で一通り解説しているので、本記事では Operandum1 の Script の解説のみとなります。また、今回も「オペランダムへの反応」→「得点上昇」と「強化オペランダムへの反応」→「得点上昇」の2つの場合を考慮して解説したいと思います。
2. 反応率分化強化スケジュールとは
反応率分化強化スケジュールは、反応形成において反応を変容する手続きです。本記事では反応間時間 ( IRT ) を分化強化の対象とする反応型の分化強化スケジュールとして、低反応率分化強化スケジュール ( Differential reinforcement of low rate; DRL )と高反応率分化強化スケジュール ( Differential reinforcement of high rate; DRH )の2つを解説します。
2.1. DRLとは
DRLとは、「IRTがある値を超えた後の最初の反応に強化子が随伴する(坂上・井上, 2018, pp175)」強化スケジュールです。もしその値以内で反応した場合は、再びその時点からIRTが計測されます。無反応での一定時間の経過が強化の必要条件となっています。
Unityで作成する場合は、弁別刺激点灯時に限り反応はいつでも受けつけるけれど、スケジュール値 ( x sec ) を超過してから反応しなければ強化子を得られない ( あるいは Ramp が点灯しない ) ようにすれば良いです。また、スケジュール値 ( x sec ) 経過中に反応してしまうと時間を0にリセットするよう設定します。2.2. DRHとは
DRHとは、DRLの逆にあたる強化スケジュールで(坂上・井上, 2018)、スケジュール値よりも短い時間での反応に強化子が随伴する強化スケジュールです(Ono & Iwabuchi, 1997)。もしスケジュール値以上で反応した場合は、再びその時点からIRTが計測されます。
Unityで作成する場合は、弁別刺激点灯時に限り反応はいつでも受けつけるけれど、反応がスケジュール値 ( x sec ) 未満でなければ強化子を得られない ( あるいは Ramp が点灯しない ) ようにすれば良いです。また、スケジュール値 ( x sec ) 以降に反応してしまうと時間を0にリセットするよう設定します。3. DRL
3.1. 「オペランダムへの反応」→「得点上昇」
Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の Operandum1 の Script にはなかったコードです。
Operandum1using 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 float time; public float DRL; public AudioClip Operandum1SE; void Start() { audioSource = GetComponent<AudioSource>(); } // New void Update() { if (Sd1_on.activeSelf) { time += Time.deltaTime; if (Input.GetKeyDown(KeyCode.F)) { audioSource.PlayOneShot(Operandum1SE); if (time > DRL) { audioSource.PlayOneShot(PointSE); CountText.text = "Point : " + Point.ToString(); Point += 1; time = 0; } else { time = 0; } } } if (Sd1_off.activeSelf) { time = 0; } } }このScriptの流れをざっくり書くと、変数の宣言 → Start() → Update()となります。解説は「// New」と書かれている箇所のみ行います。
変数の宣言
- float time; ... float型の変数 time を宣言
- public float DRL; ... public な float型の変数 DRL を宣言
→ Editor上ではDRLのスケジュール値を入れてください- public AudioClip Operandum1SE; ... public な AudioClip として Operandum1SE を宣言
→ Editor上では Operandum1 に反応したときに鳴るSEを入れてくださいUpdate()
- 「if (Sd1_on.activeSelf)」の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
- 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
- 「if (Input.GetKeyDown(KeyCode.F))」の処理 ... キーボードのFキーが押されたときの処理
- 効果音( Operandum1SE )が鳴る
- 「if (time > DRL)」の処理 ... timeがDRLのスケジュール値を超過したときの処理
- 効果音( PointSE )が鳴る
- 得点が1点上昇( Point += 1; )
- time を0にリセット
→ 得点が上昇すると時間がリセットされ、もう一度時間を計測しはじめる
→ 弁別刺激が点灯している間、DRLが走り続ける- 「else」の処理 ... timeがDRLのスケジュール値を超過したとき以外の処理
- time を0にリセット
→ オペランダムに反応すると時間がリセットされ、もう一度時間を計測しはじめる3.2. 「強化オペランダムへの反応」→「得点上昇」
Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の Operandum1 の Script にはなかったコードです。
Operandum1using 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 float time; public float DRL; void ResetTime() { time = 0; } void Start() { audioSource = GetComponent<AudioSource>(); Ramp = GameObject.Find("Ramp"); Ramp_Script = Ramp.GetComponent<Ramp_Script>(); } void Update() { // New_1 if (Sd1_on.activeSelf) { time += Time.deltaTime; if (Input.GetKeyDown(KeyCode.F)) { audioSource.PlayOneShot(Operandum1SE); // New_2 if (time > DRL) { Ramp_off.SetActive(false); Ramp_on.SetActive(true); Invoke("ResetTime", Ramp_Script.ReinforceableTime); } else { time = 0; } } } if (Sd1_off.activeSelf) { time = 0; Ramp_off.SetActive(true); Ramp_on.SetActive(false); } } }このScriptの流れをざっくり書くと、変数の宣言 → Start() → Update()となります。解説は「// New」と書かれている箇所のみ行います。
変数の宣言
- float time; ... float型の変数 time を宣言
- public float DRL; ... public な float型の変数 DRL を宣言
→ Editor上ではDRLのスケジュール値を入れてくださいUpdate()
- New_1
- 「if (Sd1_on.activeSelf)」の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
- 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
- 「if (Input.GetKeyDown(KeyCode.F))」の処理 ... キーボードのFキーが押されたときの処理
- 効果音( Operandum1SE )が鳴る
- New_2
- 「if (time > DRL)」の処理 ... timeがDRLのスケジュール値を超過したときの処理
- Ramp_off を非アクティブ化、Ramp_on をアクティブ化
→ 疑似的に強化可能ランプの点灯を表現- Invoke("ResetTime", Ramp_Script.ReinforceableTime)
→ 強化可能ランプ点灯時から強化可能時間が経過すると、time を0にリセット- 「else」の処理 ... timeがDRLのスケジュール値を超過したとき以外の処理
- time を0にリセット
→ オペランダムに反応すると時間がリセットされ、もう一度時間を計測しはじめる4. DRH
4.1. 「オペランダムへの反応」→「得点上昇」
Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の Operandum1 の Script にはなかったコードです。
Operandum1using 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 float time; public float DRH; public AudioClip Operandum1SE; void Start() { audioSource = GetComponent<AudioSource>(); } // New void Update() { if (Sd1_on.activeSelf) { time += Time.deltaTime; if (Input.GetKeyDown(KeyCode.F)) { audioSource.PlayOneShot(Operandum1SE); if (time < DRH) { audioSource.PlayOneShot(PointSE); CountText.text = "Point : " + Point.ToString(); Point += 1; time = 0; } else { time = 0; } } } if (Sd1_off.activeSelf) { time = 0; } } }このScriptの流れをざっくり書くと、変数の宣言 → Start() → Update()となります。解説は「// New」と書かれている箇所のみ行います。
変数の宣言
- float time; ... float型の変数 time を宣言
- public float DRH; ... public な float型の変数 DRH を宣言
→ Editor上ではDRHのスケジュール値を入れてください- public AudioClip Operandum1SE; ... public な AudioClip として Operandum1SE を宣言
→ Editor上では Operandum1 に反応したときに鳴るSEを入れてくださいUpdate()
- 「if (Sd1_on.activeSelf)」の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
- 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
- 「if (Input.GetKeyDown(KeyCode.F))」の処理 ... キーボードのFキーが押されたときの処理
- 効果音( Operandum1SE )が鳴る
- 「if (time < DRH)」の処理 ... timeがDRHのスケジュール値未満だったときの処理
- 効果音( PointSE )が鳴る
- 得点が1点上昇( Point += 1; )
- time を0にリセット
→ 得点が上昇すると時間がリセットされ、もう一度時間を計測しはじめる
→ 弁別刺激が点灯している間、DRHが走り続ける- 「else」の処理 ... timeがDRHのスケジュール値未満だったとき以外の処理
- time を0にリセット
→ オペランダムに反応すると時間がリセットされ、もう一度時間を計測しはじめる4.2. 「強化オペランダムへの反応」→「得点上昇」
Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の Operandum1 の Script にはなかったコードです。
Operandum1using 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 float time; public float DRH; void ResetTime() { time = 0; } void Start() { audioSource = GetComponent<AudioSource>(); Ramp = GameObject.Find("Ramp"); Ramp_Script = Ramp.GetComponent<Ramp_Script>(); } void Update() { // New_1 if (Sd1_on.activeSelf) { time += Time.deltaTime; if (Input.GetKeyDown(KeyCode.F)) { audioSource.PlayOneShot(Operandum1SE); // New_2 if (time < DRH) { Ramp_off.SetActive(false); Ramp_on.SetActive(true); Invoke("ResetTime", Ramp_Script.ReinforceableTime); } else { time = 0; } } } if (Sd1_off.activeSelf) { time = 0; Ramp_off.SetActive(true); Ramp_on.SetActive(false); } } }このScriptの流れをざっくり書くと、変数の宣言 → Start() → Update()となります。解説は「// New」と書かれている箇所のみ行います。
変数の宣言
- float time; ... float型の変数 time を宣言
- public float DRH; ... public な float型の変数 DRH を宣言
→ Editor上ではDRHのスケジュール値を入れてくださいUpdate()
- New_1
- 「if (Sd1_on.activeSelf)」の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
- 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
- 「if (Input.GetKeyDown(KeyCode.F))」の処理 ... キーボードのFキーが押されたときの処理
- 効果音( Operandum1SE )が鳴る
- New_2
- 「if (time < DRH)」の処理 ... timeがDRHのスケジュール値未満だったときの処理
- Ramp_off を非アクティブ化、Ramp_on をアクティブ化
→ 疑似的に強化可能ランプの点灯を表現- Invoke("ResetTime", Ramp_Script.ReinforceableTime)
→ 強化可能ランプ点灯時から強化可能時間が経過すると、time を0にリセット- 「else」の処理 ... timeがDRHのスケジュール値未満だったとき以外の処理
- time を0にリセット
→ オペランダムに反応すると時間がリセットされ、もう一度時間を計測しはじめる4.3. x sec 間に n 回
「2.2. DRHとは」では、DRHはスケジュール値よりも短い時間での反応に強化子が随伴する強化スケジュールと書きましたが、x sec 間に n 回反応すると強化子が随伴する場合もあるようです(e.g., 小野, 1994)。
Unityで作成する場合は、弁別刺激点灯時に限り反応はいつでも受けつけるけれど、スケジュール値 ( x sec ) 未満かつ n 回反応しなければ強化子を得られない ( あるいは Ramp が点灯しない ) ようにすれば良いです。また、スケジュール値 ( x sec ) 以降に反応したり n 回に到達していない場合は、時間を0にリセットするよう設定します。4.3.1. 「オペランダムへの反応」→「得点上昇」
Operandum1using 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 float time; int Counter = 0; public float DRHTime; public float DRHTimes; public AudioClip Operandum1SE; void Start() { audioSource = GetComponent<AudioSource>(); } // New void Update() { if (Sd1_on.activeSelf) { time += Time.deltaTime; if (Input.GetKeyDown(KeyCode.F)) { audioSource.PlayOneShot(Operandum1SE); if (time < DRHTime) { if (Counter == DRHTimes) { audioSource.PlayOneShot(PointSE); CountText.text = "Point : " + Point.ToString(); Point += 1; time = 0; Counter = 0; } } else { time = 0; Counter = 0; } } } if (Sd1_off.activeSelf) { time = 0; Counter = 0; } } }4.3.2. 「強化オペランダムへの反応」→「得点上昇」
Operandum1using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; 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 float time; public float DRHTime; public float DRHTimes; int Counter = 0; void ResetTime() { time = 0; 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) { time += Time.deltaTime; Debug.Log(time); if (Input.GetKeyDown(KeyCode.X)) { audioSource.PlayOneShot(Operandum1SE); Counter += 1; // New_2 if (time < DRHTime) { if (Counter == DRHTimes) { Ramp_off.SetActive(false); Ramp_on.SetActive(true); Invoke("ResetTime", Ramp_Script.ReinforceableTime); } } else { time = 0; Counter = 0; } } } if (Sd1_off.activeSelf) { time = 0; Counter = 0; Ramp_off.SetActive(true); Ramp_on.SetActive(false); } } }5. 最後に
「反応率分化強化スケジュール」の中の、「低反応率分化強化(Differential Reinforcement of Low rates)」と「低反応率分化強化(Differential Reinforcement of High rates)」を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/引用文献
小野 浩一 (1994). 迷信行動と言語. 駒澤社会学研究, 26, 59-84.
Ono K, & Iwabuchi K. (1997). Effects of histories of differential reinforcement of response rate on variable-interval responding. Journal of the Experimental Analysis of Behavior. 67(3), 311–322.
坂上 貴之・井上 雅彦 (2018). 行動分析学──行動の科学的理解をめざして── 有斐閣
- 投稿日:2021-02-27T18:54:52+09:00
テキストの長さに合わせてラベルの長さを変える方法
これはテスト用にTABキーを押すと変更するようにしていますが、使いたいタイミングでScript内の関数を呼び出す感じで使います。
やり方
1.Canvasを追加
2.Panelを追加
Canvasを左クリックで選択>>右クリック>>UI>>Panel
3.Panelを設定
4.TEXTを追加
Panelを左クリックで選択>>右クリック>>UI>>テキスト
5.TEXTを設定
整列の設定だけ同じであれば問題ないです。
そのほかの設定はTEXTがぼやけないようにするための設定です。
参考:https://qiita.com/tyoberiba225/items/3a44d6b2456b61166276
6.Panel に ContentsSizefitter を追加
Panel>>Inspector>>AddComponent>>Layout>>ContentsSizefitter
7.Panel に HorizontalLayoutGroup を追加
Panel>>AddComponent>>Layout>>HorizontalLayoutGroup
8.Panelの ContentsSizefitter と HorizontalLayoutGroupを設定
9.スクリプトを作成
Projectで右クリック>>Create>>Script
名前はFitLabelToTextにする
10.下記のスクリプトをコピペ
FitLabelToText.csusing System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class FitLabelToText : MonoBehaviour { [SerializeField] LayoutElement labelLe; [SerializeField] Text text; [SerializeField] float padding; RectTransform textRect; private void Start() { textRect = text.rectTransform; OnTextUpdate(); } private void Update() { if (Input.GetKeyDown(KeyCode.Tab)) { UpdateLabel(); } } //テキストの長さが変わったら呼び出す void UpdateLabel() { labelLe.preferredWidth = text.preferredWidth / (1 / textRect.localScale.x) + padding; } }11.FitLabelToText スクリプトを Panel に追加して設定
これで終わりです。
再生ボタンを押した後にテキストを変えてTABキーを押して確かめてみてください。
アイテムの個数をスロットの右下に表示したいときなんかに使ってます。
もっといい方法があれば教えてください。
- 投稿日:2021-02-27T18:05:43+09:00
Unity の IMGUI を High DPI 対応
Unity の IMGUI は
GUI.matrix
にスケール行列を掛けることでサイズを変えられる。
GUIUtility.ScaleAroundPivot
はその計算を行ってくれるヘルパークラスなので、それを利用する。Windows の DPI は標準で 96 dpi なので、それと
Screen.dpi
との比でスケールさせる。
Unity Editor には Low Resolution Aspect Ratio という DPI を下げる機能(チェックボックス)があるが、Screen.dpi はそれに対応していないので、GameView.lowResolutionForAspectRatios
の getter プロパティーをチェックすることで対応する。
GameView
クラスを取得するには、EditorWindow.GetWindow
にUnityEditor
Assembly 内のUnityEditor.GameView
型を渡すことになる。GameView
型は参照できないので、PropertyInfo によるリフレクションによってlowResolutionForAspectRatios
にアクセスする。最終的には、下記のようなスクリプトになる。
ImguiHighdpiScaler.csvar scale = Screen.dpi / 96f; #if UNITY_EDITOR var typeGameView = System.Type.GetType("UnityEditor.GameView,UnityEditor"); var gameView = EditorWindow.GetWindow(typeGameView); var propLowRes = typeGameView.GetProperty("lowResolutionForAspectRatios"); var isLowRes = (bool)propLowRes.GetValue(gameView); if (isLowRes) scale *= 0.5f; #endif GUIUtility.ScaleAroundPivot(new Vector2(scale, scale), Vector2.zero);
- 投稿日:2021-02-27T10:31:52+09:00
Unityで行動分析 ~その3:時間スケジュール編~
1. はじめに
準備編 で作成した Operandum1 の Script を編集して、時間スケジュールを作成します。基本的なことは準備編で一通り解説しているので、本記事では Operandum1 の Script の解説のみとなります。また、今回も「オペランダムへの反応」→「得点上昇」と「強化オペランダムへの反応」→「得点上昇」の2つの場合を考慮して解説したいと思います。
2. 時間スケジュールとは
時間スケジュール(Time schedule)とは、1つ前の強化子の提示から一定時間経過後に強化子が随伴する強化スケジュールです(坂上・井上, 2018)。一定時間 ( スケジュール値 ) が固定である場合は固定時間スケジュール ( Fixed Time schedule; FT ) 、平均するとスケジュール値になる場合は変動時間スケジュール ( Variable Time schedule; VT ) と呼びます。
Unityで作成する場合は、スケジュール値 ( x sec ) に到達すると強化子が提示される ( あるいは Ramp が点灯する ) ようにすれば良いです。時間スケジュールのイメージ図を下に示します。下図の左は「オペランダムへの反応」→「得点上昇」を、下図の右は「強化オペランダムへの反応」→「得点上昇」を示しています。FTであれば x sec が常に一定となり、VTであれば x sec が毎回変動します。
3. FT
3.1. 「オペランダムへの反応」→「得点上昇」
Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の Operandum1 の Script にはなかったコードです。
Operandum1using 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 float time; public float FTTime; public AudioClip Operandum1SE; void Start() { audioSource = GetComponent<AudioSource>(); } // New void Update() { if (Sd1_on.activeSelf) { time += Time.deltaTime; if (Input.GetKeyDown(KeyCode.F)) { audioSource.PlayOneShot(Operandum1SE); } if (time >= FTTime) { audioSource.PlayOneShot(PointSE); CountText.text = "Point : " + Point.ToString(); Point += 1; time = 0; } } if (Sd1_off.activeSelf) { time = 0; } } }このScriptの流れをざっくり書くと、変数の宣言 → Start() → Update()となります。解説は「// New」と書かれている箇所のみ行います。
変数の宣言
- float time; ... float型の変数 time を宣言
- public float FTTime; ... public な float型の変数 FTTime を宣言
→ Editor上ではFTのスケジュール値を入れてください- public AudioClip Operandum1SE; ... public な AudioClip として Operandum1SE を宣言
→ Editor上では Operandum1 に反応したときに鳴るSEを入れてくださいUpdate()
- 「if (Sd1_on.activeSelf)」の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
- 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
- 「if (Input.GetKeyDown(KeyCode.F))」の処理 ... キーボードのFキーが押されたときの処理
- 効果音( Operandum1SE )が鳴る
- 「if (time >= FTTime)」の処理 ... timeがFTのスケジュール値に到達したときの処理 * 効果音( PointSE )が鳴る * 得点が1点上昇( Point += 1; ) * time を0にリセット
→ 得点が上昇すると時間がリセットされ、もう一度時間を計測しはじめる
→ 弁別刺激が点灯している間、FTが走り続ける3.2. 「強化オペランダムへの反応」→「得点上昇」
Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の Operandum1 の Script にはなかったコードです。
Operandum1using 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 float time; public float FTTime; void ResetTime() { time = 0; CancelInvoke(); } void Start() { audioSource = GetComponent<AudioSource>(); Ramp = GameObject.Find("Ramp"); Ramp_Script = Ramp.GetComponent<Ramp_Script>(); } void Update() { // New_1 if (Sd1_on.activeSelf) { time += Time.deltaTime; if (Input.GetKeyDown(KeyCode.F)) { audioSource.PlayOneShot(Operandum1SE); } // New_2 if (time >= FTTime) { Ramp_off.SetActive(false); Ramp_on.SetActive(true); Invoke("ResetTime", Ramp_Script.ReinforceableTime); } } if (Sd1_off.activeSelf) { time = 0; Ramp_off.SetActive(true); Ramp_on.SetActive(false); } } }このScriptの流れをざっくり書くと、変数の宣言 → Start() → Update()となります。解説は「// New」と書かれている箇所のみ行います。ResetTime()に、「CancelInvoke();」が追加されているので注意してください。CancelInvoke()については、 こちら を参照してください。
変数の宣言
- float time; ... float型の変数 time を宣言
- public float FTTime; ... public な float型の変数 FTTime を宣言
→ Editor上ではFTのスケジュール値を入れてくださいUpdate()
- New_1
- 「if (Sd1_on.activeSelf)」の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
- 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
- 「if (Input.GetKeyDown(KeyCode.F))」の処理 ... キーボードのFキーが押されたときの処理
- 効果音( Operandum1SE )が鳴る
- New_2
- 「if (time >= FTTime)」の処理 ... timeがFTのスケジュール値に到達したときの処理
- Ramp_off を非アクティブ化、Ramp_on をアクティブ化
→ 疑似的に強化可能ランプの点灯を表現- Invoke("ResetTime", Ramp_Script.ReinforceableTime)
→ 強化可能ランプ点灯時から強化可能時間が経過すると、time を0にリセット4. VT
4.1. Pythonで x sec のリストを作成する
VTでは、FTとは異なり、x sec が一定ではなく変動します。この変動した値をUnity上で作成しても良いのですが、先にPythonで x sec のリストを作成してCsv形式で出力しておきます。その後、作成したCsv形式の x sec のリストをUnityで読み込みます。
4.1.1. x sec のリストを作成する関数
x sec のリストを作成する関数は下記のとおりです。環境について、Pythonのバージョンは「Python 3.7.1」で、Jupyter Notebookを使用しています。
Pythonimport 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ループは行われない4.1.2. x sec のリストをCsvファイルに出力
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というファイルを作成して、その中に入れます。
4.2. 「オペランダムへの反応」→「得点上昇」
Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の Operandum1 の Script にはなかったコードです。
Operandum1using 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; float time; 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) { time += Time.deltaTime; if (Input.GetKeyDown(KeyCode.F)) { audioSource.PlayOneShot(Operandum1SE); } // New_4 if (first) { if (time >= int.Parse(CsvVariable[CsvCounter])) { audioSource.PlayOneShot(PointSE); CountText.text = "Point : " + Point.ToString(); Point += 1; CsvCounter += 1; time = 0; } } } if (Sd1_off.activeSelf) { time = 0; } } }このScriptの流れをざっくり書くと、変数の宣言 → Start() → Update()となります。5行目に「using System.IO;」が追加されているので注意してください。解説は「// New」と書かれている箇所のみ行います。
変数の宣言
- bool first = true; ... bool型の変数 first が true であることを宣言
- float time; ... float型の変数 time を宣言
- 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ループの書き方については こちら を参照してください
Update()
- New_3
- 「if (Sd1_on.activeSelf)」の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
- 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
- 「if (Input.GetKeyDown(KeyCode.F))」の処理 ... キーボードのFキーが押されたときの処理
- 効果音( Operandum1SE )が鳴る
- New_4
- 「if (first)」の処理 ... 取得するList内の要素の順番についての処理
- 「if (time >= int.Parse(CsvVariable[CsvCounter]))」の処理 ... timeがVTのスケジュール値に到達したときの処理
- 効果音( PointSE )が鳴る
- 得点が1点上昇( Point += 1; )
- CsvCounterが1つ増加
→ 取得するList内の要素の順番を1つずらす- time を0にリセット
→ 得点が上昇すると時間がリセットされ、もう一度時間を計測しはじめる
→ 弁別刺激が点灯している間、VTが走り続ける4.3. 「強化オペランダムへの反応」→「得点上昇」
Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の Operandum1 の Script にはなかったコードです。
Operandum1using 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; float time; int i; int CsvCounter = 0; private List<string> CsvVariable = new List<string>(); void ResetTime() { time = 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) { time += Time.deltaTime; if (Input.GetKeyDown(KeyCode.F)) { audioSource.PlayOneShot(Operandum1SE); } // New_4 if (first) { if (time >= int.Parse(CsvVariable[CsvCounter])) { Ramp_off.SetActive(false); Ramp_on.SetActive(true); CsvCounter += 1; first = false; Invoke("ResetTime", Ramp_Script.ReinforceableTime); } } if (Sd1_off.activeSelf) { time = 0; Ramp_off.SetActive(true); Ramp_on.SetActive(false); } } } }このScriptの流れをざっくり書くと、変数の宣言 → Start() → Update() → ResetTime()となります。5行目に「using System.IO;」が追加されているので注意してください。また、ResetTime()では「time = 0;」だけではなく「first = true;」や「CancelInvoke();」も書かれてあるので注意してください。CancelInvoke()については、 こちら を参照してください。
解説は「// New」と書かれている箇所のみ行います。変数の宣言
- bool first = true; ... bool型の変数 first が true であることを宣言
- float time; ... float型の変数 time を宣言
- 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ループの書き方については こちら を参照してください
Update()
- New_3
- 「if (Sd1_on.activeSelf)」の処理 ... Sd1_on がアクティブな時(弁別刺激点灯時)の処理
- 弁別刺激が点灯したら制限時間をカウントアップ形式で作成
- 「if (Input.GetKeyDown(KeyCode.F))」の処理 ... キーボードのFキーが押されたときの処理
- 効果音( Operandum1SE )が鳴る
- New_4
- 「if (first)」の処理 ... 取得するList内の要素の順番についての処理
- 「if (time >= int.Parse(CsvVariable[CsvCounter]))」の処理 ... timeがVTのスケジュール値に到達したときの処理
- Ramp_off を非アクティブ化、Ramp_on をアクティブ化
→ 疑似的に強化可能ランプの点灯を表現- CsvCounterが1つ増加
→ 取得するList内の要素の順番を1つずらす- first を false にする → 取得するList内の要素の順番が2つ以上ずれないようにする
- Invoke("ResetTime", Ramp_Script.ReinforceableTime)
→ 強化可能ランプ点灯時から強化可能時間が経過すると、time を0にリセットして、first を true に戻す5. 最後に
「時間スケジュール(time schedule)」の中の、「固定時間スケジュール(Fixed Time schedule)」と「変動時間スケジュール(Variable Time 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/・【Unity】Invokeの使い方!実行タイミングを自在に操ろう
https://www.sejuku.net/blog/83762引用文献
坂上 貴之・井上 雅彦 (2018). 行動分析学──行動の科学的理解をめざして── 有斐閣
- 投稿日:2021-02-27T08:55:26+09:00
Unityで行動分析 ~その2:比率スケジュール編~
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 にはなかったコードです。
Operandum1using 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が走り続ける3.2. 「強化オペランダムへの反応」→「得点上昇」
Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の Operandum1 の Script にはなかったコードです。
Operandum1using 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増加
- New_2
- 「if (Counter == FR)」の処理 ... CounterがFRのスケジュール値に到達したときの処理
- Ramp_off を非アクティブ化、Ramp_on をアクティブ化
→ 疑似的に強化可能ランプの点灯を表現- Invoke("ResetTimes", Ramp_Script.ReinforceableTime)
→ 強化可能ランプ点灯時から強化可能時間が経過すると、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を使用しています。
Pythonimport 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ループは行われない4.1.2. *x times * のリストをCsvファイルに出力
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というファイルを作成して、その中に入れます。
4.2. 「オペランダムへの反応」→「得点上昇」
Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の Operandum1 の Script にはなかったコードです。
Operandum1using 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ループの書き方については こちら を参照してください
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つ増加
→ 取得するList内の要素の順番を1つずらす- Coutner を0にリセット
→ 得点が上昇すると回数がリセットされ、もう一度時間を計測しはじめる
→ 弁別刺激が点灯している間、VRが走り続ける4.3. 「強化オペランダムへの反応」→「得点上昇」
Script の内容は下記のとおりです。ちなみに、「// New」の下に書かれてあるコードは、準備編の Operandum1 の Script にはなかったコードです。
Operandum1using 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ループの書き方については こちら を参照してください
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 をアクティブ化
→ 疑似的に強化可能ランプの点灯を表現- CsvCounterが1つ増加
→ 取得するList内の要素の順番を1つずらす- first を false にする → 取得するList内の要素の順番が2つ以上ずれないようにする
- Invoke("ResetTimes", Ramp_Script.ReinforceableTime)
→ 強化可能ランプ点灯時から強化可能時間が経過すると、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). 行動分析学──行動の科学的理解をめざして── 有斐閣
- 投稿日:2021-02-27T01:50:35+09:00
Unityで作った占いゲームをOculus Quest2アプリとして動かす
Unityで作った占いゲームをOculus Quest2アプリとして動かす
Unity入門の定番、占いゲームをOculus Quest2で動かすまでメモとしてまとめておく。
まず、ゲームに使う素材を作る。
今回はIllustratorを使用。
針の部分は適当に三角形を作って塗りつぶせば良い。
ルーレットの円盤は同心円グリッドツール等を使えばお手軽にできます。
または、円を描いて、直線描画ツールで区切って、
ウインドウ→パスファインダー
でオブジェクトを選択し、「分割」して色を変えるといい感じです。作った素材(※Adobe Illustratorを使用して作成)
valueinfo.png(aiファイルを上げておきます。お試しで動かす等、自由に使っていただいて構いません。)
https://1drv.ms/u/s!Apotz-GvRNC9gudRPQ9mxTdrjZqHdQ?e=9lNjMq次にUnityを開きます。インストールがまだの人はインストールしましょう。
使用したバージョンは以下の通りです。ちなみに、今回はmacOSを使用していますが、バージョン等は以下の通りです。
Unityが立ち上がったら、用意した素材をインポートします。
また、初期状態だと景色がなく味気ないので、Assets Storeから景色をダウンロードして使用する。
今回はこのAssetsを使う。
まず、Assets Storeへアクセスし、ダウンロードする。
その後、パッケージマネージャーからダウンロードし、インポートする。
Window→Package Manager→Import
次に、Oculus関係のパッケージもダウンロードしておく。1
その前に、まず、以下の手順を終えておかなければImportに非常に長い時間がかかってしまうので、順番に気をつけること。Build Settingsを開いてプラットフォームをAndroidにスイッチする。
また、PlayerSettingsを開き、次の設定をしておく。
OtherSettings→GraphicsAPIsのVulkanを削除、
ColorSpace→Linearに変えておきましょう。また、MinimumAPILevelをAndroid6.0 Marshmallow(APIlevel23)に変更しなければなりません。
次に、XR Plug-in ManagementでOculusをインポートしておきましょう。
チェックを入れると自動的にインポートされますので、チェック間違いに注意しましょう。最後にOculusIntegrationをインポートします。
Asset StoreにあるOculusIntegrationをプロジェクトにインポートします。ちなみに、初回では、OculusIntegrationのインポートに長時間かかります。
インポートが完了すると ダイアログメッセージが出るので以下のように進んでください。
ここで、Unityが自動的に再起動します。
以上でOculus関係のライブラリ等のインポートは完了です。
次に、先ほどインポートした「Free HDR Sky」のゲームシーンをCopyしておきます。
ここにあるmainSceneを以下の場所におきます。
次に、ControllerModelsにあるOVRPlayerController等をそのまま利用したいので、Copyし先ほどCopyしたmainSceneに貼り付けます。
ちなみに、囲ってあるRoom等のオブジェクトは足場になるTileオブジェクト(Grabbables等)以外は必要ないので取り除いておきます。
また、Tileのテクスチャーはデフォルトだとダサいので、好みに応じて変えると良いと思います。
Selectで変更できます。
次に、序盤でインポートしていた占い盤や針等を画面に配置します。
簡単に座標系を示しておくと、
占い盤の座標系は以下の通りです。
配置には、3Dオブジェクトを追加し、画像を表面に貼り付ける。
最後にスクリプトを書いて、占い盤を制御します。
ちなみに、Oculusのコントローラーの値を取得する場合は次のように記述します。if (OVRInput.GetDown(OVRInput.RawButton.A)){ Debug.Log("Aボタンを押す(押された瞬間に一度だけTrueを返す)"); }詳細は公式サイトをみてください。
https://developer.oculus.com/documentation/unity/unity-ovrinput/ひとまず、全体像を示しておきます。
RouletteController.csusing System.Collections; using System.Collections.Generic; using UnityEngine; public class RouletteController : MonoBehaviour { float xrotSpeed = 0; float yrotSpeed = 0; float zrotSpeed = 0; void Start() { this.yrotSpeed = 10; } // Update is called once per frame void Update() { if (OVRInput.GetDown(OVRInput.RawButton.A)){ Debug.Log("Aボタンを押す(押された瞬間に一度だけTrueを返す)"); this.yrotSpeed = 10; } if (Input.GetMouseButton(0)){ this.yrotSpeed = 10; } transform.Rotate(this.xrotSpeed,this.yrotSpeed,this.zrotSpeed); this.yrotSpeed = this.yrotSpeed * 0.99f; } }ソースコードのアタッチは以下のようにドラッグ&ドロップすれば良い↓
ちなみに、プログラムはUnityの教科書を参考にしました。2
詳しくは、この教科書を読んでください。
https://www.sbcr.jp/product/4815606657/最後に動作している様子を上げておきます↓
https://youtu.be/KfKJXmgHHpQまた、適当ですが、こんな感じにオブジェクトを配置してみるのもいいかもしれません↓
https://youtu.be/A35ygDLtsZUお疲れ様でした、Unityを使うとこんなに簡単にVRアプリケーションが作れてしまうことに驚きですが、ここから、面白いものを作るのは非常に難しそうです、Unity初心者ですが、これから色々と勉強したいと思います。
間違い、問題等ございましたらご指摘いただければ幸いです。
参考
「Unity + Oculus Quest 2 開発メモ」 https://framesynthesis.jp/tech/unity/oculusquest/ ↩
「Unityの教科書 Unity 2020完全対応版 2D&3Dスマートフォンゲーム入門講座」 https://www.sbcr.jp/product/4815606657/ ↩