今回はタイトルでは何がやりたいのかわからないシリーズです。
コライダーがついているオブジェクトを同じ場所に複製すると、複製した瞬間にコリジョン判定が起きます。
それを避けるために、レイヤーを分けてコリジョン判定を起きないようにすると、今度は二度と複製したオブジェクトに触れることができなくなります。
複製した瞬間は当たって欲しくないし、その後は当たって欲しい!
それを何とかするために、少しだけ頑張ってみたお話です。
ちなみに話の前半の方は、前回のドミノ倒しを作った内容と同じような感じになっています。
この記事にはUnity2017.1.1f1を使用しています。
- まず下準備として移動制御を作ろう
- さあ、複製していくぞ
- レイヤーを分けて複製した瞬間に対応だ
- 当たらないときの見た目を変えよう
- 一定距離離れたらレイヤーをチェンジだ
- コライダーから離れたらレイヤーをチェンジだ
- やったことを振り返ろうかな
まず下準備として移動制御を作ろう
本題に入る前にオブジェクトの移動制御処理を作りましょうか。
これがないと、本題の実験が出来ません。
オブジェクトを左右前後に移動する処理と、ジャンプする処理を作りましょう。
ObjectController.cs
using UnityEngine; [RequireComponent( typeof( Rigidbody ) )] public class ObjectController : MonoBehaviour { [Header( "Control" )] [SerializeField] private bool m_controlled = true; [SerializeField] private float m_moveSpeed = 0.0f; [SerializeField] private float m_turnSpeed = 0.0f; [SerializeField] private float m_jumpForce = 0.0f; private Rigidbody m_rigidbody = null; private void Awake() { m_rigidbody = GetComponent<Rigidbody>(); } private void Update() { if( m_controlled ) { ControlObject(); } } private void ControlObject() { Vector3 moveDir = Vector3.zero; if( Input.GetKey( KeyCode.UpArrow ) ) { moveDir += Vector3.forward; } if( Input.GetKey( KeyCode.DownArrow ) ) { moveDir += Vector3.back; } if( Input.GetKey( KeyCode.RightArrow ) ) { moveDir += Vector3.right; } if( Input.GetKey( KeyCode.LeftArrow ) ) { moveDir += Vector3.left; } if( moveDir.sqrMagnitude > Mathf.Epsilon ) { moveDir = moveDir.normalized; Turn( moveDir ); Move( moveDir ); } if( Input.GetKeyDown( KeyCode.Space ) ) { Jump(); } } private void Move( Vector3 i_forward ) { Vector3 delta = i_forward * m_moveSpeed * Time.deltaTime; Vector3 targetPos = transform.position + delta; m_rigidbody.MovePosition( targetPos ); } private void Turn( Vector3 i_forward ) { Quaternion toRot = Quaternion.LookRotation( i_forward ); Quaternion fromRot = transform.rotation; float delta = m_turnSpeed * Time.deltaTime; Quaternion targetRot = Quaternion.RotateTowards( fromRot, toRot, delta ); m_rigidbody.MoveRotation( targetRot ); } private void Jump() { // えっ、このままじゃ空中でもジャンプできちゃうって!? // 仕様だよ! m_rigidbody.velocity = Vector3.zero; Vector3 jumpVec = Vector3.up * m_jumpForce; m_rigidbody.AddForce( jumpVec, ForceMode.Impulse ); } } // class ObjectController
これで準備完了。
さあ、複製していくぞ
さあ、問題となっている複製処理にはいりましょう。
まずはなにも考えずに、オブジェクトが今いる場所に自分自身と同じオブジェクトを複製する処理を作っていきます。
惨事が起きることは目に見えているけど。
ObjectController.cs
private void ControlObject() { // 移動処理 // ... if( Input.GetKeyDown( KeyCode.A ) ) { Duplication(); } } private void Duplication() { var copiedObj = Instantiate( this, transform.position, transform.rotation ); copiedObj.SetDuplicationParameter(); } private void SetDuplicationParameter() { m_controlled = false; }
やっぱり惨事が起きた。
複製した瞬間にオブジェクト同士のコリジョン判定が起きるので、オブジェクトがあらぬ方向に飛んで行ってしまう。
レイヤーを分けて複製した瞬間に対応だ
惨事を起きなくするにはオブジェクト同士が当たらないようにする必要があります。
コライダーを後からつけるという方法もありますが、床などのオブジェクトとはコリジョン判定を取る必要があるため、コリジョンは常時必要になってきます。
では操作するオブジェクトと複製するオブジェクトのレイヤーを分けることで対応することにしましょう。
このレイヤーをObjectController
のエディタ上で指定したいけど、レイヤーはint
型なので数値指定になってしまいます。
それではわかりにくいので、レイヤー指定用アトリビュートを使いましょう。
LayerTypeFieldAttribute.cs
using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif // UNITY_EDITOR public class LayerTypeFieldAttribute : PropertyAttribute { } // class TagAttribute #if UNITY_EDITOR [CustomPropertyDrawer( typeof( LayerTypeFieldAttribute ) )] public class LayerTypeFieldDrawer : PropertyDrawer { public override void OnGUI( Rect i_position, SerializedProperty i_property, GUIContent i_label ) { i_property.intValue = EditorGUI.LayerField( i_position, i_label, i_property.intValue ); } public override float GetPropertyHeight( SerializedProperty i_property, GUIContent i_label ) { return EditorGUI.GetPropertyHeight( i_property ); } } // class TagAttribute #endif // UNITY_EDITOR
このアトリビュートを使って、操作するオブジェクトにはObjectレイヤーを、複製したオブジェクトにはCopiedレイヤーを使うように処理を修正しますね。
ObjectController.cs
[Header( "Layer" )] [SerializeField, LayerTypeField] private int m_copiedLayer = 0; private void SetDuplicationParameter() { m_controlled = false; gameObject.layer = m_copiedLayer; }
これで複製した瞬間にコリジョン判定が起きることはなくなりました。
しかし、このままでは複製したオブジェクトとはずっと当たらないままだ。
僕は複製した後は普通にコリジョン判定をしてほしいのです。
当たらないときの見た目を変えよう
ちょっと話がそれるのだが、複製したオブジェクトとコリジョン判定が行われないレイヤーの場合は、オブジェクトの色を変えるようにしておきましょう。
見た目上でコリジョン判定が行われているかの判断が出来るようにしておいたほうが、テストが楽になりそうです。
ObjectController.cs
private Renderer m_renderer = null; private Color m_defaultColor = Color.white; private Color ObjectColor { get { return m_renderer.material.color; } set { m_renderer.material.color = value; } } private void Awake() { m_rigidbody = GetComponent<Rigidbody>(); m_renderer = GetComponentInChildren<Renderer>(); m_defaultColor = ObjectColor; } private void SetDuplicationParameter() { m_controlled = false; gameObject.layer = m_copiedLayer; ObjectColor = new Color( 0.0f, 0.0f, 0.0f, 0.3f ); }
これでオブジェクトに当たらないときは半透明になりました。
一定距離離れたらレイヤーをチェンジだ
複製したオブジェクトから離れたと判断できたら、オブジェクトとコリジョン判定を行うレイヤーに変えるようにすれば複製したオブジェクトと触れるようになるはずです。
では、どうやって操作しているオブジェクトが複製オブジェクトと離れたと判断するべきでしょうか。
まずはシンプルに考えてみましょう。
エディタ上で距離を指定した、それ以上離れたらレイヤーを変えるようにしてみます。
ObjectController.cs
[Header( "Copied" )] [SerializeField] private float m_copiedDistance = 0.0f; private Transform m_sourceTransform = null; private int m_defaultLayer = 0; private void Awake() { m_rigidbody = GetComponent<Rigidbody>(); m_renderer = GetComponentInChildren<Renderer>(); m_defaultColor = ObjectColor; m_defaultLayer = gameObject.layer; } private void Update() { if( m_controlled ) { ControlObject(); } else { UpdateCopiedLayer(); } } private void UpdateCopiedLayer() { if( gameObject.layer == m_defaultLayer ) { return; } // ルート計算は重いから、二乗された値で判定するよ! float sqrLength = ( transform.position - m_sourceTransform.position ).sqrMagnitude; if( sqrLength > m_copiedDistance * m_copiedDistance ) { ApplyDefaultObject(); } } private void ApplyDefaultObject() { gameObject.layer = m_defaultLayer; ObjectColor = m_defaultColor; } private void Duplication() { var copiedObj = Instantiate( this, transform.position, transform.rotation ); copiedObj.SetDuplicationParameter( transform ); } private void SetDuplicationParameter( Transform i_source ) { m_controlled = false; gameObject.layer = m_copiedLayer; ObjectColor = new Color( 0.0f, 0.0f, 0.0f, 0.3f ); m_sourceTransform = i_source; }
距離は大雑把に決めたけど、なんとなく上手くいっているぞ。
しかし、複製したオブジェクトの最終的なレイヤーを、操作するオブジェクトと同じものを使うことは問題かもしれないね。
このレイヤーは複製した瞬間のオブジェクトとコリジョン判定を行わないので、このままではオブジェクトを積み上げることが出来ないかもしれません。
あっ、やっぱり……。
ということは、複製した瞬間のオブジェクトともコリジョン判定を行うレイヤーを追加せねばならないのか。
新しく作るのも面倒くさいなぁ。
複製した瞬間のオブジェクトともコリジョン判定を行うということは、結局すべてのオブジェクトと判定を行っているので、今回は、床などで使っているDefaultのレイヤーでいいや。
ということで、m_defaultLayer
をエディタ上で指定する形に変更する必要があるので、ちょっとだけスクリプトを修正しましょう。
ObjectController.cs
[SerializeField, LayerTypeField] private int m_defaultLayer = 0; private void Awake() { m_rigidbody = GetComponent<Rigidbody>(); m_renderer = GetComponentInChildren<Renderer>(); m_defaultColor = ObjectColor; // エディタ上で指定したレイヤーを使うので、設定は不要。 // m_defaultLayer = gameObject.layer; }
これでオブジェクトを積み上げることが出来るようになりました。
コライダーから離れたらレイヤーをチェンジだ
さて、距離で判断する処理は大雑把にうまくいったが、他に方法はないものでしょうか。
操作オブジェクトが複製したオブジェクトから離れたかをコライダーのトリガーで判断するやり方も考えられます。
というわけで、さっきほど作成した処理は一部破棄しますね。
まずオブジェクトが離れたかを判定するコンポーネントを作ってみます。
ExitBoxTrigger.cs
using UnityEngine; public class ExitBoxTrigger : MonoBehaviour { public event System.Action onExit = () => {}; private void OnTriggerExit( Collider i_other ) { onExit(); } public void Initialize( Transform i_parent, BoxCollider i_sourceCollider, int i_triggerLayer ) { transform.SetParent( i_parent, false ); var collider = gameObject.AddComponent<BoxCollider>(); collider.isTrigger = true; collider.center = i_sourceCollider.center; collider.size = i_sourceCollider.size; gameObject.layer = i_triggerLayer; } } // class ExitTrigger
今回のオブジェクトはボックス型なのでBoxCollider
専用のものを作ってみました。
このExitBoxTrigger
についているオブジェクトとコリジョン判定できるオブジェクトが離れたらonExit
イベントを経由して知らせてくれるようになっています。
このExitBoxTrigger
を使って判定するレイヤーを追加しましょう。
コリジョン判定としては、操作しているオブジェクトとのみ当たる専用レイヤーです。
あとは複製したオブジェクトに、ExitBoxTrigger
がついたGameObject
が子オブジェクトとして生成するようにObjectController
の処理を改良します。
ObjectController.cs
[SerializeField, LayerTypeField] private int m_exitLayer = 0; private ExitBoxTrigger m_exitTrigger = null; private void SetDuplicationParameter() { m_controlled = false; gameObject.layer = m_copiedLayer; ObjectColor = new Color( 0.0f, 0.0f, 0.0f, 0.3f ); var trigger = new GameObject( "Trigger" ).AddComponent<ExitBoxTrigger>(); trigger.Initialize( transform, GetComponent<BoxCollider>(), m_exitLayer ); trigger.onExit += OnExitObject; m_exitTrigger = trigger; } private void OnExitObject() { ApplyDefaultObject(); if( m_exitTrigger != null ) { Destroy( m_exitTrigger.gameObject ); } }
距離で判断した時より、きっちりと離れたと判断できるようになりました。
やったことを振り返ろうかな
距離を使って判定した場合は、範囲が球の判定なるので今回のようなボックス型のオブジェクトを使う場合、どうしても大雑把な判定になってしまいました。
とはいえコライダーのトリガーを使って判定する場合は、専用のコンポーネントを作ったり、レイヤーを追加したり、とやることは多かったですね。
ただ、どちらにしろ追加するレイヤーの数が多いなぁ。
もっとスマートにする方法ないものだろうか。
調べることはまだまだ多そうですねー。