Сообщество - Лига Разработчиков Видеоигр
Добавить пост

Лига Разработчиков Видеоигр

6 694 поста 22 148 подписчиков

Популярные теги в сообществе:

Isla de libertad #5: эффект горячего воздуха, смена времени суток, приборная панель, освещение

Ну вот и конец!

Я небольшой инди разработчик, как и многие совмещаю основную работу и разработку небольших инди игр, чаще всего в жанре хоррор.

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

Ну вот и конец! Steam, Unity, Gamedev, Инди игра

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

Что в итоге и стало роковой ошибкой.

Огромная машина под названием Steam просто заморозила проверку моей игры на неограниченное время.

Без объяснения причин. Без точных сроков.

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

Полный цикл статей тут:

https://pikabu.ru/story/3_mesyatsa_ne_mogu_vyipustit_igru_11021656

https://pikabu.ru/story/15_mesyatsa_ne_mogu_vyipustit_igru_10879170

https://pikabu.ru/story/vtoruyu_nedelyu_ne_mogu_vyipustit_igru_10770281

Ну вот и конец! Steam, Unity, Gamedev, Инди игра

Но у этой истории всетаки счастливый конец.

Спустя 3 месяца, после тонны писем в поддержку, мне дали зеленую кнопку и я выпустил игру.

P.S. Если вдруг наберу много лайков, выложу пост с финансовым отчетом по игре.

P.S.S. Ссылка на игру: https://store.steampowered.com/app/2538330/Finding_Anastasia/

Показать полностью 1

Делаю игрулю на Playdate на чистом C. Глава 2

Я пишу игру на игровую консоль Playdate на чистом C. Игра в жанре "выживальщик" наподобие Vampire Survivors. Так как в чистом C отсутствуют многие современные объектно-ориентированные удобства мне приходится по-всякому изворачиваться чтобы адаптировать свои идеи в код. В этих заметках ты узнаешь из первых уст как создаётся игруля с нуля от идеи до публикации.

В прошлой главе я описал сеттинг, показал видео с тем что получилось после первой итерации (оно продублировано ниже), а также детально рассказал как я реализовал в коде свой динамический массив с нуля, потому что ничего подобного ни сишка, ни Playdate SDK мне не предоставляют из коробки. Если ты не читал прошлую главу, то лучше начать с неё.

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

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

  • GameCreate

  • GameSetup

  • GameUpdate

  • GameDestroy

Интерфейс класса Game выглядит вот так:

Делаю игрулю на Playdate на чистом C. Глава 2 Программа, Гайд, Видео, Гифка, Длиннопост

Содержимое файла Game.h

Обрати внимание на слово typedef на строке 12. Это слово придётся встречать в моём коде часто. Зачем оно тут нужно? Чтобы я мог использовать тип Game как отдельно-живущий полностью самостоятельный тип без постоянного указания слова struct перед ним. То есть, в сишке по-умолчанию если написать так:

struct MyStruct { ... };

а потом попробовать инстанс структуры MyStruct объявить вот так

MyStruct myStruct;

или попробовать MyStruct передать в функцию

void myFunction(const MyStruct *myStruct)

то мы получим ошибку компиляции. А всё потому что надо добавить слово struct перед названием типа:

struct MyStruct myStruct;

и

void myFunction(const struct MyStruct *myStruct)

Ты спросишь "нахуа?". А я отвечу "ну потому что вот так, потому что это сишка, детка". Если в сишке ты объявляешь структуру, то каждый инстанс с типом этой структуры должен иметь приписку что это структура чтобы не дай Боб компилятор не заподозрил что ты имеешь в виду что-то другое.

Полагаю, если ты шарпер или свифтер ты сидишь в афиге от этого, потому что и в C#, и во Свифте просто объявленая структура это самостоятельный тип без всяких приписок. Да, всё так, но C# и Свифт это современные языки, а сишка создавалась примерно за миллион лет до красной революции, а тогда тренды и привычки в разработке были совсем другие, и, в частности, типы данных struct и union были чем-то диковинным, потому при инстанциировании их нужно писать дополнительно слово struct и union соответственно. Время шло, struct и union из равноправных сущностей изменились в статусе: struct стал мегапопулярным (из него появились объекты в ООП), а union остался местечковой заморочкой. Примерно как USB-флэшки и компакт-диски: когда-то и те, и другие были плюс-минус равноправными способами передачи информации, но со временем мы пришли в точку повествования, где современное поколение не в курсе что такое компакт-диск. Да и я сам забыл когда использовал компакт-диски. А нет, вспомнил: когда у стоматолога делал снимок зубов. Я понятия не имею почему стоматологи снимки зубов принципиально скидывают на компакт-диски, а не на USB-флэшки. Но, возможно, чтобы это понять нужно получить медицинское образование вместе со способностью писать от руки непонятным почерком.

Так, со странностью описания структур в сишке понятно, но причём тут слово typedef? А это совершенно другая фича, которая в некотором виде существует во всех современных языках программирования: конструкция typedef existing new; это объявление алиаса примерно как

using new = existing; // в С++

typealias new = existing // в Swift

(как это делать в C# я забыл. Напиши в коментах если знаешь).

Вот только, как ты видишь, в конструкции typedef новое и старое значения переставлены местами. Почему? Потому что сишка, не задавай вопросы!

В итоге, конструкция типа

typedef struct {...} MyStruct;

это указание того, что я буду считать выражение MyStruct алиасом на struct {...} указанную в этой же строке. Иногда можно встретить вот такое:

typedef struct MyStruct {...} MyStruct;

Это то же самое, но с избыточным указанием имени в оригинальной структуре. Первый вариант не имеет имени, и по сути объявляет анонимную структуру (да, в сишке есть анонимные структуры!) и заводит на неё синоним. А второй вариант создаёт именованную структуру, которую можно из коробки использовать с припиской struct, но так как мы тут же объявляем на неё алиас с тем же именем, то struct можно опустить.

Думаешь "ну и дичь!"? А что если я тебе скажу, что в современном C++ эта особенность сохранилась, но только в виде необязательной фичи? Смотри: можно вот прям сегодня открыть твою любимую плюсовую IDE и написать не

std::vector<int> vec;

а

class std::vector<int> vec;

и оно скомпилится! Я когда впервые об этом узнал несколько лет назад моя реакция была примерно такой

Делаю игрулю на Playdate на чистом C. Глава 2 Программа, Гайд, Видео, Гифка, Длиннопост

Простой программист осознающий, что его жизнь поделилась на "до" и "после"

Что ж, с typedef'ом разобрались, фух! Давай приступим к сути: к функциям. Пойдём сначала прямо внутрь GameCreate.

Делаю игрулю на Playdate на чистом C. Глава 2 Программа, Гайд, Видео, Гифка, Длиннопост

Реализация функции GameCreate в файле Game.c

Задача GameCreate создать инстанс игры - это билдер-функция, аналог конструктора в C++. Инстанс мы создаём в куче - это позволяет не создавать инстанс на старте приложения, а отложить его создание до получения init-события от операционной системы Playdate. Там, конечно, разница во времени почти нулевая между стартом игры и получением init-события, но чисто технически эта разница существует, потому делаем так.

Единственный аргумент, который нужен для создания инстанса игры это указатель на PlaydateAPI. Без него мы не можем вызывать API у операционной системы Playdate. Указатель на PlaydateAPI это как контекст в Android'е - без него можно писать код конечно, но только сферический код в вакууме, а не реальный код, который нагло и невозбранно взаимодействует с API системы направо и налево.

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

На строке 16 происходит как раз выделение памяти для инстанса Game, который функция вернёт в конце. Далее мы заполняем все поля ничего не пропуская.

Указатель на PlaydateAPI мы прихранили в игре на строке 17 - это важно. Далее, мы создаём инстанс машины - той самой, на которой катается игрок и расстреливает животных (смотри видео). Кстати, про машину - это отдельный класс, инстанс которого хранится в игре в единственном экземпляре, так как машина только одна. Создаётся инстанс машины тоже в куче потому что в начале я думал, что инстансы всего буду создавать в куче. Спойлер: уже буквально после машины я передумал, так как достаточно хранить всё что нужно в виде значений как есть внутри игры как часть игры. Но с машиной пока так.

Делаю игрулю на Playdate на чистом C. Глава 2 Программа, Гайд, Видео, Гифка, Длиннопост

Описание класса PlayerVehicle в файле PlayerVehicle.h

Машина имеет совсем мало полей: ей много и не надо. Это:

  • позиция в формате Vec2f (вектор имеющий поля x и y в формате float) - строка 9

  • направление angle, один из восьми вариантов направлений машины. Это перечисление (enum), объявление которого я покажу очень скоро - строка 10

  • булевое значение isMoving, которое равно 0 если машина стоит, и 1 если машина движется. Так как типа bool в сишке нет мы нагло используем int - строка 11

  • значение ускорения, которое нужно для реализации физики движения в формате float - строка 12

И ещё две функции: конструктор и деструктор (строки 15 и 16 соответственно). Как и API массива эти функции принимают указатель на функцию realloc так как эту функцию нам предоставляет PlaydateAPI.

Теперь покажу как выглядит PlayerVehicleAngle:

Делаю игрулю на Playdate на чистом C. Глава 2 Программа, Гайд, Видео, Гифка, Длиннопост

Описание перечисления PlayerVehicleAngle

Как ты помнишь из первой главы, в значения перечислений я вставляю имена этих перечислений в качестве префикса чтобы не было пересечений имён так как в сишке все значения перечислений доступны в глобальном пространстве имён. На Свифте я бы всё сделал в разы проще:

Делаю игрулю на Playdate на чистом C. Глава 2 Программа, Гайд, Видео, Гифка, Длиннопост

Как бы выглядел PlayerVehicleAngle если бы проект писался на Свифте где-то в альтернативной вселенной

Ну или на C#:

Делаю игрулю на Playdate на чистом C. Глава 2 Программа, Гайд, Видео, Гифка, Длиннопост

Как бы выглядело описание PlayerVehicleAngle если бы проект писался на C# в другой альтернативной вселенной

Делаю игрулю на Playdate на чистом C. Глава 2 Программа, Гайд, Видео, Гифка, Длиннопост

Как бы выглядело использование PlayerVehicleAngle если бы проект писался на C#

И как же без С++:

Делаю игрулю на Playdate на чистом C. Глава 2 Программа, Гайд, Видео, Гифка, Длиннопост

Как бы выглядело описание PlayerVehicleAngle если бы проект писался на C++ (где-то упала одна моя скупая мужская слеза) в ещё одной альтернативной вселенной

С направлением всё понятно, полагаю, как и с тем, как я описываю перечисления. Теперь вернёмся к машине, то есть, ко классу PlayerVehicle. Его объявление я тебе показал, давай теперь покажу реализацию.

Делаю игрулю на Playdate на чистом C. Глава 2 Программа, Гайд, Видео, Гифка, Длиннопост

Реализация класса PlayerVehicle

Как видишь, всё очень просто - создаём инстанс также в куче, присваиваем все без исключения поля и возвращаем что получилось. Это я говорю про конструктор - функцию PlayerVehicleCreate. А деструктор, PlayerVehicleDestroy, просто чистит выделенную под машину память.

Возвращаемся в игру туда, откуда мы ушли: в GameCreate, конструктор игры. На строке 19 я присваиваю cameraOffset - это смещение камеры, которое нужно для отрисовки позиции всего. Об этом подробнее позже. Далее я присваиваю NULL в поля cactusImage и sandImage. Это вот как раз то, о чём я говорил: это важно сделать, потому что иначе эти значения будут иметь мусор, а так как их тип это указатель (LCDBitmap *cactusImage), то без инициализации нулём на старте эти указатели будут указывать чёрт знает куда, и если я вдруг решу обратиться по этому адресу, то моя программа пойдёт по другому известному адресу, то есть, упадёт, крашнется или словит segfault (или 'сегфолт' по-русски если верить словарю Даля). Этого мы не хотим, потому что не для этого американцы создавали эту маленькую жёлтую консоль, а для бесконечного фана.

Далее прошу обратить внимание на три строки: 22, 23 и 24. В этих строках мы инициализируем наши динамические массивы. Первый это кактусы (cactuses), второй - песчаные горочки (просто sands), третий - перекати-поля (tumbleweeds). Если у тебя после прочтения первой главы остался вопрос как инициализировать массив, так скрупулёзно созданный мной байтик за байтиком, то вот это как раз тот самый пример.

Далее строку 25 давай пока пропустим - потом всё объясню. А вот после у нас идёт заполнение массивов кактусов и песков (буду говорить так вместо "песчаных горочек" , российский усатый политик тут ни при чём). Самые главные строчки это 40 и 43. В них мы непосредственно добавляем свежесозданный кактус либо песок в соответствующий массив вызывая уже известную из первой главы функцию ArrayPushBack.

Как именно заполняется карта игры песком и кактусами и что за такие RangeCreate (строки 31 и 33) и RandomIntInRange (строка 37)? Range это невероятно удобный вспомогательный класс, который я создал подглядев идею в стандартной библиотеке Свифта. Range(x, y) это диапазон значений по аналогии как в математике указывается [x, y), что означает диапазон от числа x включая x (об этом свидетельствует квадратная скобка) и до числа y НЕ включая y (об этом свидетельствует круглая скобка).

Делаю игрулю на Playdate на чистом C. Глава 2 Программа, Гайд, Видео, Гифка, Длиннопост

Содержимое файла Range.h

Делаю игрулю на Playdate на чистом C. Глава 2 Программа, Гайд, Видео, Гифка, Длиннопост

Содержимое файла Range.c

А ещё я решил прикрутить генератор случайных чисел прям к диапазону: если у тебя есть диапазон, скажем, от нуля включительно до пятидесяти невключительно, то можно вызвать функцию RandomIntInRange и передать Range, а в ответ вернётся случайное число в указанном диапазоне. Мне показалось это в разы удобнее всех тех миллионов функций для генерации случайных чисел, которыми забит сайт StackOverflow.

Теперь давай я расскажу как же я заполняю игровое поле кактусами, песком, и почему в этом коде нет перекати-поле. Самый тупой вариант это нагеренить N объектов (кактусов и песков) со случайными координатами x и y в пределах размера поля (размер поля, кстати, от -1000 до 1000, то есть, 2000). Но так делать не надо потому что с таким подходом чисто теоретически может получиться, что на достаточно большом куске поля (например, размером с экран, то есть 400 на 240) не выпадет ни одного объекта. И в таком случае поле будет выглядеть пустым и будет казаться, что машина едет по белому листу в MS Paint'е. Такое нам не нужно. Вместо этого нужно чтобы игрок регулярно видел реквизит, напоминающий о том, что мы едем именно по пустыне. Можно, конечно, повтыкать через одно и то же расстояние последовательно кактусы и пески как будто их вкопали солдаты квадратно-гнездовым методом, но это будет выглядеть обсосно и без души. Важно найти золотую середину - использовать рандом, но укротить его. Потому алгоритм я выбрал такой: я делю карту на прямоугольные блоки 150 на 100 (строка 27)

const Vec2i blockSize = Vec2iCreate(75 * 2, 50 * 2);

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

const int isCactus = RandomIntInRange(RangeCreate(0, 2));

И если isCactus равна 1, значит ставим кактус, иначе - песок.

Вообще, конечно, можно попробовать ради простого теста сгенерить столько же кактусов и песков с абсолютно рандомной позицией без всяких ограничений в виде блоков. Давай вот прям щас и попробуем это сделать. Смотри: всего кактусов/песков на карте создаётся

ceil(2000 / 150) * (2000 / 100)

ceil это функция округления вверх. В математике она обозначается как квадратные скобки без нижних пип (у квадратных скобок есть верхние и нижние горизонтальные пипы, вот если нижние убрать, то получится округление вверх). Почему я добавил эту функцию сюда - потому что деление 2000 на 150 не даст целое число, а если нам нужно точное значение, то надо решить что делаем с остатком. Исходя из условия нашего цикла на строке 28

for (float xMin = -1000; xMin <= 1000; xMin += blockSize.x + 20) {

остаток следует учитывать. Упс, я только что заметил, что есть ещё +20 на той же строке 28 - это я делаю пробел между блоками для пущей правдоподобности чтобы не было двух кактусов приклеенных друг к другу. Хм, тогда пересчитываем всё. По оси x я делаю отступ 20, по оси y - 15. Значит, всего кактусов/песков на карте создаётся

ceil(2000 / (150 + 20)) * ceil(2000 / (100 + 15)

это упрощается в

12 * 18 = 216

Значит, всего 216 объектов. Теперь давай перепишем генерацию кактусов/песков на рандомную позицию от -1000 до 1000 и посмотрим что получится.

Делаю игрулю на Playdate на чистом C. Глава 2 Программа, Гайд, Видео, Гифка, Длиннопост

Изменённый алгоритм генерации кактусов и песка

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

Кстати, возможно логичнее было бы положить создание кактусов в GameSetup, а не GameCreate. Ну да ладно - как сделали так сделали.

На этом с функцией GameCreate мы закончили. Далее идёт GameSetup - небольшая функция, которая стартует игру. Как я говорил, она по логике похожа на GameCreate потому что тоже вызывается единожды и тоже до всех обновлений (тиков) игры, но строго после GameCreate.

Делаю игрулю на Playdate на чистом C. Глава 2 Программа, Гайд, Видео, Гифка, Длиннопост

Реализация функции GameSetup

Тут кода мало: мы сначала вызываем функцию с забавным названием srand, которая инициализирует генератор случайных чисел. Можно её и не вызывать, но тогда на пятидесятую игру ты начнёшь замечать, что все случайности в игре (например, позиции кактусов) ничуть не случайны. Нам это не нужно. Далее мы вызываем GamePreloadImages для загрузки картинок (помнишь, мы все картинки проинициализировали как NULL в GameCreate?). После этого мы устанавливаем значение в cameraOffset равным половине экрана - так надо. Давай я подробнее расскажу про загрузку картинок - там есть интересные вещи.

Делаю игрулю на Playdate на чистом C. Глава 2 Программа, Гайд, Видео, Гифка, Длиннопост

Исходный код функции GamePreloadImages

Все картинки у меня лежат в папке images в проекте. Кадры машины, которых 8 штук по одному для каждого направления, имеют гениальные названия: 1.png, 2.png и так далее до восьми. Для загрузки одной картинки мне нужно вызвать функцию loadImageAtPath, которая принимает путь к картинке в пределах проекта (images/1.png, images/2.png и так далее) и необходимый указатель на PlaydateAPI.

Делаю игрулю на Playdate на чистом C. Глава 2 Программа, Гайд, Видео, Гифка, Длиннопост

Реализация функции loadImageAtPath

(LCDBitmap это тип из Playdate SDK, который означает картинку) Я честно позаимствовал эту функцию в примере игры на сишке у самого Playdate, так что тут ничего особенного нет. А вот то, как я загружаю, это следует понять. Как вообще загрузить 8 картинок с последовательными названиями? Конечно же, можно сделать просто 8 строк типа такого:

Делаю игрулю на Playdate на чистом C. Глава 2 Программа, Гайд, Видео, Гифка, Длиннопост

Пример того, как делать не стоит

Такой подход в простонародье называют китайский код. Конечно же, я делаю не так, а реализовал цикл. Имя картинки в цикле можно сгенерить зная индекс итерации. И в языках высокого уровня использовалась бы функция форматирования строки. Вот как я бы такое сделал на Свифте:

Делаю игрулю на Playdate на чистом C. Глава 2 Программа, Гайд, Видео, Гифка, Длиннопост

Потенциальная реализация загрузки картинок на Свифте

В коде на Свифте важно обратить внимание на генерацию полного пути к файлу картинки. Конструкция покрашенная красным создаёт строку вставляя в неё значение переменной-итератора i, которое меняется от 1 до 8 включительно. В сишке, к большому-пребольшому сожалению, нет простого API для подобной генерации динамических строк. Да, есть функция sprintf, которая позволяет вытворять что-то похожее, однако она не заботится о размере строки потому что это наша забота, а такое нам не нужно. Потому этой функцией я тоже не пользуюсь. Вместо этого я в своём сишном коде создал один раз шаблон пути к файлу "images/0.png" (строка 205) и в каждой итерации просто подменяю одну буковку, точнее, циферку перед точкой (строка 207):

imagePath[7] = '0' + i;

Что же тут такого особенного? А то, что в варианте на Свифте (или на любом другом языке высокого уровня в том числе С++ с той же либой fmt) каждую итерацию будет выделяться и чиститься по одной динамической строке, а у нас в сишке выделяется 0 динамических строк, есть только одна статическая (потому что нам заранее известен её размер), а мы аккуратно скальпелем меняем в ней один байтик и вуаля - никаких строк не нужно генерить. Не нужно использовать хитрый форматер, я сам себе форматер!

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

Кстати, подробнее про строку в коде imagePath[7] = '0' + i;. Тут происходит магия ascii-символов. Ноль в одинарных кавычках это литерал одиночного символа равный нулю, как строка, но только один её элемент. Если строка это поезд, то ноль в одинарных кавычках это один вагон. Если к нему прибавить целое число, то символ станет другим. Если прибавить один, то символ изменится на один вперёд в таблице ascii. А в таблице ascii цифры стоят последовательно, к счастью. То есть, если к нулю я прибавлю 1, то получу символ единицы. Если бы в таблице ascii цифры были бы в беспорядке, то данный фокус бы не сработал, и задачу пришлось бы решать либо форматтером, либо китайским кодом.

Фух. Как самочувствие? Не устал ещё? Мы прошли хороший путь: разобрали GameCreate, GameSetup, а сейчас я хочу завершить эту главу функцией, которой завершается игра - GameDestroy.

Делаю игрулю на Playdate на чистом C. Глава 2 Программа, Гайд, Видео, Гифка, Длиннопост

Реализация функции GameDestroy

Тут всё суперпросто: мы уничтожаем объект машины (строка 186) так как он создавался в куче как и массивы, потом уничтожаем все три массива, которые аналогично последовательно создавали внутри GameCreate (кактусы, пески и перекати-поле), а потом уничтожаем саму игру.

Кстати, в самом начале функции я проверяю game на NULL (строка 182), и если это так, то просто выхожу из функции. Я сначала думал, что это логично и позволяет писать безопасный код. В частности, на С++ я так делаю всегда. Однако на сишке в отличие от С++ почти всё передаётся через указатели, и ты просто запаришься проверять всё на NULL (например, представь если везде проверять указатель PlaydateAPI на NULL). Потому это был первый и последний раз когда я осуществил такую проверку - далее я просто решил для себя, что везде где ожидаются ненулевые данные я просто буду верить себе на слово, что там не NULL.

Итого в первый день я создал машину и поле, а во второй - кактусы и песчаные насыпи.

А что же с функцией GameUpdate? GameUpdate это самая важная функция в моём коде на данный момент потому что она в отличие от всех прочих функций класса Game вызывается стабильно каждый тик примерно так же, как сердце гоняет кровь туда и обратно по организму каждую секунду. Без этой функции игры не будет. Но разберём эту функцию мы уже в следующей главе.

UPD:

Глава 3

Показать полностью 19 1

Дневник начинающего разработчика. Часть 2. Длиннопостные животные и сотворение жизни в Лесу

Продолжение истории о том, как я пишу Духов Леса - свою первую текстовую ММОРПГ в телеграм-боте на Python.

Но, для начала, посмотрите на эти прекрасные мордашки.

Дневник начинающего разработчика. Часть 2. Длиннопостные животные и сотворение жизни в Лесу Текстовая ролевая игра, MMORPG, Gamedev, Чат-бот, Telegram бот, Компьютерные игры, ОБТ, Python, Животные, Длиннопост

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

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

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

Первым делом, нужно было создать персонажей, чтобы все дальнейшие нововведения было на ком проверять. И на старте я неделю расписывал их создание. Сразу стало понятно, что одним знанием Python тут не ограничиться. Нужно хотя бы на базовом уровне понимать SQL, чтобы хранить данные о персонажах.

Довольно быстро я понял, что записывать в одну базу десятки характеристик, переменных и состояний каждого персонажа будет проблематично. Тем более, игре придется потом часто к ним обращаться. Поэтому, подробные переменные я стал записывать в отдельный JSON файл для каждого персонажа, а в базе данных оставил только основные сведения (id класс имя и т.д.).

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

Дневник начинающего разработчика. Часть 2. Длиннопостные животные и сотворение жизни в Лесу Текстовая ролевая игра, MMORPG, Gamedev, Чат-бот, Telegram бот, Компьютерные игры, ОБТ, Python, Животные, Длиннопост

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

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

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

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

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

Дневник начинающего разработчика. Часть 2. Длиннопостные животные и сотворение жизни в Лесу Текстовая ролевая игра, MMORPG, Gamedev, Чат-бот, Telegram бот, Компьютерные игры, ОБТ, Python, Животные, Длиннопост

Первый из "длиннопостных" животных. Он рад каждому новому путнику в Изумрудном Холме, и ему есть что рассказать тем, кто не ленится читать. Так выглядит создание персонажа.

Здесь же, на стадии создания персонажа, нужно было кратко ввести игроков в курс боевки. Как любителя РПГ типа Diablo и POE меня всегда радовала вариативность. Потому и в своем мире мне хотелось сделать не жесткое деление на классы, а возможность их комбинировать. Основа должна быть простой и понятной с первого взгляда: воин, танк, убийца - которые по принципу камень-ножницы-бумага имеют разные преимущества. Однако, с самого начала игры новички предупреждаются, что все классы можно комбинировать, и ни один из первоначальных выборов не ограничивает другие возможности. Просто стоимость прокачки "чужих" навыков будет больше, чем "родных".

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

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

Навыки к этому моменту уже были в общих чертах распланированы в таблице, оставалось их только перенести в код и надеяться, что на стадии создания боевки я смогу это все учесть в формулах и функциях. Сначала я написал только первые 3 школы по 6 боевых навыков, а впоследствии добавил к ним 4 ремесленные школы по 8 - 10 скиллов.

Дневник начинающего разработчика. Часть 2. Длиннопостные животные и сотворение жизни в Лесу Текстовая ролевая игра, MMORPG, Gamedev, Чат-бот, Telegram бот, Компьютерные игры, ОБТ, Python, Животные, Длиннопост

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

С инвентарем пришлось повозиться тоже достаточно долго. Была создана база данных с видами предметов и функции для создания баз данных экземпляров предметов для каждого чата, где запущена игра. Так например вид предмета "катана", в зависимости от материала, из которого ее создают, может превратиться в экземпляры: Стальная катана, Кобальтовая катана, Титановая катана и т.д. со своими характеристиками. Плюс каждый из экземпляров можно улучшить, пока что до 5 уровня. Таким образом 1 строчка предмета "катана" в типовой базе с учетом коэффициентов материалов и улучшений превращалась в игре в 25 разных катан с отличающимися характеристиками.

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

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

Дневник начинающего разработчика. Часть 2. Длиннопостные животные и сотворение жизни в Лесу Текстовая ролевая игра, MMORPG, Gamedev, Чат-бот, Telegram бот, Компьютерные игры, ОБТ, Python, Животные, Длиннопост

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

Часть функций изначально вызывалась только через текстовые команды. Потом я задался целью дублировать все команды кнопочками. И чем больше кнопочек добавлялось, тем игра становилась удобнее и приятнее. Как показала практика, на удобстве игрока экономить нельзя. Если вместо текстовой команды можно сделать кнопочку - сделай. Если для одного действия игроку требовалось три клика, но есть возможность улучшить интерфейс, что бы оно же требовало один клик - сделай. Каждое такое маленькое улучшение интерфейса вызывало чуть ли не больше положительных отзывов, чем введение новых функций.

За сим на сегодня откланяюсь и побегу делать следующее обновление. Сейчас у меня в разгаре разработка веток крафта, отвечающих за строительство домиков и клановых замков, и обустройство их мебелью и пристройками. А всех, кого заинтересовал мой проект, прошу заглядывать на ОБТ.

Спасибо всем, кто дочитал! Продолжение следует.

Играйте в игры, любите игры, следуйте за своей мечтой и просто хорошо проводите время!

Показать полностью 4

Новые персонажи для шутера Starcraft

Топтуны)

Русы не против Ящерок - Новости!

Мир вашему дому, Русы!

В новой бересте – новости от нашей скромной команды.

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

Русы не против Ящерок - Новости! Компьютерные игры, Игры, Русы не против Ящерок, Инди, Инди игра, Gamedev, Steam, Длиннопост, Эротика

Сложнее всего дались, как ни странно, не ящерки, а Мирослава. Почему-то Вдова никак не хотела вставать так, чтобы все самое интересное было видно. Но мы еще посидим в камышах, до победного конца!

Русы не против Ящерок - Новости! Компьютерные игры, Игры, Русы не против Ящерок, Инди, Инди игра, Gamedev, Steam, Длиннопост, Эротика

Долго решали, как выточить идола, который играет важную роль в нашей истории. По сюжету достаточно бросить один взгляд на него, чтобы что-то зашевелилось, завелось внутри. И, возможно, Веинте при случае тоже не устоит и использует его… не совсем по назначению…

Русы не против Ящерок - Новости! Компьютерные игры, Игры, Русы не против Ящерок, Инди, Инди игра, Gamedev, Steam, Длиннопост, Эротика

Мы финализировали общий сюжет игры. Как мы уже писали в предыдущем девлоге, в игре будут параллельно идти две сюжетные ветки – противоборство богатырей и ящерок с одной стороны, вдова, приютившая Малсаса – с другой. Эти истории пересекутся трижды, и в итоге определят судьбу народов Гипербореи.

Наши писатели особенно увлеклись проработкой мира. Мы имеем в виду не только стилизацию под “древнерусское”, но и погружение в реалии и поиск способов вписать в них нашу историю. В этом им немало помогла книга Надежды Адамович и Натальи Серегиной “Интимная Русь”. Все мы почерпнули оттуда много нового и увлекательного!

А диалоги тем временем закончены уже более чем на четверть!

Русы не против Ящерок - Новости! Компьютерные игры, Игры, Русы не против Ящерок, Инди, Инди игра, Gamedev, Steam, Длиннопост, Эротика

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

Следите за новостями!

Страница игры в Steam: https://store.steampowered.com/app/2715650/

Показать полностью 3

Пустота холодна

Ввожу новых персонажей

Песнь Копья на фестивале демоверсий Steam

Песнь Копья на фестивале демоверсий Steam Компьютерные игры, Видеоигра, Инди, Игры, Видео, Длиннопост

А ещё у нас есть ламповый трейлер вдохновлённый "В гостях у сказки"

Мы небольшая команда из двух человек основы и нескольких ребят на аутсорсе. На днях мы обновили демку и принимаем участие в фестивале демоверсий Steam. Мы будем рады если попробуете нашу демоверсию и добавите игру в вишлист.

Добавить в вишлист и поиграть в демку можно на странице Steam

Также на всякий случай вот страничка в VKPlay

Группа в ВК

Показать полностью 13 1
Отличная работа, все прочитано!