Unity使用shader實現高性能小地圖

這是結果圖和對應的網格圖,網格圖把上面多於的字體和路徑刪了,只留下背景地圖和小圖標,只使用了一張image就可以實現地圖和多個小圖標

在寫之前查了好多小地圖的寫法,這裏有三種

  1. 在地圖下面鋪一張圖片,每個需要顯示的角色身下再來一張icon的image,正交相機從上面照着角色。好處是不用控制相機的位置和icon的位置,npc monster腳下自帶,主角移動到周圍就自動顯示,但是需要新加相機,每個需要顯示icon的物體都要新加一張圖片。
  2. ugui原生圖片,通過計算移動地圖的位置和生成icon,上面加一個mask,類似下面這樣,底圖會很大,勢必會增加OverDraw。
  3. 上面方法改進版,通過更改uv來顯示圖片的不同部分。不過icon的多個image也會增加多一層DC。
  4. 最後就是我現在使用的一種方法,手動畫mesh,指定不同uv,一個DC

 

下面上實現方法:

自定義Image

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class MinimapImage : Image
{
    public List<Icon> Icons = new List<Icon>();

    private UIVertex GetVertex(float x, float y, Vector2? iconTag = null, Color? color = null)
    {
        UIVertex uiVertex = new UIVertex();
        uiVertex.color = color ?? this.color;
        uiVertex.position.x = rectTransform.rect.width * (x - 0.5f);
        uiVertex.position.y = rectTransform.rect.height * (y - 0.5f);
        uiVertex.uv0 = new Vector2(x, y);
        uiVertex.uv1 = iconTag != null ? iconTag.Value / 2 + 0.5f * Vector2.one : Vector2.zero;
        return uiVertex;
    }

    protected override void OnPopulateMesh(VertexHelper toFill)
    {
        toFill.Clear();
        toFill.AddUIVertexQuad(new[] {GetVertex(0, 0), GetVertex(0, 1), GetVertex(1, 1), GetVertex(1, 0)});
        lock (Icons)
        {
            for (int i = 0; i < Icons.Count; i++)
            {
                AddIconVertexArr(toFill, Icons[i]);
            }
        }
    }

    private void AddIconVertexArr(VertexHelper vh, Icon icon)
    {
        UIVertex[] vertices = new UIVertex[4];
        Vector2[] poss =
        {
            icon.LBPos,
            icon.LTPos,
            icon.RTPos,
            icon.RBPos,
        };
        vertices[0] = GetVertex(poss[0].x,poss[0].y, icon.UVLeftBottom);
        vertices[1] = GetVertex(poss[1].x,poss[1].y,
            new Vector2(icon.UVLeftBottom.x, icon.UVLeftBottom.y + icon.UVWidthLength.y));
        vertices[2] = GetVertex(poss[2].x,poss[2].y, new Vector2(icon.UVLeftBottom.x + icon.UVWidthLength.x, icon.UVLeftBottom.y + icon.UVWidthLength.y));
        vertices[3] = GetVertex(poss[3].x,poss[3].y,
            new Vector2(icon.UVLeftBottom.x + icon.UVWidthLength.x, icon.UVLeftBottom.y));
        vh.AddUIVertexQuad(vertices);
    }

    public class Icon
    {
        public Vector2 PosCenter;
        public Vector2 LBPos;
        public Vector2 LTPos;
        public Vector2 RTPos;
        public Vector2 RBPos;
        public float Angle;
        public Vector2 UVLeftBottom;
        public Vector2 UVWidthLength;

        public Icon(Vector2 posCenter, Vector2 lbPos, Vector2 ltPos, Vector2 rtPos, Vector2 rbPos, float angle, Vector2 uvLeftBottom, Vector2 uvWidthLength)
        {
            PosCenter = posCenter;
            LBPos = lbPos;
            LTPos = ltPos;
            RTPos = rtPos;
            RBPos = rbPos;
            Angle = angle;
            UVLeftBottom = uvLeftBottom;
            UVWidthLength = uvWidthLength;
        }
    }
}


shader:從unity自帶的sprite shader稍加修改過來的

Shader "Custom/minimap"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _IconTex("Icon Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)

        _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 SrcAlpha 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
                fixed2 texcoord1 : TEXCOORD1;
            };

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

            fixed4 _Color;
            sampler2D _MainTex;
            fixed4 _MainTex_ST;
            sampler2D _IconTex;
            fixed4 _IconTex_ST;
            fixed4 _TextureSampleAdd;
            float4 _ClipRect;

            v2f vert(appdata_t v)
            {
                v2f OUT;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
                OUT.worldPosition = v.vertex;
                OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);
                OUT.texcoord1 = v.texcoord1;
                OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);

                OUT.color = v.color * _Color;

                return OUT;
            }
            
            fixed4 frag(v2f IN) : SV_Target
            {
 
                fixed4 c;
                if(IN.texcoord1.x >= 0.5)
                {
                    c = tex2D(_IconTex, (IN.texcoord1 - 0.5)*2);
                }else{
                    c = tex2D(_MainTex, IN.texcoord);
                }
                c * IN.color;
                
                #ifdef UNITY_UI_CLIP_RECT
                c.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
                #endif

                #ifdef UNITY_UI_ALPHACLIP
                clip (c.a - 0.001);
                #endif
                return c;
            }
        ENDCG
        }
    }
}

cs代碼

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Framework;
using UnityEngine;

public class MiniMap : MonoBehaviour
{
    public enum MapType
    {
        Radar,
        Map,
    }

    public MapType Type;
    private MinimapImage MinimapImage;
    private Vector2 imgMapLB
    {
        get
        {
            Vector2 lb = new Vector2(playerPos.x - uiMapToRealWidth / 2,
                playerPos.z - uiMapToRealHeight / 2);
            if (playerPos.x + uiMapToRealWidth / 2 > mapWidth)
            {
                lb.x = mapWidth - uiMapToRealWidth;
            }
            else if (playerPos.x - uiMapToRealWidth / 2 < 0)
            {
                lb.x = uiMapToRealWidth / 2;
            }
            
            if (playerPos.z + uiMapToRealHeight / 2 > mapHeight)
            {
                lb.y = mapHeight - uiMapToRealHeight;
            }
            else if (playerPos.z - uiMapToRealHeight / 2 < 0)
            {
                lb.y = uiMapToRealHeight / 2;
            }
            return lb;
        }
    }

    private Vector3 originMapCenter;
    private Vector3 playerPos;
    //UI map的寬高
    private float uiMapHeight => MinimapImage.rectTransform.rect.height;
    private float uiMapWidth => MinimapImage.rectTransform.rect.width;
    //對應真是地圖的寬 高
    private float uiMapToRealHeight => uiMapToRealWidth * uiMapHeight / uiMapWidth;
    private float uiMapToRealWidth;

    private float mapWidth;
    private float mapHeight;

    private Vector2 uvScale;

    private UiSpriteMultiAsset spriteAsset;
    private bool isSpriteLoaded;
    
    private List<MinimapIcon> Icons = new List<MinimapIcon>(); 
    
    private void Awake()
    {
        MinimapImage = GetComponent<MinimapImage>();
    }

    public void SetMapWidthHeight(float width, float height)
    {
        mapWidth = width;
        mapHeight = height;
    }

    public void SetUI2RealScale(float widthScale)
    {
        uiMapToRealWidth = widthScale;
        uvScale = new Vector2(uiMapToRealWidth / mapWidth, uiMapToRealHeight / mapHeight);
    }

    public void SetOriginMapCenter(Vector3 mapCenter)
    {
        originMapCenter = mapCenter;
    }

    public void SetPlayerPos(Vector3 pos)
    {
        playerPos = pos;
    }
    
    public void UpdateIcons(List<MinimapIcon> icons)
    {
        Icons.Clear();
        Icons.AddRange(icons);
        MinimapImage.Icons.Clear();
        foreach (var icon in icons)
        {
            AddIcon(icon.Pos, icon.Radius, icon.Dir, icon.SpritePath, false);
        }
        MinimapImage.SetVerticesDirty();
    }

    public void UpdateMapUV()
    {
        Vector2 lb = imgMapLB;
        Vector2 playerOffset = new Vector2(lb.x + uiMapToRealWidth / 2, lb.y + uiMapToRealHeight / 2);
        float widthRate = playerOffset.x / mapWidth;
        float heightRate = playerOffset.y / mapHeight;
        SetMapUV(new Vector4(uvScale.x, uvScale.y, widthRate - uvScale.x / 2, heightRate - uvScale.y / 2));
    }

    private Vector2 GetIconPos(Vector3 pos)
    {
        float widthRate = 1;
        float heightRate = 1;
        switch (Type)
        {
            case MapType.Radar:
                var offsetPos = new Vector2(pos.x, pos.z) - imgMapLB;
                widthRate = offsetPos.x / uiMapToRealWidth;
                heightRate = 1 - offsetPos.y / uiMapToRealHeight;
                break;
            case MapType.Map:
                var offset = pos - originMapCenter;
                widthRate = offset.x / mapWidth;
                heightRate = offset.z / mapHeight;
                break;
        }
        return new Vector2(widthRate, heightRate);
    }

    private void AddIcon(Vector3 pos, float radius, Vector3 dir, string spritePath, bool isUpdateVertices = true)
    {
        if (spriteAsset == null)
        {
            InitSpriteAsset(spritePath);
        }
        Icons.Add(new MinimapIcon(pos, radius, dir, spritePath));
        if(!isSpriteLoaded) return;
        var (uvLb, wl) = CalcUV(spriteAsset.GetSprite(GetSpriteName(spritePath)));
        var iconPos = GetIconPos(pos);
        if(Type == MapType.Radar && (iconPos.x > 1 || iconPos.y > 1)) return;
        float angle = Vector3.Angle(Vector3.forward, dir);
        Vector3 normal = Vector3.Cross (dir, Vector3.forward);
        angle *= Mathf.Sign (Vector3.Dot(normal,Vector3.up));
        MinimapImage.Icons.Add(AdjustIconPos(iconPos, radius, angle, uvLb, wl));
        if (isUpdateVertices)
            MinimapImage.SetVerticesDirty();
    }

    /// <summary>
    /// 調整icon的旋轉,還有因爲地圖寬高不一致造成的icon拉伸
    /// </summary>
    private MinimapImage.Icon AdjustIconPos(Vector2 center, float radius, float angle, Vector2 uvLb, Vector2 wl)
    {
        Matrix4x4 rotateMat = Matrix4x4.Translate(center) * Matrix4x4.Rotate(Quaternion.Euler(0, 0, angle)) *
                              Matrix4x4.Translate(-center);
        Vector2 lb = rotateMat * new Vector4(center.x - radius / 2, center.y - radius / 2,1,1);
        Vector2 lt = rotateMat * new Vector4(center.x - radius / 2, center.y + radius / 2,1,1);
        Vector2 rt = rotateMat * new Vector4(center.x + radius / 2, center.y + radius / 2,1,1);
        Vector2 rb = rotateMat * new Vector4(center.x + radius / 2, center.y - radius / 2,1,1);
        float rate = uiMapWidth / uiMapHeight;
        lb.y = center.y + (lb.y - center.y) * rate;
        lt.y = center.y + (lt.y - center.y) * rate;
        rt.y = center.y + (rt.y - center.y) * rate;
        rb.y = center.y + (rb.y - center.y) * rate;
        var icon = new MinimapImage.Icon(center, lb, lt, rt, rb, angle, uvLb, wl);
        return icon;
    }
    

    private void InitSpriteAsset(string path)
    {
        path = GetAtlasPath(path);
        spriteAsset = new UiSpriteMultiAsset();
        spriteAsset.Load(path, () =>
        {
            List<MinimapIcon> tempIcons = new List<MinimapIcon>(Icons);
            Icons.Clear();
            MinimapImage.material.SetTexture("_IconTex", spriteAsset.assets.First().Value.texture);
            isSpriteLoaded = true;
            UpdateIcons(tempIcons);
        });
    }

    private string GetAtlasPath(string path)
    {
        int index = path.LastIndexOf("/");
        path = path.Substring(0, index);
        return $"{path}.png";
    }

    private string GetSpriteName(string path)
    {
        return Path.GetFileNameWithoutExtension(path);
    }

    private (Vector2 uvLb, Vector2 wl) CalcUV(Sprite sprite)
    {
        Rect UVs = sprite.rect;
        UVs.x /= sprite.texture.width;
        UVs.width /= sprite.texture.width;
        UVs.y /= sprite.texture.height;
        UVs.height /= sprite.texture.height;
        return (UVs.position, UVs.size);
    }

    private void SetMapUV(Vector4 st)
    {
        MinimapImage.material.SetVector("_MainTex_ST", st);
    }

    private void OnDestroy()
    {
        spriteAsset?.Release();
    }

    public struct MinimapIcon
    {
        public Vector3 Pos;
        public float Radius;
        public Vector3 Dir;
        public string SpritePath;

        public MinimapIcon(Vector3 pos, float radius, Vector3 dir, string spritePath)
        {
            Pos = pos;
            Radius = radius;
            Dir = dir;
            SpritePath = spritePath;
        }
    }

}

整體思想就是背景地圖圖片更改uv來顯示,其他icon畫到背景的image上,通過指定不同的uv來顯示icon的圖標,在遊戲過程中不斷更改mesh和uv來達到小地圖的效果。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章