#瞬間移動
例えば、
「左クリックする毎に赤いキューブをy軸で45度回したい」
「右クリックする毎に赤いキューブと青いキューブの場所が入れ替わる」
これを文章見たまま実装すると、
using UnityEngine;
public class Main : MonoBehaviour
{
[SerializeField]
private GameObject aCube;
[SerializeField]
private GameObject bCube;
void Update()
{
if (Input.GetMouseButtonDown(0))
{
aCube.transform.localRotation *= Quaternion.Euler(0, 45, 0); //45度回転!
}
if (Input.GetMouseButtonDown(1))
{
var aPos = aCube.transform.localPosition;
var bPos = bCube.transform.localPosition;
aCube.transform.localPosition= bPos;
bCube.transform.localPosition= aPos;
}
}
}
実際に確かめてみるとこんな感じになります。
まぁ、要件は満たしていますね。
しかし、「45度回っている」というより、なんだか0⇔45度を繰り返しているようにも見えます。 これが90度回転だったら回していることに気づかないでしょうし。
入れ替えも瞬間入れ替わってしまうので、仮にどちらかのキューブが画面外にあったら入れ替わっているようには見えないでしょう。
#スムーズにしよう!
そんな状況を打破すべく、こんなコンポーネントを作ってみました
using UnityEngine;
namespace KszUtil
{
public class SmoothTransform : MonoBehaviour
{
public Vector3 TargetPosition;
public Vector3 TargetScale;
public Quaternion TargetRotation;
public float TimeFact { set; get; } = 0.5f;
public void Start()
{
TargetPosition = transform.localPosition;
TargetScale = transform.localScale;
TargetRotation = transform.localRotation;
}
public void Update()
{
var t = 1 - Mathf.Pow(0.1f, Time.deltaTime / TimeFact); //TimeFact秒で今いる場所から1/10まで間を詰めるための値
transform.localPosition = Vector3.Lerp(transform.localPosition, TargetPosition, t);
transform.localScale = Vector3.Lerp(transform.localScale, TargetScale, t);
transform.localRotation = Quaternion.Lerp(transform.localRotation, TargetRotation, t);
}
}
}
この上記スクリプトを赤いキューブ青いキューブ(aCube
,bCube
)それぞれに付けます。
そして、右クリックで45度回転、左クリックで入れ替え。 をやっている処理もちょっとだけ修正します。
using KszUtil;
using UnityEngine;
public class Main : MonoBehaviour
{
[SerializeField]
private SmoothTransform aCube; //GameObjectだったところを ↑SumoothTransoformに
[SerializeField]
private SmoothTransform bCube; //GameObjectだったところを ↑SumoothTransoformに
void Update()
{
if (Input.GetMouseButtonDown(0))
{
//transform.rotation ではなく、TargetRotation に
aCube.TargetRotation *= Quaternion.Euler(0, 45, 0);
}
if (Input.GetMouseButtonDown(1))
{
//transform.positionではなく、TargetPositionに
var aPos = aCube.TargetPosition;
var bPos = bCube.TargetPosition;
bCube.TargetPosition = aPos;
aCube.TargetPosition = bPos;
//TODO 今風に書くならこう
//(aCube.TargetPosition, bCube.TargetPosition) = (bCube.TargetPosition, aCube.TargetPosition);
}
}
}
どちらも途中クリックを連打していますが、破綻していません。
どうですか? なんだか、ちょっとだけクオリティが上がった気がしませんか?
#何をやっているか
SmoothTransform内で使用されている Vector3.Lerp,Quaternion.lerp
は、渡された二つの値の内分した値を返却してくれる便利なメソッドです。
TargetPositionの処理だけ抜くと
transform.localPosition = Vector3.Lerp(transform.localPosition, TargetPosition, t);
となっていますね。
この t
が内分点で0~1の値を指定します。 仮にこれが毎回 0.1f(10%)
だとし transform.localPosition
の xが0.0f
TargetPosition
のxが5.0f
だったとすると
初回フレームでは
transform.localPosition = (0.0f → 5.0f までの距離の10%進んだところ = 0.5f);
で transform.localPosition
のxは0.5f
となり、0.5f
動いたことになります。
次フレームでは、
transform.localPosition = [0.5f → 5.0 までの距離の10%進んだところ = 0.95f];
で transform.localPosition
のxは0.95f
となり、前回フレームから0.45f
動いたことになります。
次フレームでは、
transform.localPosition = [0.95f → 5.0 までの距離の10%進んだところ = 1.355f];
で transform.localPosition
のxは1.355f
となり、前回フレームから0.405f
動いたことになります。
このように、移動速度がちょっとずつ落ちていく ので、後になればなるほど速度が遅くなり疑似的(?)に「イーズアウト」の状態になっています。
##利点
利点は、**「すごく離れた値でも、そこそこの時間で目的値になる」事と、「何も考えずにTargetに場所・回転・サイズを入れるだけで破綻なくアニメーションしてくれる」**事です。
例として、「左クリックで45度回転」に
「クリックするたびにちょっと右に移動、ある程度右に行ったら元の位置へ。」を追加します。
if (Input.GetMouseButtonDown(0))
{
aCube.TargetRotation *= Quaternion.Euler(0, 360 - 45, 0);
aCube.TargetPosition += new Vector3(1,0,0); //x方向に1移動
if (aCube.TargetPosition.x > 3.0f) aCube.TargetPosition.x = 0.0f; //ある程度右へ行ったら最初の位置へ
}
ついでに遷移スピード SmoothTransform.TimeFact
をちょっと1.0fと遅くしてあるのですが、
最後は4回連続でクリックしています。 すると、内部的には TargetPosition.x は 0 → 1 → 2 → 3 → 0 と目まぐるしく変化しているんですが、特にアニメーションと停止処理等入れなくてもそれなりに動いてくれているのがわかると思います。
便利ですね!!?
#考え方・注意点
UnityのtransformによるlocalPositionやlocalRotation,localScaleなどは、見た目に直結しています。これらの値を変更したのに見た目は変更しない。 ということができません。
そのため、今回のようにスムーズに移動・回転・(拡縮)をしたい。となった場合は
これらlocalPositionやlocalRotation,localScaleは見た目用(演出用)と割り切って、論理的な位置・回転・サイズを表すものを別に用意してあげる必要があります。 それが SmoothTransform に用意した TargetPosition
TargetRotation
TargetScale
です。
そして、この SmoothTransform の機能を使う以上は、(基本的には)localPositionやlocalRotation,localScaleを直接参照してはいけないです。
ちょっとわかりづらいので駄目な例を一つ
先ほどの「右クリックで赤キューブと青キューブ入れ替え」の処理ですが、こうやってしまいがちです。
if (Input.GetMouseButtonDown(1))
{
//var aPos = aCube.TargetPosition; //わざわざ1回変数に入れるの無駄に見えるので
//var bPos = bCube.TargetPosition;
bCube.TargetPosition = aCube.transform.localPosition; //入れ替えたいキューブの位置を直接指定
aCube.TargetPosition = bCube.transform.localPosition;
{
これは一旦は上手くいきます。でも、クリックを連打するとあっという間に破綻します。
ゆっくり入れ替える分には問題ない(ように見える)のですが、移動途中でまたクリックされると、transform.localPositionの値はあくまでも演出用の値なので、移動中の本来なら知らなくて良い位置が入っています。
そこを目的地(TargetPosition)としてしまっているので、最終的には赤キューブも青キューブも同じ場所に重なってしまいました。
これは**「演出と実データを混同してしまった結果」**です。
今回のこのコンポーネントはあくまでも**「実データを変更したついでに、演出としてスムーズに移動してほしい」**という用途なのでご注意を・・・。
#ちょっと横道に逸れますが
これに限らず演出と実データの切り分けが出来ていないゲームを時々見かけます。
ここでいう「演出」とは「無くても根本のゲーム性に問題が無い挙動」の事を指しています。 例えば、マリオがダッシュすると画像を切り替えアニメーションして手足を速く動かしていますが、別に画像を切り替えがなくてもゲーム性は損なわれない(走っている感はなくなるかもしれませんが)ですよね。
「手足が速く動いている(演出)」から「速く動く(実データ)」ではなく、
「速く動く(実データ)」ので、「手足を速く動かす(演出)」ということです。
何かゲームを作るときに「これは演出なのかな?実データなのかな?」はちゃんと考える必要がありますよね。1
#最後に
使ってみて何か不具合や改善点等ありましたらご連絡いただければと思います。(もちろん、勝手に修正して使っていただいても結構です。2)