强制引导需要的内容
开发强制引导,则要求玩家必须点击指定的地方、按钮,否则不予受理。那么基于这个通用需求,可以分析出我们核心需要的内容主要有两部分:
- 置灰图或遮罩图:我们需要通过这张图片,将屏幕置灰,并突出需要玩家点击的按钮或区域;
- 非强制引导交互屏蔽:强制引导阶段需要屏蔽任何非引导按钮的点击等交互操作。
方案1,Canvas
使用一张Mask图片实现屏幕置灰功能,并屏蔽所有UI交互。同时,将需要高亮或突出的按钮添加Canvas组件,以确保该UI元素不会被Mask图片屏蔽。
网上有看到过这样的方案,但是需要手动维护Canvas组件的添加删除,如果游戏架构开发不完善则很可能会因遗漏导致后续出现bug,所以没有选用此方案。
方案2,事件穿透
使用一张Mask图片实现屏幕置灰功能,并屏蔽所有UI交互。使用事件穿透实现对指定按钮交互不受屏蔽影响。在此基础上,Mask图片如果没有额外的显示需求,则可以直接生成镂空图片,以实现置灰其他UI而不置灰指定UI的功能。
后续开发说明以方案2为基准。
事件穿透
UGUI的事件穿透,借助EventSystem
就可以实现。
EventPermeate
代码如下:
public class EventPermeate : MonoBehaviour, IPointerClickHandler, IPointerDownHandler, IPointerUpHandler
{
/// <summary>
/// 事件穿透对象
/// </summary>
[HideInInspector] public GameObject target;
/// <summary>
/// 监听点击
/// </summary>
public void OnPointerClick(PointerEventData eventData)
{
PassEvent(eventData, ExecuteEvents.submitHandler);
PassEvent(eventData, ExecuteEvents.pointerClickHandler);
}
/// <summary>
/// 监听按下
/// </summary>
public void OnPointerDown(PointerEventData eventData)
{
PassEvent(eventData, ExecuteEvents.pointerDownHandler);
}
/// <summary>
/// 监听抬起
/// </summary>
public void OnPointerUp(PointerEventData eventData)
{
PassEvent(eventData, ExecuteEvents.pointerUpHandler);
}
/// <summary>
/// 将事件透下去
/// </summary>
public void PassEvent<T>(PointerEventData data, ExecuteEvents.EventFunction<T> function) where T : IEventSystemHandler
{
List<RaycastResult> results = new List<RaycastResult>();
EventSystem.current.RaycastAll(data, results);
for (int i = 0, imax = results.Count; i < imax; i++)
{
if (target == results[i].gameObject)
{
ExecuteEvents.Execute(results[i].gameObject, data, function);
break;
}
}
}
}
EventPermeate
添加到Mask图片,并指定可以穿透的目标,就可以实现事件穿透功能。代码很好理解此处就不再赘述。
遮罩图片的生成
思路
遮罩图片的生成我使用Image配合自定义Shader实现。思路如下:
- 继承Image,重写
OnPopulateMesh
方法,在这里面去生成我们所需镂空的形状,并对所有顶点的颜色进行赋值- 非镂空形状上的顶点,正常赋予颜色值
- 镂空形状上的顶点,赋予颜色值,但是alpha值设为0
- 实现一个UGUI的Shader,顶点计算照旧,片元计算处需要特殊处理:
- 若片元的alpha值为0,则该片元的alpha值就返回0
- 若片元的alpha值不为0,则使用从Image颜色传递来的alpha值
Shader说明
下面给出Shader片元算法:
float _MaskAlpha; // 由Image传入的alpha值
fixed4 frag(v2f i) : SV_Target
{
fixed4 col = i.color;
return fixed4(col.rgb, col.a > 0 ? _MaskAlpha : 0);
}
此处所针对的需求,是围绕Mask图片仅仅是一层半透明遮罩展开。如果需要的是一张图片,则还需要通过UV进行采样,如此一来在继承的Image中,每个顶点需要赋予正确的UV值才可以(计算很简单,但会额外增加一些计算量)。
Image图片生成说明
覆写OnPopulateMesh
方法生成我们所需要的镂空图片,核心思路如下(所有生成的三角形保持顺时针):
- 计算出镂空部分的AABB,然后将Image以内、AABB以外的部分按照固定格式生成三角形数据
- 限制镂空部分为凸多边形,这样又可以按照以下规则将AABB内的三角形生成出来:
- 寻找凸多边形的最左点、最高点、最右点、最低点
- 从最左点遍历至最高点,与AABB左上角依次生成三角形
- 从最高点遍历至最右点,与AABB右上角依次生成三角形
- 从最右点遍历至最低点,与AABB右下角依次生成三角形
- 从最低点遍历至最左点,与AABB左下角依次生成三角形
- 凸多边形内部以一个点为起始点,遍历剩余的点生成这个凸多边形的所有三角形
- 寻找凸多边形的最左点、最高点、最右点、最低点
核心算法思路展示如下:
// Image生成核心算法,rect是Image的Rect,innerPos是需要镂空的凸多边形的顶点坐标数据
private void _PickOutConvex(VertexHelper toFill, Rect rect, List<Vector2> innerPos)
{
toFill.Clear();
// 凸多边形获取边界四点
int left, top, right, bottom;
...
// 顶点数量
int innerCount = innerPos.Count;
Vector2 min = new Vector2(innerPos[left].x, innerPos[bottom].y);
Vector2 max = new Vector2(innerPos[right].x, innerPos[top].y);
Color maskColor = new Color(color.r, color.g, color.b, 0);
// 凸多边形AABB的四个点的计算与添加
int nA = toFill.currentVertCount;
toFill.AddVert(new UIVertex() { position = new Vector2(min.x, max.y), color = color });
toFill.AddVert(new UIVertex() { position = max, color = color });
toFill.AddVert(new UIVertex() { position = new Vector2(max.x, min.y), color = color });
toFill.AddVert(new UIVertex() { position = min, color = color });
// 凸多边形顶点数据添加
int ni = toFill.currentVertCount;
for (int i = 0; i < innerCount; i++) toFill.AddVert(innerPos[i], maskColor, Vector4.zero);
// 从最左点遍历至最高点,与AABB左上角依次生成三角形
for (int i = left, imax = left > top ? top + innerCount : top; i < imax; i++)
{
int index1 = i % innerCount + ni;
int index2 = (i + 1) % innerCount + ni;
toFill.AddTriangle(nA, index2, index1);
}
// 从最高点遍历至最右点,与AABB右上角依次生成三角形
...
// 从最右点遍历至最低点,与AABB右下角依次生成三角形
...
// 从最低点遍历至最左点,与AABB左下角依次生成三角形
...
// 凸多边形内部三角形的构建
for (int i = 1, imax = innerCount - 1; i < imax; i++) toFill.AddTriangle(ni, ni + i, ni + i + 1);
// Image的四个顶点的添加
int nX = toFill.currentVertCount;
toFill.AddVert(new UIVertex() { position = new Vector2(rect.xMin, rect.yMax), color = color }); // 0
toFill.AddVert(new UIVertex() { position = rect.max, color = color }); // 1
toFill.AddVert(new UIVertex() { position = new Vector2(rect.xMax, rect.yMin), color = color }); // 2
toFill.AddVert(new UIVertex() { position = rect.min, color = color }); // 3
// Image到凸多边形AABB空间的三角形生成
toFill.AddTriangle(nX + 0, nX + 1, nA + 1);
toFill.AddTriangle(nA + 1, nA + 0, nX + 0);
...
}
有了上述方法后,剩下的就是在OnPopulateMesh
方法内,根据指定的几种固定形状,生成他们的顶点数据,再传入_PickOutConvex
方法,即可获得所需的Mask图片。
可优化
这套实现,已经完成了这套方案下所有的核心点。根据不同的应用场景,可以扩展shader或是Image的算法,以实现效果、性能上的需求。