うら干物書き

ゲームを作っています。

【Unity】Rigidbody.AddForceAtPositionで吹き飛ばす

 最近暑くなってきましたね。
 今回はそんな夏の暑さを吹き飛ばすべく、Rigidbody.AddForceAtPosition()を使ってオブジェクトを吹き飛ばしていこうと思います。

 きっと暑さは吹き飛ばないでしょうけど……。


この記事にはUnity2017.1.0f3を使用しています。

AddForceAtPositionって何だろう?

 Rigidbody.AddForceAtPosition()とは何なのでしょうか。
 僕自身、あまりRigidbodyを使ってきていないので、少し調べて見ましょう。

 Rigidbodyの有名どころにAddForce()がありますよね。
 
docs.unity3d.com

 これを使うと、指定した方向に飛んでいきます。
 

using UnityEngine;

[RequireComponent( typeof( Rigidbody ) )]
public class TestObject : MonoBehaviour
{
    [SerializeField]
    private float   m_power     = 0.0f;
    [SerializeField]
    private Vector3 m_powerDir  = Vector3.zero;

    private void FixedUpdate()
    {
        if( Input.GetMouseButtonDown( 0 ) )
        {
            Rigidbody rigidbody = GetComponent<Rigidbody>();
            rigidbody.AddForce( m_powerDir.normalized * m_power );
        }
    }

} // class TestObject

f:id:urahimono:20170818104311g:plain

 AddTorque()というものもあるみたいです。

docs.unity3d.com

 こちらを使うと、指定した方向に回転し始めます。

using UnityEngine;

[RequireComponent( typeof( Rigidbody ) )]
public class TestObject : MonoBehaviour
{
    [SerializeField]
    private float   m_power     = 0.0f;
    [SerializeField]
    private Vector3 m_powerDir  = Vector3.zero;

    private void FixedUpdate()
    {
        if( Input.GetMouseButtonDown( 0 ) )
        {
            Rigidbody rigidbody = GetComponent<Rigidbody>();
            rigidbody.AddTorque( m_powerDir.normalized * m_power );
        }
    }

} // class TestObject

f:id:urahimono:20170818104446g:plain

 ではAddForceAtPosition()はどんなものなのでしょうか。

docs.unity3d.com

 位置を渡してあげる必要があるみたいですね。
 ポイントして、第二引数として渡す位置ワールド座標であるところですね。ローカル座標じゃないんですね。

 ではオブジェクトの中心より少し上の辺りに力を加える処理を書いてみましょう。

using UnityEngine;

[RequireComponent( typeof( Rigidbody ) )]
public class TestObject : MonoBehaviour
{
    [SerializeField]
    private float   m_power     = 0.0f;
    [SerializeField]
    private Vector3 m_powerDir  = Vector3.zero;
    [SerializeField]
    private Vector3 m_offset    = Vector3.zero;

    private void FixedUpdate()
    {
        if( Input.GetMouseButtonDown( 0 ) )
        {
            Rigidbody rigidbody = GetComponent<Rigidbody>();
            rigidbody.AddForceAtPosition( m_powerDir.normalized * m_power, transform.position + m_offset );
        }
    }

} // class TestObject

f:id:urahimono:20170818102543p:plain
f:id:urahimono:20170818102552p:plain
f:id:urahimono:20170818104459g:plain

 くるくると回りながら、飛んでいきました。
 AddForce()AddTorque()を足したような挙動です。

 なるほど、これは楽しい関数です。

AddForceAtPositionで指定する位置について

 さて、先ほど第二引数として渡す位置ワールド座標であるとお話しました。
 ということはRigidbodyがアタッチしてあるオブジェクトの位置とかけ離れた位置にAddForceAtPosition()の力を指定してしまうと、力は発揮されないのでしょうか。

f:id:urahimono:20170818104531p:plain

 上の図では、オブジェクトと明後日の位置に力が掛けられています。
 これでは力が外れて効果がないように見えますが……。
 調べたところそういうわけでは無いようですね。
 AddForceAtPosition()は別にアタッチしているメッシュやコリジョンを加味して力の計算を行っているわけではないみたいです。
 そうでなければ、RigidbodyしかアタッチしていないGameObjectではAddForceAtPositionが使えないことになってしまいますし。
 さきほどAddForceAtPositionを使ったときも、カプセルの中心から1m上に力を加えていますので、カプセル外の位置に力を加えています。
 それでもちゃんと動いてますしね。

 図にするとこんな感じでしょうか。

f:id:urahimono:20170818104521p:plain

 ……うまく説明できていない気がする。
 挙動で見てみましょう。

 横一列にオブジェクトを配置して、全てのオブジェクトに同じ力と位置をAddForceAtPosition()を使って与えるスクリプトを記述しましょう。

using UnityEngine;

public class TestController : MonoBehaviour
{
    [SerializeField]
    private float       m_power         = 0.0f;
    [SerializeField]
    private Vector3     m_powerDir      = Vector3.zero;
    [SerializeField]
    private Rigidbody[] m_rigidbodies   = null;

    private void FixedUpdate()
    {
        if( Input.GetMouseButtonDown( 0 ) )
        {
            foreach( var rigidbody in m_rigidbodies )
            {
                rigidbody.AddForceAtPosition( m_powerDir.normalized * m_power, Vector3.zero );
            }
        }
    }

} // class TestController

 上記のスクリプトでは全て原点(0,0,0)の位置から真上(0,1,0)の力を発生させます。

f:id:urahimono:20170818104712p:plain
f:id:urahimono:20170818104723p:plain

 さて、さっそく動作させてみましょう。

f:id:urahimono:20170818104751g:plain

 オブジェクトの移動する速さは全てのオブジェクトで同じですが、回転は異なっていますね。

 そのため、AddForceAtPosition()に指定する位置は、指定した位置で力が発生するわけではない点にご注意ください。
 僕はAddForceAtPosition()を使う前はそういう挙動なのだと思っていました。

まとめ

 上記までのスクリプトでは第三引数を省略していますので、渡す力のタイプはForceMode.Forceを使用しています。
 ただ叩く、蹴る、などの瞬間的な力を与える場合には、ForceMode.Impulseを使った方がいいかもしれませんね。

 マウスをクリックするたびに、適当な方向に瞬間的な力を加えるスクリプトを記述してみました。
 使うとこんな挙動になります。

using UnityEngine;

[RequireComponent( typeof( Rigidbody ) )]
public class TestObject : MonoBehaviour
{
    [SerializeField]
    private float   m_power         = 0.0f;
    [SerializeField]
    private float   m_offsetRange   = 2.0f;

    private void FixedUpdate()
    {
        if( !Input.GetMouseButtonDown( 0 ) )
        {
            return;
        }

        Rigidbody rigidbody = GetComponent<Rigidbody>();

        Vector3 randomDir       = ( Random.insideUnitSphere - Vector3.zero ).normalized;
        float   randomLength    = Random.Range( -m_offsetRange, m_offsetRange );

        Vector3 power   = m_power * randomDir;
        Vector3 pos     = transform.position + transform.up * randomLength;

        rigidbody.AddForceAtPosition( power, pos, ForceMode.Impulse );
    }

} // class TestObject

 ……まるでカプセルをいじめているようだ。

 というわけで夏の暑さに負けずスクリプトを組んでみてくださいねー。