Java Android. Урок 10 (“Создание apk”)

Теперь, когда приложение готово и протестировано, пора его отправить в ваш мобильный телефон или планшет и проверить окончательно. Для этого необходимо открыть основное меню Android Studio:

Теперь, когда приложение готово и протестировано, пора его отправить в ваш мобильный телефон или планшет и проверить окончательно.

Сначала нужно выбрать тип пакета, пока это будет apk чтобы мы могли этот файл без проблем переместить в смартфон и так выполнить его установку:

Теперь, когда приложение готово и протестировано, пора его отправить в ваш мобильный телефон или планшет и проверить окончательно.-2

Перед первой генерацией программного пакета, нужно либо указать ключ подписи, если он уже есть, либо создать его – это наш вариант, т.к. мы только в Студио ключи ещё не генерировали.

Укажите необходимые поля в зависимости от того как будет назван файл подписи и как под каким именем вы хотите фигурировать сами в этой подписи. Особо это ни на что не влияет:

Теперь, когда приложение готово и протестировано, пора его отправить в ваш мобильный телефон или планшет и проверить окончательно.-3

Далее будет как-то так:

Теперь, когда приложение готово и протестировано, пора его отправить в ваш мобильный телефон или планшет и проверить окончательно.-4

Укажите, что это релиз вашего приложения (готовый для публикации продукт) и нажмите Create:

Теперь, когда приложение готово и протестировано, пора его отправить в ваш мобильный телефон или планшет и проверить окончательно.-5

После того как ваш ПК пошуршит диском, справа внизу будет выдано что-то вроде этого:

Теперь, когда приложение готово и протестировано, пора его отправить в ваш мобильный телефон или планшет и проверить окончательно.-6

Теперь осталось только скопировать этот файл на ваш смартфон. Сам файл находится где-то вот тут (в папке вашего проекта):

Теперь, когда приложение готово и протестировано, пора его отправить в ваш мобильный телефон или планшет и проверить окончательно.-7

Т.е. внутри вашей пользовательской папки находится папка с проектами, затем внутри папки этого определенного проекта, а там ещё пройти пару папок и вот он app-release.apk.

Закинуть в смартфон можно несколькими способами, я это могу делать либо через Телеграмм – с компа отправляю файл в Избранное, а на телефоне из Изранного сохраняю в телефоне и кликаю этому файлу. Второй вариант – просто подключить кабель к ПК и закинуть непосредственно в телефон как на флешку.

В самом смартфоне этот файл находится проводником и кликается по нему. Ваш Андроид увидит, что вы хотите запустить пакет для установки и возможно попросит вас изменить условия безопасности (установка из неизвестных источников), с этим придётся согласиться.

Далее наслаждайтесь запуском и работой вашего первого приложения на Android!

Java Android. Урок 11 (“Динамика”)

Продолжаем изучение Java Android. На прошлом курсе мы сделали приложение “Быки и коровы”. Сама игра интересная, но нет в ней какой-нибудь мало-мальской динамики. Давайте её немного доработаем и приступим к более живеньким проектам.

Сделаем так, чтобы на ход игроку отводилось определенное время, например 30 секунд. Если игрок не успевает сделать ход, то он проиграл. Но игрока нужно подтрунивать, а для этого вывести на экран что-то похожее на таймер, чтобы игрок его видел, спешил, старался быстрее думать, но в общем всё, как в жизни.

Сначала я размещу View, куда буду потом отправлять каждый отсчет таймера:

После этого нужно сделать так, чтобы после первого хода он считал, а с каждым следующим сбрасывался и считал снова.

Тут следует сделать теперь небольшое отступление. В Android программа выполняется нелинейно. Если говорить очень просто, то процесс выполнения следующий:

  • Возникает какое-нибудь событие, запускается его обработчик, возвращается из выполнения.
  • Возникает новое событие, выполняется, возвращается и т.д.

Часто бывает так, что во время одного события происходит другое, ещё не дав ему завершиться, например, вы играете на телефоне в шахматы, вдруг приходит входящий звонок, который сворачивает шахматы и запускает приложение телефона. Звонок завершается, возвращается всё опять к шахматам. Т.е. события выстраиваются в очередь выполнения в разных приоритетах. Однако, есть события, которые должны выполняться независимо от очереди, либо в собственной ветке очередей. Например, вы переходите в телефоне в браузер, переходите на какую-то страницу и сразу начинаете её листать, но она при этом продолжает дозагружать картинки. Часто наверно такое встречали? Т.е. программа не заставляет вас скучно ждать пока не загрузит всю страницу целиком, а даёт возможность с ней работать по мере поступления контента. Такая работа называется параллельной. Т.е. есть пользователь и его взаимодействие с устройством, и есть что-то внутри, что должно работать параллельно. Всё заточено на то, чтобы не заставлять пользователя чего-то ждать, по крайней мере, чтобы пользователь мог изменить ход или логику работы в любой момент времени. Например, страница долго не загружается и пользователю надоело ждать, он должен в любой момент прервать этот процесс – закрыть страницу и возможно поискать другую, которая будет грузиться быстрее. Если пользователю мешать управлять устройством, либо делать это с запаздыванием, с плохим откликом, работать будет некомфортно, будет создаваться ощущение медлительности устройства. По этому совместная работа устройства и пользователя вынесена для разработчиков в отдельную парадигму и называется поток пользовательского интерфейса UIThread (User Interface Thread). Это поток, которому отводится определенное комфортное для пользователя время, как правило измеряемое миллисекундами. За это время программа должна как-то успеть отреагировать на действия пользователя и ответить на экране, если это необходимо. Всё, что превосходит это комфортное время, называется термином “Длительное выполнение” и должно быть распараллелено, т.е. вынесено в отдельный поток выполнения. Говоря иначе, если время чего-то предполагается таким, что пользователь может заметить какую-то задержку реакции, это нужно запускать отдельным потоком, и уже по мере его завершения как-то дать понять пользователю, что оно завершено, а НЕ(!) блокировать работу устройства от начала выполнения и до конца, пока оно не завершиться. Возвращаясь к примеру о браузере и странице это выглядит так – пользователь ввел адрес страницы и нажал кнопку “Перейти”. В этот момент браузер быстро принял этот адрес из поля EditText, быстро создаёт параллельный поток, передает ему этот адрес и запускает его, возвращая пользователю возможность продолжать работу с устройством. Само создание нового потока занимает столь малое время, что пользователь даже не успевает заметить, но в это время уже параллельный поток делаёт длительную операцию по обращению к серверу с нужной страницей, загрузки её в браузер, распознавание кода, дозагрузка картинок и т.п., время от времени передавая в пользовательский поток малыми порциями элементы загрузки, например, показать очередную картинку. В нашей задаче должно быть всё тоже самое. Пользователь сохраняет контроль над устройством, но параллельно приложение должно отсчитывать таймер и периодически сообщать его новое значение в пользовательский поток, который и будет уже его отображать на экране.

Самое простое создание отдельного потока – это создание экземпляра класса Runnable, давайте его добавим в наш основной класс MainActivity сразу после generateComNumber():

Теперь вначале класса MainActivity поместим несколько переменных класса:

И ещё добавим один метод где-нибудь тоже после generateComNumber():

Теперь давайте разбираться что тут для чего. Класс GameTimer наследуется от Runnable и обязательно должен реализовывать метод run(). Этот метод и есть то самое, что будет внутри него выполняться параллельно. А выполняться там будет вечный цикл, который будет прерываться каждую итерацию на 1 секунду и снова, и снова повторяться. Вся его задача – это передать короткое сообщение пользовательскому потоку. Это сообщение не содержит в себе никакой информации, т.к. сам факт этого сообщения говорит о том, что снова параллельный поток подождал 1 секунду времени и повторяет итерацию. А ведь нам это и надо, чтобы кто-то пользовательский интерфейс взбадривал каждую секунду.

Переменная sender и соответствующая константа SENDER_REFRESH совсем не обязательны, в нашем случае весь метод mainMessage() мог использоваться для реализации функционала отсчета, однако я решил показать как нужно получать параметры из другого потома, если вдруг нужно передать ещё какую-нибудь информацию.

Теперь нужно создать экземпляр этого класса и передать этот экземпляр в конструктор специального системного объекта Thread. Чтобы не вносить сумятицу, поток этот будет создаваться в специальном методе, а завершаться в противоположном (см.далее):

Можно заметить странную конструкцию у объявления переменной mainHandler. Она хранит экземпляр специального класса, который опрашивает пул поступающих сообщений, в частности будет принимать сообщения от нашего класса GameTimer и вызывать mainMessage() с этим параметром; это происходит благодаря встраиванию класса с имплементацией (метод handleMessage() как раз и есть реализация).

Чтобы параллельный потом не остался “висеть” в памяти и фанатично отматывать километры кода даже после завершения игры (он же параллельный независимый поток), нужно при завершении приложения его тоже ПРАВИЛЬНО завершить. Для этого достаточно вызвать уже заранее подготовленный нами метод:

Если посмотрим внимательно, этот метод stopAll() наш, и делает он всего лишь только то, что в переменную isRunnig помещает false, “вечный” цикл прерывается и поток выходит из выполнения, сам экземпляр класса потока уничтожится системой автоматически при завершении Активити.

Метод onStop() вызывается когда происходит остановка работы Активити (в отличии onStart(), который автоматически вызывается при старте).

Вообще порядок вызовов методов “жизни” Активити называется lifecycle и описан тут:

https://developer.android.com/guide/components/activities/activity-lifecycle

, однако по представленной там картинке и так становится понятным как всё остальное:

Тут хорошо видно, что после onStop() может быть одно из трех, но по нашему алгоритму либо окончательно выйдет из приложения, либо возвратится и повторно создаст наш таймер.


Задание по уроку:

1. Теперь, когда вы знаете метод, который у вас будет постоянно вызываться каждую секунду, реализуйте функционал уменьшения таймера и его сброс каждый ход.

2. Самостоятельно найдите описание и назначение контейнера RelativeLayout. 

3. Попробуйте сделать так, чтобы когда оставшееся время становилось менее 10 секунд, таймер начинал показывать десятые доли секунды. Используйте RelativeLayout для отображения долей секунды в уменьшенном виде:

В динамике это выглядит так:

Если всё получилось как нужно – молодцы! Если нет, давайте разберёмся что не так.

Урок 10 Урок 12

El Vinto, 2023 (Copyright)

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)

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)

Java Android. Урок 19 (“Делаем игру Salvador”)

Продолжаем практическое изучение Java Android на примере игры Salvador. На этом уроке и в его домашнем задании будем делать следующее, т.е. заставку к игре:

Сделать её можно используя тысячи методов, но нужно было что-то выбрать и я решил попробовать тот, который ещё ни разу не проверял.

Для начала добавим переменную roundTime класса GameEngine, которая будет фиксировать прошедшее в раунде время. Также добавим переменную mainGameLogic, которая будет определять логику игры, т.к. что сейчас у нас на экране, игровой процесс, заставка или ещё что-то иное:

Объект GameObject теперь хранит много самостоятельной информации общей для всех его дочерних классов, а некоторые функции и переменные из дочерних вынесены в этот:

Класс Alien теперь умеет работать с несколькими подтипами:

Сам метод поведения Alien тоже изменился:

Начальная инициализация объектов теперь начинается с группировки корабля игрока, а логика игры – заставка:

Заметьте, теперь информация о будущем объекте после группировки передаётся прямо в конструктор GroupingParts.

Основной код метода 1/25 секунды тоже поменялся, и чтобы уже определиться, переименовал его в mainStrob():

тут дальше следует череда разного рода действий заставки возникающих как только время раунда достигает нового значения 2000 мс, 3000 мс и т.д., тут то группируется корабль игрока, потом группируется инопланетянин, через 4 сек это стреляет в того ракетой, через 4.4 сек наш стреляет бластером в этого и т.д.

Ключевой код работы с объектами теперь упростился, т.к. часть функционала взял на себя GameObject и его дочерние классы:

Теперь посмотрим зачем в сначала вводил переменную позиция по X корабля игрока на экране. До этого момента это была лишняя переменная, т.к. корабль всегда находился по горизонтали в центре, так спрашивается, а зачем тогда она нужна. Нужна она вот для чего:

Правильно, чтобы расширить возможности игрового пространства. Когда корабль игрока летит по рельефу вправо, то его корабль отодвигается влево, чтобы улучшить реакцию на инопланетян появляющихся по ходу движения. При этом и игровая ситуация для манёвров более предсказуема. Как можно заметить, это не влияет на реальную скорость, т.к. определяет только визуальное представление, но не координату в игровом пространстве. Однако, учитывая как у нас организован функционал, сделать это оказалось крайне просто:

А в стробе просто делаю так:

“300” – это та самая граница в которую корабль упирается слева или справа.

onScreenVX – определяет с какой скоростью он будет перемещаться по экрану в эту граничную область.

Задание по уроку:

1. Сделайте другие подюниты класса Alien

2. Сделайте так, чтобы заставка циклически повторялась, а по клику завершалась и начиналась игра

3. Добавьте на отображением суммы бонусов отображение времени раунда.

Если всё получилось как нужно – молодцы! Если нет, давайте разберёмся что не так.

Урок 18 Урок 20