Делаю игрулю на Playdate на чистом C. Глава 3
Я пишу игру на игровую консоль 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, которая весь описываемый мной тут балаган отрисовывает чтобы игрок видел что происходит, иначе зачем всё это?
Индия ч.7. Кхаджурахо
Кхаджурахо, о котором я абсолютно ничего не знала до момента прибытия в него, оказался знаменит прекрасно сохранившимися храмами со сценами из Камасутры.
Это очень приятный городок. Здесь много белых туристов, из разных стран. Местное население к ним привыкло и чувствуешь здесь себя относительно комфортно. Тем не менее, это не отменяет обязанности белого туриста фоткаться со всеми местными.
Табличка на въезде в город
Город по индийским меркам чистый.
Утром я пошла в главный комплекс храмов, вход в который платный, 600 рупий. Билет нужно купить онлайн, русские карты не проходят. Хозяин гостиницы порывался купить мне билет, но я попросила приятеля с казахской картой.
Офигела от комплекса. Здесь стриженные газоны, клумбы с цветами и кафе, в котором есть почти настоящий капучино. Первый раз встречаю такое в Индии, до этого было все такое, грязноватенькое.
Эту лепнину можно часами рассматривать. Она потрясающая. Это та Индия, которую я ожидала увидеть. Из рассказов про Маугли. Я хожу, трогаю все и разглядываю. Как они могли это сделать?! Спойлер - я потом ещё доехала до пещер Эллоры и Аджанты и там охренела от упорства древних индусов окончательно.
И конечно сцены из камасутры. Такие затейники были древние индусы.
Гиды подсвечивают лазерными указками в самые развратные сцены, индусы-туристы краснеют и хихикают, белые туристы разглядывают с интересом, все счастливы.
Отмахиваюсь от желающих сделать со мной селфи. Но в один момент они меня облепляют, загоняют в угол и мне приходится делать с ними 100500 фото. Один парень выкладывает наше совместное фото в инсту и подписывает типа - "спасибо за прекрасные моменты, проведенные вместе". Ржу. Я даже не знаю его имени 😂😂😂 до сих пор на меня там подписан.
Они достали меня со своими селфи. Я хочу ходить и разглядывать лепнину, а не вот это вот все. Сфоткаешься с одним - тут же выстраивается очередь из других желающих. Сажусь на лавочку, полюбоваться огромным деревом, просто садятся рядом и начинают фоткаться. Чувствую себя обезьянкой на набережной Анапы в разгар туристического сезона.
Вечером хозяин отеля сообщает мне, что несколько постояльцев едут завтра на экскурсию по остальным храмам и предлагает мне присоединиться, соглашаюсь.
Вход в другие храмы бесплатный, но они в отдалении друг от друга, и чтобы их посмотреть, лучше взять туктук. На троих у нас получилось 450рупий/человека за водителя и гида.
Всего в Кхаджурахо 82 храма было. Сейчас осталось 23, остальные разрушены.
Гид рассказывает историю храмов и символы, но я почти ничего не запоминаю. Так ли это важно, это просто невероятно красиво. Можно бесконечно смотреть.
Конкурс для мемоделов: с вас мем — с нас приз
Конкурс мемов объявляется открытым!
Выкручивайте остроумие на максимум и придумайте надпись для стикера из шаблонов ниже. Лучшие идеи войдут в стикерпак, а их авторы получат полугодовую подписку на сервис «Пакет».
Кто сделал и отправил мемас на конкурс — молодец! Результаты конкурса мы объявим уже 3 мая, поделимся лучшими шутками по мнению жюри и ссылкой на стикерпак в телеграме. Полные правила конкурса.
А пока предлагаем посмотреть видео, из которых мы сделали шаблоны для мемов. В главной роли Валентин Выгодный и «Пакет» от Х5 — сервис для выгодных покупок в «Пятёрочке» и «Перекрёстке».
Реклама ООО «Корпоративный центр ИКС 5», ИНН: 7728632689
Macuco Safari. Национальный парк Игуасу, Бразилия
Во время нашего пребывания в бразильском Национальном парке Игуасу предлагалось дополнительное развлечение - сафари на лодке по нижнему течению реки вверх к водопадам Игуасу. А почему бы и нет?
На общей территории парка нашли отдельно стоящий офис этой компании
и благополучно купили три билета (цена 198 бразильских реалов - примерно 1700 рублей) на ближайший отъезд.
Мы взяли комплексный билет, включающий поездку на электромобилю по сельве и непосредственно сафари на лодке.
На время ожидания отправления мы расположились в зоне ожидания с магазинчиком сувениров, да и просто в тени (с учетом жаркой погоды)
Ожидали около получаса, затем нас позвали на посадку на электромобиль
Посадочные места на электромобиле расположены на немного возвышенной прицепной платформе, которую тянет тягач
Вместе с нами ехали около 10 человек, в основном европейцы и бразильцы с аргентинцами, русские - только мы.
В головной части расположился гид-экскурсовод, рассказывавший о сельве, по которой мы проезжаем (реликтовый лес, где никогда не велась хозяйственная деятельность, сохранившийся с давних времен в первозданном виде).
Лес просто удивительный - тишина, нет следов человека (передвижение людей там запрещено, только на электромобиле в рамках экскурсионной программы), птицы. А воздух просто чистейший, ощущение спокойствия и релакс - вот что нужно человеку из мегаполиса!
Экскурсовод рассказывал информацию на трех языках параллельно - сначала на португальском, затем на испанском и потом на английском. Английский хороший, на уточняющие вопросы отвечал охотно и дружелюбно.
Поездка по сельве длилась около получаса.
Прибыв на конечную точку маршрута, мы выгрузились из электромобиля и далее пошли пешком мимо каменных скал по узкой тропе
Спуск надежный и вполне безопасный, гид контролировал всех, в том числе возрастных туристов.
Затем небольшая фотосессия около небольшого водопада в живописном местечке
Далее - на станцию посадки в лодку на причале уже около реки, но на вершине берега.
Прослушав предварительный инструктаж, переодевшись в комфортных раздевалках и оставив лишние вещи в персональных локерах, отправились получать спасательные жилеты (нашлись на любой тип фигуры, в том числе детские).
Поскольку все это происходило на верхней части берега, то потребовалось обеспечить спуск группы к воде для посадки в лодку.
Для этого всех погрузили в небольшую вагонетку и по рельсам (на фото) спустили к причалу.
Спустившись на причал, погрузились в лодку.
Лодка современная, вместительная двухмоторная на 24 места
Сиденья очень удобные, с крепкой спинкой, что оказалось весьма к месту с учетом виражей и качки, добавляя дополнительный упор для удержания себя в лодке:)
Лодкой управляет специально обученный рулевой, на носу лодки располагался гид, развлекающий по ходу поездки и рассказывающий все о водопадах и нашем маршруте.
Отмечу, что на всем протяжении поездки осуществлялась фото- и видеосъемка на GoPro, так что особой необходимости брать с собой персональную аппаратуру для съемки не было, хотя все взяли (в водозащитных чехлах, естественно).
После небольшого инструктажа по технике безопасности отправляемся в путь.
Маршрут начинается, как я уже упоминал, в нижнем течении реки, то есть после водопадов, лодка идет против течения в направлении каскада водопадов Игуасу
Вокруг - абсолютное великолепие тропической природы, тишина, и только далеко впереди еле видны водопады...
По пути проходим небольшие пороги, рулевой не просто ведет лодку, но и добавляет некоторого экстрима в поездку крутыми виражами и подбрасывает лодку вверх, в общем - не скучно!
Лодок с туристами на маршруте не много - всего лишь мы и встречная, возвращающаяся на базу, так что толкотни и множества туристов нет
Через некоторое время приближаемся к высокому водопаду (отмечу отдельно - к главному каскаду водопадов лодки НЕ ходят, это очень опасно!)
При приближении начинает окружать нереальный шум и все сразу становятся мокрыми, не спасают даже дождевики, которые некоторые надели (в данном случае они абсолютно бесполезны), плюс - сильная качка
Здесь рулевой начинает показывать свое искусство по максимуму, профессионально проводя лодку максимально близко к падающим потокам справа и слева, а в итоге - завел лодку прямо под сильнейшие потоки (берегите голову).
Ощущения и впечатления просто нереальные - все мокрые, счастливые, никто ничего не слышит, шум, яркое солнце, совсем не холодно, выброс адреналина максимальный.
Вот оно - единение с природой и ощущение себя "никем" в этом мире, полное абстрагирование от всего окружающего, то, что и нужно городскому жителю для "перезагрузки", отличный антистресс!
Покрутившись под водопадом, идем на разворот и далее - в обратный путь также на огромной скорости (плюс попутное течение) и "акробатика" на воде - подскоки, виражи и тому подобное!
По возвращению на причал, пошли переодеваться в раздевалку, а затем нам предложили приобрести DVD диск с фото и видео, стоимость - 20 долларов США. Мы купили и не пожалели - хоть мы и снимали своим оборудованием, но предложенная съемка была качественнее и состояла как из общих планов, так и из персональных съемок каждого участника поездки, то есть все, что нужно на диске было. Не отказывайтесь от этого диска, не пожалеете!
После небольшого отдыха группа собралась и погрузилась на электромобиль и отправилась в обратный путь через сельву (тем же путем). А поскольку дорога однопутная, то по пути приходилось пропускать встречный электромобиль, что немного задерживало по времени. Но это мелочи по сравнению с полученными впечатлениями:)
Кому-то может показаться, что цена высоковата, но уверяю вас, что каждый потраченный реал стОит полученных впечатлений и удовольствия! Поездка несложная для людей всех возрастов, много было детей, которые также не выглядели утомленными, а также забота о безопасности со стороны персонала была на высоте!
Полюбовались красотами!
В результате душераздирающего поворота событий 80-летняя американская туристка погибла во время замбийского сафари в национальном парке Кафуэ, когда огромный пятитонный слон преследовал и в конечном итоге атаковал автомобиль для сафари .
Слон убийца
Слон убил американского туриста В Замбии в национальном парке Кафу, во время джип-сафари туристы встретили агрессивного слоняру-самца весом 5 тонн. Когда туристы увидели, что слон бежит к ним, они начали на своем автомобиле сдавать назад, но застряли на тропе из-за завалов и были атакованы гигантом. Четверо туристов отделались легким испугом, один получил серьезные травмы, а один американец погиб.
КИНОДОКТОР