{"id":2074,"date":"2025-02-14T14:07:25","date_gmt":"2025-02-14T14:07:25","guid":{"rendered":"http:\/\/nikolapacekvetnic.rs\/?p=2074"},"modified":"2025-02-26T23:21:51","modified_gmt":"2025-02-26T23:21:51","slug":"pixel-perfect-text-in-unity","status":"publish","type":"post","link":"http:\/\/nikolapacekvetnic.rs\/?p=2074","title":{"rendered":"Pixel Perfect Text in Unity"},"content":{"rendered":"\n<p>In this article we will take a look at the solution of the pixel perfect font problem in Unity as I implemented it for my currently developed game <a href=\"http:\/\/nikolapacekvetnic.rs\/?page_id=1799\" target=\"_blank\" rel=\"noopener noreferrer\"><span style=\"color: #ffffff;\">SODHARA<\/span><\/a>. Unity, given that it is a modern tool geared towards high res output, is not particularly friendly to the retro-inspired style of the yesteryear &#8211; the script presented in this article is one of the ways in which this hurdle can be overcome.<\/p>\n\n\n\n<h2>1 Overview<\/h2>\n<p>In <a href=\"https:\/\/sodhara.com\/\" target=\"_blank\" rel=\"noopener noreferrer\"><span style=\"color: #ffffff;\">SODHARA<\/span><\/a> I use <span style=\"color: #c5c5c5;\"><code>TextMeshPro<\/code><\/span> text components for everything text related. I use pixel perfect bitmap fonts in which all the glyphs&#8217; sizes should be integer values, and the resolution is low enough so that each &#8220;pixel&#8221; (or, rather, a constituent square) of each glyph should correspond to a single pixel on screen.<\/p>\n<p>The truth is however that these fonts are not perfect, and the <span style=\"color: #ffffff;\">problem is mostly with padding<\/span>. This means that for the left (or right) aligned text the first glyph will probably be set properly but then the next one will note be an integer value pixels away, which will accumulate after a while and present rather unpleasant artifacts. <span style=\"color: #ffffff;\">Much bigger problem lies in centered text<\/span>, since usually not even the first glyph is properly set in such a case.<\/p>\n<p>This can be somewhat circumvented by <span style=\"color: #ffffff;\">carefully placing the text objects on screen<\/span> so that, even though their position lies between pixels, the text itself is rendered properly. This only works if the <span style=\"color: #ffffff;\">resolution of the game is 1:1<\/span>, i.e. if one pixel of the graphics corresponds to one pixel on screen. However, this approach yields diminishing results as one introduces features such as localization, template strings, and <span style=\"color: #ffffff;\">any process which tampers with the strings in runtime<\/span>, thus making it impossible to place the objects properly in advance.<\/p>\n<p>To get my point across, please consider images 1-4, whereas the odd ones are taken from the editor and the even ones are in-game screenshots.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"376\" src=\"http:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot01a_SubPixels-1024x376.png\" alt=\"\" class=\"wp-image-2076\" srcset=\"http:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot01a_SubPixels-1024x376.png 1024w, http:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot01a_SubPixels-300x110.png 300w, http:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot01a_SubPixels-768x282.png 768w, http:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot01a_SubPixels-210x77.png 210w, http:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot01a_SubPixels.png 1043w\" sizes=\"auto, (max-width: 980px) 100vw, 980px\" \/><figcaption>Image 1. Centered text (font <a href=\"https:\/\/www.dafont.com\/uni-05-x.font\" target=\"_blank\" rel=\"noreferrer noopener\">Uni 05_x by Craig Kroeger<\/a>) in Editor.<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot02a_SubPixels-1024x576.png\" alt=\"\" class=\"wp-image-2077\" srcset=\"http:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot02a_SubPixels-1024x576.png 1024w, http:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot02a_SubPixels-300x169.png 300w, http:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot02a_SubPixels-768x432.png 768w, http:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot02a_SubPixels-210x118.png 210w, http:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot02a_SubPixels.png 1280w\" sizes=\"auto, (max-width: 980px) 100vw, 980px\" \/><figcaption>Image 2. Centered text (font Uni 05_x) in game.<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"376\" src=\"https:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot01b_SubPixels-1024x376.png\" alt=\"\" class=\"wp-image-2078\" srcset=\"http:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot01b_SubPixels-1024x376.png 1024w, http:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot01b_SubPixels-300x110.png 300w, http:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot01b_SubPixels-768x282.png 768w, http:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot01b_SubPixels-210x77.png 210w, http:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot01b_SubPixels.png 1043w\" sizes=\"auto, (max-width: 980px) 100vw, 980px\" \/><figcaption>Image 3. Centered text (font <a href=\"https:\/\/www.dafont.com\/5mikropix.font\" target=\"_blank\" rel=\"noreferrer noopener\">5MikroPix by Winter Design Studios)<\/a> in Editor.<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot02b_SubPixels-1024x576.png\" alt=\"\" class=\"wp-image-2079\" srcset=\"http:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot02b_SubPixels-1024x576.png 1024w, http:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot02b_SubPixels-300x169.png 300w, http:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot02b_SubPixels-768x432.png 768w, http:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot02b_SubPixels-210x118.png 210w, http:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot02b_SubPixels.png 1280w\" sizes=\"auto, (max-width: 980px) 100vw, 980px\" \/><figcaption>Image 4. Centered text (font <a href=\"https:\/\/www.dafont.com\/5mikropix.font\" target=\"_blank\" rel=\"noreferrer noopener\">5MikroPix)<\/a> in game.<\/figcaption><\/figure>\n\n\n\n<p>Whenever a glyph pixel founds itself in a <span style=\"color: #ffffff;\">sub-pixel position<\/span> it will be <span style=\"color: #ffffff;\">&#8220;rounded&#8221; to the closest integer<\/span> value, which, in case of being around the center of the pixel, means it will <span style=\"color: #ffffff;\">appear in two places<\/span> at once, yielding <span style=\"color: #ffffff;\">&#8220;chunky&#8221; and irregular glyphs<\/span> (both in case of letters and spaces) all over the text. What&#8217;s worse, in case of dynamic text, the <span style=\"color: #ffffff;\">glyphs will change width and shape based on their (changing) position<\/span>, which I find truly horrendous.<\/p>\n<p>The solutions I found online usually boil down to either <span style=\"color: #ffffff;\">moving the entire text object<\/span> to such a sub-pixel position that will &#8220;trick&#8221; the rounding process and make the text snap into place, or <span style=\"color: #ffffff;\">using custom scripts that snap the text to grid<\/span>. As I stated previously, the first solution is problematic when one has to deal with varying text, i.e. when localization is involved, and it is near impossible to achieve with centered text. The second solution seemed more plausible, but none of the users that mentioned it shared the scripts they use. Thus, I decided to write my own.<\/p>\n\n\n\n<h2>2 Pixel Perfect Text Enforcer<\/h2>\n<p>Retro-styled games usually use a small number fonts &#8211; <a href=\"https:\/\/sodhara.com\/\" target=\"_blank\" rel=\"noopener noreferrer\">SODHARA<\/a> in particular uses three, so the first step was to <span style=\"color: #ffffff;\">describe all the fonts used<\/span> in order to have relevant info on each of them in one place. The class I used is shown in listing 1.<\/p>\n<div class=\"code-toolbar\">\n<pre class=\"language-csharp\"><code class=\"language-csharp\"><span class=\"token keyword\">public<\/span> <span class=\"token keyword\">class<\/span> <span class=\"token class-name\">CharSpacingInfo<\/span>\n<span class=\"token punctuation\">{<\/span>\n    <span class=\"token keyword\">public<\/span> <span class=\"token keyword\">bool<\/span> IsFontMonospaced <span class=\"token operator\">=<\/span><span class=\"token operator\">&gt;<\/span> _fixedCharWidth <span class=\"token operator\">&gt;<\/span> <span class=\"token number\">0<\/span><span class=\"token punctuation\">;<\/span>\n    <span class=\"token keyword\">public<\/span> <span class=\"token keyword\">int<\/span> <span class=\"token function\">SuggestedWidthFor<\/span><span class=\"token punctuation\">(<\/span><span class=\"token keyword\">float<\/span> width<span class=\"token punctuation\">)<\/span> <span class=\"token operator\">=<\/span><span class=\"token operator\">&gt;<\/span> \n        IsFontMonospaced <span class=\"token operator\">?<\/span> _fixedCharWidth <span class=\"token punctuation\">:<\/span> <span class=\"token punctuation\">(<\/span><span class=\"token keyword\">int<\/span><span class=\"token punctuation\">)<\/span><span class=\"token punctuation\">(<\/span>width <span class=\"token operator\">\/<\/span> _pixelSize<span class=\"token punctuation\">)<\/span><span class=\"token punctuation\">;<\/span>\n\n    <span class=\"token keyword\">public<\/span> <span class=\"token keyword\">int<\/span> _pixelSize<span class=\"token punctuation\">;<\/span>\n    <span class=\"token keyword\">public<\/span> <span class=\"token keyword\">int<\/span> _fontSize<span class=\"token punctuation\">;<\/span>\n    <span class=\"token keyword\">public<\/span> <span class=\"token keyword\">int<\/span> _fixedCharWidth<span class=\"token punctuation\">;<\/span>\n    <span class=\"token keyword\">public<\/span> <span class=\"token keyword\">int<\/span> _interCharSpaceWidth<span class=\"token punctuation\">;<\/span>\n    <span class=\"token keyword\">public<\/span> <span class=\"token keyword\">int<\/span> _whitespaceWidth<span class=\"token punctuation\">;<\/span>\n\n    <span class=\"token keyword\">public<\/span> <span class=\"token function\">CharSpacingInfo<\/span><span class=\"token punctuation\">(<\/span>\n        <span class=\"token keyword\">int<\/span> pixelSize<span class=\"token punctuation\">,<\/span> <span class=\"token keyword\">int<\/span> fontSize<span class=\"token punctuation\">,<\/span> <span class=\"token keyword\">int<\/span> fixedCharWidth<span class=\"token punctuation\">,<\/span> \n        <span class=\"token keyword\">int<\/span> interCharSpaceWidth<span class=\"token punctuation\">,<\/span> <span class=\"token keyword\">int<\/span> whitespaceWidth<span class=\"token punctuation\">)<\/span>\n    <span class=\"token punctuation\">{<\/span>\n        _pixelSize <span class=\"token operator\">=<\/span> pixelSize<span class=\"token punctuation\">;<\/span>\n        _fontSize <span class=\"token operator\">=<\/span> fontSize<span class=\"token punctuation\">;<\/span>\n        _fixedCharWidth <span class=\"token operator\">=<\/span> fixedCharWidth<span class=\"token punctuation\">;<\/span>\n        _interCharSpaceWidth <span class=\"token operator\">=<\/span> interCharSpaceWidth<span class=\"token punctuation\">;<\/span>\n        _whitespaceWidth <span class=\"token operator\">=<\/span> whitespaceWidth<span class=\"token punctuation\">;<\/span>\n    <span class=\"token punctuation\">}<\/span>\n<span class=\"token punctuation\">}<\/span><\/code><\/pre>\n<div class=\"toolbar\">\n<div class=\"toolbar-item\"><button class=\"copy-to-clipboard-button\" type=\"button\" data-copy-state=\"copy\">Copy<\/button><\/div>\n<\/div>\n<\/div>\n<div class=\"code-toolbar\">\n<div class=\"toolbar\">\n<div class=\"toolbar-item\"><button class=\"copy-to-clipboard-button\" type=\"button\" data-copy-state=\"copy\">Copy<\/button><\/div>\n<\/div>\n<\/div>\n<p style=\"text-align: center;\">Listing 1. The <span style=\"color: #ffffff;\"><code>CharSpacingInfo<\/code><\/span> class keeps tracks of the various font measurements.<\/p>\n<p>The fields of the <span style=\"color: #ffffff;\"><code>CharSpacingInfo<\/code><\/span> class represent the following font qualities:<\/p>\n<ul>\n<li><span style=\"color: #ffffff;\"><code>pixelSize<\/code><\/span>: size of the &#8220;font pixel&#8221;, or the &#8220;constituent square&#8221; as I dubbed it previously,<\/li>\n<li><span style=\"color: #ffffff;\"><code>fontSize<\/code><\/span>: size of the font as set in the <span style=\"color: #ffffff;\"><code>TextMeshPro<\/code><\/span> script within the inspector,<\/li>\n<li><span style=\"color: #ffffff;\"><code>fixedCharWidth<\/code><\/span>: width of the glyphs in monospaced fonts (set to <span style=\"color: #ffffff;\"><code>-1<\/code><\/span> in case the font is not monospaced),<\/li>\n<li><span style=\"color: #ffffff;\"><code>interCharSpaceWidth<\/code><\/span>: width of the space between individual glyphs, and finally<\/li>\n<li><span style=\"color: #ffffff;\"><code>whitespaceWidth<\/code><\/span>: width of the whitespace, or spacebar character.<\/li>\n<\/ul>\n<p>The data given for each of the three fonts are used to force the glyphs to snap to pixel grid by means of wrapping the chars and spaces with <span style=\"color: #76c090;\"><code>&lt;mspace={size}px&gt;...&lt;\/mspace&gt;<\/code><\/span> tags, and also followed up by <span style=\"color: #76c090;\"><code>&lt;space={size}px&gt;<\/code><\/span> tag when a non-whitespace character is followed by another non-whitespace char. The <span style=\"color: #76c090;\"><code>size<\/code><\/span> are taken from the <span style=\"color: #ea8248;\"><code>CharSpacingInfo<\/code><\/span> instances, namely the <span style=\"color: #c5c5c5;\"><code>_fixedCharWidth<\/code><\/span> and\u00a0 <span style=\"color: #c5c5c5;\"><code>_interCharSpaceWidth<\/code><\/span> fields.<\/p>\n<p>The actual wrapping is done by the methods shown in listing 2.<\/p>\n<div class=\"code-toolbar\">\n<pre class=\"language-csharp\"><code class=\"language-csharp\">    <span class=\"token keyword\">public<\/span> <span class=\"token keyword\">static<\/span> <span class=\"token keyword\">string<\/span> <span class=\"token function\">CharWithTags<\/span><span class=\"token punctuation\">(<\/span><span class=\"token keyword\">char<\/span> c<span class=\"token punctuation\">,<\/span> <span class=\"token keyword\">int<\/span> size<span class=\"token punctuation\">)<\/span> <span class=\"token operator\">=<\/span><span class=\"token operator\">&gt;<\/span> \n        $<span class=\"token string\">\"&lt;mspace={size}px&gt;{c}&lt;\/mspace&gt;\"<\/span><span class=\"token punctuation\">;<\/span>\n\n    <span class=\"token keyword\">public<\/span> <span class=\"token keyword\">static<\/span> <span class=\"token keyword\">string<\/span> WrapStringInTags\n        <span class=\"token punctuation\">(<\/span>TMP_FontAsset fontAsset<span class=\"token punctuation\">,<\/span> <span class=\"token keyword\">string<\/span> text<span class=\"token punctuation\">)<\/span>\n    <span class=\"token punctuation\">{<\/span>\n        <span class=\"token keyword\">if<\/span> <span class=\"token punctuation\">(<\/span><span class=\"token keyword\">string<\/span><span class=\"token punctuation\">.<\/span><span class=\"token function\">IsNullOrEmpty<\/span><span class=\"token punctuation\">(<\/span>text<span class=\"token punctuation\">)<\/span><span class=\"token punctuation\">)<\/span>\n            <span class=\"token keyword\">return<\/span> text<span class=\"token punctuation\">;<\/span>\n\n        <span class=\"token keyword\">var<\/span> wrappedText <span class=\"token operator\">=<\/span> <span class=\"token string\">\"\"<\/span><span class=\"token punctuation\">;<\/span>\n\n        <span class=\"token keyword\">for<\/span> <span class=\"token punctuation\">(<\/span><span class=\"token keyword\">var<\/span> i <span class=\"token operator\">=<\/span> <span class=\"token number\">0<\/span><span class=\"token punctuation\">;<\/span> i <span class=\"token operator\">&lt;<\/span> text<span class=\"token punctuation\">.<\/span>Length<span class=\"token punctuation\">;<\/span> i<span class=\"token operator\">++<\/span><span class=\"token punctuation\">)<\/span>\n            wrappedText <span class=\"token operator\">+<\/span><span class=\"token operator\">=<\/span> <span class=\"token function\">WrapCharInTags<\/span><span class=\"token punctuation\">(<\/span>\n                fontAsset<span class=\"token punctuation\">,<\/span>\n                text<span class=\"token punctuation\">[<\/span>i<span class=\"token punctuation\">]<\/span><span class=\"token punctuation\">,<\/span>\n                i <span class=\"token operator\">&lt;<\/span> text<span class=\"token punctuation\">.<\/span>Length <span class=\"token operator\">-<\/span> <span class=\"token number\">1<\/span> <span class=\"token operator\">?<\/span> text<span class=\"token punctuation\">[<\/span>i <span class=\"token operator\">+<\/span> <span class=\"token number\">1<\/span><span class=\"token punctuation\">]<\/span> <span class=\"token punctuation\">:<\/span> <span class=\"token keyword\">null<\/span><span class=\"token punctuation\">)<\/span><span class=\"token punctuation\">;<\/span>\n\n        <span class=\"token keyword\">return<\/span> wrappedText<span class=\"token punctuation\">;<\/span>\n    <span class=\"token punctuation\">}<\/span>\n\n    <span class=\"token keyword\">private<\/span> <span class=\"token keyword\">static<\/span> <span class=\"token keyword\">string<\/span> WrapCharInTags\n        <span class=\"token punctuation\">(<\/span>TMP_FontAsset fontAsset<span class=\"token punctuation\">,<\/span> <span class=\"token keyword\">char<\/span> curr<span class=\"token punctuation\">,<\/span> <span class=\"token keyword\">char<\/span><span class=\"token operator\">?<\/span> next<span class=\"token punctuation\">)<\/span>\n    <span class=\"token punctuation\">{<\/span>\n        <span class=\"token keyword\">var<\/span> result <span class=\"token operator\">=<\/span> <span class=\"token function\">CharWithTags<\/span><span class=\"token punctuation\">(<\/span>\n            curr<span class=\"token punctuation\">,<\/span> \n            <span class=\"token function\">GetCharPixelWidth<\/span><span class=\"token punctuation\">(<\/span>\n                fontAsset<span class=\"token punctuation\">,<\/span> \n                CHAR_SPACING_INFO<span class=\"token punctuation\">[<\/span>fontAsset<span class=\"token punctuation\">.<\/span>name<span class=\"token punctuation\">]<\/span><span class=\"token punctuation\">,<\/span> \n                curr<span class=\"token punctuation\">)<\/span><span class=\"token punctuation\">)<\/span><span class=\"token punctuation\">;<\/span>\n\n        <span class=\"token keyword\">if<\/span> <span class=\"token punctuation\">(<\/span>curr <span class=\"token operator\">!=<\/span> <span class=\"token string\">' '<\/span> <span class=\"token operator\">&amp;&amp;<\/span> next<span class=\"token punctuation\">.<\/span>HasValue <span class=\"token operator\">&amp;&amp;<\/span> next<span class=\"token punctuation\">.<\/span>Value <span class=\"token operator\">!=<\/span> <span class=\"token string\">' '<\/span><span class=\"token punctuation\">)<\/span>\n            result <span class=\"token operator\">+<\/span><span class=\"token operator\">=<\/span> \n                $<span class=\"token string\">\"&lt;space={CHAR_SPACING_INFO[fontAsset.name]._interCharSpaceWidth}px&gt;\"<\/span><span class=\"token punctuation\">;<\/span>\n\n        <span class=\"token keyword\">return<\/span> result<span class=\"token punctuation\">;<\/span>\n    <span class=\"token punctuation\">}<\/span>\n\n    <span class=\"token keyword\">private<\/span> <span class=\"token keyword\">static<\/span> <span class=\"token keyword\">int<\/span> GetCharPixelWidth\n        <span class=\"token punctuation\">(<\/span>TMP_FontAsset fontAsset<span class=\"token punctuation\">,<\/span> CharSpacingInfo spacingInfo<span class=\"token punctuation\">,<\/span> <span class=\"token keyword\">char<\/span> curr<span class=\"token punctuation\">)<\/span>\n    <span class=\"token punctuation\">{<\/span>\n        <span class=\"token keyword\">if<\/span> <span class=\"token punctuation\">(<\/span>curr <span class=\"token operator\">==<\/span> <span class=\"token string\">' '<\/span><span class=\"token punctuation\">)<\/span>\n            <span class=\"token keyword\">return<\/span> spacingInfo<span class=\"token punctuation\">.<\/span>_whitespaceWidth<span class=\"token punctuation\">;<\/span>\n\n        <span class=\"token keyword\">if<\/span> <span class=\"token punctuation\">(<\/span>fontAsset<span class=\"token punctuation\">.<\/span>characterLookupTable<span class=\"token punctuation\">.<\/span>TryGetValue\n            <span class=\"token punctuation\">(<\/span>curr<span class=\"token punctuation\">,<\/span> <span class=\"token keyword\">out<\/span> TMP_Character tmpCharacter<span class=\"token punctuation\">)<\/span><span class=\"token punctuation\">)<\/span>\n            <span class=\"token keyword\">return<\/span> spacingInfo<span class=\"token punctuation\">.<\/span>SuggestedWidthFor\n                <span class=\"token punctuation\">(<\/span>tmpCharacter<span class=\"token punctuation\">.<\/span>glyph<span class=\"token punctuation\">.<\/span>metrics<span class=\"token punctuation\">.<\/span>width<span class=\"token punctuation\">)<\/span><span class=\"token punctuation\">;<\/span>\n\n        Debug<span class=\"token punctuation\">.<\/span><span class=\"token function\">LogWarning<\/span><span class=\"token punctuation\">(<\/span>$<span class=\"token string\">\"Glyph for character '{curr}' not found in the font asset. Using 0 width.\"<\/span><span class=\"token punctuation\">)<\/span><span class=\"token punctuation\">;<\/span>\n        <span class=\"token keyword\">return<\/span> <span class=\"token number\">0<\/span><span class=\"token punctuation\">;<\/span>\n    <span class=\"token punctuation\">}<\/span><\/code><\/pre>\n<div class=\"toolbar\">\n<div class=\"toolbar-item\"><button class=\"copy-to-clipboard-button\" type=\"button\" data-copy-state=\"copy\">Copy<\/button><\/div>\n<\/div>\n<\/div>\n<p style=\"text-align: center;\">Listing 2. The <span style=\"color: #c5c5c5;\"><code>WrapStringInTags<\/code><\/span> and <span style=\"color: #c5c5c5;\"><code>WrapCharInTags<\/code><\/span> methods of the <span style=\"color: #ea8248;\"><code>PixelPerfectTextEnforcerStatic<\/code><\/span> class.<\/p>\n<p>The <span style=\"color: #c5c5c5;\"><code>WrapStringInTags<\/code><\/span> method handles the string processing obviously. Each character (alongside the following one) is fed into the <span style=\"color: #c5c5c5;\"><code>WrapCharInTags<\/code><\/span> method which then wraps the input character into the required <span style=\"color: #76c090;\"><code>mspace<\/code><\/span> tags and adds the <span style=\"color: #76c090;\"><code>space<\/code><\/span> tag if necessary. Since the character glyphs themselves are pixel perfect, in this way we are only forcing the proper spacing between the characters and the proper whitespace character width as well.<\/p>\n<p>There is however another case one should account for, and that is the situation in which the string of pixel perfect text is supposed to be centered, but is of odd length in pixels \u2013 such a string can never snap perfectly to the pixel grid since all of its pixels will smack in the middle of the grid fields the entire time. For that purpose the methods shown in listing 3 are employed to adjust the <span style=\"color: #c5c5c5;\"><code>TextMeshPro<\/code><\/span> object position in such a way that pixel perfect text is assured even in such a case.<\/p>\n<div class=\"code-toolbar\">\n<pre class=\"language-csharp\"><code class=\"language-csharp\">    <span class=\"token keyword\">private<\/span> <span class=\"token keyword\">static<\/span> <span class=\"token keyword\">int<\/span> GetStringPixelWidth\n        <span class=\"token punctuation\">(<\/span>TMP_FontAsset fontAsset<span class=\"token punctuation\">,<\/span> <span class=\"token keyword\">string<\/span> text<span class=\"token punctuation\">)<\/span>\n    <span class=\"token punctuation\">{<\/span>\n        <span class=\"token keyword\">var<\/span> totalPixelLength <span class=\"token operator\">=<\/span> <span class=\"token number\">0<\/span><span class=\"token punctuation\">;<\/span>\n\n        <span class=\"token keyword\">for<\/span> <span class=\"token punctuation\">(<\/span><span class=\"token keyword\">int<\/span> i <span class=\"token operator\">=<\/span> <span class=\"token number\">0<\/span><span class=\"token punctuation\">;<\/span> i <span class=\"token operator\">&lt;<\/span> text<span class=\"token punctuation\">.<\/span>Length<span class=\"token punctuation\">;<\/span> i<span class=\"token operator\">++<\/span><span class=\"token punctuation\">)<\/span>\n        <span class=\"token punctuation\">{<\/span>\n            totalPixelLength <span class=\"token operator\">+<\/span><span class=\"token operator\">=<\/span> <span class=\"token function\">GetCharPixelWidth<\/span><span class=\"token punctuation\">(<\/span>\n                fontAsset<span class=\"token punctuation\">,<\/span> \n                CHAR_SPACING_INFO<span class=\"token punctuation\">[<\/span>fontAsset<span class=\"token punctuation\">.<\/span>name<span class=\"token punctuation\">]<\/span><span class=\"token punctuation\">,<\/span> \n                text<span class=\"token punctuation\">[<\/span>i<span class=\"token punctuation\">]<\/span><span class=\"token punctuation\">)<\/span><span class=\"token punctuation\">;<\/span>\n\n            totalPixelLength <span class=\"token operator\">+<\/span><span class=\"token operator\">=<\/span> \n                i <span class=\"token operator\">&lt;<\/span> text<span class=\"token punctuation\">.<\/span>Length <span class=\"token operator\">-<\/span> <span class=\"token number\">1<\/span> <span class=\"token operator\">&amp;&amp;<\/span> text<span class=\"token punctuation\">[<\/span>i<span class=\"token punctuation\">]<\/span> <span class=\"token operator\">!=<\/span> <span class=\"token string\">' '<\/span> <span class=\"token operator\">&amp;&amp;<\/span> text<span class=\"token punctuation\">[<\/span>i <span class=\"token operator\">+<\/span> <span class=\"token number\">1<\/span><span class=\"token punctuation\">]<\/span> <span class=\"token operator\">!=<\/span> <span class=\"token string\">' '<\/span> <span class=\"token operator\">?<\/span> \n                    CHAR_SPACING_INFO<span class=\"token punctuation\">[<\/span>fontAsset<span class=\"token punctuation\">.<\/span>name<span class=\"token punctuation\">]<\/span><span class=\"token punctuation\">.<\/span>_interCharSpaceWidth <span class=\"token punctuation\">:<\/span> \n                    <span class=\"token number\">0<\/span><span class=\"token punctuation\">;<\/span>\n        <span class=\"token punctuation\">}<\/span>\n\n        <span class=\"token keyword\">if<\/span> <span class=\"token punctuation\">(<\/span>fontAsset<span class=\"token punctuation\">.<\/span>name <span class=\"token operator\">==<\/span> <span class=\"token string\">\"HalfEighties\"<\/span><span class=\"token punctuation\">)<\/span> <span class=\"token comment\">\/\/ special adjustment for the \"HalfEighties\" font.<\/span>\n            totalPixelLength <span class=\"token operator\">+<\/span><span class=\"token operator\">=<\/span> <span class=\"token number\">1<\/span><span class=\"token punctuation\">;<\/span>\n\n        <span class=\"token keyword\">return<\/span> totalPixelLength<span class=\"token punctuation\">;<\/span>\n    <span class=\"token punctuation\">}<\/span>\n\n    <span class=\"token keyword\">public<\/span> <span class=\"token keyword\">static<\/span> <span class=\"token keyword\">float<\/span> GetPosXAdjustment\n        <span class=\"token punctuation\">(<\/span>TMP_Text tmpText<span class=\"token punctuation\">,<\/span> <span class=\"token keyword\">string<\/span> currText<span class=\"token punctuation\">)<\/span>\n    <span class=\"token punctuation\">{<\/span>\n        <span class=\"token keyword\">if<\/span> <span class=\"token punctuation\">(<\/span>tmpText<span class=\"token punctuation\">.<\/span>horizontalAlignment <span class=\"token operator\">!=<\/span> HorizontalAlignmentOptions<span class=\"token punctuation\">.<\/span>Center<span class=\"token punctuation\">)<\/span>\n            <span class=\"token keyword\">throw<\/span> <span class=\"token keyword\">new<\/span> <span class=\"token class-name\">System<span class=\"token punctuation\">.<\/span>ArgumentException<\/span>\n                <span class=\"token punctuation\">(<\/span><span class=\"token string\">\"Adjusting horizontal position for non-centered text is pointless. Aborting...\"<\/span><span class=\"token punctuation\">)<\/span><span class=\"token punctuation\">;<\/span>\n\n        <span class=\"token keyword\">var<\/span> lengthInPixels <span class=\"token operator\">=<\/span> <span class=\"token function\">GetStringPixelWidth<\/span><span class=\"token punctuation\">(<\/span>tmpText<span class=\"token punctuation\">.<\/span>font<span class=\"token punctuation\">,<\/span> currText<span class=\"token punctuation\">)<\/span><span class=\"token punctuation\">;<\/span>\n        <span class=\"token keyword\">var<\/span> pos <span class=\"token operator\">=<\/span> tmpText<span class=\"token punctuation\">.<\/span><span class=\"token generic-method function\">GetComponent<span class=\"token punctuation\">&lt;<\/span>RectTransform<span class=\"token punctuation\">&gt;<\/span><\/span><span class=\"token punctuation\">(<\/span><span class=\"token punctuation\">)<\/span><span class=\"token punctuation\">.<\/span>anchoredPosition<span class=\"token punctuation\">;<\/span>\n\n        <span class=\"token keyword\">var<\/span> isTextPosXInt <span class=\"token operator\">=<\/span> pos<span class=\"token punctuation\">.<\/span>x <span class=\"token operator\">%<\/span> <span class=\"token number\">1<\/span> <span class=\"token operator\">==<\/span> <span class=\"token number\">0<\/span><span class=\"token punctuation\">;<\/span>\n\n        <span class=\"token keyword\">var<\/span> shouldNudge <span class=\"token operator\">=<\/span> isTextPosXInt <span class=\"token operator\">&amp;&amp;<\/span> lengthInPixels <span class=\"token operator\">%<\/span> <span class=\"token number\">2<\/span> <span class=\"token operator\">!=<\/span> <span class=\"token number\">0<\/span><span class=\"token punctuation\">;<\/span> <span class=\"token comment\">\/\/ text has not yet been nudged but it should be<\/span>\n        <span class=\"token keyword\">var<\/span> shouldRevert <span class=\"token operator\">=<\/span> <span class=\"token operator\">!<\/span>isTextPosXInt <span class=\"token operator\">&amp;&amp;<\/span> lengthInPixels <span class=\"token operator\">%<\/span> <span class=\"token number\">2<\/span> <span class=\"token operator\">==<\/span> <span class=\"token number\">0<\/span><span class=\"token punctuation\">;<\/span> <span class=\"token comment\">\/\/ text has been nudged but should now be pulled back<\/span>\n\n        <span class=\"token keyword\">return<\/span> shouldNudge <span class=\"token operator\">?<\/span> <span class=\"token operator\">-<\/span><span class=\"token punctuation\">.<\/span><span class=\"token number\">5f<\/span> <span class=\"token punctuation\">:<\/span> <span class=\"token punctuation\">(<\/span>shouldRevert <span class=\"token operator\">?<\/span> <span class=\"token operator\">+<\/span><span class=\"token punctuation\">.<\/span><span class=\"token number\">5f<\/span> <span class=\"token punctuation\">:<\/span> <span class=\"token number\">0f<\/span><span class=\"token punctuation\">)<\/span><span class=\"token punctuation\">;<\/span>\n    <span class=\"token punctuation\">}<\/span><\/code><\/pre>\n<div class=\"toolbar\">\n<div class=\"toolbar-item\"><button class=\"copy-to-clipboard-button\" type=\"button\" data-copy-state=\"copy\">Copy<\/button><\/div>\n<\/div>\n<\/div>\n<p style=\"text-align: center;\">Listing 3. The <span style=\"color: #c5c5c5;\"><code>GetStringPixelWidth<\/code><\/span> and <span style=\"color: #c5c5c5;\"><code>GetPosXAdjustment<\/code><\/span> methods of the <span style=\"color: #ea8248;\"><code>PixelPerfectTextEnforcerStatic<\/code><\/span> class.<\/p>\n<p>The result is responsive pixel perfect text that is suitable for all alignment settings, and for both static and dynamic text alike. How it looks like in the end can be viewed in image 5.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-2083\" src=\"http:\/\/nikolapacekvetnic.rs\/wp-content\/uploads\/2025\/02\/Screenshot03_SnapToPixels.gif\" alt=\"\" width=\"800\" height=\"450\" \/><\/p>\n<p style=\"text-align: center;\">Image 5. Pixel perfect text enforcer.<\/p>\n<p>There is a short interval which the text needs to snap into place. This float value is exposed by the script and can be adjusted in the inspector. One more important thing to note is that the <span style=\"color: #c5c5c5;\"><code>TextMeshPro<\/code><\/span> object&#8217;s <span style=\"color: #c5c5c5;\"><code>RectTransform<\/code><\/span> component has to be set to integer coordinates for the <span style=\"color: #c5c5c5;\"><code>PixelPerfectTextEnforcer<\/code><\/span> script to work properly.<\/p>\n<p>The <span style=\"color: #ffffff;\">entire script<\/span> may be viewed on <span style=\"color: #bec2a6;\"><a style=\"color: #bec2a6;\" href=\"https:\/\/gist.github.com\/NikolaVetnic\/9f81ed90c8d50711249bf05a59429e51\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub<\/a><\/span> and <span style=\"color: #ffffff;\">freely used<\/span>. It is tested and works well with all kinds of text \u2013 static, dynamic, localized or not.<\/p>\n\n\n\n<h2>3 Conclusion<\/h2>\n<p>Nasty artifacts can really spoil the illusion of playing a game released a few decades ago, which breaks the immersion and ruins the style which the developer worked so hard to achieve. This simple script handles the <span style=\"color: #c5c5c5;\"><code>TextMeshPro<\/code><\/span> font problems neatly and effectively, and lets developers concentrate on things more important then nudging the text objects by sub-pixel values around.<\/p>\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":1417,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[55,126,34],"tags":[152,154,151,88,136,157,156,155,153,42,137,135],"class_list":["post-2074","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-code","category-game-dev-code","category-tutorial","tag-dev","tag-font","tag-game","tag-gamedev","tag-indie","tag-perfect","tag-pixel","tag-pixel-perfect","tag-text","tag-tutorial","tag-ui","tag-unity"],"_links":{"self":[{"href":"http:\/\/nikolapacekvetnic.rs\/index.php?rest_route=\/wp\/v2\/posts\/2074","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/nikolapacekvetnic.rs\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/nikolapacekvetnic.rs\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/nikolapacekvetnic.rs\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/nikolapacekvetnic.rs\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2074"}],"version-history":[{"count":51,"href":"http:\/\/nikolapacekvetnic.rs\/index.php?rest_route=\/wp\/v2\/posts\/2074\/revisions"}],"predecessor-version":[{"id":2135,"href":"http:\/\/nikolapacekvetnic.rs\/index.php?rest_route=\/wp\/v2\/posts\/2074\/revisions\/2135"}],"wp:featuredmedia":[{"embeddable":true,"href":"http:\/\/nikolapacekvetnic.rs\/index.php?rest_route=\/wp\/v2\/media\/1417"}],"wp:attachment":[{"href":"http:\/\/nikolapacekvetnic.rs\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2074"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/nikolapacekvetnic.rs\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2074"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/nikolapacekvetnic.rs\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2074"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}