Unity实现脱屏提示

先看效果图

这里写图片描述这里写图片描述

再贴代码

using System.Collections;
using UnityEngine;
using Joker.ResourceManager;
using PalmPoineer.Mobile;
using UnityEngine.UI;

namespace Joker.Battle {
    public enum EOutScreenGuiderType {
        Monster,
        PropWingman,
        PropBloodBottle,
        PropShield,
        PropShockWave,
        Friend,
        Enemy
    }

    /// <summary>
    /// 脱屏提示组件
    /// </summary>
    public class OutScreenGuider : EntityBase {
        public bool Disable { get; private set; }

        private static LineSegment _lineLeft;
        private static LineSegment _lineRight;
        private static LineSegment _lineBottom;
        private static LineSegment _lineTop;

        private static Vector2 _leftBottom;
        private static Vector2 _leftTop;
        private static Vector2 _rightBottom;
        private static Vector2 _rightTop;
        private static float _boundaryLeft;
        private static float _boundaryRight;
        private static float _boundaryTop;
        private static float _boundaryBottom;


        private static Rect _outGuideAreaRect;//脱屏指引区域Rect

        private static RectTransform _canvasRectTrans;

        private int _monsterId;
        private EOutScreenGuiderType _guiderType;
        private GameObject _outscreenUI;
        private float _outScreenUIWidth;//ui的像素宽
        private float _outScreenUIHeight;//ui的像素高
        private bool _notVisible;
        private Renderer _renderer;

        protected override EEntityType ThisType {
            get { return EEntityType.OutScreenGuider; }
        }

        private static RectTransform canvasRectTrans {
            get {
                if (_canvasRectTrans == null) {
                    _canvasRectTrans = UIManager.Instance.SceneUiCanvas.GetComponent<RectTransform>();
                }
                return _canvasRectTrans;
            }
        }

        private Vector2 employeScreenPos {
            get {
                //非Boss关卡,角色始终在屏幕中间
                return new Vector2(Screen.width / 2f, Screen.height / 2f);
            }
        }


        public static OutScreenGuider Create(Transform renderTarget, EOutScreenGuiderType guiderType, int monsterId = 0) {//加这个monsterId是为了怪物死亡的时候接收消息,立即让怪物的脱屏UI消失
            if (_lineLeft == null) {
                InitParams();
            }
            Renderer renderer = renderTarget.GetComponent<Renderer>();
            Debug.Assert(renderer, string.Format("脱屏检测需要依附于MeshRender, 请确保有该组件。类型:{0}, renderTarget: {1}", guiderType, renderTarget), renderTarget.gameObject);
            OutScreenGuider instance = EntityBase.Create<OutScreenGuider>(renderTarget.gameObject, EntityPool.Instance);
            instance.Init(guiderType, renderer, monsterId);
            return instance;
        }

        public override void Receive(string actionName, Hashtable args) {

            if (actionName.Equals(EntityConsts.Actions.ON_VICTORY_COURSE_START) ||
                actionName.Equals(EntityConsts.Actions.ON_FAIL_COURSE_START)) {
                Disable = true;
                _outscreenUI.SetActive(false);
            }

            if (actionName.Equals(EntityConsts.Actions.ON_MONSTER_DEAD)) {
                int id = (int) args["targetId"];
                if (id == _monsterId) {
                    Disable = true;
                    _outscreenUI.SetActive(false);
                }
            }
        }


        public void Init(EOutScreenGuiderType guiderType, Renderer renderer, int monsterId) {
            _guiderType = guiderType;
            _renderer = renderer;
            if (_guiderType == EOutScreenGuiderType.Monster) {
                _monsterId = monsterId;         
            }
            _outscreenUI = CreateUIByType();
            StartCoroutine(DelayToEnableGuide());
        }

        /// <summary>
        /// 纠正世界坐标,当坐标位于摄像机平面的后方。
        /// </summary>
        /// <param name="position"></param>
        /// <param name="camera"></param>
        /// <returns></returns>
        public static Vector3 CalculateWorldPosWhenBehindCamera(Vector3 position, Camera camera) {
            //if the point is behind the camera then project it onto the camera plane
            Vector3 camNormal = camera.transform.forward;
            Vector3 vectorFromCam = position - camera.transform.position;
            float camNormDot = Vector3.Dot(camNormal, vectorFromCam.normalized);
            if (camNormDot <= 0f) {
                //we are beind the camera, project the position on the camera plane
                float camDot = Vector3.Dot(camNormal, vectorFromCam);
                Vector3 proj = (camNormal * camDot * 1.01f);   //small epsilon to keep the position infront of the camera
                position = camera.transform.position + (vectorFromCam - proj);
            }
            return position;
        }

        /// <summary>
        /// 计算平面上两线段的交点
        /// </summary>
        /// <param name="line1"></param>
        /// <param name="line2"></param>
        /// <param name="crossPoint">交点</param>
        /// <returns>平行无交点则返回假,否则返回真</returns>
        public static bool CalculateCrossPointOfTwoLineSegment(LineSegment line1, LineSegment line2, out Vector2 crossPoint, Rect rect) {
            Vector2 P1 = line1.Origin;
            Vector2 P2 = line1.End;
            Vector2 P3 = line2.Origin;
            Vector2 P4 = line2.End;

            crossPoint = Vector2.zero;

            float denominator = (P1.x - P2.x) * (P3.y - P4.y) - (P1.y - P2.y) * (P3.x - P4.x);
            if (denominator == 0) {//平行的情况
                return false;
            }

            float px = ((P1.x * P2.y - P1.y * P2.x) * (P3.x - P4.x) - (P1.x - P2.x) * (P3.x * P4.y - P3.y * P4.x)) / denominator;
            float py = ((P1.x * P2.y - P1.y * P2.x) * (P3.y - P4.y) - (P1.y - P2.y) * (P3.x * P4.y - P3.y * P4.x)) / denominator;

            float distanceTargetToCross = Vector2.Distance(line2.End, new Vector2(px, py));
            float distanceOriginToEnd = Vector2.Distance(line2.Origin, line2.End);


            //延长线上的交点--非法
            if (distanceTargetToCross > distanceOriginToEnd) {
                return false;
            }

            //另一条平行线上的交点--非法
            if (!CheckTargetIsInScreenRect(new Vector2(px, py), rect)) {
                return false;
            }

            crossPoint = new Vector2(px, py);
            return true;
        }

        protected override void OnDestroyed() {
            if (_outscreenUI != null) {
                Destroy(_outscreenUI);
            }
        }

        private IEnumerator DelayToEnableGuide() {
            Disable = true;
            yield return new WaitForSeconds(1f);
            Disable = false;
        }

        private static void InitParams() {
            OutScreenOffsetParams offsetParams = RuntimeResourceManager.Instance.ClientConfig.OutScreenParams;

            //屏幕坐标系的坐标计算
            _boundaryLeft = offsetParams.OffsetPercentToLeft*Screen.width;
            _boundaryRight = offsetParams.OffsetPercentToRight*Screen.width;
            _boundaryTop = offsetParams.OffsetPercentToTop*Screen.height;
            _boundaryBottom = offsetParams.OffsetPercentToBottom*Screen.height;

            _lineLeft = new LineSegment(new Vector2(_boundaryLeft, _boundaryBottom),
                new Vector2(_boundaryLeft, Screen.height - _boundaryTop));
            _lineRight = new LineSegment(new Vector2(Screen.width - _boundaryRight, _boundaryBottom),
                new Vector2(Screen.width - _boundaryRight, Screen.height - _boundaryTop));
            _lineBottom = new LineSegment(new Vector2(_boundaryLeft, _boundaryBottom),
                new Vector2(Screen.width - _boundaryRight, _boundaryBottom));
            _lineTop = new LineSegment(new Vector2(_boundaryLeft, Screen.height - _boundaryTop),
                new Vector2(Screen.width - _boundaryRight, Screen.height - _boundaryTop));

            _outGuideAreaRect = Rect.MinMaxRect(_boundaryLeft, _boundaryRight, Screen.width - _boundaryLeft, Screen.height - _boundaryRight);


            _leftBottom = TranslateToUICordinate(new Vector2(_boundaryLeft, _boundaryBottom));
            _leftTop = TranslateToUICordinate(new Vector2(_boundaryLeft, Screen.height - _boundaryTop));
            _rightBottom = TranslateToUICordinate(new Vector2(Screen.width - _boundaryRight, _boundaryBottom));
            _rightTop = TranslateToUICordinate(new Vector2(Screen.width - _boundaryRight, Screen.height - _boundaryTop));

            //DebugBoundary();
        }

        private static void DebugBoundary() {
            GameObject dotPrefab = Resources.Load<GameObject>("DebugDot");
            GameObject lb = Instantiate(dotPrefab) as GameObject;
            lb.name = "left_bottom";

            lb.transform.SetParent(UIManager.Instance.SceneUiCanvas.transform);
            lb.transform.localScale = Vector3.one;
            lb.transform.localPosition = _leftBottom;

            GameObject lt = Instantiate(dotPrefab) as GameObject;
            lt.name = "left_top";
            lt.transform.SetParent(UIManager.Instance.SceneUiCanvas.transform);
            lt.transform.localScale = Vector3.one;
            lt.transform.localPosition = _leftTop;

            GameObject rb = Instantiate(dotPrefab) as GameObject;
            rb.name = "right_bottom";
            rb.transform.SetParent(UIManager.Instance.SceneUiCanvas.transform);
            rb.transform.localScale = Vector3.one;
            rb.transform.localPosition = _rightBottom;

            GameObject rt = Instantiate(dotPrefab) as GameObject;
            rt.name = "right_top";
            rt.transform.SetParent(UIManager.Instance.SceneUiCanvas.transform);
            rt.transform.localScale = Vector3.one;
            rt.transform.localPosition = _rightTop;
        }


        private void Update() {
            if (Disable) {
                _outscreenUI.SetActive(false);
                return;
            }

            _notVisible = !_renderer.isVisible;

            if (_notVisible) {
                //Debug.DrawLine(transform.position, employeTrans.position, Color.red, 0.01f);//调试用

                Vector2 pos; 
                if (CalculateUICurrentPos(out pos) && pos!= Vector2.zero) {
                    _outscreenUI.SetActive(true);
                    _outscreenUI.transform.localPosition = pos;//有效的才更新

                    if (_guiderType.Equals(EOutScreenGuiderType.Monster)) {
                       _outscreenUI.transform.SetAsFirstSibling();//降低怪物UI的显示层级 
                    }
                }
                //else {
                //    //Debug.LogError("异常脱屏情况!");
                //    _outscreenUI.SetActive(false); 
                //}
            }
            else {
                _outscreenUI.SetActive(false);
            }
        }


        private GameObject CreateUIByType() {
            string uiPrefabName = string.Empty;
            uiPrefabName = GlobalConfig.OutScreenConfig[_guiderType.ToString()].ToString();
            if (string.IsNullOrEmpty(uiPrefabName)) {
                Debug.LogError("脱屏UI的prefab名读取失败!");
                return null;
            }

            GameObject go = UIManager.Instance.CreateUI(uiPrefabName, UIManager.Instance.SceneUiCanvas.gameObject);
            go.gameObject.SetActive(false);
            go.transform.localPosition = new Vector3(1000f, 1000f, 0f);//初始化的时候拉远,防止出现在屏幕中间
            InitUIItemSize(go);

            return go;
        }

        private void InitUIItemSize(GameObject itemGo) {
            Image img = itemGo.GetComponentInChildren<Image>();
            RectTransform rt = img.GetComponent<RectTransform>();
            _outScreenUIWidth = rt.sizeDelta.x;
            _outScreenUIHeight = rt.sizeDelta.y;
        }


        private bool CalculateUICurrentPos(out Vector2 uiPos) {
            uiPos = Vector2.zero;
            Vector2 cross = Vector2.zero;
            LineSegment lineSegmentTargetToEmploye = GetLineSegmentTargetToEmploye();

            //[左边竖直]线段
            if (CalculateCrossPointOfTwoLineSegment(_lineLeft, lineSegmentTargetToEmploye, out cross, _outGuideAreaRect)) {
                uiPos = TranslateToUICordinate(cross);
                //uiPos = uiPos + Vector2.right * (_outScreenUIWidth / 2f);
                CorrectUIPos(uiPos, out uiPos, _outScreenUIWidth/2f, _outScreenUIHeight/2f);
                return true;
            }

            //[右边竖直]线段
            if (CalculateCrossPointOfTwoLineSegment(_lineRight, lineSegmentTargetToEmploye, out cross, _outGuideAreaRect)) {
                uiPos = TranslateToUICordinate(cross);

                //uiPos = uiPos - Vector2.right * (_outScreenUIWidth / 2f); 
                CorrectUIPos(uiPos, out uiPos, _outScreenUIWidth / 2f, _outScreenUIHeight/2f);
                return true;
            }

            //[下边水平]线段
            if (CalculateCrossPointOfTwoLineSegment(_lineBottom, lineSegmentTargetToEmploye, out cross, _outGuideAreaRect)) {
                uiPos = TranslateToUICordinate(cross);
                //uiPos = uiPos + Vector2.up * (_outScreenUIHeight / 2f); 
                CorrectUIPos(uiPos, out uiPos, _outScreenUIWidth/2f, _outScreenUIHeight/2f);

                return true;
            }

            //[上边水平]线段
            if (CalculateCrossPointOfTwoLineSegment(_lineTop, lineSegmentTargetToEmploye, out cross, _outGuideAreaRect)) {
                uiPos = TranslateToUICordinate(cross);
                //uiPos = uiPos - Vector2.up * (_outScreenUIHeight / 2f);  
                CorrectUIPos(uiPos, out uiPos, _outScreenUIWidth/2f, _outScreenUIHeight/2f);
                return true;
            }

            //存在目标的屏幕坐标在区域内,这样的情况,连线是没有交点的。
            //原因在于visible的检测和坐标的改变是不同步的,visible的检测滞后了。
            return false;
        }

        /// <summary>
        /// 把屏幕坐标转化为UI坐标
        /// </summary>
        /// <param name="screenPos"></param>
        /// <returns></returns>
        private static Vector2 TranslateToUICordinate(Vector2 screenPos) {
            Vector2 uiPos;
            RectTransformUtility.ScreenPointToLocalPointInRectangle(
                canvasRectTrans,
                screenPos,
                null,
                out uiPos
                );

            return uiPos;
        }

        private static bool CorrectUIPos(Vector2 unCorrected, out Vector2 corrected, float widthOffset, float heightOffset) {
            corrected = unCorrected;

            if (corrected.x < _leftBottom.x + widthOffset) {
                corrected = new Vector2(_leftBottom.x + widthOffset, corrected.y);
            }

            if (corrected.x > _rightBottom.x - widthOffset) {
                corrected = new Vector2(_rightBottom.x - widthOffset, corrected.y);
            }

            if (corrected.y < _leftBottom.y + heightOffset) {
                corrected = new Vector2(corrected.x, _leftBottom.y + heightOffset);
            }

            if (corrected.y > _leftTop.y- heightOffset) {
                corrected = new Vector2(corrected.x, _leftTop.y - heightOffset);
            }

            return unCorrected == corrected;
        }

        private LineSegment GetLineSegmentTargetToEmploye() {
            Vector2 targetScreenPos = GetTargetScreenPos(transform.position);//该脚本依附于Target,所以直接去trans.position
            //Debug.DrawLine(employeScreenPos, targetScreenPos, Color.red, 0.1f);
            return new LineSegment(employeScreenPos, targetScreenPos);
        }

        private Vector2 GetTargetScreenPos(Vector3 worldPos) {
            Vector3 correctWorldPos = CalculateWorldPosWhenBehindCamera(worldPos, CameraManager.Instance.MainCamera);
            Vector3 screenPos = CameraManager.Instance.MainCamera.WorldToScreenPoint(correctWorldPos);
            //Debug.Log("guide target pos: " + correctWorldPos + "   screen pos: " + screenPos);
            return screenPos;
        }

        private static bool CheckTargetIsInScreenRect(Vector2 screenPos, Rect rect) {
            //因为Rect是半闭半开区间,即包含xMin,yMin,不包含xMax,yMax,所以需要做适当修正
            float correctFactor = 0.1f;

            if (Mathf.Abs(screenPos.x - rect.xMax) <= 0.01f) {
                float correctX = screenPos.x - correctFactor;
                screenPos = new Vector2(correctX, screenPos.y);
            }
            if (Mathf.Abs(screenPos.y - rect.yMax) <= 0.01f) {
                float correctY = screenPos.y - correctFactor;
                screenPos = new Vector2(screenPos.x, correctY);
            }
            bool contain = rect.Contains(screenPos);
            return contain;
        }
    }

    /// <summary>
    /// 二维平面上的线段
    /// </summary>
    public class LineSegment {
        public Vector2 Origin;
        public Vector2 End;

        public LineSegment(Vector2 origin, Vector2 end) {
            this.Origin = origin;
            this.End = end;
        }
    }
}

会遇到的问题

当脱屏的目标对象的坐标位于摄像机的背后,转换出来的屏幕坐标就会区域一个很大的数值,这个时候就需要纠正坐标。
这里写图片描述

纠正代码如下:

  public static Vector3 CalculateWorldPosWhenBehindCamera(Vector3 position, Camera camera) {
            //if the point is behind the camera then project it onto the camera plane
            Vector3 camNormal = camera.transform.forward;
            Vector3 vectorFromCam = position - camera.transform.position;
            float camNormDot = Vector3.Dot(camNormal, vectorFromCam.normalized);
            if (camNormDot <= 0f) {
                //we are beind the camera, project the position on the camera plane
                float camDot = Vector3.Dot(camNormal, vectorFromCam);
                Vector3 proj = (camNormal * camDot * 1.01f);   //small epsilon to keep the position infront of the camera
                position = camera.transform.position + (vectorFromCam - proj);
            }
            return position;
        }

unity官方社区也有一篇关于这个问题的讨论:camera.WorldToScreenPoint() bug?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值