[Из песочницы] Игра на чистой Java от новичка, для новичков

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

b3ae28377f734b71ac7bd85b6b9ed3fd.jpg

Сначала идет долгий и мучительный выбор Самой Правильной Книги, затем первый восторг от работы перепечатанных из нее листингов программ. Затем осознание растущей крутости и профессионализма. Падение в яму собственного ничтожества, при попытке написать что-то самостоятельно. И долгий путь наверх.

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

Общая идея (не моя, а взята из флеш-игры Chain Rxn)
На прямоугольном игровом поле, в зависимости от уровня, появляется некоторое количество шариков, которые носятся по нему, с разной скоростью, отражаясь от стенок. Игрок нажимает курсором мыши, на игровом поле, и в точке нажатия возникает растущий шарик, который увеличивается до заданного радиуса. По истечении определенного времени, Остальные шарики, если сталкиваются с ним, останавливаются, увеличиваются в размерах, и также уменьшаются и исчезают.

Для каждого уровня определенная цель — сколько шариков должно быть «выбито».

6c16a590c06a46d8b934a7311c3398c2.jpg

Реализация.
Для начала был создан интерфейс 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 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


Обратите внимание, что вывод сообщений на экран происходит в отдельном потоке. Подробнее об этом можно прочитать в Хорстманне, глава 14 «Многопоточная обработка», раздел «Потоки и библиотека Swing».

С каждым уровнем увеличивается общее количество шариков, и цель (сколько нужно выбить). Изначально я сделал, так, чтобы игроку нужно было сначала выбить много шариков (например 8 из 10), но тестирующим это показалось скучно, и игру забрасывали. Поэтому, я решил постепенно повышать градус неадеквата уровень сложности.

Официальный рекорд — 86 уровень. Сам автор прошел максимум до 15 уровня.

Засим позвольте откланяться. Жду советов, критики и поддержки.

© Habrahabr.ru