系列目錄
【Unity3D基礎】讓物體動起來①--基於UGUI的鼠標點擊移動
【Unity3D基礎】讓物體動起來②--UGUI鼠標點擊逐幀移動
時光煮雨 Unity3D讓物體動起來③—UGUI DoTween&Unity Native2D實現
時光煮雨 Unity3D實現2D人物動畫① UGUI&Native2D序列幀動畫
時光煮雨 Unity3D實現2D人物動畫② Unity2D 動畫系統&資源效率
背景
最近研究Unity3d,2d尋路的實現。所以又一次涉及到了角色坐標位移的問題。系統的對於這個簡單問題進行整理和總結。本來就是一個簡單的幾何問題,結果發現已經有兩個小坑,順便填上,這里做下總結。
實現
需求:通過鼠標點擊,控制2d角色移動,就是點哪里,角色向移動到哪里
問題分解:按照時間進行動畫分解,鼠標輸入(動畫開始)、平移(動畫進行)、移動結束(動畫結束)
前提:這里前面的文章基本解決了一些基礎的知識,比如IO獲取(鼠標輸入),移動的基本方式(Unity中的位置系統transform)
坑:1、平移中的平滑移動,2、如何確定移動了目標點,並使物體停止下來
坑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()
{
//1、獲得當前位置
Vector3 curenPosition = this.transform.position;
//2、獲得方向
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
{
//3、插值移動
//目標位置方向加上速度移動
Vector3 target = moveDirection*speed*Time.deltaTime + curenPosition;
target.z = 0;
transform.position = target;
}
}
第二種,MoveTowards進行移動更新
/// <summary>
/// 使用Vector3的MoveTowards 直接進行位置更新
/// </summary>
private void MoveByVector3MoveTowards()
{
//1、獲得當前位置
Vector3 curenPosition = this.transform.position;
//2、獲得方向
if (Input.GetButton("Fire1"))
{
moveTowardPosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
moveTowardPosition.z =0;
}
if (Vector3.Distance(curenPosition, moveTowardPosition) < 0.01f)
{
transform.position = moveTowardPosition;
}
else
{
//3、插值移動
//距離就等於 間隔時間乘以速度即可
float maxDistanceDelta = Time.deltaTime * speed;
transform.position = Vector3.MoveTowards(curenPosition, moveTowardPosition, maxDistanceDelta);
}
}
第三種,transform.Translate
/// <summary>
/// 使用Vector3的Translate 直接進行位置更新
/// </summary>
private void MoveByTransformTranslate()
{
//1、獲得當前位置
Vector3 curenPosition = this.transform.position;
//2、獲得方向
if (Input.GetButton("Fire1"))
{
moveTowardPosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
moveTowardPosition.z = 0;
moveDirection = moveTowardPosition - curenPosition;
moveDirection.z = 0;
moveDirection.Normalize();
}
//3、插值移動
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
* 每一幀,
* 1、累加 已經逝去的時間,並得到costTime,並獲得移動的百分比 precent = costTime/totalTime
* 2、獲得當前精靈的位置,根據precent 進行位置插值,得到這一幀應該移動的位置
* 3、使用設置移動
* 4、通過precent判斷是否<1 來判斷是否移動到了目標位置
* 5、如果完成,則調用最后一次移動實現,終點移動誤差,並置為一些標志位
*/
//獲得當前位置
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的核心原理(為什么,還需要進一步探索,也許擴展性更強)
總結
反正被坑很不爽,不過也怪不了別人,還是自己才疏學淺(不是天才,就使勁干)。下一篇 繼續探索角色的系列目標點的移動

