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



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

В моем случае Самой Правильной Книгой стал двухтомник «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 уровня.

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

Комментарии (17)


  1. 1nt3g3r
    04.09.2015 15:30
    +1

    Зачем реализовать интерфейс GameConstants? Если вы хотите иметь удобный доступ к константам из этого интерфейса, достаточно сделать import static GameConstants.*;

    Этот же вопрос про BallGame. Это класс-лаунчер, ему-то точно не нужно иметь доступ к GameConstants.

    Также у вас в каждом экземпляре Ball используется Timer. Каждый Timer — это отдельный поток. У меня подозрение, что это может плохо сказаться на производительности. Вам проще было бы сделать отдельный метод (например, update()) в классе Ball и из BallComponent вызывать этот метод.

    Я советую вам дальше почитать «Чистый код» Роберта Мартина. Не воспринимайте ее очень уж серьезно, но стиль кода она должна вам поднять.

    Почитайте про паттерны проектирования, это вам пригодится всегда, особенно если вы пишете игры.

    Пользуясь случаем, хочу проагитировать libGDX, чудесный фреймворк для написания игр. Хорошая документация, отличная архитектура — если хотите двинуться в сторону разработки игр на Java, это отличный выбор.

    В общем, поздравляю вас с первой игрой!


    1. alhimik45
      06.09.2015 16:52

      Пользуясь случаем, хочу проагитировать libGDX, чудесный фреймворк для написания игр. Хорошая документация, отличная архитектура — если хотите двинуться в сторону разработки игр на Java, это отличный выбор.

      Подтверждаю, действительно очень хороший фреймворк. Учавствовал во флешмобе «100 игр за неделю», была придумана идея, для реализации выбрал libGDX. В итоге ни разу не пожалел — действительно отличная официальная документация, много информации на StackOverflow и хорошие примеры. Единственной странностью было то, что html-бэкенд не определял пользовательскую локаль, пришлось немного гуглить про GWT и делать определение отдельно.


  1. Snakecatcher
    04.09.2015 15:40

    Большое спасибо!
    Учту советы, и поправлю код.

    Как раз открыл для себя «Паттерны проектирования» от издательства Head First. Буду пугать народ в метро (точнее тех, кто любит читать через плечо).
    Как-то услышал совет — «Если учитесь, изобретайте велосипеды». Естественно, что в серьезных проектах лучше всего использовать уже проверенные библиотеки и фреймворки, но в целях обучения, лучше поломать голову и попытаться написать все с нуля.
    В будущем думаю двинуться в сторону Android (для чего собственно и вгрызаюсь в Java). Там же, в создании игр, сейчас как я понял рулит NDK?


    1. 1nt3g3r
      04.09.2015 15:50
      -2

      NDK — это «нативный» код для Andorid, пишете на С++. И как раз NDK и не рулит там. Я наблюдаю тенденцию, что люди хотят выпускать игры для нескольких платформ одновременно. NDK здесь никак не применим, его юзают, если нужно написать что-то низкоуровневое и быстрое.

      Для игр на Android популярны кросплатформенные игровые движки. Это монстр Unity 3d, конечно же, это Cocos2d-x. Лично я использую libGDX, очень быстро и удобно писать — после написания очередного куска кода можно сразу запустить на десктопе, и намного реже — на телефоне.

      Есть возможность создать сборки под Linux, Windows, Mac OS X, Android — эти платформы вообще без проблем. Несколько хуже, есть некоторые ограничения — для iOS. И если ваша игра очень уж простая — можно сделать сборку под html5. При этом пишете вы все на той же Java, просто под капотом в libGDX много черной магии, которая скрывает от вас много сложного :)


      1. Snakecatcher
        04.09.2015 15:57
        +1

        Обязательно попробую. У вас, как раз смотрю есть подборка туториалов, почитаю.
        Почему NDK вспомнился — где-то год или полтора назад, ZeptoLab, проводила конкурс на написание игры. И обязательным требованием было как раз использование NDK.


        1. zagayevskiy
          04.09.2015 17:50

          У ZeptoLab'a собственный движок на С++ и OpenGL. Поэтому они и хотят крутых программистов на С++.


      1. Snakecatcher
        04.09.2015 16:02
        +1

        Кстати, я игру реализовал еще как апплет, и поставил ее, как заглушку для своего сайта.
        Не знаю, разрешено ли публиковать ссылки?


        1. QuePaso
          04.09.2015 23:22

          Вы можете добавить личный сайт в свой профиль. Желающие пройдут по ссылке

          и еще небольшой комментарий в продолжение 1nt3g3r
          Как написано в Item 19 в книге Effective Java: интерфейсы необходимо использовать только для определения типов. Там как раз в качестве плохого примера приводится интерфейс с константами. В таком случае советуют создавать самостоятельный утилитарный класс, в котором уже объявлять все константы, а потом обращаться к ним либо по полному имени ClassName.CONSTANT, либо через статический импорт, о чем уже было написано выше


          1. Snakecatcher
            05.09.2015 11:36

            Добавил сайт в профиль. Единственная проблема, что по-умолчанию браузеры не доверяют Java-апплетам. Я нашел выход в том, что сайт надо добавлять в доверенные. Кстати, может подскажете, есть ли возможность сделать так, чтобы апплет запускался без дополнительного шаманства, со стороны пользователя?


      1. zagayevskiy
        04.09.2015 17:48
        +2

        Я наблюдаю тенденцию, что люди хотят выпускать игры для нескольких платформ одновременно. NDK здесь никак не применим
        Расскажите это ZeptoLab, а то они не знают.


        1. vagran
          04.09.2015 22:20
          +1

          Собственно да, писать кросс-платформенно на C++, конечно, сложнее, чем на джаве, но ровно настолько, насколько вообще сложнее писать на C++, чем на джаве. Ну то есть для C++ программиста это не проблема.


          1. zagayevskiy
            05.09.2015 00:46

            Ну, я, вроде, обратного не утверждал:)


    1. pashtikus
      04.09.2015 17:44
      +1

      Касаемо паттернов могу порекомендовать книгу «Game Programming Patterns» за авторством Боба Нистрома.
      Прямо настольное чтиво мое, хотя вообще нечасто читаю подобного рода литературу.
      www.gameprogrammingpatterns.com


  1. nikitasius
    05.09.2015 12:30

    Статья про чистый код, а комментарии про то, что «надо использовать фреймворки» :facepalm:
    Автор молодец, что не использовал фреймворк, а сделал все as is сам.

    Это вечный холивар (чистый или грязный кодинг) и я на вашей стороне, я за чистоту.


    1. bohdan4ik
      05.09.2015 17:31

      Нет никакого «чистого» и «грязного» кодинга. Есть задача и есть время на её решение. Фреймворки дают приемлимое качество в приемлимые сроки.

      P.S.: «чистый код» это уже устоявшееся выражение и означает оно отнюдь не «отсутствие фреймворков». Подкорректируйте свой словарик, пожалуйста, дабы не сбивать с толку собеседников.


      1. nikitasius
        06.09.2015 00:47
        -2

        Подкорректируйте свой словарик, пожалуйста, дабы не сбивать с толку собеседников.

        Вы мне ваше мнение навязываете?
        Чистый это: clear/clean. Первый — это и чистый и ясный (компактные методы и т.д.), второй — это четкий, соотвествующий неким принципам написания.
        Теперь название статьи «Игра на чистой Java от новичка, для новичков» — вы внимательно прочитали? Чистый язык программирования подразумевает только этот язык, и в контексте статьи про чистую java уместно употреблять выражение чистый код. Не находите?
        Код, который не загажен или перегружен фреймворками без фреймворков.


        1. vaa25
          08.09.2015 23:01
          -1

          А вы внимательно прочитали первый комментарий? Как раз он и определил контекст выражения «Чистый код». После него выражение «Чистый код» означает здесь примерно это, но никак не чистую java без фреймворков. Ваш комментарий действительно сбивает с толку.