雪玉のインタラクション

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

…投稿日ちがくね?という話はさておき…
仏の顔も3度までやし!

雪感も増えて少し満足してしまったところもありますが、
雪玉に対してのインタラクションはまだ足りないのでこれを付けていきます。
主に以下の3つ。

  1. 地面に触れながらトリガーを引くと雪玉を掴める
  2. 雪玉を掴みながら地面を擦るように振ると雪玉が大きくなる
  3. 雪玉がぶつかったときのリアクション

雪玉を作り出す

接地判定が必要なのでとりあえずBox Colliderを地面にアタッチしておきます。


タグ参照で衝突判定を弾くつもりでいるので、タグを"Snow"として設定しておきます。

コントローラに以前取り上げたCatchと以下のBall Craftをアタッチすることで、
コントローラと地面との間でやり取りを行えます。

using UnityEngine;
using System.Collections;

public class BallCraft : MonoBehaviour {

    /// <summary>
    /// オブジェクトを握る手
    /// </summary>
    [SerializeField]
    private Catch _catcher = null;

    /// <summary>
    /// 握るオブジェクト(生成対象)
    /// </summary>
    [SerializeField]
    private ThrowableObject _ball = null;
    /// <summary>
    /// オブジェクトを大きくする際の閾値となる加速度
    /// </summary>
    [SerializeField]
    private float _makableVelocityMagnitude = 2.0f;
    /// <summary>
    /// 閾値を満たしたときの増加スケール[scale/s]
    /// </summary>
    [SerializeField]
    private float _addMakingScale = 0.1f;

	// Use this for initialization
	void Start () {
        Debug.Assert(null != _catcher);
        Debug.Assert(null != _ball);
	}
	
    void OnCollisionStay(Collision other)
    {
        if (IsAccept(other.gameObject))
        {
            OnCollidingProcess();
        }
    }
    void OnTriggerStay(Collider other)
    {
        if (IsAccept(other.gameObject))
        {
            OnCollidingProcess();
        }
    }

    /// <summary>
    /// 衝突時の処理を行えるか
    /// </summary>
    /// <param name="obj">衝突したオブジェクト</param>
    /// <returns>処理の可否</returns>
    private bool IsAccept(GameObject obj)
    {
        if ("Snow" != obj.gameObject.tag)
        {
            return false;
        }
        return true;
    }

    /// <summary>
    /// 衝突時の処理
    /// </summary>
    private void OnCollidingProcess()
    {
        if (_catcher.IsHolding())
        {
            Making(_catcher);
        }
        else if (_catcher.IsHoldable())
        {
            if (_catcher.Controller.GetPressDown(SteamVR_Controller.ButtonMask.Trigger))
            {
                Spawn(_catcher);
            }
        }
    }

    /// <summary>
    /// オブジェクトの生成および握り
    /// </summary>
    /// <param name="catcher">握る手</param>
    private void Spawn(Catch catcher)
    {
        Debug.Assert(null != catcher);

        ThrowableObject instance = GameObject.Instantiate(_ball);
        
        catcher.Take(instance, true);
    }
    /// <summary>
    /// 握っているオブジェクトを大きくする
    /// </summary>
    /// <param name="catcher">握る手</param>
    private void Making(Catch catcher)
    {
        Debug.Assert(null != catcher);

        ThrowableObject taking = catcher.TakingObject;
        if (null != taking)
        {
            if (catcher.Velocity.sqrMagnitude >= _makableVelocityMagnitude * _makableVelocityMagnitude)
            {
                taking.transform.localScale += Vector3.one * _addMakingScale * Time.deltaTime;
            }
        }
    }
}



こんな感じで掴めます。

雪玉が大きくなるのはこんな感じ。

最初ポッと出てくるのも違和感があるかもしれないですね。もっと小さいスケールから振って大きくするでもいいのかもなー。
大きくしてるときは何となく小さい雪しぶき的なパーティクルがあると良い気がしてきました。

衝突時のインタラクション

やはり雪玉なので割れるようなインタラクションが必要。
何となくそれっぽい?程度のパーティクルを作りました。
雪玉と同じマテリアルを使ってSphereを真上に散らすような感じのものですね。
スクショが無いので後ほど…

雪玉に以下のクラスの派生クラスを作成します。

using UnityEngine;
using System.Collections;

public abstract class BreakableObject : MonoBehaviour {

    /// <summary>
    /// 非破壊フラグ.
    /// 何かに衝突しても破壊されなくなります.
    /// IsUnbreakable == trueの場合、OnBreakEvent()はコールされません.
    /// </summary>
    [SerializeField]
    private bool _isUnbreakable = false;
    public bool IsUnbreakable
    {
        set
        {
            _isUnbreakable = value;
            OnChangeUnbreakableFlag(_isUnbreakable);
        }
        get { return _isUnbreakable; }
    }

    /// <summary>
    /// 非破壊フラグ更新時のイベント
    /// </summary>
    /// <param name="isUnbreakable">更新後の非破壊フラグの値</param>
    protected abstract void OnChangeUnbreakableFlag(bool isUnbreakable);
    /// <summary>
    /// オブジェクト破壊時のイベント
    /// </summary>
    /// <param name="obj">衝突したオブジェクト</param>
    /// <param name="contacts">衝突情報</param>
    /// <returns>自身のオブジェクトの破壊の有無</returns>
    protected abstract bool OnBreakEvent(GameObject obj, ContactPoint[] contacts = null);
    
    void OnCollisionEnter(Collision other)
    {
        Debug.Assert(null != other);
        OnBreak(other.gameObject, other.contacts);
    }
    void OnCollisionStay(Collision other)
    {
        Debug.Assert(null != other);
        OnBreak(other.gameObject, other.contacts);
    }
    void OnCollisionExit(Collision other)
    {
        Debug.Assert(null != other);
        OnBreak(other.gameObject, other.contacts);
    }

    void OnTriggerEnter(Collider other)
    {
        Debug.Assert(null != other);
        OnBreak(other.gameObject);
    }
    void OnTriggerStay(Collider other)
    {
        Debug.Assert(null != other);
        OnBreak(other.gameObject);
    }
    void OnTriggerExit(Collider other)
    {
        Debug.Assert(null != other);
        OnBreak(other.gameObject);
    }
    
    private bool OnBreak(GameObject obj, ContactPoint[] contacts = null)
    {
        Debug.Assert(null != obj);
        if (IsUnbreakable)
        {
            return false;
        }
        if (!OnBreakEvent(obj, contacts))
        {
            return false;
        }
        Destroy();
        return true;
    }

    private void Destroy()
    {
        this.gameObject.SetActive(false);
        GameObject.Destroy(this.gameObject);
    }
}


派生先で衝突点からパーティクルを発生させる処理を追加します。
衝突コードは何となくな感じ。計算式がアレなのでブラッシュ段階で手入れですかね…

        // 破壊時のパーティクル
        if (null != contacts && null != _crashParticle)
        {
            GameObject particle = GameObject.Instantiate(_crashParticle);
            Rigidbody rigid = contacts[0].thisCollider.GetComponent<Rigidbody>();
            
            particle.transform.position = contacts[0].point;
            particle.transform.localScale = this.transform.localScale;
            particle.transform.localRotation = Quaternion.LookRotation(Vector3.Reflect(contacts[0].normal, rigid.velocity));

            ParticleSystem system = particle.GetComponentInChildren<ParticleSystem>();
            if (null != system)
            {
                var mainModule = system.main;
                mainModule.startSpeed = _crashVelocityCoef * rigid.velocity.magnitude / particle.transform.localScale.x;
                
                var velocity = system.velocityOverLifetime;
                Vector3 r = Vector3.Reflect(rigid.velocity, contacts[0].normal);
                r = r.normalized / particle.transform.localScale.x;
                velocity.x = r.x;
                velocity.y = r.y;
                velocity.z = r.z;
            }
        }

こんな具合になりました。


フィールドとしての体裁はなんとなく整った気がするので、
そろそろかわいい女の子にご登場頂きたいですね…
モーションどうしよう…(白目)

見た目を良くして進捗あるように見せる

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

前回球を投げるところまで作成したところで、雪感が欲しくなりました。
後ほどどうするかはわからないけど手早く出してまずはモチベーションアップを図ります。

アセットを使ってお手軽に

手早く…ということでアセットを活用していきます。
・4スノーマテリアル[高画質] [マテリアルコレクション]
https://www.assetstore.unity3d.com/jp/#!/content/69201


これを使ってマテリアルを変更します。
これが…

こうじゃ!

球の方も変更。

Standardシェーダが紐づけられているので、各種パラメータを調整してやるとなお良さが出ると思います。

雪を降らせる

そもそも雪を降らせて無かったということに気付いたので降らせます。
とりあえずShriken(Particle System)を使う。

手軽ながらも雰囲気が出てきてモチベも上がってきますぞ!

掴んで、投げる

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

昨日の記事が完全に間に合わせなので、昨日の部分を改めて…(今日作業しようと思ったら一日中出ていたのでできなかったマン)

初日に挙げた、

1.雪面(地面)に触れてコントローラを握る(トリガーを引く)と雪玉を持てる

2.それを投げる

を実現するところから始めました。

ざっくばらんにゲームオブジェクトを配置

なんとなくでゲームオブジェクトを配置。


・地面となるPlane(+Collider)
・雪玉となるSphere(+Collider,Rigidbody)
・SteamVRアセットに含まれている[CameraRig]
※カメラが競合するのでもともとあるカメラは削除しておきます。

掴んで投げる

とりあえず投げれないとダメなので、そこから。
SteamVRアセットに付属していたSteamVR_TestThrow.csを元に、握れるオブジェクト&それを握るスクリプトを追加。

using UnityEngine;
using System.Collections;

/// <summary>
/// アタッチ可能オブジェクト.
/// FixedJointを介してオブジェクト間の接続をサポート.
/// </summary>
public class AttachableObject : MonoBehaviour {

    private FixedJoint _joint = null;

    /// <summary>
    /// アタッチ.
    /// </summary>
    /// <param name="to">接続先のリジッドボディ.</param>
    public void Attach(Rigidbody to)
    {
        Debug.Assert(null != to);

        if (null == _joint)
        {
            _joint = gameObject.AddComponent<FixedJoint>();
        }
        Debug.Assert(null != _joint);

        _joint.connectedBody = to;
    }
    /// <summary>
    /// デタッチ.
    /// </summary>
    public void Detach()
    {
        if (null != _joint)
        {
            Object.DestroyImmediate(_joint);
            _joint = null;
        }
    }
}

using UnityEngine;
using System.Collections;


public class ThrowParam
{
    public Vector3 velocity;
    public Vector3 angularVelocity;

    public ThrowParam()
    {
        velocity = Vector3.zero;
        angularVelocity = Vector3.zero;
    }
}

/// <summary>
/// 投げられる用オブジェクト.
/// </summary>
[RequireComponent(typeof(Rigidbody))]
public class ThrowableObject : AttachableObject {

    private Rigidbody _rigid = null;

    /// <summary>
    /// 投射.
    /// </summary>
    /// <param name="param">投射パラメータ.</param>
    public void Throw(ThrowParam param)
    {
        if (null == _rigid)
        {
            return;
        }
        _rigid.velocity = param.velocity;
        _rigid.angularVelocity = param.angularVelocity;

        _rigid.maxAngularVelocity = _rigid.angularVelocity.magnitude;
    }

	// Use this for initialization
	void Start ()
    {
        _rigid = gameObject.GetComponent<Rigidbody>();
        Debug.Assert(null != _rigid);
	}
	
	// Update is called once per frame
	void Update () {
	    
	}
}

掴む側のスクリプト
SteamVR_Controller.DeivceはサンプルではUpdate()で取ってたけど今のところ問題になっていないのでStart()で取得したきりにしてます。
この時点ではデバッグ用にとりあえず球を生み出す処理も含めてます。

using UnityEngine;
using System.Collections;

public class Catch : MonoBehaviour {

    [SerializeField]
    private SteamVR_TrackedObject _tracker = null;

    [SerializeField, HideInInspector]
    private SteamVR_Controller.Device _controller = null;

    [SerializeField]
    private Rigidbody _attachPoint = null;

    private Thrower _thrower = null;

    public ThrowableObject TakingObject
    {
        get { return (null == _thrower) ? null : _thrower.Taking; }
    }
    public Vector3 Velocity
    {
        get { return (null == _controller) ? Vector3.zero : _controller.velocity; }
    }
    public SteamVR_Controller.Device Controller
    {
        get { return _controller; }
    }

    [SerializeField]
    private ThrowableObject _spawner = null;
    [SerializeField]
    private float _throwPowerScale = 1.0f;

    // Use this for initialization
    void Start()
    {
        if (null == _tracker)
        {
            _tracker = GetComponent<SteamVR_TrackedObject>();
        }
        if (null != _tracker)
        {
            _controller = SteamVR_Controller.Input((int)_tracker.index);
        }
        if (null == _thrower)
        {
            _thrower = new Thrower();
        }
    }

    // Update is called once per frame
    void FixedUpdate()
    {
        if (_controller.GetTouchDown(SteamVR_Controller.ButtonMask.Touchpad))
        {
            if (IsHoldable())
            {
                ThrowableObject obj = GameObject.Instantiate(_spawner);
                obj.transform.position = _attachPoint.position;
                Take(obj);
            }
        }

        if (!IsHolding())
        {
            return;
        }
        if (_controller.GetTouchUp(SteamVR_Controller.ButtonMask.Trigger))
        {
            _thrower.Release(_tracker, _controller, _throwPowerScale);
        }
    }

    void OnTriggerStay(Collider other)
    {
        if (!IsHoldable())
        {
            return;
        }

        if (_controller.GetPressDown(SteamVR_Controller.ButtonMask.Trigger))
        {
            ThrowableObject throwable = other.gameObject.GetComponent<ThrowableObject>();
            if (null != throwable)
            {
                Take(throwable);
            }
        }
    }
    public bool IsHoldable()
    {
        return !IsHolding() 
            && (null != _thrower) 
            && (null != _controller) 
            && (null != _tracker);
    }

    public bool IsHolding()
    {
        return (null != _thrower) 
            && _thrower.HasObject();
    }
    public void Take(ThrowableObject throwable, bool isFixedPosition = false)
    {
        if (null == throwable)
        {
            return;
        }
        Debug.Assert(IsHoldable());
        Debug.Assert(!IsHolding());
        if (isFixedPosition)
        {
            throwable.transform.position = _attachPoint.position;
        }
        
        _thrower.Take(throwable, _attachPoint);
    }
}

で?


こうすることでゲーム中にSteamVRコントローラを通じて球を掴むことができるようになりました。
タイミングよくトリガーを放すと球を投げることが可能です。

雪面に触れて…の部分は勿体ぶるほどのことはしてませんが明日に…もう眠くてしんどい!

とりあえず投げよう

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

今日も今日とてこの時間です…なので簡潔に…
前日挙げた、

1.雪面(地面)に触れてコントローラを握る(トリガーを引く)と雪玉を持てる

2.それを投げる

を実現するところから始めました。


後ほど画像はっつけますが、
・地面となる板を置く(コライダ)
・のちに雪玉と化す球(コライダ+リジッドボディ)
を付けてプレハブ化。

球を掴んで投げる部分はSteamVRアセットについてたサンプルスクリプトを参考にほぼほぼ似たような感じで実装。

あー!時間ない!一旦ここまで!

何を作るか

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

早速ギリ過ぎてあれなんですが…時間ないので何をまず作るか。というところで…

二日前にふと、かわいい女の子と雪合戦をしたらよさげなのでは?という電波を受信したので、雪合戦をVRに持ち込んでみようと思います。
既にあるんだろうなぁとか思ったけど気にしない方向で!

絵的なところどうしよ…とか思ってますが、まずは画像のとおり、

1.雪面(地面)に触れてコントローラを握る(トリガーを引く)と雪玉を持てる
2.それを投げる
3.女の子に当てる→リアクションかわいい!

というところを目指して行きたいなぁ…と、所信表明したところでもう日をまたぎそうなので1日目終了。

仕事が思った以上にヤバい流れになってるのでどうなるのやら…