1. 程式人生 > >UGUI 類爐石傳說箭頭效果實現

UGUI 類爐石傳說箭頭效果實現

一.目標效果

之前在網上搜索過如何實現爐石箭頭效果,但是並沒有得到靠譜的答案,更別說完整的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