ユニティちゃんに雪玉をぶつける

※この記事は『ひとりVRゲーム作ろうな Advent Calendar 2016』の7日目の記事です。

ね、寝るまでが1日だから…(震え声)

今回はユニティちゃんに雪玉をぶつけるとやられモーションを取ってもらいます。

自分が衝突していると知覚させる

今回ユニティちゃんにアタッチしているColliderは複数のGameObjectにそれぞれアタッチされています。


ルートとなっているGameObjectに対して自分が当たっていることを通知する必要があります。
ということで各種コリジョンイベント通知クラスとそれを受信するクラスを作成しました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 子のイベント送信者.
/// </summary>
public class CollisionEventSender : MonoBehaviour {

    private CollisionEventReceiver _sendTo = null;

    public static CollisionEventSender Instantiate(
        GameObject attachTo, CollisionEventReceiver sendTo)
    {
        Debug.Assert(null != attachTo);
        Debug.Assert(null != sendTo);

        CollisionEventSender sender = attachTo.AddComponent<CollisionEventSender>();
        if (null != sender)
        {
            sender._sendTo = sendTo;
        }
        return sender;
    }

    /// <summary>
    /// デフォルトコンストラクタ.
    /// インスタンス化の際にSender-Receive間の紐づけを行いたいので使用禁止.
    /// </summary>
    private CollisionEventSender() { }

    void OnTriggerEnter(Collider other)
    {
        _sendTo.ReceivedOnTriggerEnter(other);
    }
    void OnTriggerStay(Collider other)
    {
        _sendTo.ReceivedOnTriggerStay(other);
    }
    void OnTriggerExit(Collider other)
    {
        _sendTo.ReceivedOnTriggerExit(other);
    }

    void OnCollisionEnter(Collision other)
    {
        _sendTo.ReceivedOnCollisionEnter(other);
    }
    void OnCollisionStay(Collision other)
    {
        _sendTo.ReceivedOnCollisionStay(other);
    }
    void OnCollisionExit(Collision other)
    {
        _sendTo.ReceivedOnCollisionExit(other);
    }
}


受信側は以下の通り。
いちいち手動でアタッチさせていくのも面倒かな、という気がしたので、
コンストラクタで子階層でColliderのつく子にはCollisionEventSenderを自動でアタッチするようにしてみました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public delegate void ColliderEvent(Collider other);
public delegate void CollisionEvent(Collision other);

/// <summary>
/// 子の衝突イベント受信者.
/// </summary>
public class CollisionEventReceiver
{
    public ColliderEvent OnTriggerEnterCallback { private get; set; }
    public ColliderEvent OnTriggerStayCallback { private get; set; }
    public ColliderEvent OnTriggerExitCallback { private get; set; }
    public CollisionEvent OnCollisionEnterCallback { private get; set; }
    public CollisionEvent OnCollisionStayCallback { private get; set; }
    public CollisionEvent OnCollisionExitCallback { private get; set; }


    private List<CollisionEventSender> _senders = null;

    private CollisionEventReceiver() { }
    public CollisionEventReceiver(Transform parent)
    {
        _senders = new List<CollisionEventSender>();
        AddSenderFromChildren(parent);
    }

    /// <summary>
    /// 通知者達の破棄.
    /// デストラクタでコールするとメインスレッド以外でGameObjectはDestroy出来ない様なエラーが発生するため、
    /// 受信者側で明示的に破棄する必要があります.
    /// </summary>
    public void Dispose()
    {
        for (int i = 0; i < _senders.Count; ++i)
        {
            GameObject.Destroy(_senders[i]);
        }
    }

    public void ReceivedOnTriggerEnter(Collider other)
    {
        if (null != OnTriggerEnterCallback)
        {
            OnTriggerEnterCallback(other);
        }
    }
    public void ReceivedOnTriggerStay(Collider other)
    {
        if (null != OnTriggerStayCallback)
        {
            OnTriggerStayCallback(other);
        }
    }
    public void ReceivedOnTriggerExit(Collider other)
    {
        if (null != OnTriggerExitCallback)
        {
            OnTriggerExitCallback(other);
        }
    }

    public void ReceivedOnCollisionEnter(Collision other)
    {
        if (null != OnCollisionEnterCallback)
        {
            OnCollisionEnterCallback(other);
        }
    }
    public void ReceivedOnCollisionStay(Collision other)
    {
        if (null != OnCollisionStayCallback)
        {
            OnCollisionStayCallback(other);
        }
    }
    public void ReceivedOnCollisionExit(Collision other)
    {
        if (null != OnCollisionExitCallback)
        {
            OnCollisionExitCallback(other);
        }
    }

    /// <summary>
    /// 子供に送信用コンポーネントを追加.
    /// Colliderがついているオブジェクトが対象となります.
    /// </summary>
    /// <param name="parent">親となるトランスフォーム</param>
    private void AddSenderFromChildren(Transform parent)
    {
        Debug.Assert(null != parent);

        Collider[] targets = parent.GetComponentsInChildren<Collider>();
        for (int i = 0; i < targets.Length; ++i)
        {
            CollisionEventSender sender = targets[i].GetComponent<CollisionEventSender>();
            if (null == sender)
            {
                sender = CollisionEventSender.Instantiate(targets[i].gameObject, this);
            }
            _senders.Add(sender);
        }
    }
}


実はこれ作ってるときにハマったのですが、デストラクタでGameObject.Destroy()を呼ぶと、

Destroy can only be called from the main thread.

というエラーが表示されてうまくいかない様です。
そもそもデストラクタを呼ぶのが間違い、のような気がしてきたので取り急ぎは明示的に破棄をコールする形にしました。

Receiverを使う場合は以下のような感じでデリゲートアクセサに設定してください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// キャラのリアクション.
/// </summary>
public class Reaction : MonoBehaviour {

    private CollisionEventReceiver _receiver = null;
    private Animator _animator = null;

    void Awake()
    {
        _receiver = new CollisionEventReceiver(transform);
        if (null != _receiver)
        {
            _receiver.OnTriggerEnterCallback = null;
            _receiver.OnTriggerStayCallback = null;
            _receiver.OnTriggerExitCallback = null;
            _receiver.OnCollisionEnterCallback = OnHitEvent;
            _receiver.OnCollisionStayCallback = null;
            _receiver.OnCollisionExitCallback = null;
        }
        Debug.Assert(null != _receiver);
    }

    // Use this for initialization
    void Start() {
        _animator = GetComponent<Animator>();

        Debug.Assert(null != _animator);
    }
    
    void OnDestroy()
    {
        if (null != _receiver)
        {
            _receiver.Dispose();
        }
    }

    private void OnHitEvent(Collision other)
    {
        // やられアニメーション遷移用トリガ
        _animator.SetTrigger("onDamage");
    }
}



Receiverをルートにアタッチすると実行時にはこのようにスクリプトがアタッチされます。
楽ちん。

と、ここでトラブル。
コリジョンはあるのにリアクションが起きなかったり、地面突き抜けたり、雪玉壊れのパーティクル発生イベントが起きないといった問題が…
眠い頭では収集つけられそうになかったのでとりあえず以下のようにRigidbodyを追加して、IsKinematicで物理演算をOffにすることで対処。
後ほどこの捻じれ設定は何とかしないとダメそうです。


とまあ、そんな感じでごまかした結果がこちら。


ユニティちゃんかわかわのかわーって感じです!