05w buzzkill 06s wip, 07s abort, prog-draft change (06>07 09>06)
This commit is contained in:
parent
71bcdcffd2
commit
1acfe2e18a
|
@ -27,7 +27,7 @@
|
|||
@ Что происходит & @ Время & @ Слайды & @ Описание \\
|
||||
\hline
|
||||
\endhead
|
||||
@ Организационный момент & 5 tag(beg) & @ 1-5 & @ Преподаватель ожидает студентов, поддерживает активность и коммуникацию в чате, озвучиает цели и планы на семинар. Важно упомянуть, что выполнение домашних заданий с лекции является, фактически, подготовкой к семинару \\
|
||||
@ Организационный момент & 5 tag(beg) & @ 1-5 & @ Преподаватель ожидает студентов, поддерживает активность и коммуникацию в чате, озвучивает цели и планы на семинар. Важно упомянуть, что выполнение домашних заданий с лекции является, фактически, подготовкой к семинару \\
|
||||
\hline
|
||||
@ Quiz & 5 & @ 6-18 & @ Преподаватель задаёт вопросы викторины, через 30 секунд демонстрирует слайд-подсказку и ожидает ответов (6 вопросов, по минуте на ответ) \\
|
||||
\hline
|
||||
|
@ -172,7 +172,7 @@ catch (Exception ex) { throw new RuntimeException(ex); }
|
|||
|
||||
\textbf{Вариант решения}
|
||||
|
||||
Метод конкатенации моет также быть написан множеством способов, но важно, чтобы при буферизации (чаще всего решения содержат именно буферизованные варианты) не терялись символы, например, пробелы или переходы на новую строку. Также часто встречается дополнительная буферизация внутри программы, например, в строку, при помощи \code{StringBuilder}, что будет являться грубой ошибкой при конкатенации больших или бинарных файлов. Поэтому ниже представлен вариант решения с посимвольным чтением и записью в результирующий файл.
|
||||
Метод конкатенации может также быть написан множеством способов, но важно, чтобы при буферизации (чаще всего решения содержат именно буферизованные варианты) не терялись символы, например, пробелы или переходы на новую строку. Также часто встречается дополнительная буферизация внутри программы, например, в строку, при помощи \code{StringBuilder}, что будет являться грубой ошибкой при конкатенации больших или бинарных файлов. Поэтому ниже представлен вариант решения с посимвольным чтением и записью в результирующий файл.
|
||||
\begin{lstlisting}[language=Java,style=JCodeStyle,label={},caption={}]
|
||||
private static void concatenate(String file_in1, String file_in2, String file_out) throws IOException {
|
||||
FileOutputStream fos = new FileOutputStream(file_out);
|
||||
|
|
|
@ -279,16 +279,16 @@
|
|||
Блок курса & Название & Содержание
|
||||
\\ \hline
|
||||
\endhead
|
||||
Л & Графический интерфейс пользователя & GUI (Swing): Окна и свойства окон, \textbf{Многопоточность} и абстргирование асинхронных вызовов; менеджеры размещений и проведение параллелей с веб-фреймворками, разделение на фронт-энд и бэк-энд; JPanel и рисование, Обработка действий пользователя \\
|
||||
С & Простейшие интерфейсы пользователя & Кнопки, канва для рисования, смена кадров, движение, работа с параллельностью (закрепление материала по многопоточности) \\
|
||||
Л & Интерфейсы & Понятие и принцип работы, implements; Наследование и множественное наследование интерфейсов; Значения по-умолчанию\\
|
||||
С & & \\
|
||||
Л & Обобщённое программирование & \\
|
||||
С & & \\
|
||||
Л & Коллекции & List, Set; Хэш-код, интерфейс Comparable; Map, Object (Использование методов, Переопределение методов); Итераторы, \textbf{Многопоточные} коллекции \\
|
||||
С & Структурирование данных & \\
|
||||
Л & Многопоточность & Понятие, Принцип (реальная и псевдопараллельность); Runnable, Thread (Свойства, Особенности создания); Deadlock, Состояние гонки, Object (Ожидание/уведомление); Синхронизация (Синхронизация по монитору, Частичная, по классу) \\
|
||||
С & Паралеллизм и асинхронность & \\
|
||||
Л & Графический интерфейс пользователя & GUI (Swing): Окна и свойства окон, \textbf{Многопоточность} и абстргирование асинхронных вызовов; менеджеры размещений и проведение параллелей с веб-фреймворками, разделение на фронт-энд и бэк-энд; JPanel и рисование, Обработка действий пользователя \\
|
||||
С & Простейшие интерфейсы пользователя & Кнопки, канва для рисования, смена кадров, движение, работа с параллельностью (закрепление материала по многопоточности) \\
|
||||
Л & Коллекции & List, Set; Хэш-код, интерфейс Comparable; Map, Object (Использование методов, Переопределение методов); Итераторы, \textbf{Многопоточные} коллекции \\
|
||||
С & Структурирование данных & \\
|
||||
Л & Управление проектом: сборщики проектов & Управление проектом: Jar-файлы; Gradle/Maven: зависимости, выгрузка артефакта, публичные репозитории, свойства проекта, приватные репозитории (хостинг);
|
||||
Bazel\\
|
||||
С & Сборка проекта & Maven сборка, Gradle сборка, компиляция jar-файла. Настройка хранения кэша сборщиков, настройка дополнительных репозиториев и аплоуда артефактов.\\
|
||||
|
|
|
@ -1,94 +1,297 @@
|
|||
\documentclass[../j-spec.tex]{subfiles}
|
||||
|
||||
\begin{document}
|
||||
\section{Интерфейсы}
|
||||
\section{GUI: Графический интерфейс пользователя}
|
||||
\begin{longtable}{|p{35mm}|p{135mm}|}
|
||||
\hline
|
||||
Экран & Слова \\ \hline
|
||||
\endhead
|
||||
|
||||
Титул & Здравствуйте, добро пожаловать на курс посвящённый инструментарию разработчика на джава \\ \hline
|
||||
Титул & Здравствуйте, добро пожаловать на курс посвящённый инструментарию разработчика на джава \\ \hline
|
||||
|
||||
Отбивка & и сегодня на повестке дня у нас интерфейсы, но не те интерфейсы, что графические, а те, что программные. \\ \hline
|
||||
Отбивка & и сегодня на повестке дня у нас интерфейсы, но не те интерфейсы, что программные, а те, что графические. \\ \hline
|
||||
|
||||
На прошлом уроке & На прошлом уроке мы поговорили о файловых системах и представлении данных в запоминающих устройствах; Поверхностно посмотрели на популярные пакеты ввода-вывода. Подробно разобрали один из самых популярных ссылочных типов данных String и разные механики вокруг него, такие как стрингбилдер и стрингпул. \\ \hline
|
||||
На предыдущем курсе & На предыдущем курсе были рассмотрены механизмы работы знакомых концепций на примере языка Java, такие как базовое процедурное программирование, ООП, исключения. Было рассмотрено устройство языка Java и сопутствующих технологических решений, платформы для создания и запуска приложений на JVM-языках (Groovy, Kotlin, Scala, и др). Также рассмотрены некоторые базовые средства ввода-вывода, позволяющие манипулировать данными за пределами программы. Получены знания принципов работы платформы Java, понимание того, как язык выражает принципы программирования, его объектную природу. Мы научились писать базовые терминальные приложения и утилиты, решать задачи (в том числе алгоритмические, не требующие сложных программных решений) с использованием языка Java и с учётом его особенностей. \\ \hline
|
||||
|
||||
На этом уроке & Поговорим об интерфейсах, рассмотрим понятие и принцип работы, поговорим о ключевом слове implements; Наследование и множественное наследование интерфейсов, реализация, значения по умолчанию и частичная реализация интерфейсов. Естественно, что говоря об интерфейсах нельзя не сказать о функциональных интерфейсах и анонимных классах. Коротко рассмотрим модификаторы доступа при использовании интерфейсов и немного подробнее поговорим о глубинных процессах, таких как раннее и позднее связывание в объектно-ориентированном программировании. \\ \hline
|
||||
На этом уроке & Сегодня, наконец-то всё самое интересное начинается. Сегодня будем знакомиться со всеми любимыми окошками. Интерфейс - это чрезвычайно важно, поскольку это именно то, что видят наши пользователи. Мы с вами поигрались в консольке, научились делать интересные и полезные (но немного неполноценные) приложения, и именно сегодня пришла пора обернуть нашу логику в красивую обёртку, добавить свистелки-тарахтели и выпускать в жизнь. Сегодня, пожалуй, самое сложное занятие начального этапа, но не потому что оно содержит какую-то очень сложную информацию, а потому что заставит вас необычным образом взглянуть на те принципы ООП, которые вы уже знаете. Сегодня поговорим о создании окна, менеджерах размещений, элементах графического интерфейса, и обработчиках событий. \\ \hline
|
||||
|
||||
06-01 & Разговор об интерфейсах хотелось бы построить не так, как мы это делали на лекциях обычно, а от некоторой практической составляющей, чтобы где-то в середине лекции у вас сложилось явное впечатление, что что-то тут явно можно улучшить, а когда мы применим интерфейсы, вы подумали «ааааа так вот зачем оно нужно и как применяется», а потом уже поговорим о теоретической составляющей и особенностях. \\ \hline
|
||||
06-01 & Сразу хочу закрыть вопрос всех людей, которые когда-либо начинали смотреть эту лекцию: почему именно фреймфорк Swing? Нет, не потому что разработчики Swing платят мне за рекламу, нет, не потому что это модный и современный фреймворк. Скорее всего даже не потому что он пригодится любому программисту на джава. Мы будем пользоваться им потому, что он поможет нам лучше понять ООП. Как работают композиции из объектов, как заставить объекты обмениваться информацией между собой, чем нам помогает ссылочная природа данных в джаве, как удерживать в голове базовые взаимосвязи объектов. И чтобы не выдумывать какие-то искусственные примеры, мы напишем простую игру, например, крестики-нолики с графическим интерфейсом. Почему не джаваФХ? потому что он перестал быть стандартным начиная с джава9, почему не либГДХ? потому что он в итоге всё равно на свинге написан. Ну и взгляните ещё раз внимательно на интеллиж идею, которая почти наверняка у вас открыта - она написана на свинге.\\ \hline
|
||||
|
||||
Отбивка & Приложение для примера \\ \hline
|
||||
отбивка окно JFrame & Ну что, приобщаемся к прекрасному по порядку, создадим первое окошко? для этого нам понадобится то, что в терминах свинга называется джейфрейм.\\ \hline
|
||||
|
||||
06-02 & Итак начнём с преамбулы, здесь я призываю вас не особенно обращать внимание на то, какие именно классы и методы используются, а внимательно следить за взаимодействием и отношениями объектов, потому что интерфейсы, о которых мы сегодня планируем поговорить - это как раз механизм упрощающий и универсализирующий взаимодействия объектов. Код может показаться непростым, но зато задачу мы поставим таким образом, что если делать нормально, то без интерфейсов не обойтись, и когда мы закончим - у вас, как я только что и сказал, должно появиться ощущение что «о, так вот зачем они нужны». Почему будет сложно? потому что несмотря на то что вы уже знаете принципы ооп - надо уметь их применять.\\ \hline
|
||||
06-02 & Прежде чем мы начнём, ещё раз хочу вас успокоить: мы не изучаем фреймворк свинг, поэтому не надо зазубривать названия вообще всех классов и компонентов. Мы изучаем программирование и ООП, на примере и с использованием свинга, поэтому сосредоточьтесь, пожалуйста, на объектах, их свойствах и взаимосвязях. Создадим новый класс для нашей программы, и поскольку интереснее всего писать игры, а не какие-то сложные корпоративные программы, то мы назовём наш класс GameWindow, и приступим. Окошки мы будем рисовать при помощи библиотеки swing, основной для рисования в Java.
|
||||
%%
|
||||
|
||||
06-03 & Итак, что мы будем делать? мы сделаем некий небольшой набросок своего собственного игрового 2д движка, без физики и прочего, а просто с объектами и анимацией, чисто демонстрационный. на его основе можно что угодно запилить, и будет отлично работать. На слайде вы видите окно, кружки летают (имейте ввиду, они перемещаются плавно, если у кого-то что-то дёргается, это интернет с низким ФПС передаёт, а не приложение). пока что ничего тут не обрабатывается, ничего толком не происходит. Итак, самое главное, что нам понадобится, это окно, которое будет как-то взаимодействовать с операционной системой, на окне будет канва, на которой мы будем всё рисовать, и собственно объекты, которые мы будем рисовать. \\ \hline
|
||||
Для того чтобы начать, надо получить доступ к методам, содержащимся в библиотеке, а для этого, наш класс игрового окна должен быть наследником класса JFrame.
|
||||
%%
|
||||
|
||||
06-04 & Быстро создаём окно, буквально константы с размерами, координатами, и конструктор окна прямо здесь же вместе с методом мейн, не вдаваясь в подробности того, как работает фреймворк свинг на котором мы всё это пишем, на эту тему у нас будет отдельный урок, на котором мы закрепим информацию об ООП, многопоточности и отлову исключений в графических интерфейсах. Самое важное для нас сейчас - это то, что окно - это объект с какими-то свойствами и каким-то поведением. Если коротко, стартуем программу, создаём объект окна, говорим, что окно с каким-то заголовком, какого-то размера и находится по каким-то координатам, а когда мы нажмём на крестик, то закрыть нужно будет не только окно, но и программу целиком. \\ \hline
|
||||
Сразу на будущее - все модификаторы видимости будем делать публичными и дефолтными, потому что будем сегодня работать в одном пакете и не очень думать об инкапсуляции. Создадим конструктор.
|
||||
%%
|
||||
|
||||
06-05 & Итак есть такой компонент JPanel, с ним можно много что делать но самое интересное, что можно на нём рисовать. Итак создали наследника Jpanel с названием GameCanvas. Что умеет любой компонент в свинге? он умеет перерисовываться, посредством вызова метода paintComponent. Применим полиморфизм. Из документации мы знаем, что метод пэинтКомпонент вызывается тогда, когда фреймворку надо перерисовать панельку. Мы этот метод переопределим и напишем свою собственную реализацию перерисовки. Давайте сделаем так: создадим конструктор этой пенльки, и будем в конструкторе делать что то незначительное, например, менять цвет фона на синий. Заоверрайдили пэинтКомпонент и тут-же вызвали родительский метод. То есть по сути мы говорим, что нас вполне устраивает то, как перерисовывается панелька, но мы потом захотим туда что-то добавить. Ну и добавим четыре метода, возвращающие границы нашей канвы, левую, правую, верхнюю, нижнюю.\\ \hline
|
||||
А в мэйн классе просто создадим новый объект нашего новоиспечённого класса с окошком. Пока ничего значительно отличающегося от котиков и пёсиков мы не сделали, страшно быть не должно. Если прямо сейчас запустить приложение, оно должно запуститься, и не подав никаких внешних признаков жизни завершиться, это признак того, что мы всё сделали верно, главное, что нет ошибок, а что именно произошло - пойдём выяснять.\\ \hline
|
||||
|
||||
06-06 & Наладим взаимодействие компонентов и привяжем все действия нашего игрушечного движка ко времени физического мира. В нашем основном окошке, в конструкторе, создали переменную класса GameCanvas и расположили её на окне прям в центре. Пока что всё понятно и хорошо. Всё работает. В принципе можно начать писать логику игры прям здесь, в классе панельки, но это архитектурно не очень хорошо, ведь это же канва на которой мы рисуем, значит здесь мы должны по логике только рисованием заниматься. Давайте примем архитектурное решение писать логику игры в нашем классе с кружками, а GameCanvas будет универсальным, чтобы рисовать вообще всё что угодно. Для этого описали в нашем основном окошке метод, назвали его как-нибудь нормально onDrawFrame и в нём будем описывать реализацию цикла для нашего модного приложения, то есть так называемую бизнес-логику. На данный момент это будут два метода - апдейт который будет как то изменять состояние нашего приложения, и рендер, который будет отдавать команды всяким рисующим компонентам. \\ \hline
|
||||
06-03 & Самое интересное это то, каким будет создано наше окно и где. большая часть свойств окна неизменна и задаётся в конструкторе, то есть окно при создании будет наделено какими-то свойствами, которые в некоторых случаях можно будет поменять во время работы этого окна. Самое не очевидное на первый взгляд, но очень логичное, когда окон в программе больше одного - это то, что в библиотеке свинг при нажатии на крестик в углу окна программа не завершается. По умолчанию, все создаваемые окна, как вы могли заметить, если запустили программу с нашим пустым окном, невидимые. Это сделано потому что мы можем создавать сколько угодно окон для нашего приложения, и, согласитесь, было бы не слишком приятно, если бы наша программа закрывалась при закрытии первого попавшегося всплывающего окна, да и окна постоянно лезли бы в глаза пользователю не в нужные моменты, а в момент создания в программе.
|
||||
|
||||
06-07 & Теперь нам надо чтобы при перерисовке наш с вами GameCanvas этот метод дёргал, и тем самым изображение как-то менялось. Для этого нашей канве надо знать как минимум чей метод она будет дёргать, то есть ей нужно знать на каком окне она находится. создаём в классе канвы локальную переменную, которая умеет хранить объекты класса MainCircles и передадим значение этой переменной в конструкторе. И в пэинтКомпоненте будем вызывать controller.onDrawFrame(); Далее, чтобы зациклить это действие мы можем пойти несколькими путями: самый простой - создать постоянно обновляющуюся канву, то есть в методе пэинтКомпонент взять и написать repaint() но это вариант прямо скажем "так себе", он полностью нагрузит одно из ядер процессора только отрисовкой окна - не самое лучшее применение одного из ядер. Второй путь - это применение магии из занятия по потокам. мы можем заставить наш поток какое-то время поспать, начиная с 20-й строки вы видите некоторую конструкцию, смысл которой вам будет понятен немного позже Thread.sleep(16); это даст нам фпс близкий к 60, приемлемо для условной игры, не надо ни больше ни меньше, пожалуй. Получится, что мы отрисовали компонент, посчитали что там изменится апдейтом, отправили обратно на отрисовку рендером, отрисовали пэинтКомпонентом.\\ \hline
|
||||
Для того, чтобы программа всё-же когда-нибудь закрылась, нужно придумать для неё основное окошко, и в нашем случае, на данный момент, оно будет единственным. Чтобы программа завершалась при закрытии этого только что созданного игрового окна (и это первое, что нужно делать при создании одно оконных приложений) мы установим нашему экземпляру JFrame такое свойство, как setDefaultCloseOperation, то есть установим, что нужно сделать, когда это окно закроется. Мы передадим туда константу EXIT_ON_CLOSE, чтобы программа завершалась вместе с закрытием окна. А если этого не сделать, то по умолчанию, окно просто снова сделается невидимым, и приложение не завершится. \\ \hline
|
||||
|
||||
06-08 & Довольно интересно, кстати, как при этом изменился код, вызывающий конструктор канвы, ведь мы из основного класса с окном теперь как-то должны в канву передать это самое основное окно, как вы могли догадаться, для этой цели мы применяем указатель на текущий объект класса this, то есть в конструкторе основного окна мы передали ссылку на экземпляр этого окна канве. немного ломает привычное использование для обращения к полям в конструкторе, но, поверьте, то ли ещё будет. Предлагаю запомнить такой способ применения ключевого слова this, он нам сегодня ещё пригодится. \\ \hline
|
||||
06-04 & Естественно, теперь очень сильно хочется, чтобы окно стало видимым, но не спешите, сначала мы припомним, почему мы можем просто брать и вызывать методы, которые никогда не видели из конструктора нашего окна не обращаясь даже ни к каким объектам через точку? из-за устройства фреймворка Swing, из-за наследования от JFrame, из-за импорта классов Swing
|
||||
|
||||
06-09 & Закончим с отрисовкой и методом onDrawFrame. Он будет обновлять сцену и рендерить её. Для обновления сцены было бы очень неплохо знать дельту времени, которая прошла с предыдущего кадра, чтобы обновлять физику. Конечно можно писать физику, опираясь на частоту кадра, или на то что мы там спим 16миллисекунд, но это всё очень сомнительная опора, потому что мы гарантированно спим 16миллисекунд, но сколько именно мы будем спать неизвестно, потому что отрисовка происходит не через фиксированные промежутки времени и ещё куча факторов. Лучше всего точно знать сколько времени прошло с предыдущего кадра. поэтому сделаем так чтобы метод onDrawFrame эту самую дельту у канвы получал и отдавал методу обновления. Соответственно считаем дельту в канве. Вот, собственно и вся физика, которая нам понадобится. Теперь посмотрим вот на что - у нас в пэинтКомпонент прилетает объект класса графикс, который отвечает, как это ни удивительно, за графику, то есть за рисование. Это же то, что нам нужно. Давайте этот самый объект отдадим нашему методу onDrawFrame, пусть он что-нибудь рисует. а где рисует? на канве, значит нам надо будет как минимум знать её размеры. какой канвы размеры получаем? нашей, той самой. себя и отдадим. onDrawFrame будет распределять - апдейту отдаст дельту и канву, а рендеру - канву и графику. Для универсальности. Все изменения канвы вы видите на слайде, все изменения основного окна ему соответствуют, изменена сигнатура метода онДроФрейм, на неё мы посмотрим через несколько секунд. Пока самое главное, что нам нужно понять об этих двух объектах - канва считает для нас время и постоянно перерисовывает себя, сообщая об этом факте основному окну, а основное окно на этот факт как-то реагирует. Ну и ООП вокруг этого тоже было бы хорошо понимать, объекты передают ссылки друг на друга и вызывают друг у друга всякие интересные методы. \\ \hline
|
||||
...30 сек ждём...
|
||||
|
||||
06-10 & Давайте попробуем что нибудь нарисовать при помощи нашего с вами игрового цикла, например прямую белую линию. Видим, что отлично получается, теперь мы умеем рисовать и наша канва умеет это отображать. Отлично. Наше приложение будет рисовать какие-то объекты, будут это кружки, квадратики, картинки, человечки или какие-то другие объекты - не важно. Важно, чтобы у программы было описан механизм и поведение этих объектов. Это как раз то, о чём я говорил и говорю - применение архитектуры, применение ООП. Мало просто посмотреть уроки, почитать книжки, посидеть на семинаре. Надо сидеть и думать в рамках парадигмы ООП. Это ещё простенькая архитектура. Я понимаю вас, вы смотрите сейчас, и вроде всё понятно, а сами в жизни бы такое не написали. Я сам несколько лет назад такое в жизни не написал бы, так что не переживайте, всё придёт с опытом. Главное, не путайте понятия уметь программировать и знать язык программирования. В какой-то момент вы поймаете себя на мысли, что вы не думаете о циклах и условиях, а думаете на следующем уровне абстракции, думаете о взаимосвязях, об архитектуре, а пальцы сами набирают какие-то языковые конструкции. \\ \hline
|
||||
конечно же это происходит из-за наследования, наше окно теперь не просто использует жфрейм, а само является жфреймом. это и есть то самое применение ООП в жизни.\\ \hline
|
||||
|
||||
06-11 & Соответственно, рисовать просто линии круги и прочее - довольно скучно, поэтому будем рисовать объекты, Создадим класс Спрайт. Ни от чего наследоваться не будем. Просто опишем общее для всех рисуемых объектов в нашей программе поведение. Что может быть у всех объектов в приложении общего? размеры, местоположение. Обычно, когда вы начинаете изучать какой-то графический фреймворк вы замечаете, что начало координат у этого фреймворка находится в верхнем левом или нижнем левом углу. Однако очень часто, когда пишутся какие-то игры или другие приложения с использованием графики в качестве координат используется центр объекта. То есть надо условиться - что х и у - это центр любого визуального объекта на нашей канве. И соответственно удобно хранить не длину-ширину, а половину длины и половину ширины. А границы объекта соответственно будем отдавать через геттеры и сеттеры. Дополнительно научим наш спрайт рисоваться. Ну как научим, просто скажем, что он умеет обновляться и рендериться, а его наследники пусть уже решают, как именно они хотят это делать. Но спрайты лишены логики, они ничего не знают о том, как именно будут меняться их свойства. \\ \hline
|
||||
06-05 & Быстро добавим всяких констант для нашего окошка. Как мы помним, считается хорошим тоном не держать в коде никаких магических цифр, чтобы не думать, что они значат и почему они именно такие, и что будет если их поменять, поэтому объявим ширину и высоту окошка в виде вынесенной константы. Я уже говорил, что принято константы писать большими буквами, или оставил вас в жестоком неведении? пишем высота = 555, ширина = 507, положение окна по оси икс = 800, положение окна по оси игрек = 300.
|
||||
%%
|
||||
|
||||
06-12 & Поэтому естественно нужно создать класс собственно шарика, который будет по нашему экрану прыгать, а то непорядок какой-то. В конструкторе задаём ему рэндомные размеры. Давайте придумаем ему какие-нибудь глобальные свойства. Можно конечно придумать класс, который будет направлять наш спрайт и вообще задавать ему скорость и прочие физические величины, но мы ж с вами пример пишем, так что придумаем ему просто - скорость по осям х и у соответственно и цвет. для цвета есть совершенно неожиданно называющийся класс Color которому можно задать величины в формате РГБ в диапазоне от 0 до 255. Для шарика переопределяем апдейт и рендер. суперы нам не нужны, они всё равно пустые. Самый простой рендер - мы объекту графики зададим цвет текущего шарика и сделаем fillOval, которому передадим лево, верх, ширину и высоту. Несмотря на то что наши объекты содержат поля типа флоут, мы работаем с пиксельной системой координат (что конечно же не подходит для реальных проектов, там нужно всё сразу переводить в мировые координаты (например принять центр экрана за 0, верх и лево за -1 низ и право за 1, как это делает OpenGL) чтобы рендерить экраны). Но это нам и пяти лекций не хватит чтобы вникнуть так что не будем. А в методе апдейт мы просто прибавляем к нашим текущим координатам нашу скорость, умноженную на дельту времени, то есть как в третьем классе. Расстояние, которое должен был преодолеть наш шарик за то время пока наш поток спал и наша канва рендерилась. ну и обрабатываем отскоки, то есть описываем 4 условия, что при достижении границы мы меняем направление вектора. \\ \hline
|
||||
соответственно и настроим размеры окна в конструкторе, настроим позицию на экране стандартными методами. Ну и давайте наконец на него взглянем. По-умолчанию наше окошко невидимое, а мы сделаем его видимым setVisible true.
|
||||
%%
|
||||
|
||||
06-13 & давайте быстренько допилим основной класс и будем переходить к беседе об интерфейсах уже, а то чувствую вы подустали от вступлений. В основном классе мы делаем очень прямолинейно - создаём классовый массив из спрайтов и называться он будет sprites и мы говорим что будет у нас допустим 1 кружчек. В методе апдейт мы пробежимся по всем спрайтам и скажем каждому из них - апдейться так как ты умеешь. В методе рендер мы сделаем тоже самое - пробежимся по всем спрайтам и скажем - отрисуйся так, как ты хочешь. И всё, реализацию обновления и отрисовки мы оставили самим объектам, то есть инкапсулировали в них, только каждый объект сам по себе знает, как именно ему обновляться с течением времени, и как рисоваться, а основной экран уже управляет - на какой канве, когда и кого рисовать. Итак инициализировали наше приложение одним новым шариком и напишем небольшую магию по добавлению новых шариков в динамически расширяемый массив. Как известно, в джава все массивы имеют неизменяемую размерность. Это накладывает некоторые архитектурные ограничения на программиста, но мы ж с вами крутые программисты, вон сколько лекций и семинаров позади, можем обмануть систему. Код не сложный, когда старый массив заполнился, просто увеличиваем его в два раза и переносим шарики из старого в новый. \\ \hline
|
||||
Короткая справка, пока мы наслаждаемся видом нашего первого окна. Окно -- это всегда отдельный поток программы, и окно крутится в бесконечном цикле, в этом бесконечном цикле есть очередь сообщений, которые цикл опрашивает и выполняет. \\ \hline
|
||||
|
||||
06-14 & И осталось эти методы откуда-то вызывать. Мы на нашу канву в конструкторе вешаем слушателя мышки, который нам рассказывает когда произоши какие-то события. Сегодня как раз будем анонимные классы проходить, поговорим об этом явлении подробнее. И по этому произошедшему событию мы говорим - добавь нам спрайт с новым мячиком, у которого будут координаты этого события. Для этого конечно пришлось немного модифицировать класс шарика, добавив в него ещё один конструктор, который сразу задаёт начальные координаты. На правую кнопку мышки повесим удаление спрайта из списков обновления и отрисовки. \\ \hline
|
||||
06-06 & Чтобы чуть получше начать понимать про многопоточность - давайте напишем sout(method main is over), запустим нашу программу и внимательно смотрите, видим, окошко создалось, в консоли видим, что работа метода мэйн закончилась, а наше окошко всё равно выполняется, его при желании можно подвигать, изменить его размер и всё такое. Это и есть наглядная демонстрация многопоточности. Но подробнее о том как она работает вы будете узнавать чуть позже на этом курсе. Вот и получается, что когда мы создаём новое окно - нам не нужно его ни в какой контейнер помещать, ни думать, как оно там будет взаимодействовать с пользователем, оно создастся и будет жить своей жизнью. Инкапсуляция. Если мы захотим что-то ещё писать в мэйне, никто не запретил, и потоки будут выполняться параллельно, асинхронно. \\ \hline
|
||||
|
||||
06-15 & Получается, мы меньше чем за полчаса написали довольно простое и очень хорошо расширяемое приложение, которое не только рисует вещи, но и на действия пользователя реагирует, кажется, неплохо. Напомню, что самое главное, что мы должны из этого приложения извлечь - это взаимодействия и взаимовлияния объектов. Наследование, полиморфизм, инкапсуляция поведений и свойств. Если честно, я слегка устал от синего цвета нашего фона и решил создать отдельный класс фона, но сразу столкнулся с необходимостью думать головой. Логично было бы предположить, что фон - это спрайт, имеющий прямоугольную форму и всегда рисующийся первым. но вот беда, при изменении размеров окна фон тоже желательно изменить в размерах. Поэтому в отрисовке я просто говорю канве, что она должна изменить свой цвет фона, а что, ссылка то на канву у меня есть. Цвет фона я меняю синусоидально по каждому из трёх компонент цвета, поэтому изменение происходит довольно плавно. В общем, получается, что от спрайта нам фактически нужно только поведение, а свойства не нужны. Но и отказаться от наследования нам бы не хотелось, потому что тогда мы не сможем фон единообразно в составе массива спрайтов обновлять. Это наталкивает нас на мысль об унификации поведения, на мысль об интерфейсе. \\ \hline
|
||||
06-07 & Подведём некоторые промежуточные итоги. Отвечайте первое, что приходит вам в голову. Чтобы создать пустое окно в программе нужно 1. импортировать библиотеку Swing 2. создать класс MainWindow 3. создать класс-наследник JFrame
|
||||
|
||||
Отбивка Понятие интерфейса & Механизм наследования очень удобен, но он имеет свои ограничения. В частности мы можем наследовать только от одного класса, в отличие, например, от языка С++, где имеется множественное наследование \\ \hline
|
||||
...30сек...
|
||||
|
||||
интерфейс - это описание способов взаимодействия с объектом. фото руля-педалей-коробки, фото юсб, фото клавиатуры-мышки-монитора & В языке Java эту проблему частично позволяют решить интерфейсы. Интерфейсы определяют некоторый функционал, не имеющий конкретной реализации, который затем реализуют классы, применяющие эти интерфейсы. И один класс может применить к себе множество интерфейсов. Правильно говорить реализовать интерфейс, будем сразу говорить правильно. Если сказать проще, интерфейс можно очень-очень грубо представить как очень-очень абстрактный класс. До седьмой джавы это был просто набор методов без реализации. Начиная с восьмой наделали много тонкостей, с ними и будем разбираться. Итак интерфейс - это описание методов. Примером интерфейса в реальной жизни может быть интерфейс управления автомобилем, интерфейс взаимодействия с компьютером или даже интерфейс USB, так, компьютеру не важно, что именно находится по ту сторону провода, флешка, веб-камера или мобильный телефон, а важно, что компьютер умеет работать с интерфейсом USB, отправлять туда байты или получать. Потоки ввода-вывода, которые мы проходили чуть раньше - это тоже своего рода интерфейс, соединяющий не важно какой программный код и не важно какой, например, файл. Все методы во всех интерфейсах всегда публичные, и в классическом варианте не имеют реализации. Ну и поскольку все методы всегда паблик то этот модификатор принято просто не писать. Для новичка это неочевидно и сбивает с толку, может показаться что модификатор дефолтный, а на самом деле он публичный, поблагодарим разработчиков джавы и пойдём дальше. \\ \hline
|
||||
конечно, создать класс-наследник жфрейма, как мы это сделали только что для нашего приложения. Свойства окна, такие как размер и заголовок возможно задать 1. написанием методов в классе-наследнике 2. вызовом методов в конструкторе 3. созданием констант в первых строках класса
|
||||
|
||||
06-16 & Интерфейсы объявляются также, как классы, и вообще могут иметь очень похожую на класс структуру, то есть быть вложенным или внутренним, но чаще всего интерфейсы описывают в отдельном файле, также как класс, но используя ключевое слово интерфейс. Создадим пару интерфейсов, например, человек и бык, опишем в них методы, например, ходить и издавать звуки. \\ \hline
|
||||
...30сек...
|
||||
|
||||
06-17 & Для чего мы так сделали? Продолжим пример, создадим пару классов, класс мужчина и класс был. Класс мужчины будет у нас реализовывать интерфейс человека. То есть множественного наследования нет, но мы можем реализовать сколько угодно интерфейсов. Для того, чтобы реализовать интерфейс - мы должны переопределить все его методы, либо сделать класс абстрактным. Вот, статический анализатор кода в идее нам об этом явно говорит. Выведем модное сообщение о том что это классы мужчины или быка.
|
||||
И теперь самая соль - мы можем в мэйне объявлять не только классы и создавать объекты, но и создать переменную, которая реализовывает интерфейс. То есть тут могут лежать абсолютно никак не связанные между собой объекты, главное, чтобы они реализовывали интерфейсА. И мы можем работать с методами интерфейса, которые могут быть для разных классов вообще по-разному реализованы. Такой вот полиморфизм. Понимаете насколько сильно это отличается от наследования, когда мы с вами создавали общий абстрактный класс животное и от него наследовали наших котиков? \\ \hline
|
||||
вызовом методов в конструкторе окна, потому что наше окно теперь наследует все свойства и методы окна в библиотеке \\ \hline
|
||||
|
||||
06-18 & Чтобы стало сильно понятнее, создадим класс минотавра, кто плохо помнит греческую мифологию, это такой товарищ, который с телом человека и головой быка скучно сидел в лабиринте и ждал заблудившихся путников. соответственно, реализовывал интерфейсы человека и быка своим способом, ходил на ногах человека, но не мычал, как бык, а загадки загадывал. Интересно то, что в программе мы можем к минотавру обратиться не только как к человеку, но и как к быку, то есть гипотетически, можно создать некоторого Тесея, погонщика минотавровых стад. Но это уже, так сказать, полёт фантазии, нас интересует только техническая часть вопроса - классы не связаны между собой наследованием, а обращение к ним единообразное. \\ \hline
|
||||
|
||||
06-19 & Также важно, что в интерфейсах разрешено наследование. То есть у нас интерфейс может наследоваться от другого интерфейса, соответственно при реализации интерфейса мы должны переопределить методы всех родителей нашего интерфейса, то есть тут картина очень похожа на наследование классов, но внимание не запутайтесь, в интерфейсах разрешено множественное наследование. \\ \hline
|
||||
|
||||
06-20 & На несколько минут вернёмся к бэкграунду, пока мы его не забыли, применим наши новые интерфейсные знания на практике. Фон наследуется от спрайта, но ему от спрайта вообще ничего не надо, кроме двух методов. в которых он полностью пишет собственную реализацию, которая никак не коррелирует с тем, как ведут себя остальные спрайты. И вот сложилась ситуация в которой нам надо хранить в одном массиве очень похожие объекты но наследовать их друг от друга не логично. Как поправить? \\ \hline
|
||||
|
||||
06-21 & Напишем некий интерфейс, назовём его GameObject и скажем, что у него есть методы апдейт и рендер, без реализации. То есть это будут некие объекты которые должны уметь рисоваться и обновляться. Идём в спрайт, и говорим, что мы реализуем интерфейс гейм объекта. Смотрим, что сломалось, кто навскидку скажет, почему? Не нужно судорожно хвататься за клавиатуру, но мысль правильная - модификатор должен быть паблик. И Фон тоже теперь у нас реализует интерфейс гейм объекта. При этом получается, то фон вообще никак не связан со спрайтом, у них даже набор полей разный. Но оба умеют рисоваться и апдейтиться, благодаря интерфейсу. Быстро поправив основное окно и логику, сменив массив спрайтов на массив геймОбъектов запускаем и видим как бодро летают наши шарики. \\ \hline
|
||||
|
||||
06-22 & И вот мы подошли к самому главному, той гибкости, которую даёт нам работа с интерфейсами. Если вы обратите внимание, как развивается наше повествование по курсу, вы заметите, что сначала мы выходили за пределы одного метода, потом за пределы одного класса, затем за пределы одного пакета, за пределы программного кода, а теперь вовсе хотим написать код, который возможно будет использовать несколькими программами. Посмотрим со стороны, что мы тут понаписали. У наших классов канвы и спрайта, а также у интерфейса нет никакой специфики, их можно применить где угодно. Универсальные получились штуковины. Создадим какую-то условную вторую игру: новый пакет, новый класс, скопипастим немного кода, чтобы всё запускалось. И не писать же нам по новой спрайты и интерфейсы. сделаем правильное дробление по пакетам. то есть получается, что у нас есть некий библиотечный пакет, и какие-то игры с конкретными реализациями. создадим пакет КОММОН и переносим туда канву, спрайт и геймобъект, и ща будем всё чинить. Структура же понятна, только что её всесторонне обговорили? \\ \hline
|
||||
|
||||
06-23 & Гейм объект это интерфейс, ему вообще на всё плевать, только импорты лишние выкинем. Спрайт тоже не сломался, модификаторы доступа поправим на публичные и защищённые, где это возможно. Второе главное окно также инитим конструктор, размеры, положение. И вот хотим воспользоваться нашей канвой. Но не даёт же. почему, мы ж так классно всё придумали, никакой специфики. Но нет, канва то может принимать в конструкторе mainCircles. и канва уже у этого класса вызывает метод onDrawFrame(). Как это решается? это решается интерфейсом. нам надо написать какой-то интерфейс вроде canvasPaintListener, который будет уметь ждать от канвы вызов метода и как-то по своему его реализовывать. А то у нас получается катастрофа - игры зависят от общего пакета, а по хорошему общий пакет вообще ничего не должен знать о том, какие игры он помогает создать. Создаём в общем пакете интерфейс с одним методом, который имеет сигнатуру из нашего mainCircles. И перепишем канву, чтобы она не какой-то класс с шариками на вход принимала, а canvasPaintListener, назовём переменную листнер. и везде используем слушателя через интерфейс. В главном окне мы говорим что классы вы конечно хорошие, с прекрасным наследованием от фреймворка, но только ещё будете реализовывать интерфейс слушателя канвы. а метод у нас уже правильно написан, с верной сигнатурой. Чтобы подчеркнуть, что это реализация интерфейса, напишем аннотацию оверрайд. \\ \hline
|
||||
|
||||
06-24 & Насладимся летающими шариками в одном приложении, и летающими квадратиками в другом. Теперь на основе нашего очень хорошо отделённого интерфейсами общего пакета можно штамповать такие приложения практически без усилий. Понимаете, как разработчики в игровых студиях делают по сотне очень похожих игр в год? \\ \hline
|
||||
|
||||
отбивка Анонимные классы & Программные интерфейсы открывают перед разработчиком широчайшие возможности по написанию более выразительного кода. Одна из наиболее часто используемых возможностей - анонимные классы. \\ \hline
|
||||
|
||||
& Итак все мы знаем
|
||||
что классы это такой новый для нашей программы тип данных. Мы уже привычными движениями создаём публичные классы в отдельный файлах и пишем в них логику, но это не всё, что позволяет нам джава. Классы также бывают вложенными и внутренними. Внутренние классы - это классы, которые пишутся внутри класса, который описан в файле, мы его даже можем использовать и вызывать его методы. А также вложенные или локальные классы, которые мы можем объявлять прямо в методах, и работать с ними, как с обычными классами, но область видимости у них будет только внутри метода.
|
||||
К чему я это веду? к анонимным классам. Давайте переименуем наш интерфейс во что то вроде MouseListener и опишем в нём пару методов, например mouseUp(), mouseDown(). Перейдём в наш основной класс и скажем, что наш локальный класс будет реализовывать интерфейс и значит должен переопределять все его методы. Видим. Теперь мы можем экземпляр этого класса создать и даже попользоваться его методами. \\ \hline
|
||||
|
||||
& Пока что ловите мысль?
|
||||
Очень часто, какието элементы управления (кнопчки, события входящих датчиков (клавиатура, мышка) требуют на вход каких нибудь слушателей, которые будут отлавливать их события и вообще знать что делать и это всё интерфейсы. Значит если мы туда передадим что-то, что реализует интерфейс, то это нечто уже начнёт ловить события и как-то их обрабатывать. Соответственно весьма часто такие классы создаются без имени, прямо в аргументе методов. Ну и действительно, зачем ему имя, если он будет использован только один раз и только в этом методе? Вот допустим у нас есть переменная MouseListener listener и нам нужно создать туда какой-то экземпляр, который реализует этот интерфейс. для этого есть такой синтаксис:
|
||||
new MouseListener дальше пишем интерфейс, который реализуем
|
||||
{} и у нас по сути открывается объявление класса, где мы пишем реализацию. И получается что мы создаём один экземпляр анонимного класса, который реализует интерфейс MouseListener. И конечно потом мы этот анонимный класс уже кладём в идентификатор (или можем прям вот так передать в метод). Конечно мы можем избежать этого, сейчас поговорим как, но в чужом коде это сплошь и рядом, вот такой замороченный синтаксис, спасибо разработчикам джава.
|
||||
Понятен-ли этот синтаксис? Это создание объекта анонимного класса, никак не называющегося, реализующего интерфейс.
|
||||
Тут можно поговорить о лямбдах, для того чтобы сократить синтаксис мы можем просто убрать то что неизменно для этого анонимного класса - название интерфейса, название метода и класс аргумента. \\ \hline
|
||||
отбивка Компоненты и менеджеры размещений & Пустое окно - это скучно, поэтому на него обязательно надо что-то добавить и как-то это расположить. поговорим об этом. \\ \hline
|
||||
|
||||
& \\ \hline
|
||||
& \\ \hline
|
||||
& \\ \hline
|
||||
& \\ \hline
|
||||
& \\ \hline
|
||||
|
||||
\end{longtable}
|
||||
\end{document}
|
||||
|
||||
****** Вернёмся к нашим кружочкам
|
||||
Смотрим прям первый метод, invokeLater() он принимает на вход какой-то Runnable. Идём в Runnable и видим, что это интерфейс, который реализует один единственный метод run(). Получается, что мы в invokeLater передаём новый экземпляр анонимного класса который РЕАЛИЗУЕТ ИНТЕРФЕЙС Runnable, вот описание этого класса, мы в нём переопределяем метод run. Вот так это читается. Ровно тоже самое мы сделали с mouseListener. Здесь немного сложнее. Есть интерфейс MouseListener в котором описаны вот эти все методы. И есть специальный класс MouseAdapter в котором все эти методы уже реализованы. Реализации пустые, но они есть, поэтому мы можем не все методы оверрайдить, а только те, которые посчитаем нужными. И когда мы этот MouseAdapter отдаём нашему методу, мы говорим: создай нам новый экземпляр анонимного класса, который НАСЛЕДУЕТСЯ ОТ КЛАССА MouseAdapter и переопредели вот этот метод. Остальные оставляй пустыми. Чувствуете разницу? Как можно избежать таких конструкций и при этом получить понятный код без лишних заморочек? Очень просто. Скажем, что наш класс - реализует тот или иной интерфейс, и переопределим все методы. В некоторые из них даже напишем реализацию, и туда, где требуется интерфейс - передадим себя. Мы же теперь MouseListener, да и Runnable.
|
||||
|
||||
***** Давайте начнём сразу писать графическую оболочку
|
||||
для игры крестики-нолики, для этого напишем заголовок setTitle, запретим пользователю изменять размеры нашего окна, для крестиков-ноликов это будет важно, чтобы всё красиво отображалось и никуда не уплывало пишем setResizeable. И начнём говорить об элементах графического интерфейса. Элементы это всем нам знакомые кнопочки, текстовые поля, лейблы, и всякое остальное. Начнём с кнопки «новая игра».
|
||||
За кнопки отвечает класс JButton, его экземпляр мы и создадим, JButton btnNewGame = new JButton(); можно сразу в конструкторе задать надпись, которая будет отображаться на кнопке пишем надпись «New Game». и сразу напишем ещё одну кнопку, например, «выход». Давайте одну из них добавим на окошко. для этого воспользуемся методом add(), который просит в сигнатуре передать ему какой-то компонент.
|
||||
Все кнопки-лейблы - это наследники класса Component. Можем проследить по иерархии, дойдём до класса Компонент. Итак вот мы в конструкторе её добавили, и можем запустить, увидеть, что наша кнопка заняла всё окно нашего приложения. давайте уберём вызов метода setResizeable, и увидим, что если мы решим поменять размер окна, размер кнопки также будет меняться. Такой, адаптивный дизайн получается.
|
||||
Теперь попробуем добавить вторую кнопку, и видим, что вторая кнопка полностью перекрыла первую.
|
||||
***** Пришла пора поговорить о компоновщиках,
|
||||
или как их ещё называют, менеджерах размещений. Очень рекомендую погуглить какого нибудь дополнительного материала на эту тему, она не очень простая
|
||||
=======рисовать===========
|
||||
По умолчанию в библиотеке swing используется компоновщик borderLayout. Он располагает всё, что вы ему передаёте в центре, но также у него есть ещё четыре положения. маленькие области по краям. Поэтому, если мы хотим какой-то наш компонент расположить где-то не в центре, мы должны это явно указать при добавлении. Тут немного неочевидно, поэтому запомните, пожалуйста, что при добавлении надо указать ещё один параметр, константу BorderLayout.SOUTH. вот так неожиданно сторона света приплелась сюда, юг.
|
||||
Давайте попробуем поиграться, добавим какой-то другой лэйаут, например, setLayout(new FlowLayout()); он будет располагать элементы друг за другом в том порядке, как мы пишем, слева направо, сверху вниз. Ну и так далее
|
||||
всякие ГридЛэйауты и прочие, некоторые рассмотрены в методичке, некоторые можно увидеть на вышеупомянутых вебинарах, давайте лучше сегодня побольше поговорим о коде нашего приложения. Если каких-то принципиальных вопросов по компоновщикам нет, давайте продолжим. Основная идея, которую надо понять, что в свинге вся работа происходит через компоновщики - вот эти вот самые лэйауты, которые как-то по-своему располагают элементы в окошке.
|
||||
***** Одними лэйаутами сыт не будешь,
|
||||
так что джависты придумали использовать не только компоненты сами по себе, но ещё и группы элементов, группы элементов складывают на так называемые JPanel. И внутри каждой панели мы можем использовать свой лэйаут. (рисуем окно и две панельки, большую и под ней маленькую)
|
||||
Внутри нижней панели у нас будет грид лэйаут, а на верхней мы будем ручками рисовать наше поле. Ну и от слов к делу, создадим ещё один класс, назовём его Map, и он у нас в программе будет отвечать за поле для игры, унаследуем его от JPanel, опишем конструктор, и пока что её просто сделаем чёрной, чтобы увидеть её на окне. для этого воспользуемся вот такой константой setBackground(Color.BLACK). ну и создадим для неё метод startNewGame(), который на вход должен что то принимать.
|
||||
А что принимать, давайте подумаем: у нас будут два режима игры, компьютер против игрока и игрок против игрока, первый мы напишем, второй будет домашним заданием, но пишем, int mode, дальше, что естественно, размер поля, и давайте сразу не будем привязываться к квадратному полю, поэтому сделаем int field_size_x, int field_size_y. и соответственно для нашей логики из третьего занятия, нам понадобится выигрышная длина. давайте сейчас на всякий случай поставим здесь так называемые заглушки, чтобы мы знали, что метод вызывается и всё у него хорошо, напишем sout(mode, sizes, win_len)
|
||||
***** Сразу давайте всю архитектуру опишем,
|
||||
наше приложение будет работать в двух окнах, первое будет стартовое, где мы зададим настройки поля и выберем режим игры, и основное, где будет происходить собственно игра. Основное окно у нас есть, и при его закрытии мы выйдем из программы, давайте опишем стартовое окно, для этого создадим ещё один класс, назовём его StartNewGameWindow, унаследуем от JFrame, делаем конструктор. в нашем GameWindow нам понадобится две константы, одна класса StartNewGameWindow чтобы мы могли это окошко показывать когда захотим и вторая это наша панелька для Map.
|
||||
Сразу скажу, что нашему вспомогательному окошку понадобится основное, поэтому в конструктор мы его передадим, ну чтобы красиво отцентрировать его относительно основного, а не лишь-бы куда пихнуть. в основном окне вызовем startNewGameWindow = new SNGW(this), вот, кстати, ещё один способ применять this, когда нам надо передать методу объект, который вызывает этот метод.
|
||||
Теперь создадим панельку для кнопочек в основном окне, для панелей у нас есть класс Jpanel, пишем JPanel pBottom = new JPanel(); в ней как мы помним из чудо картинки мы можем поставить любой лэйаут, и будем использовать setLayout(newGridLayout); который на вход будет у нас требовать два параметра, количество строк и количество столбцов на которые он будет стараться всё поделить. для нас это будет 1 и 2. и на неё мы добавим наши кнопки. pBottom.add(btnNewGame); и pBottom.add(btnExitGame); Создадим нашу карту, map = new Map; и добавим все эти чудные элементы управления на окошко add(map, borderLayout.CENTER), add(pBottom, BL,SOUTH); и вот после всего того что мы описали наше приветственное второе окно должно стать видимым, чтоб мы выбрали с вами параметры для нашей новой игры, startNewGameWindow.setVisible(true);
|
||||
***** Переварили?
|
||||
теперь возьмёмся за описание последовательности выполнения нашей программы. в основном окне нам понадобится метод, инициализирующий новую карту, помните мы там для панельки конструктор писали? вот вызывать мы его будем отсюда. копипастим void startNewGame() из класса мэп, и вызываем из метода основного класса метод из панельки map.startNewGame.
|
||||
Зачем мы так делаем, казалось бы усложняем, но нет, смотрите, панелька находится на основном окне. а кнопка начала игры будет находиться на другом окошке, которое вообще не в курсе, какие у нас там панельки, или может там вообще дальше нет никакого интерфейса. В этом и есть суть ООП, когда один объект максимально отделён от другого, и каждому из них вообще наплевать, как реализован другой. Соответсвенно, мы в стартовом окошке нажали кнопку начать игру, и оно главному окну говорит - давай, начинай новую игру, а главное окно уже знает, что оно разделено на панельки, и говорит мэпу, мол, давай, пора начинать. понимаете для чего вот этот промежуточный метод?
|
||||
Чтобы не делать лишних связей между классами. это логично с точки зрения абстракций. Одно окно не должно никак управлять панелькой на другом окне. И таким образом мы уже прям почти закончили с классом GameWindow.
|
||||
осталось только поговорить здесь о последней на сегодня теме - об обработчиках событий. Тут на данный момент для вас будет магия. Потому что чтобы полноценно объяснить как повесить листнер, который будет отслеживать нажатия кнопки, для этого нужно как минимум объяснить интерфейсы и анонимные классы, а это мягко говоря не на пять минут. Так вот, давайте определимся, что это магия и это надо просто запомнить.
|
||||
***** Итак, как повесить листнер
|
||||
и заставить нашу кнопку реагировать на нажатия? Давайте опишем кнопку выхода, она попроще, у кнопок есть такой метод как addActionListener() и ему на вход нужно передать переменную типа экшнЛистнер. Которая требует на вход интерфейсы, а их мы на J1 не проходим, поэтому просто запомните конструкцию, пожалуйста, и не забивайте себе пока что голову. пишем в метод new ActionListener, жмём энтер и среда сама вставляет вам вот такой страшный шаблон, в который надо писать что должно происходить при нажатии на кнопку. Здесь мы будем пользоваться методом exit класса System, которому передадим стандартное для корректного завершения программы число 0. Систем.Экзит полностью грохнет нашу программу и освободит от неё все ресурсы, так что это самый простой и гарантированный способ убить процес. Что называется, топором. Так вот создание обработчиков - это единственная магия, которая есть на J1. Если разобраться в ситуации - то мы создаём экземпляр анонимного класса, который реализует интерфейс. Вот оно вам сейчас надо, в подробностях? по моему для первого уровня сложновато.
|
||||
Кнопка newGame оживляется таким же образом. пишем новый листнер, и внутри мы делаем ни что иное, как показываем наше моднное второе окошко с настройками новой игры. startNewGameWindow.setVisible(true); тут то нам и пригодится то, что закрытие окна не убивает программу, а уходит в невидимость
|
||||
вот на этом таки создание нашего класса основного окна заканчивается.
|
||||
***** Давайте пилить стартовое окно.
|
||||
Объявим константы, заботливо подобранные заранее, ширина окна 350 высота 230, минимальная выигрышная длина 3, минимальный размер поля 3, и максимальные размеры 10, 10. эти значения нам нужны будут для создания слайдеров. И давайте сразу объявим переменную типа GameWindow gameWindow, и зададим ей значение из конструктора. this.gameWindow = gameWindow. установим ширину и длину. Теперь давайте, чтобы сделать красиво и ровно посередине, воспользуемся ещё одним новым на сегодня классом, Rectangle, который представляет собой абстрактный прямоугольник. назовём его gameWindowBounds и создадим, = gameWindow.getBounds(), сразу передав в него значения границ нашего окна. у окон есть и такой метод, getBounds(), да. В общем это просто удобный класс для работы с прямоугольниками, то есть с окнами например. И теперь делаем немного математики, задаём переменную
|
||||
int pos_x = (int)gameWindowBounds.getCenterX() - WIN_WIDTH / 2;
|
||||
int pos_y = (int)gameWindowBounds.getCenterY() - WIN_HEIGHT / 2;
|
||||
setLocation(pos_x, pos_y);
|
||||
setResizeable(false);
|
||||
setBorder(new EmptyBorder(3, 3, 6, 3));
|
||||
Сделаем заголовок, СОЗДАНИЕ НОВОЙ ИГРЫ. расположение сделаем сеткой setLayout(new GridLayout(10, 1)); Дальше нам надо добавить сюда много-много разных управляшек, и эти однотипные по сути действия мы для удобства вынесем в отдельный метод void addGameControlsMode(){};
|
||||
Для начала добавим надпись, для этого существует класс JLabel, пишем add(new JLabel(“Choose gaming mode”)); в конструктор можем передать собственно надпись, а можем ничего не передавать и задать её потом, но для этого надо будет поместить надпись в какой-то именованный контейнер, для нас сейчас это, понятно, лишнее.
|
||||
Дальше нужны будут две радиокнопки, которые пригодятся не только в этом методе, поэтому объявление вынесем вверх, а присваивание значений сделаем в нашем методе, который всё добавляет. выносим
|
||||
private JRadioButton humVSAI;
|
||||
private JRadioButton humVShum;
|
||||
и присвоим humVSAI = new JRadioButton(“Play VS Computer”); с надписью переданной в конструктор, и вторую также. добавим на наше окно и увидим что всё неправильно работает. add(hva); add(hvh);
|
||||
Чтобы всё заработало как мы ожидаем есть класс который связывает радиобатоны в группы, что видно из его названия ButtonGroup gameMode = newBG(); gmode.add(hvm); gmode.add(hvh); и саму радиогруппу никуда класть не надо, она просто объединит. и чтобы одна из них была выбрана по умолчанию, используем перегруженный конструктор радио кнопки, (“play vs comp”, true);
|
||||
***** Слайдеры
|
||||
пишем следующий метод addGameControlsFieldWinLen(){} в котором будем описывать наши слайдеры, то есть понятно, что это всё ещё конструктор, просто мы делаем его чисто внешне менее жирным. Ну и чтобы логически разграничить, потому что код уже получается достаточно непростой.
|
||||
Напишем некий подзаголовок add(new JLabel(“Выбери длину”)); и пишем новенький лейбл на котором будем писать выигрышную длину
|
||||
JLabel lbl_win_len = new JLabel(“WIN_LEN” + WIN_LEN));
|
||||
add(lbl_win_len); этот видите мы создаём в два действия, потому что цифру на нём мы будем менять, а не задавать из программы. Давайте объявим слайдеры где-то рядом с JRadioButton'ами.
|
||||
private JSlider sField_size;
|
||||
JSlider sWin_len; и в методе также создадим. в конструкторе слайдеру можно передать три значения, мин, макс и текущее пишем
|
||||
sWin_len = new Slider(MIN_WIN, MIN_FIELD, MIN_WIN);
|
||||
на слайдер мы тоже вешаем слушателя изменений, только тут он называется ChangeListener пишем sWin_len.addChangeListener(new ChangeListener)); и в нём делаем изменение текста для нашего лейбла лбл_вин_лен.сетТекст(“WIN_LEN” + sWin_len.getValue);
|
||||
и добавим его на окно после лейбла. add(sWin_len); дальше создаём такой-же лейбл с размерами поля и аналогично ставим ему текст, добавим на окно. Теперь поинтереснее, создаём слайдер для поля, тоже аналогично, пишем чендж листнер и начинаем прикалываться, введём ещё одну переменную, current_field_size = sField_size.getValue() и будем использовать его в двух местах, во первых нам надо значения на лейбле менять, а во вторых, максимальное значение выигрышной длины тоже менять. лейбл.сетТекст(ФИЛД + каррент); и сВинЛен.сетМаксимум(каррент); и давайте аккуратно по порядку добавим слайдеры и надписи. Вот теперь вы никак не сможете ошибиться и сделать выигрышную длину больше чем размер поля.
|
||||
***** И осталась нам только кнопочка “начать игру”
|
||||
в конце конструктора пишем JButton btnStartGame = new JButton(“New Game”); add(btnSG); вот у нас и получился один столбец и десять строчек. Осталось только оживить кнопочку, для этого напишем последний на сегодня метод назову его btnStart_onClick(),
|
||||
в кнопке пишем btnSG.addActionListener(new ActionListener()); и внутри любезно предоставленного шаблона просто вызовем наш метод, чтобы много не писать. А обработчик очень интересный, в нём нам надо вызвать метод startNewGame из основного окошка, но чтобы сделать это нам надо наполнить сигнатуру разными параметрами, которые мы возьмём с наших управляшек.
|
||||
Давайте в классе Map объявим пару констант, раз мы не изучали с вами перечисления. GAME_MODE_H_V_A = 0, GAME_MODE_H_V_H = 1; и в классе в котором работаем создадим новую переменную инт game_mode;
|
||||
пишем if(rb_hva.isSelected()) значит если кнопка выделена - game_mode = Map.HVA иначе rb_hvh.isSelected() game_mode = Map.HVH; ну и совсем иначе генерим исключение. мало-ли мы через пол года захотим ещё режимов надобавлять, батон добавим, а сюда забудем дописать, или пользователю не отобразится радиобатон, throw new RuntimeException(“No Buttons Selected”); и программа нам подскажет где искать.
|
||||
также с размером поля, которое пока что у нас будет квадратным, хоть мы и архитектурно заложили, что оно может быть не квадратным. эти показания мы просто снимем со слайдеров и передадим в конструктор slider.getValue(); ну и перед тем как закончить обработчик нам надо наше окошко спрятать от глаз пользователя до следующего раза, setVisible(false);
|
||||
ну вот мы выполнили план на сегодня, хоть мы и немного отошли от методички, но мы сделали хороший задел для нашего последнего занятия, нам там надо будет только написать логику, и расчертить полянку
|
||||
***** Домашняя работа
|
||||
1. Полностью разобраться с кодом (попробовать полностью самостоятельно переписать одно из окон)
|
||||
2. Составить список вопросов и приложить в виде комментария к домашней работе
|
||||
3. * Раcчертить панель Map на поле для игры, согласно fieldSize
|
||||
|
||||
|
||||
***** К сложному и интересному:
|
||||
метод paintComponent() вызывается фреймворком когда что-то происходит, например, когда мы перекрываем наше окно каким-то другим, или двигаем само окно, но в любом случае вызывается он гораздо реже, чем нам нужно. Но важно помнить, что мы не должны непосредственно этот метод вызывать из кода. этот метод должен вызываться только фреймворком, но сейчас нам важно не это, сейчас нам важно отделить этот метод от собственно нашего рендеринга. давайте создадим ещё один метод void render(Graphics g) и будем его вызывать из нашего paintComponent();
|
||||
Итак чтобы нарисовать сеточку нам понадобится ширина и высота нашего поля в пикселях, как это сделать: у панельки есть свойства ширина и высота, ими и воспользуемся, создав две дополнительные переменные int panelWidth = getWidth(); int panelHeight = getHeight(); можем вывести в консоль для отладочных нужд, и заодно посмотреть сколько раз и когда вызовется метод перерисовки sout(w, h);
|
||||
помимо этого нам понадобятся переменные, в которых мы будем хранить высоту и ширину каждой ячейки. размеры каждой ячейки нам пригодятся, так что заведём две классовые переменные cellW, cellH = panelW/fsx, panelH/fsy; и в методе который рендерит разлинуем. идём циклом от 0 до <= fieldSizeY и рисуем горизонтальные линии, пишем int y = i * cellH; g.drawLine(0, y, panelWidth, y); и у многих сейчас не полностью нарисовалось, это происходит потому, что перерисовалась только часть компонента, так уж устроена отрисовка в свинге, чтобы не жрать много ресурсов. мы должны заставить наш компонент полностью перерисоваться. сделаем это вызвав метод repaint(); из метода startNewGame; то есть в методе СНГ мы все переменные инициализировали и попросили панель перерисоваться после этого.
|
||||
Опять же если копнуть немного вглубь, мы сказали фреймворку что надо перерисоваться, он поставил метод пэйнтКомпонент куда-то в очередь сообщений окна, и когда очередь дошла - выполнил его. то есть это действует асинхронно и не напрямую. Отвлеклись, продолжим. отрисуем вертикальные линии fori(<=fsX){int x=i*cellW; g.drawLine(x, 0, x, panelHeight)}.
|
||||
***** Теперь давайте сделаем ещё одну магию,
|
||||
добавим слушателя на мышку, в конструкторе пишем addMouseListener(new MouseAdapter() {}) нам понадобится заоверрайдить метод mouseReleased(), то есть нам важно когда пользователь отпустит кнопку (метод touchUp()) и снова чтобы не заполнять конструктор огромным количеством кода, создадим отдельный метод update(MouseEvent e) и вызовем его из обработчика мышки. и тут мы тоже принудительно вызовем репэйнт, вот собственно и получается наш игровой цикл, клик мыши, отрисовка, клик-отрисовка.
|
||||
На самом деле это делается ещё и для того чтобы мы нашу логику могли портировать куда-то ещё. не обязательно же мышка должна вызывать у нас изменения игрового поля, а например тап по тачскрину. Теперь мы в нашем методе update из класса MouseEvent вытаскиваем координаты клика, делим на размер ячейки и тем самым получаем номер ячейки, в которую мы кликнули. пишем: int cellX = e.getX/cellW; int cellY = e.getY/cellH. для верности можем их sout чтобы посмотреть как это работает и работает-ли.
|
||||
***** берём наш код от крестиков-ноликов с третьего занятия
|
||||
свой я вам выкладывал, так что если не готовы воспользоваться своим - возьмите мой. и копипастим его от мэйна до метода humanTurn(). всё, где логика и нет работы с консолью. вот вам пример необходимости писать методы, которые ничего лишнего не выводят. код не переписываем, а приложение уже совсем другое. Ну и видим, что нам не хватает кое каких констант. объявим private static final int EMPTY_DOT = 0, HUMAN_DOT = 1, AI_DOT = 2; убираем статики из методов, и чиним наш проект дальше.
|
||||
В методе checkLine меняем чары на инты, для того чтобы переименовать все переменные в данной области видимости надо навести на неё курсор и нажать шифт-ф6. переименуем на dot.
|
||||
в checkWin() делаем точно также.
|
||||
для aiTurn() после того как убрали статик видим, что не хватает только рандомайзера, создадим классовую переменную pr fin Random random = new R();
|
||||
статики мы тогда создавали потому что не знали ООП, и не создавали объекты, и не знали, кто такие поля объектов. Напомню, что в статические методы нельзя передавать ссылки на объекты, и из статических методов можно обращаться только к статическим переменным и методам.
|
||||
***** Ну что, нашли мы кликнутую ячейку, давайте в неё ходить,
|
||||
только нам для начала надо проверить, валидная-ли ячейка, и можно-ли туда ходить, вот и проверим: (!isValid(cellX, cellY) || !isEmptyCell(cellX, cellY)) return; игнорим клик, выходим из метода
|
||||
ну а если всё хорошо - просто ходим филд[селХ][селУ] = hum_dot;
|
||||
И добавить в наш метод отрисовки ещё немного логики. сделаем двойной массив по всем ячейкам нашего поля фори(филдСайзУ) фори(филдСайзХ) и внутри пишем
|
||||
if(isEmptyCell(ж, и)) continue; то есть если ячейка пустая - просто идём дальше, ничего не делаем, дальше пишем маленькую заготовку:
|
||||
if(field[i][j] == H_D) {} else if (field[i][j] == A_D) {} else throw new RTE (не пойму что в ячейке + и,ж) мало ли что мы там перекодили и допилили, может решили сделать баттл для трёх игроков?
|
||||
И теперь пришли к отрисовке. хотите, можете сюда картинку вставлять, хотите закрашенные квадратики, хотите рисуйте крестики-нолики, я для простоты буду пользоваться методом g.fillOval() и буду рисовать кружки. ему в сигнатуре передаётся левая верхняя координата прямоугольника, в который потом будет вписан овал, и его ширина и высота соответственно. тем, кто хоть раз рисовал в пэинте - знакома эта техника рисования кружков. ну и чтобы придать ему цвета - перед тем как рисовать изменим цвет объекта graphics, g.setColor(Color.BLUE); или задать интами от 0 до 255 в формате РГБ new Color(RGB);
|
||||
Итак я предлагаю для человека рисовать синие кружки, а для компа красные. и после проверки собственно его рисовать, в проверках пишем setColor(); а после проверок пишем g.fillOval(j*CellW, i*CellH, CW, CH); и чтобы сделать небольшой отступ сделаем следующее, заведём классовую константу DOTS_PAGGING = 5 пикселя. и первые координаты смещаем на + пэддинга вторые на - 2 пэддинга.
|
||||
Так посимпатичнее.
|
||||
***** Давайте завязывать с этим безумием
|
||||
объявим константы которые будут содержать исходы игры, псфи DRAW = 0; HUM_WIN = 1; AI_WIN = 2; и нужна переменная stateGameOver в которой будет храниться статус.
|
||||
Значит теперь в методе апдейт мы поставили точку, перерисовали и жедаем по старому сценарию
|
||||
if(checkWin(H_D)) stGO = H_W; return;
|
||||
if (isMapFull()) stGO = DR; return;
|
||||
aiTurn();
|
||||
repaint();
|
||||
if(checkWin(A_D)) stGO = A_W; return;
|
||||
if (isMapFull()) stGO = DR;
|
||||
***** дальше в принципе нам только и надо что нарисовать все красотульки.
|
||||
значит идём в метод рендер и думаем там. как только мы вывели поле, и если игра закончилась нам надо вывести сообщение с одним из вариантов исхода. так и запишем if (gameOver) showOverMessage(g); вот просто перевели наши слова на английский. теперь смотрим, чего у нас для этого не хватает в коде, как минимум метода showOverMessage() и переменной признака конца игры. да не вопрос, мыжпрограммисты, напишем войд sOW(Graphics g), private boolean gameOver. и поехали наполнять эти штуки смыслом:
|
||||
в методе начала новой игры нам, очевидно, надо сказать что геймовер = фолс. и закончим на этом метод СНГ.
|
||||
идём в метод апдейт и в самом начале пишем, что if(gameover) return; очевидно, что если геймовер случился - надо игнорировать вообще все клики. ну а если кто-то выиграл или ничья - геймовер также становится тру. и на этом закончим с методом апдейт и останется только дописать собственно метод показывающий, что у нас закончилась игра.
|
||||
Ну и для того чтобы нарисовать в классе графики какой-то текст есть метод g.drawString() с выводимой строкой и координатами по х и по у, внимание, координата по у это координата нижней части букв, как строчка в тетради. Ну и для того чтобы этот текст кастомизировать да и вообще для работы со шрифтами есть класс не поверите Font. создадим классовый private final Font который назовём не поверите font = new Font( и на вход конструктор внезапно принимает название шрифта, стиль и размер “Times new roman”, Font.BOLD, 48);
|
||||
будьте осторожны при работе с такими неочевидными классами. и теперь наш метод с отрисовкой строки немного дополняем, setColor(); setFont(font). вот так он и выводится. Давайте создадим классовые константы, в которых будем хранить наши сообщения о победах. psfS MSG_DRAW= “DRAW”; MSG_HW; MSG_AW;
|
||||
***** в showGameOver
|
||||
пишем свич (стейтГеймОвер) и кейсы ДРО, Х_В, А_В. дефолт throw new RTE(unexpected GO message);
|
||||
теперь если дро, g.drawString(MSG_DRAW, 180, getHeight/2);
|
||||
если H_W, g.drawString(HW, 70, gH/2);
|
||||
если A_W (20);
|
||||
и перед ним пишем setColor(YELLOW) setFont(font);
|
||||
ну и чтобы было виднее, я предлагаю рисовать тёмно серый прямоугольник в качестве фона работает он также как fillOval только рисует прямоугольник пишем fillRect(0, 200, getWidth, 70); естественно надо все магические цифры либо вывести в константы, либо как-то динамически считать). и БУМ! всё классно, мы закончили.
|
||||
|
||||
|
||||
**** 3: Крестики-нолики
|
||||
***** public class TicTacToe {
|
||||
private static final char HUMAN_DOT = 'X';
|
||||
private static final char AI_DOT = 'O';
|
||||
private static final char EMPTY_DOT = '.';
|
||||
private static final Scanner SCANNER = new Scanner(System.in);
|
||||
private static final Random RANDOM = new Random();
|
||||
private static int fieldSizeY;
|
||||
private static int fieldSizeX;
|
||||
private static char[][] field;
|
||||
***** Инициализация игрового поля
|
||||
private static void initMap() {
|
||||
fieldSizeY = 3;
|
||||
fieldSizeX = 3;
|
||||
field = new char[fieldSizeY][fieldSizeX];
|
||||
for (int i = 0; i < fieldSizeY; i++) {
|
||||
for (int j = 0; j < fieldSizeX; j++) {
|
||||
field[i][j] = EMPTY_DOT;
|
||||
}
|
||||
}
|
||||
}
|
||||
***** Вывели поле с любыми украшениями вокруг него
|
||||
private static void printMap() {
|
||||
System.out.print("+");
|
||||
for (int i = 0; i < fieldSizeX * 2 + 1; i++)
|
||||
System.out.print((i % 2 == 0) ? "-" : i / 2 + 1);
|
||||
System.out.println();
|
||||
|
||||
for (int i = 0; i < fieldSizeY; i++) {
|
||||
System.out.print(i + 1 + "|");
|
||||
for (int j = 0; j < fieldSizeX; j++)
|
||||
System.out.print(field[i][j] + "|");
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
for (int i = 0; i <= fieldSizeX * 2 + 1; i++)
|
||||
System.out.print("-");
|
||||
System.out.println();
|
||||
}
|
||||
***** Ход игрока
|
||||
private static void humanTurn() {
|
||||
int x, y;
|
||||
do {
|
||||
System.out.print("Введите координаты X и Y через пробел>> ");
|
||||
x = SCANNER.nextInt() - 1;
|
||||
y = SCANNER.nextInt() - 1;
|
||||
} while (!isValidCell(x, y) || !isEmptyCell(x, y));
|
||||
field[y][x] = HUMAN_DOT;
|
||||
}
|
||||
***** ячейка-то вообще правильная?
|
||||
private static boolean isValidCell(int x, int y) { return x >= 0 && x < fieldSizeX && y >= 0 && y < fieldSizeY; }
|
||||
***** а пустая?
|
||||
private static boolean isEmptyCell(int x, int y) { return field[y][x] == EMPTY_DOT; }
|
||||
***** Ход компьютера
|
||||
private static void aiTurn() {
|
||||
int x, y;
|
||||
do {
|
||||
x = RANDOM.nextInt(fieldSizeX);
|
||||
y = RANDOM.nextInt(fieldSizeY);
|
||||
} while (!isEmptyCell(x, y));
|
||||
field[y][x] = AI_DOT;
|
||||
}
|
||||
***** проверка на победу
|
||||
private static boolean checkWin(char c) {
|
||||
if (field[0][0]==c && field[0][1]==c && field[0][2]==c) return true;
|
||||
if (field[1][0]==c && field[1][1]==c && field[1][2]==c) return true;
|
||||
if (field[2][0]==c && field[2][1]==c && field[2][2]==c) return true;
|
||||
//.......
|
||||
}
|
||||
***** ничья?
|
||||
private static boolean isMapFull() {
|
||||
for (int i = 0; i < fieldSizeY; i++) {
|
||||
for (int j = 0; j < fieldSizeX; j++) {
|
||||
if (field[i][j] == EMPTY_DOT) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
***** Игровой цикл
|
||||
public static void main(String[] args) {
|
||||
initMap(7, 5, 4);
|
||||
printMap();
|
||||
while (true) {
|
||||
humanTurn();
|
||||
printMap();
|
||||
if (checkWin(HUMAN_DOT)) {
|
||||
System.out.println("Выиграл игрок!!!");
|
||||
break;
|
||||
}
|
||||
if (isMapFull()) {
|
||||
System.out.println("Ничья!!!");
|
||||
break;
|
||||
}
|
||||
aiTurn();
|
||||
printMap();
|
||||
if (checkWin(AI_DOT)) {
|
||||
System.out.println("Выиграл компьютер!!!");
|
||||
break;
|
||||
}
|
||||
if (isMapFull()) {
|
||||
System.out.println("Ничья!!!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
SCANNER.close();
|
||||
}
|
||||
***** Домашнее задание
|
||||
1. Полностью разобраться с кодом;
|
||||
2. Переделать проверку победы, чтобы она не была реализована просто набором условий.
|
||||
3. * Попробовать переписать логику проверки победы, чтобы она работала для поля 5х5 и количества фишек 4.
|
||||
4. *** Доработать искусственный интеллект, чтобы он мог блокировать ходы игрока, и пытаться выиграть сам.
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
\documentclass[../j-spec.tex]{subfiles}
|
||||
|
||||
\begin{document}
|
||||
\section{Интерфейсы}
|
||||
\begin{longtable}{|p{35mm}|p{135mm}|}
|
||||
\hline
|
||||
Экран & Слова \\ \hline
|
||||
\endhead
|
||||
|
||||
Титул & Здравствуйте, добро пожаловать на курс посвящённый инструментарию разработчика на джава \\ \hline
|
||||
|
||||
Отбивка & и сегодня на повестке дня у нас интерфейсы, но не те интерфейсы, что графические, а те, что программные. \\ \hline
|
||||
|
||||
На прошлом уроке & На прошлом уроке мы поговорили о файловых системах и представлении данных в запоминающих устройствах; Поверхностно посмотрели на популярные пакеты ввода-вывода. Подробно разобрали один из самых популярных ссылочных типов данных String и разные механики вокруг него, такие как стрингбилдер и стрингпул. \\ \hline
|
||||
|
||||
На этом уроке & Поговорим об интерфейсах, рассмотрим понятие и принцип работы, поговорим о ключевом слове implements; Наследование и множественное наследование интерфейсов, реализация, значения по умолчанию и частичная реализация интерфейсов. Естественно, что говоря об интерфейсах нельзя не сказать о функциональных интерфейсах и анонимных классах. Коротко рассмотрим модификаторы доступа при использовании интерфейсов и немного подробнее поговорим о глубинных процессах, таких как раннее и позднее связывание в объектно-ориентированном программировании. \\ \hline
|
||||
|
||||
06-01 & Разговор об интерфейсах хотелось бы построить не так, как мы это делали на лекциях обычно, а от некоторой практической составляющей, чтобы где-то в середине лекции у вас сложилось явное впечатление, что что-то тут явно можно улучшить, а когда мы применим интерфейсы, вы подумали «ааааа так вот зачем оно нужно и как применяется», а потом уже поговорим о теоретической составляющей и особенностях. \\ \hline
|
||||
|
||||
Отбивка & Приложение для примера \\ \hline
|
||||
|
||||
06-02 & Итак начнём с преамбулы, здесь я призываю вас не особенно обращать внимание на то, какие именно классы и методы используются, а внимательно следить за взаимодействием и отношениями объектов, потому что интерфейсы, о которых мы сегодня планируем поговорить - это как раз механизм упрощающий и универсализирующий взаимодействия объектов. Код может показаться непростым, но зато задачу мы поставим таким образом, что если делать нормально, то без интерфейсов не обойтись, и когда мы закончим - у вас, как я только что и сказал, должно появиться ощущение что «о, так вот зачем они нужны». Почему будет сложно? потому что несмотря на то что вы уже знаете принципы ооп - надо уметь их применять.\\ \hline
|
||||
|
||||
06-03 & Итак, что мы будем делать? мы сделаем некий небольшой набросок своего собственного игрового 2д движка, без физики и прочего, а просто с объектами и анимацией, чисто демонстрационный. на его основе можно что угодно запилить, и будет отлично работать. На слайде вы видите окно, кружки летают (имейте ввиду, они перемещаются плавно, если у кого-то что-то дёргается, это интернет с низким ФПС передаёт, а не приложение). пока что ничего тут не обрабатывается, ничего толком не происходит. Итак, самое главное, что нам понадобится, это окно, которое будет как-то взаимодействовать с операционной системой, на окне будет канва, на которой мы будем всё рисовать, и собственно объекты, которые мы будем рисовать. \\ \hline
|
||||
|
||||
06-04 & Быстро создаём окно, буквально константы с размерами, координатами, и конструктор окна прямо здесь же вместе с методом мейн, не вдаваясь в подробности того, как работает фреймворк свинг на котором мы всё это пишем, на эту тему у нас будет отдельный урок, на котором мы закрепим информацию об ООП, многопоточности и отлову исключений в графических интерфейсах. Самое важное для нас сейчас - это то, что окно - это объект с какими-то свойствами и каким-то поведением. Если коротко, стартуем программу, создаём объект окна, говорим, что окно с каким-то заголовком, какого-то размера и находится по каким-то координатам, а когда мы нажмём на крестик, то закрыть нужно будет не только окно, но и программу целиком. \\ \hline
|
||||
|
||||
06-05 & Итак есть такой компонент JPanel, с ним можно много что делать но самое интересное, что можно на нём рисовать. Итак создали наследника Jpanel с названием GameCanvas. Что умеет любой компонент в свинге? он умеет перерисовываться, посредством вызова метода paintComponent. Применим полиморфизм. Из документации мы знаем, что метод пэинтКомпонент вызывается тогда, когда фреймворку надо перерисовать панельку. Мы этот метод переопределим и напишем свою собственную реализацию перерисовки. Давайте сделаем так: создадим конструктор этой пенльки, и будем в конструкторе делать что то незначительное, например, менять цвет фона на синий. Заоверрайдили пэинтКомпонент и тут-же вызвали родительский метод. То есть по сути мы говорим, что нас вполне устраивает то, как перерисовывается панелька, но мы потом захотим туда что-то добавить. Ну и добавим четыре метода, возвращающие границы нашей канвы, левую, правую, верхнюю, нижнюю.\\ \hline
|
||||
|
||||
06-06 & Наладим взаимодействие компонентов и привяжем все действия нашего игрушечного движка ко времени физического мира. В нашем основном окошке, в конструкторе, создали переменную класса GameCanvas и расположили её на окне прям в центре. Пока что всё понятно и хорошо. Всё работает. В принципе можно начать писать логику игры прям здесь, в классе панельки, но это архитектурно не очень хорошо, ведь это же канва на которой мы рисуем, значит здесь мы должны по логике только рисованием заниматься. Давайте примем архитектурное решение писать логику игры в нашем классе с кружками, а GameCanvas будет универсальным, чтобы рисовать вообще всё что угодно. Для этого описали в нашем основном окошке метод, назвали его как-нибудь нормально onDrawFrame и в нём будем описывать реализацию цикла для нашего модного приложения, то есть так называемую бизнес-логику. На данный момент это будут два метода - апдейт который будет как то изменять состояние нашего приложения, и рендер, который будет отдавать команды всяким рисующим компонентам. \\ \hline
|
||||
|
||||
06-07 & Теперь нам надо чтобы при перерисовке наш с вами GameCanvas этот метод дёргал, и тем самым изображение как-то менялось. Для этого нашей канве надо знать как минимум чей метод она будет дёргать, то есть ей нужно знать на каком окне она находится. создаём в классе канвы локальную переменную, которая умеет хранить объекты класса MainCircles и передадим значение этой переменной в конструкторе. И в пэинтКомпоненте будем вызывать controller.onDrawFrame(); Далее, чтобы зациклить это действие мы можем пойти несколькими путями: самый простой - создать постоянно обновляющуюся канву, то есть в методе пэинтКомпонент взять и написать repaint() но это вариант прямо скажем "так себе", он полностью нагрузит одно из ядер процессора только отрисовкой окна - не самое лучшее применение одного из ядер. Второй путь - это применение магии из занятия по потокам. мы можем заставить наш поток какое-то время поспать, начиная с 20-й строки вы видите некоторую конструкцию, смысл которой вам будет понятен немного позже Thread.sleep(16); это даст нам фпс близкий к 60, приемлемо для условной игры, не надо ни больше ни меньше, пожалуй. Получится, что мы отрисовали компонент, посчитали что там изменится апдейтом, отправили обратно на отрисовку рендером, отрисовали пэинтКомпонентом.\\ \hline
|
||||
|
||||
06-08 & Довольно интересно, кстати, как при этом изменился код, вызывающий конструктор канвы, ведь мы из основного класса с окном теперь как-то должны в канву передать это самое основное окно, как вы могли догадаться, для этой цели мы применяем указатель на текущий объект класса this, то есть в конструкторе основного окна мы передали ссылку на экземпляр этого окна канве. немного ломает привычное использование для обращения к полям в конструкторе, но, поверьте, то ли ещё будет. Предлагаю запомнить такой способ применения ключевого слова this, он нам сегодня ещё пригодится. \\ \hline
|
||||
|
||||
06-09 & Закончим с отрисовкой и методом onDrawFrame. Он будет обновлять сцену и рендерить её. Для обновления сцены было бы очень неплохо знать дельту времени, которая прошла с предыдущего кадра, чтобы обновлять физику. Конечно можно писать физику, опираясь на частоту кадра, или на то что мы там спим 16миллисекунд, но это всё очень сомнительная опора, потому что мы гарантированно спим 16миллисекунд, но сколько именно мы будем спать неизвестно, потому что отрисовка происходит не через фиксированные промежутки времени и ещё куча факторов. Лучше всего точно знать сколько времени прошло с предыдущего кадра. поэтому сделаем так чтобы метод onDrawFrame эту самую дельту у канвы получал и отдавал методу обновления. Соответственно считаем дельту в канве. Вот, собственно и вся физика, которая нам понадобится. Теперь посмотрим вот на что - у нас в пэинтКомпонент прилетает объект класса графикс, который отвечает, как это ни удивительно, за графику, то есть за рисование. Это же то, что нам нужно. Давайте этот самый объект отдадим нашему методу onDrawFrame, пусть он что-нибудь рисует. а где рисует? на канве, значит нам надо будет как минимум знать её размеры. какой канвы размеры получаем? нашей, той самой. себя и отдадим. onDrawFrame будет распределять - апдейту отдаст дельту и канву, а рендеру - канву и графику. Для универсальности. Все изменения канвы вы видите на слайде, все изменения основного окна ему соответствуют, изменена сигнатура метода онДроФрейм, на неё мы посмотрим через несколько секунд. Пока самое главное, что нам нужно понять об этих двух объектах - канва считает для нас время и постоянно перерисовывает себя, сообщая об этом факте основному окну, а основное окно на этот факт как-то реагирует. Ну и ООП вокруг этого тоже было бы хорошо понимать, объекты передают ссылки друг на друга и вызывают друг у друга всякие интересные методы. \\ \hline
|
||||
|
||||
06-10 & Давайте попробуем что нибудь нарисовать при помощи нашего с вами игрового цикла, например прямую белую линию. Видим, что отлично получается, теперь мы умеем рисовать и наша канва умеет это отображать. Отлично. Наше приложение будет рисовать какие-то объекты, будут это кружки, квадратики, картинки, человечки или какие-то другие объекты - не важно. Важно, чтобы у программы было описан механизм и поведение этих объектов. Это как раз то, о чём я говорил и говорю - применение архитектуры, применение ООП. Мало просто посмотреть уроки, почитать книжки, посидеть на семинаре. Надо сидеть и думать в рамках парадигмы ООП. Это ещё простенькая архитектура. Я понимаю вас, вы смотрите сейчас, и вроде всё понятно, а сами в жизни бы такое не написали. Я сам несколько лет назад такое в жизни не написал бы, так что не переживайте, всё придёт с опытом. Главное, не путайте понятия уметь программировать и знать язык программирования. В какой-то момент вы поймаете себя на мысли, что вы не думаете о циклах и условиях, а думаете на следующем уровне абстракции, думаете о взаимосвязях, об архитектуре, а пальцы сами набирают какие-то языковые конструкции. \\ \hline
|
||||
|
||||
06-11 & Соответственно, рисовать просто линии круги и прочее - довольно скучно, поэтому будем рисовать объекты, Создадим класс Спрайт. Ни от чего наследоваться не будем. Просто опишем общее для всех рисуемых объектов в нашей программе поведение. Что может быть у всех объектов в приложении общего? размеры, местоположение. Обычно, когда вы начинаете изучать какой-то графический фреймворк вы замечаете, что начало координат у этого фреймворка находится в верхнем левом или нижнем левом углу. Однако очень часто, когда пишутся какие-то игры или другие приложения с использованием графики в качестве координат используется центр объекта. То есть надо условиться - что х и у - это центр любого визуального объекта на нашей канве. И соответственно удобно хранить не длину-ширину, а половину длины и половину ширины. А границы объекта соответственно будем отдавать через геттеры и сеттеры. Дополнительно научим наш спрайт рисоваться. Ну как научим, просто скажем, что он умеет обновляться и рендериться, а его наследники пусть уже решают, как именно они хотят это делать. Но спрайты лишены логики, они ничего не знают о том, как именно будут меняться их свойства. \\ \hline
|
||||
|
||||
06-12 & Поэтому естественно нужно создать класс собственно шарика, который будет по нашему экрану прыгать, а то непорядок какой-то. В конструкторе задаём ему рэндомные размеры. Давайте придумаем ему какие-нибудь глобальные свойства. Можно конечно придумать класс, который будет направлять наш спрайт и вообще задавать ему скорость и прочие физические величины, но мы ж с вами пример пишем, так что придумаем ему просто - скорость по осям х и у соответственно и цвет. для цвета есть совершенно неожиданно называющийся класс Color которому можно задать величины в формате РГБ в диапазоне от 0 до 255. Для шарика переопределяем апдейт и рендер. суперы нам не нужны, они всё равно пустые. Самый простой рендер - мы объекту графики зададим цвет текущего шарика и сделаем fillOval, которому передадим лево, верх, ширину и высоту. Несмотря на то что наши объекты содержат поля типа флоут, мы работаем с пиксельной системой координат (что конечно же не подходит для реальных проектов, там нужно всё сразу переводить в мировые координаты (например принять центр экрана за 0, верх и лево за -1 низ и право за 1, как это делает OpenGL) чтобы рендерить экраны). Но это нам и пяти лекций не хватит чтобы вникнуть так что не будем. А в методе апдейт мы просто прибавляем к нашим текущим координатам нашу скорость, умноженную на дельту времени, то есть как в третьем классе. Расстояние, которое должен был преодолеть наш шарик за то время пока наш поток спал и наша канва рендерилась. ну и обрабатываем отскоки, то есть описываем 4 условия, что при достижении границы мы меняем направление вектора. \\ \hline
|
||||
|
||||
06-13 & давайте быстренько допилим основной класс и будем переходить к беседе об интерфейсах уже, а то чувствую вы подустали от вступлений. В основном классе мы делаем очень прямолинейно - создаём классовый массив из спрайтов и называться он будет sprites и мы говорим что будет у нас допустим 1 кружчек. В методе апдейт мы пробежимся по всем спрайтам и скажем каждому из них - апдейться так как ты умеешь. В методе рендер мы сделаем тоже самое - пробежимся по всем спрайтам и скажем - отрисуйся так, как ты хочешь. И всё, реализацию обновления и отрисовки мы оставили самим объектам, то есть инкапсулировали в них, только каждый объект сам по себе знает, как именно ему обновляться с течением времени, и как рисоваться, а основной экран уже управляет - на какой канве, когда и кого рисовать. Итак инициализировали наше приложение одним новым шариком и напишем небольшую магию по добавлению новых шариков в динамически расширяемый массив. Как известно, в джава все массивы имеют неизменяемую размерность. Это накладывает некоторые архитектурные ограничения на программиста, но мы ж с вами крутые программисты, вон сколько лекций и семинаров позади, можем обмануть систему. Код не сложный, когда старый массив заполнился, просто увеличиваем его в два раза и переносим шарики из старого в новый. \\ \hline
|
||||
|
||||
06-14 & И осталось эти методы откуда-то вызывать. Мы на нашу канву в конструкторе вешаем слушателя мышки, который нам рассказывает когда произоши какие-то события. Сегодня как раз будем анонимные классы проходить, поговорим об этом явлении подробнее. И по этому произошедшему событию мы говорим - добавь нам спрайт с новым мячиком, у которого будут координаты этого события. Для этого конечно пришлось немного модифицировать класс шарика, добавив в него ещё один конструктор, который сразу задаёт начальные координаты. На правую кнопку мышки повесим удаление спрайта из списков обновления и отрисовки. \\ \hline
|
||||
|
||||
06-15 & Получается, мы меньше чем за полчаса написали довольно простое и очень хорошо расширяемое приложение, которое не только рисует вещи, но и на действия пользователя реагирует, кажется, неплохо. Напомню, что самое главное, что мы должны из этого приложения извлечь - это взаимодействия и взаимовлияния объектов. Наследование, полиморфизм, инкапсуляция поведений и свойств. Если честно, я слегка устал от синего цвета нашего фона и решил создать отдельный класс фона, но сразу столкнулся с необходимостью думать головой. Логично было бы предположить, что фон - это спрайт, имеющий прямоугольную форму и всегда рисующийся первым. но вот беда, при изменении размеров окна фон тоже желательно изменить в размерах. Поэтому в отрисовке я просто говорю канве, что она должна изменить свой цвет фона, а что, ссылка то на канву у меня есть. Цвет фона я меняю синусоидально по каждому из трёх компонент цвета, поэтому изменение происходит довольно плавно. В общем, получается, что от спрайта нам фактически нужно только поведение, а свойства не нужны. Но и отказаться от наследования нам бы не хотелось, потому что тогда мы не сможем фон единообразно в составе массива спрайтов обновлять. Это наталкивает нас на мысль об унификации поведения, на мысль об интерфейсе. \\ \hline
|
||||
|
||||
Отбивка Понятие интерфейса & Механизм наследования очень удобен, но он имеет свои ограничения. В частности мы можем наследовать только от одного класса, в отличие, например, от языка С++, где имеется множественное наследование \\ \hline
|
||||
|
||||
интерфейс - это описание способов взаимодействия с объектом. фото руля-педалей-коробки, фото юсб, фото клавиатуры-мышки-монитора & В языке Java эту проблему частично позволяют решить интерфейсы. Интерфейсы определяют некоторый функционал, не имеющий конкретной реализации, который затем реализуют классы, применяющие эти интерфейсы. И один класс может применить к себе множество интерфейсов. Правильно говорить реализовать интерфейс, будем сразу говорить правильно. Если сказать проще, интерфейс можно очень-очень грубо представить как очень-очень абстрактный класс. До седьмой джавы это был просто набор методов без реализации. Начиная с восьмой наделали много тонкостей, с ними и будем разбираться. Итак интерфейс - это описание методов. Примером интерфейса в реальной жизни может быть интерфейс управления автомобилем, интерфейс взаимодействия с компьютером или даже интерфейс USB, так, компьютеру не важно, что именно находится по ту сторону провода, флешка, веб-камера или мобильный телефон, а важно, что компьютер умеет работать с интерфейсом USB, отправлять туда байты или получать. Потоки ввода-вывода, которые мы проходили чуть раньше - это тоже своего рода интерфейс, соединяющий не важно какой программный код и не важно какой, например, файл. Все методы во всех интерфейсах всегда публичные, и в классическом варианте не имеют реализации. Ну и поскольку все методы всегда паблик то этот модификатор принято просто не писать. Для новичка это неочевидно и сбивает с толку, может показаться что модификатор дефолтный, а на самом деле он публичный, поблагодарим разработчиков джавы и пойдём дальше. \\ \hline
|
||||
|
||||
06-16 & Интерфейсы объявляются также, как классы, и вообще могут иметь очень похожую на класс структуру, то есть быть вложенным или внутренним, но чаще всего интерфейсы описывают в отдельном файле, также как класс, но используя ключевое слово интерфейс. Создадим пару интерфейсов, например, человек и бык, опишем в них методы, например, ходить и издавать звуки. \\ \hline
|
||||
|
||||
06-17 & Для чего мы так сделали? Продолжим пример, создадим пару классов, класс мужчина и класс был. Класс мужчины будет у нас реализовывать интерфейс человека. То есть множественного наследования нет, но мы можем реализовать сколько угодно интерфейсов. Для того, чтобы реализовать интерфейс - мы должны переопределить все его методы, либо сделать класс абстрактным. Вот, статический анализатор кода в идее нам об этом явно говорит. Выведем модное сообщение о том что это классы мужчины или быка.
|
||||
И теперь самая соль - мы можем в мэйне объявлять не только классы и создавать объекты, но и создать переменную, которая реализовывает интерфейс. То есть тут могут лежать абсолютно никак не связанные между собой объекты, главное, чтобы они реализовывали интерфейсА. И мы можем работать с методами интерфейса, которые могут быть для разных классов вообще по-разному реализованы. Такой вот полиморфизм. Понимаете насколько сильно это отличается от наследования, когда мы с вами создавали общий абстрактный класс животное и от него наследовали наших котиков? \\ \hline
|
||||
|
||||
06-18 & Чтобы стало сильно понятнее, создадим класс минотавра, кто плохо помнит греческую мифологию, это такой товарищ, который с телом человека и головой быка скучно сидел в лабиринте и ждал заблудившихся путников. соответственно, реализовывал интерфейсы человека и быка своим способом, ходил на ногах человека, но не мычал, как бык, а загадки загадывал. Интересно то, что в программе мы можем к минотавру обратиться не только как к человеку, но и как к быку, то есть гипотетически, можно создать некоторого Тесея, погонщика минотавровых стад. Но это уже, так сказать, полёт фантазии, нас интересует только техническая часть вопроса - классы не связаны между собой наследованием, а обращение к ним единообразное. \\ \hline
|
||||
|
||||
06-19 & Также важно, что в интерфейсах разрешено наследование. То есть у нас интерфейс может наследоваться от другого интерфейса, соответственно при реализации интерфейса мы должны переопределить методы всех родителей нашего интерфейса, то есть тут картина очень похожа на наследование классов, но внимание не запутайтесь, в интерфейсах разрешено множественное наследование. \\ \hline
|
||||
|
||||
06-20 & На несколько минут вернёмся к бэкграунду, пока мы его не забыли, применим наши новые интерфейсные знания на практике. Фон наследуется от спрайта, но ему от спрайта вообще ничего не надо, кроме двух методов. в которых он полностью пишет собственную реализацию, которая никак не коррелирует с тем, как ведут себя остальные спрайты. И вот сложилась ситуация в которой нам надо хранить в одном массиве очень похожие объекты но наследовать их друг от друга не логично. Как поправить? \\ \hline
|
||||
|
||||
06-21 & Напишем некий интерфейс, назовём его GameObject и скажем, что у него есть методы апдейт и рендер, без реализации. То есть это будут некие объекты которые должны уметь рисоваться и обновляться. Идём в спрайт, и говорим, что мы реализуем интерфейс гейм объекта. Смотрим, что сломалось, кто навскидку скажет, почему? Не нужно судорожно хвататься за клавиатуру, но мысль правильная - модификатор должен быть паблик. И Фон тоже теперь у нас реализует интерфейс гейм объекта. При этом получается, то фон вообще никак не связан со спрайтом, у них даже набор полей разный. Но оба умеют рисоваться и апдейтиться, благодаря интерфейсу. Быстро поправив основное окно и логику, сменив массив спрайтов на массив геймОбъектов запускаем и видим как бодро летают наши шарики. \\ \hline
|
||||
|
||||
06-22 & И вот мы подошли к самому главному, той гибкости, которую даёт нам работа с интерфейсами. Если вы обратите внимание, как развивается наше повествование по курсу, вы заметите, что сначала мы выходили за пределы одного метода, потом за пределы одного класса, затем за пределы одного пакета, за пределы программного кода, а теперь вовсе хотим написать код, который возможно будет использовать несколькими программами. Посмотрим со стороны, что мы тут понаписали. У наших классов канвы и спрайта, а также у интерфейса нет никакой специфики, их можно применить где угодно. Универсальные получились штуковины. Создадим какую-то условную вторую игру: новый пакет, новый класс, скопипастим немного кода, чтобы всё запускалось. И не писать же нам по новой спрайты и интерфейсы. сделаем правильное дробление по пакетам. то есть получается, что у нас есть некий библиотечный пакет, и какие-то игры с конкретными реализациями. создадим пакет КОММОН и переносим туда канву, спрайт и геймобъект, и ща будем всё чинить. Структура же понятна, только что её всесторонне обговорили? \\ \hline
|
||||
|
||||
06-23 & Гейм объект это интерфейс, ему вообще на всё плевать, только импорты лишние выкинем. Спрайт тоже не сломался, модификаторы доступа поправим на публичные и защищённые, где это возможно. Второе главное окно также инитим конструктор, размеры, положение. И вот хотим воспользоваться нашей канвой. Но не даёт же. почему, мы ж так классно всё придумали, никакой специфики. Но нет, канва то может принимать в конструкторе mainCircles. и канва уже у этого класса вызывает метод onDrawFrame(). Как это решается? это решается интерфейсом. нам надо написать какой-то интерфейс вроде canvasPaintListener, который будет уметь ждать от канвы вызов метода и как-то по своему его реализовывать. А то у нас получается катастрофа - игры зависят от общего пакета, а по хорошему общий пакет вообще ничего не должен знать о том, какие игры он помогает создать. Создаём в общем пакете интерфейс с одним методом, который имеет сигнатуру из нашего mainCircles. И перепишем канву, чтобы она не какой-то класс с шариками на вход принимала, а canvasPaintListener, назовём переменную листнер. и везде используем слушателя через интерфейс. В главном окне мы говорим что классы вы конечно хорошие, с прекрасным наследованием от фреймворка, но только ещё будете реализовывать интерфейс слушателя канвы. а метод у нас уже правильно написан, с верной сигнатурой. Чтобы подчеркнуть, что это реализация интерфейса, напишем аннотацию оверрайд. \\ \hline
|
||||
|
||||
06-24 & Насладимся летающими шариками в одном приложении, и летающими квадратиками в другом. Теперь на основе нашего очень хорошо отделённого интерфейсами общего пакета можно штамповать такие приложения практически без усилий. Понимаете, как разработчики в игровых студиях делают по сотне очень похожих игр в год? \\ \hline
|
||||
|
||||
отбивка Анонимные классы & Программные интерфейсы открывают перед разработчиком широчайшие возможности по написанию более выразительного кода. Одна из наиболее часто используемых возможностей - анонимные классы. \\ \hline
|
||||
|
||||
06-25 & Итак, мы уже неплохо знаем, что классы -- это такой новый для нашей программы тип данных. Мы уже привычными движениями создаём публичные классы в отдельный файлах и пишем в них логику, но это не всё, что позволяет нам джава. Классы также бывают вложенными и внутренними. Внутренние классы - это классы, которые пишутся внутри класса, который описан в файле, мы его даже можем использовать и вызывать его методы. А также вложенные или локальные классы, которые мы можем объявлять прямо в методах, и работать с ними, как с обычными классами, но область видимости у них будет только внутри метода. Это мы уже тоже прошли. К чему я это веду? к анонимным классам. Создадим интерфейс, назовём его как-нибудь хорошо, например, MouseListener и опишем в нём пару методов, например mouseUp(), mouseDown(). Перейдя в основной класс можем написать, что некоторый локальный класс будет реализовывать интерфейс и значит должен переопределять все его методы. После чего мы привычно можем экземпляр этого класса создать и даже попользоваться его методами. Удобно, привычно, хорошо. \\ \hline
|
||||
|
||||
06-26 & Очень часто, какие-то элементы управления (кнопчки, события входящих датчиков (клавиатура, мышка), сеть требуют на вход каких-нибудь обработчиков своих данных, которые будут внимательно слушать конкретный источник данных, отлавливать события и вообще знать что делать. Это всё делается через интерфейсы. Значит если мы в такой элемент управления в качестве слушателя передадим что-то, что реализует нужный интерфейс, то это нечто, очевидно, объект, уже начнёт ловить события и как-то их обрабатывать. Соответственно, весьма часто такие классы создаются не просто без названия экземпляра, но и вовсе без имени, прямо в аргументе методов. \\ \hline
|
||||
|
||||
06-27 & Придумаем какой-нибудь метод, принимающий на вход наш только что созданный МаусЛистенер. Ну и действительно, зачем ему имя, если он будет использован только один раз и только в этом методе? То есть передать туда экземпляр нашего маусЛистнера - несложно и привычно, мы уже создали класс, реализующий интерфейс маусЛистенер, создадим объект и передадим в метод.
|
||||
|
||||
Вот допустим у нас есть переменная MouseListener listener и нам нужно создать туда какой-то экземпляр, который реализует этот интерфейс. для этого есть такой синтаксис:
|
||||
new MouseListener дальше пишем интерфейс, который реализуем
|
||||
{} и у нас по сути открывается объявление класса, где мы пишем реализацию. И получается что мы создаём один экземпляр анонимного класса, который реализует интерфейс MouseListener. И конечно потом мы этот анонимный класс уже кладём в идентификатор (или можем прям вот так передать в метод). Конечно мы можем избежать этого, сейчас поговорим как, но в чужом коде это сплошь и рядом, вот такой замороченный синтаксис, спасибо разработчикам джава.
|
||||
Понятен-ли этот синтаксис? Это создание объекта анонимного класса, никак не называющегося, реализующего интерфейс.
|
||||
Тут можно поговорить о лямбдах, для того чтобы сократить синтаксис мы можем просто убрать то что неизменно для этого анонимного класса - название интерфейса, название метода и класс аргумента. \\ \hline
|
||||
|
||||
& \\ \hline
|
||||
& \\ \hline
|
||||
& \\ \hline
|
||||
|
||||
\end{longtable}
|
||||
\end{document}
|
||||
|
||||
****** Вернёмся к нашим кружочкам
|
||||
Смотрим прям первый метод, invokeLater() он принимает на вход какой-то Runnable. Идём в Runnable и видим, что это интерфейс, который реализует один единственный метод run(). Получается, что мы в invokeLater передаём новый экземпляр анонимного класса который РЕАЛИЗУЕТ ИНТЕРФЕЙС Runnable, вот описание этого класса, мы в нём переопределяем метод run. Вот так это читается. Ровно тоже самое мы сделали с mouseListener. Здесь немного сложнее. Есть интерфейс MouseListener в котором описаны вот эти все методы. И есть специальный класс MouseAdapter в котором все эти методы уже реализованы. Реализации пустые, но они есть, поэтому мы можем не все методы оверрайдить, а только те, которые посчитаем нужными. И когда мы этот MouseAdapter отдаём нашему методу, мы говорим: создай нам новый экземпляр анонимного класса, который НАСЛЕДУЕТСЯ ОТ КЛАССА MouseAdapter и переопредели вот этот метод. Остальные оставляй пустыми. Чувствуете разницу? Как можно избежать таких конструкций и при этом получить понятный код без лишних заморочек? Очень просто. Скажем, что наш класс - реализует тот или иной интерфейс, и переопределим все методы. В некоторые из них даже напишем реализацию, и туда, где требуется интерфейс - передадим себя. Мы же теперь MouseListener, да и Runnable.
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue