Unity - 製作2D拋物線(Parabola)物件2 - 假的模擬阻力

說明
  接續著前一篇完美拋物線(http://www.iverv.com/2014/08/unity-2dparabola1.html),那接著就是要做有受到阻力的效果,但是這邊為什麼叫做假的呢,因為這邊用的方法不全然是照著公式來做,但是在概念上是接近的,同時也可以達到類似受到阻力移動的效果。

  但是跟前一篇不同的是,前一篇是利用公式,以X座標去求Y座標位置,來得到拋物線段上各點的座標位置,用這個座標位置來設定球的座標。這次使用的方式是以向量的方式來移動,概念就是行進中的點,以當下移動的向量調整阻力後,乘上一個frame的時間求得下一個位置。



  移動的路線跟完美拋物線類似,但是因為有阻力,所以X軸的速度會越來越慢,Y軸的速度同樣也會受影響,因此曲線會變成下面這樣的狀況。


  上圖灰色的拋物線段來自於完美拋物線公式,藍色的線段為經過阻力調整後的移動路徑,阻力係數drag=0.02。


  上圖阻力係數drag=0.08。

  明顯可以看到阻力會造成前進的速度(X軸速度)跟上升的速度(Y軸速度)越來越慢,同時如果阻力係數設定為0的話,藍色的拋物線會跟灰色的重疊,也就變成了完美拋物線。

  這兩張圖可以看到其中有個小點,以及從小點延伸出去的線段,這條線段就是該點當下的前進向量。



公式
  接著同樣要來看拋射物的公式,這邊還是參考相同的WIKI,不過參考下半的空氣阻力的部分。
http://en.wikipedia.org/wiki/Trajectory_of_a_projectile

參考圖(F的公式使用 F=-kv^2)。


  公式推導之類的有點煩,所以這邊我就乾脆簡化直接設定k係數,都假設物體質量為1,並把物體移動的向量作為V,同時會有一個反向的向量作為F(air),每次移動的時候都讓V向量減去一個時間量的F來達到類似的效果。

  實際使用上,參考這個大概圖。


  藍色的箭頭就是還沒減去阻力當前(前一個frame)的向量,而藍色的虛線就是先使用當前向量移動一個時間單位後再把向量減去阻力,為什麼藍色線段會在實際線段上方,是因為先用前一個frame的向量移動後,才把向量減去阻力,所以在移動上會較晚旋轉角度,線段會比實際線段高。
void EachFrame() {
    Vector3 prePos = transform.position; //移動前的位置
    float dt = Time.deltaTime;
    transform.position = transform.position + new Vector3(Vx, Vy, 0) * dt; //使用前一個frame的向量先移動了

    Vector3 deltaV = transform.position - prePos;
    Vector3 Vdir = deltaV / dt; //取得這段時間的向量,使用這個來求反作用力

    //移動完後才調整向量
    float dVx = (-k * Mathf.Pow(Vdir.x, 2)) * dt;
    float dVy = (-k * Mathf.Pow(Vdir.y, 2) - g) * dt;
    Vx += dVx; //加上阻力後的X軸速度
    Vy += dVy; //加上阻力後的Y軸速度
}



  橘色的箭頭就是減去阻力以後的當前向量,橘色虛線就是先把當前向量減去阻力後,再用這個向量移動一個時間單位,而這條線會在實際線段的下方,是因為先減去了阻力,使用改變後的向量移動,所以在移動上會提早旋轉角度,線段就會比實際線低。
void EachFrame()
{
    Vector3 prePos = transform.position; //移動前的位置
    float dt = Time.deltaTime;

    Vector3 preMove = transform.position + new Vector3(Vx, Vy, 0) * dt; //假設先移動了
    Vector3 deltaV = preMove - prePos; //使用預測的位置來取得單位時間的向量
    Vector3 Vdir = deltaV / dt; //除以單位時間,再用這個來算阻力

    //計算相反方向的向量,作為阻力。乘上單位時間,就是假設在這時間內受到的阻力
    float dVx = (-k * Mathf.Pow(Vdir.x, 2)) * dt;
    float dVy = (-k * Mathf.Pow(Vdir.y, 2) - g) * dt;
    Vx += dVx; //加上阻力後的X軸速度
    Vy += dVy; //加上阻力後的Y軸速度

    transform.position = transform.position + new Vector3(Vx, Vy, 0) * dt; //使用調整後的向量來移動
}

  因為這兩個都會有些微偏差,所以就乾脆把這兩個向量取其中間點,作為物體移動的目標點,也就是大概可以接近公式算出來的線,當k值為0的時候移動的軌跡會跟完美拋物線重疊。

  雖然阻力的計算有些亂搞,運作的流程也還有可以簡化的地方,簡單使用應該沒問題,如果要拿來做些什麼的話可能就要考慮考慮了,說不定用內建的物理引擎讓它跑還比較輕鬆。




實作測試
(素材來自於FGJ2014活動本組的資源,方向鍵上下調整砲管角度,空白鍵發射,同樣可以調整初速度跟阻力係數)





CODE
(拋物線移動物件Component,此物件只單純做一次拋物線移動部分,同時初始座標在(0,0),如果要有其他效果,碰撞、彈飛等等還需要額外設置)

public class ParabolaDragObject : MonoBehaviour {

    public GameObject target; //初始投射的目標方向,用gameObject只是方便在editor中方便拖拉編輯
    public float drag = 0.02f; //阻力的係數
    public float g = 9.81f;  //g = 9.81 m/s^2
    public float speed = 0.8f; //物體transform在拋物線路徑上移動的速度,此速度不影響拋物線的形狀
    public float V0 = 8.0f; //初速度
    public float ground = 0f; //地面的位置
    private Vector3 startPos = Vector3.zero; //備份起始點位置用

    void Start()
    {
        if (target != null)
            StartCoroutine(DragMove(target.transform.position));
    }

    //使用公式在Unity編輯室窗中畫出完美拋物線,方便編輯
    void OnDrawGizmos()
    {
        if (target == null)
            return;

        Vector3 dir = (target.transform.position - startPos).normalized;
        float theta = Mathf.Atan2(dir.y, dir.x); //the angle at which the projectile is launched

        float t = (V0 * Mathf.Sin(theta) + Mathf.Sqrt(Mathf.Pow(V0 * Mathf.Sin(theta), 2) + 2 * g * startPos.y)) / g;
        float distance = t * V0 * Mathf.Cos(theta);
        Debug.DrawLine(startPos, new Vector3(distance, 0, 0), Color.cyan);

        float h = startPos.y + (distance / 2) * Mathf.Tan(theta) - (g * (distance / 2) * (distance / 2)) / (2 * (V0 * Mathf.Cos(theta)) * (V0 * Mathf.Cos(theta)));
        Debug.DrawLine(new Vector3(distance / 2, 0, 0), new Vector3(distance / 2, h, 0), Color.green);

        //Now draw the parabola
        Vector3[] points = new Vector3[51];
        for (int i = 0; i < 51; ++i)
        {
            float x = startPos.x + (distance / 50) * i;
            float y = startPos.y + x * Mathf.Tan(theta) - (g * Mathf.Pow(x, 2)) / (2 * Mathf.Pow(V0 * Mathf.Cos(theta), 2));
            points[i] = new Vector3(x, y, 0);
        }

        for (int i = 0; i < 51 - 1; ++i)
        {
            //Parabola
            Gizmos.color = Color.gray;
            Gizmos.DrawLine(points[i], points[i + 1]);
        }
    }

    private IEnumerator DragMove(Vector3 target)
    {
        startPos = transform.position;
        float targetGround = ground;
        if (target.y < targetGround)
            targetGround = target.y;
  
        Vector3 dir = (target - startPos).normalized; //先取得初始投射方向的向量
        if (dir.x == 0) dir.x = 0.001f; //避免x=0無法移動error
        float theta = Mathf.Atan2(dir.y, dir.x); //再由向量求得初始角度
        float Vy = V0 * Mathf.Sin(theta); //Y軸初速度
        float Vx = V0 * Mathf.Cos(theta); //X軸初速度
        float k = this.drag; //係數

        while (true)
        {
            Vector3 prePos = transform.position; //移動前的位置
            float dt = Time.deltaTime * this.speed;

            Vector3 preMove = transform.position + new Vector3(Vx, Vy, 0) * dt; //使用前一個單位時間位置的移動向量,來預測下一個單位時間的位置

            Vector3 deltaV = preMove - prePos; //使用預測的位置來取得單位時間的向量
            Vector3 Vdir = deltaV / dt; //除以單位時間,求得原始向量

            //計算相反方向的向量,作為阻力。乘上單位時間,就是假設在這時間內受到的阻力
            float dVx = (-k * Mathf.Pow(Vdir.x, 2)) * dt; //Fx = -k * Vx * Vx
            float dVy = (-k * Mathf.Pow(Vdir.y, 2) - g) * dt; //Fy = -k * Vy * Vy - mg
            Vx += dVx; //加上阻力後的X軸速度
            Vy += dVy; //加上阻力後的Y軸速度

            Vector3 postMove = transform.position + new Vector3(Vx, Vy, 0) * dt; //使用加上阻力後的移動向量,來預測下一個單位時間的位置

            transform.position = (preMove + postMove) / 2; //最後把兩個預測點求其中點,並把物件移動到該點

            if (transform.position.y < targetGround) //高度小於地面就停止
                break;
            yield return null;
        }
    }
}


No comments:

Post a Comment