吃鸡游戏小地图的实现

Posted by JohnWayne on June 2, 2023

最近负责吃鸡游戏中小地图部分,总结了一些经验,分享队友位置更新和毒圈实现思路

毒圈

首先,毒圈不可以用图片实现,缩放后不能保证圈的宽度,而且缩小后分辨率低到看不清圈。最开始我用LineRender来画圈,后来发现用shader实现更好。

LineRender

private static int segments = 360;
private static int pointCount = segments + 1;
private static Vector3[] pointsArray;
pointsArray = new Vector3[pointCount];

public static void DrawCircle(GameObject container, float radius, float lineWidth)
{
    var line = container.GetComponent<LineRenderer>();
    line.useWorldSpace = false;
    line.startWidth = lineWidth;
    line.endWidth = lineWidth;
    line.positionCount = segments + 1;
    
    for (int i = 0; i < pointCount; i++)
    {
        var rad = Mathf.Deg2Rad * (i * 360f / segments);
        pointsArray[i] = new Vector3(Mathf.Sin(rad) * radius, Mathf.Cos(rad) * radius, 0);
    }
    
    line.SetPositions(pointsArray);
}

LineRender本来是画线段的,而且可以替换材质,进圈最近的虚线就是用LineRender实现的,后来想到圆也可以用线段组成,在网上也看到了相关实现方法。但毕竟这种实现需要开辟数组,为了极致性能,可以写个shader,传半径和圆心坐标。

Shader

Shader "Unlit/CircleSeletor"
{
Properties
    {
        _BoundColor("Bound Color", Color) = (1,1,1,1)
        _BgColor("Background Color", Color) = (1,1,1,1)
        _MainTex("Albedo (RGB)", 2D) = "white" {}
        _BoundWidth("BoundWidth", float) = 10
        _ComponentWidth("ComponentWidth", float) = 100
        _Radius("Radius", Range(0.0, 100)) = 0.5
        _CenterX("CenterX", float) = 0.5
        _CenterY("CenterY", float) = 0.5
    }
SubShader{
Pass
            {
            Blend SrcAlpha OneMinusSrcAlpha
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag Lambert alpha
            // make fog work
            #pragma multi_compile_fog
            #include "UnityCG.cginc"
            sampler2D _MainTex;
            float _BoundWidth;
            fixed4 _BoundColor;
            fixed4 _BgColor;
            float _ComponentWidth;
            float _Radius;
            float _CenterX;
            float _CenterY;
            
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };
            float4 _MainTex_ST;
            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }
            float antialias(float w, float d, float r) {
                    return 1-(d-r-w/2)/(2*w);
            }
            fixed4 frag(v2f i) : SV_Target
            {
                fixed4 c = tex2D(_MainTex,i.uv);
                float x = i.uv.x;
                float y = i.uv.y;
                float dis = sqrt(pow((_CenterX  - x), 2) + pow((_CenterY  - y), 2));
                if (dis > _Radius ) {
                    discard;
                } else {
                    float innerRadius = (_ComponentWidth * _Radius  - _BoundWidth) / _ComponentWidth;
                    if (dis > innerRadius) {
                        c = _BoundColor;
                        c.a = c.a*antialias(_BoundWidth, dis, innerRadius);
                    }
                    else {
                        c = _BgColor;
                    }
                }
                return c;
            }
            ENDCG
            }
}
}

注意,上面这段代码没有考虑UGUI的Mask,参考了unity官方图片自带的Shader后,代码如下

// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)

Shader "UI/PoisonCircle"
{
    Properties
    {
        _BoundColor("Bound Color", Color) = (1,1,1,1)
        _BoundWidth("BoundWidth", float) = 0.001
        _Radius("Radius", Range(0.0, 1)) = 0.5
        _CenterX("CenterX", float) = 0.5
        _CenterY("CenterY", float) = 0.5
        
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _StencilComp ("Stencil Comparison", Float) = 8
        _Stencil ("Stencil ID", Float) = 0
        _StencilOp ("Stencil Operation", Float) = 0
        _StencilWriteMask ("Stencil Write Mask", Float) = 255
        _StencilReadMask ("Stencil Read Mask", Float) = 255

        _ColorMask ("Color Mask", Float) = 15

        [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
    }

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Stencil
        {
            Ref [_Stencil]
            Comp [_StencilComp]
            Pass [_StencilOp]
            ReadMask [_StencilReadMask]
            WriteMask [_StencilWriteMask]
        }

        Cull Off
        Lighting Off
        ZWrite Off
        ZTest [unity_GUIZTestMode]
        Blend One OneMinusSrcAlpha
        ColorMask [_ColorMask]

        Pass
        {
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0

            #include "UnityCG.cginc"
            #include "UnityUI.cginc"

            #pragma multi_compile_local _ UNITY_UI_CLIP_RECT
            #pragma multi_compile_local _ UNITY_UI_ALPHACLIP
       
            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                float2 texcoord  : TEXCOORD0;
                float4 worldPosition : TEXCOORD1;
                float4  mask : TEXCOORD2;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            float _BoundWidth;
            fixed4 _BoundColor;
            float _Radius;
            float _CenterX;
            float _CenterY;
        
            sampler2D _MainTex;
            fixed4 _TextureSampleAdd;
            float4 _ClipRect;
            float4 _MainTex_ST;
            float _UIMaskSoftnessX;
            float _UIMaskSoftnessY;

            v2f vert(appdata_t v)
            {
                v2f OUT;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
                float4 vPosition = UnityObjectToClipPos(v.vertex);
                OUT.worldPosition = v.vertex;
                OUT.vertex = vPosition;

                float2 pixelSize = vPosition.w;
                pixelSize /= float2(1, 1) * abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy));

                float4 clampedRect = clamp(_ClipRect, -2e10, 2e10);
                float2 maskUV = (v.vertex.xy - clampedRect.xy) / (clampedRect.zw - clampedRect.xy);
                OUT.texcoord = TRANSFORM_TEX(v.texcoord.xy, _MainTex);
                OUT.mask = float4(v.vertex.xy * 2 - clampedRect.xy - clampedRect.zw, 0.25 / (0.25 * half2(_UIMaskSoftnessX, _UIMaskSoftnessY) + abs(pixelSize.xy)));

                OUT.color = v.color;
                return OUT;
            }
        
            fixed4 frag(v2f IN) : SV_Target
            {
                half4 color = tex2D(_MainTex,IN.texcoord);
                float dis = pow(_CenterX - IN.texcoord.x, 2) + pow(_CenterY - IN.texcoord.y, 2);
                if (dis > pow(_Radius, 2)) {
                    discard;
                }
                else
                {
                    float innerRadius = _Radius  - _BoundWidth;
                    if (dis > pow(innerRadius, 2)) {
                        color = _BoundColor;
                    }
                    else {
                        color.a = 0;
                    }
                }
                
                #ifdef UNITY_UI_CLIP_RECT
                half2 m = saturate((_ClipRect.zw - _ClipRect.xy - abs(IN.mask.xy)) * IN.mask.zw);
                color.a *= m.x * m.y;
                #endif

                #ifdef UNITY_UI_ALPHACLIP
                clip (color.a - 0.001);
                #endif
                
                color.rgb *= color.a;
                return color;
            }
        ENDCG
        }
    }
}

总结

两种方法各有优劣,LineRender更加简洁易懂,只需注意361长度的数组千万不要每次刷新时都创建。

队友位置更新

队友的状态有个很麻烦的点,就是在小地图外时,需要在小地图边缘显示。我在UI工程中的设计是把自己的图标放在小地图中心,地图和我前进的方向反向移动,队友单独放在一个节点作为地图的子节点跟随地图一起动。然后通过一些转换判断队友和我的位置有没有超过小地图的显示范围。下面这个函数index是队友编号,source是我的位置,target是队友位置。如果在小地图外,IsOutOfBorder = true,返回的位置是在小地图的边缘。

private Vector2 OutOfBorder(int index, float3 source, float3 target)
{
    float diffX = target.x - source.x;
    float diffY = target.y - source.y;

    float resultX = target.x;
    float resultY = target.y;
    var iconRad = 16; 
    if (Mathf.Abs(diffX) > miniMapMaskSize.x / 2 - iconRad || Mathf.Abs(diffY) > miniMapMaskSize.y / 2 - iconRad)
    {
        othersImgGroup[index].IsOutOfBorder = true;
        if (Mathf.Abs(diffX) > Mathf.Abs(diffY))
        {
            resultX = source.x + miniMapMaskSize.x / 2 * (diffX > 0 ? 1 : -1);
            resultY = source.y + miniMapMaskSize.x / 2 * diffY / Mathf.Abs(diffX);
            var lowerBound = source.y - (miniMapMaskSize.x / 2 - iconRad);
            var upperBound = source.y + (miniMapMaskSize.x / 2 - iconRad);
            resultY = resultY < lowerBound ? lowerBound : (resultY > upperBound ? upperBound : inputValue);
            othersImgGroup[index].borderOrientation = diffX > 0 ? OrientationType.East : OrientationType.West;
        }
        else
        {
            resultX = source.x + miniMapMaskSize.y / 2 * diffX / Mathf.Abs(diffY);
            resultY = source.y + miniMapMaskSize.y / 2 * (diffY > 0 ? 1 : -1);
            var lowerBound = source.y - (miniMapMaskSize.x / 2 - iconRad);
            var upperBound = source.y + (miniMapMaskSize.x / 2 - iconRad);
            resultX = resultX < lowerBound ? lowerBound : (resultX > upperBound ? upperBound : inputValue);
            othersImgGroup[index].borderOrientation = diffY > 0 ? OrientationType.North : OrientationType.South;
        }
    }
    else
    {
        othersImgGroup[index].IsOutOfBorder = false;
    }

    return new Vector2(resultX, resultY);
}

作者 [JohnWayne]

2023 年 06月 02日