На данном скриншоте Вам представлена, казалось бы, обыкновенная игра с пиксельной графикой. Однако не все так просто.

Рельеф земли местами напоминает синусоиду, а пули похожи на два симметричных графика корня из x.

На самом же деле, все что вы видите на экране так или иначе относится к математике, математическим кривым и графикам.

Предыстория


Как-то раз, просматривая видео канала «Numberphile», я наткнулся на очень интересный видеоматериал, под названием «Формула всего».

В данном видеоролике была представленна самореферентная формула Таппера, которая при неком значении k, воссоздавала свое изображение на графике. Выглядит данная формула вот так:

$\frac{1}{2} < \lfloor mod(\lfloor \frac{y}{17}\rfloor 2 ^ {-17\lfloor x \rfloor - mod(\lfloor y \rfloor, 17)}, 2) \rfloor$


Данная формула очень заинтересовала меня, и у меня появилась идея:

«А что если создать игру, где вместо обыкновенных текстур, которые хранятся в различных файлах .png и .jpg формата, будут использоваться математические графики, кривые?»

Мне данная идея показалась довольно интересной и непростой в реализации.

Задачи


Передо мной стояли следующие задачи:

  • Придумать смысл игры, геймплей
  • Вывести формулы, графики которых будут представлять собой нужные мне силуэты персонажей, пуль, поверхностей
  • Реализовать все это в игре

Геймплей и смысл игры


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

Формулы и последующая их реализация в игре


Следующие два пункта я объеденил в один подзаголовок, потому что «скакать» между одной формулой и её реализацией нецелесообразно.

Для создания игры был выбран язык программирования c++ и библиотека SFML, для создания окон и отрисовки на них чего-либо.

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

В конце статьи будет ссылка на GitHub, где выложен весь код. В статье же будут приведены только маленькие кусочки кода, дабы не засорять её.

Поверхность планеты


Для поверхности планеты я вывел следующую формулу:

$f(x) = |sin(x)|$

Довольно простая формула, но при реализации возникла потребность в управлении высотой и длиной данной синусоиды. Также, во избежании кривизны рельефа, x домножается на ?. По этому, конечный код выглядит вот так:

int groundC = ceil(abs(sin(((i+1)*groundScale*M_PI)))*groundHeight);

Текстура планеты в космосе






Текстура планеты состоит из круга и узора на нем. В игре присутствует 4 формулы для создания узоров и 12 текстур планет с различным узором. В зависимости от «шага» формулы создаются различные узоры. Также, при генерации планеты, ей псевдорандомным способом устанавливается цвет, размер и позиция в космосе.

Пули




Изображение пули из игры. Пуля повернута.

Для пуль была выбрана очень простая формула:

$\sqrt{x}$

График данной формулы отзеркален по оси абсцисс.

Главный герой


Вот и добрались мы до самых сложных формул.

Формулу главного героя я вывел с большим трудом. Выглядит она так:

$\sqrt{x^{\frac{1}{2.8}}+x^{10.9-x^{9.3-x}}}-0.3$

Да, очень кривая, очень некрасивая формула. Но главное не формула, главное результат.

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



Далее нужна была текстура главного героя в космосе. Она выглядит так:



В её основу лег круг. Главная кабина выполнена с помощью следующей формулы:

$(x^{7-x})^{\frac{0.8}{x}}$

График данной формулы отзеркален по оси ординат.

Вот так данная формула выглядит на c++:

int x = round(pow(pow(i, 7 - i), 0.8 / i));

Враги и их спаунер



Справа на изображении синий спаунер, красные объекты — враги.

Спаунер представляет собой обыкновенную планету с необыкновенным узором. Этот узор — график формулы:

$sin(x)*x^{0.8}$


Формула текстур врагов:

$(x^{3-x})^{\frac{1}{x}}$



Деревья


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


Пример фрактального дерева

Скорее всего вы согласитесь, что сами по себе фрактальные деревья выглядят достаточно скучно. Если добавить немного элементов случайности, например вырасти может не обязательно 2 ветки, но также 3 либо 1, либо вообще не вырасти. Также, можно сделать не везде одинаковый угол наклона.

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

«А что если выдать каждому дереву определенное число(сид), от которого будут высчитываться псевдо рандомные числа, влияющие на параметры дерева?»

К счастью, в c++ есть отдельная библиотека, отвечающая за псевдорандом.

В итоге, сгенерированные деревья выглядят вот так:



Слева находится дерево с сидом 13, а справа — 22

А код, генерирующий эти деревья так:

Branch Branch::createNewBranch(Branch cur, Tree* parent, float angleMultiplier, int level) {
	
  Vector2f sp(cur.startPoint.x, cur.startPoint.y);

  float randomAngle = ((*parent).getRand() * 15) - 5;

  float t = cur.thickness * 0.75;
  float l = cur.length * 0.67;
  float a = cur.angle + 30*angleMultiplier + randomAngle;
  sp.y -= (cos((cur.angle-180)*3.1415926 / 180) * cur.length);
  sp.x += (sin((cur.angle-180)*3.1415926 / 180) * cur.length);
  Branch gen(sp, t, l, a, level);
    if (level > 0) {
      int count = 100 * (*parent).getRand();
      if (count >= 25 && count < 80) {  //только после многочисленных тестов я заметил, что в этом месте пропустил && count < 80, по этому дальнейшие скрины могут иметь небольшие неточности, почти незаметные. Также, из-за этого пришлось понизить шанс не выростания одной ветки с 20% до 10%, по этому, в конечном коде count<90
	(*parent).addBranch(gen.createNewBranch(gen, parent, 1, level - 1));
	(*parent).addBranch(gen.createNewBranch(gen, parent, -1, level - 1));
      }
      if (count >= 80) { //как я уже объяснял раньше, в конечном варианте count >= 90
	if (count % 2 == 0) {
	   (*parent).addBranch(gen.createNewBranch(gen, parent, -1, level - 1));
	}
	else {
	   (*parent).addBranch(gen.createNewBranch(gen, parent, 1, level - 1));
	}
      }
    }

  return gen;
}

Примечание. Да, я знаю, что я «схардкодил» некоторые переменные, но прошу не винить меня в этом. Я посчитал, что не имеет смысла создавать отдельные константные переменные, которые впринципе влияют только на шанс создания новой ветки.

Еще немного кода


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

Игрок


У игрока есть два разных метода update — spaceUpdate и planetUpdate. Соответсвенно, spaceUpdate обновляет игрока, когда он находится в космосе, planetUpdate — когда на планете. На планете рассчитывается ускорение и скорость игрока. В зависимости он горизонтального ускорения меняется и угол наклона тарелки — от 30 градусов до -30. Приближаясь к барьерам скорость игрока уменьшается. Такие барьеры существуют для оси x(0; mapSize.x) и для оси y. Для оси y все чуть сложнее. Есть минимальная высота, которая рассчитывается так: берется минимальная высота земли, складывается с высотой синусоиды и еще прибавляется высота деревьев. Высота деревьев посчитана очень простым способом — начальная длина ветки умноженная на количество циклов, выполняемых при генерации дерева. Верхней границы нету — вылетая за карту сверху игрок переключается на spaceUpdate и отрисовывается космос.

SpaceUpdate действует следующим образом: рассчитывается ускорение и скорость игрока. Далее рассчитывается угол поворота игрока. Рассчитывается угол следующим образом: если ускорение равно нулю, то рассчитывается угол относительно скорости игрока, если же нет — относительно ускорения. Также, в космосе у игрока присутсвует возможность стрельбы. Стрельба происходит следующим образом — создается пуля с поворотом как у игрока и добавляется в список. При обновлении игрока в космосе, каждая пуля в этом списке также обновляется. При отрисовке игрока также отрисовываются и пули. Также, в космосе все немного сложнее с барьерами. Космос поделен на сектора, в каждом секторе по 4 планеты, всего — 1 000 000 планет и 25 000 секторов. У каждого сектора есть уникальный id. Если остаток при делении на 500 равен 0 — присутствует левый барьер, если остаток 499 — правый, если при делении на 500 результат равен 0 — пристуствует верхний барьер, если 499 — верхний. Если каких либо барьеров нету, то при вылетании за рамки игрок перемещается в соотвествующий сектор.

Космос


Большую часть я уже изложил, но все же остались некоторые вещи. В каждом из секторов космоса есть по 4 планеты. Когда игрок нажимает на клавишу E, если он находится на расстоянии радиуса от этой планеты, то игрок перемещается на планету.

Враги


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

Спаунер


В каждом секторе космоса присутствует 1 спаунер. Спаунеры могут быть разных размеров. Размер влияет на дальность видимости игрока. Если игрок находится в зоне их видимости, то спаунер создает врагов каждые 5 секунд, но количество врагов не может превышать 10.

Заключение


Потратив около недели я создал игру, которая не использует никаких .png либо .jpg файлов.

Ссылка на проект на GitHub

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

Комментарии (34)


  1. eduard93
    14.10.2018 22:18

    Интересно.


    Добавьте пожалуйста GitHub релиз с бинарником.


    1. SampleCoder Автор
      16.10.2018 16:49

      Релиз добавил. GitHub'ом пользуюсь всего неделю, по этому что такое «бинарник» не знаю. Если объясните что это такое, постараюсь обязательно добавить.


      1. eduard93
        16.10.2018 17:15

        К релизу часто добавляют бинарный (исполняемый) файл — скомпилированную программу. В данном случае это exe и dll которые выдаёт Visual Studio.


        1. SampleCoder Автор
          16.10.2018 19:52

          Все прикрепил, вот ссылка на релиз.


  1. shiru8bit
    14.10.2018 22:24
    +7

    Это называется процедурная графика. Ещё немного, и получится kkrieger.


  1. prambeat
    14.10.2018 22:43

    Попробуйте поиграться с реймаршингом на шейдертое, думаю вам очень понравится


  1. Colorbit
    14.10.2018 23:09

    Для генерации текстур и объектов также можно использовать клеточные автоматы.


    1. SampleCoder Автор
      16.10.2018 16:53

      Достаточно интересная идея, возможно в будущем реализую её.


  1. IgorRJ
    15.10.2018 00:14
    +2

    Простите за наивный вопрос. Во втором абзаце читаю: «а пули похожи на два симметричных графика корня из 2.» А как он выглядит, график корня из 2?


    1. Zolg
      15.10.2018 00:49
      +1

      как он выглядит, график корня из 2

      По идее, как горизонтальная прямая. Ну или две.


      1. IgorRJ
        15.10.2018 08:31

        Упс… Помилуйте, сударь, а вторая-то откуда?
        А, понял, кажись — это которая «симметричная».
        Все равно — на пулю как-то не очень.
        Вообще-то, все это для автора статьи пишется, чтобы поправил. Ау?


        1. vp_arth
          15.10.2018 09:16

          Симметричная, ага. Квадрат отрицательного аргумента тоже квадрат…


          1. IgorRJ
            15.10.2018 09:47

            А где в выражении y = sqrt(2) отрицательный аргумент?


            1. vp_arth
              15.10.2018 10:18

              В строгом смысле sqrt(2)=±1.4142… Потому что (-1.4142...)?=2. Отсюда «Ну или две».


    1. SampleCoder Автор
      15.10.2018 17:37

      Спасибо что указали на мою ошибку. Я имел ввиду график квадратного корня из x.


  1. tormozedison
    15.10.2018 06:36

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


  1. suharik
    15.10.2018 10:24

    Думали о том, что можно генерировать не только деревья, но и NPC-живность на разных типах планет?


    1. SampleCoder Автор
      15.10.2018 18:02

      Достаточно интересная идея, но проблема в том, что FPS на планете и так не достаточно высокий из-за фрактальных деревьев. Также, для NPC нужны будут анимации, которые, боюсь, не смогу выразить через графики различных формул. Спасибо за идею, задумаюсь над её реализацией.


      1. c0f04
        15.10.2018 20:54
        +2

        Попробуйте алгоритмы кеширования использовать. По сути все объекты, которые у вас рисуются по формулам можно сделать одним и тем же классом. Формулу задавать в виде функции обратного вызова (указатель на функцию или сигнал). Отрисовку делать в отдельную карту пикселей, которую затем и рисовать при перемещении.

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

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

        Так Вы сможете значительно увеличить FPS, загрузив все ядра процессора. А потом можно уже экспериментировать и с GPU.


        1. SampleCoder Автор
          15.10.2018 22:59

          Спасибо большое за советы! Постараюсь пояснить, какие возникают сложности с SFML.


          У меня была следующая идея по оптимизации игры: я заметил что низкий FPS вызван отрисовкой фрактальных деревьев, так как отрисовать ~20-30 веток разных размеров и толщины не простая задача. Я настроил генерацию деревьев так, чтобы в кадре помещалось максимум 3 дерева(при таком количестве был достаточно стабильный FPS). Но вот какая идея мне пришла в голову: что если просто каждому дереву выдать свой спрайт, текстура для которого будет генерироваться при создании дерева? В таком случае, просадка FPS была бы только при генерации планеты. Но когда я решил реализовать это, я столкнулся с проблемой SFML: чтобы сгенерировать текстуру, я использовал RenderTexture. Если функцию, в которой объявляется RenderTexture вызывать из функции main, все отлично работает. Но если же вызывать непосредственно из конструктора класса выскакивает исключение. Я пытался бороться с ним, но увы, решения не нашел. Я пошел другим путем: я создал массив текстур, который хранил текстуры всех деревьев. Но опять же, SFML меня и тут удивил: при попытке загрузить текстуру из массива, загружалась абсолютно пустая текстура. Из-за вот таких проблем, FPS в игре достаточно низкий, а например текстур планет в космосе всего 12(опять же, генерация текстур при входе в сектор не может быть произведена, выскакивает исключение).


          Из-за таких неудобных моментов я и подумываю перенести проект на какой-нибудь движок.


          В c++ с потоками я не работал, но например на java я реализовывал потоки. Раньше, когда я создавал игры на Яве, мне нужны были 2-3 потока. Чаще всего первый отвечал за логику, второй за отрисовку, а третий за музыку. При создании же данной игры я хотел создать отдельный поток для отрисовки деревьев, но решил оставить это на потом.


          Ещё раз спасибо большое за Ваши советы, обязательно посмотрю видеоуроки про на c++.


          1. c0f04
            16.10.2018 08:00

            Спрайт и есть пиксельная карта, только встроенная в SFML, так что идея верная.

            Под спрайтом понимается sf::Sprite? Можете кусок кода, выкидывающий исключение сюда скинуть. Я сейчас спешу, как буду свободен, посмотрю, может смогу помочь.


            1. SampleCoder Автор
              16.10.2018 15:41

              Данный вопрос я задал на stackoverflow, но ответа не дождался. Если вы мне поможете я буду несказанно благодарен.

              P.S. Там вы найдете и пример кода в котором выскакивает исключение, и скриншот с самим исключением.


              1. c0f04
                16.10.2018 18:53

                Версия SFML какая? В таких вопросах всегда требуется указывать версию, если задаёте вопрос, т. к. ошибки в популярных фреймворках бывают, исправляется обычно в новых версиях.


                1. SampleCoder Автор
                  16.10.2018 19:44

                  Версия 2.4.2


                  1. c0f04
                    16.10.2018 21:14

                    У меня в репозиториях версия 2.1, на ней креш не воспроизводится. Предполагаю, что дело в версии. Попробуйте обновиться до 2.5.0 или откатиться до 2.1. Так можно проверить, в версии ли дело.

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

                    #include <SFML/Graphics.hpp>
                    
                    using namespace sf;
                    
                    class A {
                    public:
                      A() {
                        rend();
                      }
                    
                      void rend() {
                        RenderTexture r; // исключение возникает в этом месте
                      }
                    };
                    
                    int main() {
                      A a = A();
                      return 0;
                    }
                    


                    Для облегчения задачи тестирования желательно ещё указывать команды для компиляции, например, в моём случае:
                    g++ test.cpp -fsanitize=address `pkg-config --libs sfml-all` && ./a.out && echo Ok
                    


                    Ещё можете провести тест в виртуальной машине с Ubuntu/Debian, если есть сложность в установке библиотек под Windows. Команды для установки:
                    sudo apt-get update &&
                    sudo apt-get install -y libsfml-dev build-essential
                    


                  1. c0f04
                    16.10.2018 22:04

                    И ещё момент, в документации сказано, что после вызова конструктора RenderTexture объект становится невалидным, пока не будет вызван метод create. Не может быть ошибка в том, что забыли этот метод вызвать?


                    Пока это всё, чем могу помочь.


      1. suharik
        16.10.2018 09:13

        Здесь подойдет либо функция (математическая) с параметром, зависящим от времени, либо функция (C++), строящая разные графики в зависимости от времени. Каждый график — кадр анимации.


        1. SampleCoder Автор
          16.10.2018 15:47

          Я подумывал реализовать все немного по-другому. С помощью графиков функций построить отдельные части тела(голову, ноги, туловище) и потом, с помощью поворота и передвижения данных текстур создать иллюзию передвижения NPC.


  1. maxzh83
    15.10.2018 10:31
    +1

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


    1. c0f04
      15.10.2018 20:59

      Это очень хороший способ научиться программировать, т. к. сильно затягивает. Сам начинал с формул и графики, только это ещё под ДОСом было. А эта статья с объяснениями может мотивировать других людей, поэтому, да, тут суть в процессе, а не в игре.


  1. DolphinSoft
    15.10.2018 11:20

    … я учусь в школе и только недавно узнал о том, что такое синусоида и как она выглядит...

    Не останавливайтесь! Вы на верном пути. Желаю творческих успехов, но не забывайте успевать по остальным дисциплинам.


    1. SampleCoder Автор
      15.10.2018 18:25

      Спасибо!


    1. tormozedison
      16.10.2018 00:53

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


      1. Vasilii_B2
        16.10.2018 14:15

        Раньше, когда я создавал игры на Яве, мне нужны были 2-3 потока…
        Сначала создавал многопоточные игры на Яве, а потом в школе узнал о синусах ))) Не ну всякое бывает, я вот тоже позавчера холодный ядерный синтез домашнего изготовления успешно протестировал!