Лучший способ рисовать текст в MonoGame без использования SpriteFont?

Итак, я работаю над игрой в MonoGame, установленной в операционной системе компьютера. Как и ожидалось, он выполняет много рендеринга текста. Внутриигровая ОС позволяет пользователям настраивать почти каждый аспект операционной системы — люди создали скины для ОС, которые делают ее похожей на Mac OS Sierra, почти все основные выпуски Windows с 95-го года, Xubuntu, Ubuntu и многое другое.

Раньше эта игра была написана на Windows Forms, однако я хочу реализовать некоторые функции, которые просто невозможны в WinForms. Итак, мы решили перейти с WinForms на MonoGame, и столкнулись с одной огромной проблемой.

Формат скина, который мы сделали, позволяет пользователю выбирать любой шрифт, установленный на его компьютере, для использования в различных элементах, таких как текст заголовка, основной текст пользовательского интерфейса, текст терминала и т. д. Это было нормально в WinForms, потому что мы могли использовать System.Drawing для отображения текста и что позволяет использовать любой шрифт TrueType в системе. Если его можно загрузить в System.Drawing.Font, его можно отрендерить.

Но MonoGame использует другую технологию для отображения текста на экране. SpriteFont объектов. Проблема в том, что, похоже, нет никакого способа динамически генерировать SpriteFont из тех же данных, которые использовались для генерации System.Drawing.Font (семейство, размер, стиль и т. д.) в коде.

Итак, поскольку я, по-видимому, не могу динамически создавать SpriteFonts, в моем вспомогательном графическом классе (который занимается рисованием текстур и т. д. на текущем графическом устройстве без необходимости везде копировать код) у меня есть собственные методы DrawString и MeasureString, которые используют System. Drawing.Graphics для наложения текста на растровое изображение и использования этого растрового изображения в качестве текстуры для рисования на экране.

И вот мой код для этого.

public Vector2 MeasureString(string text, System.Drawing.Font font, int wrapWidth = int.MaxValue)
{
    using(var gfx = System.Drawing.Graphics.FromImage(new System.Drawing.Bitmap(1, 1)))
    {
        var s = gfx.SmartMeasureString(text, font, wrapWidth); //SmartMeasureString is an extension method I made for System.Drawing.Graphics which applies text rendering hints and formatting rules that I need to make text rendering and measurement accurate and usable without copy-pasting the same code.
        return new Vector2((float)Math.Ceiling(s.Width), (float)Math.Ceiling(s.Height)); //Better to round up the values returned by SmartMeasureString - it's just easier math-wise to deal with whole numbers
    }
}

public void DrawString(string text, int x, int y, Color color, System.Drawing.Font font, int wrapWidth = 0)
{
    x += _startx; 
    y += _starty;
    //_startx and _starty are used for making sure coordinates are relative to the clip bounds of the current context
    Vector2 measure;
    if (wrapWidth == 0)
        measure = MeasureString(text, font);
    else
        measure = MeasureString(text, font, wrapWidth);
    using (var bmp = new System.Drawing.Bitmap((int)measure.X, (int)measure.Y))
    {
        using (var gfx = System.Drawing.Graphics.FromImage(bmp))
        {
            var textformat = new System.Drawing.StringFormat(System.Drawing.StringFormat.GenericTypographic);
            textformat.FormatFlags = System.Drawing.StringFormatFlags.MeasureTrailingSpaces;
            textformat.Trimming = System.Drawing.StringTrimming.None;
            textformat.FormatFlags |= System.Drawing.StringFormatFlags.NoClip; //without this, text gets cut off near the right edge of the string bounds

            gfx.TextRenderingHint = System.Drawing.Text.TextRenderingHint.SingleBitPerPixel; //Anything but this and performance takes a dive.
            gfx.DrawString(text, font, new System.Drawing.SolidBrush(System.Drawing.Color.FromArgb(color.A, color.R, color.G, color.B)), 0, 0, textformat);
        }
        var lck = bmp.LockBits(new System.Drawing.Rectangle(0, 0, bmp.Width, bmp.Height), System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); //Lock the bitmap in memory and give us the ability to extract data from it so we can load it into a Texture2D
        var data = new byte[Math.Abs(lck.Stride) * lck.Height]; //destination array for bitmap data, source for texture data
        System.Runtime.InteropServices.Marshal.Copy(lck.Scan0, data, 0, data.Length); //cool, data's in the destination array
        bmp.UnlockBits(lck); //Unlock the bits. We don't need 'em.
        using (var tex2 = new Texture2D(_graphicsDevice, bmp.Width, bmp.Height))
        {
            for (int i = 0; i < data.Length; i += 4)
            {
                byte r = data[i];
                byte b = data[i + 2];
                data[i] = b;
                data[i + 2] = r;
            } //This code swaps the red and blue values of each pixel in the bitmap so that they are arranged as BGRA. If we don't do this, we get weird rendering glitches where red text is blue etc.

            tex2.SetData<byte>(data); //Load the data into the texture
            _spritebatch.Draw(tex2, new Rectangle(x, y, bmp.Width, bmp.Height), Color.White); //...and draw it!
        }
    }
}

Я уже кеширую кучу текстур, созданных динамически - оконные буферы для внутриигровых программ, текстуры кожи и т. д., так что они не сильно влияют на производительность, если вообще, но этот код рендеринга текста сильно бьет. У меня проблемы даже с игрой выше 29 FPS!

Итак, есть ли лучший способ рендеринга текста без SpriteFonts, а если нет, то есть ли способ вообще динамически создавать спрайт-шрифт в коде, просто указав семейство шрифтов, размер и стиль шрифта (жирный, курсив, зачеркнутый и т. д.) ?

Я бы сказал, что сейчас я средний с MonoGame, но мне достаточно трудно заставить RenderTargets работать - поэтому, если вы хотите ответить на этот вопрос, пожалуйста, ответьте на него, как если бы вы разговаривали с учеником детского сада.

Мы будем очень признательны за любую помощь, и, поскольку это серьезная проблема в моей команде разработчиков игры, вы можете увидеть себя упомянутым в титрах игры как главную помощь: P


person Michael VanOverbeek    schedule 06.07.2017    source источник
comment
Похоже, перед вами стоит непростая задача. Я не знаю точного решения вашей проблемы, но если это поможет, вы можете позаимствовать код из MonoGame.Extended BitmapFont.   -  person craftworkgames    schedule 07.07.2017
comment
Спасибо, чувак, я заменил свой код рендеринга шрифта на твой с использованием System.Drawing, и я считаю, что это намного лучший рендеринг шрифтов в MonoGame, особенно для азиатских символов. MonoGame плохо поддерживает многобайтовые символы Unicode.   -  person ryancheung    schedule 25.02.2019
comment
Если этот проект все еще актуален для вас, возможно, стоит проверить класс, который я создал именно для этой проблемы, он использует GDI+, но совершенно другим (более простым способом), чем ваш, что почти не влияет на производительность вашей игры: github.com/Zintom/BitmapTextRenderer   -  person Zintom    schedule 03.02.2020


Ответы (1)


Вы можете создать собственный шрифт спрайта, используя System.Drawing, и использовать этот. По сути, это любой символ, который можно использовать, сохраненный в словаре с соответствующим Texture2D.

Когда вы хотите нарисовать текст, вы просто рисуете каждый символ рядом друг с другом.

Это все еще медленно (потому что рисование текста без векторной графики всегда медленно), но, по крайней мере, вам не нужно разбирать все в каждом кадре.

Просто укажите где-нибудь, какие символы можно использовать, и импортируйте их. Словари в C# очень быстро индексируются, так что это вообще не должно быть проблемой.

Надеюсь это поможет. Удачи.

person Max Play    schedule 06.07.2017
comment
рисовать текст без векторной графики всегда медленно, вы в этом уверены? - person craftworkgames; 07.07.2017
comment
в случае написания писем в среде, которая не может вывести необработанные буквы из своих шрифтов, да, я. Шрифты представляют собой векторную графику, поэтому вы можете масштабировать их до нужного размера без каких-либо искажений, но большинство игровых движков анализируют файлы шрифтов в набор текстур и создают четырехугольник для каждой буквы. Это часто происходит медленнее из-за быстро растущего числа индивидуально отрисовываемых вершин, которые необходимо группировать. Это, конечно, зависит от объема текста, который вы хотите отобразить на экране, но по сравнению с небольшой математикой для каждого символа, который не зависит от размера, текстуры имеют больше накладных расходов. - person Max Play; 07.07.2017
comment
На самом деле, я понял это. Я модифицировал код так, чтобы он рисовал сам текст белым цветом, затем я использовал функцию цветового оттенка SpriteBatch, чтобы получить желаемый цвет. Это вырезает цикл for, в котором я конвертирую данные RGBA из System.Drawing в BGRA для Texture2D. - person Michael VanOverbeek; 08.07.2017
comment
Также стоит отметить, что я кеширую текст, когда рисую его, сохраняя текстуру в списке вместе со шрифтом, необработанным текстом и шириной переноса, а когда я перехожу к рендерингу текста, я проверяю список на наличие соответствующей кэшированной текстуры и визуализирую ее, если я могу найти его вместо создания совершенно новой текстуры. Это ДРАМАТИЧЕСКИ сокращает использование ЦП и ОЗУ. Я также очищаю кеш каждые 30 секунд, чтобы устаревшие кеши удалялись из оперативной памяти. - person Michael VanOverbeek; 08.07.2017
comment
Это очень хорошее решение! Как вы обрабатываете текст, поступающий от пользовательского ввода? - person Max Play; 08.07.2017
comment
@MaxPlay Я понимаю, откуда вы исходите, но я думаю, вы игнорируете тот факт, что даже система векторной графики все еще должна иметь проход отрисовки. Это не просто небольшая математика, чтобы вычислить форму вектора. После того, как вектор рассчитан, его все еще необходимо преобразовать во что-то, что может отобразить графическая карта. В современных технологиях есть много факторов, влияющих на то, как это фактически происходит но можно с уверенностью сказать, что это не так просто, как сказать, что один способ всегда лучше. - person craftworkgames; 10.07.2017