うら干物書き

ゲームを作っています。

【Unity】君と僕とが当たる距離感

 今回はタイトルでは何がやりたいのかわからないシリーズです。

 コライダーがついているオブジェクトを同じ場所に複製すると、複製した瞬間にコリジョン判定が起きます。
 それを避けるために、レイヤーを分けてコリジョン判定を起きないようにすると、今度は二度と複製したオブジェクトに触れることができなくなります。
 複製した瞬間は当たって欲しくないし、その後は当たって欲しい!

 それを何とかするために、少しだけ頑張ってみたお話です。
 ちなみに話の前半の方は、前回のドミノ倒しを作った内容と同じような感じになっています。

www.urablog.xyz


この記事には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

f:id:urahimono:20171001003327p:plain
f:id:urahimono:20171001003340g:plain

 これで準備完了。

さあ、複製していくぞ

 さあ、問題となっている複製処理にはいりましょう。
 まずはなにも考えずに、オブジェクトが今いる場所に自分自身と同じオブジェクトを複製する処理を作っていきます。
 惨事が起きることは目に見えているけど。

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;
}

f:id:urahimono:20171001003422g:plain

 やっぱり惨事が起きた
 複製した瞬間にオブジェクト同士のコリジョン判定が起きるので、オブジェクトがあらぬ方向に飛んで行ってしまう。

レイヤーを分けて複製した瞬間に対応だ

 惨事を起きなくするにはオブジェクト同士が当たらないようにする必要があります。
 コライダーを後からつけるという方法もありますが、床などのオブジェクトとはコリジョン判定を取る必要があるため、コリジョンは常時必要になってきます。
 では操作するオブジェクトと複製するオブジェクトのレイヤーを分けることで対応することにしましょう。

f:id:urahimono:20171001003524p:plain

 このレイヤーを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;
}

f:id:urahimono:20171001003542p:plain
f:id:urahimono:20171001003554g:plain

 これで複製した瞬間にコリジョン判定が起きることはなくなりました。
 しかし、このままでは複製したオブジェクトとはずっと当たらないままだ。
 僕は複製した後は普通にコリジョン判定をしてほしいのです。

当たらないときの見た目を変えよう

 ちょっと話がそれるのだが、複製したオブジェクトとコリジョン判定が行われないレイヤーの場合は、オブジェクトの色を変えるようにしておきましょう。
 見た目上でコリジョン判定が行われているかの判断が出来るようにしておいたほうが、テストが楽になりそうです。

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 );
}

f:id:urahimono:20171001003712g:plain

 これでオブジェクトに当たらないときは半透明になりました。

一定距離離れたらレイヤーをチェンジだ

 複製したオブジェクトから離れたと判断できたら、オブジェクトとコリジョン判定を行うレイヤーに変えるようにすれば複製したオブジェクトと触れるようになるはずです。

 では、どうやって操作しているオブジェクトが複製オブジェクトと離れたと判断するべきでしょうか。

 まずはシンプルに考えてみましょう。
 エディタ上で距離を指定した、それ以上離れたらレイヤーを変えるようにしてみます。

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;
}

f:id:urahimono:20171001003752p:plain
f:id:urahimono:20171001003804g:plain

 距離は大雑把に決めたけど、なんとなく上手くいっているぞ。
 しかし、複製したオブジェクトの最終的なレイヤーを、操作するオブジェクトと同じものを使うことは問題かもしれないね。

 このレイヤーは複製した瞬間のオブジェクトとコリジョン判定を行わないので、このままではオブジェクトを積み上げることが出来ないかもしれません。

f:id:urahimono:20171001003851g:plain

 あっ、やっぱり……。

 ということは、複製した瞬間のオブジェクトともコリジョン判定を行うレイヤーを追加せねばならないのか。
 新しく作るのも面倒くさいなぁ。
 複製した瞬間のオブジェクトともコリジョン判定を行うということは、結局すべてのオブジェクトと判定を行っているので、今回は、床などで使っている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;
}

f:id:urahimono:20171001003922p:plain
f:id:urahimono:20171001003937g:plain

 これでオブジェクトを積み上げることが出来るようになりました。

コライダーから離れたらレイヤーをチェンジだ

 さて、距離で判断する処理は大雑把にうまくいったが、他に方法はないものでしょうか。

 操作オブジェクトが複製したオブジェクトから離れたかをコライダーのトリガーで判断するやり方も考えられます。

 というわけで、さっきほど作成した処理は一部破棄しますね。

 まずオブジェクトが離れたかを判定するコンポーネントを作ってみます。

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を使って判定するレイヤーを追加しましょう。
 コリジョン判定としては、操作しているオブジェクトとのみ当たる専用レイヤーです。

f:id:urahimono:20171001004047p:plain

 あとは複製したオブジェクトに、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 );
    }
}

f:id:urahimono:20171001004102p:plain
f:id:urahimono:20171001004115g:plain f:id:urahimono:20171001004031p:plain

 距離で判断した時より、きっちりと離れたと判断できるようになりました。

やったことを振り返ろうかな

 距離を使って判定した場合は、範囲が球の判定なるので今回のようなボックス型のオブジェクトを使う場合、どうしても大雑把な判定になってしまいました。

 とはいえコライダーのトリガーを使って判定する場合は、専用のコンポーネントを作ったり、レイヤーを追加したり、とやることは多かったですね。

 ただ、どちらにしろ追加するレイヤーの数が多いなぁ。
 もっとスマートにする方法ないものだろうか。

 調べることはまだまだ多そうですねー。

【Unity】離れたら触れるようになるオブジェクト