Привет! Спустя несколько месяцев работы, я наконец решился показать демку!
Для демки я подготовил 16 уникальных персонажей, 10 видов построек и 1 игровой уровень! Каждый из персонажей имеет свои индивидуальные базовые статы, скиллы и оружие.
Меню выбора персонажа
Меню строительства
В ближайшее время я планирую обновить тизер чтобы он соответствовал всем изменениям и обновлениям, а далее сосредоточусь на полировке демки к предстоящим ей фестивалям. Сейчас демо версия уже доступна в Steam и в неё можно поиграть!
Как же приятно осознавать, что твоя маленькая мечта осуществилась самым интересным образом!
С 12 лет я пыталась что-нибудь озвучивать. Сперва это был «Наруто» , а после самые разные онгоинги того времени (мне 28 год идет, если что).
В какой-то момент даже удалось заработать 200-400 рублей за серию (чувство гордости)
Мы сколотили небольшую и дружную команду подростков, которые любили своё дело: озвучивали различные тайтлы, обменивались опытом друг с другом и даже сотрудничали с другими командами!
В то время я подрабатывала в магазине, чтобы купить свой первый микрофон. Да и уговаривала родственников потерпеть моё хобби, ведь…я так и не научилась нормально заниматься озвучкой, как бы иронично это ни звучало.
Зато хотелось бы отметить, что некоторые мои коллеги по цеху таки продолжили это дело и довольно успешно! Невероятно горжусь ими и слежу за их творчеством! (´。• ᵕ •。`) ♡
А также представляю вашему вниманию пару отрывков. Эт я пыталась найти более-менее хорошие! Только звук потише сделайте)
Возможно, спойлеры!!!
Тайтлы: Hamatora The Animation; Gunslinger Stratos The Animation; Koe no Katachi
А какая мечта была, а?
Я ХОТЕЛА ПРИНЯТЬ УЧАСТИЕ В ОЗВУЧКЕ ИГРЫ!
Это же так волнительно! Волнительно подарить свой голос персонажу! Волнительно слышать себя по ту сторону экрана. Даже сейчас появляются мурашки от этой мысли.
И как же я воплотила это?
Оч изощренно: устроилась QA в небольшую инди-компанию….и им было не отвертеться.
С моим прекрасным коллегой мы скооперировались, набросали список звуков, которые необходимо внедрить в игру и…началось
Конечно, это неполноценные фразы, ибо в нашей игре подобный формат не предусмотрен, но…я смогла записать свой голос для игры… Для игры, в разработке которой принимаю активное участие. И даже не просто голос, но и попытки озвучить какие-нибудь…предметы? Ага! Для этого уже много лет в заводских коробочках тусят микрофон и звуковая карта (очень даже неплохие!)
И вот! Прилагаю примеры того, какие попытки были предприняты + часть того, что вошло в игру)
Предупреждаю сразу, что это не вся озвучка и впереди нас ждет немало работы и для других персонажей!
Если будет интересно, запилю еще чего-нибудь про нашу работу и озвучку
Я пишу игру на игровую консоль Playdate на чистом C. Игра в жанре "выживальщик" наподобие Vampire Survivors. Так как в чистом C отсутствуют многие современные объектно-ориентированные удобства мне приходится по-всякому изворачиваться чтобы адаптировать свои идеи в код. В этих заметках ты узнаешь из первых уст как создаётся игруля с нуля от идеи до публикации.
В прошлой главе я описал как инициализирую сцену, как очищаю ресурсы, показал как заполняю сцену реквизитом и даже поэкспериментировал с генерацией этого самого реквизита. В этой главе я расскажу как работает самая важная функция GameUpdate, в частности, обработка ввода и процессинг данных.
GameUpdate это функция-колбэк, которая вызывается каждый тик. А значит её задача это реализовать святую троицу любой игры:
считать ввод от игрока
обновить состояние игры
отрисовать обновлённое состояние игры.
Если ты когда-нибудь был на собесе в геймдев контору, то 90% вероятность, что у тебя спрашивали про эти три шага. А ещё если ты когда-нибудь писал код для Arduino, то ты должно быть помнишь две функции, которые там всегда должны быть: setup и loop. Вот GameUpdate это как раз аналог loop.
На сцене есть машина, которая двигается от нажатия крестовины, и есть реквизит: кактусы, песчаные насыпи и перекати-поле. Перекати-поле двигается прям как в жизни. То есть, оно меняет позицию по-горизонтали (по оси X) и ещё прыгает вверх-вниз как бы отскакивая от земли. Чтобы реализовать движение нам нужно совладать со временем. Для этого нам в каждом тике нужно знать сколько точно прошло времени с прошлого тика. Из коробки этого параметра в событии Update нет, однако мы можем этот параметр высчитать при помощи функции API playdateApi->system->getElapsedTime();. Эта функция возвращает количество секунд прошедших с момента запуска игрули. Это не разница в тиках, но уже что-то. Для разницы времени в тиках надо ещё знать значение полученное из той же функции в прошлый тик. Потому в структуре Game есть поле float previousElapsedTime;. В конце функции GameUpdate мы сохраняем в это поле результат вызова getElapsedTime, а в начале GameUpdate мы вычитаем разницу между нынешним значением getElapsedTime и previousElapsedTime. Это значение и есть тот самый dt, который равняется количеству секунд прошедших с прошлого тика. Так как на старте игры в файле main.c в первой главе я установил FPS равный 30, то в среднем dt у меня равен 0.033 секунд.
Начало функции GameUpdate
Далее мы процессим инпут - собираем значения нажатых кнопок и в зависимости от них обновляем данные.
Процессинг инпута
PDButtons это битовая маска объявленая в Playdate SDK. Битовые маски в сишке реализуются либо как enum, либо просто как int в отличие от Свифта, где битовая маска это совершенно иной особый класс данных.
Описание битовой маски PDButtons
Битовая маска PDButtons содержит в себе список нажатых и ненажатых кнопок консоли.
Ещё, возможно, у тебя есть вопрос что за такая функция PlayerVehicleAngleCreateFromButtons на строке 72. Это способ определения одного из восьми направлений машины имея на руках нажатые кнопки девайса:
Реализация функции PlayerVehicleAngleCreateFromButtons
Зачем нужен параметр oldValue в ней? Дело в том, что нам надо что-то возвращать даже если ни одна кнопка не нажата. А что вернуть если ни одна кнопка не нажата? Какое направление? В Свифте/C++/C# я бы вернул зануляемое значение (Optional в Свифте, std::optional в C++ и Nullable в C#), но в сишке это не так удобно потому что нет дженериков/шаблонов, потому я решил передавать старое значение направления потому что в случае когда ни одна кнопка не нажата направление машины просто не меняется. Это логично потому что в жизни если ты не трогаешь руль, то направление автомобиля тоже не меняется. Вот потому мы передаём старое значение и возвращаем его тогда, когда в Свифте/C++/C# вернули бы null. Если бы я работал в корпорации с отжайл-митами, ретроспективами, эффективными менеджерами, тимбилдингами и код-ревью, то обязательно появился бы ревьювер, который мне рассказал, что аргумент oldValue, если посмотреть на ситуацию под определённым углом, переносит логику того как движется машина внутрь функции PlayerVehicleAngleCreateFromButtons, а это неправильно потому что если следовать SOLID, стремиться писать идеальный код, утром и вечером чистить зубы, ходить на йогу, участвовать в городских марафонах, отказаться от мяса, глютена, молока, сахара, соли, глутамата натрия и кока-колы, то эта функция должна отвечать исключительно за создание инстанса перечисления PlayerVehicleAngle и больше ничего, а логика передачи старого значения обязательно, прям кровь из носу, век воли не видать, без разговоров, обсуждений и переговоров, должна находиться за пределами функции PlayerVehicleAngleCreateFromButtons потому что чисто теоретически у нас может эта функция использоваться не только для машины, а для чего-либо другого, что имеет также 8 направлений, но в случае если ничего не нажато направление будет, например, сбрасываться вверх. И пофиг ревьюверу на то, что такое случится примерно когда рак на горе свистнет, в четверг после дождя и ровно в следующую секунду после второго пришествия.
Если отбросить иронию и сформулировать ответ для занудного воображаемого ревьювера, то он (ответ) будет таким: значение oldValue это прекрасный подход реализации кода очень схожий с построением электрических цепей. Значение словно ток по цепи идёт сквозь функцию, и при определённых условиях оно может измениться на выходе, а может остаться таким же. Вообще код в стиле электрических цепей популярен в сишке, и при этом не так популярен в объектно-ориентированных языках. Я, понятное дело, не призываю всех на сишке писать именно в такой парадигме, но за себя я отвечаю вот таким вот образом.
Фух. Далее. Есть ещё функция GameAnyArrowIsPressed.Она возвращает 1 если хотя бы одна кнопка на крестовине нажата и 0 в противном случае:
Реализация функции GameAnyArrowIsPressed (возможно, следовало разнести операторы & по отдельным строкам для красоты кода)
Штош, мы пришли к следующему невероятно важному шагу нашего жизненно-важного тика - обработка перекати-поле.
Процессинг перекати-поле в тике
Констанцията screenSize пока не нужна - она пригодится позже. Далее мы проходимся по массиву старым проверенным методом: достаём количество объектов в нём и описываем цикл for. Получив очередной объект перекати-поля на строке 118 я готов его менять (потому указатель на Tumbleweed неконстантный). Разминаю руки и говорю себе "делай красиво!". Первым делом процессим позицию потому что перекати-поле перекатывается по полю по-горизонтали. Каждый тик двигающийся объект сдвигается (вот это поворот!), а значит мы должны проделать нехитрые манипуляции с позицией. Это требует базовые знания раздела механики из физики (того самого про "скорость равняется пройденный пуць делить на время", а ещё я обожаю слово "пуць" которое я подцепил в Беларуси когда жил там три года. Я стараюсь говорить "пуць" везде вместо слова "путь" и рекомендую тебе тоже так делать так как от такого русский язык станет только краше!). Чтобы лучше понять как происходит процесс движения в коде в первую очередь надо понять что нам нужно в конце концов сделать за тик. За тик нам нужно изменить позицию каждого объекта перекати-поле. Точнее, понять насколько изменилась позиция объекта перекати-поле относительно старой позиции за тик. Это изменение как раз хранится в константе dTumbleweedPosition, которая создаётся на строке 121. Высчитывается она очень просто: скорость перекати-поле умножается на dt, то есть, скорость умножаем на прошедшее время за один тик. А далее изменение позиции dTumbleweedPosition просто прибавляется к позиции этого же перекати-поле.
Подобным образом движение работает у всего вообще везде, не только в моей игре, а во всех играх и не только играх - всякие плавно двигающиеся кнопки в пользовательском интерфейсе, прыгающая иконка загрузки в яблочном браузере Safari, всплывающее окно антивируса Avast, падающие пуш-уведомления на iOS и многое другое что можно перечислять тут ещё до полуночи.
Окэй, с движением мы разобрались. Идём далее. А далее мы процессим прыжок. Дело в том, что перекати-поле подпрыгивает в движении. Значит нам в нашем мире который мы создаём своей мыслью и кодом нужно запрограммировать аналогичные прыжки перекати-поле и желательно чтобы результат выглядел правдоподобно, а не топорно как анимация в Героях 4. Вот только как сделать подпрыгивания чтобы они выглядели достаточно правдоподобно? Просто линейно как движение? Но это будет обсосно так как в реальности в любом движении по-вертикали участвует ускорение свободного падения, а это делает функцию движения квадратичной, а значит линейное движение не подойдёт. Функция нужна точно квадратичная, то есть, аргумент в ней обязательно должен хотя бы в одном месте возводиться в квадрат. Самое банальное это парабола. Она самая подходящая тут потому что в реальной жизни всё падает по параболе (конечно если игнорировать ветер и вообще если экспериментировать на сферических цыплятах в вакууме). Но если перекати поле будет лениво лететь в сторону земли по параболе, то тогда при столкновении с землёй мне надо будет иметь реализованную логику этого самого столкновения для отскока. Тут я оценил-взвесил прям как Экшон-мэн (помнишь такого супергероя? я в детстве обожал мультик "Экшон-мэн", и особенно мне нравилась его трёхмерная компьютерная рисовка. Тогда мне казалось, что это лучшая графика на свете. Недавно я решил пересмотреть этот мультик и офигел от того какая оказывается ужасная графика там на самом деле! RDR2 меня разбаловала! В общем, Экшон-мэн в момент кульминации каждой серии произносил "оценить, взвесить", просчитывал свои движения до мельчайшей точности, а в следующие 10 секунд нагибал всех врагов ультой) и решил сделать проще: я использую уравнение окружности, точнее, уравнение косинуса (или суниса если угодно, потому что график синуса это график косинуса сдвинутый на 90 градусов).
График синуса собственной персоной y = sin(x)
Вот только для наших целей мы график синуса чуть модернизируем - засунем его в модуль. Засовывание любой функции в модуль делает с её визуальным отображением занимательный фокус - отображает нижнюю половину вверх словно ось x превратилась в зеркало.
График модуля синуса y = abs(sin(x))
И вот такой вариант прям идеально похож на траекторию движения перекати-поля, и при этом нам не нужно писать логику столкновения с землёй и последующего отскока. Это тот редкий случай когда та фигня, которой тебя пичкали в школе, тебе пригодилась в работе!
Для адаптации данного математического фокуса в код нам нужно чтобы каждый объект перекати-поля имел значение "поворота" прыжка, а также скорость этого поворота (на сколько радиан значение поворота изменится за 1 секунду). Почему поворот? Потому что график синуса принимает в качестве переменной именно направление. Для пущего понимания на это можно смотреть как на фазу, которая крутится. Таким образом, у структуры Tumbleweed есть поля jumpVelocity и jumpAngle. На строке 125 мы высчитываем значение dTumbleweedJumpAngle равное количеству радиан на которое изменился jumpAngle, на строке 126 прибавляем это значение к jumpAngle, а на строке 127 нормализуем jumpAngle. Нормализация направлений это вещь, которую иногда следует делать если работаешь с направлениями - примерно как убирать какашки за кошкой если ты живёшь с кошкой (или она с тобой, лол). Так как значение направления циклично (0 радиан и 2*π радиан это одно и то же значение, например), можно для чистоты кода, совести и кредитной истории после операций над направлением приводить его в диапазон [0; 2*π) если вдруг это направление вышло за пределы (если кошка покакала мимо лотка надо всё вытереть, потому что сама кошка это вряд ли сделает).
Реализация функции normalizeAngle
Вообще будь у нас С++ я, возможно, нормализацию бы засунул прям внутрь класса Angle в оператор присваивания, который можно невозбранно перегружать. А может и нет - неявности порой делают код хуже. Как бы там ни было, именно таким образом мы процессим прыжки перекати-поля.
Итого, мы разобрались с процессингом позиции перекати-поля, прыжков (на самом деле процессить прыжки это лишь полдела, надо ещё их кошерно отрисовать, а это я покажу далее), осталось запроцесить кадр. Да, перекати-поле в моей игруле имеют несколько кадров для красивости. Я так сделал так как иначе если бы у перекати-поля был бы один кадр это выглядело бы обсосно. А я не хочу чтобы моя игруля выглядела обсосно. Вот для процессинга кадра я в структуру Tumbleweed добавил поле frameIndex. Вообще в игре у много чего будет такое поле и подобная логика. Ну и скорость изменения frameIndex тоже есть: это поле frameIndexVelocity. Да, это поле есть у каждого объекта Tumbleweed, хотя у всех объектов оно имеет одинаковое значение. Можно было бы не добавлять это поле потому что вроде как оно избыточно, но пусть будет - вдруг я решу сделать скорость разной у разных инстансов перекати-поля (а такие мысли в момент написания кода у меня были), а экономить память на спичках это путь в сумасшедший дом. Всего кадров у перекати-поля сделано 4. В одной из прошлых глав ты видел константу TumbleweedSpritesCount = 4 - вот это про это. frameIndex - это число с плавающей точкой, которое меняется в диапазоне [0; 4) со скорость указанной в frameIndexVelocity. Логика строк 130 - 134 осуществляет именно это.
Вот так устроен процессинг перекати-поля. Как тебе? Меня лично вставляет. Идём дальше.
Порой надо создавать перекати-поле, а не только процесить. Для этого надо решить по какой логике оно будет создаваться. Когда я усердно играл в Minecraft я частенько читал вики по нему. И в вики по Майнкрафту рассказывали каким образом спаунятся различные сущности. И логика спауна примерно такая: шанс один из десяти тысяч что в конкретном тике заспаунится сущность. Вот такую же логику я решил впиндюрить потому что это просто и понятно.
Создание перекати-поля
Строка 139 говорит нам, что с шансом 1 к 100 (tumbleweedSpawnChangePercentage равна 1) создастся новое перекати-поле в тике. На строке 154 создаётся инстанс перкати-поля функцией TumbleweedCreate, а на следующей строке этот инстанс отправляется (на самом деле копируется) в массив game->tumbleweeds.
Для создания перекати-поле нам нужно 4 аргумента: позиция на карте, скорость передвижения, скорость подпрыгивания и скорость изменения кадра. Позиция на карте высчитывается суперхитрым образом - новое перекати-поле появляется просто ровно за границей экрана левой либо правой. И едет в сторону машины игрока по-горизонтали. Можно, конечно спаунить "по-честному" в случайной точке достаточно большого игрового поля, но тогда игрок просто будет редко видеть перекати-поле, особенно в начале игры, а это ухудшает пользовательский опыт. Скорость подпрыгивания это количество радиан прошедших за секунду для значения от которого мы считаем синус график которого я ранее показывал. А про скорость изменения кадра ты уже и так знаешь: у перекати-поля 4 кадра, как я говорил ранее, и их надо с определённой скоростью менять.
Далее на строке 158 смещению камеры присваивается позиция машины чтобы машина всегда была в центре экрана куда бы она не ехала. А на строке 160 вызывается функция GameDraw, которая весь описываемый мной тут балаган отрисовывает чтобы игрок видел что происходит, иначе зачем всё это?
Отрисовку мы рассмотрим в следующей главе, а тебе спасибо что дочитал. Если нравится как я пишу и хочешь меня поддержать денюшкой, то я есть на патреоне и бусти.
МИРНЫЙ КЕМПИНГ жутковатое приключение про кемпинг в лесу!
ИСТОРИЯ
A PEACEFUL CAMPING — это короткая игра ужастик, в которой рассказывается о друзьях, которые отправились в поход, но в лесу, куда они пошли, обитает монстр, и теперь вам остается только бежать...
ПРЕДУПРЕЖДЕНИЕ
Эта игра содержит скримеры
От разработчика:
"Все пожертвования считаются суперподдержкой. Спасибо всем, кто делает пожертвования и играет в игру!"
Движение
WASD || ходить
Shift || бегать
E || подобрать
Ctrl || присесть
Обновлено : 19 дней назад
Опубликовано : 54 дня назад
Статус : выпущен
Платформы : Windows, macOS, Linux
Автор : AL7USSAIN
Жанр : Приключения
Сделано с Unity
Теги : 3D, Жуткий, От первого лица, Лес, Хоррор, Инди, Для одного игрока, Жуткий, Unity
Описание: Развитию одного из крупнейших тематических парков в стране угрожает особняк с привидениями. Присоединяйтесь к призрачной горничной в ее испытаниях и невзгодах, а также к ее сомнительным работодателям!
Очень рад рассказать впервые на Pikabu о Megaloot, разрабатываемый соло девом.
В Megaloot предстоит исследовать таинственную башню, полную бесконечного множества предметов из других реальностей, населённую монстрами, странными существами и, что самое важное, лутом.
В сердце Megaloot лежит уникальная игровая механика: игрокам необходимо стратегически управлять своим инвентарем и лутом, чтобы сделать мощного персонажа с его уникальным билдом, способного преодолевать противников в пошаговых боях. Каждый предмет лута не только ценен для экипировки, но и может быть преобразован в статы прямо посреди боя, позволяя игрокам подавлять врагов мощным ударом. Более 100 уникальных предметов лута, 24+ статы и колода карт с баффами, дебаффами и уникальными играбельными персонажами для зачистки этажей бесконечной таинственной башни.
Мы активно собираем заявки на закрытый плейтест в Steam и будем безумно рады всем, кто сможет помочь с фидбеком!
Нам очень важен фидбек игроков, чтобы сделать игру лучше и интереснее во всем, в чем только можно.
Добавьте в список желаемого и запросите доступ к закрытому плейтесту в Steam:
В процессе разработки игры мы часто сталкиваемся с проблемами, о которых на старте разработки не могли даже предположить. Забавно, что про многие специфичные вещи и подходы почти нигде не упоминают, а исправление ошибок часто требует уйму времени, которого всегда не хватает. Хочется хотя бы в качестве разнообразия (надеемся не только него) поделиться с вами какими-то моментами, встретившимися нам на пути. Собственно именно о них и будет данная серия постов.
И начнем мы с "лица" любой игры - графики. В этом посте мы бы хотели рассказать, почему нам пришлось перерабатывать все спрайт-листы и наглядно показать, как это изменило восприятие игровых локаций. В прошлом году начался активный этап проработки сцен, как раз в процессе которого мы и поняли, что в визуальной части сцен все чаще и чаще всплывают специфичные изъяны. Больше всего смущала разрозненность композиций по цвету и яркости, причиной чему стали как сами спрайт-листы, так и организация работы в рамках игрового движка.
Закономерным исходом стала практически полная переработка спрайт-листов, а именно: - Выравнивание спрайтов по тону (причем в силу специфики Unity пришлось делать спрайты гораздо ярче изначального варианта); - Избавление спрайтов от излишней черноты в местах оттенений; - Приведение всех спрайтов к более-менее единому уровню детализации; - Перенос работы с воздушной перспективой из спрайтов на уровень движка (в рамках Unity наконец получилось добиться нужного эффекта); - Правильное соотношение размеров спрайтов относительно персонажа и между друг другом;
В рамках же игрового движка тоже была осуществлена куча положительных изменений, связанных не только с инструментами разработки, но и самим подходом к выстраиванию графики. Упомянем только самые основные моменты, потому что перечислить все нюансы в рамках подобного формата весьма сложно: - Выстраивание иерархии объектов, которая позволила бы учесть специфику Unity в совокупности с большим кол-вом объектов и биением их по слоям параллакса (удивительно, что об это вообще никто не говорит, так как задача крайне не простая); - Проработка визуального стиля и общих правил выстраивания уровней с точки зрения графики; - Использование источников освещения в приоритете над конфигурацией конкретных спрайтов, что с одной стороны ускорило создание сцен, а с другой добавило гармонии в общую композицию и избавило ее от "эффекта пестрения", который часто резал нам глаза. - Активное использование шейдеров, компетенцию в создании которых пришлось значительно нарастить (на самом все еще наращиваем :D )
На самом деле про каждый из пунктов можно написать отдельный пост, но пока нет уверенности, что подобного рода технические нюансы действительно интересны большинству читающих этот пост. Так что... по крайней мере пока не будем углубляться в детали.
В заключении стоит сказать, что графика все еще не имеет финального вида, и в планах есть несколько значительных улучшений, которые будут сделаны перед релизом демо-версии. Однако даже в момент написания этого поста виден результат наших трудов - достаточно сравнить скриншоты до и после. Прикрепляем их к посту, чтобы вы своими глазами могли увидеть, что именно изменилось.
Как вам результат проделанной работы? Будем рады обратной связи!