Игра на чистой Java от новичка, для новичков

от автора

Я начинающий программист на 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), и в каком состоянии находится проверяемый шар
*/
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;
}
}
}

Класс 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() {
Override
public void run() {
// TODO Auto-generated method stub
JFrame ballFrame = new BallGameFrame();
ballFrame.setVisible(true);
}});
}}

Класс BallGameFrame, наследующий JFrame, создает внешнюю оболочку для игрового поля, то есть отвечает за размещение элементов, отработку слушателей событий мыши, вывод информационных сообщений.

Класс 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®;
t.start();
}
// внутренний Класс MousePlayer, для отработки событий от мыши:
class MousePlayer extends MouseAdapter{
public void mouseClicked(MouseEvent e) { //Создаем шарик игрока
// TODO Auto-generated method stub
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 уровня.

Засим позвольте откланяться. Жду советов/критики/поддержки/восхваления (нужное подчеркнуть).

ссылка на оригинал статьи http://habrahabr.ru/post/266117/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *