UGUI 類爐石傳說箭頭效果實現
阿新 • • 發佈:2019-02-11
一.目標效果
之前在網上搜索過如何實現爐石箭頭效果,但是並沒有得到靠譜的答案,更別說完整的Demo了。
後來仔細觀察了一下箭頭的效果,箭頭的身體是不斷向前流動的,箭頭的頭部以及尾部接近透明,應該是設計師為了方便使用者觀察起點和終點。當箭頭指向了合適的目標時,會出現聚焦圓環。
經過辛苦的摳圖得到了必須資源,最終成功用UGUI模仿著實現。
二.場景搭建
三.大致原理
1.使用Mask以及ScrollRect對NodesContainer以及其子物體進行遮罩;
2.重點在於將ArrowMask這個物體的Pivot置為TopCenter;
3.計算箭頭應該展示的長度,設定ArrowMask的RectTransform的DeltaSize,使其Height匹配。
四.箭頭實現
using UnityEngine;
using System.Collections;
using UnityEngine.EventSystems;
using System.Collections.Generic;
using UnityEngine.UI;
public class ArrowManager : MonoBehaviour{
public static ArrowManager instance;
/// <summary>
/// 箭頭聚焦的圓環
/// </summary>
Transform focusRingTfm;
RectTransform canvasRect;
/// <summary>
/// 將螢幕上的一點投影到RectTransform上的世界空間座標
/// </summary>
Vector3 worldPosInRect;
//Transform arrowHeadTfm ;
/// <summary>
/// The arrow mask tfm.
/// </summary>
Transform arrowMaskTfm;
Transform nodesContainerTfm;
/// <summary>
/// 箭頭從哪個物體起始的
/// </summary>
Transform startTfm;
/// <summary>
/// 箭頭身體的從第幾個孩子——Node開始
/// </summary>
int initialIndex;
/// <summary>
/// 臨時的Node節點
/// </summary>
Transform tempNodeTfm;
/// <summary>
/// 箭頭的可見長度
/// </summary>
float visibleLen = 1500f;
/// <summary>
/// 箭頭的流動速度
/// </summary>
[Range(10f,300f)]
public float flowSpeed = 150f;
/// <summary>
/// 遮罩箭頭Node的RectTransform
/// </summary>
RectTransform maskRect;
/// <summary>
/// Rectangle上拖拽的起始位點的世界座標
/// </summary>
Vector2 dragStartPos;
[Range(30f,120f)]
public float offset = 50f;
Vector3 offsetV = new Vector3 (0f, 50f,0f);
/// <summary>
/// Arrow 的 Head 部分的高度
/// </summary>
[Range(40f,120f)]
const float minHeight = 80f;
/// <summary>
/// 箭頭當前是否處於啟用狀態
/// </summary>
bool mActive = false;
float dist;
protected void Awake ()
{
instance = this;
}
// Use this for initialization
void Start () {
Initialize ();
}
void FixedUpdate()
{
MakeArrowFlow ();
}
void Initialize()
{
//arrowHeadTfm = transform.Find ("ArrowHead");
mActive = false;
arrowMaskTfm = transform.GetChild (0);
maskRect = arrowMaskTfm.GetComponent<RectTransform> ();
nodesContainerTfm = arrowMaskTfm.Find ("NodesContainer");
focusRingTfm = GameObject.Find ("FocusRing").transform;
canvasRect = GameObject.Find ("Canvas").GetComponent<RectTransform> ();
}
void MakeArrowFlow()
{
if (!mActive)
return;
//改變箭頭前端的透明度
for(int i =0;i<nodesContainerTfm.childCount;i++)
{
tempNodeTfm = nodesContainerTfm.GetChild(i);
tempNodeTfm.localPosition = new Vector3(0f,tempNodeTfm.localPosition.y+Time.fixedDeltaTime*flowSpeed,0f);
//改變箭頭起點的透明度
initialIndex = (int)(visibleLen/100f);
if (i <= 2) {
tempNodeTfm.GetComponent<Image> ().color = Color.Lerp (tempNodeTfm.GetComponent<Image> ().color, new Color (1, 1, 1, (60 * i + 60) / 255f), Time.fixedDeltaTime * 5f);
} else if (i <= (initialIndex + 3) && i >= (initialIndex - 3)) {
int diff = i - (initialIndex - 3);
tempNodeTfm.GetComponent<Image> ().color = Color.Lerp (tempNodeTfm.GetComponent<Image> ().color, new Color (1, 1, 1, (255f - 40f * diff) / 255f), Time.fixedDeltaTime * 5f);
} else if (i > (initialIndex + 3)) {
tempNodeTfm.GetComponent<Image> ().color = new Color (1, 1, 1, 0);
} else {
tempNodeTfm.GetComponent<Image> ().color = Color.white;
}
if (tempNodeTfm.localPosition.y > -100f)
{
tempNodeTfm.GetComponent<Image>().color = Color.white;
tempNodeTfm.localPosition = new Vector3(0f,-100 + nodesContainerTfm.GetChild(nodesContainerTfm.childCount-1).localPosition.y,0f);
tempNodeTfm.SetAsLastSibling();
}
}
}
public void OnBeginDrag(PointerEventData eventData)
{
mActive = true;
transform.localScale = Vector3.one;
Vector3 startObjPos = eventData.pointerDrag.gameObject.transform.position;
WorldPointInRectangle (canvasRect, startObjPos, Camera.main, out worldPosInRect);
transform.position = worldPosInRect;
dragStartPos = worldPosInRect;
}
/// <summary>
/// 將世界空間下一點投影到目標Rectangle上,得到投影點在世界空間中的座標
/// </summary>
/// <param name="rect">Rect.</param>
/// <param name="worldPos">World position.</param>
/// <param name="camera">Camera.</param>
/// <param name="worldPosInRect">World position in rect.</param>
void WorldPointInRectangle(RectTransform rect,Vector3 worldPos,Camera camera, out Vector3 worldPosInRect)
{
Vector3 screenPos = Camera.main.WorldToScreenPoint (worldPos);
RectTransformUtility.ScreenPointToWorldPointInRectangle (rect, screenPos, camera, out worldPosInRect);
}
public void OnDrag(PointerEventData eventData)
{
//transform.position = eventData.pointerDrag.gameObject.transform.position;
RectTransformUtility.ScreenPointToWorldPointInRectangle (canvasRect, eventData.position, Camera.main, out worldPosInRect);
transform.position = worldPosInRect;
transform.rotation = CaculateRotation (worldPosInRect,dragStartPos);
//dist = Vector2.Distance (worldPosInRect, dragStartPos) + offset;
CaculateVisibleLen(worldPosInRect);
dist = visibleLen + offset;
dist = dist >= minHeight ? dist : minHeight;
maskRect.sizeDelta = new Vector2 (100f, dist);
RayCastCheck ();
}
void RayCastCheck()
{
Ray ray =Camera.main.ScreenPointToRay(Input.mousePosition);
//Debug.DrawLine (ray.origin, ray.origin + 10000 * ray.direction, Color.red);
RaycastHit hit;
if (Physics.Raycast (ray, out hit, Mathf.Infinity, 1 << LayerMask.NameToLayer ("BattleUnit"))) {
Transform hitTfm = hit.collider.transform;
if (hitTfm != startTfm) {
ShowFocusRing (hitTfm);
RotateFocusRing (worldPosInRect);
}
} else {
HideFocusRing ();
}
}
void ShowFocusRing(Transform focusTargetTfm)
{
//focusRingTfm.localScale = Vector3.one;
focusRingTfm.gameObject.SetActive(true);
focusRingTfm.position = focusTargetTfm.position;
}
/// <summary>
/// Rotates the focus ring.
/// </summary>
/// <param name="currentPos">當前滑鼠投影到CanvasRect上的世界座標.</param>
void RotateFocusRing(Vector3 currentPos)
{
Vector3 focusRingPosInRect;
WorldPointInRectangle (canvasRect, focusRingTfm.position, Camera.main, out focusRingPosInRect);
focusRingTfm.rotation = CaculateRotation (currentPos, focusRingPosInRect);
}
void HideFocusRing()
{
//focusRingTfm.localScale = Vector3.zero;
focusRingTfm.gameObject.SetActive(false);
}
public void OnEndDrag(PointerEventData eventData)
{
transform.localScale = Vector3.zero;
this.startTfm = null;
mActive = false;
HideFocusRing ();
}
/// <summary>
/// 計算箭頭身體的可見長度
/// </summary>
/// <param name="currentPos">Current position.</param>
void CaculateVisibleLen(Vector2 currentPos )
{
Vector2 dirVector = currentPos - dragStartPos;
//因為Arrow本身是處在Canvas上的,Arrow的長度會受父物體影響
//這先將這一長度尺寸還原到世界空間下的尺寸,然後被父物體縮放影響,得到正確尺寸
visibleLen = dirVector.magnitude / canvasRect.localScale.x;
}
/// <summary>
/// 輸入當前拖拽位置,獲得箭頭的正確轉向——rotation
/// </summary>
/// <returns>The rotation.</returns>
/// <param name="currentPos">Current position.</param>
Quaternion CaculateRotation(Vector2 currentPos,Vector2 middlePos)
{
Vector2 fromVector = Vector2.up;
Vector2 toVector = currentPos - middlePos;
//雖然形參的名稱好像是會有方向區別
//即從哪個向量到哪個向量
//然而實際中操作發現,它只會返回兩個向量之間的最小非負數夾角
float angle = Vector2.Angle (fromVector, toVector);
//當x分量大於0時,Vector2.Angle 函式得到的角度為繞z軸順時針度數
if (toVector.x > 0) {
angle = 360f - angle;
}
//組合得到尤拉角
Vector3 diff = new Vector3 (0f, 0f, angle);
//將尤拉角轉化為四元數
Quaternion rotation = Quaternion.Euler (diff);
return rotation;
}
}
五.具體使用
通過實現UnityEngine.EventSystem中的介面來使用:
IBeginDragHandler
IDragHandler
IEndDragHandler
六.後記
整個Demo操作過程中遇到的關鍵點:
1.Mask的使用
2.錨點的設定
3.箭頭旋轉時尤拉角的獲得、尤拉角轉為四元數
4.將螢幕上一點或世界空間中的一點,通過攝像機投影到目標Rectangle上,如何得到該投影點的世界座標
5.使箭頭流動時,要禁用之前用的VerticalLayout元件,因為它會不停的計算並設定子元素的約束位置
6.各個計算步驟儘量按照功能做函式劃分,清晰、可讀性強、複用用性強、易修改維護
七.Demo資源
http://pan.baidu.com/s/1eSckzlC