- 投稿日:2019-10-11T22:34:36+09:00
Unity で Lorentz 方程式の軌道を描画してみた
宇宙の深淵からこんにちは、
\displaystyle\int {\rm d}\boldsymbol{r}\hat{\psi}^{\dagger}(\boldsymbol{r})ぽっぴんフレンズ\hat{\psi}(\boldsymbol{r})です!今回は栄えある Qiita 初投稿ということで、Unity で Lorentz 方程式の軌道を描画することを内容として記事にしたいと思います!(Lorentz 方程式について知らない方もいるかと思いますが、結果をチラ見せした方が早いでしょう。今回は下の画像のような曲線を描くことを目標にします!もしかしたら、どこかで見たことがあったりするのではないでしょうか?)
ということで、Lorentz 方程式及びアルゴリズムの紹介、そしてコードの実装を見ていきましょう!
Unityのバージョン:
Unity 2018.4.6f1
こんな方にオススメ:
- Unity でゲーム制作以外のことをしてみたい
- 数値計算の結果をいい感じに視覚化したい
- ローレンツ方程式が大好き
Lorentz 方程式とは
Lorentz 方程式は、以下のように表されます。
\begin{align} \frac{{\rm d}x}{{\rm d}t}&=-px+py\\ \frac{{\rm d}y}{{\rm d}t}&=-xz+rx-y\\ \frac{{\rm d}z}{{\rm d}t}&=xy-bz \end{align}$t$ は時間、$x, y, z$ は時間 $t$ の関数あり物体の座標、$p, r, b$ は実数の定数です。左辺は $x, y, z$ の時間微分、右辺は $x, y, z$ を変数とする関数となっています。
Lorentz 方程式は対流の研究のために流体力学の方程式を非常に簡単にしで出来たものらしいのですが、初期値鋭敏性という非常に興味深い特徴がみられる方程式として有名です。初期値鋭敏性とは、簡単に言うと「初めの状態(位置)のほんの僅かな誤差が後々大きな違いとなって現れる」ことです。物理現象を予測する上で初めの状態を測定することは非常に重要ですが、測定には当然誤差がついてきます。Lorentz 方程式ではその誤差の影響で後々の結果が大きく変わってきてしまう、すなわち現象の長期的な予測が予測が非常に困難となるのです!天気予報で遠い未来の天気を予測したいときほど予報が大きく外れるのと同じですね!
方程式といえば、普通は「解」がありますよね?連立方程式であれば片方の文字を消去するなどすれば $x$ と $y$ の値が求まります。 Lorentz 方程式にももちろん解があることが想像されますが、実はこの Lorentz 方程式、解を厳密に(手計算で)求めることができません!(な、なんだってー!!!) しかし、こののような場合のために、厳密な解ではなくそれっぽい解(数値解)を求める方法が存在します。詳しくは次節で取り扱いますが、この数値解を計算することで、手計算で解が出なくとも、方程式のおおまかな振る舞いを知ることができるのです!
Runge-Kutta 法
これから Runge-Kutta 法というアルゴリズムについて紹介します。ぶっちゃけると、本節の内容を理解していなくても、次節で紹介するコードなどをパクってしまえば実装はできてしまいます。なので、この節は最悪読み飛ばしても構いません(本節も一生懸命書いたので、できれば読み飛ばさないでほしいです(迫真))。
Lorentz 方程式のような、関数の微分を含む方程式を微分方程式と呼びます。とくに Lorentz 方程式は時間 $t$ の関数が $x, y, z$ の3つあり、これらが互いに絡まりあっているので、連立微分方程式と呼ばれます。微分方程式は手計算で解を求められるものもありますが、そうでないものが圧倒的に多いのです。そして、前節でも述べたとおり、Lorentz 方程式も手計算では解を求められないものになります。そこで、コンピュータのパワーを借りることによて、「それっぽい解」を求める方法を用います。今回はいくつかある方法の中でも、精度が高い** Runge-Kutta 法**(厳密には4段4次 Runge-Kutta 法)というものを用います。
簡単のため、まずは時間 $t$ の関数が $x$ 1つのみである1階微分方程式( $x$ の $t$ による1階微分のみを含む方程式)を考えましょう。一般的な1階微分方程式は以下のように書かれます。
\frac{{\rm d}x}{{\rm d}t}=f(x, t)左辺は $x$ の $t$ による1階微分、右辺は $x, t$ による何かしらの関数です。この方程式において、時刻 $t=0$ における $x$ の値 $x_0$ から、一定のアルゴリズムに従って次々と方程式の解を生成します。記事の趣旨上、詳しい説明は省き、アルゴリズムの内容のみを紹介します。
h\ は十分小さい正の実数、x_i\ は時刻\ t_i=ih\ における\ x\ の値とする。\\ 時刻\ t_{i+1}=(i+1)h\ における\ x\ の値\ x_{i+1}\ を以下の手順で求める。\\ \begin{align} \\ &k_1=h\times f(x_i, t)\\ &k_2=h\times f(x_i+\frac{k_1}{2}, t+\frac{h}{2})\\ &k_3=h\times f(x_i+\frac{k_2}{2}, t+\frac{h}{2})\\ &k_4=h\times f(x_i+k_3, t+h)\\ \\ &k=\frac{k_1+2k_2+2k_3+k_4}{6}\\ &x_{i+1}=x_i+k \end{align}以上が Runge-Kutta 法のアルゴリズムです。時刻 $t_0=0$ における $x$ の値 $x_0$ を自分で決めておけば、上記のアルゴリズムに従って、時刻 $t_1=h$ における $x$ の値 $x_1$, 時刻 $t_2=2h$ における $x$ の値 $x_2$, $\cdots$, 時刻 $t_i=ih$ における $x$ の値 $x_i$, $\cdots$ が次々に求まるという寸法です。この方法は精度が高く( $x(t+h)$ の $h$ についてのテイラー展開に対し $h^4$ の項まで一致する)、微分方程式の数値計算によく用いられます。
上記のアルゴリズムは $t$ の関数が $x$ の1つだけの場合のものでした。Lorentz 方程式は$t$ の関数が $x, y, z$ の3つあるので、少し改良を加えましょう。まず、$\vec{r}=(x, y, z)$ として $x, y, z$ を1つのベクトルにまとめてしまいましょう。$\vec{r}$ は言うなれば $x, y, z$ を座標とする位置ベクトルになります。そうすると、$x, y, z$による連立1階微分方程式は以下のように表されます。
\frac{{\rm d}\vec{r}}{{\rm d}t}=\vec{f}(\vec{r}; t)\\ \begin{align} ただし、&\vec{r}=(x, y, z),\\ &\vec{f}(\vec{r}; t)=(f_x(x, y, z; t), f_y(x, y, z; t), f_z(x, y, z; t)) \end{align}左辺は $\vec{r}$ の各成分を微分したもの、右辺は $\vec{r}, t$ を変数とするベクトル関数です。ベクトル関数 $\vec{f}(\vec{r}; t)$ は、$x, y, z$ 成分それぞれが異なる形の関数 $f_x, f_y, f_z$ となっています。この微分方程式の Runge-Kutta 法は以下のようになります。
h\ は十分小さい正の実数、\vec{r}_i\ は時刻\ t_i=ih\ における位置とする。\\ 時刻\ t_{i+1}=(i+1)h\ における位置\ \vec{r}_{i+1}\ を以下の手順で求める。\\ \begin{align} \\ &\vec{k}_1=h\times \vec{f}(\vec{r}_i, t)\\ &\vec{k}_2=h\times \vec{f}(\vec{r}_i+\frac{\vec{k}_1}{2}, t+\frac{h}{2})\\ &\vec{k}_3=h\times \vec{f}(\vec{r}_i+\frac{\vec{k}_2}{2}, t+\frac{h}{2})\\ &\vec{k}_4=h\times \vec{f}(\vec{r}_i+\vec{k}_3, t+h)\\ \\ &\vec{k}=\frac{\vec{k}_1+2\vec{k}_2+2\vec{k}_3+\vec{k}_4}{6}\\ &\vec{r}_{i+1}=\vec{r}_i+\vec{k} \end{align}以上のアルゴリズムによって、時刻 $t_0=0$ における位置 $\vec{r}_0$ をはじめに決めておけば、方程式の解となる点が次々と求まります。
以上が、今回用いる Runge-Kutta 法の説明でした。少々難しかったでしょうかね(汗)。次節からいよいよUnity上で実装していきます!
実装
いよいよ、Unity上で Lorentz 方程式を実装していきます!前節の Runge-Kutta 法が良くわからなくても、以下の内容に沿っていけば誰でも実装できますので、頑張っていきましょう!
Unity上での準備
はじめに、Unity で新しいプロジェクト(3D)を開きます(Unityがない人は諦めるか今すぐインストールしてください!)。
Hierarchy
のCreate
からCreate Empty
を押し、空の GameObject を作ります。名前は Point としましょう。また、Inspector
にあるTransform
で Object の位置を設定します(これが初めの位置になります)。今回は $(x, y, z)=(0, 10, 1)$ としましょう。この GameObject に色々なものを付け足すことで、Lorentz 方程式に従って動きながら、軌道が線として残るようにします。カメラの位置と向きも調整しておきましょう。軌道はかなり大きくなるので、カメラも割合遠くに置きます。
Hierarchy
のMain Camera
を押し、Inspector
にあるTransform
で位置を調整します。今回は $(x, y, z)=(0, 25, -60)$ としましょう。これで最低限の準備はできました!次はいよいよスクリプトを書いていきます!
Lorentz 方程式の実装
いよいよ Lorentz 方程式を Unity の C# スクリプトで実装していきますが、ここで一つ面倒な問題があります。それは、Unity での座標軸の取り方と数学・物理学での座標軸の取り方が異なるということです。数学・物理では、水平面に $x-y$ 平面が置かれ、鉛直方向に $z$ 軸が置かれますが、Unity では水平面に $x-z$ 平面が置かれ、鉛直方向に $y$ 軸が置かれます。すなわち、$y$ 軸と $z$ 軸が入れ替わった感じになります。最初に紹介した Lorentz 方程式の座標は数学・物理学での慣習に則っているので、Unity 上で実装する際に元の式の $y$ と $z$ を入れ替えて式を少し変えます(入れ替えなくても動くには動くのですが、軌道が横倒しになった形になります)。以下が、Unity 用に $y$ と $z$ を入れ替えた式になります。
\begin{align} \frac{{\rm d}x}{{\rm d}t}&=-px+pz\\ \frac{{\rm d}y}{{\rm d}t}&=xz-by\\ \frac{{\rm d}z}{{\rm d}t}&=-xy+rx-z \end{align}右辺だけでなく左辺の微分のところも入れ替えることに注意してください。これで準備は整いました。
さて、いよいよコードを書いていきましょう!Unity の
Project
でCreate
を押し、C# Script
を生成します。スクリプト名は LorentzEquation としましょう。スクリプトを開いて以下のコードを書いていきます。LorentzEquation.csusing System.Collections; using System.Collections.Generic; using UnityEngine; public class LorentzEquation : MonoBehaviour { //Lorentz 方程式内の定数 float p = 10.0f; float r = 28.0f; float b = 8.0f / 3.0f; //Runge-Kutta 法で用いる、時間の刻み幅 float h = 5e-3f; //毎フレームごとに Runge-Kutta 法で GameObject の位置をアップデート void Update() { RungeKutta(); } //Runge-Kutta法の関数 void RungeKutta() { Vector3 k1, k2, k3, k4; Vector3 position, deltaPosition; position = this.transform.position; k1 = h * Lorentz(position); k2 = h * Lorentz(position + k1 / 2); k3 = h * Lorentz(position + k2 / 2); k4 = h * Lorentz(position + k3); deltaPosition = (k1 + 2 * k2 + 2 * k3 + k4) / 6; this.transform.position += deltaPosition; } //Lorents 方程式の関数 Vector3 Lorentz(Vector3 position) { float x = position.x; float y = position.y; float z = position.z; // dx/dt=-px+pz // dy/dt=xz-by // dz/dt=-xy+rx-z float xdot = p * (-x + z); float ydot = x * z - b * y; float zdot = -x * y + r * x - z; return new Vector3(xdot, ydot, zdot); } } } // Lorentz 方程式の関数 Vector3 Lorentz(Vector3 position) { float x, y, z; float xdot, ydot, zdot; x = position.x; y = position.y; z = position.z; // dx/dt=-px+pz // dy/dt=xz-by // dz/dt=-xy+rx-z xdot = p * (-x + z); ydot = x * z - b * y; zdot = -x * y + r * x - z; return new Vector3(xdot, ydot, zdot); } }このコードを先ほど生成した GameObject にひも付すれば、Object がローレンツ方程式に従って動いてくれるようになります。
コード内の関数の説明をしていきます。まずは Lorentz 方程式の関数から行きましょう。方程式内の定数はコードの冒頭で $p=10, r=28, b=8/3$ に設定します。
p,r,b//Lorentz 方程式内の定数 float p = 10.0f; float r = 28.0f; float b = 8.0f / 3.0f;GameObject の位置を引数としてローレンツ方程式の右辺を計算し、方程式の左辺を返す関数を作ります。はじめに Vector3 型の引数の $x, y, z$ 成分を x, y, z にそれぞれ格納し、次に方程式の右辺の計算結果を xdot, ydot, zdot にそれぞれ格納します。最後に xdot, ydot, zdot を成分とする Vector3 型の変数を返り値として返します。これで Lorentz 方程式の関数の完成です。
Lorentz()// Lorentz 方程式の関数 Vector3 Lorentz(Vector3 position) { float x, y, z; float xdot, ydot, zdot; x = position.x; y = position.y; z = position.z; // dx/dt=-px+pz // dy/dt=xz-by // dz/dt=-xy+rx-z xdot = p * (-x + z); ydot = x * z - b * y; zdot = -x * y + r * x - z; return new Vector3(xdot, ydot, zdot); }次に、Runge-Kutta 法で GameObject の位置をアップデートする関数について説明します。時間 $t$ の刻み幅 $h$ はコードの冒頭で設定しております。$h$ を大きくとると線が荒くなってしまいますが、小さくとりすぎると GameObject の動きが遅すぎになってしまいます。今回は $h=0.005(=5e-3)$ としました。(※ $h$ として1フレームの時間 Time.deltaTime を用いたい場合は関数の中で $h$ を設定する必要があります。たぶん。)
h//Runge-Kutta 法で用いる、時間の刻み幅 float h = 5e-3f;Runge-Kutta 法の関数は、毎フレームごとに GameObject の位置を取得し、Runge-Kutta 法のアルゴリズムに従って位置の変化量を計算し、それを位置に加算することで GameObject の位置を更新します。まず、Runge-Kutta 法で用いる Vector3 型変数 k1, k2, k3, k4 と、そのフレームでの位置を格納する変数 position 及び Runge-Kutta 法の計算結果を格納する変数 deltaPosition を宣言します。次に、position に GameObject の位置を格納します。そして Runge-Kutta 法のアルゴリズムに従って k1, k2, k3, k4, 及び deltaPosition を計算し、最後に GameObject の位置に deltaPosition を加算します。
RungeKutta()//Runge-Kutta法の関数 void RungeKutta() { Vector3 k1, k2, k3, k4; Vector3 position, deltaPosition; position = this.transform.position; k1 = h * Lorentz(position); k2 = h * Lorentz(position + k1 / 2); k3 = h * Lorentz(position + k2 / 2); k4 = h * Lorentz(position + k3); deltaPosition = (k1 + 2 * k2 + 2 * k3 + k4) / 6; this.transform.position += deltaPosition; }上の関数は1フレーム分の処理なので、これを void Update() 関数内に書くことで毎フレーム処理を行ってくれるようになります。
Update()//毎フレームごとに Runge-Kutta 法で GameObject の位置をアップデート void Update() { RungeKutta(); }これでスクリプトの実装ができました!念のため、本節冒頭で作った GameObject にスクリプトをひも付することを忘れないでください。次は GameObject の軌道を描く方法について紹介していきます!
軌道の描画の実装
前小節で、GameObjectを Lorentz 方程式に従て動かすためのスクリプトを作りましたが、これだけでは Object の軌道が見えません。ここでは、Object の軌道を表示させるための設定をしていきましょう!
Unity には線を描くためのコンポーネントとして Line Renderer というものがあります。まずはこれを最初に作った GameObject に追加しましょう。
Hierarchy
に先ほど生成した Point があるのでそれを押し、Inspector
にあるAdd Component
を押します。検索窓が出てくるので、そこで「Line Renderer」と検索してコンポーネントを追加します。コンポーネントを追加したら、線の太さを変えていきます。width
という名前で謎のグラフがある個所があります。左上のボックスの数字を変えると、線の太さを調整する上限を変えられます。デフォルトで1.0になっているはずですが、これだと太すぎるので、今回は数字を0.15にし、赤い線をマウスで一番上まで引っ張ります。これで線の太さが0.15になります。次に、線を描くスクリプトを作っていきます。
Project
からC# Script
を生成し、以下のコードを書きます。DrawLine.csusing System.Collections; using System.Collections.Generic; using UnityEngine; public class DrawLine : MonoBehaviour { //オObject の軌跡を描画する LineRenderer line; // LineRenderer Componentを受ける変数 int count; // 線の頂点の数 // Start is called before the first frame update void Start() { line = GetComponent<LineRenderer>(); // LineRenderer Componentを取得 } void Update() { count += 1; // 頂点数を1つ増やす line.positionCount = count; // 頂点数の更新 line.SetPosition(count - 1, this.transform.position); // Object の位置情報をセット } }1フレームごとに線を描くための頂点を増やしていき、増やした頂点に GameObject の位置をセットすることで線を描くことができます。尚、頂点の番号は 0, 1, 2, ... のようにゼロから振られるので、新しく追加される点の番号は $点の総数-1$ となることに注意してください(たとえば100個の点で描かれる曲線の100個目の点の番号は99番)。
ここまでで、既に Lorentz 方程式の軌道を描画する準備は整いました!では、いざ実行してみましょう!
実装結果
準備が整ったので、実行してみましょう!...とその前に、以下の点を今一度確認してみてください。
- GameObject に LorentzEquation のスクリプト、DrawLine のスクリプト及び LineRenderer がひも付されているか。
- LineRenderer の線の太さは適切か(オススメ:0.15)。
- LorentzEquation のスクリプト、DrawLine のスクリプトの内容に間違いはないか。
- GameObject の位置は適切か(オススメ:$(0, 10, 1)$ )。
- カメラの位置は適切か(オススメ:$(0, 25, -60)$ )。
何かしらのエラーがあれば
Console
に表示されるので、エラーに従って直してください。すべて確認できたら、いざ実行してみましょう!以下のように動くはずです。(よかったらTwitterのフォローお願いします!)
UnityでLorentz方程式の軌道を描画してみました。#Unity#ローレンツアトラクタ pic.twitter.com/SxJC76hU5l
— ∫drψ†ぽっぴんフレンズψ (@dr30793443) October 11, 2019圧巻ですね。。。
おまけ:カメラの移動
さらなる圧巻を求めて、軌道のまわりを回転しながら眺められるように、カメラに動きを付け足していきましょう!
Project
でC# Script
を生成し、名前を CameraScript としましょう。そして、以下のコードを書いていきます。CameraScript.csusing System.Collections; using System.Collections.Generic; using UnityEngine; public class CameraScript : MonoBehaviour { public float omega = 0.30f; //半径 public float A = 60.0f; //角速度 public float height = 25.0f; //高さ float theta; //回転角 float sinTheta; //回転角のsin float cosTheta; //回転角のcos void Update() { theta = omega * Time.time; //角度=時刻*角速度 sinTheta = Mathf.Sin(theta); //回転角のsinを計算 cosTheta = Mathf.Cos(theta); //回転角のcosを計算 //カメラの位置の代入 this.transform.position = new Vector3(A * sinTheta, height, -A * cosTheta); //カメラの方向の代入 this.transform.forward = new Vector3(-sinTheta, 0.0f, cosTheta); } }はじめに、回転の半径(A)、角速度(Omega)、および回転する位置の高さ(height)を決めます。変数の前に
public
を付けることで、カメラのInspector
から変数をいじれるようになりますが、付けなくても動きはします。A,omega,heightpublic float omega = 0.30f; //半径 public float A = 60.0f; //角速度 public float height = 25.0f; //高さ各フレームにおけるカメラの回転角とその $\sin, \cos$ を格納する変数も用意しておきます。
theta,sin,cosfloat theta; //回転角 float sinTheta; //回転角のsin float cosTheta; //回転角のcosそして、以下の部分でカメラの位置と向きを毎フレーム更新していきます。はじめに theta, sinTheta, cosTheta の値を計算します。最初に計算してしまうことで、$\sin, \cos$ を使いたいたびに Mathf.sin 関数で計算をする手間が省けます。時刻 $t$ におけるカメラの位置は $r = (A\sin{\omega t}, h, -A\cos(\omega t))$ にします。こうすることで、$(x, y, z)=(0, 25, -60)$ からスタートし、上から見て反時計回りに回転してくれます。カメラの向きは this.transform.forward というベクトルをいじることで調整でき、$(-\sin{\omega t}, 0, \cos(\omega t))$ とすることで常に円の中心を向いてくれます。
Update()void Update() { theta = omega * Time.time; //角度=時刻*角速度 sinTheta = Mathf.Sin(theta); //回転角のsinを計算 cosTheta = Mathf.Cos(theta); //回転角のcosを計算 //カメラの位置の代入 this.transform.position = new Vector3(A * sinTheta, height, -A * cosTheta); //カメラの方向の代入 this.transform.forward = new Vector3(-sinTheta, 0.0f, cosTheta); }以上のコードによって、カメラが高さ height=25 にある半径 A=60 の円周上を、角速度 omega=0.30 で、円の中心を向きながら回転してくれます。以下がその結果です。
カメラを回転させた場合です#Unity#ローレンツアトラクタ#プログラミング初心者 pic.twitter.com/efsVBDl162
— ∫drψ†ぽっぴんフレンズψ (@dr30793443) October 11, 2019より臨場感が増しましたね!
まとめ
今回は Lorentz 方程式を Unity で実装し、軌道を描画してみました。皆様、いかがでしたか?個人的には思ったより簡単にできたなという印象だったので、Unity が使える方は是非試してみてください!また、この記事の続きとして、複数の点を少しだけずらして出発させ、初期値鋭敏性が現れる様子を見るというのもやる予定なので、楽しみにしておいてください!
もしこの記事が面白いと感じていただけたのならば、是非いいねやコメントをお願いします!また、式や文章の間違い、記事の書き方についてのアドバイス、その他取り上げてほしい題材のリクエスト等あれば遠慮なくコメントしてください!それでは、また次の記事でお会いしましょう!
- 投稿日:2019-10-11T22:34:36+09:00
【圧巻】Unity で Lorentz 方程式の軌道を描画してみた
宇宙の深淵からこんにちは、
\displaystyle\int {\rm d}\boldsymbol{r}\hat{\psi}^{\dagger}(\boldsymbol{r})ぽっぴんフレンズ\hat{\psi}(\boldsymbol{r})です!今回は Unity で Lorentz 方程式の軌道を描画することを内容として記事にしたいと思います!(Lorentz 方程式について知らない方もいるかと思いますが、結果をチラ見せした方が早いでしょう。今回は下の画像のような曲線を描くことを目標にします!もしかしたら、どこかで見たことがあったりするのではないでしょうか?)
ということで、Lorentz 方程式及びアルゴリズムの紹介、そしてコードの実装を見ていきましょう!
Unityのバージョン:
Unity 2018.4.6f1
こんな方にオススメ:
- Unity でゲーム制作以外のことをしてみたい
- 数値計算の結果をいい感じに視覚化したい
- ローレンツ方程式が大好き
Lorentz 方程式とは
Lorentz 方程式は、以下のように表されます。
\begin{align} \frac{{\rm d}x}{{\rm d}t}&=-px+py\\ \frac{{\rm d}y}{{\rm d}t}&=-xz+rx-y\\ \frac{{\rm d}z}{{\rm d}t}&=xy-bz \end{align}$t$ は時間、$x, y, z$ は時間 $t$ の関数あり物体の座標、$p, r, b$ は実数の定数です。左辺は $x, y, z$ の時間微分、右辺は $x, y, z$ を変数とする関数となっています。
Lorentz 方程式は対流の研究のために流体力学の方程式を非常に簡単にしで出来たものらしいのですが、初期値鋭敏性という非常に興味深い特徴がみられる方程式として有名です。初期値鋭敏性とは、簡単に言うと「初めの状態(位置)のほんの僅かな誤差が後々大きな違いとなって現れる」ことです。物理現象を予測する上で初めの状態を測定することは非常に重要ですが、測定には当然誤差がついてきます。Lorentz 方程式ではその誤差の影響で後々の結果が大きく変わってきてしまう、すなわち現象の長期的な予測が予測が非常に困難となるのです!天気予報で遠い未来の天気を予測したいときほど予報が大きく外れるのと同じですね!
方程式といえば、普通は「解」がありますよね?連立方程式であれば片方の文字を消去するなどすれば $x$ と $y$ の値が求まります。 Lorentz 方程式にももちろん解があることが想像されますが、実はこの Lorentz 方程式、解を厳密に(手計算で)求めることができません!(な、なんだってー!!!) しかし、こののような場合のために、厳密な解ではなくそれっぽい解(数値解)を求める方法が存在します。詳しくは次節で取り扱いますが、この数値解を計算することで、手計算で解が出なくとも、方程式のおおまかな振る舞いを知ることができるのです!
Runge-Kutta 法
これから Runge-Kutta 法というアルゴリズムについて紹介します。ぶっちゃけると、本節の内容を理解していなくても、次節で紹介するコードなどをパクってしまえば実装はできてしまいます。なので、この節は最悪読み飛ばしても構いません(本節も一生懸命書いたので、できれば読み飛ばさないでほしいです(迫真))。
Lorentz 方程式のような、関数の微分を含む方程式を微分方程式と呼びます。とくに Lorentz 方程式は時間 $t$ の関数が $x, y, z$ の3つあり、これらが互いに絡まりあっているので、連立微分方程式と呼ばれます。微分方程式は手計算で解を求められるものもありますが、そうでないものが圧倒的に多いのです。そして、前節でも述べたとおり、Lorentz 方程式も手計算では解を求められないものになります。そこで、コンピュータのパワーを借りることによて、「それっぽい解」を求める方法を用います。今回はいくつかある方法の中でも、精度が高い** Runge-Kutta 法**(厳密には4段4次 Runge-Kutta 法)というものを用います。
簡単のため、まずは時間 $t$ の関数が $x$ 1つのみである1階微分方程式( $x$ の $t$ による1階微分のみを含む方程式)を考えましょう。一般的な1階微分方程式は以下のように書かれます。
\frac{{\rm d}x}{{\rm d}t}=f(x, t)左辺は $x$ の $t$ による1階微分、右辺は $x, t$ による何かしらの関数です。この方程式において、時刻 $t=0$ における $x$ の値 $x_0$ から、一定のアルゴリズムに従って次々と方程式の解を生成します。記事の趣旨上、詳しい説明は省き、アルゴリズムの内容のみを紹介します。
h\ は十分小さい正の実数、x_i\ は時刻\ t_i=ih\ における\ x\ の値とする。\\ 時刻\ t_{i+1}=(i+1)h\ における\ x\ の値\ x_{i+1}\ を以下の手順で求める。\\ \begin{align} \\ &k_1=h\times f(x_i, t)\\ &k_2=h\times f(x_i+\frac{k_1}{2}, t+\frac{h}{2})\\ &k_3=h\times f(x_i+\frac{k_2}{2}, t+\frac{h}{2})\\ &k_4=h\times f(x_i+k_3, t+h)\\ \\ &k=\frac{k_1+2k_2+2k_3+k_4}{6}\\ &x_{i+1}=x_i+k \end{align}以上が Runge-Kutta 法のアルゴリズムです。時刻 $t_0=0$ における $x$ の値 $x_0$ を自分で決めておけば、上記のアルゴリズムに従って、時刻 $t_1=h$ における $x$ の値 $x_1$, 時刻 $t_2=2h$ における $x$ の値 $x_2$, $\cdots$, 時刻 $t_i=ih$ における $x$ の値 $x_i$, $\cdots$ が次々に求まるという寸法です。この方法は精度が高く( $x(t+h)$ の $h$ についてのテイラー展開に対し $h^4$ の項まで一致する)、微分方程式の数値計算によく用いられます。
上記のアルゴリズムは $t$ の関数が $x$ の1つだけの場合のものでした。Lorentz 方程式は$t$ の関数が $x, y, z$ の3つあるので、少し改良を加えましょう。まず、$\vec{r}=(x, y, z)$ として $x, y, z$ を1つのベクトルにまとめてしまいましょう。$\vec{r}$ は言うなれば $x, y, z$ を座標とする位置ベクトルになります。そうすると、$x, y, z$による連立1階微分方程式は以下のように表されます。
\frac{{\rm d}\vec{r}}{{\rm d}t}=\vec{f}(\vec{r}; t)\\ \begin{align} ただし、&\vec{r}=(x, y, z),\\ &\vec{f}(\vec{r}; t)=(f_x(x, y, z; t), f_y(x, y, z; t), f_z(x, y, z; t)) \end{align}左辺は $\vec{r}$ の各成分を微分したもの、右辺は $\vec{r}, t$ を変数とするベクトル関数です。ベクトル関数 $\vec{f}(\vec{r}; t)$ は、$x, y, z$ 成分それぞれが異なる形の関数 $f_x, f_y, f_z$ となっています。この微分方程式の Runge-Kutta 法は以下のようになります。
h\ は十分小さい正の実数、\vec{r}_i\ は時刻\ t_i=ih\ における位置とする。\\ 時刻\ t_{i+1}=(i+1)h\ における位置\ \vec{r}_{i+1}\ を以下の手順で求める。\\ \begin{align} \\ &\vec{k}_1=h\times \vec{f}(\vec{r}_i, t)\\ &\vec{k}_2=h\times \vec{f}(\vec{r}_i+\frac{\vec{k}_1}{2}, t+\frac{h}{2})\\ &\vec{k}_3=h\times \vec{f}(\vec{r}_i+\frac{\vec{k}_2}{2}, t+\frac{h}{2})\\ &\vec{k}_4=h\times \vec{f}(\vec{r}_i+\vec{k}_3, t+h)\\ \\ &\vec{k}=\frac{\vec{k}_1+2\vec{k}_2+2\vec{k}_3+\vec{k}_4}{6}\\ &\vec{r}_{i+1}=\vec{r}_i+\vec{k} \end{align}以上のアルゴリズムによって、時刻 $t_0=0$ における位置 $\vec{r}_0$ をはじめに決めておけば、方程式の解となる点が次々と求まります。
以上が、今回用いる Runge-Kutta 法の説明でした。少々難しかったでしょうかね(汗)。次節からいよいよUnity上で実装していきます!
実装
いよいよ、Unity上で Lorentz 方程式を実装していきます!前節の Runge-Kutta 法が良くわからなくても、以下の内容に沿っていけば誰でも実装できますので、頑張っていきましょう!
Unity上での準備
はじめに、Unity で新しいプロジェクト(3D)を開きます(Unityがない人は諦めるか今すぐインストールしてください!)。
Hierarchy
のCreate
からCreate Empty
を押し、空の GameObject を作ります。名前は Point としましょう。また、Inspector
にあるTransform
で Object の位置を設定します(これが初めの位置になります)。今回は $(x, y, z)=(0, 10, 1)$ としましょう。この GameObject に色々なものを付け足すことで、Lorentz 方程式に従って動きながら、軌道が線として残るようにします。カメラの位置と向きも調整しておきましょう。軌道はかなり大きくなるので、カメラも割合遠くに置きます。
Hierarchy
のMain Camera
を押し、Inspector
にあるTransform
で位置を調整します。今回は $(x, y, z)=(0, 25, -60)$ としましょう。これで最低限の準備はできました!次はいよいよスクリプトを書いていきます!
Lorentz 方程式の実装
いよいよ Lorentz 方程式を Unity の C# スクリプトで実装していきますが、ここで一つ面倒な問題があります。それは、Unity での座標軸の取り方と数学・物理学での座標軸の取り方が異なるということです。数学・物理では、水平面に $x-y$ 平面が置かれ、鉛直方向に $z$ 軸が置かれますが、Unity では水平面に $x-z$ 平面が置かれ、鉛直方向に $y$ 軸が置かれます。すなわち、$y$ 軸と $z$ 軸が入れ替わった感じになります。最初に紹介した Lorentz 方程式の座標は数学・物理学での慣習に則っているので、Unity 上で実装する際に元の式の $y$ と $z$ を入れ替えて式を少し変えます(入れ替えなくても動くには動くのですが、軌道が横倒しになった形になります)。以下が、Unity 用に $y$ と $z$ を入れ替えた式になります。
\begin{align} \frac{{\rm d}x}{{\rm d}t}&=-px+pz\\ \frac{{\rm d}y}{{\rm d}t}&=xz-by\\ \frac{{\rm d}z}{{\rm d}t}&=-xy+rx-z \end{align}右辺だけでなく左辺の微分のところも入れ替えることに注意してください。これで準備は整いました。
さて、いよいよコードを書いていきましょう!Unity の
Project
でCreate
を押し、C# Script
を生成します。スクリプト名は LorentzEquation としましょう。スクリプトを開いて以下のコードを書いていきます。LorentzEquation.csusing System.Collections; using System.Collections.Generic; using UnityEngine; public class LorentzEquation : MonoBehaviour { //Lorentz 方程式内の定数 float p = 10.0f; float r = 28.0f; float b = 8.0f / 3.0f; //Runge-Kutta 法で用いる、時間の刻み幅 float h = 5e-3f; //毎フレームごとに Runge-Kutta 法で GameObject の位置をアップデート void Update() { RungeKutta(); } //Runge-Kutta法の関数 void RungeKutta() { Vector3 k1, k2, k3, k4; Vector3 position, deltaPosition; position = this.transform.position; k1 = h * Lorentz(position); k2 = h * Lorentz(position + k1 / 2); k3 = h * Lorentz(position + k2 / 2); k4 = h * Lorentz(position + k3); deltaPosition = (k1 + 2 * k2 + 2 * k3 + k4) / 6; this.transform.position += deltaPosition; } //Lorents 方程式の関数 Vector3 Lorentz(Vector3 position) { float x = position.x; float y = position.y; float z = position.z; // dx/dt=-px+pz // dy/dt=xz-by // dz/dt=-xy+rx-z float xdot = p * (-x + z); float ydot = x * z - b * y; float zdot = -x * y + r * x - z; return new Vector3(xdot, ydot, zdot); } } } // Lorentz 方程式の関数 Vector3 Lorentz(Vector3 position) { float x, y, z; float xdot, ydot, zdot; x = position.x; y = position.y; z = position.z; // dx/dt=-px+pz // dy/dt=xz-by // dz/dt=-xy+rx-z xdot = p * (-x + z); ydot = x * z - b * y; zdot = -x * y + r * x - z; return new Vector3(xdot, ydot, zdot); } }このコードを先ほど生成した GameObject にひも付すれば、Object がローレンツ方程式に従って動いてくれるようになります。
コード内の関数の説明をしていきます。まずは Lorentz 方程式の関数から行きましょう。方程式内の定数はコードの冒頭で $p=10, r=28, b=8/3$ に設定します。
p,r,b//Lorentz 方程式内の定数 float p = 10.0f; float r = 28.0f; float b = 8.0f / 3.0f;GameObject の位置を引数としてローレンツ方程式の右辺を計算し、方程式の左辺を返す関数を作ります。はじめに Vector3 型の引数の $x, y, z$ 成分を x, y, z にそれぞれ格納し、次に方程式の右辺の計算結果を xdot, ydot, zdot にそれぞれ格納します。最後に xdot, ydot, zdot を成分とする Vector3 型の変数を返り値として返します。これで Lorentz 方程式の関数の完成です。
Lorentz()// Lorentz 方程式の関数 Vector3 Lorentz(Vector3 position) { float x, y, z; float xdot, ydot, zdot; x = position.x; y = position.y; z = position.z; // dx/dt=-px+pz // dy/dt=xz-by // dz/dt=-xy+rx-z xdot = p * (-x + z); ydot = x * z - b * y; zdot = -x * y + r * x - z; return new Vector3(xdot, ydot, zdot); }次に、Runge-Kutta 法で GameObject の位置をアップデートする関数について説明します。時間 $t$ の刻み幅 $h$ はコードの冒頭で設定しております。$h$ を大きくとると線が荒くなってしまいますが、小さくとりすぎると GameObject の動きが遅すぎになってしまいます。今回は $h=0.005(=5e-3)$ としました。(※ $h$ として1フレームの時間 Time.deltaTime を用いたい場合は関数の中で $h$ を設定する必要があります。たぶん。)
h//Runge-Kutta 法で用いる、時間の刻み幅 float h = 5e-3f;Runge-Kutta 法の関数は、毎フレームごとに GameObject の位置を取得し、Runge-Kutta 法のアルゴリズムに従って位置の変化量を計算し、それを位置に加算することで GameObject の位置を更新します。まず、Runge-Kutta 法で用いる Vector3 型変数 k1, k2, k3, k4 と、そのフレームでの位置を格納する変数 position 及び Runge-Kutta 法の計算結果を格納する変数 deltaPosition を宣言します。次に、position に GameObject の位置を格納します。そして Runge-Kutta 法のアルゴリズムに従って k1, k2, k3, k4, 及び deltaPosition を計算し、最後に GameObject の位置に deltaPosition を加算します。
RungeKutta()//Runge-Kutta法の関数 void RungeKutta() { Vector3 k1, k2, k3, k4; Vector3 position, deltaPosition; position = this.transform.position; k1 = h * Lorentz(position); k2 = h * Lorentz(position + k1 / 2); k3 = h * Lorentz(position + k2 / 2); k4 = h * Lorentz(position + k3); deltaPosition = (k1 + 2 * k2 + 2 * k3 + k4) / 6; this.transform.position += deltaPosition; }上の関数は1フレーム分の処理なので、これを void Update() 関数内に書くことで毎フレーム処理を行ってくれるようになります。
Update()//毎フレームごとに Runge-Kutta 法で GameObject の位置をアップデート void Update() { RungeKutta(); }これでスクリプトの実装ができました!念のため、本節冒頭で作った GameObject にスクリプトをひも付することを忘れないでください。次は GameObject の軌道を描く方法について紹介していきます!
軌道の描画の実装
前小節で、GameObjectを Lorentz 方程式に従て動かすためのスクリプトを作りましたが、これだけでは Object の軌道が見えません。ここでは、Object の軌道を表示させるための設定をしていきましょう!
Unity には線を描くためのコンポーネントとして Line Renderer というものがあります。まずはこれを最初に作った GameObject に追加しましょう。
Hierarchy
に先ほど生成した Point があるのでそれを押し、Inspector
にあるAdd Component
を押します。検索窓が出てくるので、そこで「Line Renderer」と検索してコンポーネントを追加します。コンポーネントを追加したら、線の太さを変えていきます。width
という名前で謎のグラフがある個所があります。左上のボックスの数字を変えると、線の太さを調整する上限を変えられます。デフォルトで1.0になっているはずですが、これだと太すぎるので、今回は数字を0.15にし、赤い線をマウスで一番上まで引っ張ります。これで線の太さが0.15になります。次に、線を描くスクリプトを作っていきます。
Project
からC# Script
を生成し、以下のコードを書きます。DrawLine.csusing System.Collections; using System.Collections.Generic; using UnityEngine; public class DrawLine : MonoBehaviour { //オObject の軌跡を描画する LineRenderer line; // LineRenderer Componentを受ける変数 int count; // 線の頂点の数 // Start is called before the first frame update void Start() { line = GetComponent<LineRenderer>(); // LineRenderer Componentを取得 } void Update() { count += 1; // 頂点数を1つ増やす line.positionCount = count; // 頂点数の更新 line.SetPosition(count - 1, this.transform.position); // Object の位置情報をセット } }1フレームごとに線を描くための頂点を増やしていき、増やした頂点に GameObject の位置をセットすることで線を描くことができます。尚、頂点の番号は 0, 1, 2, ... のようにゼロから振られるので、新しく追加される点の番号は $点の総数-1$ となることに注意してください(たとえば100個の点で描かれる曲線の100個目の点の番号は99番)。
ここまでで、既に Lorentz 方程式の軌道を描画する準備は整いました!では、いざ実行してみましょう!
実装結果
準備が整ったので、実行してみましょう!...とその前に、以下の点を今一度確認してみてください。
- GameObject に LorentzEquation のスクリプト、DrawLine のスクリプト及び LineRenderer がひも付されているか。
- LineRenderer の線の太さは適切か(オススメ:0.15)。
- LorentzEquation のスクリプト、DrawLine のスクリプトの内容に間違いはないか。
- GameObject の位置は適切か(オススメ:$(0, 10, 1)$ )。
- カメラの位置は適切か(オススメ:$(0, 25, -60)$ )。
何かしらのエラーがあれば
Console
に表示されるので、エラーに従って直してください。すべて確認できたら、いざ実行してみましょう!以下のように動くはずです。(よかったらTwitterのフォローお願いします!)
UnityでLorentz方程式の軌道を描画してみました。#Unity#ローレンツアトラクタ pic.twitter.com/SxJC76hU5l
— ∫drψ†ぽっぴんフレンズψ (@dr30793443) October 11, 2019圧巻ですね。。。
おまけ:カメラの移動
さらなる圧巻を求めて、軌道のまわりを回転しながら眺められるように、カメラに動きを付け足していきましょう!
Project
でC# Script
を生成し、名前を CameraScript としましょう。そして、以下のコードを書いていきます。CameraScript.csusing System.Collections; using System.Collections.Generic; using UnityEngine; public class CameraScript : MonoBehaviour { public float omega = 0.30f; //半径 public float A = 60.0f; //角速度 public float height = 25.0f; //高さ float theta; //回転角 float sinTheta; //回転角のsin float cosTheta; //回転角のcos void Update() { theta = omega * Time.time; //角度=時刻*角速度 sinTheta = Mathf.Sin(theta); //回転角のsinを計算 cosTheta = Mathf.Cos(theta); //回転角のcosを計算 //カメラの位置の代入 this.transform.position = new Vector3(A * sinTheta, height, -A * cosTheta); //カメラの方向の代入 this.transform.forward = new Vector3(-sinTheta, 0.0f, cosTheta); } }はじめに、回転の半径(A)、角速度(Omega)、および回転する位置の高さ(height)を決めます。変数の前に
public
を付けることで、カメラのInspector
から変数をいじれるようになりますが、付けなくても動きはします。A,omega,heightpublic float omega = 0.30f; //半径 public float A = 60.0f; //角速度 public float height = 25.0f; //高さ各フレームにおけるカメラの回転角とその $\sin, \cos$ を格納する変数も用意しておきます。
theta,sin,cosfloat theta; //回転角 float sinTheta; //回転角のsin float cosTheta; //回転角のcosそして、以下の部分でカメラの位置と向きを毎フレーム更新していきます。はじめに theta, sinTheta, cosTheta の値を計算します。最初に計算してしまうことで、$\sin, \cos$ を使いたいたびに Mathf.sin 関数で計算をする手間が省けます。時刻 $t$ におけるカメラの位置は $r = (A\sin{\omega t}, h, -A\cos(\omega t))$ にします。こうすることで、$(x, y, z)=(0, 25, -60)$ からスタートし、上から見て反時計回りに回転してくれます。カメラの向きは this.transform.forward というベクトルをいじることで調整でき、$(-\sin{\omega t}, 0, \cos(\omega t))$ とすることで常に円の中心を向いてくれます。
Update()void Update() { theta = omega * Time.time; //角度=時刻*角速度 sinTheta = Mathf.Sin(theta); //回転角のsinを計算 cosTheta = Mathf.Cos(theta); //回転角のcosを計算 //カメラの位置の代入 this.transform.position = new Vector3(A * sinTheta, height, -A * cosTheta); //カメラの方向の代入 this.transform.forward = new Vector3(-sinTheta, 0.0f, cosTheta); }以上のコードによって、カメラが高さ height=25 にある半径 A=60 の円周上を、角速度 omega=0.30 で、円の中心を向きながら回転してくれます。以下がその結果です。
カメラを回転させた場合です#Unity#ローレンツアトラクタ#プログラミング初心者 pic.twitter.com/efsVBDl162
— ∫drψ†ぽっぴんフレンズψ (@dr30793443) October 11, 2019より臨場感が増しましたね!
まとめ
今回は Lorentz 方程式を Unity で実装し、軌道を描画してみました。皆様、いかがでしたか?個人的には思ったより簡単にできたなという印象だったので、Unity が使える方は是非試してみてください!また、この記事の続きとして、複数の点を少しだけずらして出発させ、初期値鋭敏性が現れる様子を見るというのもやる予定なので、楽しみにしておいてください!
もしこの記事が面白いと感じていただけたのならば、是非いいねやコメントをお願いします!また、式や文章の間違い、記事の書き方についてのアドバイス、その他取り上げてほしい題材のリクエスト等あれば遠慮なくコメントしてください!それでは、また次の記事でお会いしましょう!
- 投稿日:2019-10-11T19:52:12+09:00
WPFでカーソルの位置を取得する(C#)
はじめに
ウィンドウ外にカーソルがあってもその位置を取得する方法です。
調べてもすぐに出てこなかったため、備忘録として残します。動作環境
- Visual Studio Community 2019 - 16.3.2
- .NET Framework - 4.7.03190
方法
System.Windows.Forms
を使用します。まずはソリューションエクスプローラーから参照
を右クリックし、参照の追加
を選択します。
検索欄から"Forms"等と入力して、
System.Windows.Forms
をチェックしてOK
を選択します。
あとは
System.Windows.Forms.Cursor.Position
で場所が取得できます。System.Drawing.Point p = System.Windows.Forms.Cursor.Position; // Labelを作成し、そこに出力するサンプル label.Content = "(x, y) = ("+ p.X +", "+ p.Y +")"; // p.X, p.Yで取得できます。
System.Drawing.Point
で怒られたら同じく参照の追加
からSystem.Drawing.Point
を持ってくると大丈夫です。
- 投稿日:2019-10-11T18:47:22+09:00
Googleスプレッドシートを使ったUnityコラボ開発 (ScriptableObject、Addressables 対応版)
はじめに
- この記事は、過去に公開した記事の焼き直しで、
ScriptableObject
とAddressables
に対応した版です。- また、スプレッドシート側の計算式を使わずに、シート全体を取得して解析するように変更しています。
前提
- unity 2018.4.8f1
- Addressables 1.2.4
- Googleスプレッドシート
- Googleアカウント
このシステムのセキュリティについて
- このシステムのセキュリティ
- スプレッドシート
- 特別な共有設定は不要で、共同編集者のGoogleアカウントに対する共有のみを設定します。
- ドキュメントIDが、平文でエディターの設定内(EditorPrefs)に保存されます。
- GAS (Google Apps Script)
- URLを知っていれば誰でも使用できるように設定します。
- 承認したGoogleアカウントの権限で実行され、承認者のGoogleドライブに保存された全てのスプレッドシートにアクセス可能です。
- URLが、平文でエディターの設定内(EditorPrefs)に保存されます。
できること
- 言語別テキストや定数などをGoogleスプレッドシートで共同編集できます。
- 編集結果は、任意のタイミングでプロジェクトへ取り込むことができます。
- 「シナリオやユーザー向けメッセージなどの編集者」や「各種の初期値を設定する担当者」がプログラマーでない場合に共同制作が容易になります。
- 多言語対応のテキストと、言語に依存しない固定値 (int、float、string、bool)を扱えます。
特徴
- 取り込んだスプレッドシートの情報を、C#スクリプトと言語別ScriptableObjectに変換します。
- ScriptableObjectはAddressablesで管理されます。
- スプレッドシートの取り込みは、別スレッドで実行され、エディターをブロックしません。
導入
アセットの入手 (GitHub)
- ダウンロード ⇒ GetSharedData.unitypackage
Google Apps Script の作成
- The Apps Script Dashboardへアクセスします。
- 「新規スクリプト」を作成し、
getspreadseet
と名付けます。- 「コード.gs」の中身を、以下のコードで置き換えます。
getspreadseet/コード.gsfunction doGet(e) { if (e.parameter.k == '《Access Key》') { var spreadsheet = SpreadsheetApp.openById(e.parameter.d); if (e.parameter.w == null) { // 読み出し if (e.parameter.a != null) { // セル var value = spreadsheet.getSheetByName(e.parameter.s).getRange(e.parameter.a).getValue(); Logger.log (value); return ContentService.createTextOutput(value).setMimeType(ContentService.MimeType.TEXT); } else if (e.parameter.s != null) { // シート var values = spreadsheet.getSheetByName(e.parameter.s).getDataRange().getValues(); Logger.log(values); return ContentService.createTextOutput(JSON.stringify(values)).setMimeType(ContentService.MimeType.TEXT); } else if (e.parameter.id != null) { // シートID一覧 var sheets = spreadsheet.getSheets(); var sheetids = [sheets.length]; for (var i = 0; i < sheets.length; i++) { sheetids [i] = sheets [i].getSheetId(); } Logger.log(sheetids); return ContentService.createTextOutput(JSON.stringify(sheetids)).setMimeType(ContentService.MimeType.TEXT); } else { // シート名一覧 var sheets = spreadsheet.getSheets(); var sheetnames = [sheets.length]; for (var i = 0; i < sheets.length; i++) { sheetnames [i] = sheets [i].getName(); } Logger.log(sheetnames); return ContentService.createTextOutput(JSON.stringify(sheetnames)).setMimeType(ContentService.MimeType.TEXT); } return ContentService.createTextOutput("[]").setMimeType(ContentService.MimeType.TEXT); // 空json } else { // 書き込み if (e.parameter.a != null) { // セル spreadsheet.getSheetByName(e.parameter.s).getRange(e.parameter.a).setValue(e.parameter.w); } else if (e.parameter.s != null) { // シート var sheet = spreadsheet.getSheetByName(e.parameter.s); if (sheet != null) { sheet.getDataRange().clear(); } else { sheet = spreadsheet.insertSheet(e.parameter.s); } var values = JSON.parse(e.parameter.w); sheet.getRange(1,1,values.length,values[0].length).setValues(values); sheet.autoResizeColumns (1, values[0].length); if (e.parameter.r != null) { sheet.setFrozenRows(e.parameter.r); } if (e.parameter.c != null) { sheet.setFrozenColumns(e.parameter.c); } } return ContentService.createTextOutput("").setMimeType(ContentService.MimeType.TEXT); // 空 } } } function doPost (e) { return doGet (e); }
- スクリプト冒頭の「《Access Key》」を、キーワードで書き換えてください。
- これは秘密のURLの一部になるので、ランダムで十分長い文字列にしてください。
- このキーワードは、後で使用しますので、何処か安全な場所に控えておいてください。
- スクリプトを保存します。
- 「公開」メニューから、「ウェブ アプリケーションとして導入」を選び、以下を選択します。
- プロジェクト バージョン:
New
- 次のユーザーとしてアプリケーションを実行:
自分(~)
- アプリケーションにアクセスできるユーザー:
全員(匿名ユーザーを含む)
- 「導入」ボタンを押します。
続いて、スクリプトを「承認」する必要があります。
- 承認の進め方
- 「承認が必要です」と言われたら「許可を確認」するボタンを押します。
- 「このアプリは確認されていません」と表示されたら「詳細」をクリックします。
- 「getspreadsheet(安全ではないページ)に移動」と表示されるので、クリックします。
- 「getspreadsheet が Google アカウントへのアクセスをリクエストしています」と表示されるので、「許可」します。
- 「現在のウェブ アプリケーションの URL」が表示されます。
- このURLは、後で使用しますので、何処か安全な場所に控えておいてください。
- これによって、URLを知る誰でも、承認したアカウントの権限で、スクリプトが実行可能になります。
- スクリプトは、アカウントにアクセス権がありドキュメントIDが分かる任意のスプレッドシートを読み取ることができます。
- スクリプトと権限は、いつでもThe Apps Script Dashboardから削除可能です。
プロジェクトの準備
スプレッドシートの用意
- スプレッドシートの雛形を開き、ファイルメニューから「コピーを作成…」を選びます。
- フォルダを選んで保存します。
- Googleドライブに保存されます。(ドライブの容量は消費しません。)
- マイドライブから保存したフォルダへ辿り、コピーされたスプレッドシートを開きます。
- 開いたスプレッドシートのURLで、
https://docs.google.com/spreadsheets/d/~~~/edit#gid=~
の「/d/~~~/edit#」の部分に注目してください。
- この「~~~」の部分が、このスプレッドシートの
Document ID
です。- このIDは、後で使用しますので、何処か安全な場所に控えておいてください。
Addressablesの導入
- パッケージマネージャーを使ってAddressablesを導入します。
- Window (Menu) > Package Manager > Pakkages (Window) > Addressables > Install (Button)
アセットの導入と設定
- プロジェクトにアセット「GetSharedData.unitypackage」を導入してください。
- 「Window」メニューの「GetSharedData > OPen Setting Window」を選択しウインドウを開きます。
- 「Application URL*」に、控えておいた「ウェブ アプリケーションの URL」を記入します。
- 「Access Key*」に、控えておいたキーワードを記入します。
- 「Document ID」に、控えておいたスプレッドシートのIDを記入します。
- 「Asset Folder」は、プロジェクトのフォルダ構造に合わせて書き換え可能です。
- 設定はエディターを終了しても保存されます。
- 「*」の付く設定は、プロジェクトを跨いで共有されます。
- その他の設定はプロジェクト毎に保持されます。
- プロジェクトは、
Player Settings
のCompany Name
とProduct Name
で識別されますので、適切に設定してください。- 全ての設定は、EditorPrefsに平文で保存されます。
使い方
スプレッドシート
- 「Text」と「Const」は、シート名として予約されています。
- シートの追加や並び替えは任意に行うことができますが、シート名の重複はできません。
- 「Text」シートに記載したものは言語切り替えテキスト、「Const」シートに記載したものは定数に変換されます。
- 他のシートは無視されます。
- シートでは、行を3つの部分(「列ラベル」、「列ラベルより上」、「列ラベルより下」)に分けます。
- 「Key」と書かれたセルが最初に見つかった行が、列ラベルになります。
- 列ラベルより上は無視されます。
- 「Key」、「Type」、「Value」、「Comment」、および、
UnityEngine.SystemLanguage
で定義されている名前は、列ラベル名として予約されています。
- ラベルの文字種や空白文字の有無は区別されます。
- 予約されていない名前の列は無視されます。
- セルの値(計算結果)だけが使われ、シートの計算式や書式は無視されます。
- 「Text」シートでは、「Key」および「Comment」列と、任意の数の言語名の列を記入します。
- 最も左の列の言語がデフォルトになります。
- セル内改行は、改行文字
\n
に変換されます。- 「Const」シートでは、「Key」、「Type」、「Value」、「Comment」列を記入します。
- キーとして、「BuildNumber」、「BundleVersion」が予約されています。
Unityエディター
- GetSharedDataウィンドウの「GetSharedData」ボタンを押すか、Windowメニューの「GetSharedData > Get Spreadsheet」を選ぶか、そのメニュー項目に表示されるキーボードショートカットを使うことで、スプレッドシートの情報が取り込まれ、コンパイルされます。
Addressables
- 言語別テキストを格納したアセット
Text_言語名.asset
は、Addressablesに入れてください。
- アドレスを単純化(Simplify)して、名前だけにしておいてください。
- 再取り込みを行う際は、ファイルは作り直されずに内容だけが上書きされます。
- Unityエディターで内容を編集することは想定していません。
スクリプトでの使用
using SharedConstant;
を指定してください。テキスト
- システムの言語設定(
Application.systemLanguage
)に従って初期設定されます。SystemLanguage Txt.Locale
言語 (プロパティ)
SystemLanguage
型の値を代入することで、強制的に言語を切り替えます。SystemLanguage.Unknown
を指定すると、システムの言語設定に従います。- 相当する言語がアセットにない場合は、デフォルト(シート最左)の言語が使われます。
- 切り替えてから実際に値が変化するまでには、アセットをロードする時間分のラグがあります。
string Txt.S (Nam.key[, ...])
テキスト (メソッド)
- キーを指定して現在設定されている言語のテキストを返します。
- キーを列挙すると該当するテキストを連結して返します。
string Txt.F (Nam.key, object arg[, ...])
フォーマットテキスト (メソッド)
- 指定したキーをフォーマットとして他の引数を展開したテキストを返します。
int Txt.N (string str)
キーの逆引き (メソッド)
Txt.S (~)
で得たテキストを渡すと、該当のキーを返します。- 未定義なら
-1
を返します。- キー
Nam.~
は、const int
として定義された文字列のインデックスです。数値
- キーは
const
として定義されます。Cns.~
として使用します。Cns.BuildNumber
、Cns.BundleVersion
が自動的に定義されます。Unity使用者が複数いる場合の留意点
- 例えば、「Unityを使わない企画者、Unityを使うデザイナとプログラマの3人」といったように、スプレッドシートからUnityに情報を取り込むメンバーが複数いる場合は以下の点に留意してください。
- メンバーで共有するアセットの設定は、「Document ID」と「Assets/Scripts/ Folder」だけです。
- 「Application URL*」と「Access Key*」は共有してはいけません。つまり、各メンバーそれぞれがGoogle Apps Script の作成を行う必要があります。
- なお、これらの設定は、プロジェクトのフォルダには保存されないので、GitHubなどによるアセットの共有で問題が生じることはありません。
- 投稿日:2019-10-11T18:35:05+09:00
.NET Core2.2 で、IFormFileをモデルと一緒にリストにするとPostが終わらない
初めに
画像アップロードで、同じモデルの項目を一括で複数Postしようとした際に、Post処理が終わらないことがあったので原因を調査しました。
環境
- .NET Core2.2 ※ここ重要
- Visual Studio 2019
症状
submitでPostを実行すると、Postが完了せずに処理が終わらない。
また、メモリ使用量がどんどん上がっていく。
原因
・モデルと一緒にIFormFileをリストにすると起きます。
(View内にIFormFile用のタグを記載しなくても、コードがそうなっているだけで症状が出ます。)↓↓ .NET Core2.2のバグらしいです。(GitHubのissueに載ってました)
IFormFile bind error when using in sub collection対応
・ .NET Coreのバージョンを 2.1.7に戻すか、3.0にあげる。
・ モデルと一緒にせずに、IFormFileだけでListにする(IFormFileCollection使ってもいい)再現方法
1. .NET Core2.2を使用する。
2. 適当にモデルを作成Movie.cspublic class Movie { [Display(Name = "タイトル")] public string Title { get; set; } [Display(Name = "詳細")] public string Detail { get; set; } [Display(Name = "画像")] public string Img { get; set; } }3. 上記のモデルと「IFormFile 」のプロパティを含むモデルを作成
Movie.cspublic class MovieWithPost { public Movie Movie{ get; set; } // ↓ファイル受信用のプロパティを追加 public IFormFile FormFile { get; set; } }4. 上記で作成したモデルをリストにする
Movie.cspublic class MoviesViewModel { public List<MovieWithPost> MovieList { get; set; } }5. viewでsubmitでpostを行う(コードは割愛)
6. Postが完了しない日本語の記事がもっと増えてくれたらうれしいですね。
- 投稿日:2019-10-11T13:02:38+09:00
Unity玉転がしチュートリアル 2-2.プレイエリアの構成
この記事の対象者
- Unity入門したい人
- 最初の一歩が踏み出せない人
OSとか環境とか
- Windows 10 Pro
- macOS Mojave
- Unity 2019.2.8f1
- Rider 2019.2.2
補足
- 公式動画にて利用しているのはMacなので、Windowsユーザーはある程度脳内変換して見る事
- 筆者はWindows、Macの両方の環境で確認。Ubuntuとかでは検証してない。
- 基本Unityは英語メニューで利用
- 間違いがあったらツッコミ大歓迎
公式
https://unity3d.com/jp/learn/tutorials/projects/roll-ball-tutorial/setting-play-area
プレイエリアの作成
4方向を壁で囲まれたシンプルで奥深いプレイエリア
という条件の元、プレイエリアを作成するオブジェクトの整理
GameObject>Create Emptyで名前をWallsとしてGameObjectを作成する
→これは親オブジェクトとなり、壁群を整理する為に利用するProjectの整理はフォルダ
Hierarchyの整理にはゲームオブジェクトを利用
→Emptyはフォルダのように利用出来るので覚えておく事追加したらいつもどおり原点に配置
壁の追加
GameObject>3D Object>Cubeで名前をWest wallとしてGameObjectを作成する
Wallsの子としてWast Wallを配置する
CubeのTransformでスケールのXYZをゲームエリアに合わせる
今回は
Scale
X:0.5
Y:2.0
Z:20.5Position
X:-10
とする上記値を入れると、プレイエリアにぴったりな壁が出来ます
※出来ない場合は、GroundのScaleを調整しましょうこのまま4つの壁を同様の手順で追加しても良いが、せっかくなのでコピーする
Edit>Duplicateで複製が可能複製したら名前をEast Wallへ変更する
Position
X:10
とすれば、逆側に壁が出来上がる同じく残りの壁も作成する
パラメータはこんな感じですNorth Wall
Scale
X:20.5
Y:2.0
Z:0.5Position
Z:10South Wall
Scale
X:20.5
Y:2.0
Z:0.5Position
Z:-104つ配置したら、玉が外に出ていかないエリアが完成します
気の済むまでボールを動かしましょうここまで来るとある程度「あれ?ゲーム出来る?」って感じがします
- 投稿日:2019-10-11T09:05:54+09:00
.Net Core2.2でappsetting.jsonの値を使用するには
環境
Tool Version .Net Core 2.2 appsetting.json は以下とします。
appsetting.json{ "Sample": { "XXX": "XXX_Value" } }DI を利用
Startup.cs の ConfigureServices で appsetting.json のセクションを登録して置き、DI を利用して値を取得する方法です。
SampleConfig.cspublic class SampleConfig { public string XXX { get; set; } }Startup.cspublic class Startup { public void ConfigureServices(IServiceCollection services) { //.... services.AddOptions().Configure<SampleConfig>(Configuration.GetSection("Sample")); } }SampleService.cspublic class SampleService { private readonly IOptions<SampleConfig> _sampleConfig; public SampleService(IOptions<SampleConfig> sampleConfig) { _sampleConfig = sampleConfig; } public void SampleMethod() { string xxx = _sampleConfig.Value.XXX; } }コンストラクタで Value の値を入れてもいいかもしれません。
DI を利用しない
Startup.cs の ConfigureServices で値を利用したい場合は、DI を利用することはできません。
Configuration.GetValue
を利用この場合は値の単一取得になります。
:
をつなげていくことで階層を指定して取得が可能です。Startup.cspublic class Startup { public void ConfigureServices(IServiceCollection services) { //.... string xxx = Configuration.GetValue<bool>("Sample:XXX"); // or Configuration.GetSection("Sample").GetSection("XXX").Value; // or Configuration.GetSection("Sample:XXX").Value; } }Section の存在チェックがしたい場合は、
Exists
を使えばよいです。if (Configuration.GetSection("Sample:XXX").Exists()) { // 存在した場合の処理 }
GetChildren
による取得もありますが、GetSection で取れればいいのではないでしょうか。Bind を利用
複数の値を取得したい場合はこちらのほうが良さそうです。
Startup.cspublic class Startup { public void ConfigureServices(IServiceCollection services) { //.... var sampleConfig = new SampleConfig(); Configuration.GetSection("Sample").Bind(sampleConfig); } }参考
- 投稿日:2019-10-11T01:17:39+09:00
噛み砕いたC#(json読み込み)
JavaScriptでやった覚えがあったぐらいでしたが。。
C#だとこんな感じみたいです。
今回はlivedoorから提供されているWeather Hacks
のAPIを使用する。まずは東京のお天気情報のリクエストURLを叩いてレスポンスを確認しよう。
http://weather.livedoor.com/forecast/webservice/json/v1?city=1300101,Visual Studioで新規コンソールアプリプロジェクトを作成したら、まずHttp通信ができるようする。
参照を右クリックで追加
2,開いたメニューの検索窓にHttpと入力し、System.Net.Httpを選択し、OKボタンを押す
3,Jsonパースができるようにする。こんどはパッケージを右クリック。
Manage NuGet Packages…をクリック4,開いたメニューの検索窓にJsonと入力し、Newtonsoft.Jsonにチェックをいれてパッケージを追加を押す
Program.csusing System; using System.Net.Http; using Newtonsoft.Json.Linq; namespace WeatherApp { class MainClass { public static void Main(string[] args) { //WebAPIのURL string url = "http://weather.livedoor.com/forecast/webservice/json/v1?city=130010"; //HttpClientインスタンス作成(using System.Net.Http;を忘れないこと) HttpClient client = new HttpClient(); //Get通信して結果を文字列として取得 string result = client.GetStringAsync(url).Result; //表示 //Console.WriteLine(result); //今回はルートが{で始まるオブジェクトなのでJObject.Parse,ルートが配列の場合はJArray.Parseを用いる //(using Newtonsoft.Json.Linq;を忘れないこと) JObject jobj = JObject.Parse(result); //表示してみる Console.WriteLine(jobj); } } }これを三日のみ出す場合
Program.csusing System; using System.Net.Http; using Newtonsoft.Json.Linq; namespace WeatherApp { class MainClass { public static void Main(string[] args) { //WebAPIのURL string url = "http://weather.livedoor.com/forecast/webservice/json/v1?city=130010"; //HttpClientインスタンス作成(using System.Net.Http;を忘れないこと) HttpClient client = new HttpClient(); //Get通信して結果を文字列として取得 string result = client.GetStringAsync(url).Result; //表示 //Console.WriteLine(result); //今回はルートが{で始まるオブジェクトなのでJObject.Parse,ルートが配列の場合はJArray.Parseを用いる //(using Newtonsoft.Json.Linq;を忘れないこと) JObject jobj = JObject.Parse(result); //表示してみる //Console.WriteLine(jobj); //jobjが持つforecastsは配列なので以下のようにその配列部分を取得する。 JArray jarr = (JArray)jobj["forecasts"]; //jarrに入っているのはオブジェクトなので以下のようなforeachで回す foreach(JObject f in jarr) { //fとして取り出したJObjectのdateLabelは文字列なのでstringでキャストしてあげる string dateLabel = (string)f["dateLabel"]; //確認してみる //Console.WriteLine(dateLabel); string telop = (string)f["telop"]; string date = (string)f["date"]; Console.WriteLine($"{dateLabel}({date})...{telop}"); } } } }実際は10分ぐらいの説明でふんふんと聞いてただけだったのでちゃんと理解しなきゃ。。