Я начинающий программист на Java, и путь мой пройден тысячами.

Сначала идет долгий и мучительный выбор Самой Правильной Книги, затем первый восторг от работы перепечатанных из нее листингов программ. Затем осознание растущей крутости и профессионализма. Падение в яму собственного ничтожества, при попытке написать что-то самостоятельно. И долгий путь наверх.
В моем случае Самой Правильной Книгой стал двухтомник «Java. Библиотека профессионала.» за авторством Кея Хорстманна и Гари Корнелла, а самой первой книгой, которая открыла дверь в мир Java – Яков Файн «Программирование на Java для детей, родителей, дедушек и бабушек».
Чтобы закрепить пытающиеся разбежаться знания, которые упорно пытались остаться на страницах Умных Книжек, а не в голове, я решил написать простую игру. Основная задача была в том, чтобы писать без применения сторонних библиотек.
На прямоугольном игровом поле, в зависимости от уровня, появляется некоторое количество шариков, которые носятся по нему, с разной скоростью, отражаясь от стенок. Игрок нажимает курсором мыши, на игровом поле, и в точке нажатия возникает растущий шарик, который увеличивается до заданного радиуса. По истечении определенного времени, Остальные шарики, если сталкиваются с ним, останавливаются, увеличиваются в размерах, и также уменьшаются и исчезают.
Для каждого уровня определенная цель – сколько шариков должно быть «выбито».

Для начала был создан интерфейс GameConstants, в который были размещены все основные константы. Для всех классов было указано implements GameConstants:
Затем был создан класс Ball. У каждого объекта данного класса, есть свой набор координат по осям x и y, переменные dx и dy, в которых записывается приращение координаты в единицу времени (по сути — скорость), значения радиуса и приращения радиуса, а также цвет и уникальный идентификатор. Идентификатор пригодится позже, когда будем отслеживать столкновения.
Также у каждого шарика есть переменная inAction характеризующая его текущее состояние, а именно 0 — до столкновения, 1 — столкновение и рост, 2 — жизнь и уменьшение размера.
Еще в класс добавлен таймер, назначение которого — отслеживать время «жизни» шарика, начиная с того момента, как был достигнут максимальный размер. По истечении времени указанного в вышеприведённом интерфейсе (LIFETIME), приращение размера станет отрицательным, и по достижении нулевого размера объект будет удален.
В функции moveBall, отслеживается положение шарика, и его размер. Для этого, к координате, прибавляется величина скорости, которая в приведенном ниже классе BallGame, задается как случайная величина, а к значению базового радиуса добавляется его приращение (задается равным нулю).
Класс BallComponent наследует JPanel, и отвечает за отрисовку непосредственно игрового поля.Также в нем создается список, в который помещаются объекты типа Ball, и ведется счет. По истечении времени жизни объекта, он удаляется из списка.
Далее, в лучших традициях учебных примеров их Хорстманна и Корнелла был создан основной класс BallGame, который из которого вызывался класс BallGameFrame():
Класс BallGameFrame, наследующий JFrame, создает внешнюю оболочку для игрового поля, то есть отвечает за размещение элементов, отработку слушателей событий мыши, вывод информационных сообщений. А также он содержит функцию startGame(), вызываемую по щелчку мыши. Данная функция запускает поток, в котором крутится бесконечный игровой цикл.
Класс BallRunnable, в котором происходит основное действие.
Обратите внимание, что вывод сообщений на экран происходит в отдельном потоке. Подробнее об этом можно прочитать в Хорстманне, глава 14 «Многопоточная обработка», раздел «Потоки и библиотека Swing».
С каждым уровнем увеличивается общее количество шариков, и цель (сколько нужно выбить). Изначально я сделал, так, чтобы игроку нужно было сначала выбить много шариков (например 8 из 10), но тестирующим это показалось скучно, и игру забрасывали. Поэтому, я решил постепенно повышатьградус неадеквата уровень сложности.
Официальный рекорд — 86 уровень. Сам автор прошел максимум до 15 уровня.
Засим позвольте откланяться. Жду советов, критики и поддержки.

Сначала идет долгий и мучительный выбор Самой Правильной Книги, затем первый восторг от работы перепечатанных из нее листингов программ. Затем осознание растущей крутости и профессионализма. Падение в яму собственного ничтожества, при попытке написать что-то самостоятельно. И долгий путь наверх.
В моем случае Самой Правильной Книгой стал двухтомник «Java. Библиотека профессионала.» за авторством Кея Хорстманна и Гари Корнелла, а самой первой книгой, которая открыла дверь в мир Java – Яков Файн «Программирование на Java для детей, родителей, дедушек и бабушек».
Чтобы закрепить пытающиеся разбежаться знания, которые упорно пытались остаться на страницах Умных Книжек, а не в голове, я решил написать простую игру. Основная задача была в том, чтобы писать без применения сторонних библиотек.
Общая идея (не моя, а взята из флеш-игры Chain Rxn)
На прямоугольном игровом поле, в зависимости от уровня, появляется некоторое количество шариков, которые носятся по нему, с разной скоростью, отражаясь от стенок. Игрок нажимает курсором мыши, на игровом поле, и в точке нажатия возникает растущий шарик, который увеличивается до заданного радиуса. По истечении определенного времени, Остальные шарики, если сталкиваются с ним, останавливаются, увеличиваются в размерах, и также уменьшаются и исчезают.
Для каждого уровня определенная цель – сколько шариков должно быть «выбито».

Реализация.
Для начала был создан интерфейс GameConstants, в который были размещены все основные константы. Для всех классов было указано implements GameConstants:
Интерфейс GameConstants
public interface GameConstants { public final int DEFAULT_WIDTH = 600;//Ширина игрового поля public final int DEFAULT_HEIGHT = 300; //Высота игрового поля public final int DELAY = 8; //Задержка между «кадрами» игры public final int BASERADIUS=5; //Начальный радиус шариков public final int LIFETIME=1300; //Время «жизни» шарика public final int MAXRADIUS=25; //Максимальный радиус шарика public final int STARTQNTBALLS=10; //Количество шариков на первом уровне }
Затем был создан класс Ball. У каждого объекта данного класса, есть свой набор координат по осям x и y, переменные dx и dy, в которых записывается приращение координаты в единицу времени (по сути — скорость), значения радиуса и приращения радиуса, а также цвет и уникальный идентификатор. Идентификатор пригодится позже, когда будем отслеживать столкновения.
Также у каждого шарика есть переменная inAction характеризующая его текущее состояние, а именно 0 — до столкновения, 1 — столкновение и рост, 2 — жизнь и уменьшение размера.
Еще в класс добавлен таймер, назначение которого — отслеживать время «жизни» шарика, начиная с того момента, как был достигнут максимальный размер. По истечении времени указанного в вышеприведённом интерфейсе (LIFETIME), приращение размера станет отрицательным, и по достижении нулевого размера объект будет удален.
Класс Ball
public class Ball implements GameConstants { private int inAction; // Состояние шарика private int x; // координаты по x и y private int y; private int dx; //ускорение по осям x и y private int dy; private int radius; //радиус private int dRadius; //приращение радиуса private Color color; //цвет private static int count; public final int id=count++; // идентификатор (номер) шарика private static int score; // счёт private Timer gameTimer; private TimerTask gameTimerTask; //таймер отслеживающий время жизни шарика //конструктор Ball Ball(int x, int y, int dx, int dy, int radius, Color color, int inAction, int dRadius){ this.x=x; this.y=y; this.dx=dx; this.dy=dy; this.radius=radius; this.color=color; this.inAction=inAction; this.dRadius=dRadius; gameTimer = new Timer(); } //функция отвечающая за отрисовку шарика public Ellipse2D getShape(){ return new Ellipse2D.Double(x-radius, y-radius, radius*2, radius*2); } //отслеживание движения и столкновения мячиков: public void moveBall(BallComponent ballComponent){ x+=dx; y+=dy; radius+=dRadius; if(x<=0+radius){ x=radius; dx=-dx; } if (x>=DEFAULT_WIDTH-radius){ x=DEFAULT_WIDTH-radius; dx=-dx; } if(y<=0+radius){ y=radius; dy=-dy; } if (y>=DEFAULT_HEIGHT-radius){ y=DEFAULT_HEIGHT-radius; dy=-dy; } for(Ball ballVer: ballComponent.listBall){ //Столкновение - мы пробегаем по массиву содержащему все объекты Ball, //и построчно проверяем, не столкнулся ли «неактивированный» шарик, //с проверяемым (ballVer), и в каком состоянии находится проверяемый шар //И не является ли он сам собой (для чего и понадобился id) if(inAction==0) if((Math.sqrt(Math.pow(x-ballVer.x,2)+Math.pow(y-ballVer.y,2)))<=radius+ballVer.radius && id!=ballVer.id && (ballVer.inAction==1 || ballVer.inAction==2)) { ballComponent.score++; ballComponent.totalScore++; dx=dy=0; inAction=1; ballComponent.setBackground(ballComponent.getBackground().brighter()); } if(inAction==1){ dRadius=1; if (radius>=MAXRADIUS){ inAction=2; dRadius=0; //запускается таймер, который по прошествии времени жизни, начнёт уменьшать радиус шарика gameTimerTask = new gameTimerTask(this); gameTimer.schedule(gameTimerTask, LIFETIME); } } //Если радиус достиг нуля - мы удаляем шарик из списка if(inAction==2 && radius<=0){ ballComponent.listBall.remove(this); }}} //таймер, запускаемый по истечении LIFETIME, если радиус шарика достиг максимального: class gameTimerTask extends TimerTask{ private Ball ballTimer; public gameTimerTask(Ball ball) { // TODO Auto-generated constructor stub this.ballTimer = ball; } public void run() { // TODO Auto-generated method stub ballTimer.dRadius=-1; } } }
В функции moveBall, отслеживается положение шарика, и его размер. Для этого, к координате, прибавляется величина скорости, которая в приведенном ниже классе BallGame, задается как случайная величина, а к значению базового радиуса добавляется его приращение (задается равным нулю).
x+=dx; y+=dy; radius+=dRadius;
Класс BallComponent наследует JPanel, и отвечает за отрисовку непосредственно игрового поля.Также в нем создается список, в который помещаются объекты типа Ball, и ведется счет. По истечении времени жизни объекта, он удаляется из списка.
Класс BallComponent
public class BallComponent extends JPanel implements GameConstants { List<Ball> listBall = new CopyOnWriteArrayList<>(); boolean startClick; public int score=0; public int totalScore=0; //добавляем объект Ball в список public void addBall(Ball b){ listBall.add(b); } public void paintComponent(Graphics g){ super.paintComponent(g); Graphics2D g2d = (Graphics2D)g; for(Ball ball: listBall){ g2d.setColor(ball.getColor()); g2d.fill(ball.getShape()); } } public Dimension getPreferredSize() { return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT); }}
Далее, в лучших традициях учебных примеров их Хорстманна и Корнелла был создан основной класс BallGame, который из которого вызывался класс BallGameFrame():
Класс BallGame
public class BallGame implements GameConstants { public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { public void run() { JFrame ballFrame = new BallGameFrame(); ballFrame.setVisible(true); }}); }}
Класс BallGameFrame, наследующий JFrame, создает внешнюю оболочку для игрового поля, то есть отвечает за размещение элементов, отработку слушателей событий мыши, вывод информационных сообщений. А также он содержит функцию startGame(), вызываемую по щелчку мыши. Данная функция запускает поток, в котором крутится бесконечный игровой цикл.
Класс BallGameFrame
class BallGameFrame extends JFrame implements GameConstants{ private int level=1; //Первый уровень private int ballQnt; private BallComponent ballComponent; private MousePlayer mousePlayerListener; //конструктор public BallGameFrame() { ballQnt=STARTQNTBALLS; setTitle("BallGame"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); ballComponent = new BallComponent(); ballComponent.setBackground(Color.DARK_GRAY); mousePlayerListener = new MousePlayer(); add(ballComponent, BorderLayout.CENTER); final JPanel buttonPanel = new JPanel(); final JButton startButton = new JButton("Начать игру."); buttonPanel.add(startButton); final JLabel scoreLabel = new JLabel(); buttonPanel.add(scoreLabel); startButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent arg0) { ballComponent.addMouseListener(mousePlayerListener); ballComponent.addMouseMotionListener(mousePlayerListener); startButton.setVisible(false); ballComponent.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR)); startGame(scoreLabel, ballQnt); }}); add(buttonPanel, BorderLayout.SOUTH); pack(); } public void startGame(JLabel scoreLabel, int ballQnt){ Runnable r = new BallRunnable(ballComponent, scoreLabel, level, ballQnt); Thread t = new Thread(r); t.start(); } // внутренний Класс MousePlayer, для отработки событий от мыши: class MousePlayer extends MouseAdapter{ public void mouseClicked(MouseEvent e) { //Создаем шарик игрока Random random = new Random(); //Создаем шарик игрока, с приращением радиуса равным единице //и приращением координат (скоростями), равными нулю Ball ball = new Ball(e.getX(), e.getY(), 0, 0, BASERADIUS, new Color(random.nextInt(255),random.nextInt(255),random.nextInt(255)), 1, 1); ballComponent.startClick=true; ballComponent.addBall(ball); //Удаляем слушателя мыши, чтобы пользователь не мог накликать еще шариков, и приводим курсор мыши в первоначальное положение ballComponent.removeMouseListener(mousePlayerListener); ballComponent.removeMouseMotionListener(mousePlayerListener); ballComponent.setCursor(Cursor.getDefaultCursor()); }}}
Класс BallRunnable, в котором происходит основное действие.
Класс BallRunnable
class BallRunnable implements Runnable, GameConstants{ private BallComponent ballComponent; private JLabel scoreLabel; private int level, ballQnt; private MousePlayer mousePlayerListener; private int goal; public BallRunnable(final BallComponent ballComponent, JLabel scoreLabel, int level, int ballQnt) { this.ballComponent = ballComponent; this.scoreLabel = scoreLabel; this.level=level; this.ballQnt=ballQnt; this.goal=2; } class MousePlayer extends MouseAdapter{ public void mousePressed(MouseEvent e) { Random random = new Random(); Ball ball = new Ball(e.getX(), e.getY(), 0, 0, BASERADIUS, new Color(random.nextInt(255),random.nextInt(255),random.nextInt(255)), 1, 1); ballComponent.addBall(ball); ballComponent.startClick=true; ballComponent.removeMouseListener(mousePlayerListener); ballComponent.removeMouseMotionListener(mousePlayerListener); ballComponent.setCursor(Cursor.getDefaultCursor()); }} public void run(){ while(true){ try{ mousePlayerListener = new MousePlayer(); ballComponent.addMouseListener(mousePlayerListener); ballComponent.addMouseMotionListener(mousePlayerListener); //меняем внешний вид курсора на крестик ballComponent.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR)); //сколько осталось шариков в работе int countInWork=1; // Генерация массива шариков //приращения скорости задаются случайно //приращение радиуса равно нулю for (int i=0;i<ballQnt; i++){ Random randomX = new Random(); Random randomY = new Random(); Ball ball = new Ball(randomX.nextInt(DEFAULT_WIDTH), randomY.nextInt(DEFAULT_HEIGHT), randomX.nextInt(2)+1, randomY.nextInt(2)+1, BASERADIUS, new Color(randomX.nextInt(255),randomX.nextInt(255),randomX.nextInt(255)), 0, 0); ballComponent.addBall(ball); } // пока есть активированные шарики while (countInWork!=0){ countInWork=0; if(!ballComponent.startClick) { EventQueue.invokeLater(new Runnable() { public void run() { // TODO Auto-generated method stub scoreLabel.setText("Цель: выбить "+ goal+" шаров из "+ ballQnt); } } ); countInWork=1; } for(Ball ball: ballComponent.listBall){ if((ball.inAction()==1 || ball.inAction()==2)) countInWork++; //если остались активированные шарики ball.moveBall(ballComponent); ballComponent.repaint(); if(ballComponent.startClick){ //обновляем информационную строку EventQueue.invokeLater(new Runnable() { public void run() { scoreLabel.setText("Уровень: "+ level+", Вы выбили "+ballComponent.score+" из "+ballQnt); }}); }} Thread.sleep(DELAY); } } catch (InterruptedException ex){ ex.printStackTrace(); } ballComponent.listBall.clear(); ballComponent.repaint(); //Выводим результат if(ballComponent.score<goal) { EventQueue.invokeLater(new Runnable() { public void run() { scoreLabel.setText("Цель уровня не достигнута!"); } }); JOptionPane.showMessageDialog(ballComponent, "Цель уровня не достигнута. \nНабрано очков: "+ ballComponent.totalScore+".\n Попробуйте еще раз."); ballComponent.startClick=false; ballComponent.score=0; ballComponent.setBackground(Color.DARK_GRAY); } else{ EventQueue.invokeLater(new Runnable() { public void run() { scoreLabel.setText("Уровень пройден!!!"); } }); ballComponent.startClick=false; level++; ballQnt++; goal++; ballComponent.setBackground(Color.DARK_GRAY); ballComponent.score=0; JOptionPane.showMessageDialog(ballComponent, "Уровень "+level+".\nЦель: выбить "+ goal+" шаров из "+ ballQnt); }}}
Обратите внимание, что вывод сообщений на экран происходит в отдельном потоке. Подробнее об этом можно прочитать в Хорстманне, глава 14 «Многопоточная обработка», раздел «Потоки и библиотека Swing».
С каждым уровнем увеличивается общее количество шариков, и цель (сколько нужно выбить). Изначально я сделал, так, чтобы игроку нужно было сначала выбить много шариков (например 8 из 10), но тестирующим это показалось скучно, и игру забрасывали. Поэтому, я решил постепенно повышать
Официальный рекорд — 86 уровень. Сам автор прошел максимум до 15 уровня.
Засим позвольте откланяться. Жду советов, критики и поддержки.