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?

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Unity双屏飞屏是指在一个Unity应用程序中,将两个显示器连接成为一个连续的屏幕,并且应用程序的内容可以无缝地跨越这两个屏幕显示。 实现Unity双屏飞屏的原理如下: 1. 获取屏幕尺寸:首先,Unity会通过调用操作系统提供的API获取主显示器和辅助显示器的屏幕尺寸以及相对位置关系。 2. 定位摄像机:Unity会根据显示器的位置和分辨率来确定摄像机的位置和视野范围,以确保摄像机可以正确地渲染跨越多个显示器的场景。 3. 渲染分割:Unity将场景分割成多个视图,每个视图对应一个显示器。每个视图都有自己的摄像机和渲染目标,用于渲染特定显示器的图像。 4. 同步画面:为了实现屏幕画面的同步,Unity会使用多个摄像机同时渲染场景,并将各个视图的渲染结果进行合并,最终生成一个连续的图像。 5. 屏幕适配:为了在双屏飞屏中保持良好的用户体验,Unity会根据显示器的分辨率和纵横比,自动调整场景的布局和UI元素的位置,以适应不同的屏幕设置。 6. 全局坐标映射:Unity使用一个全局坐标映射系统,将场景中的物体的位置和尺寸进行映射,以确保它们正确地显示在双屏飞屏中。 总结起来,Unity双屏飞屏通过获取屏幕尺寸、定位摄像机、渲染分割、同步画面、屏幕适配和全局坐标映射等步骤来实现。这样,用户可以通过Unity应用程序,在连接的两个显示器上获得一个无缝连续的屏幕体验。 ### 回答2: Unity双屏飞屏实现原理是通过将Unity游戏窗口在两个显示屏上进行扩展显示,从而实现游戏画面的双屏显示。具体实现步骤如下: 首先,需要在Unity的编辑器中设置游戏窗口的分辨率为两个显示屏的总分辨率。这样Unity会自动将游戏画面进行扩展,使其能够同时在两个屏幕上显示。 其次,需要在游戏脚本中进行相应的配置,以适配双屏显示。可以使用Unity的Screen类来获取显示屏的分辨率和位置等信息。可以通过获取两个屏幕的宽度之和和高度的最大值来设置游戏画面的分辨率,以确保画面能够充满两个屏幕并且不会变形。 然后,需要调整相机的视口(Viewport)来适应双屏显示。可以通过设置相机的视口大小和位置来实现画面在两个屏幕上的显示。可以使用相机的ViewportRect属性来设置视口,其中视口的x和y参数代表相对于屏幕左下角的百分比,width和height参数代表相对于屏幕宽度和高度的百分比。 最后,可以在游戏脚本中根据需要自定义双屏的显示效果。可以在渲染游戏画面之前对相机的投影矩阵进行修改,以实现不同的透视效果或者投影方式。 总结来说,Unity双屏飞屏实现原理主要包括设置游戏窗口分辨率,配置游戏脚本适配双屏显示,调整相机视口来适应双屏,以及自定义双屏的显示效果。通过这些步骤,可以实现Unity游戏画面在两个显示屏上的同时显示。 ### 回答3: Unity是一款流行的游戏引擎,可以方便地实现双屏飞屏效果。在Unity实现双屏飞屏的原理主要涉及到以下几个方面。 首先,Unity提供了多屏幕管理的功能,可以通过代码控制多个屏幕的布局和显示效果。通过调用Unity的屏幕管理接口,可以获取当前系统中所有屏幕的信息,包括分辨率、位置等。通过获取到的屏幕信息,可以计算出所需的显示区域和布局方式。 其次,双屏飞屏效果的实现需要将屏幕空间映射到世界空间。通过使用Unity提供的相机组件,可以将屏幕空间的像素坐标转换为世界空间的坐标。通过设置相机的位置、旋转和投影方式,可以决定相机在世界空间中的位置和视野范围。 另外,为了实现双屏飞屏的效果,还需要对游戏物体进行适配和处理。可以通过设置游戏物体的位置、尺寸和层级关系,使其在不同屏幕上按照预期的方式进行展示。同时,还可以通过使用Unity提供的层级管理和渲染队列等功能,对游戏物体的渲染进行控制,以实现双屏飞屏效果。 最后,Unity还提供了丰富的工具和插件,可以方便地进行双屏飞屏效果的调试和优化。通过使用调试工具和性能分析工具,可以查看双屏飞屏效果的实时表现和性能状况,并进行相应的调整和优化。 综上所述,Unity实现双屏飞屏的原理主要包括多屏幕管理、屏幕空间到世界空间的转换、游戏物体适配和处理,以及调试和优化等方面。通过综合应用这些功能和工具,可以方便地实现双屏飞屏效果,提升游戏的展示效果和用户体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值