А это, друзья, сумчатый муравьед, или намбат – ещё один необычный зверёк из эвкалиптовых и акациевых лесов Австралии. Намбат – товарищ некрупный: длина тела не превышает 27 см, а весит он не более 500 граммов, но, несмотря на скромные размеры зверь он крепенький и невозмутимый.
Сумчатым там или не сумчатым, но муравьедом кого попало не назовут. Как и его плацентарный друг из Южной Америки, намбат приспособлен к потреблению муравьёв и термитов в поистине промышленных масштабах, ежедневно поедая до 20 тыс. насекомых. Правда, термитов он любит больше. Отыскав источник вкусняшек с помощью чрезвычайно острого обоняния, он ловит термитов с помощью своего липкого языка, а затем глотает их целиком или чуть раздавив хитиновые покровы.
Впрочем, если вы сейчас представили себе действия муравьеда южноамериканского, то совершенно напрасно. Лапки у намбата слабенькие и взломать здоровенный и очень прочный термитник ему не под силу. Он может только раскопать рыхлую почву или разломать гнилую древесину острыми когтями, а в таких местах его излюбленная добыча бывает только днём, когда насекомые в поисках корма передвигаются по подземным галереям или под корой деревьев. Вот и приходится бедолаге-намбату, в отличие от других сумчатых, вести дневной образ жизни, синхронизируясь с термитами.
Намбат – зверёк проворный и осторожный, а также великолепно лазает по деревьям. При малейшей опасности он прячется в укрытие – но только если в данный момент не занят поглощением термитов. В процессе трапезы он совершенно не обращает внимания на окружающую обстановку. В такие моменты его можно погладить или даже взять в руки.
Да и сон у намбата очень глубокий. Когда он забирается на ночь в своё укрытие в норе, трухлявом пне или дупле дерева, то разбудить его просто невозможно. Можно подумать, что он впал в анабиоз – вот только очень короткий. Местные жители рассказывают, что вместе с валежником не раз случайно собирали спящих намбатов.
Ну а когда намбат всё же просыпается и понимает, что его поймали, то не кусается и не царапается, а только отрывисто посвистывает и ворчит. Тоже мне, мол, удумали тут – намбатов собирать. К сожалению, эта невозмутимость и способность беспробудно спать сыграли со зверьком злую шутку. На сегодняшний день вид находится под серьезной угрозой исчезновения, страдая, как и другие дикие животные, от хозяйственного освоения земель и пожаров. Но самый серьёзный урон численности намбатов наносят ввезённые на континент хищники, наловчившиеся легко добывать спящих или увлечённо обедающих намбатов. На них охотятся хищные птицы, динго, одичавшие кошки и особенно рыжие лисы, которых в XIX в. завезли в Австралию.
К счастью, намбаты совсем не прочь размножаться в неволе – лишь бы кормили хорошо и выспаться давали, так что австралийцы организуют программы по разведению родных муравьедов. Например, зоопарк города Перта разводит намбатов для выпуска в дикую природу с 1986 года.
В природе же намбаты общественный образ жизни не признают и соглашаются потерпеть рядом представителя своего вида только в период размножения. Но чтобы кого-то потерпеть, этого кого-то сначала нужно ещё отыскать, и эта задача ложится на самцов. В брачный сезон они покидают свои охотничьи угодья и отправляются на поиски дамы сердца, по пути помечая территорию особым мускусным секретом, который у них выделяется специальной железой, расположенной – внезапно! – на груди.
Ну а после спаривания работа самца закончена, дальше дело за будущей мамой. Через 2 недели на свет появляются до 4 крошечных недоразвитых детёнышей, и им бы в этот момент к маме в сумку дозревать, но тут, понимаете ли, засада. У самок сумчатого муравьеда сумок не имеется! Поэтому бедняги просто повисают на сосках, цепляясь за шерсть матери, да так и болтаются около 4 месяцев, пока не вырастут хотя бы до 5 см в длину.
После этого жизнь мамы становится попроще: деток можно оставить в укрытии и целый день охотиться на термитов налегке, ночами возвращаясь домой, чтобы покормить малышню. Ну а ещё через пару месяцев они начнут выходить из норы и учиться самостоятельно питаться термитами под строгим контролем матери. В результате на всё про всё уходит более 9 месяцев – зато какая красота получается!
Приглашаю вас также на свой канал Записки учителя биологии – там ещё больше интересного о живой природе.
Мы с коалой хотим вас удивить и предлагаем 7 любопытных фактов:
1. Папиллярный узор на подушечках пальцев коал уникален, и их отпечатки пальцев похожи на человеческие.
2. Мозг коал очень маленький и гладкий, а 40% черепной коробки занимает жидкость, предохраняющая мозг от сотрясений.
Нет, это не куриная грудка, это правда мозг коалы :)
Соотношение массы головного мозга к массе тела у коал – одно из наименьших среди всех сумчатых: вес головного мозга составляет не более 0,2% веса коалы
3. Коалы облизывают деревья после дождя – так они получают воду в дополнение к влаге, содержащейся в пище.
Да и прохладнее оно - в жару обнимать эвкалипты
4. Коалы являются одними из самых долгоспящих зверей на планете, они спят до 22 часов в сутки, ведь листья эвкалипта низкокалорийны, тут и на 2 часа активности энергии едва хватит.
5. Коалы не так безобидны, как кажется: в ходе борьбы за самку самцы часто нападают друг на друга, нанося увечья.
Вот это махач!
6. Детёныши коал поедают экскременты своей матери, состоящие из полупереваренных листьев эвкалипта – так они получают микроорганизмы, необходимые для пищеварения.
Вы думали я экскременты иллюстрировать буду? Не буду! Просто посмотрите на этого милаху :)
7. Коалы почти всегда молчат и издают звуки (зато какие!) только в брачный период. Да, молчание – это тоже способ сэкономить энергию.
Приглашаю вас также на свой канал Записки учителя биологии – там ещё больше интересного о живой природе.
Я пишу игру на игровую консоль Playdate на чистом C. Игра в жанре "выживальщик" наподобие Vampire Survivors. Так как в чистом C отсутствуют многие современные объектно-ориентированные удобства мне приходится по-всякому изворачиваться чтобы адаптировать свои идеи в код. В этих заметках ты узнаешь из первых уст как создаётся игруля с нуля от идеи до публикации.
Если ты не читал предыдущие главы, то лучше начать с них.
Глава 1 - создание аналога объекта динамического массива для будущих нужд на чистом С;
Глава 2 - программирование внедорожника и объектов пустыни, инициализация и очистка ресурсов игры;
Глава 3 - описание процессинга тика, в частности, обработка пользовательского ввода, а также обновление модели данных.
====================
В этой главе тебя ждут математика за пятый класс, пьяные перекати-поле и обуздание неопределённого поведения.
Итак, большинство людей в мире визуалы. Это значит, что им привычнее всего воспринимать информацию глазами. В прошлых главах я создал целый мир, но какой в этом смысл если это невозможно увидеть? Нет, конечно можно в баре рассказывать про то, какой невероятный код я написал, но собеседник не сможет его увидеть так как у него нет Playdate (ты же помнишь, что я живу в Казахстане? У нас на всю страну три человека имеют Playdate), ну и потому что собеседник бухой в щи, пьяный в зюзю, надрался, под мухой, на рогах, зелёный как снег.
В общем, в чему это я... Наша заветная функция GameDraw... Она рисует игру (внезапно). Напомню, у нас есть машинка (внедорожник или "джип"), перекати-поле, кактусы, насыпи песка и на этом всё.
Начало тела функции GameDraw
Начинаем мы всё с отрисовки машины. На самом деле машину мы не рисуем на верхнем скриншоте, но комментарий на строке 233 утверждает что рисуем. Так уж вышло. Если помнишь в первой главе я тебе рассказывал, что машину мне нарисовала моя художница в восьми вариантах так как мы крестовиной можем указывать 8 направлений как на любой уважающей себя консольке. Все эти 8 картинок хранятся в массиве game->vehicleImage (об это я тоже рассказывал в предыдущих главах), а индекс картинки в этом массиве мы определяем по направлению машины на строке 234. Мы вызываем хитрую функцию GameVehicleImageIndexFromAngle, которая имеет максимально простую логику:
Реализация функции GameVehicleImageIndexFromAngle
Далее нам надо проделать фокусы с картинкой. Так как картинку мы хотим центрировать по позиции машины на карте (а в итоге в экране) нам следует раздобыть размеры картинки. На строке 238 это как раз и происходит. Функция getBitmapData у PlaydateAPI возвращает всякую полезную инфу о картинке, при этом выходные данные указываются аргументами функции в виде указателей. Если передаёшь NULL, значит данные не получишь. Потому последние три аргумента это NULL потому что это данные для сырых байтов картинки, маски и данных картинки (не помню чем это точно отличается от сырых байтов, если честно), а это мне пока не надо, мне надо только ширину и высоту, заверните в пакет, пожалуйста, взболтать, но не смешивать. Значения newVehicleImageWidth и newVehicleImageHeight мне пригодятся чуть позже. Едем дальше.
На строке 241 мы очищаем экран белым цветом функцией fillRect. 1 в конце это белый, а если поставить 0 - будет чёрный. Фон мы перерисовываем на всём экране. Размер экрана 400 на 240 пикселей как я упоминал в прошлых главах.
Далее мы рисуем окружение. Первое это кактусы.
Отрисовка кактусов
Кактусы рисуются относительно позиции машины, то есть, кактусы не центрируются по экрану, а значит нам надо хитрожопо высчитывать их позицию на экране. А ещё важная деталь: заранее условимся, что картинка кактуса будет "крепиться" к позиции кактуса на карте нижней серединкой. То есть, если кактус находится в позиции x = 5; y = 5, то рисовать картинку кактуса надо в позиции x = 5 - width / 2, y = 5 - height, где width и height это ширина и высотка картинки кактуса соответственно. Почему именно нижняя серединка? Потому что мы смотрим как будто в 3D, и мы как будто втыкнули кактус его жопкой в поле, словно канапэшку в пенопласт. Для всего этого мы вытаскиваем размеры картинки кактуса на строке 248 знакомой функцией getBitmapData.
(UPD: на самом деле в коде кактус центрируется, это уже в будущих коммитах будет изменен якорь кактусов, а пока как есть).
Далее мы идём по массиву кактусов циклом, и на строке 252 у нас в итерации есть указатель на очередной кактус, константный указатель, потому что при прорисовке мы не намерены менять состояние кактуса. Всё это делается ради вызова функции drawBitmap на строке 259. Именно эта функция рисует картиночку на Playdate, и именно эта функция будет главной героиней в этой главе. Функция drawBitmap простая как автомат Калашникова. Она принимает 4 аргумента: непосредственно картиночка для отрисовки, координата x, координата y и ещё enum'чик указывающий хотим ли вы нарисовать картиночку развёрнутой по какой-либо оси (нам это не понадобится, но если понадобится я обязательно тебе сообщу, зуб даю, век воли не видать). Координаты x и y указываются на экране девайса. PlaydateAPI не в курсе про мою сцену и объекты на ней - это всё мои личные абстракции, которые я придумал чтобы лучше воспринимать строительство игры после опыта со всякими Unity, Godot и cocos2d-x.
На строке 256 есть cactusRect - это прямоугольник отрисовки кактуса в координатах экрана. Если этот прямоугольник не пересекается с прямоугольником экрана, то drawBitmap мы не вызываем. На самом деле эта проверка избыточна потому что операционная система при вызове drawBitmap тоже делает такую же проверку внутри. Но на тот момент я решил, что так надо. Пересечение проверяется функцией RectIsOutOfBounds
Тело функции RectIsOutOfBounds
С кактусами всё понятно? Как не всё? А что не понятно с позицией? Ладно, объясняю. У кактуса есть позиция, её мы вытаскиваем в удобненькую константу на строке 253. А на следующих двух строках мы вычисляем x и y - координаты кактуса в координатах экрана. Для этого преобразования используем простую линейную функцию: берём game->cameraOffset со знаком минус, вычитаем половину размера картинка кактуса, прибавляем половину размера экрана и непосредственно позицию кактуса в поле игры, посолить, поперчить на глаз (всегда фигел с выражения "поперчить на глаз" - это ведь для глаза крайне неприятно, хотя я ни разу не пробовал). Почему именно такая формула? Блин, а можно я не буду рассказывать? Ну просто мне лень. Спасибо, друг!
Теперь рисуем кучки песка.
Отрисовка песка
Тут всё ровно то же самое, что и у отрисовки кактуса, только вместо массива кактусов game->cactuses мы итерируемся по массиву песка game->sands. И картиночка у нас game->sandImage, а не game->cactusImage. Вообще некоторые из вас тут возразят "зачем повторять код, в ООП это легко делается одним циклом, а если ECS использовать, то вообще можно в космос улететь". Да. в ООП это занимает меньше кода, но это чуть увеличивает время рантайма из-за виртуальных вызовов (да, в ECS тоже). Не то чтобы очень много времени, это экономия на спичках, и эта экономия не является моей целью, но мы же в сишке, тут у нас нет виртуальных функций. Можно наворотить указатели на функцию, и как-то в итоге сэмулировать виртуальную таблицу, но это всё равно будет не то потому что указатель this не будет работать из коробки.
А что у нас далее? Далее идёт отрисовка перекати-поле. И тут не всё так банально как у кактусов и куч песка. У перекати-поля есть тень, а сам объект подпрыгивает по синусоиде как описывалось в прошлой главе. Давай я ещё раз покажу видосик как оно всё выглядит.
Тут у нас два цикла вместо одного. Это потому что сначала мы рисуем тень, а потом уже саму тушку перекати-поля. И заметь, что сначала рисуются все тени, а потом уже все тушки. Можно сделать всё одним циклом и рисовать сначала тень, потом тушки каждого объекта по очереди, и я так и сделал в начале, но в таком случае получается так, что при пересечении друг с другом разные перекати-поле могут иметь тень поверх тушки. То есть, тень второго в цикле перекати-поля рисуется после тушки первого, и если их позиции окажутся рядом на карте, то визуально получится, что тень второго как бы "выше" тушки первого, что в реальной жизни невозможно потому что в жизни тень всегда рисуется на поверхности, на которую эта тень падает (в нашем случае это плоскость земли (нет, я не фанат плоской земли)). В общем, если делать один цикл, то выглядеть всё будет обсосно. Насколько обсосно? Давай покажу гифкой:
Тень поверх тушки
Вот поэтому сначала рисуются все тени перекати-поля, а потом все тушки. Более того, если в будущем у других объектов тоже будут тени, то они тоже должны рисоваться до отрисовки тушек всех объектов. Такая вот логика, и мы её заложники независимо от платформы.
Первый цикл банальный и привычный:
получаем константный указатель на объект Tumbleweed (строка 295);
достаём его позицию в отдельную константу чисто для удобства (строка 296);
вычисляем x и y тени для отрисовки на экране (строки 297 и 298) - позиция объекта Tumbleweed это на самом деле позиция центра его тени;
если получившийся прямоугольник хотя бы одним пикселем наслаивается на экран, то рисуем (строка 302).
Интересности начинаются во втором цикле. Во-первых, мы повторяем первые строки первого цикла во втором цикле (строки 308-311) и я ничуть не сожалею об этом. В смысле, такой код неверен в академическом смысле потому что повторение кода это фу-фу-фу, надо повторяющийся код вынести в отдельную функцию, вызывать эту функцию из разных точек, покрыть её юнит-тестами, SOLID, отжайл, митинги-скрам-мастера, чистый код, ретроспектива, планнинг покер и Джон Кармак. Но мне как-то поровну в данном случае потому что тут мы ничего не теряем в рантайме, а повторяющийся код суперпростой, да и у меня нет цели сделать академически верный код (вообще это плохая цель делать академически верный код потому что практически такой код никому и никогда не нужон). Мне нужно сделать код, который можно написать в разумные сроки, потом без трудностей прочесть, и чтобы этот код чётко исполнял свои цели. Как видишь, академической верности в этом списке нет. В общем, повторяю я код, не учитесь у меня красивому коду, детишки.
Во-вторых, я проверяю что tumbleweedFrameIndex находится в пределах от [0; 4) на строке 315. Эта константа это тикающий индекс кадра объекта перекати-поля. Если индекс привести в целому числу (а то он-то сам по себе float), то это будет индекс картинки перекати-поля в массиве картинок используемых для отрисовки. Всего их 4 штуки, как я ранее упоминал. И tumbleweed->frameIndex тикает в секции обновления данных (описывалось в третьей главе), и там же проверяется на выход их границ, но я тут всё равно его проверяю. Зачем? Просто для верности. Так как на строке 319 я лезу в массив по этому индексу мне надо быть уверенным, что индекс валиден. Потому что если индекс будет невалиден, а я всё равно обращусь к массиву по такому индексу, то я не получу исключение как C#, Swift или даже C++ (std::vector::at кидает исключение), а просто получу какое-то значение, которое не будет представлять ничего вразумительного. Такое поведение называют UB или undefined behavior - неопределённое поведение. Конкретно в данном случае понятно что будет - я просто чуть выйду за пределы массива, получу реальные данные приведённые к указателю на картинку, и потом когда я её буду пытаться отрисовать на строке 326 игруля будет бурагозить: либо отрисует полную ерудну (видал такое), либо операционная система грохнет процесс потому процесс будет пытаться залезть в чужой кусок памяти, либо ещё какая дичь может случиться.
А давай не отходя от кассы так и сделаем! Смотри: на строке 319 мы обращаемся к массиву по максимально правильному кошерному проверенному индексу. Но мы с тобой устроим моему коду небольшой саботаж! Давай я вместо
то есть, сымулируем выход индекса за границы на единицу и посмотрим что получится. Итак, код саботировали, компилируем, запускаем (звук запуска ракеты, Илон Маск радостный смотрит в небо ладошками обхватив свою голову, телеканал Хабар ведёт прямую трансляцию, а меня выписывают из программистов за намеренное UB в коде).
(Музыка из "Секретных материалов")
И внезапно мы получаем очень странную картину: кадр тушки перекати-поля иногда превращается в свою же тень отображая неправдоподобную ситуацию - двойную тень. Представь в жизни такое: идёт человек, отбрасывает тень, и иногда вместо самого человека в воздухе висит ещё одна его тень. Почему так получилось? Почему неопределённое поведение не уронило игру вместо этого? Причина в том, что сразу после массива в памяти лежит как раз картинка тени. А порядок полей в структуре в сишке гарантируется. Помнишь, во второй главе я показывал структуру Game? Там на строках 20 и 21 как раз расположились массив кадров тушки перекати-поля и его тень. Массив имеет 4 картинки, то есть, это как если бы я просто объявил 4 поля с картинками кадров подряд - в памяти оно бы лежало так же. И следом лежит картинка тени, что схематично идентично если бы вместо массива на 4 и одной картинки был массив на 5. То есть, вместо индексов [0; 4) мы используем индексы [1; 5), и последний индекс равный 4 как раз попадает в тень. Да, это тебе не C# который бы плевался исключениями. У нас сишка с контролируемым неопределённым поведением, мазафака!
Раз такая пьянка, я позволю себе отвлечься и рассказать обалденную историю из моего опыта программирования на Objective-C, которая произошла в бородатые времена когда ещё не существовало Свифта, и вся нативная iOS-разработка велась как раз на Objective-C. И вот я пишу код, у меня класс на Objective-C, у него тоже есть поле статичный массив из 20 объектов, а объекты в Objective-C в принципе хранятся как сишные указатели всегда. Объекты в этом массиве наследуют один протокол (интерфейс в C# и Java и абстрактный класс без полей в C++), и у меня есть индекс в виде int'а, по этому индексу я лезу в массив и вызываю протокольные функции. Чуешь чем пахнет? Я, значит, написал весь этот код, запускаю прилажку на своём айфончике, и в определённый момент получаю исключение говорящее, что я вызываю протокольную функцию у класса (!) в котором все эти поля хранятся, то есть, как будто бы эта функция статичная, хотя я точно не вызываю статичные функции нигде - я проверил свой код несколько раз. Однако при запуске я стабильно получаю ошибку, что вызываю протокольную функцию у самого класса, в котором лежит указанный мной массивчик, а реализации этой функции у класса нет.
Адепты Objective-C скорее всего уже имеют ответ. А произошло вот что. Я же обращаюсь к массиву по индексу, который храню в том же классе. И в роковой момент, когда бросалось исключение, этот индекс был равен -1. Это означает, что при доступе к массиву (а массив сишный, не обжэктивсишный) мы вышли на одно значение назад. В примере выше с перекати-полем я вышел на одно значение вперёд, потому зацепил картинку, которая лежит в памяти следом, точнее, указатель на картинку. Тут в Objective-C я вышел за пределы массива назад. Что это значит? То же самое, что и при выходе вперёд, только надо смотреть на то, что лежит в памяти до массива. Осознав это в моменте я пошёл смотреть на поля класса. Но вот в чём незадача: массив лежит самым первым полем, до него полей у класса нет. Почему тогда вызов протокольной функции распознаётся как будто статичный? И в этот момент мне пришло осознание. В Objective-C объекты это тоже структуры под капотом, но у каждого объекта перед его полями лежит один особый указатель. Это указатель на его класс-объект. Класс-объект это аналог виртуальной таблицы в С++, но на стероидах, потому что он хранит всю информацию о публичных функциях в таком формате, который позволяет проитерироваться по функциям и свойствам класса, и даже добавить новые прямо в рантайме словно мы пишем на JavaScript'е. И ещё важная деталь про Objective-C - вызов функций членов класса в Objective-C это не то же самое, что и в С++, где ты просто как в сишке получаешь адрес функции, подставляешь аргументы и поехали. В Objective-C ты отправляешь сообщение объекту - это более высокоуровневая операция. И ты можешь послать любое сообщение любому объекту с любыми аргументами. Это в С++, C#, Java и Свифте если ты попытаешься у класса вызвать функцию, которой в нём нет, ты получишь ошибку компиляции. А в Objective-C всё скомпилится, но будет ошибка в рантайме говорящая, что данный класс не может ответить на такое сообщение. Изначальное исключение как раз было об этом: что класс не имеет вот такую статичную функцию. Я для прикола взял и реализовал такую статичную функцию у класса и поставил бряку (breakpoint, точка остановки в дебаге) в ней, и тут-то я всё понял окончательно. Из-за индекса равного -1 мы смещаемся от массива, который по совместительству является самым полем класса, на класс-объект этого объекта, любые сообщения посланные объектам-классам считаются вызовами статичных функций, реализации статичной протокольной функции у меня не было (хотя в конце я её добавил исключительно ради эксперимента), и в итоге мы упали с исключением. Вот до чего доводит неопределённое поведение!
Окэй, вернёмся к перекати-полю. Вообще изначально я тебе рассказывал про особенность второго цикла в отрисовке перекати-полей. Первое, что я обозначил, это повторение кода. Второе - проверка tumbleweedFrameIndex чтобы он не вышел за пределы. Думаю, теперь тебе мои намерения проверки индекса стали намного яснее. Я не утверждаю, что так должен делать каждый программист, я лишь поясняю почему я мыслю таким образом, а делать тебе так же в своём коде или нет - это уже тебе решать. В третьих, интересные штуки происходят на строке 325. Мы пересчитываем значение переменной y для отрисовки тушки перекати-поля. x мы оставляем как есть потому что ширина поля и ширина тушки одинаковые. А y должен вилять вверх-вниз по синусиоде как я показывал на графике в третьей главе. Для этого у каждого объекта перекати-поля есть поле tumbleweed->jumpAngle, уверен, ты помнишь об этом. Это "вращающееся" float значение, от которого мы берём синус (на самом деле косинус (строка 323), но синус это косинус со смещением, так что всё норм). На строке 323 как раз происходит взятие модуля от косинуса - то, о чём я рассказывал в конце прошлой главы. А на строке 325 мы умножаем это значение на 13 и рисуем чуть выше от тени (на 20 пикселей, внимание на константу spaceBetweenTumbleweedAndShadow на строке 322). Чтобы лучше показать важность коэффициента 13 давай мы его немного изменим - в два раза и в десять раз - и глянем что у нас получилось.
Как оно было бы, если были бы другие цифры
Вот потому там 13. В конце цикла мы просто рисуем наконец-таки саму тушку перекати-поля уже известной функцией drawBitmap.
Ну и на этом всё. Нет, конечно ещё в конце рисуется машина. Но там всё супербанально, что я даже не вижу смысла показывать это: вычисляем по известной формуле координаты x и y и рисуем newVehicleImage, которую мы высчитали в самом начале функции GameDraw, функцией drawBitmap.
Ладно, так и быть, давай я расскажу как высчитываются координаты. В корне всего лежит линейная функция. Давай глянем ещё раз на вычисление в коде у перекати-поля
int x = -game->cameraOffset.x - tumbleweedShadowImageSize.x / 2 + screenWidth / 2 + tumbleweedPosition.x; int y = -game->cameraOffset.y - tumbleweedShadowImageSize.y / 2 + screenHeight / 2 + tumbleweedPosition.y;
и у кактусов
const int x = -game->cameraOffset.x - cactusImageWidth / 2 + screenWidth / 2 + cactusPosition.x; const int y = -game->cameraOffset.y - cactusImageHeight / 2 + screenHeight / 2 + cactusPosition.y;
В обеих формулах виден паттерн, который можно записать вот так:
Это и есть наша линейная функция. Как мы к ней пришли? Давай попробуем придумать её с нуля, это занимательное занятие.
Если мы допустим, что формула на самом деле вот такая
screenPosition = objectPosition
то если кактус будет иметь позицию {5; 5}, то и рисоваться он будет в позиции {5; 5} на экране всегда независимо от позиции машины. Это заведомо неверная формула, но с этого мы начинаем. Более того, в позиции {5; 5} будет верхний правый угол картинки кактуса, что не совсем то что мы хотим - мы хотим чтобы картинка кактуса была центрирована относительно той самой точки. Для центрирования надо от исходной позиции отнять половину размера. Таким образом, формула превратилась в
screenPosition = objectPosition - imageSize / 2
Теперь давай прикинем камеру. Камера в теории может летать над миром как пожелает. И если, скажем, камера отъедет от нулевой позиции влево на 5 пикселей, то есть cameraOffset будет равен {-5; 0}, то кактус находящийся в позиции {5; 5} должен будет нарисоваться наоборот правее в позиции {10; 5} минус половина размера его картинки. Если камера отъедет на {-10; 0}, то кактус переедет на {15; 5} - ещё правее на экране. То есть, нам для вычисления позиции объекта на экране надо отнимать позицию камеры. Именно таким образом формула превратилась в
Всё хорошо, вот только камера не центрирована на экране, а лево-верхнирована, то есть, "приклеена" не к центру экрана, а к верхнему левому углу потому что это нулевой угол, то есть, угол по-умолчанию, с которого начинается жизнь в координатной плоскости. Чтобы камеру центрировать надо её сместить на половинку размера экрана. В итоге получаем нашу линейную функцию
Достаточно легко на самом деле, и это, по идее, курс математики за пятый класс - линейные функции на координатной плоскости. Тот редкий случай когда школьная программа пригодилась мне во взрослой жизни!
На этом с отрисовкой у нас всё. В следующих главах тебя ждёт первое подобие виртуальной таблицы, зарождение фрэймворка и логика ускорения машины. А пока можешь поддержать меня на патреоне и бусти чтобы ускорить выход следующих глав и новых игр.
Предобзор к Гран-При Австралии Формулы-1 (31.03.2023-02.04.2023)
История официальных взаимоотношений Зеленого континента и Формулы-1 насчитывает уже практически 50 лет. В далеком 1985 году «Большой цирк» впервые приехал на экзотические по тем временам земли кенгуру и коал, где солнечным ноябрьским днем состоялся завершающий этап 36-го чемпионата мира (справедливости ради стоит сказать, что внезачетные гонки в Австралии появились в соревновательном графике гонщиков раньше, и их победителями становились Джеки Стюарт, Джим Кларк, Брюс МакЛарен и другие известные пилоты). С тех пор этап в Аделаиде прочно вошел в календарь соревнований и на протяжении 11 лет неизменно завершал гоночный сезон. Квинтэссенцией сражений у ипподрома «Виктория-Парк» стала гонка 1994 года, схлестнувшая в борьбе за титул Михаэля Шумахера и Дэймона Хилла, когда лидировавший Benetton немца после ошибки своего наездника хитро довернул в Williams британца, вынудив последнего сойти, оставив чемпионский титул Михаэлю.
Старт 1995-го года стал последним для самобытной городской трассы, после чего Формула-1 дружно перебралась на 650 километров юго-восточнее, в Мельбурн. Видя коммерческий успех этапа в Аделаиде, власти столицы штата Виктория пораскинули мозгами и сделали руководству серии предложение, от которого то не смогло отказаться. Данное решение вызвало серию протестов защитников природы, опасавшихся за судьбу городского парка. В споре администрации и правозащитников ожидаемо победили первые, и Австралия, не успев перевернуть календарь 1996-го года, переехала с конца чемпионата на его начало – уже 8 марта рев гоночных двигателей раздался у вод мельбурнского залива Порт-Филипп. Однако перенос этапа несколько ударил по популярности серии в Австралии, первая гонка на новом треке собрала почти на 20% меньшую аудиторию, чем в Аделаиде.
Уже через пару лет к новому автодрому все попривыкли, и в Альберт-парке всегда стояла дружественная и благодушная атмосфера – болельщики за зиму успевали соскучиться по гонкам, поэтому первый этап становился большим праздником. Но для команд, едва успевших подготовить новые болиды, трасса в Мельбурне добавляла головной боли – в течение года автодром не использовался для других стартов, и на грязном, пыльном, кочковатом асфальте гонщики частенько корректировали облик только что созданных боевых «коней» об окружающие трек стены, чем доставляли механикам незапланированную конструкторскую практику. Кроме того, среднескоростная трасса с обилием виражей и коротких прямых крайне требовательна к настройкам – требуется найти компромисс между скоростью и прижимной силой, уделив особое внимание стабильности на выходе из поворотов.
Лучше всех справиться со своенравной австралийской трассой пока удается Ferrari – красные болиды девять раз первыми увидели клетчатый флаг, а лидером по победам остается Михаэль Шумахер – именно он был за рулем машины с черным жеребцом на эмблеме в 4 случаях из 9. Из нынешних пилотов пока даже близко никто не подобрался к немцу – ближайшим преследователем является Льюис Хэмилтон, праздновавший успех в 2008 и 2015 годах, а лучшим результатом действующего чемпиона мира Макса Ферстаппена является 3-е место в 2019 году. Российским болельщикам арена первой гонки сезона подарила настоящий праздник в 2011 году, когда на призовой подиум взошел Виталий Петров.
Хочется провести параллели с прошлым годом, когда Гран-При Австралии кончился полным доминированием Ferrari и утверждениями, что сезон точно останется за «красными», а в итоге все перевернулось с ног на голову. Но нынешняя форма Red Bull не оставляет ни единого шанса своим соперникам, и при прочих равных австрийцы не упустят своего в ближайшие выходные. Тем не менее, трасса в Мельбурне имеет свой неповторимый характер, не прощающий самоуверенности и расслабленности. К кому фортуна повернется лицом, а к кому – кенгуриной сумкой, узнаем уже в текущие выходные.
Доброго времени суток, друзья! В очередной раз увидев эту гифку в коментах к посту о футболе, подумал, а какого собственно черта? Она же заслуживает поста о себе на Пикабу! О ком это я? Да, о той самой плюющей кровью на газон девчонке.
Итак, наша героиня - Джорджия Пейдж, австралийская регбистка (играет в регби, не в этот ваш богомерзкий футбол - хотел написать это зачеркнутым шрифтом, но не нашел как))). Родилась 21 марта 1995 года в Виндзоре, Австралия, с детства успела позаниматься всем: лёгкой атлетикой, теннисом, баскетболом, короче универсальный солдат) Пока в возрасте 17 лет, не отправилась на просмотр в Сидней, где тренер, имевший связи в Регбийном союзе США, предложил ей попробовать себя в регби-7 (для тех кто не знает, это как регби-15, только по 7 человек в команде, 2 тайма по 7 минут, но упахаться успеваешь будь здоров). Джорджия поступила в Институт Линденвуда (Сент-Луис, штат Миссури), выиграв спортивную стипендию на 5 лет.
И вот, на дворе 2014 год, дебют в регби-7, против команды Университета Нотр-Дам. Джорджи идет в первый за матч захват, разбивает нос до кровавых соплей, встаёт, несмотря на обильное кровотечение, занимает свою позицию, дает сопернице еще один контакт, пока наконец судья не свистит, и не просит покинуть поле. Медобследование после матча установило, что Джорджи продолжила играть, получив перелом носа. Именно этот момент и разлетается в интернете, под названием "Регбийная богиня войны".
На следующее утро, Джорджия проснулась знаменитой, с тысячей запросов в друзья в соцсетях, и став настоящим примером отваги и регбийного духа.
"Увидев название видео, просто расхохоталась. Вовсе не думала ни о чём кроме команды в тот момент, очень не хотелось подводить товарищей. Надеюсь, этот случай только с хорошей стороны осветит регби и сделает его ещё популярней. Было бы здорово, если после такого ролика количество девушек в спорте увеличится."
В 2015 году Пейдж отчислилась из университета Линденвуда, узнав, что её степень бакалавра не будет признана в системе образования Австралии, и была вынуждена вернуться на родину. По возвращению, в первом же турнире по регби-7, сломала ногу и пропустила, до 10 недель. После восстановления Пейдж спустя некоторое время получила ещё одну травму — разрыв мениска, вследствие чего ей потребовалось хирургическое вмешательство. Востановившись, продолжает играть в регби, окончила университет по специальности ""врач-остеопат".
Да, конечно ворваться на высочайший уровень, и пробиться в сборную Австралии по регби для Джорджи уже наверное очень тяжело, вследствии травм и высочайшей конкуренции, но она навсегда будет той девчонкой, что будут постить в каждой футбольной теме, как пример стойкости, любви к игре, и наличию яиц, несмотря на пол) Всем регби!!! 🏉
Римская империя. Гладиаторские бои. Народ требует хлеба и зрелищ. В центр амфитеатра под овации толпы выходит ретиарий — гладиатор с нетипичным обмундированием. Его оружие — это трезубец и сеть. От правильного броска пут зависела их жизнь. И пускай Римская империя развалилась, гладиаторы-то остались! Встречайте на арене Книги животных: восьмилапого воина с перевёрнутого континента — паука-гладиатора.
Неточная, но очень пафосная реплика древнеримского гладиатора с сетью.
Эта животина и по сей день выживает при помощи тех же техник, что и древние воины. Конечно, паук-гладиатор не выходит на ровный песок, чтобы сразиться с очередной мухой или цикадой. Но и восточные леса Австралии не очень походят на ристалище. Опасность поджидает там на каждом шагу, и всем глубоко наплевать на правила честного боя.
Интересно, он именно поэтому такой злой?
Наш герой, в отличие от атлетичных римских воителей, не отличается формами. Животина вырастает всего до 2,5 сантиметров. Поэтому гладиатор не стремится вступить в открытый бой, он берёт своё хитростью. Скрывшись под покровом ночи, членистоногий гладиатор устраивает засаду: располагается на ветвях, прядёт паутину и натягивает её между передними лапками
Готовсь...
Ну а дальше остаётся только застыть и ждать. Паук-гладиатор в засаде — жуткое зрелище. Длинные лапы непропорционально большие по сравнению крохотным тельцем. А бездонные чёрные глаза без отдыха сканируют пространство на предмет вероятной добычи.
Цельсь...
Паук будет вознаграждён за своё терпение, стоит лишь жертве приблизиться в радиус атаки. Один рывок, и квадратная сеть накинута, букашка даже не успела оказать сопротивление. Остальное — дело техники: добычу замаринуют в убийственной дозе яда и укутают в плотный кокон. Охота удалась, он сможет прожить ещё один день.
Пли!
Если наш герой умудрится выживать достаточно долго, до осени, то он открывает в себе новые, незнакомые чувства. Кроме желания убить всех вокруг, у него просыпается охота пересечься с гладиатором противоположного пола. Такая встреча грозит смертью для самца, но не потому что самка съедает своего возлюбленного. Просто он одноразовый и погибнет после первого акта любви.
Так, в смысле помирать? А если я не хочу?
Ну а самке предстоит тяжёлая работа. Из паутины коричневого цвета она выплетает уютные коконы. Колыбельные малышей чем-то напоминают шарики Nesquik. Правда, наполнен такой «завтрак» будет не питательными микроэлементами, как нам утверждает реклама, а сотней-другой яиц. Сделав кладку, самка уйдёт из жизни в след за самцом, непобеждённой. Молодые паучата покинут свои шёлковые бастионы лишь следующей весной. И сразу же начнут наводить хаос на близлежащие территории.
Нет, это не апельсинка, это самая ценная вещь в жизни паучихи — её дети.
Моя оценка пауку гладиатору: 8 успешных бросков сетей из 10. Животина невероятно точна, и его коллегам из Рима стоило бы поучиться такому профессионализму. К сожалению, возможности для этого не представится, людей-гладиаторов больше не существует. Зато пауки живы, здоровы и вымирать не планируют!