运行效果
场景中缩圈
小地图中挖孔
大地图中挖孔
小地图
方案1使用Mask
给了一个方形的mask组件,然后根据玩家位置计算出地图左下角的位置进行移动。这种实现方式虽然简单,但是会有两个问题:
1.Overdraw特别大,几乎很多时候会有整个屏幕的overdraw;
2.玩家在移动过程中,因为一直在持续移动图片的位置(做了适当的降频处理),所以会一直有UI的Mesh重建过程。
方案2使用RawImage,UV
小地图使用RawImage,设置显示大小为300*300,其中Texture,放入场景的顶视图
如何确定小地图的UV范围
1.先确定w的值为0.1,代表会从整个顶视图中取宽度占比0.1
2.再确定h的值,因为地图长宽不相等,按照 w 总宽 = h*总高,这样可以得到一个正方形显示
public float m_mapWidth = 1680; //世界中场景的宽度,米
public float m_mapHeight = 960; //世界中场景的高度,米
m_xScale = 0.1f;
m_yScale = m_xScale * m_mapWidth / m_mapHeight;
这样得到w = 0.1,h = 0.175
如何确定小地图的UV中X,Y
public void SetMeInMini()
{
float posPlayerX = m_player.position.x / m_mapWidth;
float posPlayerY = m_player.position.z / m_mapHeight;
m_imgMap.uvRect = new Rect(posPlayerX - m_xScale / 2, posPlayerY - m_yScale / 2, m_xScale, m_yScale);
Vector3 oriArrow = m_playerArrow.transform.eulerAngles;
oriArrow.z = -m_player.eulerAngles.y;
m_playerArrow.eulerAngles = oriArrow;
}
假设地图的起点是从0点开始(实际项目时可以加偏移值,x,y,代表3d场景是从偏移点开始,并且增加3d场景的实际宽高)。这里为了简单实现,未加偏移值,与实际宽高
因为地图从0点开始
x方向,我占地图的百分比 float posPlayerX = m_player.position.x / m_mapWidth;
y方向,我占地图的百分比float posPlayerY = m_player.position.z / m_mapHeight;
m_imgMap.uvRect = new Rect(posPlayerX - m_xScale / 2, posPlayerY - m_yScale / 2, m_xScale, m_yScale);
x方向百分比-m_xScale的一半,即为uv的x
y方向百分比-m_yScale的一半,即为uv的y
这样显示出玩家的位置,一定是在小地图的中间,并加上箭头表示我的方向
如何确定地图上目标在小地图位置
把目标的世界坐标,转换成小地图的localPosition
public Vector3 GetTarget2MiniMapPoint(Vector3 targetWorldPos,Vector3 posPlayer)
{
//世界坐标上与me的差,
float x = (targetWorldPos.x - posPlayer.x) * m_meter2Pixel;
float y = ( targetWorldPos.z - posPlayer.z) * m_meter2Pixel;
return new Vector3(x, y,0);
}
小地图大小为300*300,因为是正方形,所以300像素单位最多代表的米为
public float m_mapWidth = 1680; //世界中场景的宽度,米
public float m_xScale = 0; //表示 uv 的取值w, 0-1,例如x = 0.1
public float m_totalMeter = 0; //小地图,总像素长代表的米
m_totalMeter = m_mapWidth * m_xScale;
那么每个像素单位可代表的米
m_viewWidth = m_imgMap.rectTransform.rect.width;
m_meter2Pixel = m_viewWidth / m_totalMeter;
用目标的世界坐标x-我的世界坐标x,结果*m_meter2Pixel,则为目标在小地图localPosition
这里需要注意:
1.小地图的Pivot,min,max为0.5,才能让localPosition等于anchoredPosition,否则只能用anchoredPosition设置目标在小地图位置
2.目标点localPosition超过小地图的长宽,可以设置该点显示隐藏。或者使用RestMask2d
大地图
点击小地图,可展开大地图
如何确定我大地图的localPosition
世界坐标单位米与大地图上像素对应
float m_widthPixel = 1680; //大地图顶视图这张图的尺寸,单位像素
float m_heightPixel = 960;
public float m_widthScene = 1680; //场景中真实的最大宽,单位米
public float m_heightScene = 960;
public float m_scale = 0; //1米可对应多少像素。单位为 像素/米
m_scale = m_widthPixel / m_widthScene;
大地图采用真实像素大小,1680,960。虽然在1280*720的视图中有些边界显示不到,那是项目设计如此,边界不可达到
世界坐标转大地图localPosition
public Vector2 PosWorld2Local(Vector2 pos)
{
Vector2 ret = Vector2.zero;
float x = pos.x * m_scale;
x -= m_widthPixel / 2;
float y = pos.y * m_scale;
y -= m_heightPixel / 2;
ret = new Vector2(x, y);
return ret;
}
目标位置x * m_scale,可得到像素坐标x,-m_widthPixel / 2,地图bg的像素的一半,可得到在大地图中的localPosition
缩圈机制
1.小圆一定是全部包含在大圆内部。运动的是大圆,直到大圆与小圆圆心,半径重合
2.缩圈运动分两个阶段,第一阶段为向内切运动:大圆圆心不变,按照速度缩小大圆半径,直到大圆半径 = 圆心距离+小圆半径
3.第二阶段为先小圆运动:大圆圆心向着小圆圆心移动,同时大圆半径缩小,直到大圆半径= 小圆半径
第一阶段内切运动
小圆一开始在大圆内部,如果大圆半径R1> 小圆半径R2+圆心距离,说明还处在第一阶段向内切运动,否则转向第二阶段,向小圆运动
第二阶段向小圆运动
大圆的圆心P1向小圆圆心P2移动,每帧半径减少
float diffBigR = m_circleData.speed * Time.deltaTime
那么大圆圆心在x,y方向上变化量为
m_circleData.bigPos.x += diffBigR * m_circleData.cos;
m_circleData.bigPos.y += diffBigR * m_circleData.sin;
缩圈数据
public class CircleData
{
public float bigR; //大圆半径
public Vector2 bigPos; //世界坐标,大圆圆心
public float smallR; //小圆半径
public Vector2 smallPos;//世界坐标,小圆圆心
public float time = 10; //总共运动时间
public float speed; //速度
public float dis; //两圆心初始距离
public float cos; //大圆移动x上投影分量
public float sin; //大圆移动y上投影分量
速度speed即为每帧大圆半径需要减少数值,用半径差/总时间
speed = (bigR - smallR) / time;
dis两圆心初始距离
dis = Vector2.Distance(bigPos, smallPos);
cos,sin的运动分量
内切运动运动结束后,每次大圆圆心,x方向+半径变化值*cos,即为新的大圆圆心x位置
有以下几种状态
if (bigPos.x == smallPos.x && bigPos.y != smallPos.y)
{
cos = 0;
sin = 1;
}
else if (bigPos.x != smallPos.x && bigPos.y == smallPos.y)
{
cos = 1;
sin = 0;
}
else if (bigPos.x == smallPos.x && bigPos.y == smallPos.y)
{
cos = 0;
sin = 0;
}
else
{
float a = smallPos.x - bigPos.x;
float b = smallPos.y - bigPos.y;
cos = a / dis;
sin = b /dis;
}
大圆运动
在update中执行
1.每帧大圆半径的变化量为m_circleData.speed * Time.deltaTime
2.大圆半径的减少为m_circleData.bigR -= diffBigR;
3.当是向内切阶段运动,则只会减少大圆半径
4.当是向小圆阶段运动,减少大圆半径的同时,大圆圆心运动
m_circleData.bigPos.x += diffBigR * m_circleData.cos;
m_circleData.bigPos.y += diffBigR * m_circleData.sin;
5.直到大圆半径《=小圆半径,说明运动结束
float diffBigR = m_circleData.speed * Time.deltaTime;
m_circleData.bigR -= diffBigR;
if (m_circleData.bigR > m_circleData.smallR + m_circleData.dis)
{
Debug.Log("内切");
UpdateTransCircle();
}
else if (m_circleData.bigR > m_circleData.smallR && m_circleData.bigR <= m_circleData.smallR + m_circleData.dis)
{
Debug.Log("小圆");
m_circleData.bigPos.x += diffBigR * m_circleData.cos;
m_circleData.bigPos.y += diffBigR * m_circleData.sin;
UpdateTransCircle();
}
else if (m_circleData.bigR <= m_circleData.smallR)
{
m_isMove = false;
}
毒圈在场景中表现
为一个去掉上顶,下底的圆柱体,每次移动改变它的坐标与缩放
void UpdateTransCircle()
{
m_transCircle.position = new Vector3(m_circleData.bigPos.x, 0, m_circleData.bigPos.y);
m_transCircle.localScale = new Vector3(m_circleData.bigR * 2, 1, m_circleData.bigR * 2);
}
UGUI上毒圈挖孔
效果
小地图上显示
大地图上显示
其中白圈为小圆,即最终安全区
外围大圈会大圆不断缩小移动
小地图Mask
使用跟小地图同样像素大小的RawImage。
大地图Mask
可以使用不同像素大小,例如屏幕分辨率为1280720,大地图bg实际为1680960。但是他们的中心点是一致的,这样传递的localPosition在大地图上,和传递到Mask上是对等的
shader处理
v2f vert(appdata_t v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.worldPosition = mul(unity_ObjectToWorld, v.vertex);
o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
o.color = v.color;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
//显示白圈
float2 diffCurWithSmallllCenter = float2(i.worldPosition.x, i.worldPosition.y) - float2(_CenterSmall.x, _CenterSmall.y);
float disCurWithSmallCenter = sqrt(diffCurWithSmallllCenter.x * diffCurWithSmallllCenter.x + diffCurWithSmallllCenter.y * diffCurWithSmallllCenter.y);
if (abs(disCurWithSmallCenter - _SmallR) < _SmallWidth )
{
return float4(1, 1, 1, 1);
}
float2 center = float2(i.worldPosition.x, i.worldPosition.y) - float2(_Center.x, _Center.y);
float disSquare = (center.x * center.x + center.y * center.y);
//比较距离大小,可以不用开平方
clip(disSquare - _BigRSquare);
fixed4 col = tex2D(_MainTex, i.texcoord) * i.color;
return col;
}
显示小圆白圈
当前世界坐标与小圆圆心的坐标相差
float2 diffCurWithSmallllCenter = float2(i.worldPosition.x, i.worldPosition.y) - float2(_CenterSmall.x, _CenterSmall.y);
当前世界坐标与小圆圆心距离disCurWithSmallCenter,坐标开平方
float disCurWithSmallCenter = sqrt(diffCurWithSmallllCenter.x * diffCurWithSmallllCenter.x + diffCurWithSmallllCenter.y * diffCurWithSmallllCenter.y);
disCurWithSmallCenter - 小圆半径的绝对值,< _SmallWidth,直接返回白色
if (abs(disCurWithSmallCenter - _SmallR) < _SmallWidth )
{
return float4(1, 1, 1, 1);
}
因为小圆是要距离差的绝对值与圈宽_SmallWidth比较,所以不能使用平方比较
大圆挖孔
当前世界坐标与大圆圆心坐标差
float2 center = float2(i.worldPosition.x, i.worldPosition.y) - float2(_Center.x, _Center.y);
当前世界坐标与大圆圆心的距离的平方
float disSquare = (center.x * center.x + center.y * center.y);
距离的平方<大圆半径的平方,则舍弃
//比较距离大小,可以不用开平方
clip(disSquare - _BigRSquare);
因为只要比较距离的平方的大小,这样省去一步开平方计算,提高点性能
C#中传递参数给Shader
public void SetClip(Vector2 vec, float radius, Vector2 smallPos,float smallR)
{
Vector3 centerWrold = transform.TransformPoint(new Vector3(vec.x,vec.y,0));
m_mat.SetVector("_Center", centerWrold);
Vector3 pointInCircle = new Vector3(vec.x + radius, vec.y,0);
Vector3 pointInCircleWorld = transform.TransformPoint(pointInCircle);
Vector2 diffVecWorld = centerWrold - pointInCircleWorld;
float bigRSquare = diffVecWorld.x * diffVecWorld.x + diffVecWorld.y * diffVecWorld.y;
m_mat.SetFloat("_BigRSquare", bigRSquare);
Vector3 smallPosWorld = transform.TransformPoint(new Vector3(smallPos.x, smallPos.y, 0));
m_mat.SetVector("_CenterSmall", smallPosWorld);
Vector3 pointInCircleSmall = new Vector3(smallPos.x + smallR, smallPos.y, 0);
Vector3 pointInCircleWorldSamll = transform.TransformPoint(pointInCircleSmall);
Vector2 diffVecWorldSmall = smallPosWorld - pointInCircleWorldSamll;
float smallRSquare = diffVecWorldSmall.x * diffVecWorldSmall.x + diffVecWorldSmall.y * diffVecWorldSmall.y;
float smallRWorld = Vector2.Distance(smallPosWorld, pointInCircleWorldSamll);
m_mat.SetFloat("_SmallR", smallRWorld);
}
传递圆心坐标值
不管大地图,小地图,传递的坐标为基于地图的localPosition,所以都要用Mask的transform转为世界坐标。mask即是挖孔
Vector3 centerWrold = transform.TransformPoint(new Vector3(vec.x,vec.y,0));
传递半径值
大圆
传入参数radius是指UI上的像素值,先用圆心x+半径,得到半径上一点,在UI上localPosition
Vector3 pointInCircle = new Vector3(vec.x + radius, vec.y,0);
再转为mask世界坐标上一点
Vector3 pointInCircleWorld = transform.TransformPoint(pointInCircle);
大圆只需要传递世界坐标下半径的平方,在shader做为世界坐标差平方,与大圆半径平方比较,进行clip。shader中减少一次开根运算
Vector2 diffVecWorld = centerWrold - pointInCircleWorld;
float bigRSquare = diffVecWorld.x * diffVecWorld.x + diffVecWorld.y * diffVecWorld.y;
m_mat.SetFloat("_BigRSquare", bigRSquare);
小圆
小圆因为要画一个白圈,具有一定的宽度,所以只能传递小圆半径
传入半径smallR,是UI上像素长
先用圆心x+半径,得到半径上一点,在UI上localPosition
Vector3 pointInCircleSmall = new Vector3(smallPos.x + smallR, smallPos.y, 0);
再转基于mask的世界坐标
Vector3 pointInCircleWorldSamll = transform.TransformPoint(pointInCircleSmall);
算出半径上一点,与圆心的世界坐标上差距,即为小圆世界坐标下半径长,传入shader
Vector3 pointInCircleWorldSamll = transform.TransformPoint(pointInCircleSmall);
float smallRWorld = Vector2.Distance(smallPosWorld, pointInCircleWorldSamll);
m_mat.SetFloat("_SmallR", smallRWorld);
小地图传递值
大圆的世界坐标圆心,转小地图的localPosition
Vector3 bigV3 = new Vector3(CircleMgr.instance.m_circleData.bigPos.x, 0, CircleMgr.instance.m_circleData.bigPos.y);
Vector3 bigPos = GetTarget2MiniMapPoint(bigV3, m_player.position);
大圆的半径转成小地图的像素值
float bigR = CircleMgr.instance.m_circleData.bigR * m_meter2Pixel;
小圆同理,传递进入Mask
Vector3 bigV3 = new Vector3(CircleMgr.instance.m_circleData.bigPos.x, 0, CircleMgr.instance.m_circleData.bigPos.y);
Vector3 bigPos = GetTarget2MiniMapPoint(bigV3, m_player.position);
float bigR = CircleMgr.instance.m_circleData.bigR * m_meter2Pixel;
Vector3 smallV3 = new Vector3(CircleMgr.instance.m_circleData.smallPos.x, 0, CircleMgr.instance.m_circleData.smallPos.y);
Vector3 smallPos = GetTarget2MiniMapPoint(smallV3, m_player.position);
float smallR = CircleMgr.instance.m_circleData.smallR * m_meter2Pixel;
m_circleClip.SetClip(bigPos, bigR, smallPos, smallR);
大地图传递值
大圆圆心世界坐标转地图上localPosition
Vector2 bigPos = PosWorld2Local(CircleMgr.instance.m_circleData.bigPos);
大圆半径世界坐标(米为单位)转地图上像素单位
float bigR = CircleMgr.instance.m_circleData.bigR* m_scale;
小圆同理,传递进入Mask
Vector2 bigPos = PosWorld2Local(CircleMgr.instance.m_circleData.bigPos);
Vector2 smallPos = PosWorld2Local(CircleMgr.instance.m_circleData.smallPos);
float bigR = CircleMgr.instance.m_circleData.bigR* m_scale;
float smallR = CircleMgr.instance.m_circleData.smallR* m_scale;
m_circleClip.SetClip(bigPos,bigR, smallPos, smallR);