今回は「シーンの物理現象をシミュレートする」という、一見分かりにくいAPIのPhysics.Simulateおよび Physics2D.Simulateついてです。
物理演算を指定秒数進めるAPI
このSimulate系のAPIは、簡単に言えば「物理演算を指定秒数、進める」事のできるAPIです。
通常は物理演算は「時間」によって進みますが、コレをスクリプトから超加速出来る訳です。
と言っても凄く癖のある機能で、
- RigidbodyもしくはRigidbody2Dに動作する
- FixedUpdateは呼ばれない
- 1回に進められる時間はFixedDeltaTime(0.0167秒)辺りが限界*1
- 他のRigidbody等の接触により結果が変わる事がある*2
という、中々に微妙な機能でもあります。
物理演算を進める
実際の操作を見てみます。
まずSimulate系のAPIですが、PhysicsのautoSimulateがOFFになっていることが前提条件です。ONだと動作しません。
まあ通常はInspectorよりはPhysics.autoSimulationで切り替えます。
コレを無効にすると、ゲームループ内で自動的にPhysics.Simulateが呼ばれなくなります。ただ MonoBehaviour .FixedUpdateは普通に呼ばれます。
まずはAutoSimulationを手前でやってみます。
下のコードでは、コンポーネントが有効なときにのみ物理演算のシミュレートが進みます。
using UnityEngine; | |
public class AutoSim : MonoBehaviour { | |
private void Awake() | |
{ | |
Physics.autoSimulation = false; | |
} | |
private void OnDestroy() | |
{ | |
Physics.autoSimulation = true; | |
} | |
void FixedUpdate () { | |
Physics.Simulate(Time.fixedDeltaTime); | |
} | |
} |
次にn倍速にしてみます。0.1から1.5倍速あたり。これは1回のFixedUpdateで進める時間を調整すれば実現出来ます。
using UnityEngine; | |
public class AutoSim : MonoBehaviour { | |
[SerializeField][Range(0.1f, 1.5f)] float speed = 0.1f; | |
private void Awake() | |
{ | |
Physics.autoSimulation = false; | |
} | |
private void OnDestroy() | |
{ | |
Physics.autoSimulation = true; | |
} | |
void FixedUpdate () { | |
Physics.Simulate(Time.fixedDeltaTime * speed); | |
} | |
} |
これは所謂Time.Timescaleと異なり、時間軸自体は通常通り動く所が面白いところです。現象だけ遅くする事が出来るので、世界をゆっくりにして云々するのも面白いかもしれません。気分はカブト*3
ただコレは、移動も物理演算に頼っている場合は、少し面倒なことになるかもしれません。単純に時間を弄る系はTime.TimeScale弄ったほうが最終的に楽できます。
物理演算を指定秒数進める・巻き戻す
さて、次は物理演算を指定秒数進めてみます。
時間指定はそれ程難しくはありません。指定時間に到達するまで秒数分ぶん回すだけです。パーティクルのPrewarmと同じようなものです。
強いて問題を上げるとすれば、高い負荷を計上する事があるという点ですが、まぁ。*4
using UnityEngine; | |
public class PSim : MonoBehaviour { | |
[SerializeField][Range(1, 5)] float maxTime = 0.1f; | |
private void Awake() | |
{ | |
Physics.autoSimulation = false; | |
} | |
private void Start() | |
{ | |
float time = 0; | |
while(time < maxTime) | |
{ | |
Physics.Simulate(Time.fixedDeltaTime); | |
time += Time.fixedDeltaTime; | |
} | |
} | |
private void OnDestroy() | |
{ | |
Physics.autoSimulation = true; | |
} | |
} |
では巻き戻しは? …実は出来ません。
なので、「0秒の状態を保持しておいて、毎回0秒から指定秒まで演算を進める」という超力技で実現します。
下の2つは、このアプローチで巻き戻しを実現しています。
ただ、このときの動作が「他の物理演算の有無」により結構誤差が出ます。また負荷も半端ないので、実際にゲームで使う際にはTransform単位でキャプチャするほうが現実的かもしれません。
未来予測
最後に未来予測です。これも基本的には「巻き戻し」と同じ考え方です。
要するに、指定時間まで秒を進めて結果を回収する…という物です。
using System.Collections; | |
using System.Collections.Generic; | |
using UnityEngine; | |
[RequireComponent(typeof(LineRenderer))] | |
public class Simulate : MonoBehaviour | |
{ | |
[SerializeField] Transform target; | |
[SerializeField] LineRenderer line; | |
[SerializeField][Range(0.001f, 10)] float maxTime = 5; | |
[SerializeField] [Range(0, 12)] int power = 5; | |
private List<Vector3> positions = new List<Vector3>(); | |
private Vector3 presposition = Vector3.zero; | |
private int prePower = -1; | |
private Transform startPosition; | |
private void Start() | |
{ | |
startPosition = transform; | |
} | |
void Update () | |
{ | |
Physics.autoSimulation = false; | |
if (presposition == transform.position && power == prePower) | |
return; | |
presposition = transform.position; | |
prePower = power; | |
positions.Clear(); | |
ResetRigidbody(); | |
AddForce(); | |
ProgressSimulate(); | |
line.positionCount = positions.Count; | |
line.SetPositions(positions.ToArray()); | |
} | |
private void OnDisable() | |
{ | |
Physics.autoSimulation = true; | |
ResetRigidbody(); | |
} | |
private void AddForce() | |
{ | |
var rig = target.GetComponent<Rigidbody>(); | |
rig.AddForce(new Vector3(1, 1, 0) * power, ForceMode.Impulse); | |
} | |
private void ProgressSimulate() | |
{ | |
float time = 0; | |
float deltaTime = Time.fixedDeltaTime; | |
while (time < maxTime) | |
{ | |
Physics.Simulate(deltaTime); | |
time += deltaTime; | |
positions.Add(target.position); | |
} | |
} | |
void ResetRigidbody() | |
{ | |
target.position = startPosition.position; | |
target.rotation = startPosition.rotation; | |
var rig = target.GetComponent<Rigidbody>(); | |
rig.velocity = Vector3.zero; | |
rig.angularVelocity = Vector3.zero; | |
} | |
} |
もっと軽くても良いよ!という場合は、下の設定にすると、大分負荷が減ります。
ただし計算精度も雑になります。まぁゲームのちょっとした弾道予想ではコチラの方が都合が良いかもしれません。
- float deltaTime = Time.fixedDeltaTime を float deltaTime = Time.fixedDeltaTime * 10; に変更
(計算制度を雑にする) - 予測したいRigidbodyのCollision DetectionをContinuous Dynamicに設定
感想
中々に楽しい機能ではあるのですが、大体において「重い」です。複数フレームでやるような処理を1フレームでやってるので、仕方がないとも言えます。
個人的には、用途としては「弾道予想」と「エディターで物理演算をレコードする時に使う(GameObjectRecorder)」くらいかなと思ってます。
今回は3Dの紹介でしたが、2Dでも同じAPIがあります。動作も同じです
関連
普通に高校で習う内容で落下位置を予測しようぜ!という話。
複雑な地形や複数のオブジェクト間の接触、それの最適化等を考えるとしんどい
たぶん見るであろう「壁貫通」について
TImeScaleで行うスローモーション