当前位置: 首页 > news >正文

UGUI源代码之Text—实现自定义的字间距属性

以下内容是根据Unity 2020.1.01f版本进行编写的

UGUI源代码之Text—实现自定义的字间距属性

  • 1、目的
  • 2、参考
  • 3、代码阅读
  • 4、准备修改UGUI源代码
  • 5、实现自定义Text组件,增加字间距属性
  • 6、最终效果


1、目的

很多时候,美术在设计的时候是想要使用文本的字间距属性的,但是UGUI的Text组件并不支持字间距属性,因此想要自己实现一个
但是实现Text组件的最核心代码,Unity并没有公开出来,但是通过查看NGUI的Label控件的源代码,可以看出UGUI实现Text的核心方法

2、参考

本文参考Unity官方的UGUI源代码,以及NGUI插件
Github地址:https://github.com/Unity-Technologies/uGUI

3、代码阅读

首先看下UGUI的Text类,继承Graphic类的类一般都是通过通用OnPopulateMesh方法生成对应的Mesh的,所以直接看Text的OnPopulateMesh方法:

protected override void OnPopulateMesh(VertexHelper toFill)
{
    if (font == null)
        return;

    // We don't care if we the font Texture changes while we are doing our Update.
    // The end result of cachedTextGenerator will be valid for this instance.
    // Otherwise we can get issues like Case 619238.
    m_DisableFontTextureRebuiltCallback = true;

    Vector2 extents = rectTransform.rect.size;

    var settings = GetGenerationSettings(extents);
    cachedTextGenerator.PopulateWithErrors(text, settings, gameObject);

    // Apply the offset to the vertices
    IList<UIVertex> verts = cachedTextGenerator.verts;
    float unitsPerPixel = 1 / pixelsPerUnit;
    int vertCount = verts.Count;

    // We have no verts to process just return (case 1037923)
    if (vertCount <= 0)
    {
        toFill.Clear();
        return;
    }

    Vector2 roundingOffset = new Vector2(verts[0].position.x, verts[0].position.y) * unitsPerPixel;
    roundingOffset = PixelAdjustPoint(roundingOffset) - roundingOffset;
    toFill.Clear();
    if (roundingOffset != Vector2.zero)
    {
        for (int i = 0; i < vertCount; ++i)
        {
            int tempVertsIndex = i & 3;
            m_TempVerts[tempVertsIndex] = verts[i];
            m_TempVerts[tempVertsIndex].position *= unitsPerPixel;
            m_TempVerts[tempVertsIndex].position.x += roundingOffset.x;
            m_TempVerts[tempVertsIndex].position.y += roundingOffset.y;
            if (tempVertsIndex == 3)
                toFill.AddUIVertexQuad(m_TempVerts);
        }
    }
    else
    {
        for (int i = 0; i < vertCount; ++i)
        {
            int tempVertsIndex = i & 3;
            m_TempVerts[tempVertsIndex] = verts[i];
            m_TempVerts[tempVertsIndex].position *= unitsPerPixel;
            if (tempVertsIndex == 3)
                toFill.AddUIVertexQuad(m_TempVerts);
        }
    }

    m_DisableFontTextureRebuiltCallback = false;
}

从上面的代码可以看出,生成文本Mesh的Vertex是通过cachedTextGenerator.PopulateWithErrors的函数生成的,此函数的3个参数分别是文本的内容(string类型),Text组件的设置,Text所在的gameObject。那么这个cachedTextGenerator是啥呢,F12导航到定义此属性:

public TextGenerator cachedTextGenerator
{
    get { return m_TextCache ?? (m_TextCache = (m_Text.Length != 0 ? new TextGenerator(m_Text.Length) : new TextGenerator())); }
}

cachedTextGenerator这个属性是一个TextGenerator类,再次按F12导航到TextGenerator类(部分):

//
// 摘要:
//     Will generate the vertices and other data for the given string with the given
//     settings.
//
// 参数:
//   str:
//     String to generate.
//
//   settings:
//     Generation settings.
//
//   context:
//     The object used as context of the error log message, if necessary.
//
// 返回结果:
//     True if the generation is a success, false otherwise.
public bool PopulateWithErrors(string str, TextGenerationSettings settings, GameObject context)
{
    TextGenerationError textGenerationError = PopulateWithError(str, settings);
    if (textGenerationError == TextGenerationError.None)
    {
        return true;
    }

    if ((textGenerationError & TextGenerationError.CustomSizeOnNonDynamicFont) != 0)
    {
        Debug.LogErrorFormat(context, "Font '{0}' is not dynamic, which is required to override its size", settings.font);
    }

    if ((textGenerationError & TextGenerationError.CustomStyleOnNonDynamicFont) != 0)
    {
        Debug.LogErrorFormat(context, "Font '{0}' is not dynamic, which is required to override its style", settings.font);
    }

    return false;
}

跳转后发现这个类是dll里的类,无法直接查看函数的内容,其中以上代码是Text组件所使用到的PopulateWithErrors函数的定义
关于UGUI的Text组件实现过程在这里就断了,但是NGUI是UGUI的前身,Unity开发团队将NGUI的开发团队收到自己开发团队下,并且由此开发了UGUI。
因此可以想着可以通过查看NGUI对应UGUI的Text类的代码,了解UGUI实现Text的过程,从而让Text实现更多的功能
说干就干,新建项目导入NGUI插件,粗略看了NGUI的控件,估计Label类就是对应UGUI的Text类,生成Text的代码如下:

/// <summary>
/// Process the raw text, called when something changes.
/// </summary>

public void ProcessText (bool legacyMode = false, bool full = true)
{
	if (!isValid) return;

	mChanged = true;
	shouldBeProcessed = false;

	float regionX = mDrawRegion.z - mDrawRegion.x;
	float regionY = mDrawRegion.w - mDrawRegion.y;

	NGUIText.rectWidth    = legacyMode ? (mMaxLineWidth  != 0 ? mMaxLineWidth  : 1000000) : width;
	NGUIText.rectHeight   = legacyMode ? (mMaxLineHeight != 0 ? mMaxLineHeight : 1000000) : height;
	NGUIText.regionWidth  = (regionX != 1f) ? Mathf.RoundToInt(NGUIText.rectWidth  * regionX) : NGUIText.rectWidth;
	NGUIText.regionHeight = (regionY != 1f) ? Mathf.RoundToInt(NGUIText.rectHeight * regionY) : NGUIText.rectHeight;

	mFinalFontSize = Mathf.Abs(legacyMode ? Mathf.RoundToInt(cachedTransform.localScale.x) : defaultFontSize);
	mScale = 1f;

	if (NGUIText.regionWidth < 1 || NGUIText.regionHeight < 0)
	{
		mProcessedText = "";
		return;
	}

	bool isDynamic = (trueTypeFont != null);

	if (isDynamic && keepCrisp)
	{
		UIRoot rt = root;
		if (rt != null) mDensity = (rt != null) ? rt.pixelSizeAdjustment : 1f;
	}
	else mDensity = 1f;

	if (full) UpdateNGUIText();

	if (mOverflow == Overflow.ResizeFreely)
	{
		NGUIText.rectWidth = 1000000;
		NGUIText.regionWidth = 1000000;

		if (mOverflowWidth > 0)
		{
			NGUIText.rectWidth = Mathf.Min(NGUIText.rectWidth, mOverflowWidth);
			NGUIText.regionWidth = Mathf.Min(NGUIText.regionWidth, mOverflowWidth);
		}
	}

	if (mOverflow == Overflow.ResizeFreely || mOverflow == Overflow.ResizeHeight)
	{
		NGUIText.rectHeight = 1000000;
		NGUIText.regionHeight = 1000000;
	}

	if (mFinalFontSize > 0)
	{
		bool adjustSize = keepCrisp;

		for (int ps = mFinalFontSize; ps > 0; --ps)
		{
			// Adjust either the size, or the scale
			if (adjustSize)
			{
				mFinalFontSize = ps;
				NGUIText.fontSize = mFinalFontSize;
			}
			else
			{
				mScale = (float)ps / mFinalFontSize;
				NGUIText.fontScale = isDynamic ? mScale : ((float)mFontSize / mFont.defaultSize) * mScale;
			}

			NGUIText.Update(false);

			// Wrap the text
			bool fits = NGUIText.WrapText(printedText, out mProcessedText, false, false, mOverflowEllipsis);

			if (mOverflow == Overflow.ShrinkContent && !fits)
			{
				if (--ps > 1) continue;
				else break;
			}
			else if (mOverflow == Overflow.ResizeFreely)
			{
				mCalculatedSize = NGUIText.CalculatePrintedSize(mProcessedText);

				int w = Mathf.Max(minWidth, Mathf.RoundToInt(mCalculatedSize.x));
				if (regionX != 1f) w = Mathf.RoundToInt(w / regionX);
				int h = Mathf.Max(minHeight, Mathf.RoundToInt(mCalculatedSize.y));
				if (regionY != 1f) h = Mathf.RoundToInt(h / regionY);

				if ((w & 1) == 1) ++w;
				if ((h & 1) == 1) ++h;

				if (mWidth != w || mHeight != h)
				{
					mWidth = w;
					mHeight = h;
					if (onChange != null) onChange();
				}
			}
			else if (mOverflow == Overflow.ResizeHeight)
			{
				mCalculatedSize = NGUIText.CalculatePrintedSize(mProcessedText);
				int h = Mathf.Max(minHeight, Mathf.RoundToInt(mCalculatedSize.y));
				if (regionY != 1f) h = Mathf.RoundToInt(h / regionY);
				if ((h & 1) == 1) ++h;

				if (mHeight != h)
				{
					mHeight = h;
					if (onChange != null) onChange();
				}
			}
			else
			{
				mCalculatedSize = NGUIText.CalculatePrintedSize(mProcessedText);
			}

			// Upgrade to the new system
			if (legacyMode)
			{
				width = Mathf.RoundToInt(mCalculatedSize.x);
				height = Mathf.RoundToInt(mCalculatedSize.y);
				cachedTransform.localScale = Vector3.one;
			}
			break;
		}
	}
	else
	{
		cachedTransform.localScale = Vector3.one;
		mProcessedText = "";
		mScale = 1f;
	}
		
	if (full)
	{
		NGUIText.bitmapFont = null;
		NGUIText.dynamicFont = null;
	}
}

从代码可以看出,函数一开始基本都是对一些设置的参数进行读取,计算等,生成文本的函数可能是UpdateNGUIText,NGUIText.Update
先看UpdateNGUIText的代码:

public void UpdateNGUIText ()
{
	Font ttf = trueTypeFont;
	bool isDynamic = (ttf != null);

	NGUIText.fontSize = mFinalFontSize;
	NGUIText.fontStyle = mFontStyle;
	NGUIText.rectWidth = mWidth;
	NGUIText.rectHeight = mHeight;
	NGUIText.regionWidth = Mathf.RoundToInt(mWidth * (mDrawRegion.z - mDrawRegion.x));
	NGUIText.regionHeight = Mathf.RoundToInt(mHeight * (mDrawRegion.w - mDrawRegion.y));
	NGUIText.gradient = mApplyGradient && (mFont == null || !mFont.packedFontShader);
	NGUIText.gradientTop = mGradientTop;
	NGUIText.gradientBottom = mGradientBottom;
	NGUIText.encoding = mEncoding;
	NGUIText.premultiply = mPremultiply;
	NGUIText.symbolStyle = mSymbols;
	NGUIText.maxLines = mMaxLineCount;
	NGUIText.spacingX = effectiveSpacingX;
	NGUIText.spacingY = effectiveSpacingY;
	NGUIText.fontScale = isDynamic ? mScale : ((float)mFontSize / mFont.defaultSize) * mScale;

	if (mFont != null)
	{
		NGUIText.bitmapFont = mFont;
			
		for (; ; )
		{
			UIFont fnt = NGUIText.bitmapFont.replacement;
			if (fnt == null) break;
			NGUIText.bitmapFont = fnt;
		}

		if (NGUIText.bitmapFont.isDynamic)
		{
			NGUIText.dynamicFont = NGUIText.bitmapFont.dynamicFont;
			NGUIText.bitmapFont = null;
		}
		else NGUIText.dynamicFont = null;
	}
	else
	{
		NGUIText.dynamicFont = ttf;
		NGUIText.bitmapFont = null;
	}

	if (isDynamic && keepCrisp)
	{
		UIRoot rt = root;
		if (rt != null) NGUIText.pixelDensity = (rt != null) ? rt.pixelSizeAdjustment : 1f;
	}
	else NGUIText.pixelDensity = 1f;

	if (mDensity != NGUIText.pixelDensity)
	{
		ProcessText(false, false);
		NGUIText.rectWidth = mWidth;
		NGUIText.rectHeight = mHeight;
		NGUIText.regionWidth = Mathf.RoundToInt(mWidth * (mDrawRegion.z - mDrawRegion.x));
		NGUIText.regionHeight = Mathf.RoundToInt(mHeight * (mDrawRegion.w - mDrawRegion.y));
	}

	if (alignment == Alignment.Automatic)
	{
		Pivot p = pivot;

		if (p == Pivot.Left || p == Pivot.TopLeft || p == Pivot.BottomLeft)
		{
			NGUIText.alignment = Alignment.Left;
		}
		else if (p == Pivot.Right || p == Pivot.TopRight || p == Pivot.BottomRight)
		{
			NGUIText.alignment = Alignment.Right;
		}
		else NGUIText.alignment = Alignment.Center;
	}
	else NGUIText.alignment = alignment;

	NGUIText.Update();
}

从代码可以看出,此函数主要是读取当前文本控件的设置,并将这些设置赋值到NGUIText静态类中对应的属性上,最后也是会调用NGUIText.Update函数

因此,查看NGUIText.Update函数的代码:

static public void Update () { Update(true); }

/// <summary>
/// Recalculate the 'final' values.
/// </summary>

static public void Update (bool request)
{
	finalSize = Mathf.RoundToInt(fontSize / pixelDensity);
	finalSpacingX = spacingX * fontScale;
	finalLineHeight = (fontSize + spacingY) * fontScale;
	useSymbols = (dynamicFont != null || bitmapFont != null) && encoding && symbolStyle != SymbolStyle.None;

#if DYNAMIC_FONT
	Font font = dynamicFont;

	if (font != null && request)
	{
		font.RequestCharactersInTexture(")_-", finalSize, fontStyle);

#if UNITY_4_3 || UNITY_4_5 || UNITY_4_6 || UNITY_4_7
		if (!font.GetCharacterInfo(')', out mTempChar, finalSize, fontStyle) || mTempChar.vert.height == 0f)
		{
			font.RequestCharactersInTexture("A", finalSize, fontStyle);
			{
				if (!font.GetCharacterInfo('A', out mTempChar, finalSize, fontStyle))
				{
					baseline = 0f;
					return;
				}
			}
		}

		float y0 = mTempChar.vert.yMax;
		float y1 = mTempChar.vert.yMin;
#else
		if (!font.GetCharacterInfo(')', out mTempChar, finalSize, fontStyle) || mTempChar.maxY == 0f)
		{
			font.RequestCharactersInTexture("A", finalSize, fontStyle);
			{
				if (!font.GetCharacterInfo('A', out mTempChar, finalSize, fontStyle))
				{
					baseline = 0f;
					return;
				}
			}
		}

		float y0 = mTempChar.maxY;
		float y1 = mTempChar.minY;
#endif
		baseline = Mathf.Round(y0 + (finalSize - y0 + y1) * 0.5f);
	}
#endif
}

从代码可以看出,有两个函数可能是获取文字信息的:font.RequestCharactersInTexture,font.GetCharacterInfo

但是,鼠标悬停RequestCharactersInTexture函数发现其没有返回值(如上图),根据函数名猜测此函数是请求函数的第一个参数中的字符串,根据字符串在当前Label控件材质球的Texture上生成对应的字符

然后再看GetCharacterInfo,看到其中有out的参数,并且有返回值,根据函数名猜测是根据参数中的字符,字号以及字体样式返回该字符的参数,并将这些参数保存到一个叫做CharacterInfo的类中
跳转到GetCharacterInfo类查看代码:

[MethodImpl(MethodImplOptions.InternalCall)]
[FreeFunction("TextRenderingPrivate::GetCharacterInfo", HasExplicitThis = true)]
public extern bool GetCharacterInfo(char ch, out CharacterInfo info, [DefaultValue("0")] int size, [DefaultValue("FontStyle.Normal")] FontStyle style);

发现这是一个dll文件里的类,无法查看里面的代码
那就看看CharacterInfo类有什么属性是可以使用的
CharacterInfo类代码(为了方便看改成截图形式):
在这里插入图片描述
可以看出,CharacterInfo类也是dll文件里的类,但是里面有不少属性是public的,可以使用
逐个属性查看之后,发现有一个叫advance的属性:

//
// 摘要:
//     The horizontal distance, rounded to the nearest integer, from the origin of this
//     character to the origin of the next character.
public int advance
{
    get
    {
        return (int)Math.Round(width, MidpointRounding.AwayFromZero);
    }
    set
    {
        width = value;
    }
}

此属性是当前字符的水平距离,四舍五入到最接近的整数,从该点的原点算起。此字符到下一个字符的原点。大致意思就是字符的宽度。
根据这个属性,就可以实现Text的字间距属性了。
Text显示出文字的根本是通过Vertex顶点生成Mesh,生成Mesh后再通过Canvas Renderer渲染出来。所以我们只需要计算位置,纹理和色值都正确的Vertex顶点,并通过顶点生成Mesh,就可以实现文本了

4、准备修改UGUI源代码

请看这篇:UGUI源代码之修改源代码的前期准备
已经准备过的同学可以跳过

5、实现自定义Text组件,增加字间距属性

新建一个C#脚本,重命名为TextSpacing,并使其继承Text类,为其增加一个调整字间距的属性,然后重写OnPopulateMesh方法就可以了,上代码:

[SerializeField] private float m_characterSpacing = 0f;

protected override void OnPopulateMesh(VertexHelper toFill)
{
    if (font == null)
        return;

    if (text.Length <= 0)
    {
        toFill.Clear();
        return;
    }

    // We don't care if we the font Texture changes while we are doing our Update.
    // The end result of cachedTextGenerator will be valid for this instance.
    // Otherwise we can get issues like Case 619238.
    m_DisableFontTextureRebuiltCallback = true;

    Vector2 extents = rectTransform.rect.size;
    var settings = GetGenerationSettings(extents);
    cachedTextGenerator.PopulateWithErrors(text, settings, gameObject);

    //-------------------------------------------------------------------------------- 

    toFill.Clear();

    float currentLineTotalWidth = 0f;
    float currentTotalHeight = 0f;
    int lineCount = 1;
    Vector3 startPos = Vector3.zero;
    List<float> totalWidthtList = new List<float>();
        
    //定义字间距向量
    Vector3 characterSpacingVector = new Vector3(m_characterSpacing, 0, 0);
    font.RequestCharactersInTexture(text, fontSize, fontStyle);
        
    CharacterInfo ch_firstChar;
    font.GetCharacterInfo(text[0], out ch_firstChar,fontSize, fontStyle);
    currentLineTotalWidth = ch_firstChar.advance;

    currentTotalHeight = fontSize;
    if(rectTransform.sizeDelta.y < currentTotalHeight)
    {
        return;
    }

    List<CharacterInfo> characterInfoList = new List<CharacterInfo>();
    characterInfoList.Add(ch_firstChar);

    for (int i = 0;i < text.Length;i++)
    {
        if (i + 1 < text.Length)
        {
            CharacterInfo next_ch;
            font.GetCharacterInfo(text[i + 1], out next_ch, fontSize, fontStyle);
            characterInfoList.Add(next_ch);

            if (text[i] == '\n')
            {
                lineCount++;
                totalWidthtList.Add(currentLineTotalWidth);
                currentLineTotalWidth = next_ch.advance;
                currentTotalHeight += lineSpacing + fontSize;
                if (verticalOverflow == VerticalWrapMode.Truncate && currentTotalHeight > rectTransform.sizeDelta.y)
                {
                    break;
                }
                continue;
            }
            //自动换行
            if (horizontalOverflow == HorizontalWrapMode.Wrap && (currentLineTotalWidth + next_ch.advance + m_characterSpacing) > rectTransform.sizeDelta.x)
            {
                lineCount++;
                totalWidthtList.Add(currentLineTotalWidth);
                currentLineTotalWidth = next_ch.advance;
                currentTotalHeight += lineSpacing + fontSize;
                if (verticalOverflow == VerticalWrapMode.Truncate && currentTotalHeight > rectTransform.sizeDelta.y)
                {
                    break;
                }
            }
            else
            {
                if (!(text[i + 1] == '\n'))
                {
                    currentLineTotalWidth += next_ch.advance + m_characterSpacing;
                }
            }
        }
        else
        {
            if (text[i] == '\n')
            {
                lineCount++;
                totalWidthtList.Add(currentLineTotalWidth);
                currentLineTotalWidth = 0;
                continue;
            }
        }
    }
    //加上最后一行的字符宽度
    totalWidthtList.Add(currentLineTotalWidth);

    //重置部分属性
    lineCount = 1;
    currentLineTotalWidth = ch_firstChar.advance;
    currentTotalHeight = fontSize;

    startPos = GetStartPosition(lineCount, totalWidthtList);

    for (int i = 0; i < text.Length; i++)
    {
        CharacterInfo ch = characterInfoList[i];

        UIVertex[] vertices = new UIVertex[4];
        vertices[0] = UIVertex.simpleVert;
        vertices[1] = UIVertex.simpleVert;
        vertices[2] = UIVertex.simpleVert;
        vertices[3] = UIVertex.simpleVert;

        vertices[0].position = startPos + new Vector3(ch.minX, ch.maxY, 0);
        vertices[1].position = startPos + new Vector3(ch.maxX, ch.maxY, 0);
        vertices[2].position = startPos + new Vector3(ch.maxX, ch.minY, 0);
        vertices[3].position = startPos + new Vector3(ch.minX, ch.minY, 0);

        //Vector2 adjustVector = Vector2.zero;
        Vector2 adjustVector = new Vector2(0, 0.00f);

        vertices[0].uv0 = ch.uvTopLeft + adjustVector;
        vertices[1].uv0 = ch.uvTopRight + adjustVector;
        vertices[2].uv0 = ch.uvBottomRight + adjustVector;
        vertices[3].uv0 = ch.uvBottomLeft + adjustVector;

        vertices[0].color = color;
        vertices[1].color = color;
        vertices[2].color = color;
        vertices[3].color = color;

        if (text[i] != '\n')
            toFill.AddUIVertexQuad(vertices);

        if (i + 1 < text.Length)
        {
            CharacterInfo next_ch = characterInfoList[i + 1];
            //适应换行符,如果当前字符是换行符,则直接进行换行操作
            if (text[i] == '\n')
            {
                lineCount++;
                currentLineTotalWidth = next_ch.advance;
                startPos = GetStartPosition(lineCount, totalWidthtList);
                currentTotalHeight += lineSpacing + fontSize;
                if (verticalOverflow == VerticalWrapMode.Truncate && currentTotalHeight > rectTransform.sizeDelta.y)
                {
                    break;
                }
                continue;
            }

            //自动换行
            if (horizontalOverflow == HorizontalWrapMode.Wrap && (currentLineTotalWidth + next_ch.advance + m_characterSpacing) > rectTransform.sizeDelta.x)
            {
                lineCount++;
                currentLineTotalWidth = next_ch.advance;
                startPos = GetStartPosition(lineCount, totalWidthtList);
                currentTotalHeight += lineSpacing + fontSize;
                if (verticalOverflow == VerticalWrapMode.Truncate && currentTotalHeight > rectTransform.sizeDelta.y)
                {
                    break;
                }
            }
            else
            {
                startPos += new Vector3(ch.advance, 0, 0) + characterSpacingVector;
                //适应换行符,如果下一个字符是换行符,则不需要增加当前行的字符总宽度
                if (!(text[i + 1] == '\n'))
                {
                    currentLineTotalWidth += next_ch.advance + m_characterSpacing;
                }
            }
        }
        else
        {
            if (text[i] == '\n')
            {
                lineCount++;
                currentLineTotalWidth = 0;
                continue;
            }
        }
    }

    m_DisableFontTextureRebuiltCallback = false;
}

private Vector3 GetStartPosition(int lineCount, List<float> totalWidthtList)
{
    int totalLineCount = totalWidthtList.Count;
    float leftPosInRect = rectTransform.rect.xMin;
    float upPosInRect = rectTransform.rect.yMax;
    float halfLineWidth = totalWidthtList[lineCount - 1] / 2;
    float rectTransformWidth = rectTransform.sizeDelta.x;
    float rectTransformHeight = rectTransform.sizeDelta.y;
    switch (alignment)
    {
        case TextAnchor.UpperLeft:
            return new Vector3(leftPosInRect, upPosInRect - fontSize * lineCount - lineSpacing * (lineCount - 1), 0);
        case TextAnchor.UpperCenter:
            return new Vector3(-halfLineWidth, upPosInRect - fontSize * lineCount - lineSpacing * (lineCount - 1), 0);
        case TextAnchor.UpperRight:
            return new Vector3(leftPosInRect + rectTransformWidth - totalWidthtList[lineCount - 1], upPosInRect - fontSize * lineCount - lineSpacing * (lineCount - 1), 0);
        case TextAnchor.MiddleLeft:
            return new Vector3(leftPosInRect, (fontSize * totalLineCount + lineSpacing * (totalLineCount - 1)) / 2 - fontSize * lineCount, 0);
        case TextAnchor.MiddleCenter:
            return new Vector3(-halfLineWidth, (fontSize * totalLineCount + lineSpacing * (totalLineCount - 1)) / 2 - fontSize * lineCount, 0);
        case TextAnchor.MiddleRight:
            return new Vector3(leftPosInRect + rectTransformWidth - totalWidthtList[lineCount - 1], (fontSize * totalLineCount + lineSpacing * (totalLineCount - 1)) / 2 - fontSize * lineCount, 0);
        case TextAnchor.LowerLeft:
            return new Vector3(leftPosInRect, upPosInRect - fontSize * lineCount + (fontSize * totalLineCount - rectTransformHeight) + lineSpacing * (totalLineCount - lineCount + 1), 0);
        case TextAnchor.LowerCenter:
            return new Vector3(-halfLineWidth, upPosInRect - fontSize * lineCount + (fontSize * totalLineCount - rectTransformHeight) + lineSpacing * (totalLineCount - lineCount + 1), 0);
        case TextAnchor.LowerRight:
            return new Vector3(leftPosInRect + rectTransformWidth - totalWidthtList[lineCount - 1], upPosInRect - fontSize * lineCount + (fontSize * totalLineCount - rectTransformHeight) + lineSpacing * (totalLineCount - lineCount + 1), 0);
        default:
            return Vector3.zero;
    }
}

这里增加了一个调整字间距的属性m_characterSpacing,为了让这个属性显示在Inspector面板,需要为这个脚本增加一个对应的Editor脚本,这里不贴代码了。
实现的逻辑其实很简单,就是对于每个需要显示的字符,根据其所需的宽度,重新算一遍其所在的位置,如果当前是自动换行并且宽度不够,那么就把行数加一,重新在起始的X轴位置生成下一行的字符,适配好换行符,并正确获取到不同对齐方式的起点位置。
看着很复杂,其实就是算对齐比较麻烦。

6、最终效果

请添加图片描述

大佬们找到问题欢迎拍砖~

相关文章:

  • AutoModelForCausalLM 解析,因果模型
  • MyBatis中特殊符号处理总结
  • 安卓性能调优之-检测应用启动速度
  • 在Flutter中使用BottomNavigationBar和IndexedStack可以实现一个功能完整的底部导航栏
  • 适用于恶劣工业环境的高功率PoE+网管交换机
  • 状态管理组件Pinia 简介与底层原理 、Pinia 与其他状态管理库对比、Vue3 + Element Plus + Pinia 安装配置详解
  • DAPP实战篇:使用ethers.js连接以太坊智能合约
  • 数字图像相关(DIC)技术在土木行业的部分应用
  • 将已有 SVN 服务打包成 Docker 镜像的详细步骤
  • 蓝桥杯 区间排序
  • git操作0409
  • ruby self
  • 探索 Shell 中的扩展通配符:从 Bash 到 Zsh
  • ​​AMS行政管理系统:数字化赋能人力资源精益管理​
  • LeetCode 252 会议室题全解析:Swift 实现 + 场景还原
  • Cherry Studio配置MCP server
  • 记录学习的第二十四天
  • 用一个实际例子快速理解MCP应用的工作步骤
  • C++学习之服务器EPOLL模型、处理客户端请求、向客户端回复数、向客户端发送文件
  • Java蓝桥杯习题一:for循环和字符串的应用
  • java 视频网站开发/域名备案查询系统
  • 市建设局网站的综合业务管理平台/网络推广引流最快方法
  • ciid室内设计协会/windows7优化大师官方下载
  • 租电信网站服务器吗/新闻热点素材
  • 网站效果图设计思路/做seo推广公司
  • 如何做部落冲突网站/短视频入口seo