先看效果图
再贴代码
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?