- 投稿日:2020-12-12T23:27:23+09:00
Unityで強化学習をやってみる 【ML-Agents】
はじめに
前回、Unity上で強化学習を行えるプラグインであるML-Agentsを導入し、サンプルのゲームを動かしました。
今回はその続きとして、すでに構築されているサンプルの環境ではなく、新たな学習環境を自分自身で作成し強化学習を行うプロセスについて説明します。
この記事では、下のようにランダムな場所に出現するターゲットを自動で認識して射撃をおこなうエージェントを作っていきます。
強化学習とML-Agents
ML-Agentsを触っていく前に、簡単に強化学習と今回使うML-Agentsについて確認しておきます。
強化学習
強化学習とは人工知能の一分野である機械学習の中でも意思決定のための学習を行う手法です。ある環境におかれたエージェント(知的活動を行う対象)が「どのように意思決定をして行動すれば目標を達成することができるのか」ということを定式化して学習を行います。現実世界では、将棋や囲碁のようなゲームや自動運転車、ロボット制御、化学物質の設計などに応用されています。
より具体的には、強化学習の目標はエージェントが置かれた環境から測定できるものを入力として行動をし、その結果得られる報酬の総和を最大化する方策(ある時点でのエージェントの振る舞い方)を学習することになります。特に、ある状態である1つの行動をしたときに受ける即時報酬ではなく、長期的な利益を最大化させることを目指すことに注意してください。
例えば、ある火災現場において自動で火災原因を見つけ消火活動を行う自律消防ロボットを考えます。この時、エージェントであるロボットは常にセンサー(熱センサー、温度センサー、タッチセンサーなど)を通じて環境を認識し、この情報を元にどのように行動するか(移動する、ホースで水をかけるなど)を決定します。このロボットを学習するためには、火を消すことができたときに大きなプラスの報酬を与え、時間が経過するごとに小さなマイナスの報酬を与えるというような報酬信号を与えてやります。この時、ロボットの各行動全てのタイミングで報酬が提供されるわけではなく、ロボットが全体の消火活動に成功したか、もしくは失敗した場合にのみ報酬が提供されることに注意してください。これが長期的な利益を最大化することを目指すということであり、強化学習の特徴でもあります。
ML-Agents Toolkit
ML-Agents (Unity Machine Learning Agents Toolkit) はUnityで強化学習、模倣学習、遺伝的アルゴリズムやその他の機械学習の学習環境を構築するためのフレームワークです。ML-Agentsを使用すると、以下の3つの要素を定義するだけで簡単にエージェントの動作を訓練することができます。
- 観測
- 訓練を行う環境においてエージェントが何の情報にアクセスすることができるか
- 行動
- エージェントが環境において取ることのできる行動
- 報酬
- エージェントの行動が目的に対してどの程度良かったか(悪かったか)
この3つの要素を定義した後、エージェントの動作を訓練していきます。訓練には、同じ環境下で何度も何度も試行錯誤を繰り返し、報酬を最大化する行動を学んでいきます。
訓練手法
ここでは、ML-Agentsが提供する訓練手法の中で主なものを取り上げます。しかし、そこまで高度な学習環境を設定しない限りこれらの手法の細部まで理解する必要はありません。
深層強化学習
- Proximal Policy Optimization (PPO)
- Soft Actor-Critic (SAC)
ML-Agentsでは1つ目のPPOがデフォルトのアルゴリズムとなっており、今回の例でもPPOを用いて学習します。この手法は他の強化学習の手法と比べても実装が非常にシンプルかつ安定して高いパフォーマンスを出すアルゴリズムとして知られており、広く標準的に用いられている手法の1つです。
詳しくは、原論文を参照していただきたいのですが、PPOは方策のパラメータを勾配法で最適化する方策勾配法 (Policy Gradient)を用いる手法の一つで、方策の更新の際、更新前後の方策の比を一定の範囲に制限(クリッピング)することで急激に方策が変化するのを防ぐというのが特徴です。数式を追う必要はないですが、後々パラメータを設定しないといけないので一応目的関数を示しておきます。
L^{C L I P}(\theta)=\hat{E}_{t}\left[\min \left(r_{t}(\theta) \hat{A}_{t}, \operatorname{clip}\left(r_{t}(\theta), 1-\varepsilon, 1+\varepsilon\right) \hat{A}_{t}\right)\right]パラメータ
- $ \theta $ : 方策パラメータ
- $ \hat{E}_{t} $ : 期待値のサンプル平均近似
- $ r_{t} $ : 更新前の方策と更新後の方策の比
- $ \hat{A}_{t} $ : 時刻 $t$ でのアドバンテージ(純粋な行動の価値)の推定量
- $ \varepsilon $ : ハイパーパラメータ(0.1または0.2)
模倣学習
模倣学習は、他人が行う行動を模倣して(真似して)学習する手法です。ML-Agentsでは、人間が操作するエージェントの行動を模倣して学習を行います。ML-Agentsでは次の2つの手法が提供されています。
- GAIL(Generative Adversarial Imitation Learning)
- Behavioral Cloning (BC)
0. ML-Agents 環境構築
ML-AgentsはUnity上で、強化学習、模倣学習、その他機械学習が行えるプラグインです。
前回もML-Agentsの導入を行いましたが、最新バージョンは導入の仕方が変わっているので改めてインストール方法を簡単に説明します。環境
- Unity2019.4
- ML-Agents Release10
- Python3.7
Unityのインストール
Unityをダウンロードし、インストールします。
ML-Agents は Unity2018.4 以降のバージョンにしか対応していません。
今回は、Unity2019.4を使用していきます。Pythonのインストール
ML-Agentsのインストールが終わったらPythonと、周辺ライブラリを入れます。
まず、Anacondaをダウンロードし、インストールします。
Anacondaのインストールが終わったらML-Agents用の仮想環境を用意します。
公式ではPython3.6かPython3.7が推奨されています。今回は、Python3.7を使用していきます。conda update -n base conda conda create --name mlagents python=3.7 source activate mlagentsML-Agentsリポジトリをクローン
適当なディレクトリにML-Agentsリポジトリのクローンを作成します。
git clone --branch release_10 https://github.com/Unity-Technologies/ml-agents.gitcom.unity.ml-agentsパッケージのインストール
- Unity上で
Window->Package Managerに移動します。Package Managerwindowで、+ボタンをクリックします。Add package from disk...を選択します。- クローンしたディレクトリ内の
com.unity.ml-agentsに移動します。package.jsonファイルを選択します。
ml-agentsパッケージのインストール
クローンしたディレクトリに移動し、ml-agentsパッケージをインストールします。
pip3 install -e ./ml-agents-envs pip3 install -e ./ml-agents1. Unity上で学習環境の作成
今回は学習環境として以下のものを含む非常にシンプルなSceneを作成します。
- エージェントが移動するフィールド
- 射撃対象のターゲット
- 自動でターゲットを射撃するエージェント
フィールド ターゲット エージェント フィールドとターゲット
フィールドとターゲットに関してはColliderが含まれていれば何でも大丈夫ですが、今回はそれぞれPlaneとCubeで作成し、名前をFloorとTargetにします。
ターゲットに関する補足
説明が前後して煩雑なので、エージェントの設定が終わったあとに見るとよいと思いますが、実際にはターゲット (Target) には下のようなTargetScriptがアタッチされています。これは、エージェントが射出した弾 (Bullet) がターゲットに当たった時に、エージェントに知らせる機能を有しています。TargetScriptusing System.Collections; using System.Collections.Generic; using UnityEngine; public class TargetScript : MonoBehaviour { GameObject agent; ShootingAgentScript script; void Start() { agent = GameObject.Find("ShootingAgent"); } void OnCollisionEnter(Collision collision) { if (collision.gameObject.tag == "Bullet") { script = agent.GetComponent<ShootingAgentScript>(); script.Hit(); } } }エージェント
今回のML-Agentsで強化学習を行うにあたってメインのゲームオブジェクトとなるのがこのエージェントです。エージェントに強化学習を行うための機能を実装していき、自動でターゲットを認識し射撃をおこなうよう訓練をしていきます。
エージェントオブジェクトは以下の手順で作成します。
- Cubeを作成し、名前をShootingAgentにします。
- Rigidbodyコンポーネントを追加します。
エージェントについての詳細は次章で説明します。
ビュレット
エージェントから射出する弾はBulletという名前で下の
BulletScriptをアタッチしたSphereオブジェクトのプレハブとして用意します。また、このオブジェクトのTagをBulletに変更しておいてください。
BulletScript
BulletScriptusing System.Collections; using System.Collections.Generic; using UnityEngine; public class BulletScript : MonoBehaviour { public GameObject particleObject; void Start() { Destroy(this.gameObject, 0.5f); } }2. エージェントの実装
ShootingAgentにShootingAgentScriptという新しいスクリプトを追加し、編集していきます。
using Unity.MLAgents;、using Unity.MLAgents.Sensors;を追加します。- スーパークラスをMonoBehaviourからAgentに変更します。
- 3つのメソッドを付け加えます。
- OnEpisodeBegin()
- CollectObservations(VectorSensor sensor)
- OnActionReceived(float[] vectorAction)
それぞれのメソッドをどのように書くかは次のサブセクションで説明しますが、簡単にそれぞれの機能を説明します。
OnEpisodeBegin()
エージェントは何回も同じような環境で試行を繰り返し、失敗や成功しながら与えられたタスク(今回はターゲットを自動で認識して射撃すること)を学習していくわけですが、その時各試行の初期状態(エージェントやターゲットの配置など)をどのようにするかをOnEpisodeBegin()に書きます。
CollectObservations()
各試行では、エージェントは与えられた環境の情報(ターゲットの位置や自分の位置・向きなど)を収集し、ブレインに送信します。ブレインはその情報を元にエージェントにどのように行動をすべきか指示します。その際、エージェントが何の情報を収集するかということをCollectObservations(VectorSensor sensor)内に書き下します。
OnActionReceived()
最後に、エージェントが行動をした結果タスクにおいて成功や失敗をし、それを元にエージェントに報酬を与えます。成功した場合は高い報酬を与え、失敗した場合は低い報酬を与えたり、全く報酬を与えなかったりします。どのような条件のときにどのような報酬を与えるのかということをOnActionReceived(float[] vectorAction)に書きます。
OnEpisodeBegin()
OnEpisodeBegin()には各試行のエージェントや環境上のオブジェクトの初期状態を記載します。
今回の例では、エージェント(ShootingAgent)がターゲット(Target)の射撃に成功するたびにその試行が終了し、ターゲットをランダムな場所に移動させます。また、エージェントがフィールドの外にはみ出してフィールドから落ちた場合、エージェントの速度を0に戻し、フィールド上の初期位置に戻します。
public class ShootingAgentScript : Agent { Rigidbody agentRigidbody; void Start() { agentRigidbody = GetComponent<Rigidbody>(); } public Transform target; public GameObject bullet; public override void OnEpisodeBegin() { if (this.transform.localPosition.y < 0) { // 床の下に落ちた場合エージェントを初期位置に戻す this.agentRigidbody.angularVelocity = Vector3.zero; this.agentRigidbody.velocity = Vector3.zero; this.transform.localPosition = new Vector3(0, 0.5f, 0); } // ターゲットを新たなランダムな場所に移動させる target.localPosition = new Vector3(Random.value * 8 - 4, 0.5f, Random.value * 8 - 4); } }CollectObservations(VectorSensor sensor)
エージェントは、与えられた環境から情報を収集しブレインに送信します。このメソッドでは何の情報を収集するか、すなわち、エージェントの訓練のために用いるニューラルネットワークに何の情報を入力するのかというのを定義します。
今回の例では、 エージェントが収集する情報として、エージェントの位置と向き及びターゲットの位置が含まれます。
public override void CollectObservations(VectorSensor sensor) { sensor.AddObservation(target.localPosition); sensor.AddObservation(this.transform.localPosition); sensor.AddObservation(this.transform.forward); }OnActionReceived(float[] vectorAction)
OnActionReceived()には、エージェントがどのような行動をとった場合、どのくらいの報酬を与えるかということを書きます。
OnActionReceived()はエージェントの行動を引数に取るので、OnActionReceived()の中身の前にまず、エージェントにどのような行動が許されているかをMoveAgent()に記述していきます。今回の例では、エージェントにはxおよびz方向への移動とy軸を中心とした回転、さらに射撃(bulletオブジェクトの発射)の行動を取ることができるとします。public void MoveAgent(ActionSegment<int> act) { var dirToGo = Vector3.zero; var rotateDir = Vector3.zero; var forwardAxis = act[0]; var rightAxis = act[1]; var rotateAxis = act[2]; var shootAction = act[3]; switch (forwardAxis) { case 1: dirToGo = new Vector3(0f, 0f, 0.1f); break; case 2: dirToGo = new Vector3(0f, 0f, -0.1f); break; } switch (rightAxis) { case 1: dirToGo = new Vector3(0.1f, 0f, 0f); break; case 2: dirToGo = new Vector3(-0.1f, 0f, 0f); break; } switch (rotateAxis) { case 1: rotateDir = transform.up * -1f; break; case 2: rotateDir = transform.up * 1f; break; } if (shootAction == 1) { GameObject bullets = Instantiate(bullet) as GameObject; Vector3 force; force = this.gameObject.transform.forward * 300.0f; bullets.GetComponent<Rigidbody>().AddForce(force); bullets.transform.position = this.transform.position; } transform.Rotate(rotateDir, Time.deltaTime * 100f); transform.Translate(dirToGo); }この行動を元に、エージェントが最適なアクションを取る(今回でいうと、できるだけ早くターゲットを認識し射撃行為を行う)ように報酬を与えます。今回は、ターゲットを射撃することができた場合に
1.0の報酬を与え、フィールドから落ちた場合には報酬を与えず初期状態に戻します。さらに、できるだけ早くターゲットを射撃するように学習を行うため、時間が経過するごとに-0.001の報酬を与えます。public override void OnActionReceived(ActionBuffers actionBuffers) { SetReward(-0.001f); if (isDamage) { SetReward(1.0f); EndEpisode(); } else if (this.transform.localPosition.y < 0) { EndEpisode(); } MoveAgent(actionBuffers.DiscreteActions); }ここで、
isDamageは初期状態ではfalseになっているbool変数で、エージェントが射出した弾がターゲットと衝突した瞬間にtrueになる設計になっています。エージェントの全体像
前章までで、エージェントの実装は完了ですが、説明の分かりやすさのために省いた部分も含めエージェントにアタッチする
ShootingAgentScriptの全体のコードを改めて提示します。
ShootingAgentScript
ShootingAgentScriptusing System.Collections.Generic; using UnityEngine; using Unity.MLAgents; using Unity.MLAgents.Actuators; using Unity.MLAgents.Sensors; public class ShootingAgentScript : Agent { Rigidbody agentRigidbody; void Start() { agentRigidbody = GetComponent<Rigidbody>(); } public Transform target; public GameObject bullet; public bool isDamage; public override void OnEpisodeBegin() { if (this.transform.localPosition.y < 0) { this.agentRigidbody.angularVelocity = Vector3.zero; this.agentRigidbody.velocity = Vector3.zero; this.transform.localPosition = new Vector3(0, 0.5f, 0); } target.localPosition = new Vector3(Random.value * 8 - 4, 0.5f, Random.value * 8 - 4); isDamage = false; } public void Hit() { isDamage = true; } public override void CollectObservations(VectorSensor sensor) { sensor.AddObservation(target.localPosition); sensor.AddObservation(this.transform.localPosition); sensor.AddObservation(this.transform.forward); } public bool ReloadCheck() { if (GameObject.FindGameObjectWithTag("Bullet")) { return false; } else { return true; } } public void MoveAgent(ActionSegment<int> act) { var dirToGo = Vector3.zero; var rotateDir = Vector3.zero; var forwardAxis = act[0]; var rightAxis = act[1]; var rotateAxis = act[2]; var shootAction = act[3]; switch (forwardAxis) { case 1: dirToGo = new Vector3(0f, 0f, 0.1f); break; case 2: dirToGo = new Vector3(0f, 0f, -0.1f); break; } switch (rightAxis) { case 1: dirToGo = new Vector3(0.1f, 0f, 0f); break; case 2: dirToGo = new Vector3(-0.1f, 0f, 0f); break; } switch (rotateAxis) { case 1: rotateDir = transform.up * -1f; break; case 2: rotateDir = transform.up * 1f; break; } if (shootAction == 1 && ReloadCheck()) { GameObject bullets = Instantiate(bullet) as GameObject; Vector3 force; force = this.gameObject.transform.forward * 300.0f; bullets.GetComponent<Rigidbody>().AddForce(force); bullets.transform.position = this.transform.position; } transform.Rotate(rotateDir, Time.deltaTime * 100f); transform.Translate(dirToGo); } public override void OnActionReceived(ActionBuffers actionBuffers) { SetReward(-0.001f); if (isDamage) { SetReward(1.0f); EndEpisode(); } else if (this.transform.localPosition.y < 0) { EndEpisode(); } MoveAgent(actionBuffers.DiscreteActions); } public override void Heuristic(in ActionBuffers actionsOut) { var discreteActionsOut = actionsOut.DiscreteActions; discreteActionsOut.Clear(); if (Input.GetKey(KeyCode.W)) { discreteActionsOut[0] = 1; } if (Input.GetKey(KeyCode.S)) { discreteActionsOut[0] = 2; } if (Input.GetKey(KeyCode.A)) { discreteActionsOut[2] = 1; } if (Input.GetKey(KeyCode.D)) { discreteActionsOut[2] = 2; } if (Input.GetKey(KeyCode.E)) { discreteActionsOut[1] = 1; } if (Input.GetKey(KeyCode.Q)) { discreteActionsOut[1] = 2; } discreteActionsOut[3] = Input.GetKey(KeyCode.Space) ? 1 : 0; } }3. Unityエディター上での設定
最後に
ShootingAgentのコンポーネントの一部を変更して終了です。
TargetオブジェクトとBulletオブジェクトをShootingAgentのShootingAgentScriptのフィールドにドラッグします。- Add Componentから
Decision Requesterスクリプトを追加し、Decision Periodを10にします。- さらに、同じようにして
Behavior Parametersスクリプトを追加し、パラメータを下のようにします。
Behavior Name: ShootingVector Observation > Space Size: 9Vector Action > Space Type: DiscreteVector Action > Branches Size: 4Vector Action > Branches Size > Branch 0 Size: 3Vector Action > Branches Size > Branch 1 Size: 3Vector Action > Branches Size > Branch 2 Size: 3Vector Action > Branches Size > Branch 3 Size: 24. 学習
学習の設定
クローンしたML-Agentsリポジトリの中の
config/ディレクトリの下に学習の設定(ハイパーパラメータ)を書いたconfig.yamlを作成します。今回は、PPO (Proximal Policy Optimization) と呼ばれる強化学習手法を用いて学習を行います。
config.yamlbehaviors: Shooting: trainer_type: ppo hyperparameters: batch_size: 10 buffer_size: 100 learning_rate: 3.0e-4 beta: 5.0e-4 epsilon: 0.2 lambd: 0.99 num_epoch: 3 learning_rate_schedule: linear network_settings: normalize: false hidden_units: 128 num_layers: 2 reward_signals: extrinsic: gamma: 0.99 strength: 1.0 max_steps: 100000 time_horizon: 64 summary_freq: 1000学習の実行
ML-Agentsリポジトリに移動し、以下のコマンドを実行します。
mlagents-learn config/config.yaml --run-id=shootingすると、下のように表示されます。
▄▄▄▓▓▓▓ ╓▓▓▓▓▓▓█▓▓▓▓▓ ,▄▄▄m▀▀▀' ,▓▓▓▀▓▓▄ ▓▓▓ ▓▓▌ ▄▓▓▓▀' ▄▓▓▀ ▓▓▓ ▄▄ ▄▄ ,▄▄ ▄▄▄▄ ,▄▄ ▄▓▓▌▄ ▄▄▄ ,▄▄ ▄▓▓▓▀ ▄▓▓▀ ▐▓▓▌ ▓▓▌ ▐▓▓ ▐▓▓▓▀▀▀▓▓▌ ▓▓▓ ▀▓▓▌▀ ^▓▓▌ ╒▓▓▌ ▄▓▓▓▓▓▄▄▄▄▄▄▄▄▓▓▓ ▓▀ ▓▓▌ ▐▓▓ ▐▓▓ ▓▓▓ ▓▓▓ ▓▓▌ ▐▓▓▄ ▓▓▌ ▀▓▓▓▓▀▀▀▀▀▀▀▀▀▀▓▓▄ ▓▓ ▓▓▌ ▐▓▓ ▐▓▓ ▓▓▓ ▓▓▓ ▓▓▌ ▐▓▓▐▓▓ ^█▓▓▓ ▀▓▓▄ ▐▓▓▌ ▓▓▓▓▄▓▓▓▓ ▐▓▓ ▓▓▓ ▓▓▓ ▓▓▓▄ ▓▓▓▓` '▀▓▓▓▄ ^▓▓▓ ▓▓▓ └▀▀▀▀ ▀▀ ^▀▀ `▀▀ `▀▀ '▀▀ ▐▓▓▌ ▀▀▀▀▓▄▄▄ ▓▓▓▓▓▓, ▓▓▓▓▀ `▀█▓▓▓▓▓▓▓▓▓▌ ¬`▀▀▀█▓ Version information: ml-agents: 0.22.0, ml-agents-envs: 0.22.0, Communicator API: 1.2.0, PyTorch: 1.7.0 2020-12-12 23:47:09 INFO [learn.py:275] run_seed set to 0000 2020-12-12 23:47:10 INFO [environment.py:205] Listening on port 0000. Start training by pressing the Play button in the Unity Editor.この状態でUnityに戻り、Playボタンを押すと学習が開始されます。学習が始まるとコンソール上には、下のように学習の状況が描画されます。
Shooting. Step: 1000. Time Elapsed: 180.311 s. Mean Reward: 0.356. Std of Reward: 0.492. Training. Shooting. Step: 2000. Time Elapsed: 325.077 s. Mean Reward: 0.342. Std of Reward: 0.491. Training. Shooting. Step: 3000. Time Elapsed: 464.584 s. Mean Reward: 0.425. Std of Reward: 0.500. Training. Shooting. Step: 4000. Time Elapsed: 602.867 s. Mean Reward: 0.349. Std of Reward: 0.490. Training. Shooting. Step: 5000. Time Elapsed: 746.095 s. Mean Reward: 0.344. Std of Reward: 0.491. Training. Shooting. Step: 6000. Time Elapsed: 889.383 s. Mean Reward: 0.450. Std of Reward: 0.501. Training. Shooting. Step: 7000. Time Elapsed: 1026.152 s. Mean Reward: 0.335. Std of Reward: 0.490. Training. Shooting. Step: 8000. Time Elapsed: 1169.780 s. Mean Reward: 0.454. Std of Reward: 0.504. Training. Shooting. Step: 9000. Time Elapsed: 1319.780 s. Mean Reward: 0.464. Std of Reward: 0.509. Training. ...学習の結果
100000ステップまでの報酬を図にすると下図のようになり、試行を重ねるごとにエージェントが取得する報酬の値が大きくなっていることが分かります。
学習を開始してすぐ(1000 step)のエージェントと学習が終わったとき(100000 step)のエージェントの動きを比較してみます。初めのうちはエージェントがフィールドの上をデタラメに動き、ごくたまにターゲットを射撃することができているという状態です(左図)が、学習が進んでくるとなかなかのスピードでターゲットを認識し射撃することができるようになっている(右図)のが分かります。
1000 step 100000 step さいごに
今回、ML-Agentsを用いてランダムに出現するターゲットを自動で射撃するエージェントを作ってきました。ML-Agentsの進歩のおかげもあってそこまで苦労することなく(強化学習の深い知識がなくても)ある程度思い通りの学習をすることが可能になってきたので、ゲームの対戦相手やオープンワールを巡回するキャラクターの自動生成、自動運転やロボットのシミュレーションなど様々な用途の中で興味を持ったものに強化学習を応用してみるとよいのではないでしょうか。
参考文献
強化学習の応用例
[1] David Silver, Julian Schrittwieser, Karen Simonyan, Ioannis Antonoglou, Aja Huang,
Arthur Guez, Thomas Hubert, Lucas Baker, Matthew Lai, Adrian Bolton, Yutian Chen,
Timothy Lillicrap, Fan Hui, Laurent Sifre, George van den Driessche, Thore Graepel, and
Demis Hassabis. “Mastering the game of go without human knowledge”. Nature (2017).[2] Ahmad El Sallab, Mohammed Abdou, Etienne Perot, Senthil Yogamani. “Deep reinforcement learning framework for autonomous driving”. Autonomous Vehicles and Machines, Electronic Imaging (2017).
[3] Yu Fan Chen, Michael Everett, Miao Liu, Jonathan P. How. “Socially aware motion planning with deep reinforcement learning”. arXiv:1703.08862 (2017).
[4] Nicola De Cao, Thomas Kipf. “MolGAN: An implicit generative model for small molecular graphs”. ICML workshop (2018).
ML-Agents
[5] 「Unity ML-Agents」〔https://unity3d.com/jp/machine-learning〕 (最終検索日 : 2020 年 12 月 13 日)
[6] Arthur Juliani, Vincent-Pierre Berges, Ervin Teng, Andrew Cohen, Jonathan Harper, Chris Elion, Chris Goy, Yuan Gao, Hunter Henry, Marwan Mattar, Danny Lange. “Unity: A General Platform for Intelligent Agents”. arXiv:1809.02627 (2020).
強化学習・模倣学習
[7] J Schulman, F Wolski, P Dhariwal, A Radford and O Klimov. “Proximal Policy Optimization Algorithms”. arXiv:1707.06347 (2017).
[8] Tuomas Haarnoja, Aurick Zhou, Pieter Abbeel, Sergey Levine. “actor-critic: Off-policy maximum entropy deep reinforcement learning with a stochastic actor”. ICML (2018).
[9] Jonathan Ho, Stefano Ermon. “Generative Adversarial Imitation Learning”. NeurIPS (2016).
[10] Shreyansh Daftry, J Andrew Bagnell, Martial Hebert. “Learning transferable policies for monocular reactive mav control”. ISER (2016).
- 投稿日:2020-12-12T23:09:34+09:00
[Unity][Timeline] Unity Timeline 1.4 TimelineActionの使い方
この記事はUnity #2 Advent Calendar 2020の12/12(土)の記事です。
はじめに
この記事ではUnityのTimeline機能に存在する
TimelineActionとそれに関連する機能について解説します。ターゲット
Unityを使った開発を行っておりTimeline機能を拡張して実装を進めているエンジニアがメインターゲットです。
Unityの基礎的な内容やTimeline、Editor拡張などで頻出する単語・機能などに関しての詳細な説明は省略しますので予めご了承ください。またここで解説する機能はUnity Timeline 1.4.0以降から使えるようになるものです。
詳しくはchangelogを参照してください。得られるもの
UnityEditor.Timeline.Actions.TimelineActionの基本的な使い方を知ることができます。この機能は主にTimelineウィンドウ上のコンテキストメニューに独自の処理を追加することを可能とします。
1.4.0以前のバージョンではTimelineウィンドウに関わるクラス群がほとんど
internalな宣言になっていたこともあり、独自の処理をTimelineウィンドウ内に追加することは現実的ではなく、オリジナルのEditorWindowを定義して利用者にはTimelineウィンドウとその独自ウィンドウを一緒に開いてもらって編集をするようなワークフローを組んでいた方も多いと思います。今回登場したこの
TimelineActionの機能によりコンテキストメニューを使えるようになったので、そのような独自ウィンドウをわざわざ作らなくても済むことが大幅に増えました。本題
Timeline機能も昔と比べてサポートが充実してきました。
公式のTimelineActionに関するAPIドキュメントを見に行くとしっかりとサンプルコードが載っています。
今回はこのサンプルコードを軸に解説を進めます。SampleTimelineAction.cs[MenuEntry("Custom Actions/Sample Timeline Action")] public class SampleTimelineAction : TimelineAction { public override ActionValidity Validate(ActionContext context) { return ActionValidity.Valid; } public override bool Execute(ActionContext context) { Debug.Log("Test Action"); return true; } [TimelineShortcut("SampleTimelineAction", KeyCode.Q)] public static void HandleShortCut(ShortcutArguments args) { Invoker.InvokeWithSelected<SampleTimelineAction>(); } }
TimelineAction
TimelineActionの機能を使うためにはTimelineActionクラスを継承します。
詳細は後述するそれぞれの項目で解説しますが、このクラスを継承しただけではTimelineウィンドウ上では動作せず、MenuEntryAttributeやTimelineShortcutAttributeと組み合わせて使用することでTimelineウィンドウ上で処理を実行できるようになります。
public override ActionValidity Validate(ActionContext context)後述の
Executeメソッドを実行するかどうかを検証するメソッドです。
ActionValidity戻り値の
ActionValidityによって動作が変わります。
ActionValidityの種別 Executeメソッドが実行されるかどうかMenuEntryAttributeを使用している場合の挙動ActionValidity.Valid 実行されます。 コンテキストメニューに選択可能な状態で表示されます。 ActionValidity.NotApplicable 実行されません。 コンテキストメニューには表示されません。 ActionValidity.Inavlid 実行されません。 コンテキストメニューに選択不可能な状態で表示されます。 特殊なのが
ActionValidity.NotApplicableで、これはメニューにも表示されなくなります。
使い分けの仕方としては、条件によって使用可能な処理はActionValidity.Invalidを、そもそもその編集状態では使用することができない処理はActionValidity.NotApplicableという風にするのが良いと思います。
これは、1つのUnityプロジェクトの中で性質の違う複数のTimelineを編集する必要があるような環境で使えそうです。
(ゲーム開発で例えると、ガチャ演出のTimeline編集専用のActionと奥義演出のTimeline編集専用のAction機能が同居しているなどのケース)逆に言えば、そこまで規模の大きくない編集環境であれば、基本的には
ActionValidity.ValidとActionValidity.Invalidの2つを使えばよいと思います。
ActionContextこのActionが動作する際のTimelineウィンドウ上の様々な情報が
ActionContextとして引数に入ってきます。それぞれのプロパティ(フィールド)の詳細は以下の通りです。
プロパティ(フィールド)名 注意事項など PlayableDirector directorTimelineEditor.inspectedDirectorと同様の内容が入ります。Projectウィンドウで直接TimelineAssetを選択した状態だとnullが入ります。TimelineAsset timelineTimelineEditor.inspectedAssetと同様の内容が貼ります。double? invocationTimeコンテキストメニュー経由で実行した場合、右クリックした場所から時間を算出して秒数が入ります。※1 ショートカット経由の場合は nullが入ります。IEnumerable<TrackAsset> tracksSelectionManager.SelectedTracks()と同様の内容が入ります。※2IEnumerable<TimelineClip> clipsSelectionManager.SelectedClips()と同様の内容が入ります。※2IEnumerable<IMarker> markersSelectionManager.SelectedMarkers()と同様の内容が入ります。※2※1 クリップが置かれる場所を右クリックした際はその位置の再生時間が入ってきましたが、左側にあるトラックリストの部分を右クリックしてコンテキストメニューから実行した際に入る時間がよく分からない値になっていました。調査中です。
※2SelectionManagerはinternal classです。
public override bool Execute(ActionContext context)実際に処理したい内容をここに記述します。
引数にはValidateメソッドのときと同様にActionContextが入ってきます。戻り値には処理の成否を返却します。
実行されるActionのインスタンスは
ActionManagerという内部クラスでキャッシュされて使い回すため、フィールドに値を持ったりする場合は注意してください。
MenuEntryAttribute
TimelineActionを継承したクラスにこのAttributeをつけることで、Timelineウィンドウ上のコンテキストメニューに処理を追加することができます。機能としてはTimelineウィンドウの中でしか動作しないという点以外は
UnityEditor.MenuItemとほぼ同様の内容です。標準機能のプライオリティに関しては
MenuPriorityクラスで確認することができます。
TimelineActionの場合、マーカー、トラック、クリップ(クリップが置かれていない場所を含む)を右クリックした際のコンテキストメニューに表示されます。※マーカーのみ、トラックのみ、クリップのみに対応したい場合はそれぞれ
MarkerAction,TrackAction,ClipActionが存在します。
TimelineShortcutAttributeTimelineウィンドウ上でショートカットキーを使えるようにします。
機能としてはUnity2019から追加されたShortcuts Managerと同様ですが、このAttributeを使うことで自動的にTimelineのカテゴリに振り分けてくれます。
コンテキストメニューで処理する内容はショートカットキーでも同様に使いたいケースが多いことから、この
TimelineActionのサンプルで紹介されていますが、このAttribute自体はTimelineActionと関係なく単独で動作します。
ショートカットキー周りは時間があれば別途記事を作って紹介するかもしれません。。
Invokerとてもシンプルな名前ですが、
TimelineActionを含む各種Actionクラスの処理を外から手軽に呼び出せるようにしたユーティリティクラスです。
TimelineActionの場合、bool InvokeWithSelected<T>() where T : TimelineActionメソッドを使用することで、任意の独自Actionを呼び出すことが可能です。引数に渡す
ActionContextの用意やUndo処理なども含めて内部でやってくれるので、よほど特殊な事情がなければこのInvokerクラスを経由して各種Actionクラスを実行したほうがよいでしょう。オマケ
APIドキュメントにサンプルコードが書かれているのはとても有り難いことですが、内容は最低限しかないためイマイチどのように使うのかピンとこない人もいると思います。
実は標準的な機能(コピー&ペーストや削除など)も、このActionを使って定義されています。
※場所は↓(@の後ろは自分の使っているTimelineのバージョン)
com.unity.timeline@***/Editor/Actions/TimelineActions.cs
internalなクラスを多用しているのでそのまま参考にできるわけではありませんが、それらの実装を見ることでイメージがかなり掴めるようになると思いますので、そちらも合わせて見てから自分の実装を進めると良いと思います。おわりに
今回は
TimelineActionについて解説してみました。
これ以外にも機能を限定したMarkerAction,TrackAction,ClipActionも存在しますが、基本的な仕様はTimelineActionと同様なので、この記事だけでも使えると思います。Timelineもようやくコンテキストメニューも使えるようになり、拡張の幅もかなり広がってきました。
みなさんが開発しているツールが少しでも便利になってもらえれば幸いです。
- 投稿日:2020-12-12T23:09:34+09:00
[Unity][Timeline] Unity Timeline 1.4で追加されたTimelineActionの使い方
この記事はUnity #3 Advent Calendar 2020の12/12(土)の記事です。
はじめに
この記事ではUnityのTimeline機能に存在する
TimelineActionとそれに関連する機能について解説します。ターゲット
Unityを使った開発を行っておりTimeline機能を拡張して実装を進めているエンジニアがメインターゲットです。
Unityの基礎的な内容やTimeline、Editor拡張などで頻出する単語・機能などに関しての詳細な説明は省略しますので予めご了承ください。またここで解説する機能はUnity Timeline 1.4.0以降から使えるようになるものです。
詳しくはchangelogを参照してください。得られるもの
UnityEditor.Timeline.Actions.TimelineActionの基本的な使い方を知ることができます。この機能は主にTimelineウィンドウ上のコンテキストメニューに独自の処理を追加することを可能とします。
1.4.0以前のバージョンではTimelineウィンドウに関わるクラス群がほとんど
internalな宣言になっていたこともあり、独自の処理をTimelineウィンドウ内に追加することは現実的ではなく、オリジナルのEditorWindowを定義して利用者にはTimelineウィンドウとその独自ウィンドウを一緒に開いてもらって編集をするようなワークフローを組んでいた方も多いと思います。今回登場したこの
TimelineActionの機能によりコンテキストメニューを使えるようになったので、そのような独自ウィンドウをわざわざ作らなくても済むことが大幅に増えました。本題
Timeline機能も昔と比べてサポートが充実してきました。
公式のTimelineActionに関するAPIドキュメントを見に行くとしっかりとサンプルコードが載っています。
今回はこのサンプルコードを軸に解説を進めます。SampleTimelineAction.cs[MenuEntry("Custom Actions/Sample Timeline Action")] public class SampleTimelineAction : TimelineAction { public override ActionValidity Validate(ActionContext context) { return ActionValidity.Valid; } public override bool Execute(ActionContext context) { Debug.Log("Test Action"); return true; } [TimelineShortcut("SampleTimelineAction", KeyCode.Q)] public static void HandleShortCut(ShortcutArguments args) { Invoker.InvokeWithSelected<SampleTimelineAction>(); } }
TimelineAction
TimelineActionの機能を使うためにはTimelineActionクラスを継承します。
詳細は後述するそれぞれの項目で解説しますが、このクラスを継承しただけではTimelineウィンドウ上では動作せず、MenuEntryAttributeやTimelineShortcutAttributeと組み合わせて使用することでTimelineウィンドウ上で処理を実行できるようになります。
public override ActionValidity Validate(ActionContext context)後述の
Executeメソッドを実行するかどうかを検証するメソッドです。
ActionValidity戻り値の
ActionValidityによって動作が変わります。
ActionValidityの種別 Executeメソッドが実行されるかどうかMenuEntryAttributeを使用している場合の挙動ActionValidity.Valid 実行されます。 コンテキストメニューに選択可能な状態で表示されます。 ActionValidity.NotApplicable 実行されません。 コンテキストメニューには表示されません。 ActionValidity.Inavlid 実行されません。 コンテキストメニューに選択不可能な状態で表示されます。 特殊なのが
ActionValidity.NotApplicableで、これはメニューにも表示されなくなります。
使い分けの仕方としては、条件によって使用可能な処理はActionValidity.Invalidを、そもそもその編集状態では使用することができない処理はActionValidity.NotApplicableという風にするのが良いと思います。
これは、1つのUnityプロジェクトの中で性質の違う複数のTimelineを編集する必要があるような環境で使えそうです。
(ゲーム開発で例えると、ガチャ演出のTimeline編集専用のActionと奥義演出のTimeline編集専用のAction機能が同居しているなどのケース)逆に言えば、そこまで規模の大きくない編集環境であれば、基本的には
ActionValidity.ValidとActionValidity.Invalidの2つを使えばよいと思います。
ActionContextこのActionが動作する際のTimelineウィンドウ上の様々な情報が
ActionContextとして引数に入ってきます。それぞれのプロパティ(フィールド)の詳細は以下の通りです。
プロパティ(フィールド)名 注意事項など PlayableDirector directorTimelineEditor.inspectedDirectorと同様の内容が入ります。Projectウィンドウで直接TimelineAssetを選択した状態だとnullが入ります。TimelineAsset timelineTimelineEditor.inspectedAssetと同様の内容が入ります。double? invocationTimeコンテキストメニュー経由で実行した場合、右クリックした場所から時間を算出して秒数が入ります。※1 ショートカット経由での実行の場合は nullが入ります。IEnumerable<TrackAsset> tracksSelectionManager.SelectedTracks()と同様の内容が入ります。※2IEnumerable<TimelineClip> clipsSelectionManager.SelectedClips()と同様の内容が入ります。※2IEnumerable<IMarker> markersSelectionManager.SelectedMarkers()と同様の内容が入ります。※2※1 クリップが置かれる場所を右クリックした際はその位置の再生時間が入ってきましたが、左側にあるトラックリストの部分を右クリックしてコンテキストメニューから実行した際に入る時間がよく分からない値になっていました。調査中です。
※2SelectionManagerはinternal classです。
public override bool Execute(ActionContext context)実際に処理したい内容をここに記述します。
引数にはValidateメソッドのときと同様にActionContextが入ってきます。戻り値には処理の成否を返却します。
実行されるActionのインスタンスは
ActionManagerという内部クラスでキャッシュされて使い回すため、フィールドに値を持ったりする場合は注意してください。
MenuEntryAttribute
TimelineActionを継承したクラスにこのAttributeをつけることで、Timelineウィンドウ上のコンテキストメニューに処理を追加することができます。機能としてはTimelineウィンドウの中でしか動作しないという点以外は
UnityEditor.MenuItemとほぼ同様の内容です。標準機能のプライオリティに関しては
MenuPriorityクラスで確認することができます。
TimelineActionの場合、マーカー、トラック、クリップ(クリップが置かれていない場所を含む)を右クリックした際のコンテキストメニューに表示されます。※マーカーのみ、トラックのみ、クリップのみに対応したい場合はそれぞれ
MarkerAction,TrackAction,ClipActionが存在します。
TimelineShortcutAttributeTimelineウィンドウ上でショートカットキーを使えるようにします。
機能としてはUnity2019から追加されたShortcuts Managerと同様ですが、このAttributeを使うことで自動的にTimelineのカテゴリに振り分けてくれます。
コンテキストメニューで処理する内容はショートカットキーでも同様に使いたいケースが多いことから、この
TimelineActionのサンプルで紹介されていますが、このAttribute自体はTimelineActionと関係なく単独で動作します。
ショートカットキー周りは時間があれば別途記事を作って紹介するかもしれません。。
Invokerとてもシンプルな名前ですが、
TimelineActionを含む各種Actionクラスの処理を外から手軽に呼び出せるようにしたユーティリティクラスです。
TimelineActionの場合、bool InvokeWithSelected<T>() where T : TimelineActionメソッドを使用することで、任意の独自Actionを呼び出すこと(ValidateしてExecuteするまで)が可能です。引数に渡す
ActionContextの用意やUndo処理なども含めて内部でやってくれるので、よほど特殊な事情がなければこのInvokerクラスを経由して各種Actionクラスを実行したほうがよいでしょう。オマケ
APIドキュメントにサンプルコードが書かれているのはとても有り難いことですが、内容は最低限しかないためイマイチどのように使うのかピンとこない人もいると思います。
実は標準的な機能(コピー&ペーストや削除など)も、このActionを使って定義されています。
※場所は↓(@の後ろは自分の使っているTimelineのバージョン)
com.unity.timeline@1.4.0/Editor/Actions/TimelineActions.cs
internalなクラスを多用しているのでそのまま参考にできるわけではありませんが、それらの実装を見ることでイメージがかなり掴めるようになると思いますので、そちらも合わせて見てから自分の実装を進めると良いと思います。おわりに
今回は
TimelineActionについて解説してみました。
これ以外にも機能を限定したMarkerAction,TrackAction,ClipActionも存在しますが、基本的な仕様はTimelineActionと同様なので、この記事だけでも使えると思います。Timelineもようやくコンテキストメニューも使えるようになり、拡張の幅もかなり広がってきました。
みなさんが開発しているツールが少しでも便利になってもらえれば幸いです。
- 投稿日:2020-12-12T23:09:34+09:00
Unity Timeline 1.4で追加されたTimelineActionの使い方
この記事はUnity #3 Advent Calendar 2020の12/12(土)の記事です。
はじめに
この記事ではUnityのTimeline機能に存在する
TimelineActionとそれに関連する機能について解説します。ターゲット
Unityを使った開発を行っておりTimeline機能を拡張して実装を進めているエンジニアがメインターゲットです。
Unityの基礎的な内容やTimeline、Editor拡張などで頻出する単語・機能などに関しての詳細な説明は省略しますので予めご了承ください。またここで解説する機能はUnity Timeline 1.4.0以降から使えるようになるものです。
詳しくはchangelogを参照してください。得られるもの
UnityEditor.Timeline.Actions.TimelineActionの基本的な使い方を知ることができます。この機能は主にTimelineウィンドウ上のコンテキストメニューに独自の処理を追加することを可能とします。
1.4.0以前のバージョンではTimelineウィンドウに関わるクラス群がほとんど
internalな宣言になっていたこともあり、独自の処理をTimelineウィンドウ内に追加することは現実的ではなく、オリジナルのEditorWindowを定義して利用者にはTimelineウィンドウとその独自ウィンドウを一緒に開いてもらって編集をするようなワークフローを組んでいた方も多いと思います。今回登場したこの
TimelineActionの機能によりコンテキストメニューを使えるようになったので、そのような独自ウィンドウをわざわざ作らなくても済むことが大幅に増えました。本題
Timeline機能も昔と比べてサポートが充実してきました。
公式のTimelineActionに関するAPIドキュメントを見に行くとしっかりとサンプルコードが載っています。
今回はこのサンプルコードを軸に解説を進めます。SampleTimelineAction.cs[MenuEntry("Custom Actions/Sample Timeline Action")] public class SampleTimelineAction : TimelineAction { public override ActionValidity Validate(ActionContext context) { return ActionValidity.Valid; } public override bool Execute(ActionContext context) { Debug.Log("Test Action"); return true; } [TimelineShortcut("SampleTimelineAction", KeyCode.Q)] public static void HandleShortCut(ShortcutArguments args) { Invoker.InvokeWithSelected<SampleTimelineAction>(); } }
TimelineAction
TimelineActionの機能を使うためにはTimelineActionクラスを継承します。
詳細は後述するそれぞれの項目で解説しますが、このクラスを継承しただけではTimelineウィンドウ上では動作せず、MenuEntryAttributeやTimelineShortcutAttributeと組み合わせて使用することでTimelineウィンドウ上で処理を実行できるようになります。
public override ActionValidity Validate(ActionContext context)後述の
Executeメソッドを実行するかどうかを検証するメソッドです。
ActionValidity戻り値の
ActionValidityによって動作が変わります。
ActionValidityの種別 Executeメソッドが実行されるかどうかMenuEntryAttributeを使用している場合の挙動ActionValidity.Valid 実行されます。 コンテキストメニューに選択可能な状態で表示されます。 ActionValidity.NotApplicable 実行されません。 コンテキストメニューには表示されません。 ActionValidity.Inavlid 実行されません。 コンテキストメニューに選択不可能な状態で表示されます。 特殊なのが
ActionValidity.NotApplicableで、これはメニューにも表示されなくなります。
使い分けの仕方としては、条件によって使用可能な処理はActionValidity.Invalidを、そもそもその編集状態では使用することができない処理はActionValidity.NotApplicableという風にするのが良いと思います。
これは、1つのUnityプロジェクトの中で性質の違う複数のTimelineを編集する必要があるような環境で使えそうです。
(ゲーム開発で例えると、ガチャ演出のTimeline編集専用のActionと奥義演出のTimeline編集専用のAction機能が同居しているなどのケース)逆に言えば、そこまで規模の大きくない編集環境であれば、基本的には
ActionValidity.ValidとActionValidity.Invalidの2つを使えばよいと思います。
ActionContextこのActionが動作する際のTimelineウィンドウ上の様々な情報が
ActionContextとして引数に入ってきます。それぞれのプロパティ(フィールド)の詳細は以下の通りです。
プロパティ(フィールド)名 注意事項など PlayableDirector directorTimelineEditor.inspectedDirectorと同様の内容が入ります。Projectウィンドウで直接TimelineAssetを選択した状態だとnullが入ります。TimelineAsset timelineTimelineEditor.inspectedAssetと同様の内容が入ります。double? invocationTimeコンテキストメニュー経由で実行した場合、右クリックした場所から時間を算出して秒数が入ります。※1 ショートカット経由での実行の場合は nullが入ります。IEnumerable<TrackAsset> tracksSelectionManager.SelectedTracks()と同様の内容が入ります。※2IEnumerable<TimelineClip> clipsSelectionManager.SelectedClips()と同様の内容が入ります。※2IEnumerable<IMarker> markersSelectionManager.SelectedMarkers()と同様の内容が入ります。※2※1 クリップが置かれる場所を右クリックした際はその位置の再生時間が入ってきましたが、左側にあるトラックリストの部分を右クリックしてコンテキストメニューから実行した際に入る時間がよく分からない値になっていました。調査中です。
※2SelectionManagerはinternal classです。
public override bool Execute(ActionContext context)実際に処理したい内容をここに記述します。
引数にはValidateメソッドのときと同様にActionContextが入ってきます。戻り値には処理の成否を返却します。
実行されるActionのインスタンスは
ActionManagerという内部クラスでキャッシュされて使い回すため、フィールドに値を持ったりする場合は注意してください。
MenuEntryAttribute
TimelineActionを継承したクラスにこのAttributeをつけることで、Timelineウィンドウ上のコンテキストメニューに処理を追加することができます。機能としてはTimelineウィンドウの中でしか動作しないという点以外は
UnityEditor.MenuItemとほぼ同様の内容です。標準機能のプライオリティに関しては
MenuPriorityクラスで確認することができます。
TimelineActionの場合、マーカー、トラック、クリップ(クリップが置かれていない場所を含む)を右クリックした際のコンテキストメニューに表示されます。※マーカーのみ、トラックのみ、クリップのみに対応したい場合はそれぞれ
MarkerAction,TrackAction,ClipActionが存在します。
TimelineShortcutAttributeTimelineウィンドウ上でショートカットキーを使えるようにします。
機能としてはUnity2019から追加されたShortcuts Managerと同様ですが、このAttributeを使うことで自動的にTimelineのカテゴリに振り分けてくれます。
コンテキストメニューで処理する内容はショートカットキーでも同様に使いたいケースが多いことから、この
TimelineActionのサンプルで紹介されていますが、このAttribute自体はTimelineActionと関係なく単独で動作します。
ショートカットキー周りは時間があれば別途記事を作って紹介するかもしれません。。
Invokerとてもシンプルな名前ですが、
TimelineActionを含む各種Actionクラスの処理を外から手軽に呼び出せるようにしたユーティリティクラスです。
TimelineActionの場合、bool InvokeWithSelected<T>() where T : TimelineActionメソッドを使用することで、任意の独自Actionを呼び出すこと(ValidateしてExecuteするまで)が可能です。引数に渡す
ActionContextの用意やUndo処理なども含めて内部でやってくれるので、よほど特殊な事情がなければこのInvokerクラスを経由して各種Actionクラスを実行したほうがよいでしょう。オマケ
APIドキュメントにサンプルコードが書かれているのはとても有り難いことですが、内容は最低限しかないためイマイチどのように使うのかピンとこない人もいると思います。
実は標準的な機能(コピー&ペーストや削除など)も、このActionを使って定義されています。
※場所は↓(@の後ろは自分の使っているTimelineのバージョン)
com.unity.timeline@1.4.0/Editor/Actions/TimelineActions.cs
internalなクラスを多用しているのでそのまま参考にできるわけではありませんが、それらの実装を見ることでイメージがかなり掴めるようになると思いますので、そちらも合わせて見てから自分の実装を進めると良いと思います。おわりに
今回は
TimelineActionについて解説してみました。
これ以外にも機能を限定したMarkerAction,TrackAction,ClipActionも存在しますが、基本的な仕様はTimelineActionと同様なので、この記事だけでも使えると思います。Timelineもようやくコンテキストメニューも使えるようになり、拡張の幅もかなり広がってきました。
みなさんが開発しているツールが少しでも便利になってもらえれば幸いです。
- 投稿日:2020-12-12T22:33:08+09:00
HoloLens2 × Azure Cognitive Services(Speech SDKで音声認識)
はじめに
HoloLensアドベントカレンダー2020の10日目の記事です。
前回の続きで、エアタップして目の前の画像をキャプチャし、説明文を生成、日本語で読み上げているのですが、音声認識によってこれを動作させたいと思います。「ヨンシル、文字を読んで」「ヨンシル、何が見える?」開発環境
- Azure
- Computer Vision API (画像分析 API)
- Translator API
- Speech SDK 1.14.0
- Unity 2019.4.1f1
- MRTK 2.5.1
- Windows 10 PC
- HoloLens2
導入
1.前回の記事まで終わらせてください。
2.Unityプロジェクトはこんな感じ。エアタップはもう使わないので、前回の「TapToCaptureAnalyze」を非アクティブにしてください。代わりにMySpeechRecognizerを作成します。
3.MySpeechRecognizerにAudioSourceをAdd Componentします。
4.MySpeechRecognizerにTapToCaptureAnalyzeAPI.csをAdd Componentし、Audio SourceにMySpeechRecognizerをアタッチします。あと画像分析結果の画像となるQuadもアタッチしてください。
5.「MySpeechRecognizer.cs」スクリプトは、エアタップの代わりに音声認識してアクションするプログラムです。プログラムがスタートしたら音声認識を継続的に行います。まずWakeワードを認識し、「はい」と応答、その後Actionワードを認識するとTapToCaptureAnalyzeAPIのAirTap関数を実行します。
MySpeechRecognizer.csusing System.Collections; using System.Collections.Generic; using UnityEngine; using Microsoft.CognitiveServices.Speech; public class MySpeechRecognizer : MonoBehaviour { private string recognizedString = ""; private object threadLocker = new object(); private SpeechRecognizer recognizer; private string fromLanguage = "ja-JP"; public string WakeWord = ""; public string ActionWord = ""; public bool action = false; // Start is called before the first frame update void Start() { BeginRecognizing(); } // Update is called once per frame async void Update() { if (recognizedString != "") { // Debug.Log(recognizedString); if (action){ if (recognizedString.ToLower().Contains(ActionWord.ToLower())) { Debug.Log("Analyze"); this.GetComponent<TapToCaptureAnalyzeAPI>().AirTap(); action = false; } }else if (recognizedString.ToLower().Contains(WakeWord.ToLower())) { Debug.Log("Wake"); await this.GetComponent<TapToCaptureAnalyzeAPI>().SynthesizeAudioAsync("はい"); action = true; } } } void OnDestroy() { if (recognizer != null) { recognizer.Dispose(); } } public async void BeginRecognizing() { CreateSpeechRecognizer(); if (recognizer != null) { await recognizer.StartContinuousRecognitionAsync().ConfigureAwait(false); // recognizedString = "Say something..."; Debug.Log("Say something..."); } } void CreateSpeechRecognizer() { if (recognizer == null) { SpeechConfig config = SpeechConfig.FromSubscription("YourSubscriptionKey", "YourServiceRegion"); config.SpeechRecognitionLanguage = fromLanguage; recognizer = new SpeechRecognizer(config); if (recognizer != null) { recognizer.Recognizing += RecognizingHandler; recognizer.Recognized += RecognizedHandler; recognizer.SpeechStartDetected += SpeechStartDetected; recognizer.SpeechEndDetected += SpeechEndDetectedHandler; recognizer.Canceled += CancelHandler; recognizer.SessionStarted += SessionStartedHandler; recognizer.SessionStopped += SessionStoppedHandler; } } } #region Speech Recognition Event Handlers private void SessionStartedHandler(object sender, SessionEventArgs e) { } private void SessionStoppedHandler(object sender, SessionEventArgs e) { recognizer = null; } private void RecognizingHandler(object sender, SpeechRecognitionEventArgs e) { if (e.Result.Reason == ResultReason.RecognizingSpeech) { lock (threadLocker) { recognizedString = $"{e.Result.Text}"; Debug.Log(recognizedString); } } } private void RecognizedHandler(object sender, SpeechRecognitionEventArgs e) { if (e.Result.Reason == ResultReason.RecognizedSpeech) { lock (threadLocker) { recognizedString = $"{e.Result.Text}"; Debug.Log(recognizedString); } } else if (e.Result.Reason == ResultReason.NoMatch) { } } private void SpeechStartDetected(object sender, RecognitionEventArgs e) { } private void SpeechEndDetectedHandler(object sender, RecognitionEventArgs e) { } private void CancelHandler(object sender, RecognitionEventArgs e) { } #endregion }6."YourSubscriptionKey", "YourServiceRegion"にAzureの音声リソースからキーと場所(リージョン)をコピペしてください。
7.fromLanguageに"ja-JP"(日本語)を指定しています。
8.TapToCaptureAnalyzeAPI.csのasync Task SynthesizeAudioAsync(string text) 関数をpublicにします。MySpeechRecognizer.csからWakeワードを認識したら、「はい」と喋らせるためです。
9.Wakeワードに「ヨンシル」、Actionワードに「何が見える」を設定しました
実行
Edito上でも動くので実行してみてください。継続的に音声認識したテキストがコンソールに表示されます。
HoloLens2で実行した動画が以下になります。
音声認識→画像説明文生成→翻訳→音声合成
— 藤本賢志(ガチ本)@XRKaigi (@sotongshi) December 10, 2020
?「ヨンシル」
?「はい」
?「何が見える?」
?「赤と黄色の電子機器を持つ手」
?「装置を持っている人」www#Azure #CognitiveServices #ComputerVision #Analyze #Translator #TTS #SpeechRecognition #SpeechSDK #HoloLens2 #Unity #MRTK #OpenCV pic.twitter.com/xfbm90F5hrお疲れ様でした。
参考
1.音声認識と文字起こしの統合と使用
2.音声認識を使用したコマンドの実行
- MRTK.HoloLens2.Unity.Tutorials.Assets.GettingStarted.2.3.0.3.unitypackage
- MRTK.HoloLens2.Unity.Tutorials.Assets.AzureSpeechServices.2.3.0.0.unitypackage
- 投稿日:2020-12-12T21:05:33+09:00
既存のアニメーションを自分好みのアニメーションに変えよう!
はじめに
本記事はLife is Tech ! #1 Advent Calendarの12日目の記事になります。
Unityメンターのふくたんです。
今回の記事は結構狭い話になるとは思いますが、最後まで見てもらえると嬉しいです〜
Unityを触っている人で、3Dのゲームを作ってる時にアニメーション選定とかで結構頭抱えることが多いと思います。
Unityを使ってゲーム開発をしている人は結構共感してくれると思うのですが、
「こんなニッチなアニメーションは自作でしかできん...」とか
「このアニメーションにこの動きが少しあるだけで理想のアニメーションなんだよなぁ...」とか思うケースがあります。
そうなったときの対処法として、
- 別のアニメーションで代用する
- 圧倒的マネーパワー
- モデリングソフトに入れて根気よく1から自作でアニメーションを作る
とかで解決に持っていくのが大半じゃないでしょうか?
そう思っている中で、研究室のプロジェクトにおいて、実際にその状況にブチ当たりました。
どんな状況かというと、自分の両手の位置とユニティちゃんの両手の位置が連動し、ユニティちゃんがあたかも自分をくすぐっているのを表現させるといった感じです。
くすぐっているイメージは以下の動画を見ていただいたら。正直これを要求されたときは、少し頭を抱えましたが、これはPreview版PackageのAnimation Riggingを使うしかないと思いAnimation Riggingを使って実装しました。
その際にAnimation Riggingの素晴らしさに気づいたのでそれに関する記事を今回書けたらなと思います。
Animation Riggingとは
Animation Riggingとは、ゲームの再生中でも動いているアニメーションをプログラマブルに変更が可能におり、既存のアニメーションに対して動かしたい動きをブレンドできたりします。
例えば、このようなユニティちゃんの歩くアニメーションがデフォルトで入っていると思うのですが
Animation Riggingを使えば、こんな感じで周りを見渡しながら歩くアニメーションを作ることができます。
実はデフォルトの歩くアニメーションに対して設定を混ぜ合わせることで周りを見渡しながら歩くような自作アニメーションになっています。
今回はこいつの作り方を話しつつ、応用できる場面とか話せたらなと思います!
実行環境
- Unity2019.3.15f
- Animation Rigging preview-0.2.7
※Unity2019.1から使用できるのですが、僕自身がこのプロダクトを作成する時にUnity2019.3を使用したので、Unity2019.3で説明します!予めご了承を!
実際に作ってみましょ〜
Animation Riggingのパッケージのインストール
まず、Animation Riggingのパッケージをインストールしていきます。
上タブにある Window / PackageManager を押してこのようなWindowを開いてください。
ただ、このままだと左の一覧にAnimation Riggingは出てこないので、出てくるように設定しましょう。
上にある Advanced ボタンから Show preview packages を選択してください。
すると、こんな感じでAnimation Riggingが出てくるので、右下に install があるのでそれを押してインストールをしてください。
するとインストール完了です。
あとは、ユニティちゃんを使うのですが、Asset Storeからユニティちゃんのモデルをimportしてください!
import方法に関しては、Asset StoreからのAssetの入れ方と同じなので、割愛させてもらいます〜環境がセットアップできたら次はモデルの配置です。
モデルの設定方法
モデルの配置
下の画像のようにCubeとユニティちゃんを配置してあげてください。
↓MainCameraから見た映像
配置が終わったらユニティちゃんのコンポーネントを設定します。
デフォルトでアタッチされてる IdleChanger・FaceUpdate・AutoBlink はいらないので削除または、チェックを外してください。
Boneの設定方法
unitychanの既存のアニメーションに動きをブレンドさせるBoneを設定していきます。
unitychanのGameObjectにRigBuilderとBoneRendererを入れます。
Componentの補足説明
RigBuilderとは
RiguBuilderComponentはAnimatorComponentを持つゲームオブジェクトに入れることで、既存のAnimationに入れたい動きを混ぜ合わせるのを許可する感じです。
なので複数の動きを入れたい場合は、RigLayersに混ぜ合わせたい動きをいくつか入れていきます。
BoneRendererとは
BoneRendererComponentを入れることで、モデルの骨格を視覚化することができます。BoneRendererを設定することでどの骨格を動かしたいのかがわかりやすくなるので入れてください。
今回は腰骨を回転させることで周りを見渡すので、腰骨のBoneを見えるようにしていきます。
unitychanのGameObjectの子オブジェクトに腰骨を位置するオブジェクトがこんな感じで設定されます。
この3つをBoneRendererのTransformsに入れます。こんな感じで。
すると腰骨の部分がScene上で可視化されます。
これだと見にくいとおもうので、BoneRendererのBoneSize・Shape・Colorを変更することでより見やすくなります。
色とか緑にするとより見やすくなります。
Rigの設定方法
ここからはRigを設定してきます。
Rigとは、混ぜ合わせたい動きを設定するイメージを持ってもらって大丈夫です。unitychanのGameObjectのすぐ下の階層にSpineRigといった空のGameObjectを作成してください。
その下の階層にOffSetという名前の空のGameObjectを作成してください。
次にSpineRigにRigComponentをアタッチします。
Componentの補足説明
Rigとは
RigComponentを入れることで、混ぜ合わせたい動きの重み付けを設定できます。
さらにOffSetのGameObjectにOverrideTransformComponentをアタッチしてください。
OverrideTransformComponentとは
OverrideTransformComponentを設定することで、Animationに依存されたGameObjectのTransformを上書きすることができます。
つまりこの上書きさせるGameObjectを腰骨に割り当てることで、このOverrideTransformがアタッチされたGameObjectを回転すると腰骨も回転されるといった仕組みになっています。
このように動きを既存のアニメーションに反映させるComponentはいくつかあって、Constraint Componentsと呼ばれています。Constranit Componentsには位置や回転の値をブレンドできるのはもちろん、ある一定方向を向くように設定できるComponentも存在してたりします。
詳しくは公式マニュアルを見てもらうとイメージがつくのではないかと思います。
では、あたりを見渡すように腰骨を回転させる設定をしていきましょう。
OverrideTransformをこのように設定してください。
変更したのは、
- Constrained Object
- SourceObjects / Override Source
- Settings / Position Weight
の3つを変更しました。
それぞれがどのような役割を果たしているのかは以下に説明します。
プロパティ 概要 Weight 反映させる動きの重み付け。0にすると反映させなくなります Constrained Object 上書きさせられるTransform(今回でいうと腰骨) SourceObjects / Override Source 動きを上書きさせる元のGameObjectのTransform Settings / Space 上書きする座標軸の設定 (World / Local / Pivot) Settings / Position Weight 上書きさせる値の重み付け(位置) Settings / Rotation Weight 上書きさせる値の重み付け(回転) つまりこのOffSetを回転させることで連動して腰骨が回転します。
ただ、このままだと回転させるオブジェクトがどこにあってどれくらい回転しているのか視覚的にわからないのでわかるように設定します。RigEffectorの設定
Scene上でOffSetを視覚化するためにRig Effectorを設定します。
SceneViewの右端にこのようなUIがあると思うので、+ボタンを押してください。ただ、Effectorは足もとに下図のように表示されていて非常に見にくいです。
なのでこのEffectorをより見やすくします。先ほどEffectorを設定したUIの画面のShapeをLocalEffector → BoxEffectorに設定してください。
Effectorの動きを背骨にブレンド
unitychanのGameObjectにアタッチされたRigBuilderComponentの中のRigLayersにSpineRigを入れます。
これを行うことでEffectorの回転に合わせて、腰骨が回転するようになりました。
ユニティちゃんのアニメーションの設定
ユニティちゃんのアニメーションを新しく設定していきます。
新しくAnimation Controllerを作成し、Animation Controller内の遷移をこのようにしてください。
WALK00_FのStateは
Assets/unity-chan!/Unity-chan!Model/Art/Animations/unitychan_WALK00_F
にあります。
この設定したAnimation ControllerをunitychanのAnimatorのControllerの中に入れてください。
その状態でゲームを起動するとユニティちゃんがこのような歩くモーションになります。
この状態でOffSetのEffectorを回転させるとこんな感じであたりを見渡すような動きになります。
足も含めて体全体が回転せずに良い感じですね!
つまり、このOffSetのエフェクターをAnimationなりスクリプトなりでY軸
回転させれば自動的に腰骨が回転するようになります。
こんな感じのAnimationをOffSetにアニメーションとして反映させると周りを見ながら歩いている感じになると思います。
ユニティちゃんを前に歩かせる
最後にユニティちゃんを前に歩かせて終わりですね。
以下のスクリプトをunitychanのGameObjectにアタッチすると良い感じにユニティちゃんが前に歩いてきます。UnitychanWalk.csusing UnityEngine; public class UnitychanWalk : MonoBehaviour { private void OnAnimatorMove() { transform.position += transform.forward * Time.deltaTime; } }これをスクリプト等で応用してもらえばこんな感じで、Targetを見つけたら走り出すとか
可能性は無限大ですね〜
ちなみに完成プロジェクトはこちらにあげておくので、興味があったら是非ダウンロードしてみてくだされ〜
最後に
いかがだったでしょうか?
僕自身一昔前まではUnityはアニメーション関連にすごく弱いイメージがあったのですが、このAnimation Riggingの登場によって
簡単でよりリッチな作品を作れる可能性が広がった
と思っています。
今回紹介したもの以外も様々な機能があり、それを駆使すれば剣を振りかざすアニメーションや銃を撃つ反動も表現できたりします。
つまり、「あのアニメーションはそこらへんに転がってないし諦めよ」とか「細かなアニメーションはなしで良いでしょ」という考えは徐々になくなる気配がしています!ぜひみなさんもLet's make animation!!ですね。
さて、明日はかすぴー大先生が"深層強化学習"についてお話をしてくださるそうです。笑
字面のエグさが際立ってますね。笑引き続きLife is Tech!Advent Calendarをお楽しみに!
参考記事
この記事はユニティちゃんライセンス条項のもとに提供されています。
- 投稿日:2020-12-12T20:07:45+09:00
【Unity】Timelineからデバイスの振動を制御する【イージング付き】
はじめに
この記事はUnity Advent Calendar 2020 12日目の記事です。昨日は@__poosukeさんの『Unityから考えるDI』の記事でした。
先日リリースしたVRゲーム『ALTDEUS: Beyond Chronos(アルトデウス: ビヨンドクロノス)』というゲームでは、一部シーンでTimelineを使った演出があります。そのシーンでは臨場感を表現するためデバイスの振動を組み合わせているのですが、これをTimelineを作成するアニメーターさんが扱えるようにしたところ、非常に素晴らしい体験が出来上がりました。
今回はVRのハンドコントローラー(特にOculus)を例に、その方法を紹介したいと思います。
環境
- Unity 2019.2.21f1
- Timeline 1.1.0
- DOTween v1.2.335
Markerを使ってTimeline上からメソッドを呼び出す
Marker機能を使うと、Timelineのシークエンス上にトリガー(下図の逆ティアドロップみたいな形のやつ)を設置し、そこから任意の処理を呼ぶことができます。
参考:【Unity】Timelineからメソッドを呼ぶ新機能 「Marker」と「Signal、Signal Receiver」 - テラシュールブログ
詳しくは上の記事で丁寧に説明されているので割愛しますが、大体こんな感じのコードを書いています。Timelineが該当のマーカーに到達すると
INotificationReceiverを継承したMonoBehaviourにイベントが発行されるので、その中で具体的な処理(今回は振動)を書くイメージです。VibrateMarker.cs[System.Serializable, DisplayName ("振動マーカー")] public class VibrateMarker : Marker, INotification { public VRDefine.HandType HandType = VRDefine.HandType.Both; public float Duration = 0.5f; [Range (0f, 1f)] public float Power = 0.5f; [Range (0f, 1f)] public float Frequency = 0.5f; public PropertyName id => new PropertyName ("method"); }Timeline上からいい感じに振動パターンを指定する
次に振動のパターンを細かく指定できるようにしていきます。先ほどの
VibrateMarkerを見ていただくと分かるように、Inspectorで値の指定が可能になっています。この辺りがSignalとの違いでしょうか。そこで今回は以下のパラメータを実装しました。
- (Time: Marker標準のパラメータ、開始位置が秒/フレーム数として表示されている)
- Hand Type: 対象となるVRハンドコントローラーを指定する独自のenum(Both, Left, Rightがある)
- Duration: 持続する秒数
- Power: 振動の強さ(振幅)
- Frequency: 振動の細かさ(周波数)
- Should Easing: イージングの有無
- Power Ease: 振動の強さに対するイージングの種類
コードはこんな感じ
VibrateMarker.csusing System.ComponentModel; using DG.Tweening; using MyDearest.Platform; using UnityEngine; using UnityEngine.Playables; using UnityEngine.Timeline; [System.Serializable, DisplayName ("振動マーカー")] public class VibrateMarker : Marker, INotification { public VRDefine.HandType HandType = VRDefine.HandType.Both; public float Duration = 0.5f; [Range (0f, 1f)] public float Power = 0.5f; [Range (0f, 1f)] public float Frequency = 0.5f; [Space (20), Header ("↓ shouldEasing が true の時のみ 有効")] public bool shouldEasing = false; public Ease PowerEase = Ease.Linear; public PropertyName id => new PropertyName ("method"); }次に、これを受け取る側の処理です。一部抜粋することこんな感じ。
TimelineController.cspublic sealed class TimelineController : MonoBehaviour, INotificationReceiver { public void OnNotify (Playable origin, INotification notification, object context) { var element = notification as VibrateMarker; if (element == null) return; var time = element.time - origin.GetTime (); Vibrate (element.HandType, element.shouldEasing, element.Duration, element.Power, element.Frequency, element.PowerEase); } }Markerが呼ばれると
OnNotifyというイベント関数が発火します。ただしどのマーカーであるかはわからないので、asを使って型の変換を試みて、VibrateMarkerであれば処理を続けるという分岐を書いています。マーカーが複数ある場合はisとかで処理を分けることになる気がします。振動の処理は部分はこんな感じです。複数プラットフォームの対応をするため、ラッパーを呼んでいます。
public void VibrateCommand (VRDefine.HandType handType, float duration, float power, float frequency) { VRPlatformManager.Platform.Vibrate (handType, duration, power, frequency); }勘のいいひとはお気づきかもしれませんが、Oculusの振動メソッド
OVRInput.SetControllerVibration (frequency, amplitude, controller)には継続時間の指定がありません。なので、VRPlatformManager.Platform.Vibrate()の内側で以下のような処理を呼んで疑似的に実現しています。(2.0秒間振動続けると自動で停止する仕様があるため、2秒ごとにコールしなおしています)IEnumerator VibrateCoroutine (VRDefine.HandType hand, float length, float amplitude, float frequency) { var lastSecond = length; while (lastSecond > 0f) { SetControllerVibration (hand, amplitude, frequency); var waitTime = Mathf.Min (lastSecond, 2.0f); yield return new WaitForSeconds (waitTime); lastSecond -= waitTime; } StopControllerVibration (hand); } void StopControllerVibration (VRDefine.HandType hand) { SetControllerVibration (hand, 0, 0); } void SetControllerVibration (VRDefine.HandType hand, float amplitude, float frequency) { var controller = OVRInput.Controller.None; if (hand == VRDefine.HandType.Left) controller = OVRInput.Controller.LTouch; if (hand == VRDefine.HandType.Right) controller = OVRInput.Controller.RTouch; OVRInput.SetControllerVibration (frequency, amplitude, controller); }
TimelineControllerの全文は以下です。両手別々にイージング処理をしている関係で冗長になっていますが、大した処理はしていません。TimelineController.csusing System.Collections.Generic; using DG.Tweening; using MyDearest.Platform; using UnityEngine; using UnityEngine.Playables; public sealed class TimelineController : MonoBehaviour, INotificationReceiver { private Dictionary<VRDefine.HandType, Tween> tweens = new Dictionary<VRDefine.HandType, Tween> { {VRDefine.HandType.Right, null}, {VRDefine.HandType.Left, null} }; private Dictionary<VRDefine.HandType, float> LastPowers = new Dictionary<VRDefine.HandType, float> { {VRDefine.HandType.Right, 0f}, {VRDefine.HandType.Left, 0f} }; public void OnNotify (Playable origin, INotification notification, object context) { var element = notification as VibrateMarker; if (element == null) return; var time = element.time - origin.GetTime (); // 誤差1秒以内の発火であれば実行 if (Mathf.Abs ((float)time) < 1f) { Vibrate (element.HandType, element.shouldEasing, element.Duration, element.Power, element.Frequency, element.PowerEase); } } void Vibrate (VRDefine.HandType handType, bool shouldEasing, float duration, float power, float frequency, Ease powerEase) { if (handType == VRDefine.HandType.None) { return; } if (handType == VRDefine.HandType.Both) { Vibrate (VRDefine.HandType.Right, shouldEasing, duration, power, frequency, powerEase); Vibrate (VRDefine.HandType.Left, shouldEasing, duration, power, frequency, powerEase); return; } // 前のやつは殺す if (tweens[handType] != null) { tweens[handType].Kill (); tweens[handType] = null; } if (shouldEasing) { tweens[handType] = DOVirtual.Float ( LastPowers[handType], power, duration, v => { VibrateCommand (handType, Time.deltaTime, v, frequency); LastPowers[handType] = v; } ).SetEase (powerEase).OnComplete (() => LastPowers[handType] = 0f); } else { VibrateCommand (handType, duration, power, frequency); LastPowers[handType] = power; tweens[handType] = DOVirtual.DelayedCall (duration, () => LastPowers[handType] = 0f); } } public void VibrateCommand (VRDefine.HandType handType, float duration, float power, float frequency) { VRPlatformManager.Platform.Vibrate (handType, duration, power, frequency); } }ポイントは
LastPowersで最後の振動の強さを保持しています。イージング処理をする場合は終点を指定する形になるので前回の値が必要になるのです。前回の振動中に呼ばれた場合はその時点から上書きされますが、一度振動が終わってからイージングが呼ばれる場合もあるので、終了時点で.OnComplete (() => LastPowers[handType] = 0f);を呼ぶことを忘れないようにします。最後に
やっていることはシンプルですが、組み合わせるとかなり強力なエディタ機能になったと感じました。よかったら参考にしてみてください!
最後に宣伝ですが、VRゲーム『 ALTDEUS: Beyond Chronos(アルトデウス: ビヨンドクロノス)』では今回の技術を活用した臨場感あふれるマシンアクションを楽しめます。興味がある方はぜひやってみてください!
さて、Unity Advent Calendar 2020 13日目の記事は@yunodaさんの『初心者から始めるハイパーカジュアルゲームの作り方』です。こちらもお楽しみに。
- 投稿日:2020-12-12T19:35:25+09:00
意識低い系がプロダクト公開するときにデザインを誤魔化すために入れるUnityAssetとノウハウ
プロダクトを公開するのは割とメンドイ
最近、VZeroというプロダクトを公開しました(VRMでブラウザで動く多人数オープンワールドゲームVZeroを作った)が、それに伴って、めんどくせぇ。と思う作業が色々ありました。
基本的には、VZeroは技術調査であって、デザインはこだわらない。というスタンスなのですが、まぁ。世の中、見た目から入られるので、どうやっても30点か、40点ぐらいの見た目は確保する必要があります。この記事は、30点、40点の見た目を確保するための意識低い系のUnityAssetとノウハウの紹介です。
あ、そういう人なので、全体の見た目のクオリティが低いのはご了承ください。
作例紹介
今回紹介するアセットを組み合わせると以下のような絵が作れます。自分が趣味で作った作例です。
サンプルシーン
とてもUnity臭のするシーンです。これを改良しています。
SkyBoxを設定する
とりあえずデフォルトのSkyBoxを回避します。
AllSky Free - 10 Sky / Skybox Set
今回使ったのは、AllSkyというAssetのFree版です。有料版もあるんですが、無料版でも、5,6個綺麗な画像が入っているので、適当に選べばいいと思います。
Material+ReflectionProbeを設定する
Real MaterialsというAssetのマテリアルを入れます。アルミやガラス。スチールなど色々リアルの物体を模したマテリアルが入っているので、リアル寄りだと結構便利です。今回は、SphereにAluminium、CubeにMarble Polished Whiteを設定してます。
これだとあんまりきれいじゃないですよね?ここで、環境光を設定します。Reflection Probeというものを設定します。
Unityのライティング「Reflection Probe」を使用して反射を表現する
このReflection Probeを設定すると、印象がグッと変わります。とりあえず上の記事通りやると綺麗になります。
とりあえず水を張る
とりあえず水張っておきます。今回、入れたのはVRWaterShaderというアセットです。もともとのPlaneのマテリアルに、Middle Waterというマテリアルを付けただけです。
こいつもReflection Probeの効果が強く、割と万能感あります。ちなみにReflection Probeを切るとこんな感じ。
味気ないですね。なんとなく思っていることとして、
- 高品質で書き込まれている情報量の多い背景
- 物体に反射するマテリアル
- 水(反射があり予測不能な動きのあるもの)
- 環境光(ReflectionProbe)
あたりが鉄板かな。と思います。多分プロは情報量を引くことが上手でいい感じになると思うのですが、素人なので、とりあえず情報量モリモリで見てる人の脳みそをオーバーフローさせる方が綺麗と錯覚させられる気がします。知らんけど。
撮影効果を付ける
撮影効果というと、Post Processing Stack v2が強いと思います。ただ公式のドキュメントはわかりにくいなぁ。と思うので以下のドキュメントが良いです。
Unity ルックデヴ講座 Post Processing Stack v2編
今回、やってみて思ったのですが、自分の演出の引き出しが少ないと、どれも似たような感じの絵になるので、ちゃんと使いこなすには難しいなぁ。という印象です。たぶん、上の絵もどっかで見たことある感が強いと思います。
小物がめんどくさい
ゲームやプロダクトを作る際、必要なんだけど、用意するのめんどくせぇ。
と思うものが結構あります。どちらかというとこちらの方が有用かも。
とりあえずアイコンが欲しい
UnityAssetStoreの画像を引用するとこんな感じです。
プロダクトにおいて、"音声のミュート"とか、"設定ウィンドウを出す"とかそういう機能はありがちだと思います。しかし、それをテキストとボタンで"Mute"とか"Settings"とかでやると、え。ダサい。アイコンにしてよ。みたいなことを言われると思います。いや、モックだし?本番にはちゃんとするし?まだデザイン素材上がってこないし?みたいなことを言っても、それしか言わないメンドクサイ人が居るので、雑に逃げるのに導入が楽で豊富な絵柄がそろっているのでGoodなアセットです。
とりあえずSEが欲しい
面セレクトとか、キャラセレクトする際に、"ピロッ"って音欲しいですよね?
アレです。必要なんだけれども、ネットから1つ1つこだわって探して来たり、自分でシンセで作るのも面倒だったりします。なので、そこそこのクオリティがパッキングされてるのが楽です。あと、外部から取り込むとライセンス表記が必要だったりするので、こういうものでスキップする方が工数削減になります。
スマホ用にジョイスティック欲しい
VZeroは一般公開していませんが、Android版が存在します。そのため、移動系のUIを作るのにジョイスティックが必要になりました。ハイ。画像とロジック作るのめんどくさいですね。なので、アセットストアからもらってきます。中身をよくよく見ると何個か入っており、固定表示のジョイスティックだったり、タップしたところから出現するジョイスティックが入ってたりします。その割にファイル数とかも少ないので、取り回しやすかったです。
意識低い系のノウハウ
個人で作るものにデザインとしての統制とかブランディングとか、まぁ別に気にしなくてもいいんじゃねーの?なんか見た目綺麗な方がいいんじゃねーの?
という割と意識低い感じの私が参考にする感じです。1つ勘違いしてほしくないのは、意識が低い人(私)が参考にするのであって、「引用や貼り付けている記事の執筆主や製作者が意識低い系だ。」とは言っていません。
Unity固有系
Unityもコモディティ化しており、あ、この演出の感じ、Unityのデフォルトだよね。みたいなものがばれたときに、クオリティが一段下がって見える。みたいなことがあると思います(実際に低いかは別として)。そういうのを回避するのに便利かな。と思います。
演出のネタ帳
個人的に大好きなのを2つ。
岡崎体育のMusic Video
— こたうち@VTuberエンジニアΩ (@kotauchisunsun) December 12, 2020てさぐれ部活ものOP
— こたうち@VTuberエンジニアΩ (@kotauchisunsun) December 12, 2020こういうちょっと斜に見るコンテンツは結構好きで、逆に勉強になるな。と思います。
Music Videoは確かに、いろんな人のMVで似たような演出みたことある!!って思います。現実の映像だと、大体こんな感じの演出だよね。みたいなのが分かります。てさぐれ部活もののOPは歌詞をちゃんと聞いてみると、すごくあるあるwwwwってなります。なので、演出の一覧みたいな視点で見ると、この2つの動画はすごく役に立つと思います。撮影効果・特殊効果
アニメの撮影効果・特殊効果だとこのサイトが良いです。
用語だけでは解りにくかったので、アニメの撮影処理を実際の使用例を見せつつまとめてみた(GIFあり)
素人だと、「つけパン」とか、「パンアップ」とか、ちょっとだけ用語を知ってる専門用語ってあると思います(私もそう)。じゃあ、それって定義ってどんなんなの?実際に使われている場面は?どういった効果を狙ってるの?作品にこの効果を入れてる合理性はあるの?みたいなゴリゴリした詰めをされると答えに窮すると思います。まぁ私はプロじゃないし?別に私がいいと思ったからそうしただけだし?お前から金もらって作ってるわけじゃないから変える気ないし?って逃げるのもありですが、そういう時に上のサイトを見ると、アニメの場面において、どう効果的に演出が使われてるのか。が分かるので、割と納得感が強いと思います。あと、ここにある演出をProcessingStackとかで再現すると、そこそこ汎用性があって、おもろいんじゃない?みたいなことは思ってます。
ストーリーテリング
演出という面では舞台なんかの技法もあります。例えば、勇者と魔王の決闘という場面を描くときに、勇者は右?左?どっちに描くべきでしょうか?答えは右です。
「漫画の先輩や舞台観劇などから学んだ絵の演出意図」
— かん だんち (@dankoromochi) August 30, 2014
昔ブログに文章で書いたことを簡単に表現できないか、と思って描いてみました。 pic.twitter.com/ri9yZ4KYxeアニメで主人公サイドが(視聴者から見て)右側なのには何か深い理由があるんですか?
別に"絶対に右に書く"という必要はないと思いますが、おそらく日本人が「右に書いているのが主人公。」というのに訓練されすぎているので、そこをひっくり返してまで演出する理由は少ないかなぁと思います。べたに決まってることがあれば、そこは乗る方が聴衆は違和感の少ないものは作れるだろうな。みたいな感覚値はあります。
画面構成
止め絵だと写真のノウハウが割と役に立ちます。
大体、3分割法を意識すればいいのかなぁ。と思ってます。前に貼った作例が、とりあえず真ん中には寄せない。三分割のライン上に乗せる。とか、そういうのを意識しています。
定型のパターンが決まってるので、逆にこれ以上綺麗にするのは、素人だと無理だと思うので、プロでもない限り定型を模倣する方がコスパは良いと思います。感想
VZeroを作ってるときに、「ひたすらめんどくせぇな・・・」と思うことがありました。それが、「小物がめんどくさい」の内容です。別に自分が適当に使う分にはこだわらないけど、自分以外の他人が使うことを考えると、入れとかないとなーとか思うと、入れざるをえなかったりします。でも、多分こういう話ってどこにでもあって、興味のあるとこ以外やりたくない。ってまぁまぁあると思います。最近、見た中で納得感の強かったのが、「AWS で独自の自然言語モデルを構築する」にあった、
「Undifferentiated Heavy Lifting」(差別化につながらない重労働)
という言葉です。個人的には割とピンとくる感じがありました。いや、違うだろ。ゲームにおいて、見た目やUXってのはコアであり、そこに投資しないのは信じられない!と思う人もいます。まぁ、そういう人は頑張ってください。というのが私のスタンスで、80点90点のクオリティにいつまでも到達できず、リリースできないぐらいだったら、適当に30点40点で出せばいいやん?アマチュアなんだし。みたいなことは思います。本当にお金を稼ぐつもりのプロダクトでは別ですが。
というわけで、自分が時間かけて50点ぐらいしか稼げないのであれば、アセット使って短時間で30点,40点稼ぐのもまぁありなんじゃない?今回のやつは無料だし。と思うので、やってみればいいと思います。この内容全部追って、実際に手を動かすのすら出来ないのに、クオリティが大事。みたいなことを言う人もいると思うので、やっただけえらい!公開したらもっとえらい!って私は思います。べたを真似れるのも一技術だと思います。なので、なんかQiitaでUnityのめんどくさいのを雑に楽に解決する記事があったな・・・と思ったときに思い出してもらえれば幸いです。
- 投稿日:2020-12-12T18:56:10+09:00
Unity ML-Agentsでテトリスを学習させてみる
「QualiArts Advent Calendar 2020」 13日目の記事になります。
昨日の12日目の記事は塩塚くんによる「アセットバリデーション用のBlenderプラグインを作ってみる」です。興味がある方はこちらもどうぞ。はじめに
はじめまして、Unityエンジニアの山下といいます。この記事では、テトリスに対し、Unity ML-Agentsを用いて強化学習をしてみたことについて説明します。
テトリスは、皆さんご存知かと思いますが有名な落ち物パズルゲームです。最近、ぷよテトやテトリス99で遊んでいて自分で遊ぶちょうどいいレベルの相手を作れないかと思い、お題に選んでみました。
以下、自作してみたテトリスの画面。
落下、左右への移動、回転、列消し程度は実装してあります。T-SpinやRENの実装は一旦要らないかと思い、実装してありません。
Unity ML-Agentsとは
Unity ML-Agentsとは、Unityが提供している機会学習用のフレームワークです。AIに行動の選択肢と現在の状況、適度な報酬を与えることによって学習を行わせ、自動的に操作を行わせることができるようになります。
詳細な内容はこちらの本が日本語で詳しく書いてあるのですが、ML-Agents v0.4当時のものなので少し古いです。Agentの作り方は今でもあまり変わらないので参考になると思います。
Unity ML-Agents実践ゲームプログラミングこちらの記事ではRelease10を使用しました。Unity ML-Agents Release10
また、Unityは2018.4.30f1を利用しました。
この他、pythonの実行環境が必要です。python3.7と、pipでmlagentsパッケージをインストールしました。学習方法の詳細
学習させるに当たって、AIに行わせる操作と、AIに渡す観測情報、報酬を考える必要があります。
まず操作についてですが、基本的なテトリスで行えるアクションは左移動、右移動、下移動、左回転、右回転の5つです。(さらに言うならハードドロップやホールドもありますが今回は省略しています)
移動と回転は別軸として考えても良かったのですが、移動と回転を同時に行わないものとしてアクションは1次元でどれかしか選べないようにしてみました。離散値なのでDiscrete、何もしないを含めて1次元でサイズ6までです。
次に観測ですが、テトリスの盤面情報をAIに簡単に渡そうと思って、Visual Observationを使いました。CameraSensorコンポーネントを利用して、画像情報で現在の盤面を渡しています。Unity ML-Agentsではこの他にVector Observationというのがあって、オブジェクトの位置や、回転、盤面等をベクトル情報として渡すこともできます。
最後に報酬です。Agentクラスを継承したTetorisAgentクラスを作り、そこで上記の操作、観測と共に報酬の実装を行います。
列を消したら+1、ゲームオーバーで-1、そして毎ステップごとに-0.001という設定をしました。ゲームオーバーにならずに、列を消せていれば報酬が増えるはずなので、うまくいけばいい感じに列を消してくれそうです。実行
上記の設定で5万ステップほど学習させてみました。
ターミナルで以下のコマンドを実行させると# mlagents-learn config/ppo/tetoris.yaml --run-id=tetris以下のような画面が出てくるはずなので、この状態でUnityを実行するとUnity側で勝手に学習を進めてくれます。
今回の環境では5万ステップで16分ほどかかりました。結果
こちらが学習させたもの。意外と全然列を消す動きをしないですね...。報酬の情報を見直せばもう少しまともになりそうな気がします。例えば、前のステップより積んである分の高さが増えてたらマイナス等でしょうか。
また、時間がある時にでも挑戦してみようと思います。
ここまで読んでいただきありがとうございました!
- 投稿日:2020-12-12T16:03:31+09:00
C# Attributeを利用した、お手軽!CSVの入出力クラスを作ろう!
お久しぶりです.
株式会社グレンジでエンジニアマネージャー兼クライアントエンジニアのmesshiです.
(CyberAgentグループ、ゲーム事業部の子会社です)さて、今年もAdventCalendarの時期がやってきたので、ひょっこり投稿しておこうかなと思います.
まえおき
今回取り上げる記事は、次のようなニーズがある方に役立つかと思います.
・プロダクトへ入れるソースコードは最低限にしたい (ThirdParty製を入れたくない)
・入出力部分のメンテナンスはしたくない
・デバッグ用のコードなので、多少パフォーマンス悪くても良い私の利用用途は少し特殊で、ゲーム事業部の各子会社のチューニングを協力させてもらう事が多いのですが
チューニングのイテレーションをガンガン回すために、次のようなワークフローを作ります.
「①実機計測」→「②計測結果をCsvに書き出し」→「③Csvを送信」→「④データとして取り込み可視化」その際、Csvに関する部分に関して、上記のようなニーズがありました.
では、前置きが長くなりましたが、早速作っていきましょう!
Attributeを定義する
AttributeにCsvの入出力に必要なメタ情報を持たせます.
ヘッダー名、ヘッダーの順番さえあれば事足りるでしょう.
それを定義したクラスを生成します.SimapleCsvAttribute.cs[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class SimpleCsvAttribute : Attribute { #region variable /// 列順 private int _order; #endregion #region Property /// 列順 public int Order => _order; /// 名前 public string Name { get; set; } #endregion #region method public SimpleCsvAttribute(int order) { _order = order; } #endregion }AttributeUsageは、Attributeを付与できる対象のことです.
今回はProperty属性をしていますAllowMultipleは複数のAttributeを設定できるかどうかです
デフォルトfalseなので指定しなくてもOKですデータを定義する
プロパティでCsvに書き出すデータを定義します.
ProfileData.cs/// <summary> /// ProfileData /// </summary> public class ProfileData { [SimpleCsv(1)] public string Name { get; set; } [SimpleCsv(2, Name = "処理時間")] public int Time { get; set; } }[SimpleCsv]のAttributeを付けなければ書き出しが行われません.
Nameの指定をしなかった場合は、プロパティの変数名をそのまま使用します書き込みクラスを定義する
コンストラクタでGenericに指定されたクラスから、Attributeの情報を抜き出し、メンバ変数のリストに格納します.
あとは、そのAttributeの情報からデータを書き込むだけです.SimpleCsvWriter.csusing System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using System.Linq; using System.Reflection; using System.Text; /// <summary> /// SimpleCsvWriter /// </summary> public class SimpleCsvWriter<T> : IDisposable where T : class, new() { #region define /// 区切り文字 private const string DELIMITER = ","; /// 引用符の正規表現 private const string QUOTE_REGEX = "[\"\\r\\n,]"; /// 属性プロパティ情報 public class AttributePropertyInfo { public PropertyInfo Property; public SimpleCsvAttribute CsvAttribute; } /// 値取得のRegex private static readonly Regex VALUE_REGEX = new Regex(QUOTE_REGEX); /// ファイルのエンコードタイプ private static readonly Encoding FILE_ENCODING = Encoding.UTF8; #endregion #region variable /// StreamWriter private StreamWriter _writer; /// 属性の情報リスト private List<AttributePropertyInfo> _attirbuteInfos; /// データリスト private T[] _records; #endregion #region method /// <summary> /// コンストラクタ /// </summary> public SimpleCsvWriter(string directory, string fileName, T[] records) { if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } _records = records; var filePath = directory + Path.DirectorySeparatorChar + fileName + ".csv"; _writer = new StreamWriter(filePath, false, FILE_ENCODING); var targetType = GetType().GetGenericArguments()[0]; _attirbuteInfos = targetType .GetProperties() .Select(x => new AttributePropertyInfo() { Property = x, CsvAttribute = x.GetCustomAttributes(typeof(SimpleCsvAttribute), false).FirstOrDefault() as SimpleCsvAttribute }) .Where(x => x.CsvAttribute != null) .OrderBy(x => x.CsvAttribute.Order) .ToList(); } /// <summary> /// 書き込む /// </summary> public void Write() { WriteHeader(); for (int i = 0; i < _records.Length; i++) { WriteLine(_records[i]); } } /// <summary> /// ヘッダーを書き込む /// </summary> private void WriteHeader() { var headers = _attirbuteInfos.Select(x => x.CsvAttribute.Name ?? x.Property.Name) .Select(x => Quote(x)) .ToArray(); _writer.WriteLine(string.Join(DELIMITER, headers)); } /// <summary> /// 1行書き込む /// </summary> private void WriteLine(T record) { var values = _attirbuteInfos.Select(x => x.Property.GetValue(record)) .Select(x => Quote(x)) .ToArray(); _writer.WriteLine(string.Join(DELIMITER, values)); } /// <summary> /// 引用符を変換する (コンマやダブルクォーテーションなど) /// </summary> private string Quote(object value) { string target = value != null ? value.ToString() : ""; if (VALUE_REGEX.Match(target).Success) { return "\"" + target.Replace("\"", "\"\"") + "\""; } else { return target; } } /// <summary> /// 破棄処理 /// </summary> public void Dispose() { _writer.Dispose(); } #endregion }読み込みクラスを定義する
書き込みとほぼ同じ要領で行います
SimpleCsvReader.csusing System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using System.Linq; using System.Reflection; using System.Text; /// <summary> /// SimpleCsvReader /// </summary> public class SimpleCsvReader<T> : IDisposable where T : class, new() { #region define /// 引用符 private const string QUOTE = "\""; /// 改行正規表現 private const string NEW_LINE_PATTERN = "(?:\x0D\x0A|[\x0D\x0A])?$"; /// 区切り正規表現 private const string DELIMITER_PATTERN = "(\"[^\"]*(?:\"\"[^\"]*)*\"|[^,]*),"; /// 属性プロパティ情報 public class AttributePropertyInfo { public PropertyInfo Property; public SimpleCsvAttribute CsvAttribute; } /// 引用符の正規表現 private static readonly Regex QUOTE_REGEX = new Regex(QUOTE); /// 改行コードの正規表現 private static readonly Regex NEW_LINE_REGEX = new Regex(NEW_LINE_PATTERN, RegexOptions.Singleline); /// 区切り文字の正規表現 private static readonly Regex DELIMITER_REGEX = new Regex(DELIMITER_PATTERN); /// ファイルのエンコードタイプ private static readonly Encoding FILE_ENCODING = Encoding.UTF8; #endregion #region variable /// StreamReader private StreamReader _reader; /// 属性の情報リスト private List<AttributePropertyInfo> _attirbuteInfos; /// データリスト private T[] _records; #endregion #region method /// <summary> /// コンストラクタ /// </summary> public SimpleCsvReader(string directory, string fileName) { if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } var filePath = directory + Path.DirectorySeparatorChar + fileName + ".csv"; try { _reader = new StreamReader(filePath, FILE_ENCODING); } catch (Exception ex) { throw ex; } var targetType = GetType().GetGenericArguments()[0]; _attirbuteInfos = targetType .GetProperties() .Select(x => new AttributePropertyInfo() { Property = x, CsvAttribute = x.GetCustomAttributes(typeof(SimpleCsvAttribute), false).FirstOrDefault() as SimpleCsvAttribute }) .Where(x => x.CsvAttribute != null) .OrderBy(x => x.CsvAttribute.Order) .ToList(); } /// <summary> /// 読み込み /// </summary> public T[] Read(bool hasHeader = true) { if (_reader == null) { return null; } if (hasHeader) { // ヘッダー分だけ進める _reader.ReadLine(); } List<T> dataList = new List<T>(); while (!_reader.EndOfStream) { var line = _reader.ReadLine(); if (line == null) { break; } // 改行を考慮して行を読み込む while (!HasEnoughQuote(line)) { line += "\n" + _reader.ReadLine(); if (_reader.EndOfStream) { break; } } // 改行コードを排除する line = NEW_LINE_REGEX.Replace(line, ""); // 要素分解を行う line += ","; var matches = DELIMITER_REGEX.Matches(line); var columns = matches.Cast<Match>() .Select(x => Dequote(x)) .ToArray(); // データを作成する var data = new T(); for (int i = 0; i < columns.Length; i++) { var attribute = _attirbuteInfos[i]; attribute.Property.SetValue(data, Convert.ChangeType(columns[i], attribute.Property.PropertyType)); } dataList.Add(data); } return dataList.ToArray(); } /// <summary> /// バイト配列で読み込む /// </summary> public byte[] ReadBytes() { if (_reader == null) { return null; } byte[] readBytes = null; using (MemoryStream memoryStream = new MemoryStream()) { _reader.BaseStream.CopyTo(memoryStream); readBytes = memoryStream.ToArray(); } return readBytes; } /// <summary> /// 引用符が十分であるかどうか /// </summary> private bool HasEnoughQuote(string line) { return (QUOTE_REGEX.Matches(line).Count % 2) == 0; } /// <summary> /// 引用符を変換する /// </summary> private string Dequote(Match match) { var s = match.Groups[1].Value; var quoted = Regex.Match(s, "^\"(.*)\"$", RegexOptions.Singleline); if (quoted.Success) { return quoted.Groups[1].Value.Replace("\"\"", "\""); } else { return s; } } /// <summary> /// 破棄処理 /// </summary> public void Dispose() { if (_reader != null) { _reader.Dispose(); } } #endregion }ReadByteのメソッドは不要ですが、Csvをポストする際にByte配列にする必要があったので付け加えているだけです.
以上で、必要なクラスの定義は終了です.利用例
では、実際に使ってみましょう.
Attributeを利用したデータクラスを作成し、SimpleCsvWriterにGenricで渡すだけです.ProfileTest.csusing System.IO; using System.Collections.Generic; using UnityEngine; /// <summary> /// ProfileTest /// </summary> public class ProfileTest : MonoBehaviour { #region define /// <summary> /// ProfileData /// </summary> public class ProfileData { [SimpleCsv(1)] public string Name { get; set; } [SimpleCsv(2, Name = "処理時間")] public int Time { get; set; } } #endregion #region method /// <summary> /// データを書き込む /// </summary> public void Write() { // 適当なテスト用のデータ作成 var list = new List<ProfileData>(10); for (int i = 0; i < 10; i++) { list.Add(new ProfileData() { Name = i.ToString(), Time = (i * i) }); } // 書き込み部分 var directory = Application.persistentDataPath + Path.DirectorySeparatorChar + "Exports"; using (var writer = new SimpleCsvWriter<ProfileData>(directory, "test", list.ToArray())) { writer.Write(); } } /// <summary> /// データを読み込む /// </summary> public ProfileData[] Read() { ProfileData[] profiles = null; var directory = Application.persistentDataPath + Path.DirectorySeparatorChar + "Exports"; using (var reader = new SimpleCsvReader<ProfileData>(directory, "test")) { profiles = reader.Read(); } return profiles; } #region unity_script /// <summary> /// 開始処理 /// </summary> private void Start() { // データ書き込み Write(); // データ読み込み var profiles = Read(); foreach (var profile in profiles) { Debug.Log(profile.Name + " = " + profile.Time); } } #endregion #endregion }最後に
Csvの入出力クラスはCsvHelperなどのThird Party製のものや、その他沢山の紹介記事がありますが、
痒いところに手が届かずこのようなクラスを作成することになりました.
この記事が誰かの一助になれば幸いです.少し早いですが、今年もお疲れ様でした.
来年も頑張っていきましょう!
- 投稿日:2020-12-12T15:04:32+09:00
'Screen Space - Overlay'なCanvasでのRectTransform (Unity uGUI)
前提
- この記事は、
RectTransformに対する筆者の理解をまとめたものです。
- これまでずっと直感的に使用していたので、改めて研究してみたものです。
- 網羅的ではなく、また、独自の解釈を含みます。
- RectTransform (公式スクリプトリファレンス)
- CanvasScaler (公式スクリプトリファレンス)
検証環境
- Unity 2019.4.x (LTS)
概念
- 青色の
Targrtオブジェクトに着目します。
RectTransformは、Transformの派生クラスです。
MonoBehaviorを継承したクラスがRectTransformを持つなら、(RectTransform) transformあるいはtransform as RectTransformで、RectTransformにアクセスできます。
RectTransformがない場合、キャストは例外を発生し、asはnullを返します。- 例外が発生しなければキャストの方が高速です。
- ワールド座標系
Screen Space - OverlayなCanvasでのワールド座標系は、スクリーン座標系に一致します。- ローカル座標系
Transformは、親のローカル座標系で配置されます。RectTransform.rectは自身のローカル座標系で示されます。RectTransformのメンバーはローカル座標系に依存しているので、スケールの影響を受けます。
rectやsizeDeltaなどからワールド/スクリーン座標系の実サイズを得るためには、lossyScaleを適用する必要があります。- 正規化座標系
- 基準とする矩形の左下を
(0.0,0.0)、右上を(1.0,1.0)とする座標系です。- 負数や
1.0を超える値も有効で、矩形の外が表現できます。parent
- 赤色の矩形は、ヒエラルキーで親になる
Parentオブジェクトのrectです。- 個々のオブジェクトは、親の影響下で、自身のローカルな系を持ちます。
anchor
- 図中の、白い楔形が指し示す4点です。
- 4点が全て一致して1点となる場合や、2点ずつ一致して2点となる場合もあります。
- オブジェクトは、これらの箇所で親に癒着していて、親の
rectの変化に追従します。- 親の
rectを基準として正規化された座標系で、左下anchorMinと右上anchorMaxの位置が規定されます。pivot
- 図中の、青いドーナッツ型の中心点です。
- 自身のローカル座標の原点で、オブジェクトの位置を代表します。さらに、回転軸でもあります。
- 自身の
rectを基準として正規化された座標系で位置が規定されます。rect
- 図中の、青丸の角と白線で示されている矩形領域です。
- オブジェクトの領域を表します。
anchorMinから左下角へのオフセットoffsetMinと、anchorMaxから右上角へのオフセットoffsetMaxとして規定されます。scale
- 親に対する相対的な拡大率です。
- ワールドに対しては、ヒエラルキーのルートから積算されたスケールを持つことになります。
rotation
- 親に対する相対的な回転角です。
- ワールドに対しては、ヒエラルキーのルートから加算された回転角を持つことになります。
変数
RectTransform 意味 規格 型 anchoredPosition pivotの位置$^{※}$ anchorからの変位 Vector2 anchoredPosition3D pivotの位置$^{※}$ anchorからの変位 Vector3 pivot pivotの位置 rectの正規化座標系 Vector2 anchorMax anchorの右上位置 親のrectの正規化座標系 Vector2 anchorMin anchorの左下位置 親のrectの正規化座標系 Vector2 offsetMax rect右上角の位置 anchorMaxからの変位 Vector2 offsetMin rect左下角の位置 anchorMinからの変位 Vector2 rect rectの位置とサイズ 自身のローカル座標系 Rect sizeDelta rectのサイズ anchorとのサイズ差 Vector2 Transformから継承 説明 規格 型 localEulerAngles 回転角度 親のローカル座標系 float localRotation 回転四元数 親のローカル座標系 Quaternion localPosition pivotの位置 親のローカル座標系 Vector3 localScale 拡大率 親のローカル座標系 Vector3 eulerAngles 回転角度 ワールド座標系 float rotation 回転四元数 ワールド座標系 Quaternion position pivotの位置 ワールド座標系 Vector3 lossyScale 拡大率 ワールド座標系、読み取り専用 Vector3 ※anchorからの変位によるpivotの位置
anchoredPositionは、anchorMinとanchorMaxが一致しない(つまりanchorが一点でない)場合は、pivotの位置を示せなくなる可能性があります。
- 例えば、親が
rect=(0,0,200,200)で、対象がanchorMin=(0.5,0.5)、anchorMax=(1,1),sizeDelta=(0,0)のとき、pivotを何処に移動してもanchoredPositionは(0,0)のまま変化しません。(ただし、計算精度による微少変動を除きます。)Public 関数
RectTransform 説明 ForceUpdateRectTransforms 強制的に内部データを再計算します。 GetLocalCorners rectの頂点座標を自身のローカル座標系で取得します。 GetWorldCorners rectの頂点座標を自身のワールド座標系で取得します。 SetInsetAndSizeFromParentEdge 親のrectの一辺からの距離とサイズでrectの一軸を設定します。 SetSizeWithCurrentAnchors rectの一軸のサイズを設定します。 GetLocalCorners
public void GetLocalCorners (Vector3 [4] corners);
- 四頂点を格納可能な配列を渡して矩形の座標を得ます。
SetInsetAndSizeFromParentEdge
public void SetInsetAndSizeFromParentEdge (RectTransform.Edge edge, float inset, float size);
- 一回のコールで、矩形の一軸の領域を設定します。
pivotの値を満たすように、領域が展開されます。- 矩形を定めるためには、軸を変えて二回コールしなければなりません。
- 同じ軸に属する対辺で二回目をコールすると上書きされます。
edge
RectTransform.Edge.Bottom、Left、Right、Top- 親の矩形の四辺から一つを選択します。
inset
- 領域の開始ポイントです。
size
inset + sizeが、領域の終了ポイントです。SetSizeWithCurrentAnchors
public void SetSizeWithCurrentAnchors (RectTransform.Axis axis, float size);
- 一回のコールで、矩形の一軸のサイズを設定します。
pivotの値を満たすように、領域が展開されます。- 矩形を定めるためには、軸を変えて二回コールしなければなりません。
axis
RectTransform.Axis.Horizontal、Vertical- 軸を選びます。
size
- 軸の領域サイズです。
エディタの挙動
- ツールバー
Pivot/Center
- 選択によって、Sceneウインドウで回転させる際の中心軸が変わります。
pivotがrect中央にないときにCenterを選んでシーン・ウインドウで回転させると、rotation以外のメンバーも変化します。- インスペクター
anchorMinとanchorMaxが一点で一致する場合は、(posX,Width)と(posY,Height)を編集可能です。anchorMinとanchorMaxが一致しない軸においては、(Left,Right)あるいは(Top,Bottom)が編集可能です。RightはanchorMax.xから、TopはanchorMax.yからの、負のオフセットです。LeftはanchorMin.xから、BottomはanchorMin.yからの、正のオフセットです。CanvasScaler
CanvasScalerやCanvasと共にアタッチされているRectTransformのlocalScaleは、Canvas.scaleFactorと同じ値を示します。
- 子孫の
RectTransformはその影響下にあり、そのlossyScaleは、Canvasまで順に親を辿ってlocalScaleを積算したものに一致します。- つまり、
CanvasScalerによるスケーリングは、RectTransform.localScaleを制御しているだけなので、単なるRectTransformの挙動として捉えることができます。
- 投稿日:2020-12-12T14:19:03+09:00
Unityを始める - Unityインストールからプロジェクト作成 -
はじめに
Unityを触ったことがないという人でも気軽に始められるように、Unityで開発を始める場合に必要な事をまとめました。
Unity とは
Unityとは、ユニティ・テクノロジーズが開発しているゲームエンジンのことです。
ゲームエンジンとは、ゲームを開発するためによく使用する機能を簡単に使えるようにまとめたものです。ゲームエンジンの中でもUnityは一番使用されており全世界で100万人の開発者が使用しています。
最近では、ゲーム以外の自動車業界、アニメーション映像業界、建築業界など幅広い分野で使用されています。Unityでできること
様々なプラットフォームでゲームを作れます。
iPhone, Android, Mac, Windows, PlayStation4, Xboxなど様々なプラットフォームでゲームを制作する事ができます。
参考になる本
事前準備
Unity インストール
Unityをインストールするために事前準備 (Unity Hubをダウンロード)
Unityをインストールするためには「UnityHub」というソフトが必要なため、 ダウンロードページ からUnityHubをダウンロードします。
https://unity3d.com/jp/get-unity/downloadダウンロードフォルダにUnityHubSetupがあることを確認します。
UnityHubSetup をダブルクリックします。
ライセンス契約書に同意し、手順を確認しながらインストールします。
UnityHubの使い方
プロジェクト
プロジェクトを作成・開くときに使います。
また、Unityのプロジェクトを一覧で管理することができます。使い方を学ぶ
サンプルプロジェクトやチュートリアルをダウンロードでき、使い方を学ぶことができます。コミュニティ
Unityブログやフォーラム、質疑応答などのコミュニティ活動の情報を取得することができ、他のUnity開発者の方と互いにインスピレーションを与え合うことができますインストール
Unityのインストールができます。
また、現在PCにインストールされているバージョンが確認できます。Unityのインストール手順
インストールの項目を選択し、インストールをクリックします。
バージョンを選択してインストールします。
モジュールの追加します。
Unityでは様々なプラットフォーム向け開発することが想定されており、適当なモジュールを追加することで各プラットフォームに対応した開発を行うことができます。VisualStudioのインストールします。
VisualStudioとは、Microsoftが開発・販売している統合開発環境(IDE)です。
Unityではプログラムを書くときに使用します。Unityアカウントの作成
Unityを使用するには、Unityアカウントを作成して、ライセンスを取得してUnityHubで設定をする必要があります。
- Unityのアカウントを作成します。
- CreateUnityIDをクリックします。
- UnityHubを開き右上のマークをクリックします。
- 登録したメールアドレスとパスワードを入力します。
Unityライセンスの登録
Unityのライセンスについて
UnityではPersonal,Student,Plus,Pro,Enterpriseの5つのプランがあります。
各種プランによって値段とライセンスが変わってきます。基本的に無料のPersonalで良いですが、収益や資金が年間10万ドルを超えたらPlus、20万ドルを超えたらProまたはEnterpriseを契約する必要があります。
詳しくは以下のページに記載されています。
https://store.unity.com/ja/compare-plans?currency=JPYプロジェクトの作成
- UnityHubのプロジェクトから新規作成を選択します。
- テンプレート、プロジェクト名、保存先を選択し作成します。
Unityの標準エディタをVisualStudioにする
確認方法:プロジェクトを開き、画面上部のEditからPrefarenceタブを開き、ExternalToolsの項目をVisual Studio 2019に変更します。
まとめ
Unityインストールからプロジェクト作成までをまとめました。
Unityで簡単にゲーム作りを進めていけます。
今後、実際にゲームを作成して記事にまとめていきます。
- 投稿日:2020-12-12T07:37:32+09:00
Fingers Lite - Touch Gestures for Unity をとりあえず使えるようにする。
Fingers Lite - Touch Gestures for Unity をとりあえず使えるようにする。
Fingers - Touch Gestures for Unity
のアセットが良さそうだったが、チュートリアルドキュメントが見つからなかったのでメモを書いた。そもそもの話として、タッチ機能の実装などは車輪の再開発になるのか?
という疑問はあるが、こちらのフル機能のFingersは画像と形状の認識があるので
今後、ジェスチャーで必殺技!みたいなことをしたい時に使いたい。
(形状認識はさすがに面倒だね)買う前に Liteを使う
さて、最初に$20を出して購入するのはとても良い事だが
私の頭がこのassetを使えないかもしれない。Lite版があるのでそちらをimportして試してみよう。Fingers Lite - Free Finger Touch Gestures for Unity
動画チュートリアルは以下だ。古いが基本的な部分はさほど変わらないはずだ。
Fingers Gestures for Unity - Complete Tutorialデモスクリプトを確認
動画を見たあとはデモスクリプトを確認しよう
重要なのは以下の4点ほどだ。
(混乱が少ないように随時書き換えてあるので、実際のコードを確認してくれるとうれしい :) )一番最初に実行されるコード
private TapGestureRecognizer tapGesture; ... private void Start() { ... CreateTapGesture(); ... FingersScript.Instance.CaptureGestureHandler = CaptureGestureHandler; }ジェスチャーの作成
private void CreateTapGesture() { tapGesture = new TapGestureRecognizer(); // デリゲートにcallbackを委譲 tapGesture.StateUpdated += TapGestureCallback; // シングルタップのみを判定させるため、ダブルタップだった場合、 // シングルタップを無視させる tapGesture.RequireGestureRecognizerToFail = doubleTapGesture; // ジェスチャーを登録 FingersScript.Instance.AddGesture(tapGesture); }実際のシングルタップの動作
private void TapGestureCallback(GestureRecognizer gesture) { // ジェスチャーが認識されているか if (gesture.State == GestureRecognizerState.Ended) { Debug.Log("Tapped at " + gesture.FocusX + "," + gesture.FocusY); } }ジェスチャが有効かの判定
private static bool? CaptureGestureHandler(GameObject obj) { if (obj.name.StartsWith("PassThrough")) return false; if (obj.name.StartsWith("NoPass")) return true; return null; }
bool?はnull許容値型だ。以上の4点を書いたコードをGameObjectにアタッチすれば
タップした時の動作が確認できるはずだ。私にしては短いが、メモ程度なのでこんなものだろう。
- 投稿日:2020-12-12T03:45:32+09:00
MiniScriptでつくる「メッセージ再生&入力待ち」コマンド
はじめに
Unityで色々とゲームを作っていると、たまに
「RPGやADVで見るような会話イベント処理をつくりたい」
といった事を思うことはないでしょうか。えっわたしだけ?
会話イベントでは「メッセージの再生」「キー入力待ち」などの処理の流れがよく出てきます。
それを実装しようと思った時通常のC#スクリプトで無理に頑張ろうとするとyield return mes("ほげほげ"); // メッセージ表示&キー入力待ち yield return mes("アヘアヘ"); // 同上 ...以下色々会話イベントの処理...みたいな、yield returnをつけないといけない仕組みになったりします。いちいち書くのは正直つらいですね。
どうせテキストで書くならこれくらいシンプルにしちゃいたいところです。
mes "ほげほげ" # メッセージ表示&キー入力待ち mes "アヘアヘ" ...以下色々...なお、会話シーンを実装するのに役立つアセットとして
- ノードを使ったり、Unity上でコマンドを入れていくFungus
- Excelでシナリオデータを記述する 宴
- テキストベースで吉里吉里やティラノスクリプト寄りの構文で書ける JokerScript
など色々とありますが・・・
今回は、そこまで高機能なアセットは要らないんだけどテキストベースのスクリプトを使ったゲーム中のシナリオ再生/イベント作成の仕組みをある程度手軽に組み込みたい…といった人向けに
「MiniScript」という、組み込み型のスクリプト言語を用いた「メッセージ表示&キー入力待ち」の方法についてざっくり解説します。(結構ニッチな気がする…)
MiniScript
オープンソースのC++, C#向け組み込みスクリプト言語です。
比較的シンプルな初期命令セットと括弧の無い構文で構成されており、習得が容易な部類の言語となっています。
こちらアセットストアで有料アセットとして販売されていますが、言語のコア部分はgithubからダウンロードしてすぐに使用することができます。
有料アセットの方に含まれているのは「ゲーム中にスクリプトを編集できる、補完機能搭載のインゲーム用エディタ」です。コア機能的にはいっしょ(のはず)
今回はとりあえずコア部分だけ使えればよいので、githubからソースをダウンロードして組み込みを行います。
解説プロジェクトについて
Unity 2019.4.13f1 で動作確認しています。
MiniScriptの組み込み
まずgithubからソース一式をダウンロードします。
https://github.com/JoeStrout/miniscript
解凍して「MiniScript-cs」の中身から必要なソースコード(画像参照)を取り出してUnityプロジェクトのどこかにコピーします。
メインプログラムコードやソリューションファイルは不要なため今回は省きます。
これでプロジェクトへの組み込みは完了です。
ちなみに、「MiniScript-cs」ディレクトリをまるごとUnityプロジェクトに放り込んでも一応問題なく動きます。実行環境の作成
MiniScriptのスクリプトを実行するには、実行環境にあたるインタプリタを生成、管理する必要があります。
とりあえず今回はお試し感覚で大雑把につくりましょう。
以下のスクリプトを適当な場所に置きます。using Miniscript; using UnityEngine; public class MiniScriptPlayer : MonoBehaviour { private readonly Interpreter _interpreter = new Interpreter(); private void Start() { _interpreter.hostData = this; _interpreter.standardOutput = Debug.Log; _interpreter.implicitOutput = s => Debug.Log($@"implicit {s}"); _interpreter.errorOutput = Debug.LogError; // スクリプトテキストの入力 _interpreter.Reset($"print \"Hello World!\""); // 入力したスクリプトの実行を開始 _interpreter.Compile(); } private void Update() { if (!_interpreter.Running()) return; // スクリプトが終了する、または途中で中断(yield)されたり、一定の処理時間が経過するまで処理が実行されます _interpreter.RunUntilDone(); } } }適当なGameObjectにこのスクリプトをアタッチしてゲームを起動すると、文字列で入力されたMiniScriptのテキストが実行されます。
実行後、コンソールに以下のログが出ていれば正しく実行されています。
スクリプト入力口の作成
インスペクタからスクリプトを入力できるようにしてみます。
実際に運用するときはスクリプトはテキストファイルなどにするものですが、今回は楽な方に走ります。
MiniScriptPlayerクラスを以下のように変更します。using Miniscript; using UnityEngine; public class MiniScriptPlayer : MonoBehaviour { private readonly Interpreter _interpreter = new Interpreter(); [Multiline(7)] public string _scriptField; // 追加 private void Start() { _interpreter.hostData = this; _interpreter.standardOutput = Debug.Log; _interpreter.implicitOutput = s => Debug.Log($@"implicit {s}"); _interpreter.errorOutput = Debug.LogError; _interpreter.Reset(_scriptField); // 変更 _interpreter.Compile(); } private void Update() { if (!_interpreter.Running()) return; // スクリプトが終了する、または途中で中断(yield)されたり、一定の処理時間が経過するまで処理が実行されます _interpreter.RunUntilDone(); } }作成した入力口に以下のスクリプトを入力して実行してみましょう。
print "Hello World!" wait 1 print "a" wait 1 print "b" wait 1 print "c"wait コマンドは入力した秒数分スクリプトの実行を待機するものです。
実行後、コンソールに1秒ずつログが流れれば成功しています。コマンドの自作
本題となる、コマンドの自作をやってみましょう。
まずはコマンドの仕様をざっくり策定します。
今回UIまわりにまでは手を出しません。
そのため、最低限テキストに入力したメッセージが見えればよいということで以下のような仕様としました。
- コマンド名は mes
- 引数は文字列ひとつ
- 返り値は無し
- 実行されるとDebug.Logにメッセージが流し込まれる
- メッセージ表示後、マウス左クリックで次のコマンドに進む
Intrinsic.Create() メソッドでコマンドを作成することができます。
コマンド名は前述の通り mes とするため、引数に mes と入れます。var f = Intrinsic.Create("mes");次にコマンドの引数を設定します。
スクリプト上だと露出しませんが、内部処理のために引数に名前をつける必要があります。
今回は適当に text としました。var f = Intrinsic.Create("mes"); f.AddParam("text", string.Empty); // 第二引数は コマンド引数textのデフォルト値メッセージ再生
ここからコマンド内部の処理を作成していきます。
Create時に返ってきたIntrinsicの code に実際の処理を記述します。
var f = Intrinsic.Create("mes"); f.AddParam("text", string.Empty); f.code = (context, result) => { // ここに色々書く };まずは先程策定した引数 text をコンソールに表示する処理を書いてみます。
f.code = (context, result) => { Debug.Log(context.GetVar("text").ToString()); return Intrinsic.Result.Null; };context.GetVar()でスクリプト側で入力された引数textの値を拾いコンソールログに送っています。
return の Intrinsic.Result.Null についてですが、
各コマンドの処理は何かしらの Result インスタンスを返す必要があります。
前述の仕様の通り、今回のコマンドでは返り値を無しとするため
MiniScript側に用意されている Null Result インスタンスを使用して返しています。
(空文字列を返したり真偽値を返したりといくつか既定の種類がありますが、Resultの中身は自分で色々設定する事もできます)この時点で、mes コマンドを用いたコンソールログの出力が可能になりました。
MiniScriptPlayerを以下のようにして、スクリプト側で mes コマンドを使ってみましょう。using Miniscript; using UnityEngine; public class MiniScriptPlayer : MonoBehaviour { private readonly Interpreter _interpreter = new Interpreter(); [Multiline(7)] public string _scriptField; private void Start() { _interpreter.hostData = this; _interpreter.standardOutput = Debug.Log; _interpreter.implicitOutput = s => Debug.Log($@"implicit {s}"); _interpreter.errorOutput = Debug.LogError; var f = Intrinsic.Create("mes"); f.AddParam("text", string.Empty); f.code = (context, result) => { Debug.Log(context.GetVar("text").ToString()); return Intrinsic.Result.Null; }; _interpreter.Reset(_scriptField); _interpreter.Compile(); } private void Update() { if (!_interpreter.Running()) return; // スクリプトが終了する、または途中で中断(yield)されたり、一定の処理時間が経過するまで処理が実行されます _interpreter.RunUntilDone(); } }print コマンドと合わせて使っても同じようにうごきます。
キー入力待ち
それでは、最後に「キー入力待ち」を実装しましょう。
現状 mes コマンドで最終的に Null の Result を返すとコマンドが終了してしまいます…が、
Result は、返り値の他に「このコマンドの処理が終了したかどうか」をシステムに返す事もできます。処理が終了していないと返せば、スクリプト全体の実行が一時中断(yield)され、次のフレームで再度該当のコマンドが再実行されるようになります。
MiniScript側に用意されている Intrinsic.Result.Waiting を用いる事でその挙動を取ることができるようになります。
試しに mes コマンドの実装の返り値を Waiting に変えてみましょう。f.code = (context, result) => { Debug.Log(context.GetVar("text").ToString()); return Intrinsic.Result.Waiting; // 変更 };以下のような状態になるはずです。無限にログが流れますね。
コマンドの再実行が行われるという仕様なので、素直に上記のようにしただけだと無限にDebug.Logが呼ばれ続けてしまいます。
これをどうにかするには、コマンド実行の最初のフレームでのみログ表示を行う…といった処理にする必要があります。
Waitingで返したコマンドが再実行される際は、処理の第二引数である result に値が入ってくるようになっており中断時の状態に合わせた処理を書くことができます。
この仕様を利用し、以下のように処理を変更します。
f.code = (context, result) => { if (result == null) { Debug.Log(context.GetVar("text").ToString()); } return Intrinsic.Result.Waiting; };すると、コンソールログへの "b" の表示が一つだけになります。
さて、後もう一息です。
ログは正しく出るようになりましたが、今のままだと一生コマンドが終了しないので先に進むことができません。
マウスクリックを検知したら Null リザルトを返すように処理を修正しましょう。f.code = (context, result) => { if (result == null) { Debug.Log(context.GetVar("text").ToString()); } else { if (Input.GetMouseButtonDown(0)) { return Intrinsic.Result.Null; } } return Intrinsic.Result.Waiting; };動きがわかりやすいようにMiniScriptのスクリプトも少し変更しましょう。
mes "Hello World!" mes "a" mes "b" mes "c"これで実行し、マウスの左クリックを行うごとにログが出てくるようになれば完成です!
後はDebug.Logなところを自作のメッセージ表示処理に変えてやるなり、ゲームに合わせて改造していく事でそれっぽいコマンドを作り上げることができるようになります。
全体図
今回作成したクラスは再生環境+コマンド含めて以下な感じになりました。
using Miniscript; using UnityEngine; public class MiniScriptPlayer : MonoBehaviour { private readonly Interpreter _interpreter = new Interpreter(); [Multiline(7)] public string _scriptField; private void Start() { // ログ出力先など初期設定 _interpreter.hostData = this; _interpreter.standardOutput = Debug.Log; _interpreter.implicitOutput = s => Debug.Log($@"implicit {s}"); _interpreter.errorOutput = Debug.LogError; // mes コマンド定義 var f = Intrinsic.Create("mes"); f.AddParam("text", string.Empty); f.code = (context, result) => { if (result == null) { Debug.Log(context.GetVar("text").ToString()); } else { if (Input.GetMouseButtonDown(0)) { return Intrinsic.Result.Null; } } return Intrinsic.Result.Waiting; }; // スクリプトコンパイル、実行 _interpreter.Reset(_scriptField); _interpreter.Compile(); } private void Update() { if (!_interpreter.Running()) return; // スクリプトが終了する、または途中で中断(yield)されたり、一定の処理時間が経過するまで処理が実行されます _interpreter.RunUntilDone(); } }実運用の事を考えてない都合上かなり端折った部分も多いですが、比較的容易に機能を拡張していけるという事が少しだけでも伝われば幸いです。
さいごに
ここまで見た方はお気づきかなと思いますが、今回MiniScriptが元々持ってる構文やコマンドの解説だったり実運用上の話に関してはまったく解説していません。色々訳わからなかったらごめんなさい。
公式サイトのドキュメントがそれなりに充実しているので、MiniScriptについて知りたいことがあればそちらを参照してみてください。
シンプルで扱いやすい印象ではあるものの日本語情報が超少ないので、
もっと増えてくれると嬉しいな~~~~~~と思っています。みんなもさわってみよう、MiniScript。
- 投稿日:2020-12-12T00:05:35+09:00
Unity ShaderGraph レシピ#3 市松模様
全集中!市松模様
作り方
#1シンプルなグリッドより遥かにシンプルなシェーダーなのでこれを#1にすればよかったと思っています。ええ。
1. Checkerboard
市松模様を作るためのすべての要素はCheckerboardノードに含まれています。
CheckerboardノードのoutをそのままColorに流すだけ。簡単!
Color A, B がそれぞれの模様の色になります。
Frequencyの値が模様の繰り返し回数になります。完成!
その他のレシピはShaderGraphレシピ一覧にまとまっています
- 投稿日:2020-12-12T00:05:35+09:00
Unity ShaderGraph レシピ #3 市松模様
全集中!市松模様
作り方
#1シンプルなグリッドより遥かにシンプルなシェーダーなのでこれを#1にすればよかったと思っています。ええ。
1. Checkerboard
市松模様を作るためのすべての要素はCheckerboardノードに含まれています。
CheckerboardノードのoutをそのままColorに流すだけ。簡単!
Color A, B がそれぞれの模様の色になります。
Frequencyの値が模様の繰り返し回数になります。完成!
その他のレシピはShaderGraphレシピ一覧にまとまっています

















































































