Pull to refresh

Бессчётное количество персонажей для игры? О том, как я сделал это через процедурную генерацию

Reading time7 min
Views7.1K
Игра, которую мы разрабатываем, подразумевает огромное количество персонажей. Главный герой будет постоянно сталкиваться с ними и чтобы игроку не наскучили одинаковые лица, мы придумали как их генерировать из частей.

В статье я расскажу немного про нашу игру и про то, как мы реализовали это. Ниже, на превью, один из сгенерированных персонажей из нашего генератора:



А прежде чем я расскажу всё, попробуйте этот генератор в действии. Мы выложили его web версию тут: galaxypassstation.com/character-creator (для ПК и Планшетов)

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



Об игре, контекст


Игра называется — Galaxy Pass Station. Вы смотритель первой космической станции, куда прилетают гости со всей галактики. Есть множество инопланетных рас и культур, а в галактике правит Галактическое Правительство, которое устанавливает правила межзвездной миграции.

Игрок должен проверять пришельцев, как в игре «Papers, Please», используя разные sci-fi устройства. Он обустраивает на станции зону duty free, чтобы удовлетворять прихоти гостей. Станция состоит из разных технических и развлекательных блоков. У разных инопланетных рас разные хотелки и желания. Игрока постоянно испытывают, то неожиданными паразитами на станции, то различными монстрами, то пиратами, которые неожиданно нагрянут.

Игра ещё в разработке, используем Unity. В команде я и художник (Олег Савин).
Игра в Steam-е, там есть классный трейлер: https://store.steampowered.com/app/1571990/Galaxy_Pass_Station/


Разнообразие лиц


Наша игра предполагает, что вы будете часто видеть лица странных пришельцев и землян. Они должны быть смешными, забавными и вызывать какие-то эмоции у игрока. Визуальный стиль игры нам помог упростить эту задачу.

Мы вдохновлялись «Футурамой» и «Риком и Морти». Мы попытались создать в пиксельной графике стиль, похожий на взрослую анимацию. Рисовка в такой анимации предполагает довольно простую структуру лиц, которую можно собрать из частей. Мы пошли по похожему алгоритму, постоянно улучшая качество лиц.



Как это реализовано?


Представьте, что у вас есть десятки вариантов носов, глаз, ушей, причесок и т.д. Теперь их надо стандартизировать, выработать общие правила компоновки, чтобы всё друг с другом стыковалось наилучшим образом.


Мы по отдельности нарисовали:
  1. Каждую форму головы + варианты причесок под формы.
  2. Разные варианты волос, бороды, усы, брови.
  3. Носы, уши, глаза, рты.
  4. Костюмы и некоторые другие части.

Все это хранится через Scriptable Objects и редактируется прямо из редактора Unity. Выглядит это так:


// Классы, для хранения лиц.
// Мы используем Odin, чтобы в инспекторе было удобнее это редактировать.
namespace DataTypes
{
    [Serializable]
    public class AlienRaceFace
    {
        public Sprite Sprite => sprite;
        public bool Bald => bald;
        public List<AlienRaceBodyPart> Variants => variants;

        [SerializeField] [PreviewField(Height = 48)] private Sprite sprite;
        [SerializeField] private bool bald; // лысая форма?
        [SerializeField] [TableList] private List<AlienRaceBodyPart> variants = new List<AlienRaceBodyPart>();
    }
    
    [CreateAssetMenu(fileName = "Faces", menuName = "Alien Race/Faces", order = 31)]
    public class AlienRaceFaces : ScriptableObject
    {
        public int Count => variants?.Count ?? 0;
        public List<AlienRaceFace> Variants => variants;
        
        [SerializeField] private List<AlienRaceFace> variants;
    }
}


С помощью подобных скриптов, мы можем создавать различные конфигурации лиц, прямо из редактора Unity. ScriptableObject очень хорошая штука, чтобы хранить контент игры, если вы разрабатываете в соло, на мой взгляд.

На скрине представлены варианты формы головы для землян. На практике, мы выяснили, что проще всего хранить варианты причесок через форму головы. Если быть точнее, варианты чёлок, т.к. прически мы тоже храним отдельно.

Разделение на Женское и Мужское


Наш генератор или конструктор не предполагает, что вы должны выбирать пол персонажа. Это исходит из особенностей нашей игры. В 90% случаев сама игра генерирует персонажа и она должна определять, кто примерно получился — женщина, мужчина или что-то среднее.



Вы могли заметить выше на скрине, что у каждой части тела и лица встречается опция — Female. Это процент женственности части тела — от 0 до 1 (от 0% до 100%, если грубо). Он помогает нам определить пол персонажа после генерации. Тут ничего сложного:

Берем среднеарифметическое female коэффициента от всех частей тела.
  • Если результат больше 0.6, то это скорее всего женщина.
  • Если меньше 0.4, то скорее всего мужчина.
  • Если от 0.4 до 0.6 — это может быть как мужчина, так и женщина.
  • Например, есть много женских причесок, которые имеют female близкий к 1

Зачем нам вообще нужно знать пол персонажа? Пол фигурирует в документах посетителей станции, который должен проверять игрок, от пола может зависеть имя персонажа и многое другое. Но в целом, такой подход позволяет определять не только пол персонажа, но, например, и уровень «забавности» персонажа или чего-то еще.

Мы можем добавить еще один коэффициент — funny, и похожим образом его считать, чтобы понимать, какой персонаж получился и уже от этого менять его характер взаимодействия с игрой.

Эти коэффициенты можно использовать для подбора персонажа нужной вам конфигурации исходя из ситуации внутри игры.

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


Процедурная генерация из одного числа


Для генерации персонажа проще всего использовать Random с определенным seed числом. По-русски это зерно генерации.



На входе мы имеет объект Random с псевдо-случайным числом-зерном, из которого генерируются варианты глаз, ушей, волос и т.п. Это позволяет нам сохранять сгенерированного персонажа, просто, храня его зерно.

Представьте, что сгенерированный персонаж однажды должен появится в игре, но затем он должен периодически возвращаться к игроку. Вам не нужно хранить результат генерации, сгенерированную графику и т.п., вам достаточно сохранить число и заново сгенерировать по этому числу персонажа!

Приведу небольшой пример кода:

// кусок кода из нашего генератора...

private Random random;

private void Start()
{
      // создаем Random объект по заданному seed.
      random = new Random(seed);
}

private int RandomElement(int length, bool none = false)
{
      // просто для удобства.
      return random.NextInt(none ? -1 : 0, length);
}

private void Randomize()
{
     // получаем конфигурации персонажа по seed'у (зерну).

            @params = new PersonaResultParams
            {
                skin = RandomElement(constructor.Skins.Count),
                hairColor = RandomElement(constructor.HairColors.Count),

                head = RandomElement(constructor.Faces.Count),
                headStyle = 0,
                costume = RandomElement(constructor.Body.Costumes.Count),
                neck = RandomElement(constructor.Body.Necks.Count),

                hair = RandomElement(constructor.Hairs.Count),

                // ...
            }
}


Таким вот образом, мы можем получить порядковый номер каждой части тела, которую нужно показать. Осталось это только отобразить корректно.

Сборка тела и лица из спрайтов


Если кратко, мы используем шаблон с точками, в которых будут спавниться определенные части тела и лица. Сделано это через prefab, в котором собран типичный персонаж. Для каждой расы у нас планируется свой шаблон или даже несколько вариантов шаблонов:



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

Но что делать, если у нас разные пропорции формы головы? Мы используем всё тот же шаблон, только высчитываем разницу между формой головы в шаблоне и в той, которую создает генератор. Полученная разница — это по сути коэффициент, на основе которого мы должны модифицировать координаты наших ушей, глаз, рта, носа и т.д. Всё то, что находится на лице и рядом с ним. Тело и шея в этом случае одно целое и они не зависят от формы головы.

Тут есть один нью-анс. В процессе реализации мы часто используем Pivot точку самого спрайта, чтобы регулировать корректное смещение части тела. Например, для волос, точка pivot соответствует месту, откуда приблизительно должны расти волосы. С носом похожая ситуация.



Немного кода, который создает нос персонажа:

private void CreateNose()
{
    if (race.Constructor.Noses.Count > 0)
    {
        // достаем паттерн носа, где могут быть носы (координаты)
        var pattern = race.Constructor.Conf.Nose[@params.nosePattern]; 

        // достаем спеку носа, какой нос показывать
        var spec = race.Constructor.Noses.Variants[@params.nose];

        // досатем спрайт носа из Prefab'a, todo надо закешировать это
        var sprite = race.Constructor.Conf.Head.GetComponent<SpriteRenderer>().sprite;

        // определяем коэффициент, чтобы выработать правильные координаты 
        var factor = race.Constructor.Faces.Variants[@params.head].Sprite.rect.size / sprite.rect.size;

        AddScore(spec);  // добавляем female коэфциент

        foreach (var point in pattern.Points)
        {
            // создаем нос в нужной координате, выравнивая по пикселям.
            CreatePart("Nose", (point.target.localPosition * factor).SnapToPixel(), spriteRenderer =>
            {
                spriteRenderer.sprite = spec.Sprite;
                spriteRenderer.sortingOrder = 1400;
                ApplySkin(spriteRenderer);
            }, headTransform);
        }
    }
}


Дело осталось за малым, подобрать верные корректные координаты для шаблона, чтобы во всех случаях персонаж получался корректным, насколько это возможно.

Да, есть некоторые особенности и исключения из правил при генерации, например, брови привязываются к координатам глаз и их высоте, есть и другие исключения. Скорее всего, их будет больше когда мы начнем добавлять не гуманоидных пришельцев в свой генератор.

Про графику


Выше я уже писал про наш графический стиль. Всё, в целом, будет зависеть от него, но художнику нужно объяснить, что все части, которые он нарисовал, должны между собой стыковаться. У нас не было больших проблем с этим. Периодически, мы отбрасываем неподходящие варианты, но их довольно мало, либо они плохо получились.



Мне до сих пор не нравятся многие бороды, и мы еще не научились стыковать прически с лысиной. Эту проблему мы решим в будущем. Если вы рисуете в векторе, вам должно быть еще проще — не нужно выверять пиксели как делаем мы.

Мы не используем sprite sheets для частей тел. Да, это не очень оптимально для Unity, но в нашем случае, это не влияет на производительность так сильно, чтобы мы начали этот момент оптимизировать. Мы избавляем себя от ручной разметки спрайтов в редакторе, на что у нас раньше уходило много времени. Однако, всегда можно использовать функцию Sprite Atlas из новых версий Unity. Она позволяет собрать несколько спрайтов в одну большую текстуру без особых изменений в игре.

Перекрашивание спрайтов
Всю графику мы храним в едином цвете, мы не храним все варианты цветов. Художник рисует часть тела в одном цвете, а уже наш код-движок, если это нужно, заменяет цвета на другие. Это используется для изменения цвета кожи, перекраски костюма и изменения цвета волос. Делается это просто, через создание новой текстуры с заменёнными пикселями с одного цвета на другой, алгоритм замены примерно такой:

int i = 0;
// проходим по всем пикселям
foreach (var pix in pixels)
{
    pixels[i] = pix;

    foreach (var one in colors)
    {
        // нашли цвет, который нужно заменить на другой
        if (pix.IsEqualTo(one.In))
        {
            pixels[i] = one.Out;
            recolored = true; // перекраска произошла
            break;
        }
    }

    i++;
}


Есть еще вариант делать это через шейдеры, у нас в разработке такой вариант перекрашивания спрайтов.


В заключение


Буду рад всякой критике и отзывам о статье, игре и т.д. Если вам понравилась игра, не забудьте её добавить к себе в список желаемого в Стиме, чтобы не пропустить релиз в 2022 году.

Еще раз ссылка на игру: store.steampowered.com/app/1571990/Galaxy_Pass_Station (чтобы не листать в начало).
Tags:
Hubs:
Total votes 7: ↑7 and ↓0+7
Comments3

Articles