時光煮雨 Unity3D實現2D人物移動-總結篇

系列目錄

【Unity3D基礎】讓物體動起來①--基於UGUI的鼠標點擊移動html

【Unity3D基礎】讓物體動起來②--UGUI鼠標點擊逐幀移動函數

時光煮雨 Unity3D讓物體動起來③—UGUI DoTween&Unity Native2D實現動畫

時光煮雨 Unity3D實現2D人物動畫① UGUI&Native2D序列幀動畫this

時光煮雨 Unity3D實現2D人物動畫② Unity2D 動畫系統&資源效率spa

背景

最近研究Unity3d,2d尋路的實現。因此又一次涉及到了角色座標位移的問題。系統的對於這個簡單問題進行整理和總結。原本就是一個簡單的幾何問題,結果發現已經有兩個小坑,順便填上,這裏作下總結。3d

move1

實現

需求:經過鼠標點擊,控制2d角色移動,就是點哪裏,角色向移動到哪裏調試

問題分解:按照時間進行動畫分解,鼠標輸入(動畫開始)、平移(動畫進行)、移動結束(動畫結束)日誌

前提:這裏前面的文章基本解決了一些基礎的知識,好比IO獲取(鼠標輸入),移動的基本方式(Unity中的位置系統transform)orm

坑:一、平移中的平滑移動,二、如何肯定移動了目標點,並使物體中止下來htm

坑1:平移中的平滑移動

補充知識,關於角色的平移和位置更新,Unity無非就幾種方式

A、transform.Translate(new Vector3(1, 1, 1) * moveSpeed * Time.deltaTime); // Translate方法移動不會考慮剛體等碰撞(會直接穿過物體)

// 確保咱們的速度不會超過maxDistanceDelta
B、Vector3.MoveTowards(transform.position, targetPos.position, speed * Time.deltaTime);

// 速度會超過移動速度,像彈簧同樣
C、Vector3.Lerp(transform.position, targetPos.position, speed * Time.deltaTime);

D、直接設置transform.Positon,最簡單的方式

這個坑,真是坑了不少不少人,目前網上一半以上的教程,從嚴格意義上都是錯誤的,這裏真的想吐槽一下(太他媽不負責了),這個問題我在羣裏問過一次,結果還被懷疑是菜鳥,其實焦點仍是 我用紅色標出的這個線性插值函數,其實簡單的不得了,就是個直線方程。這裏能夠參考,如下這兩篇文章

unity3d問題集 <2> 對Vector3.Lerp 插值的理解

unity3d Vector3.Lerp解析 http://www.cnblogs.com/shenggege/p/5658650.html

分析爲何「速度會超過移動速度,像彈簧同樣」和 線性插值的函數,後來我仔細想了想,其實仍是本身知識掌握的不夠透徹,具體咱們瞭解之後分析下,經典教程中的函數

    public float moveSpeed;
    public float turnSpeed;

    private Vector3 moveDirection;
    // Use this for initialization
    void Start () {
        moveDirection = Vector3.right;
    }
   
    // Update is called once per frame
    void Update () {

        // 1
        Vector3 currentPosition = transform.position;
        // 2
        if( Input.GetButton("Fire1") ) {
            // 3
            Vector3 moveToward = Camera.main.ScreenToWorldPoint( Input.mousePosition );
            // 4
            moveDirection = moveToward - currentPosition;
            moveDirection.z = 0;
            moveDirection.Normalize();
        }

        Vector3 target = moveDirection * moveSpeed + currentPosition;
        transform.position = Vector3.Lerp( currentPosition, target, Time.deltaTime );

    }
}

這裏咱們看紅色部分的文字,這裏之因此不會出現彈簧移動的效果,主要是每次插值都是當前點和這幀將要移動點的位置的插值,其實這裏根本沒有必要 ,直接設置 transform.position = moveDirection * moveSpeed*Time.deltaTime + currentPosition;(其實自己就是一個 基於時間的線性移動)

還有 自己 Vector3.Lerp(transform.position, targetPos.position, speed * Time.deltaTime); 這麼用就有很大的問題

A、speed * Time.deltaTime 當speed設置很大而幀率很低的時候這個係數可能全是1,這樣根本就是不插值,

B、當用UGUI時座標系統是屏幕座標值很大,這樣插值會很不許(這也是我曾經問過的問題,不過沒有人回答我)

至此第一個坑填上了,下面我列出使用不一樣方式來進行移動的相關代碼

第一種,改進型插值移動

/// <summary>
/// 使用Vector3的插值進行更新位置
/// </summary>
private void MoveByVector3Lerp()
{
    //一、得到當前位置
    Vector3 curenPosition = this.transform.position;
    //二、得到方向
    if (Input.GetButton("Fire1"))
    {
        Vector3 moveToward = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        moveTowardPosition = moveToward;
        moveTowardPosition.z = 0;

        moveDirection = moveToward - curenPosition;
        moveDirection.z = 0;
        moveDirection.Normalize();
    }

    var distance = Vector3.Distance(curenPosition, moveTowardPosition);
   // Debug.Log(string.Format("curenPosition:{0}, moveTowardPosition{1},distance:{2},speed:{3}", curenPosition, moveTowardPosition, distance, speed * Time.deltaTime));
    if (distance < 0.01f)
    {
        transform.position = moveTowardPosition;
    }
    else
    {
        //三、插值移動
        //目標位置方向加上速度移動
        Vector3 target = moveDirection*speed*Time.deltaTime + curenPosition;
        target.z = 0;
        transform.position = target;
    }
}

  

第二種,MoveTowards進行移動更新

/// <summary>
/// 使用Vector3的MoveTowards 直接進行位置更新 
/// </summary>
private void MoveByVector3MoveTowards()
{
    //一、得到當前位置
    Vector3 curenPosition = this.transform.position;
    //二、得到方向
    if (Input.GetButton("Fire1"))
    {
        moveTowardPosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        moveTowardPosition.z =0;
    }
    if (Vector3.Distance(curenPosition, moveTowardPosition) < 0.01f)
    {
        transform.position = moveTowardPosition;
    }
    else
    {
        //三、插值移動
        //距離就等於 間隔時間乘以速度便可
        float maxDistanceDelta = Time.deltaTime * speed;
        transform.position = Vector3.MoveTowards(curenPosition, moveTowardPosition, maxDistanceDelta);
    }
}

  

第三種,transform.Translate

/// <summary>
/// 使用Vector3的Translate 直接進行位置更新 
/// </summary>
private void MoveByTransformTranslate()
{
    //一、得到當前位置
    Vector3 curenPosition = this.transform.position;
    //二、得到方向
    if (Input.GetButton("Fire1"))
    {
        moveTowardPosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        moveTowardPosition.z = 0;

        moveDirection = moveTowardPosition - curenPosition;
        moveDirection.z = 0;
        moveDirection.Normalize();
    }
    //三、插值移動
    Vector3 target = moveDirection * speed * Time.deltaTime + curenPosition;
    target.z = 0;
    if (Vector3.Distance(curenPosition, moveTowardPosition) < 0.01f)
    {
        transform.position = moveTowardPosition;
    }
    else
    {
        transform.Translate(target - curenPosition);
    }
}

  

坑2:如何肯定移動了目標點,並使物體中止下來

補充知識:其實坑1中列出的三種平移方法,其實並非什麼套路,不是什麼標準的動畫移動方式,雖然他們也是基於時間的,只能概括成一種簡單的順序幀移動,這裏我查了不少資料還有一種基於時間線的移動方式。

問題描述:這裏先說下坑2是怎麼回事,就是咱們但願角色移動到鼠標點擊的點之後停下來,結果發現停不下來,經過調試日誌主要的問題在這一行(這也是我之前提出過的一個問題,但無人解答)

if (Vector3.Distance(curenPosition, moveTowardPosition) < 0.01f)

實際上這行代碼很是不靠譜,至少有兩點

A、單位差別,UGUI中是屏幕座標也是localPositon像素,Native中是Unit兩個單位不一樣判斷的這個距離常量不同

B、因爲speed * Time.deltaTime 每幀移動的距離是與速度和幀率有關的,這個常量(0.01)必須與之匹配須要設置合理的值

C、使用插值計算3維座標偏差會擴大,這裏我用「第一種,改進型插值移動」,「第三種,transform.Translate」都出現了偏差較大的狀況,而「第二種,MoveTowards進行移動更新」,就很準確。

因此係統給出的函數

Vector3.MoveTowards(curenPosition, moveTowardPosition, maxDistanceDelta);

不是白給的,這也是不少人推薦使用這個函數的緣由(但不告訴咱們爲何)

最後給出我本身寫的基於時間線的位移實現

/// <summary>
    /// 鼠標點擊移動,目標點
    /// </summary>
    private Vector3 moveTowardPosition = Vector3.zero;
    private Vector3 moveStartPosition = Vector3.zero;
    private float totalTime = 0.0f;
    private float costTime = 0.0f;
    private float timePrecent = 0.0f;

    private bool _isRuning = false;

    /// <summary>
    /// 是否正在移動
    /// </summary>
    public bool IsRuning
    {
        get { return _isRuning; }
        set { _isRuning = value; }
    }

    private void MoveByTimeline()
    {
        /*
         * 得到移動的最終目標位置,根據移動速度得到一共須要移動的時間 totalTime
         * 每一幀,
         *   一、累加 已經逝去的時間,並獲得costTime,並得到移動的百分比 precent = costTime/totalTime
         *   二、得到當前精靈的位置,根據precent 進行位置插值,獲得這一幀應該移動的位置
         *   三、使用設置移動
         *   四、經過precent判斷是否<1 來判斷是否移動到了目標位置
         *   五、若是完成,則調用最後一次移動實現,終點移動偏差,並置爲一些標誌位
         */
        //得到當前位置
        Vector3 curenPosition = this.transform.position;
        if (Input.GetButton("Fire1"))
        {
            moveStartPosition = curenPosition;
            //得到移動終點位置
            moveTowardPosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            moveTowardPosition.z = 0;

            costTime = 0.0f;
            //計算記錄
            var subVector3 = moveTowardPosition - curenPosition;
            //計算須要移動的總時間
            totalTime = subVector3.magnitude / speed;

            _isRuning = true;
        }
        //若是已經移動
        if (_isRuning)
        {
            //若是時間百分比小於1 說明尚未移動到終點
            if (timePrecent < 1)
            {
                //累加時間
                costTime += Time.deltaTime;
                timePrecent = costTime/totalTime;

                Vector3 target = Vector3.Lerp(moveStartPosition, moveTowardPosition, timePrecent);
                transform.position = target;
  
            }
            else //大於或者等於1 了說明是最後一次移動
            {
                transform.position = moveTowardPosition;
                _isRuning = false;
                moveTowardPosition = Vector3.zero;
                timePrecent = 0.0f;
                costTime = 0.0f;
            }
        }
    }

  

這種方法基本排除了,移動到終點的位移偏差問題,缺點是使用的臨時變量較多(我不喜歡),而「第二種,MoveTowards進行移動更新」能夠基本不使用臨時變量。時間線動畫實際上這也是一些小的平移組件及itween的核心原理(爲何,還須要進一步探索,也許擴展性更強)

總結

反正被坑很不爽,不過也怪不了別人,仍是本身才疏學淺(不是天才,就使勁幹)。下一篇 繼續探索角色的系列目標點的移動

相關文章
相關標籤/搜索