Вышла демка The Feudal
Прошу поддержать разработчиков.
Прошу поддержать разработчиков.
Продолжение поста о теории по игре Gris
В этот момент вновь возвращается стая теней и уничтожает статую. После чего принимает форму Мурены.
Грис уплывает от ТЕНИ, которая за время погони принимает несколько форм.
От тени Грис спасает черепаха, выводящая нас на свет.
Выбравшись обратно наверх и разместив все огоньки (которые мы собирали по всей игре) Грис получает новую способность - петь, оживая Флору вокруг.
Грис поднимается наверх, к дорожке из звезд, но тут вновь появляется тень.
Грис встречается лицом к лицу с ней и тень забирает у девушки все цвета, заставляя вновь её падать в черную мутную воду.
Вынырнув из воды Крис находит статую лежащей девушки.
Не смотря на то, что в 2-х интерпретациях сюжета думают, что все это время это были статуи МАТЕРИ Грис, я думаю, что это эти статую - Это сама Грис, только уже взрослая. А статуя лежащей девушки символизирует её принятие всего того, что с ней произошло.
Поднимаясь вверх по осколкам Грис вновь начинает петь.
Пока Грис поет, статуя собирается по осколкам в то время, как тень, превратившаяся в черную воду, поглощает всё вокруг, в том числе Грис и Статую. Но статуя открывает глаза и начинает петь вместе с Грис. Тень пропадает, а весь мир, который героиня прошла наполняется ВСЕМИ красками. Из глаза статуи прокатывается слеза, Грис целует статую.
Так же стоит упомянуть секретную кат-сцену. Если собрать все фрагменты в игре, и найти нужное место в последнем Этапе то можно получить данную сцену.
Ну что-же, теперь настал момент для самого тяжелого.
Gris - Игра про девушку, пережившую травмирующее психику в жизни событие - изнасилование в юном возрасте. И о том, как она боролась с этой травмой, заставив себя дальше жить.
Начнем по порядку - Gris(Грис) это не имя. В переводе с французского Gris - серый, тусклый пасмурный, как и цвета вокруг девушку в начале пути. Как будто весь мир вокруг нее был уничтожен одним событием.
Первая стадия - отрицание.
Статуя отрицания очень похожа на то, как такие травмирующие моменты в жизни человека показывает кинематограф.
Стадия вторая - Гнев.
Песчаная буря из негативных эмоций и то, как девушка по началу не может с ней справиться, а после находит способ закрываясь от нее за внутренней стеной гнева, пытаясь скрыть эту боль. Но Гневом она пробуждает боль, которую она пыталась скрыть за отрицанием произошедшего.
Стадия третья - Торг.
Высвободив боль героиня попадает в Лес, где встречает маленькое создание, за котором ухаживает, но тот вскоре покидает её оставаясь в НОРЕ. Из-за изнасилования девушка забеременела и тут виден конфликт между болью, от того что произошел и чувством материнства. По этому в стадии "Торг" я написала про мысли "Все хорошо, все будет хорошо" потому что чувство материнства вытесняет ту боль, которую ощущает девушка.
Но это чувство было недолгим, Существо не пошло за девушкой, а вернулось в нору, под землю. Это можно трактовать так, что у девушки произошел выкидыш или плод умер не родившись, что сделало Внутреннюю боль еще сильнее.
Чуть дальше мы видим 2 статуи и Грис взмывает буквально в небо. А позже Тень, олицетворяющая боль, становиться более сильной формой - Птицей.
Это была попытка суицида, которая оказалась неудачной и девушка выжила, погрузившись в депрессию. Первая статуя показывает боящуюся девушку, которая еще не решилась на этот шаг, а следом за ней, вторая, которая плача делает шаг, ступая как бы вниз. (Так же отдельный акцент сделан на трещины на самих статуях в области интимного места). После чего девушка буквально летит вверх к небу. И в этот момент начинается противостояние с Птицей, которую мы побеждаем.
После этого Грис получает новый цвет и начинается дождь и локации начинает затапливать водой, а Грис спускается вниз в четвертую стадию.
Стадия четвертая - Депрессия.
У вас бывали депрессии? Что вы обычно делаете когда у вас депрессия? В основном это отречение от всего вокруг и перебирание воспоминаний из прошлого с размышлениями "Что бы произошло если бы вы поступили по другому?".
Это же происходит и с героиней, Погружаясь в тьму депрессии Грис встречается с Тенью - Муреной. Теми самыми травмирующими воспоминаниями, о том что произошло с ней. Теперь, после прочтения всего, что я написала выше, посмотрите на эти 2 скриншота и спросите "Что на них изображено?"
От Тени-Мурены девушку спасает черепаха. Скорее всего она является аллюзией на победу, которой не было. Посттравматическое стрессовое расстройство, фантазия на тему "а вот бы меня спасли". И эта аллюзия помогает девушки вылезти из Депрессии к Принятию произошедшего и к тому, что нужно как-то двигаться дальше.
Стадия пять - Принятие.
Статуя девушки - это её решение. Она пытается похоронить в себе то, что с ней произошло. Принять, что бы двигаться дальше.
Именно по этому в самом конце, когда боль достигает пика, захватывая душевное состояние Грис, Статуя собирается воедино и начинает петь вместе с ней, будто сама девушка будучи уже взрослой смогла пережить все это и оставить позади, найдя силы жить дальше. Ведь именно после того, как статуя начинает петь, тем же голосом, что и сама Грис, весь внутренний мир наполняется красками.
Но почему на статуи трещины и она не восстановилась полностью? - Шрамы остаются навсегда.
Секретная кат-сцена - childhood (детство).
На самом деле эта кат-сцена не происходит в прошлом, как её интерпретируют. Она все так же происходит внутри мира Девушки, а показывает она то, как уже взрослая девушка отдает своей маленький версии звездочку, ту самую, которая нужна, что бы подняться в небо. Тем самым девушка окончательно отпускает свое прошлое.
Так же в качестве подтверждения моей интерпретации можно привести цитату из интервью с главным разработчиком игры.
Хотя история предоставляет простор для интерпретации, Мендоса [один из авторов Gris] заметил, что хотел осторожно и с уважением отнестись к данной теме, и поэтому советовался с женщинами, пережившими это. Мендоса также выразил гордость тем, что затрагивает в своей игре то, что на протяжении долгих лет считалось недопустимым в видеоиграх.
Какая тема? Тема смерти затрагивается в игровой индустрии не одно десятилетие и если бы это была тема смерти, то почему не со всеми людьми, пережившими смерть близкого человека, а только с женщинами? Ответ уже был дан мною выше.
Тема оказалась очень сложной. Если вы заметите грамматические или пунктуационные ошибки в тексте - сообщите пожалуйста, я исправлю.
С вами была Кэтрин, всем спасибо за внимание.
*Чувство внутренней опустошенности посте данного поста*
Пройдя игру и прочитав Обе теории, по сюжету Gris я сложила свою, которая очень многими аспектами схожа с ними, но одновременно в корне опровергает их. Осторожно, спойлеры!
Сюжет игры очень абстрактный. В игре нет диалогов, только музыка и визуальный ряд. Это развязывает руки фанатам для интерпретации сюжета и в интернете существует 2 основные версии:
1. Играя за Грис мы переживаем её внутренние переживания о смерти матери.
2. Вся игра - это воспоминания матери главной героини о самой Грис, которая на самом деле умерла.
Обе теории основываются на изображениях статуй взрослой женщины - якобы матери Грис и о секретной концовки - Childhood (Детство).
А так же на Модели Кюблера-Росс - описание эмоционального состояния неизлечимо больных людей или людей, потерявших своих близких.
По моему мнению все куда глубже. Свой текст я представлю в виде краткого пересказа игры с вставками своего видения определенных сцен.
Игра начинается с того, что Грис поет на руках статуи взрослой девушки. Тема песни и потери голоса имеет в игре большую роль, и я уверена, что это одна из главных метафор игры - Крик о боли, о том что произошло с Грис и о чем она не может сказать.
После потери голоса статуя рушится и Грис падает вниз.
Все цвета пропадают и мир становиться черно-белым. Игроку приходиться буквально силой заставлять героиню идти и временами управление над героиней полностью пропадает.
Если после получения полного управления над главной героиней вернуться назад, в самое начало, то можно будет найти статую и получить достижение "Отрицание"
Вся локация представляет из себя черные камни и разрушенные строения, будто весь мир вокруг Грис рухнул.
Пройдя локацию и дойдя до ладони статуи мы получаем новый цвет - Красный.
Проходя дальше Грис сталкивается с новой сложностью - песчаная буря из негативных эмоций, которая, не только мешает двигаться, но и отбрасывает её назад.
Именно в этой локации Грис обретает способность превращать своё платье в тяжелый груз кубической формы, которое позволяет ей противостоять бурей и рушить хрупкие камни в некоторых местах. Данная способность означает внутреннюю стену, которую Грис выстроила вокруг себя, что бы ничего больше не могло её ранить.
Только черствея можно притупить ту боль и негатив, которая красными штормами нападает на девушку.
Пробив пол Грис проваливается вновь в черно-белый зал. Если уничтожить на ней 3 статую получаем достижение - Гнев. Это символизирует попытки девушки Гневом заглушить ту боль, что внутри нее. Но этим она только сильнее высвобождает мысли об этой самой боли.
Боль от пережитого представляется в игре в виде черных бабочек-теней, которых девушка спрятала от себя Отрицая произошедшее (по этому зал черно-белый), но под Гневом высвободила их.
Именно эта боль выводить девушку из зала Отрицания вновь в Гнев, но уже выше. Где она плавное переходит дальше получая новый цвет - Зелёный.
Отправляясь дальше и переходя мост стая бабочек-теней ломает мост, закрывая нам путь назад
Пройдя дальше Грис попадает в лес, где встречает странное квадратное существо, которое можно накормить яблоками. Так же это существо хоть и не долго, но будет помогать нам. Что это за существо и что оно обозначает мы поговорим чуть чуть попозже.
После того, как существо остается в норе и покидает Грис, девушка отправляется в храм, в которой девушка открывает способность взлетать ввысь в местах, где есть не большое скопление красных бабочек.
Я думаю, что это олицетворяет то, что Грис взмывая ввысь как бы старается улететь от своей боли и мыслях о ней, как бы говоря себе "Все хорошо, все будет хорошо". В этом храме мы встречаем еще 2 статуи, о которых мы поговорим немного позже.
Достижение "Торг" получаем, если попытаться спеть около 1-й статуи.
И как раз в момент якобы триумфа Грис, когда она взмывает все выше и выше появляются они. Черные бабочки - тени принимающие форму ЧЕРНОЙ ЛАСТОЧКИ от которой девушки предстоит сначала убегать, а потом используя крик самой Ласточки победить её, заставив ударить криком в колокол, защищаясь с помощью формы куба.
Победа над данной формой черных бабочек-теней подразумевает скорее её победу над торгом и внутренним гневом т.к. первая половина битвы, там где мы должны убегать, происходит только в зеленных оттенка, а финальная часть с колоколом - только в красных. Но победа ли это?
Кэтрин
После победы над ласточкой мы получаем новый цвет - Синий.
Это единственный этап в котором Грис не поднимается вверх, а наоборот спускается вниз, уходя глубже в себя и свои мысли.
Грис обретает способность плавать под водой и весь уровень представляет с собой водный. В какой-то момент мы достигаем темноты и дальше Грис не может проплыть.
Если попытаться проплыть в самый низ получаем достижение "Депрессия"
Что бы проплыть дальше Грис необходимо разбудить черепаху, которая осветит дальнейший путь.
Далее Грис добирается до статуи, которая оживает, открывая глаза. А так же получаем новый цвет - Желтый.
Сам этап и все, что произойдет дальше представляет из себя самые темные моменты депрессии. Когда человек не может ничего делать и погружается в себя, в свои мысли. Вновь и вновь прокручивая воспоминания. По сути все, что будет дальше в стадии "Принятие" так же связанно с "Депрессией".
Привет, Пикабу! Не так давно я писал пост о том, что являюсь разработчиком и пилю игру про пиратов. Сегодня я хотел бы поделиться с вами прогрессом.
В первую очередь, я начал работать над взрывами и эффектами корабля.
Первая итерация взрыва корабля.
Собственно, при попадании определенным типом ядер по кораблю будет небольшой шанс на мгновенное уничтожение. Другими словами, есть шанс подорвать погреба с порохом!
Кроме этого, конечно будут и другие критические повреждения, например повредить рули или уничтожить пушку. Об этом я планирую рассказать вам в других постах)
Так же были обновлены эффекты самого выстрела. Теперь они намного более сочные и красочные)
Бездымный порох? Нет, не слышали)
А в ночное время все это выглядит еще интереснее.
Эффекты это круто, но мы не забываем и про персонажей. Они представлены в виде миниатюрных портретов, которые рисует моя жена. Вот и парочка первых персонажей вышли из под ее стилуса)
Не пинайте милаху, это реально плохо закончится)
Я не буду много рассказывать про код и решения для него, мне кажется это не слишком интересно читателю. Могу лишь сказать, что я принял решение уйти от симуляции физики для кораблей. Отказ от физики произошел по многим причинам, это конечно отняло время, но на то оно и прототипирование, чтобы проверять концепции и отказываться от ошибочных предположений. Теперь у меня развязаны руки в плане поведения корабля и его плавучести. А это уже хорошие новости) Всегда приятно иметь больше свободы.
Кстати, чуть не забыл) Нас зарегистрировали на фестивале стим - "Пираты против ниндзя". Это не принесло желаемого результата, но наверное это было предсказуемо с самого начала)
Обложка к фестивалю.
В заключении напишу про технические требования. В прошлом моем посте это вызвало удивительный ажиотаж, и я прислушался к вам. Я уделил время этому вопросу, протестировал на разных конфигурациях, провел минимальную оптимизацию и обновил системные требования на странице стим. Это все еще не конечные требования, и я надеюсь, что в итоге минимальные требования окажутся еще меньше. Но хотелось сказать спасибо всем тем людям, которые обратили на это внимание и указали мне:)
Надеюсь вместе с вашим фидбеком у меня получится отличная игра. Подписывайтесь и добавляйте игру в список желаемого, чтобы не не пропустить релиз!
Сегодня мы впервые показываем вам, как некроз влияет на мир игры.
Ранее мы уже выкладывали разрушенные дома, но с этой пятиэтажкой произошло кое-что похуже: здание покрыла разумная (или нет?) и крайне опасная плесень. Любое живое существо постарается держаться подальше от этого места, хотя кое-кто может находиться здесь без риска для жизни.
Знаете ли вы, как называют таких людей в книгах по вселенной «Технотьмы»?
Статья написано на основе видео. Можете его посмотреть. Там есть кот.
Меня зовут Мария и очередная статья, в которой я рассказываю об игре, которую мы с моим мужем Алексеем разрабатываем сейчас.
В игре Вам предстоит помочь Магнолии в игре в прятки. Её подруга Рут спряталась где-то неподалёку. Изучайте локации, осматривайте предметы, помогите Магнолии отыскать Рут!
Рут прячется в бочке. Но только иногда.
В одном из прошлых видео я размышляла о том, как лучше всего обозначить границу мира для игрока.
И сегодня хочу рассказать о том, как я решила эту проблему.
Конечно, самый простой способ ограничить игрока – это поставить невидимую стену, через которую персонаж просто не сможет пройти.
Но мне хотелось более изящного решения.
Так же мне хотелось, чтобы это ограничение было как-то вписано в игру.
С одной стороны, мне хочется показать, что игра это лишь кусочек мира. И там за ее пределами, сам мир в котором живет, Магнолия не ограничивается локациями.
С другой стороны хочется подстелить соломки, в том случае если я решу расширить уже существующую карту, или выпустить дополнение к игре.
Куда же Магнолию заведёт дорога приключений?
Итак, у меня в игре есть дорога. Это всегда хорошая направляющая для игрока. Но что, если кто-то решит отклонится от нее, и пойдет в совершенно другом направлении?
Алексей убежден, что даже в рамках небольшой игры следует предоставить игроку определенную свободу действий. Если игрок пожелает выбрать альтернативный путь или, например, обойти дом, не следует лишать его такой возможности.
Я сама просто обожаю проверять другие игры на прочность и соваться в самые странные уголки локации. И очень приятно, когда создатели поощряют мое любопытство.
Проект Магнолия очень простая игра. В ней нет ни лута ни инвентаря. Поэтому я решила поощрить любознательного игрока небольшим количеством контента.
Когда я задумалась о том, как можно заблокировать условный путь, мне на ум пришли враги из моей второй игры.
Сырна прыгает и собирает мороженное
Это был платформер на Unity. Очень простой. Там Сырна прыгала и собирала мороженное перепрыгивая через злые колючки.
Эти колючки в качестве врагов для болотной местности показались мне отличной идеей. Я подумала, что они хорошо впишутся и в лесную локацию.
К тому же они довольно просты и понятны по дизайну. Темные и острые шипы сами по себе говорят: «Не подходи! Уколю!».
Процесс создания колючки
В общем, решено. Я принялась за работу.
Сами шипы я сделала в Блендере. Я решила, что мне будет достаточно четырех видов колючек: стандартная, вытянутая, колючка малыш и дугообразная.
Все четыре вида колючек
Пока моделила подумала, что будет очень классно не просто поставить их в игре, но и сделать анимацию, чтобы при приближении Магнолии они начинали угрожающе шевелится.
Анимация спокойствия им нужна была обязательно, чтобы на фоне движущихся деревьев и травы, статичные колючки не выглядели странно.
Закончив моделинг, я быстро развернула все колючки. Я сразу решила, что материалы возьму от материалов деревьев, которые есть уже в игре.
После этого начала вставлять кости. Скелет у колючек довольно простой. Только с дугообразной колючкой пришлось немного повозиться.
Создание костей
После этого я экспортировала все анимации в движок и проверила, что все работает. Ух. На этом моя работа закончена. Теперь мяч на стороне Алексея.
Он создал триггеры и написал логику для срабатывания анимаций.
Первые тесты
После проверки я снова вернулась в блендер и немного подправила анимации нападения для колючек. Мне хотелось, чтобы они были более угрожающими.
Мне нравится какими эти колючки получились темными и зловещими. Вот только ярко зеленая летняя трава не совсем к ним подходит.
Нужно их пересадить.
Я поменяла цвет листвы и коры у деревьев. А еще поменяла цвет в материале у кустов и травы. Поразительно, как такими простыми средствами можно придать локации совершенно другой облик.
Модельки статуй котов у меня уже были. Я сделала их еще в самом начале. И сейчас добавила к этой точке интереса.
Вот. Теперь колючки выглядят на своем месте!
Работа над ландшафтом
Еще немного работы над локацией, хочу показать, что там за колючками когда-то возможно и была дорога, но сейчас все заросло. И через эти заросли уже не пройти.
Проверяю, как все работает.
Кажется, чего-то не хватает... Точно, звуков!
В интернете я нашла разные звуки рычания монстров и смешала их со звуками треска веток. Теперь при приближении Магнолии колючки еще и шумят.
Итоговая версия, уже со звуком.
Конечно, можно было бы пойти дальше. Ну например, добавить титрами внизу слова Магнолии, что-то на подобии «Я не хочу туда идти мне страшно».
Или принудительно разворачивать модельку Магнолии спиной при приближении к шипам. Но мне бы не хотелось сильно долго задерживаться на этом этапе.
Конечно, для самых смелых игроков, которые не испугаются ни рычания, ни атак монстров, я добавлю прозрачную стену, которая уже будет защищать Магнолию.
Все же это игровая условность.
Я будто бы веду диалог с игроком.
-Не ходи туда
- Это еще почему?
- Там злые колючки растут! Магнолия бы точно туда не пошла.
А на этом всё.
Всем спасибо за чтение, думаю мы с Вами еще обязательно увидимся в моих следующих заметках о разработке.
Студия Black Tangerine выпустила демо-версию своей дебютной адвенчуры.
Трейлер игры:
Вальгалла (да), ученица алхимика Пандоры, живёт вдали от цивилизации вместе со своей наставницей. Пандора страдает от какой-то неясной болезни - на её руке отметина, что сделала руку нерабочей и распространяет гниль по всему организму, из-за чего наставница Вальгаллы вынуждена ежедневно принимать антидот, дабы задержать развитие хвори.
Однажды, вернувшись домой, Вальгалла обнаруживает наставницу истекающей кровью. Пандора, что едва могла говорить, молила убить "...ла" - того, кто стал причиной её недуга. Но на последнем издыхании всё же просит воспитанницу "не открывать ящик Пандоры".
Но, как все уже догадались, ящик был открыт и пути назад не было. А путь лежал на таинственный остров, на котором предстоит достать пару скелетов из шкафов наставницы и узнать, кто же стал причиной её болезни.
Игра представляет собой адвенчуру с 2D-модельками (персонажи анимированы) в 3D-пространстве. Мне графика напомнила детские книги с объёмными иллюстрациями.
Наподобие этого
Но пусть няшная графика не вводит Вас в заблуждение, ибо в игре присутствуют кровь-кишки-распидорасило. Хотя кровь и непривычного для неё малинового цвета.
Управление при помощи клавомыши. Изучаем обстановку, собираем-применяем предметы, общаемся с другими персонажами, ведём расследование. Присутствует вариативность в том, как применить предметы.
Вообще, разработчики обещают большую вариативность прохождения и несколько концовок.
Фоновая музыка неплохая, персонажи не озвучены.
В наличии английские, корейские и японские субтитры (разработчики из Кореи).
Выход полной версии намечен на второй квартал 2024 года.
С Вами была WoeOfMenelaus, приятных выходных!
Одна вакансия, два кандидата. Сможете выбрать лучшего? И так пять раз.
Не так давно (год назад на самом деле) я приобрёл необычную игровую консоль Playdate.
Вот он наш герой поста
Она такая маленькая, жёлтая и имеет крутилку (крэнк или иногда в дословном переводе с испанского кривошип). Ах да, у неё еще экран монохромный. Не чёрно-белый - чёрно-белый экран умеет показывать оттенки серого
Оттенки серого, но не те
- монохромный умеет показывать только чёрное и белое. Точнее, не совсем белое, а что-то что не чёрное.
Пример меню с выбранным пунктом "Настройки". Как видишь, белый не совсем белый
Playdate это неповторимая смесь примитивизма и современных технологий. На первый взгляд можно подумать «ну и кто в такое играет?». Однако я щас без преувеличений скажу, что уже для Playdate сделано более 800 игр. То есть, в отличие от миллиона неизвестных консолей, которые сегодня создаются для тех, кто вспоминает детство за сегой и дэнди, пардон, нинтендой, у Playdate реально есть активное сообщество.
Какую игру я хотел сделать? Примитивные головоломки я отбросил сразу же. Хотелось сделать что-то драйвовое, чтобы прям был экшон как в GTA - машины там, физика, стрельба. Значит, нам нужна машина! Понятное дело, игра будет двухмерная. Если машина, значит, она должна ездить, и желательно не с идиотским видом сверху как в GTA2 (справедливости ради уточню, что я прошёл всю GTA2, и посмотреть это можно на ютубчике), а чтобы была перспектива, чтобы было красиво. Так как никакого 3D не ожидается, а для управления у нас есть крестовина (D-Pad на английском). Значит, нам нужна машинка в восьми направлениях. Дав задачу своей художнице я получил вот такое:
Машина, на которой игрок будет кататься в игруле
"Так, стоп, а какой сеттинг у игрули?" спросишь ты. Сеттинг простой - мы катаемся на внедорожнике вооружённым пулемётом по пустыни в Австралии и, как настоящие любители дикой природы, отстреливаем живность: эму и кенгуру. Справедливости ради уточню, что живность нас тоже пытается грохнуть. То есть, жанр игры "выживальщик" наподобие Vampire Survivors.
Так как у Playdate только два цвета, нам нужно постараться чтобы создать ощущение нахожения в пустыне. Потому сначала сконцентрируемся на реквизите, который нас будет окружать. Конечно же это кактусы, небольшие песчаные насыпи и перекати-поле.
Ах-да, забыл упомянуть: размер экрана у нас 400 на 240 пикселей. То есть, ну очень маленький. Значит, объектов на экране должно быть минимум чтобы понять что происходит.
Перейдём к самому вкусному - к коду.
На Playdate официально можно разрабатывать на двух языках программирования: C и Lua. Так как Lua я не переношу как и все скриптовые языки (я лично за С++ во всех нормальных играх), значит будет сишка. Не сказать, что я фанат сишки, но это лучше луы. А что делать с отсутствием объектно ориентированного программирования? Будем симулировать и выкручиваться по возможности, потому что 10 лет работы на ООП языках (Swift, C++, C#) чётко отформатировали мою голову под объектно-ориентированное мышление.
Первый шаг в написании игры это файл main.c, в котором нет функции int main, зато есть системный колбэк ("обратный вызов" или "звони назад")
int eventHandler(PlaydateAPI* playdate, PDSystemEvent event, uint32_t arg).
Эта функция это единственная прослойка между Playdate и моим кодом. Она вызывается на любой "чих", точнее, событие. Первый аргумент PlaydateAPI* playdate это указатель на непосредственно API операционной системы девайса. PlaydateAPI это структура, которая состоит из структур, которые хранят сишные указатели на большое количество функций (нарисовать, что-то, открыть файл, показать fps и т.д.). Второй аргумент это наш тип "чиха", точнее, события:
Объявление перечисления PDSystemEvent
На третий аргумент arg пока пофиг - он нам не нужен.
Код игры можно воткнуть прям в файл main.c, но я так не хочу. Не потому что это считается зашкварно - то что как считается это вещи очень субъективные, и вряд ли они когда-то меня останавливали от самых сумасшедших вещей в коде. Я вынесу код отдельно потому что я хочу чтобы он был распределён красиво и удобно, модульно, но не слишком. То есть, чтобы лично мне было понятно где что искать, но чтобы не упарываться в оформление структуры ради оформления структуры как это делают Java-разработчики. Потому вся логика катания машины по пустыне будет аккуратно сложена в файл с супербанальным названием Game.
Game будет имитировать класс, он будет создан при получении события о старте игры, и будет удаляться в событии об окончании игры. А указатель на этот объект будет храниться где? Правильно: в статичной памяти.
Окончательный вариант файла main.c
То есть, игра создаётся в событии `Init` (строка 21), потом вызывается у игры функция GameSetup (строка 22) для единоразовых стартовых действий опосля создания (тут можно поспорить, что эти вещи можно сделать в той же функции GameCreate, но спор оставим тем, кто любит спорить вместо написания кода). Далее я прикручиваю вызов функции GameUpdate к тику игры. Напрямую я это сделать не могу так как функция обновления имеет сигнатуру int (*)(void *), а мне нужно int (*)(Game *), потому я создаю функцию-прослойку rawUpdate, которая принимает void *userData, кастит его в указатель на Game и руками вызывает GameUpdate.
Отлично, с мэйном всё понятно. Теперь давай глянем что есть в самом Game. Но сначала позволь проспойлерить и показать что получилось чтобы ты не зевал от кода.
Откатимся назад в прошлое. Сишка кажется нормальным языком, но ровно до того момента, когда тебе нужно работать с динамическими объектами: строками и массивами. Оказывается, что чтобы передать массив в функцию нужно иметь два аргумента: указатель на данные и целое число равное количеству объектов, лежащих по тому самому указателю один за другим в памяти как поезд. Ну либо можно хранить объекты в статичной памяти, там всё проще - объявил статичный массив и пользуйся. Одно но - у статичного массива константный размер, и этот размер должен быть известен в момент компиляции. Чем это чревато? Тем, что если ты объявил массив, скажем, кактусов, размером, скажем, 100 штук, это значит, что в игре 101 и более кактус быть уже не может. И так с любым статичным массивом.
Забавный факт: когда несколько лет назад слили исходники GTA3 и GTA Vice City там динамические объекты (машины, пешеходы, пикапы (броня, спрятанные пакеты, буйства, оружия, деньги)) как раз хранились в статичных массивах. И количество пикапов, например, ограничивалось числом 512. То есть, если в игре устроить заварушку чтобы вокруг валялось много денег, оружия и прочих пикапов в количестве 512, то при появлении нового один старый пикап тут же будет пропадать даже если ему ещё рано пропадать (деньги и выпавшее из врагов оружие пропадают по таймауту).
Вот я так не хочу. А хочу я чтобы у меня был один объект массива без отдельно указателя на данные и отдельно размера, потому что если таскать везде два аргумента представляя что это один, это верный способ свихнуться. А если мне нужно два массива - будет два объекта. А все детали (указатель на данные, размер, прочее) должны быть аккуратно спрятаны внутри, как это сделано в ООП языках. В С++ для таких целей есть std::vector, в Swift - Array, в C# - List. В сишке ничего такого нет, значит надо придумать!
Долго томить не буду, вот что получилось:
Заголовочный файл массива
Заголовочный файл имеет предобъявление структуры Array, которой по факту не существует, и API для создания, взаимодействия и уничтожения массива. Важная деталь: так как это сишка у нас нет деструкторов как в С++/C# или deinit-функций как в Свифте, которые автоматически вызываются когда область видимости массива заканчивается. Значит, нам надо вызывать функцию-деструктор руками. То есть, на каждый вызов ArrayCreate где-то должен быть один вызов ArrayDestroy. А что будет если забыть вызвать ArrayDestroy? Правильно: утечка памяти. Я чувствую себя программистом-дауншифтером. Но я сам так решил: начал танцевать с дьяволом - жди окончание песни.
Теперь давай я покажу тебе реализацию:
Файл Array.c
Тут у нас есть структура ArrayImpl. И мы в функции ArrayCreate создаём именно инстанс структуры ArrayImpl, а не Array (та самая несуществующая структура), однако указатель на созданные данные нагло кастим в указатель на Array. Зачем так делать? Честно скажу, я это подглядел у команды SQLite в исходном коде SQLite. Таким образом мы разделяем интерфейс и прячем реализацию, то есть, делаем её приватной в языке программирования, где приватности нет (я про слово private говорю, которое есть чуть ли не в каждом известном мной объектно-ориентированном языке программирование, за исключением Свифта самой первой версии - там уровни доступа не сразу завезли). Логика в том, что весь API массива принимает указатель на Array, а внутри этот указатель кастуется в указатель на ArrayImpl, который хранит реальные данные нашего массива.
А, кстати, что же хранит ArrayImpl? На последнем скриншоте мы видим, что там не два поля как это бывает у сишного массива, а больше:
1) int itemSize - это размер одного хранимого объекта в байтах. Этот размер нужен чтобы знать сколько байт выделять когда мы пытаемся засунуть в массив один объект (push_back в векторе на C++, append в Свифте у массива и Add в C# у листа). Ты возразишь "но ведь С++ вектор не хранит это поле, значит можно в нашем случае тоже как-то его избежать!". Однако, С++ на самом деле хранит это поле, просто не в виде явного члена класса, а в качестве параметра шаблона: std::vector<T> имеет параметр шаблона T, от которого в любой функции внутри класса std::vector можно вызвать sizeof(T) и получить заветный размер одного объекта. Однако в сишке шаблонов нет. Вот прям совсем нет. Примерно как нет воздуха на Луне. Потому приходится передавать один дополнительный параметр int itemSize, который позволит нам во время жизни массива знать размер одного элемента. Нет, конечно в сишке есть макросы, которые при достаточной сноровке можно использовать как шаблоны, однако я имею аллергию на макросы, так что макросов не будет.
2) Указатель на функцию realloc. Это может выглядеть избыточно, и по факту так оно и есть, однако у PlaydateAPI (помнишь, я тебе в файле main.c показывал указатель на такую структуру?) есть свой указатель на функцию realloc, который, как мне подсказывает мой копчик, равен системному вызову realloc из стандартной библиотеки Си, и который можно вызвать вот так playdateApi->system->realloc. То есть, нам разработчики намекают использовать их realloc вместо системного. Что ж, это нетрудно. А вторая причина - передача функции realloc по указателю позволяет покрыть вызовы этой функции юнит-тестами, то есть, сделать мок или прокси этой функции, и это невероятно удобно. Правда, я до сих пор не покрыл массив юнит-тестами, но когда-нибудь я обязательно это сделаю, честно-честно!
3) void *data это непосредственно указатель на данные. Почему именно указатель на void? Потому что массив по своей природе универсален: он способен хранить и int'ы, и кастомные структуры, а значит нужен указатель какого-то общего типа, что-то вроде object в C# или AnyObject в Свифте. И тут сишка нам щедро предлагает указатель в пустоту. Любой сишный прогер знает, что указатель на void это произвольные данные. В нашем случае это данные массива. Массив может хранить N объектов, а значит в этом состоянии у него указатель data будет указывать на кусок памяти в минимум N * itemSize байт идущих подряд если только N не равен 0. А если массив пустой, то data равен NULL.
4) int capacity это ёмкость данных. Ёмкость равняется количеству объектов, которые умещаются в объём выделенных данных, которые лежат по адресу data.
5) int size это количество реальных объектов, которые лежат по адресу data. "В чём отличие size от capacity?" спросишь ты. Тут логика та же, что и у std::vector в C++ - capacity в некоторых случаях может отличаться от size. Например, если в массиве было 4 объекта, и мы один объект удалили чтобы осталось 3, мы не будет выделять новый участок памяти под 3 элемента, а старый освобождать. Мы просто уменьшим size, но оставим capacity как есть. Это, во-первых, быстрее, чем перераспределять память (особенно если в массиве лежит больше тысячи объектов), во-вторых, если после удаления мы решим снова добавить объект, то память вновь не придётся перераспределять, так как ёмкости массива хватит. Да. безусловно если помимо этого одного мы добавим ещё один, то придётся осуществить перераспределение чтобы выделить больший кусок памяти, но тут мы уже ничего сделать не сможем, кроме как заранее оптимизировать пытаясь предсказать какая ёмкость лучше. Но это уже не ответственность массива - это ответственность того, кто этим массивом пользуется.
Итого я расписал как устроена внутрянка массива. Не будем забывать мою цель - мне нужен аналог std::vector из С++ или Array из Свифта удобный настолько, насколько это возможно в сишке. Так что давай я покажу как устроен API у массива.
1) ArrayCreate (на прошлом скриншоте есть) - эта функция создаёт объект массива. Она принимает itemSize и указатель на функцию realloc. Задача функции - выделить память под ArrayImpl, присвоить все стартовые поля ему и вернуть созданный указатель, но в качестве Array*, а не ArrayImpl*. Количество объектов у только что созданного массива всегда равно нулю. Возможности создавать массив из литерала как в С++ (auto myArray = {1, 2, 3}), C# (var myArray = new int[]{ 1, 2, 3 }), Swift (let myArray = [1, 2, 3]) и даже Objective-C (NSArray *myArray = @[@1, @2, @3]) нет, так как это просто синтаксический сахар над несколькими операциями (создание и заполнение), и негоже такое в сишке иметь.
2) ArrayClear - функция очистки массива.
Исходный код функции ArrayClear
Это не уничтожения массива, а именно опустошение хранилища если количество хранящихся объектов в массиве больше нуля. Название я полностью взял с std::vector::clear из С++. Можно было взять removeAll из Свифта, но к clear я больше привык. Суть функции: мы берём полученный аргумент и кастуем его в указатель на ArrayImpl. Если data у полученного объекта не равна нулю, т.е. если массив непустой, то мы дропаем дату и запоминаем, что capacity и size равны нулю. Ну а если массив и так пустой, то мы не производим никаких операций с памятью.
3) ArrayGetSize - самая простая функция, которая возвращает размер массива.
Исходный код функции ArrayGetSize
Просто кастуем указатель и возвращаем хранящееся значение size.
4) ArrayGetObjectAt - получение объекта. В "нормальных" языках у нас есть оператор "квадратные скобки", а тут у нас сишка, так что любое действие это просто функция.
Исходный код функции ArrayGetObjectAt
Функция возвращает адрес, то есть, указатель на нужный объект по указанному индексу в массиве. Так как храниться может внутри что угодно, то возвращаем мы уже известный нам указатель на void. А задача клиента будет уже скастовать этот указатель в указатель правильного типа: если в массиве лежат int'ы, то надо будет скастовать в int, если float - то во float, если кастомная структура или union - ну ты понел. И тут, понятное дело, можно легко спутать тип потому что мы люди, а люди ошибаются. Как страхуются от таких проблем в других языках? В С++ всё так же шаблонами: если std::vector<T> имеет T равный int, то и operator[] будет возвращать T и только T. В Свифте то же самое, только там не шаблоны, а дженерики - шаблоны на минималках, в C# тоже дженерики. А в сишке мы дауншифтим, смирись с этим и не выпендривайся! Ах да, если индекс переданный в функцию оказался за пределами доступных объектов (меньше нуля или больше либо равен размеру массива), то мы просто вернём нулевой указатель. В троице моих упомянутых выше языков в этом случае бросается исключение, но в сишке исключений нет, да и как по мне без исключений код приятнее, потому что исключения это тот же оператор goto, от которого нас так яростно отучивали 20 лет назад. Потому мы возвращаем NULL. А если индекс валиден, то мы хитрой арифметикой указателей вычисляем правильный адрес и возвращаем его.
5) ArrayGetMutableObjectAt - это копия прошлой функции, но возвращающая неконстантный указатель на объект. Почему это важно выделить в отдельную функцию?
Исходный код функции ArrayGetMutableObjectAt
Язык у нас, конечно, не самого высокого уровня, однако константность в нём есть полноценная, а константность это штука, которая невероятно повышает читаемость кода (я особенно привык объявлять константы вместо переменных во время работы на Свифте, а потом когда в плюсовом проекте везде втыкаю const иногда встречаю возмущенные ревью "ну и нахуа ты везде свой бесполезный const понапихал 🗿"), однако константы почему-то максимально игнорируются программистами на сишке, что я лично не одобряю никак.
6) ArrayPushBack - добавление объекта в массив. Аналог std::vector::push_back из C++, Array.append из Свифта и List.Add их C#.
Исходный код функции ArrayPushBack
Это самая навороченная по логике функция массива. Объект передаёт константным указателем на void. Тут нам сначала нужно проверить умещается ли новый объект в уже имеющуюся ёмкость (capacity). Если умещается, то мы просто копируем его в data со сдвигом равным старому размеру помноженному на размер объекта (itemSize), а количество байт для копирования тоже равны размеру объекта (itemSize). Важно при вызове ArrayPushBack передавать адрес объекта, а не сам объект, а то будет ошибка: ArrayPushBack(myArray, &myValue). Неудобно, согласен, зато универсально, потому что в таком массиве можно хранить и структуры, и базовые типы.
7) ArrayDestroy - последняя на сегодня функция массива. Эта функция, как ты уже знаешь, уничтожает массив, то есть чистит его из памяти. Любой массив рано или поздно окажется тут, где закончится его путешествие по этому бренной жизни, точнее, по материнской плате. Эта функция это Вальгалла всех массивов. В неё мечтает попасть каждый массив, а те, кто не попадают, те остаются болтаться в утекшей памяти.
Исходный код функции ArrayDestroy
Тело функции крайне банально: сначала вызываем ArrayClear чтобы почистить объекты если они есть, а далее дропаем массив из памяти словно он никогда и не существовал.
Заключение
Признаюсь, стилистику имитации объектов я взял в CoreFoundation - это такая сишная либа от Эпла, которая имеет API очень похожий на Objective-C, на основе которого позже появился Swift. Это, кстати, не единственный способ имитации ООП в сишке - ещё я пользовался либой GTK+, но там API немного отличается, в частности, там всё обмазано макросами, а на них у меня аллергия.