Теперь чтобы тестирование и разработка пошли более ускоренно нам понадобятся простые элементы управления процессом игры, и одно из этого – возможность перемещать наш объект в игровом пространстве. Поскольку мы имеем дело не со стандартным окном отображения, а кастомным (мы его сами делаем), то и элементы управления тоже придётся создавать самим. Но это и к лучшему – например, нам потребуются кнопки, которых нет ни в одном стандартном интерфейсе.
Начнем делать кнопку направления движения. Для этого создадим сначала класс наших кнопок и дадим ему общие для всех методы и свойства, а также объявим переменную для хранения экземпляров этого класса:
Дадим константы идентификаторов будущих кнопок:
Добавим пока только одну кнопку:
А теперь её отображение:
Проверим что получилось:
Отлично! Кнопка готова. Теперь нужно сделать так чтобы она принимала действия пользователя, но вначале изменим немного построение системы координат игрока. Уберём лишнюю координату и укажем ещё две:
Координата игрока на экране по Y больше будет не нужна, её заменит новая координата игрового поля по Y, т.к. диапазон доступный для перемещения игрока по Y соответствует размеру игрового поля по Y. По X конечно же всё как и было, т.к. размер игрового поля по X составляет несколько экранов по X.
Теперь добавим ещё парочку методов для самого движения:
Добавлю имплементацию в класс интерфейса ответственного за перехват кликов по экрану устройства:
Теперь инициализируем в конструкторе GameEngine на этот интерфейс, указывая текущий класс для реализации его методов:
Далее реализуем метод (он единственный) этого интерфейса. Он будет таким:
Теперь сделаем отображение рельефа местности исходя из текущих координат игрока:
А положение рельефа получается таким:
В результате получается как-то так:
Как теперь стало понятно, в зависимости от клика внутри круглой кнопки (даже в зависимости от области внутри этой кнопки), корабль начинает двигаться в любом направлении.
Задание по уроку:
1. Сделайте так, чтобы при движении рельефа также двигались вместе с ним и юниты инопланетян, ведь они тоже должны двигаться относительно корабля игрока (кроме ещё собственного их движение, которое пока не реализовано).
2. Попробуйте изменить поведение чтобы положение внутри кнопки определяло не координату игрока, а ускорение движение игрока.
3. Сделайте чтобы после группирования частей на центре этого группирования образовывался инопланетный корабль, после этого он “жил” 5 секунд и затем взрывался.
Если всё получилось как нужно – молодцы! Если нет, давайте разберёмся что не так.
Продолжаем практическое изучение языка программирование 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.
Если всё получилось как нужно – молодцы! Если нет, давайте разберёмся что не так.
Продолжаем делать игру, как мы её назвали “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 штук и условно их представьте тоже кругами:
Если всё получилось как нужно – молодцы! Если нет, давайте разберёмся что не так.
В этот летний жаркий день она вышла из квартиры и направилась к лифту. Через минуту уже яркое солнце согревало её тело, а знойный ветер играл с её ресницами и щекотал глаза. Она шла к метро и иногда заглядывая по сторонам замечала пристальные взгляды как молодых парней, так и зрелых мужчин, которые восхищались её внешностью. Она знала, что она красотка и нравится многим, это было видно, и это создавало ей прекрасное настроение. Она шла, и иногда лёгкая улыбка проскакивала по её лицу. Вдруг она обернулась. В нескольких метрах позади неё шёл средних лет мужчина. Его суровый и уставший вид перемешивался со злобным и отчужденным взглядом. Было заметно, что он идет за ней и держит дистанцию. Она его приметила ещё у дома, поэтому сейчас здесь, уже почти у станции метро, это казалось подозрительным. Он совершенно не в её вкусе, а выражение лица даже пугало. Она перешла мост через Портовую улицу, спустилась с него на Кронштадскую и направилась вглубь дворов. Тут она услышала быстро приближающиеся сзади шаги.
– Девушка, давайте познакомимся. Меня зовут Сергей, а Вас как? – Не скажу.- ответила она даже не обернувшись, но широкая улыбка разошлась по её лицу, ведь он всё-таки решился, догнал её и подошёл, хотя и не в её вкусе.- Всё равно смелый мужчина, хотя и надо от него как-то отделаться – не красивый, и больно уж загруженный какой-то,- подумала она. – Девушка, а почему не скажите?- снова послышался голос уже где-то совсем рядом. – Не почему, а по кочану…и ещё по кочерыжке, отстаньте! – Сказала она почти смеясь и тут впервые обернулась. Только теперь она увидела, что за ней почти бежал замечательный парень, который вглядывался в неё с восхищением и интересом. Она быстро отвернулась. – А это не тот мужик! – подумала она,- С этим я бы не против познакомиться, мне очень нравится, хороший такой. Что теперь делать? Она снова повернулась, но за ней уже никто не бежал. Парень стоял с грустью на лице и провожал её взглядом.
Выйдя из двора, она подошла к светофору через проспект Стачек. – Может он всё-таки пошёл за мной?- подумала она и обернулась, но сзади за ней больше никто не шёл.
После того, как мы закончили делать достаточно простую игру “Быки и коровы”, научились базовым методам программирования под Андроид, можно приступить к работе над более сложными вещами. Будем делать 2D игру “леталка-стрелялка”. Такие игры ещё иногда называют плоскими, т.к. движение персонажей происходит только вверх-вниз и вправо-влево. Однако уже здесь потребуются знания несколько большие, чем просто программирование. Возможно, когда вы учились в школе, то спрашивали себя,- Зачем мне эта математика, зачем мне эта физика?- Вот как раз для программирования это всё и нужно, если вы кончено собираетесь делать игры и программы хоть как-то отдалённо моделирующих окружающий нас мир. Как я обычно говорю, программирование это всего лишь инструмент, вроде молотка, чтобы не заколачивать гвозди руками. Но чтобы сколачивать дом, вам нужно всё рассчитать, прежде чем брать в руки молоток и обладать какими-то базовыми знаниями в области строительства. В играх чуть более сложных, чем та, которую мы делали, это тоже становится неизбежно необходимостью. Но огорчаться по этому вопросу не нужно, всё что потребуется из этих наук, я буду это использовать, и тут вам останется либо проверять меня и вступать в полемику, либо поверить наслово. Сразу скажу – идея игры, которую мы будем делать, я позаимствовал с другой, прочно уже забытой игры, но это не меняет нашей задачи. Выглядеть она будет как-то так:
Видео самого экшена:
После её написания, полагаю, вы сможете уже делать любую другую игру. Делать мы её будем совместно, а так же вы сможете:
Итак, приступим, что тут у нас есть; а есть следующее:
Игровое поле, которое чуть меньше экрана по высоте, и на несколько экранов в право и влево.
Есть масштабный экран сверху.
Есть один пользовательский объект, которым он может управлять и перемещать его в пределах экрана.
Само рабочее поле тоже подвижное, но только влево и вправо.
Объекты могут стрелять – пользовательский объект лазером, игровые юниты – каждое своим вооружением.
Игровые юниты могут быть различной конфигурации.
Элементы управления вынесены на рабочий экран.
В игровом поле есть рельеф местности.
Есть юниты испытывающие гравитацию.
Все юниты имеют “физику”, т.е. двигаются с ускорением и торможением.
Игра должна иметь звуковые эффекты.
Игра должна иметь динамические сцены, присущие играм такого типа.
Все это мы будем реализовывать не при помощи каких-то движков, типа 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. Попробуйте нарисовать посередине экрана, как в нашем проекте, космический корабль игрока:
Если всё получилось как нужно – молодцы! Если нет, давайте разберёмся что не так.
Продолжаем изучение 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 и описан тут:
, однако по представленной там картинке и так становится понятным как всё остальное:
Тут хорошо видно, что после onStop() может быть одно из трех, но по нашему алгоритму либо окончательно выйдет из приложения, либо возвратится и повторно создаст наш таймер.
Задание по уроку:
1. Теперь, когда вы знаете метод, который у вас будет постоянно вызываться каждую секунду, реализуйте функционал уменьшения таймера и его сброс каждый ход.
2. Самостоятельно найдите описание и назначение контейнера RelativeLayout.
3. Попробуйте сделать так, чтобы когда оставшееся время становилось менее 10 секунд, таймер начинал показывать десятые доли секунды. Используйте RelativeLayout для отображения долей секунды в уменьшенном виде:
В динамике это выглядит так:
Если всё получилось как нужно – молодцы! Если нет, давайте разберёмся что не так.
Вот вы решили сделать свой сайт, выбрали уже доменное имя, купили хостинг и закинули движок на сервер хостера. К вам стали подтягиваться посетители, вы стали постить тексты, фото, видо и вот вдруг обнаруживается, что места больше нет. Вы заходите в панель управления, а там предлагается объём до 100 ГБ и стоить это будет “космос”. Так что, кина не будет? (цит. из ф. “Джентельмены удачи”). Ну, мы же айтишники, мы понимаем, что в ИТ можно всё!..ну, почти всё))…
Для начала нам потребуется перевести хостинг на VPS. И вы не поверите, но абонентская плата будет меньше за те же ресурсы. По крайней мере у меня на Бегете (на нём я около 5 лет) это было именно так. Кстати, вот партнерская ссылка, по которой и мне будет профит и вам скидка:
Для тех, кто не имел с этим дело скажу. Обычный хостинг – это фактически папка на сервере, где ещё таких как вы могут быть сотни. Один мощный сервер распределяет свои ресурсы между множеством сайтов таких же как и вы клиентов. Поддержка, обслуживание и работы на этом сервере требуют квалифицированный персонал, по этому за это берутся отдельные деньги. VPS – это вам даётся сервер с уже предустановленной операционной системой, в частности Ubuntu, и установленным на неё ПО для работы сайта. Чаще всего это пакет LAMP (Linux, Apache, MySQL, PHP). Сам сервере виртуальный, но для нас это не имеет никакого значения, т.к. ничто не будет отличать его от реального. Но и здесь хостер выделит вам всё равно те же 10 ГБ (начальный уровень), а за всё что больше придётся доплатить…это если вы что-то не знаете, что знаю я)))…Открываем папку моего сайта https://elvinto.ru и смотрим размер свободного месте на диске:
Внимательно смотрим в правый нижний угол – 94 ГБ и з 1.8 ТБ свободно. Теперь взглянем, что в моём тарифном плане в панели управления:
Получается как в сказке “Карлик нос” – избушка изнутри больше, чем снаружи. Так-то оно так, да не совсем. Пора открывать карты)).
Есть у меня на этом VPS-сервере портал в другое измерение, а именно – на мой домашний комп. Весь процесс сводится к монтировании домашней папки в папку VPS-сервера. Домашний сервер у меня тоже на Ubuntu, дома потребуется “белый” IP, по этому и для этого примера тоже буду писать исходя из этого. Пользователь на VPS vpsuser, на домашнем компе – homeuser. В папке homeuser есть папка content, которая и является тем терабайтным хранилищем, которое мы подключаем.
Настроим сначала сервер VPS.
Для этого сначала нужно установить sshfs:
sudo apt-get install sshfs
Теперь надо создать пару ключей, ведь мы же не будем вручную контролировать процесс подключения при каждой перезагрузке и вводить пароль к домашнему серверу.
ssh-keygen -b 4096
В результате получим в папке ~/.ssh пару файлов – публичный и приватный ключ. Теперь публичный ключ можно скопировать себе на домашний комп. Сделал я это просто – выложил на сайте по ссылке прямо в корне elvinto.ru/id_rsa.pub (сейчас там нет этого файла, чтобы не засорять корень), а с домашнего компа скачал из браузера. Бояться, что его кто-то украдёт не надо, т.к. красть его глупо, это публичный ключ и расшифровать закодированную им информацию может только обладатель приватного ключа, который остался в папке .ssh на VPS (вы же ничего не напутали?!!)) ).
Настройка домашнего сервера.
Создайте удобочитаемую папку на том диске, где у вас эти самые терабайты. У меня в примере это /home/homeuser/content. Теперь в папку /home/.ssh закиньте id_rsa.pub и переименуйте его в authorized_keys (если такой файл уже есть, то допишите в конец этого файла ваш id_rsa.pub). У файла authorized_keys обязательно должны быть права (с другими может отказать в авторизации по ключу и потребует ввести пользователь и пароль):
chmod 0600 authorized_keys
Снова продолжаем настраивать VPS.
Теперь нужно в файл /etc/rc.local (если его нет, см.далее) перед exit 0 добавить нужный текст (для сайта на WordPress – расширяется папка wp-content/uploads):
Если файла rc.local нет, значит сервис rc-local просто не включен, такое встречается на Ubuntu >= 20.04. Всё делается просто. Создайте при помощи, например, nano или mcedit этот файл и дайте ему права на запуск. Файл должен начинаться со спец.строки и заканчиваться exit 0:
Теперь нужно включить сервис rc-local:
sudo systemctl enable rc-local
и стартануть его:
sudo systemctl start rc-local.service
Проверяем на VPS папку, куда монтировали, она должна ссылаться на ваш домашний сервер.
П.С.:
Вы должны понимать, что качество вашего сайта после этого будет обеспечиваться двумя основными факторами – стабильность и скорость домашнего инета и надежность домашнего хранилища. Но если речь идет о бесплатном обеспечении сайта терабайтами, то уж постарайтесь это осуществить…как-то))).
Типичная ситуация в работе программиста 1С – приходит вдруг с утра к вам в кабинет финансовый директор компании и заявляет. Товарищ программист, что-то какая-то бодяга у нас в управленческом учете. На кой ляд нам такое количество разных видов приходных документов? И на мелочевку, и на бытовую технику, и продукты питания. Наши экономисты уже забодались бегать от одного документа к другому. Хочу чтобы это всё учитывалось одним документом…и уходит…а вы после этого ещё минут пять сидите вот так:
…но это если вы только начинаете программировать на 1С, а вот если есть небольшой опыт, то сразу же поймете, что задача хотя и нетривиальная, но всё же не такая сложная. Давайте убедимся в этом наглядно. Добавить нужные табличные части в документ поступления товаров услуг – это не проблема, и мы это делать уже умеем. А вот что делать с данными? Как их после этого перебросить в этот документ, ну не вводить же их заново!
Для начала всё-таки модернизируем наш документ ПоступлениеТоваровУслуг, создав там табличные части по тому виду, которые они были в этих документах, но их придётся переименовать, т.к. нельзя в одном объекте делать табличные части или реквизиты с одинаковым названием:
На форму тоже добавим все эти табличные части.
Теперь саму форму нужно немного доработать – помните мы считали сумму по строке при помощи процедуры общего модуля? Она ведь сейчас будет считать только для табличной части “Товары”, т.к. мы это в ней явно указали, а для остальных табличных частей будет выдавать ошибку выполнения, т.к. программа будет пытаться обратиться к текущей строке таблицы Товары, в то время как текущая строка будет находится вообще в другой табличной части. Давайте доработаем код процедуры и сделаем его таким:
Видите, я могу обратиться к табличной части не только “через точку”, но и через именованный массив – программа будет брать имя табличной части из переменной ИмяТабличнойЧасти, которое мы будем передавать при вызове этой процедуры:
Кстати, а вы не заметили, что что-то тут лишнее? Две процедуры выполняют одно и то же. Давайте мы оставим только одну процедуру и дадим ей немного общее наименование, а вторая вообще больше не нужна:
Но на эти процедуры ссылались события в двух колонках – “Количество” и “Цена”, по этому там тоже надо теперь указать эту новую процедуру:
Тоже самое сделать и для колонки количества, указав ту же самую процедуру:
Добавим теперь на каждую табличную часть новую вкладку со своей табличной частью, а то сейчас у нас только товары и услуги. Делается это при помощи добавления новой страницы (это сама вкладка):
, указав элемент “Страница”:
, а затем и самой табличной части перетаскиванием справа налево:
Теперь дадим самой вкладке вразумительное имя по аналогии предыдущими:
Теперь добавьте реквизит документа “ИдентификаторСтарогоДокумента”, значение которого станет понятно чуть позже:
Теперь познакомимся с новый объектом метаданных – Обработка, и сразу же добавим новую с именем “ПереносДанных”:
Сделайте ещё одну подсистему и включите туда эту обработку:
Теперь добавим форму:
Теперь на форму нужно добавить единственную кнопку, по нажатию которой будет происходить перенос данных из существующих документов в наш новый переделанный с множеством табличных частей. Добавьте новый элемент “Кнопка” на форму:
К слову говоря, подобным способом добавляются элементы на все формы, в т.ч. справочников и документов, но мы делали это немного другим способом, т.к. у нас был элемент отображения реквизита и мы просто перетягивали его справа налево.
Кнопка появилась, но теперь нужно добавить команду – реакцию по нажатию на кнопку. Для этого нужно открыть вкладку “Команды” и добавить новую:
Теперь в свойствах кликнуть по созданию обработчика:
В этот раз обработчик будет включать в себя две процедуры – на клиенте и на сервере, т.к. нам понадобятся процедуры и функции, которые характерны только для серверного исполнения. В результате платформа автоматически подготовит их для программирования:
Теперь нужно возвратиться обратно в графическое представление формы и привязать кнопку к команде и дать человеческие наименования:
Переходим снова в код. Делаем так:
Давайте разбираться что тут. Сначала я сделал запрос для того чтобы перебрать все уже созданные документы ПоступлениеМелочевки.
Получаю уникальный идентификатор каждого документа. Этот идентификатор – объект платформы, который легко можно преобразовать в строку. Строка получается 36 символов. Каждый объект данных 1С имеет уникальную идентификацию в пределах одной базы. Она нужна мне чтобы привязать создаваемые автоматически документы с исходным “старым” документом. Такая идентификация мне нужна чтобы однозначно определить перегружался ли этот документ уже или нет, на случай, если я захочу запустить обработку повторно. Повторный запуск может потребоваться, если при первом что-то пошло не так или мы уже успели внести ещё данных по мелочевки “старым” способом.
НайтиДокументПоИдентификатору() – это нестандартная функция, как может показаться, которая будет искать документ по признаку идентификатора; я разместил её в этом же модуле:
Эта функция при помощи простого запроса с условием ищет тот документ ПоступленияТоваровУслуг, где реквизит ИдентификаторСтарогоДокумента будет равен идентификатору исходного документа (мелочёвки, продуктов питания и т.п.). Если результат поиска отрицательный, то функция возвратит значение равное “Неопределено”.
После этого я проверяю удалось ли найти документ. Если не удалось, то будет создан новый и сразу же определен реквизит с идентификатором и дата (без указания даты документ не удастся записать).
Следующие действия – это перенести построчно табличную часть с товарами. Тут должно быть понятно – берём строку исходного документа – создаём строку конечного документа.
В результате получаем что-то вроде этого:
Тут хорошо видно, что два первых документа без идентификатора – это те, что создавали интерактивно (вручную), а два с идентификатором – это те, что переносили обработкой (именно тогда мы его устанавливали). Сам документ теперь выглядит так:
Задание по уроку:
1. Добавьте на форму документа ПоступлениеТоваровУслуг страницу с табличной части “ПродуктыПитания”, сделайте необходимый расчет суммы по строке.
2. Добавьте в обработку перенос документа ПродуктыПитания
3. Ранее я говорил, что перед проведением документа всегда автоматически происходит его запись, однако при этом принудительно делаю перед этим запись. Как думаете, почему я так сделал?
Если всё получилось как нужно – молодцы! Если нет, давайте разберёмся что не так.