Автор: snake
Java Android. Урок 12 (“Делаем игру”)
После того, как мы закончили делать достаточно простую игру “Быки и коровы”, научились базовым методам программирования под Андроид, можно приступить к работе над более сложными вещами. Будем делать 2D игру “леталка-стрелялка”. Такие игры ещё иногда называют плоскими, т.к. движение персонажей происходит только вверх-вниз и вправо-влево. Однако уже здесь потребуются знания несколько большие, чем просто программирование. Возможно, когда вы учились в школе, то спрашивали себя,- Зачем мне эта математика, зачем мне эта физика?- Вот как раз для программирования это всё и нужно, если вы кончено собираетесь делать игры и программы хоть как-то отдалённо моделирующих окружающий нас мир. Как я обычно говорю, программирование это всего лишь инструмент, вроде молотка, чтобы не заколачивать гвозди руками. Но чтобы сколачивать дом, вам нужно всё рассчитать, прежде чем брать в руки молоток и обладать какими-то базовыми знаниями в области строительства. В играх чуть более сложных, чем та, которую мы делали, это тоже становится неизбежно необходимостью. Но огорчаться по этому вопросу не нужно, всё что потребуется из этих наук, я буду это использовать, и тут вам останется либо проверять меня и вступать в полемику, либо поверить наслово. Сразу скажу – идея игры, которую мы будем делать, я позаимствовал с другой, прочно уже забытой игры, но это не меняет нашей задачи. Выглядеть она будет как-то так:
Видео самого экшена:
После её написания, полагаю, вы сможете уже делать любую другую игру. Делать мы её будем совместно, а так же вы сможете:
- Создать для неё какой-нибудь объект
- Дать исходник его мне
- Я опубликую игру с ним на своём аккаунте https://apps.rustore.ru/developer/ZFQbCo1eEp3jOqp5fKusWsz0L%2B8vFUgs, а в самом приложении будет информация о вас, как о разработчике со ссылкой на ваш сайт и/или email.
Итак, приступим, что тут у нас есть; а есть следующее:
- Игровое поле, которое чуть меньше экрана по высоте, и на несколько экранов в право и влево.
- Есть масштабный экран сверху.
- Есть один пользовательский объект, которым он может управлять и перемещать его в пределах экрана.
- Само рабочее поле тоже подвижное, но только влево и вправо.
- Объекты могут стрелять – пользовательский объект лазером, игровые юниты – каждое своим вооружением.
- Игровые юниты могут быть различной конфигурации.
- Элементы управления вынесены на рабочий экран.
- В игровом поле есть рельеф местности.
- Есть юниты испытывающие гравитацию.
- Все юниты имеют “физику”, т.е. двигаются с ускорением и торможением.
- Игра должна иметь звуковые эффекты.
- Игра должна иметь динамические сцены, присущие играм такого типа.
Все это мы будем реализовывать не при помощи каких-то движков, типа Unity, а непосредственно сами. Как понимаете, тут в 10 уроков мы никак не уложимся.
Имя проекта Salvador, что переводится как “Спасающий” (с испанского). Почему так – просто имя проекта должно быть латиницей, а английский язык как-то слегка приелся)). В игре будут юниты, которых нужно будет спасать)).
Для формирования рабочей области экрана, нам придется отказаться от стандартных элементов. По этому метод создания MainActivity будет начинаться так:
Всё что этот код делает, так это скрывает такие системные элементы на экране, как часы, уровень мобильной связи, верхний бар Активити и т.п. В результате нам нужно получить идеально черный экран, полностью и целиком. Но вопросы должно вызывать следующее – я закомментировал setContentView(), который вызывается с параметром xml окна, а вместо этого создаю экземпляр какого-то класса GameEngine и передаю его в качестве параметра для своего setContentView(). Из этого действа можно заключить, что мой GameEngine – это какой-то класс, который способен принять в качестве параметра метод setContentView(), и который и будет рисовать наш экран, а вот стандартный нам не нужен. Как теперь понятно, что и сам activity_main.xml (который среда разработки создала автоматически) нам теперь тоже не нужен:
Теперь создадим этот самый класс GameEngine (ПКМ по названию пакета):
Вот так:
В результате среда разработки создаст нам этот класс:
Однако, такой класс метод setContentView() в качестве параметра не примет, т.к. параметром его должно быть что-то, что наследуется от класса View – помните в прошлом курсе мы делали activity_main.xml и корневым элементом был ConstraintLayout (наследный от View). Наш класс мы тоже должны наследовать от него, по этому сделаем так:
Тут как только мы попытались добавить это наследование, Андроид Студио стала ругаться, что мол если ты наследуешь, то обязательно реализуй конструктор того, от чего наследуешь, т.е. от суперкласса View.. Все они нам сейчас не нужны, нам нужен только первый и то, немного доработанный, выберем его и окейнем:
Получается теперь так:
Однако, если вы парой минут назад заметили, я вызывал конструктор GameEngine с двумя параметрами, вторым из которых был параметры экрана (в модуле MainActivity). Доработаем с учетом этого:
Этот параметр нужен нам будет чтобы иметь представление какого размера экран устройства, на котором запускает игру пользователь. Он как раз в себе эту информацию и хранит. Заметьте, я не трогаю суперкласс, его формат вызова строго определен во View.
Пока писал эту статью заметил, что одна версия Андроид некорректно отрабатывает один метод, по этому переделайте его на вот так:
Однако давайте теперь запустим наше приложение и посмотрим что будет на экране, а должно быть вот так:
Кстати, повернуть экран можно вот этими кнопками:
Поскольку мы наследовали наш класс GameEngine от класс View, то как следствие можем пользоваться и его методами, т.е. использовать наследование класса в полной мере, а именно – не просто будем вызывать метод родительского класса, а вообще перепишем его на свой собственный. Нужно нам это потому, что класс View в своём составе имеет метод onDraw(). Этот метод делает ни что иное, как отображает элементы этого класса View на экране – непосредственно их рисует при помощи графических методов. Происходит это тогда, когда View получает запрос на то, что теперь вдруг данные на экране стали недостоверными. Причин может быть много, например одна из них это то, что мы только что открыли приложение. Откуда там достоверным данным, их нет, мы же только что его открыли. Также, если ввиду функционала, на экране происходят какие-то изменения, например, как в прошлом уроке мы таймер на экран выводили и меняли раз в секунду его значение на экране. Когда мы записывали новое значение в TextView автоматически его в родительском классе (в том же самом View) вызывался метод недостоверности сведений и опять-таки запускался следом метод onDraw() для новой отрисовки элемента. Давайте сделаем наконец-таки и мы этот метод и отрисуем чёрный квадрат Малевича экран. Добавим в наш класс GameEngine этот метод и ещё одну переменную класса:
Ну вообще круть! Так что собственно происходит, давайте подытожим:
- Создаётся наше Активити
- При создании создаётся экземпляр нашего класса GameEngine, который наследуется от базового класса View
- Этот экземпляр передаётся методу отрисовки Активити, а поскольку он наследованный от View, то вызывается сразу следом метод недостоверности содержимого (метод invalidate() ).
- Этот метод invalidate() выполняет ряд функционала, частным случаем которого является вызов метода onDraw() класса View, но этот метод теперь в нашем классе GameEngine, по этому вызов происходит не родительского метода, а нашего. А в нашем методе уже есть код установить цвет отрисовки на чёрный (параметр Color.BLACK) и вывести его на канву canvas (полотно рисования, экранное полотно, холст) методом drawPaint().
Теперь добавим ещё ряд методов и посмотрим как рисовать на экране что-то вразумительное:
Тут видно для чего нам нужно было знать параметры экрана (чтобы знать где находится правый край) и посмотрели как нарисовать простой тонкий прямоугольник размером в ширину экрана и по высоте 10 пикселей (по координатам Y от 100 до 110). Заметьте, рисование происходит в два этапа – сначала устанавливаем стиль того как и каким цветом будем рисовать, а потом – что будем рисовать (геометрию фигуры).
Задание по уроку:
1. Самостоятельно разберитесь какие методы рисуют линию, точку, круг
2. Нарисуйте согласно плану нашей разработки эту верхнюю часть масштабного представления (саму красную рамку и белый позиционер текущего экрана).
3. Попробуйте нарисовать посередине экрана, как в нашем проекте, космический корабль игрока:
Если всё получилось как нужно – молодцы! Если нет, давайте разберёмся что не так.
Урок 11 Урок 13
El Vinto, 2023 (Copyright)
«Обозналась» (Жанр: Литературная зарисовка)
В этот летний жаркий день она вышла из квартиры и направилась к лифту. Через минуту уже яркое солнце согревало её тело, а знойный ветер играл с её ресницами и щекотал глаза. Она шла к метро и иногда заглядывая по сторонам замечала пристальные взгляды как молодых парней, так и зрелых мужчин, которые восхищались её внешностью. Она знала, что она красотка и нравится многим, это было видно, и это создавало ей прекрасное настроение. Она шла, и иногда лёгкая улыбка проскакивала по её лицу. Вдруг она обернулась. В нескольких метрах позади неё шёл средних лет мужчина. Его суровый и уставший вид перемешивался со злобным и отчужденным взглядом. Было заметно, что он идет за ней и держит дистанцию. Она его приметила ещё у дома, поэтому сейчас здесь, уже почти у станции метро, это казалось подозрительным. Он совершенно не в её вкусе, а выражение лица даже пугало. Она перешла мост через Портовую улицу, спустилась с него на Кронштадскую и направилась вглубь дворов. Тут она услышала быстро приближающиеся сзади шаги.
– Девушка, давайте познакомимся. Меня зовут Сергей, а Вас как?
– Не скажу.- ответила она даже не обернувшись, но широкая улыбка разошлась по её лицу, ведь он всё-таки решился, догнал её и подошёл, хотя и не в её вкусе.- Всё равно смелый мужчина, хотя и надо от него как-то отделаться – не красивый, и больно уж загруженный какой-то,- подумала она.
– Девушка, а почему не скажите?- снова послышался голос уже где-то совсем рядом.
– Не почему, а по кочану…и ещё по кочерыжке, отстаньте! – Сказала она почти смеясь и тут впервые обернулась. Только теперь она увидела, что за ней почти бежал замечательный парень, который вглядывался в неё с восхищением и интересом. Она быстро отвернулась.
– А это не тот мужик! – подумала она,- С этим я бы не против познакомиться, мне очень нравится, хороший такой. Что теперь делать?
Она снова повернулась, но за ней уже никто не бежал. Парень стоял с грустью на лице и провожал её взглядом.
Выйдя из двора, она подошла к светофору через проспект Стачек. – Может он всё-таки пошёл за мной?- подумала она и обернулась, но сзади за ней больше никто не шёл.
El Vinto, 2023
Java Android. Урок 13 (“Делаем игру Salvador”)
Продолжаем делать игру, как мы её назвали “Salvador” (Спаситель).
Верю, что вы по честному делаете домашние задания, по этому уже поняли, что отсчет координат на экране выглядит так:
Само домашнее задание должно быть как-то так:
Тут для того чтобы всё было в порядке, основные переменные класса я вынес как общие. Сам код их объявления тоже немного изменил:
Заметьте, всё что можно посчитать сразу, я делаю во время создания GameEngine, потому что потом это будет некогда. Дело в том, что при работе onDraw нужно максимально быстро отработать всё, что связано по потоком пользовательского интерфейса (об этом я говорил на одном из прошлых уроков). По этому в момент отрисовки лишние вычисления нам не нужны. Казалось бы это мелочи, но они накапливаются и потом приводят к тому, что нужно делать ревизию кода и выявлять где что неоптимизированно, т.к. пользовательский интерфейс начинает подтормаживать.
Так, далее я изменил и сам метод onDraw() , сделал его таким:
)))….)))….ну да, так удобнее потом искать все проблемы. drawTopper() – это мой метод, который будет рисовать только ту верхнюю рамку со всеми её элементами. Когда будем тестировать, так будет удобно сразу понять в какой части программы глючит отображение элементов верхней рамки, а где самого игрового поля (не придётся судорожно лазить по коду чтобы что-то найти и тем более вспомнить через какое-то время где тут что находится). Ну и саму эту процедуру тоже я сделал немного по другому:
После чего получаем нужный результат.
Заметьте, для отрисовки белой рамки сканера я использовал drawLine() определенной толщины, а не drawRect(), по этому и методы инициализации в onCreate() немного отличаются. Для прямоугольника нужно определить метод заполнения внутри прямоугольника, а для линий – указать тип линий и их толщину.
Теперь давайте изобразим розового нематериального слона в вакууме космический корабль игрока, но не в конечном и неизменном виде, а пока в виде круга. Для этого, как можно догадаться, я тоже сделаю отдельный метод:
Инициализация будет такой:
, а объявление таким:
Обратите внимание на комментарий – это координаты корабля игрока НЕ на всём игровом поле, которое занимает сейчас 6 экранов (параметр areaScreenCount), а координаты только на экране. Дело в том, что корабль игрока никогда не должен выходить за пределы экрана, он им управляет и должен всегда иметь визуальный контакт с ним, но при этом координаты корабля в пространстве будут другие. Единственное исключение – это можно оставить координату Y для координат на экране и координаты в пространстве игрового поля пока одинаковыми, но кто его знает, может мы потом захотим и по вертикали сделать тоже самое.
Однако, продолжать тестировать будет проблемно, пока у нас нет ориентиров на экране. Хорошим ориентиром был бы ландшафт местности; Давайте его изобразим. Для начала мне понадобится новый класс, который я размещу прямо внутри MainAcrivity. Он будет хранить координаты, но не по отдельности, как это мы делали, например, для размера экрана (X и Y в различных переменных), а внутри одной переменной класса. Делаю я это исключительно для удобства (иначе пришло бы создавать два отдельных ArrayList):
ArrayList – это специальный объект Java очень похожий на массив, но не обладает какой-то размерностью. Т.е. его размер может динамически меняться. Работает он несколько медленнее, чем массив, по этому им злоупотреблять особо не стоит, однако пока он необходим. Дело в том, что мы сейчас точно не знаем какой у нас будет ландшафт и точно с этим не определились, ни размер, ни параметры; потом сделаем просто преобразование ArrayList в более быстродействующий объект – массив, и будем уже использовать его.
Итак, создали набор для хранения координат, давайте теперь автоматически сгенерируем ландшафт. Сделаю я это чуть сложнее, чем пареную репу (+ заодно добавлю инициализацию earthPaint для будущей отрисовки):
Заметьте, после основного цикла случайной генерации координаты по Y, я добавил ещё один элемент, взяв его из самой первой координаты. Как наверно вы уже догадались, это было сделано чтобы последний рельефный элемент по Y был точно таким же как и первый – чтобы вершина или впадина совпала, а рельеф Земли замкнулся в единую циклическую поверхность.
Переменна randObject, как можно догадаться, объявляется так:
Вывод на игровое поле будем слегка сложнее:
В результате получаем так:
Задание по уроку:
1. Изучите код отрисовки drawTopper(), вы должны хорошо понимать, что делает каждая определенная команде, какую линию рисует и почему параметры вычисляются именно так. Попробуйте произвольно изменить некоторые параметры и посмотрите на измение результата, как это отразится на изображении того или иного элемента.
2. Оптимизируйте алгоритм отрисовки ландшафта так, чтобы приложение не пыталось отрисовать точки, которые находятся за пределами экрана (их же всё равно не видно).
3. Создайте ArrayList для хранения координат космических кораблей инопланетян. Сгенерируйте 10 штук и условно их представьте тоже кругами:
Если всё получилось как нужно – молодцы! Если нет, давайте разберёмся что не так.
Урок 12 Урок 14
El Vinto, 2023 (Copyright)
Java Android. Урок 14 (“Делаем игру Salvador”)
Продолжаем практическое изучение языка программирование Java под Android. Мой курс немного не обычен и ориентирован не на теорию, закреплённую практикой, а на практику подкреплённую теорией. Я когда учил языки программирования сам, именно таким и нашёл самый оптимальным метод изучения. Я пришёл к выводу, что максимальная эффективность в обучении программированию есть лишь тогда, когда есть определённые задачи и вы стараетесь их решить. Вот для этого и вступает в ход изучение теории. Но как только какая-то часть задачи решена, следует переключиться на другие. После того как вы объёмно представляете задачу, она решена до какой-то детализации, и стоит вникать во все аспекты. Т.е. иными словами, использовать в изучении программирования метод дедукции.
Рассмотрим домашнее задание по добавлению инопланетян на экран. Понятное дело, что вариантов исполнения этого функционала тысячи. Небольшое отступление – чем более ответственно вы будете подходить к выполнению этих домашних заданий, чем более тщательно будете разбираться в предлагаемом мной коде, тем прогрессивнее будет ваш результат в программировании. Здесь вас никто проверять не будет и оценку ставить некому, а обмануть учителя не получится – любой обман здесь – это обман самого себя))).
Итак, сравните мой вариант со своим, и попытайтесь понять почему есть отличия, а они будут, ведь вероятность того, что они полностью совпадут стремится к нулю)).
Для инопланетян я первым делом создал специальный класс для хранения данных этого юнита (юнит – персонаж игры). Я предполагаю, что с одним типом юнитов будет скучновато, по этому типов будет несколько.
Отличаться они будут в основном:
- Визуально выглядеть по разному
- Разное поведение
- Разное вооружение
Однако у них будут одинаковые переменные:
- Координаты
- Тип юнита
- Переменная(-ые) типа Paint
Но для начала я сделаю так:
Само хранение множества экземпляров пришельцев, уже наверно догадываетесь, будет так:
Генерация начальных координат инопланетян будет такой:
Их отображение:
Теперь настало время немного добавить динамики на сцену. Но мы уже знаем по одному из прошлых уроков как это сделать. Правильно, добавим параллельный поток:
Вы заметили наверно, что я просто откопипастил этот класс с урока про игру “Быки и коровы” и только поменял название класса. Вся остальная часть почти такая же, кроме старта и остановки процесса. Дело в том, что в нашем классе GameEngine нет наследованных методов onStart() и onStop(), по этому мы должны создать методы для аналогичного функционала, а выполнять их уже из Активити. Добавим в GameEngine ещё два метода:
А теперь просто их вызовите в нужный момент, но уже из класса Активити:
Проверим, таймер работает, кстати вызов я его настроил через каждые 40 миллисекунд. Догадываетесь почему? Правильно, потому что это 1/25 секунды, чтобы обновление динамических сцен шло со скоростью 25 кадров в секунду. Проверим, правильно ли работает таймер:
Да, действительно, таймер отрабатывает корректно, но на разницу во времени в миллисекундах (где-то действительно около 40 мс) особо не обращайте внимания, т.к. ещё достаточно ресурсов отнимает сам процесс отладки и вынесение данных в консоль отладчика. По этому время может немного “гулять” туда-сюда.
В этот таймер теперь уже можно писать код для сцен. Первое, что я хочу сделать, чтобы корабли инопланетян эффектно появлялись на экране – группировались из мелких частей. Для этого мне нужно немного переделать код, т.к. такие объекты как частицы группировки обладают рядом общих свойств с инопланетянами, но и уникальных, о которых станет понятно чуть далее. Сначала создадим новый класс внутри GameEngine:
Это будет родительский класс для всех объектов игры (инопланетяне, частицы группировок и взрывов, ракеты и т.п.). Создавать мы их тоже будем теперь не через ключевое слово “new”, а через статический метод CreateObject().
Статический метод, в отличии от обычного метода, не создаётся для каждого экземпляра класса, а общий для всех экземпляров. Это позволяет экономить на памяти (зачем создавать для каждого экземляра совершенно одинаковый код (или переменную), который не использует ни одну переменную или метод этого экземпляра). Однако, как следствие из сказанного, из статического метода вы не сможете обратиться к какому-то элементу класса, свойственному только определенному экземпляру.
Далее переделаем и сам класс Alien:
Заметьте, у этого класса теперь нет данных координат – ведь он же наследуется от GameObject (в котором и есть эти координаты), а значит наследует и все переменные и методы отмеченные как public или protected (не наследуются только private). Однако, конструктор родительского класса – класса GameObject, принимает два параметра координат:
По этому в дочернем классе мы обязаны вызывать его конструктор с этими параметрами (если бы параметров не было, super можно было бы не вызывать совсем):
Теперь посмотрим, а для чего собственно мы ввели такие сложности; ввели мы их как раз для упрощения дальнейшей разработки и более ясного представления кода. Потому что новый объект частей группировки, из которой будет собираться инопланетный корабль, будет точно также наследоваться от GameObject:
Однако ему мы сразу же добавим его уникальные свойства (не свойственные GameObject или другим дочерним классам):
Класс Cords, к сожалению, морально устарел – теперь для хранения координат потребуется хранить их не как целое число, а как вещественное:
После этого Андроид Студио попросит принудительно привести эти координаты к целому числу, там где она, видимо, боится сделать это сама)). По этому где в коде начнёт подчёркивать красным эти координаты, явно укажите, мол да, я настаиваю убрать дробную часть и оставить только целую, например вот так:
Т.е. если говорить по феншую, то мы тип double приводим к типу int.
Но с классом GroupingParts ещё далеко не всё, теперь нам нужны методы управления этим объектом, например, метод, который запустит процесс группировки объекта и ещё некоторые вспомогательные:
Метод burn() как раз инициализирует конечную точку сбора частей (cords.x, cords.y), шаг конструкции (dx, dy) и количество шагов сборки (n).
Теперь ещё нужно добавить две заглушки в родительский класс – метод отображения объекта и его и метод его рождения. Дело в том, что даже если дочерний метод не будет реализовывать какой-то из этих методов, их вызов не должен приводит к ошибки (чтобы сделать их вызов универсальным и работающим при любых условиях реализации):
А теперь реализуем этот метод в дочерних объектах, для класса Alien и класса GroupingParts. Для Alien я фактически просто его перенесу код отрисовки из GameEngine:
Давайте, поскольку теперь у нас игровые объекты – это уже не только Alien, то и их ArrayList переименуем, но чтобы не лазить по коду и в везде править, делаем это так. Выделите переменную aliens и при помощи ПКМ вызываем контекстное меню:
Меняем на gameObjects. После ввода на ENTER везде в коде программа переименует во всех использованиях этой переменной. Сам тип теперь тоже будет более общий, в результате получается так:
В конструкторе GameEngine генерацию инопланетян заменим на генерацию объектов. Причём генерировать сначала будем не Alien, а GroupingParts, да и само создание теперь, как говорили ранее, не через “new”, а через CreateObject(). Получается так:
Отображение тоже изменится, а также переименуем drawAliens() на drawObjects():
Добавим функционал тестового рождения объектов:
Отображение GroupingParts будет таким:
Перезапустим приложение и посмотрим что получилось:
Все точки группировки действительно нарисовались, теперь нужно добавить только динамическую сцену. Для этого надо просто вызывать недостоверность изображения каждые 40 мс (25 кадров в секунду). Делается это так:
Перезапустим снова и вот что происходит:
Задание по уроку:
1. Сделайте ещё один Активити, назовите его PreActivity, который будет запускаться в начале игры вместо MainActivity, отображать надпись названия игры на весь экран, а уж по прошествии 5 секунд будет запускать сам игровой процесс (наш MainAcrivity).
2. Сделайте так чтобы после группирования запускался процесс разгруппирования (взрыва), и процесс циклически повторялся вот так:
3. Самостоятельно изучите метод setAlpha() класса Paint. Сделайте так чтобы в конечных стадиях взрыва зеленые точки выглядели более тускло за счет изменения этого параметра Alpha.
Если всё получилось как нужно – молодцы! Если нет, давайте разберёмся что не так.
Урок 13 Урок 15
El Vinto, 2023 (Copyright)
Java Android. Урок 15 (“Делаем игру Salvador”)
Теперь чтобы тестирование и разработка пошли более ускоренно нам понадобятся простые элементы управления процессом игры, и одно из этого – возможность перемещать наш объект в игровом пространстве. Поскольку мы имеем дело не со стандартным окном отображения, а кастомным (мы его сами делаем), то и элементы управления тоже придётся создавать самим. Но это и к лучшему – например, нам потребуются кнопки, которых нет ни в одном стандартном интерфейсе.
Начнем делать кнопку направления движения. Для этого создадим сначала класс наших кнопок и дадим ему общие для всех методы и свойства, а также объявим переменную для хранения экземпляров этого класса:
Дадим константы идентификаторов будущих кнопок:
Добавим пока только одну кнопку:
А теперь её отображение:
Проверим что получилось:
Отлично! Кнопка готова. Теперь нужно сделать так чтобы она принимала действия пользователя, но вначале изменим немного построение системы координат игрока. Уберём лишнюю координату и укажем ещё две:
Координата игрока на экране по Y больше будет не нужна, её заменит новая координата игрового поля по Y, т.к. диапазон доступный для перемещения игрока по Y соответствует размеру игрового поля по Y. По X конечно же всё как и было, т.к. размер игрового поля по X составляет несколько экранов по X.
Теперь добавим ещё парочку методов для самого движения:
Добавлю имплементацию в класс интерфейса ответственного за перехват кликов по экрану устройства:
Теперь инициализируем в конструкторе GameEngine на этот интерфейс, указывая текущий класс для реализации его методов:
Далее реализуем метод (он единственный) этого интерфейса. Он будет таким:
Теперь сделаем отображение рельефа местности исходя из текущих координат игрока:
А положение рельефа получается таким:
В результате получается как-то так:
Как теперь стало понятно, в зависимости от клика внутри круглой кнопки (даже в зависимости от области внутри этой кнопки), корабль начинает двигаться в любом направлении.
Задание по уроку:
1. Сделайте так, чтобы при движении рельефа также двигались вместе с ним и юниты инопланетян, ведь они тоже должны двигаться относительно корабля игрока (кроме ещё собственного их движение, которое пока не реализовано).
2. Попробуйте изменить поведение чтобы положение внутри кнопки определяло не координату игрока, а ускорение движение игрока.
3. Сделайте чтобы после группирования частей на центре этого группирования образовывался инопланетный корабль, после этого он “жил” 5 секунд и затем взрывался.
Если всё получилось как нужно – молодцы! Если нет, давайте разберёмся что не так.
Урок 14 Урок 16
El Vinto, 2023 (Copyright)
Java Android. Урок 16 (“Делаем игру Salvador”)
Итак, к этому уроку, опираясь на ваши домашние задания, вы уже должны быть готовы чтобы сделать это:
Что тут есть:
- Рельеф, который при движении влево или вправо никогда не кончается; он подобно Земному шару, если идти по экватору – придёшь в ту же точку, откуда вышел.
- Сначала генерирует точка сбора каждого инопланетянина. После его группировки он появляется, а затем через несколько секунд он самовзрывается.
- Движение всех объектов на экране согласовано в относительном движении игрока
- Движение игрока выполнено с учетом физики движение (есть скорость движения, есть учёт ускорения/торможения, есть учет сопротивления воздуха). Это позволяет создавать эффект инерционного движения, когда при смене направления движения корабль игрока не мгновенно начинает лететь в обратную сторону, а сначала замедляется, и лишь потом разгоняется в обратную сторону.
То, что мы в прошлых уроках использовали для тестирования геймплея (игрового процесса), было удобно и эффективно, однако, с новыми реалиями и физикой некоторые моменты нужно будет упростить, изменить и добавить новые. Начнём.
Инициализацию параметров игрока делаем так:
К координатам игрока я добавил переменные, которые будут хранить его скорость и ускорение, а также их предельные значения (у игрока не должна бесконечно возрастать скорость, иначе это будет противоречить канонам физики). Заметьте, предел ускорения по X и Y различны, оно и понятно – двигатели нашего корабля развивают разную мощность по вертикали и горизонтали))). Тут ещё не мешало бы вспомнить школьный курс физики и математики, что
, где:
L – перемещение объекта
L0 – начальная координата
V0 – начальная скорость
a – ускорение
t – время
Однако, увы, эта формула будет для нас не пригодна, т.к. у нас нет показателя времени – t. Связано это как раз с тем, что обновление экрана мы выполняем квантами времени, как помните,- 25 раз в секунду. Т.е. с точки зрения математики мы обладаем не параметром t, а параметром ∂t (дифференциал t). Исходя из этого и перемещение объекта будем определять через совершенное за единицу времени перемещение. Так и поступим. Изменим для начала метод onButtonDown() как и было это в домашнем задании – он теперь будет определять не какие-то тестовые координаты, а именно само ускорение. С точки зрения моделирования это так и есть – как в автомобиле, чем сильнее нажимаем на педаль газа, тем с большим ускорением разгоняемся. Добавим ещё один параметр с радиусом кнопки, чтобы более универсально можно было настраивать параметры размеров интерфейса.
Разумеется при вызовах тоже нужно это учесть:
Сам расчет расстояния перемещения выведен туда, где он более всего логичен – в метод вызываемый 25 раз в секунду:
Сначала мы находим текущую скорость от ускорения. Затем изменение расстояние от этой скорости, тут вроде должно быть понятно. Подчеркиваю, метод вызывается 25 раз в секунду, по этому с точки зрения динамического моделируемого процесса, мы имеем расчет параметров за единицу времени ∂t. Просто она в математике стремится к нулю, а в жизни для игрока уже всё что чаще 24 кадров в секунду уже незаметно и кажется непрерывным движением, и этого оказывается достаточным. С таким ∂t идеально точный математический расчет траектории мы конечно же не получим, но визуально мы разницы не заметим. Далее в коде идут условия, по которым я ограничиваю и текущую скорость и координаты чтобы корабль игрока не вышел за пределы игрового поля или не развил скорость, при которой уже станет не понятным что на экране вообще происходит. После этого я умножаю на числовую константу ускорение и скорость. Это таким образом я создал эффект сопротивления воздуха, т.е. если игрок отпустит кнопку управления, то через какое-то время его корабль сам плавно остановится. Если бы это был космос, где нет сопротивления воздуха, то корабль летел бы сколь угодно долго пока не столкнулся бы с каким-либо объектом, но в нашей игровой реальности, пардон, виртуальности, условия другие.
Но с этим методом ещё не всё, давайте сразу же добавим в него и ещё кое что:
Основное предназначение этой части кода – управление трансформацией инопланетных объектов. Для каждого объекта выполняется сначала метод run(), далее посмотрим что он делает. Затем определяется тип объекта, что это ещё только группирующийся/взрывающийся объект или уже готовый инопланетянин. Тут же определяется, что если группировка уже завершена, то нужно на этом месте создать инопланетянина, а сам группирующийся объект удалить из списка игровых объектов. Если инопланетянин “прожил” некоторый лимит времени, то должен удалиться из списка игровых объектов, но в этот же момент на этом же месте нужно создать взрыв ( метод explosion() ).
Заметьте, я не сразу прямо в цикле объектов gameObjects удаляю “старый” объект из списка и добавляю туда новый, а изначально создал два временных списка, один для удаления, другой для добавления, и делаю работу над ними уже после выполнения основного цикла. Делается это для того, чтобы не получилось исключительной ситуации. Если циклом перебираем список, и сам цикл зависим от этого массива, то менять состав этого списка внутри этого цикла строго нельзя (иначе результат такого функционала может стать не предсказуемым) !
Завершает метод всё тот же invalidate(), который запускает цепочку системных вызовов по обновлению экрана и запуску метода onDraw().
Претерпел изменения класс GameObject (изменил немного формат некоторых методов). Конструктор теперь не содержит начальных координат, т.к. они генерятся в burn() дочернего класса GroupingParts и больше генерить их пока негде; в конструктор добавлен тип:
Дочерние классы:
Заметьте выполнения класса explosion() также как и burn() только координаты при этом не генерятся и лимиты наоборот, а так всё тоже самое.
Инициализация местности немного изменена (понижена), а инициализация объектов теперь использует тип:
Заметьте, инициализируется группирующийся объект, который (как я вначале написал) после схлопывания удаляется и создаётся одновременно новый уже типа Alien. Сгенерировал сразу 100 объектов чтобы оценить будет ли хромать производительность.
Батон, пардон, кнопку увеличил в размерах и перенёс выше и правее, т.к. испытав на своём телефоне с разрешением 1920 х 1080 понял, что кнопка для управления для пальца слишком мала. Также изменил и настройки виртуального мобильника на аналогичные:
В изображение кнопки добавил функционал делающие её слегка прозрачной, это позволяет и видеть что находится за ней и при этом видеть саму кнопку (если делали домашнее задание в одном из уроков то поняли, что я о setAlpha():
Рельеф Земли теперь изображается с учетом того, что она якобы круглая, т.е. летим в одну сторону, вылетаем из другой (циклически повторяется). Для этого пришлось сделать её отображение с учетом этого:
Тут было два очевидных варианта решения, либо оставить всё как есть и адаптировать под существующий функционал, либо математически представить её действительно круглой (ввести полярную систему координат и измерять не координатами по x и y, а углами alpha и beta. Но расчеты в полярной системе потребовали бы вычисления косинусов и синусов в вещественных числах, а это существенно забрало бы вычислительных ресурсов. В связи с этим, если вы ещё раз посмотрите на код объектов, то увидите, что вычисление координат при отрисовке претерпело явные изменения. Для рельефа же потребовалось сделать не только цикл его отрисовки, но и два цикла дорисовки на случай, если мы подлетаем к краю рельефа (слева или справа) – визуально он не должен обрываться, а должен дорисовываться его началом до границ экрана, создавая иллюзию циклической поверхности, вроде экватора планеты. Но делать три цикла подряд тут как-то некрасиво, по этому я сделал цикл в цикле.
По факту получилось даже лучше, чем планировалось:
Задание по уроку:
1. Снова оптимизируйте отрисовку рельефа так, чтобы его куски за пределами экрана не отрисовывались. Аналогично оптимизируйте отрисовку объектов и частей группировки / взрыва.
2. Самостоятельно разберите вопрос рисования текста на холсте.
3. Сделайте чтобы корабли инопланетян случайно двигались тоже с учетом физики
Если всё получилось как нужно – молодцы! Если нет, давайте разберёмся что не так.
Урок 15 Урок 17
El Vinto, 2023 (Copyright)
Java Android. Урок 17 (“Делаем игру Salvador”)
Итак, отключаем тестовый процесс самоликвидации инопланетян, просто закомментировав эту строку (мало ли ещё пригодится, а оно так):
Перенесём парочку полезных методов генерации диапазона чисел из GroupingParts в GameObject, т.к. теперь эти метода мы будем использовать ещё как минимум и в классе Alien:
Добавим в Alien аналогичные игроку свойства для перемещения:
Реализуем ещё не задействованный от родительского класса метод run(). Если помните, он вызывается для каждого объекта 25 раз в секунду (до этого тоже вызывался для Alien, просто был “пустым” из родительского класса, заглушкой):
Теперь этот метод в Alien устраивает хаос, причём в прямом смысле этого слова. Мы получаем броуновское движение:
Добавим в класс GameObject заглушку-метод contact() который будет вызываться аналогично run() (и вместе с ней 25 раз в секунду):
Для класса Alien выполним реализацию:
Здесь всё просто – мы находим разницу между координатами игрока и объекта, и если она меньше некой допустимой, значит будем запускать цикл взрыва объекта (переменная waitToExplosion как раз и создана в Alien для этого).
По формуле тоже просто – вспоминаем школу, геометрию, теорему Пифагора:
Заметьте как я нахожу разницу координат по X – у меня стоит знак плюс. Это не ошибка, дело в том, что изначально я спозиционировал игровой процесс в относительном движении (только по X) игрока и всего остального, таким образом его координата всегда будет противоположной всем остальным объектам игры. Также необходимо пройти условия, когда объекты оказываются на границе рельефа местности, когда, например, у одного относительная координата -5000 и у другого 5000, но тут нужно просто из суммы вычесть константу.
Чтобы выполнить взрыв мне будет достаточно просто добавить это в уже существующее условие:
Результат будет очевиден, но придётся хорошенько погоняться за объектами, и это оказалось задачей не из простых))):
Приступим теперь к тому, что нужно было вам подготовить в домашнем задании – отображение текста. Начнем с того текста, который отображает бонусы при уничтожении инопланетян. Для начала добавим новый класс:
Теперь выполним то, что должно создавать текст и убирать его по прошествии некоторого времени (в данном случае 2 сек):
В результате получится как-то так:
Задание по уроку:
1. Сделайте подсчет игровых очков в верхнем левом углу как сумму показываемых бонусов при уничтожении инопланетян.
2. Импортируйте шрифт из игры “Быки и коровы”, которую мы делали и используйте его для п.1
3. Сделайте чтобы корабли инопланетян если находятся ближе определенного расстояния к игроку переставали хаотически двигаться и начинали сами на него нападать вызывая столкновение:
Если всё получилось как нужно – молодцы! Если нет, давайте разберёмся что не так.
Урок 16 Урок 18
El Vinto, 2023 (Copyright)
Java Android. Урок 18 (“Делаем игру Salvador”)
Этот урок будет посвящен небольшому приведению игры в чувства, создание динамизма и зрелищности. Когда что-то много меняется, это для аркады то, что как раз и нужно. Всё внимание должно быть приковано к игре, особенно если она это внимание к себе требует. Будем сейчас делать что-то вроде этого:
Правда, не в тесте нужно будет убегать от инопланетян, избегать с ними столкновения, а не наоборот догонять. Это они будут стараться атаковать всеми своими средствами. Сейчас у них это и таран и ракеты (белые летящие точки). На нашем корабле что-то моргает, на их тоже самое. Это всё создаёт необходимый антураж аркады.
Итак, теперь для начала нужно немного изменить код отвечающий за полёт корабля игрока. Вообще, это частое явление, когда вы пишите некоторые части исключительно чтобы пройти следующую стадию разработки, а потом оптимизируете и меняете код уже под новые реалии и функционал.
Сделаем для всего, что связано с игроком отдельный класс и дадим ему имя Gamer:
Как видите, я перенес сюда и хранение координат и метод отрисовки, теперь это уже не просто круг, а что-то похожее на летательный аппарат, который умеет поворачиваться в ту сторону, куда направлен вектор тяги. Также добавлены для антуража некоторые моргалки на его поверхности, придающие живость, ну, живенько чтобы было)).
Для инопланетян отрисовка идёт тоже несколько по другому:
Код, отвечающий за движение инопланетян, и вызывающий у них броуновское движение тоже изменил, ведь в домашнем задании вам нужно было сделать чтобы на некотором расстоянии от игрока, их броуновское движение прекращалось и она начинали идти на таран. Посмотрим, как это сделал я:
Метод getR() (расчет расстояния от инопланетянина до игрока) работает на первый взгляд немного странно. Можно заметить, что dx, dy и r – это не локальные переменные метода, а всего класса, при этом метод ещё этот r и возвращает в качестве результата. Всё дело в том, что я использую его и тут и ещё далее сразу для двух функционалов.
Всплывающий текст с бонусами – это такой же игровой объект, как и инопланетяне и группирующиеся объекты:
Ракеты – тоже игровой объект:
Вызываемый метод normalizeCords() – это оптимизация расчета координат реализованная в родительском классе:
Paint для отображения бонусных очков шрифтом из прошлых циклов уроков, мы делаем так – за это отвечает Typeface:
Теперь для работы с этими новыми объектами нужно всего лишь добавить логику взаимодействия с игровым процессом, а делается это как раз в методе, который запускается 25 раз в секунду (не забываем, что ракеты и текст с бонусами нужно ставить на ожидание и последующее удаление, иначе они так и будут создаваться и загромождать интерфейс и отбирать ресурсы):
Задание по уроку:
1. Сделайте так, чтобы у корабля игрока отображался хвост
2. Добавьте кнопку стрельбы, чтобы игрок мог стрелять, причем только горизонтально и в том направлении, куда направлен корабль.
3. Сделайте функционал уничтожения инопланетян, если луч бластера примерно входит в область инопланетянина.
По результатам домашнего задания должно получиться как-то так:
Если всё получилось как нужно – молодцы! Если нет, давайте разберёмся что не так.
Урок 17 Урок 19
El Vinto, 2023 (Copyright)
Шаверма по-домашнему. Как я делаю
Это блюдо, которое наверно сможет сделать каждый, если конечно у него нет желания выпендриваться, вертеть шаверму по столу, шоркать ножиками, а затем сдирая мясо с гриля и улыбаться сквозь густую чёрную бороду)).
Для шавухи потребуется следующее:
- 500 гр. куриного филе
- 200 гр. сметаны
- 100 мл уксуса
- 2 луковицы среднего размера
- 50 мл растительного масла (обычное для жарки)
- 4 дольки чеснока
- Половина лимона
- Красный молотый перец чили (по вкусу, я кладу много)
- Обычная приправа для курицы или индейки
- Обычный тонкий лаваш (я покупаю длинный и режу на 4 части)
- Один большой помидор
- Половина свежего огурца
- Соль
Маринад:
Делается по принципу маринада для шашлыка. Вечером, купив после работы филе куры, режем её на мелкие части и кладём в посудину. Кура должна быть прохладной, в прохладной воде и её следует промыть. Заливаем всё уксусом, режем мелко лук (1 луковицу) и весь чеснок. Засыпаем перцем, приправой для курицы и солью по вкусу. Лично я кладу столовую ложку перца и столько же приправы, столько же соли, но на меня лучше не равняться)). Перемешиваем, а после этого тщательно моем руки, тщательно их ополаскиваем, засовываем в посудину, и начинаем крепко сжимать и переворачивать мясо как-будто мы месим тесто. После 5 минут жамканья уминаем всё на дно, режем 4 круглеша лимона (с кожурой) и кладём сверху. До утра помещаем в холодильник.
Жарить
Казалось бы, что тут такого пожОрить куру, ан нет. Делю это так. На обычной газовой конфорке, сколько ватт она не знаю, врубаю на полную катушку. Беру большую сковороду, подливаю 3 столовых ложки растительно масла, кладу туда всё что мариновалось, прямо всё содержимое через край и уже на сковороде начинаю перемешивать. По прошествии минут пяти, маринад начнёт шипеть и благоухать уксусом. Вытяжка не помешает тому, у кого проблемы с таким запахом или какие-то проблемы с уксусом в целом. Я обхожусь так, т.к. так легче контролировать процесс жарки. Через минут 15-20, когда кура будет уже готова, подливки должно оставаться ещё на сковороде много, теперь она там будет мешать, но(!) пригодится далее для другого:
ВНИМАНИЕ! Кура должна уже быть прожаренной, чтобы сливаемая подливка была уже полностью пригодна для употребления. По этому просто сливаем её в какую-нибудь посудину и продолжаем жарить дальше, подлив ещё немного растительного масла. Через некоторое время кура начнёт темнеть, покрываться кое где хрустящей корочкой и пойдёт запах, похожий как в шаверменной. Когда станет такой:
это значит, что она уже полностью готова. Ошмётки лимона можно вынуть, мелко порезать и замешать обратно. Жарка закончена.
Салат-соус
Соус и салат я делаю одним блюдом, примерно так, как это делается обычный помидорный салат со сметаной. Мелко режем помидор, огурец и половину луковицы. Кладём в бадейку, заливаем сметаной, заливаем примерно 30 гр. растительного масла, добавляем приправу для курицы и красный перец, солим. Перемешиваем всё до тех пор, пока на поверхности не будет плавать масло. Салат получается вот таким:
Заворачивание
Тут либо вы спец по заворачиванию шавермы, либо просто делаете как я. Отрезать от лаваша кусок примерно 30 х 20 см, разложить на столе:
Вылить на неё 2 столовые ложки подливы (которую сливали; именно по этому она уже должна быть пригодна к употреблению). Эта подлива не даст лавашу пересохнуть и сделает его ещё более мягким. Теперь у вас есть от силы минуты 3 чтобы лаваш не размяк. Быстро кладете по середине мясо:
После этого быстро салат:
После этого просто третью лаваша справа и третью лаваша слева внахлёст закрываем внутренности. С торцов лучше не закрывать, иначе если сильно не изловчиться, то всё распадётся и повываливается. По этому, чтобы не рисковать, я сразу это отправляю на плоский электрический гриль (двухстворчатый – нагрев сверху и снизу):
Но если быть честным, то сразу две делаю и отправляю на гриль)) :
Через пару минут приоткрыть гриль и снова той же подливой немного смазать (на фото я слегка переборщил):
Снова закрыть гриль, там всё сразу зашипит, а лишняя подлива начнёт стекать в сборник. Значит делаем всё правильно. Закрываем, периодически наблюдая степень прожарки и готовности. Тут кому как нравится. После этого получаем простую домашнюю шавуху: