Создание панели для отображения действий игры

Все действия, происходящие в игре мы будем отрисовывать вручную. Для этого нам необходимо создать собственный элемент управления (ЭУ) и переопределить у него обработчик события paintComponent, которое отвечает за прорисовку элемента управления.

Создаем класс, наследующий от JСomponent (это базовый класс для всех ЭУ)

Класс назавем TetrisPanel.

public class TetrisPanel extends JComponent{

private Timer tm; //наш таймер

//конструктор

public TetrisPanel(Timer ptm) {

super();

tm = ptm;

}

//обработчик прорисовки ЭУ

//пока мы просто заполняем ЭУ серым цветом

@Override

protected void paintComponent(Graphics g) {

super.paintComponent(g);//конструктор родителя

Graphics2D g2d = (Graphics2D) g;

Dimension dd = getSize(); //получаем размеры

g.setColor(Color.darkGray); //выбираем цвет

g.fillRect(0, 0, dd.width, dd.height); //закрашиваем

}

}

Далее необходимо разместить созданный нами ЭУ на форме, как показано на рис 6.

Для размещения ЭУ на форме без использования визуальных редакторов, в библиотеке Swing существуют специальные Менеджеры расположения (МР). Это классы-контейнеры, которые управляют положением и размерами помещенных в них ЭУ.

Мы воспользуемся простым менеджером расположения BoxLayout. Он позволяет располагать ЭУ последовательно либо по горизонтали, либо по вертикали. В нашем случае схема расположения ЭУ на форме будет следующей (рис. 7)

Напишем код, добавляющий на форму данные компоненты

//получаем ссылку на панель содержимого формы

Container c = getContentPane();

//создаем вертикальный МР (на рис. 7 показан синим)

BoxLayout bv = new BoxLayout(c, BoxLayout.Y_AXIS);

//указываем его как МР формы

c.setLayout(bv);

//добавляем вертикальную «распорку» в 50 пикселей

//это специальный ЭУ для создания отступов

c.add(Box.createVerticalStrut(50));

//создаем еще одну панель для размещения на ней горизонтального МР

JPanel ph = new JPanel();

//добавляем ее как второй элемент в вертикальный МР

c.add(ph);

//добавляем еще одну вертикальную «распорку» в 50 пикселей

c.add(Box.createVerticalStrut(50));

//создаем горизонтальный МР (на рис 7 показан красным)

BoxLayout bh = new BoxLayout(ph, BoxLayout.X_AXIS);

//устанавливаем его на панель ph (в центр формы)

ph.setLayout(bh);

//добавляем первую горизонтальную распорку в 100 пикселей

ph.add(Box.createHorizontalStrut(100));

//создаем TetrisPanel

drawPanel = new TetrisPanel(timer);

//устанавливаем для нее размеры 240 х 480

drawPanel.setPreferredSize(new Dimension(240, 480));

//добавляем на форму

ph.add(drawPanel);

//добавляем первую горизонтальную распорку в 100 пикселей

ph.add(Box.createHorizontalStrut(100));

//компонуем ЭУ

pack();

Данный код необходимо поместить в конструктор главной формы вместо вызова setSize() и перед setVisible().

Запустите и проверьте, что получилось.

Теперь приступим к разработке классов по обработке данных.

Создадим абстрактный класс, описывающий прямоугольную область. Вся область разделена на квадраты размером 20х20. Как сам «стакан», так и падающие фигуры состоят из таких квадратов.

Создайте новый класс:

public abstract class Elem {

//константа: ширина квадрата

public static final int WIDTH = 20;

// базовый массив, пригодится для фигуры и «стакана»

// для упрощения, сделаем его открытым членом класса

// будет содержать 0 или 1 (можно было его сделать не int a boolean)

public int[][] figArr;

// координата левого верхнего угла элемента

// относительно левого верхнего угла контейнера

// содержит два поля x и y.

protected Point lu;

// ширина квадрата в пикселях

protected int widthElem = WIDTH;

// цвет элемента

protected Color color;

//цвет фона

protected Color bkgColor;

// конструктор с параметрами.

public Elem(Point p, Color c, Color bkgColor, int[][]a){

//инициализируем переменные уровня класса

lu = p;

color = c;

this.bkgColor = bkgColor;

figArr = a;

}

// абстрактный метод отрисовки элемента

public abstract void draw(Graphics2D g);

// Метод рисующий элемент цветом фона (используется для

// скрытия элемента)

public abstract void hide(Graphics2D g);

// возвращает координаты правого нижнего угла в квадратиках

public abstract Point getRBPoint();

//переводит координаты в квадратиках в координаты в пикселях

public Point coordToPixels(Point p){

return new Point(p.x*widthElem, p.y*widthElem);

}

// устанавливает координаты левого верхнего угла

public void setLU(int px, int py){ lu.x=px; lu.y=py;};

// возвращает координаты левого верхнего угла в квадратиках

public Point getLU(){return new Point(lu);}

// устанавливает ширину элемента в пикселях

public void setWidth(int pw) { widthElem=pw; }

public int getWidthElem() { return widthElem; }

//устанавливает цвет фигуры и цвет фона

public void setColor(Color c) { color=c; }

public void setBkgColor(Color c) { color=c; }

// возвращает цвет фигуры и фона

public Color getColor() { return color; }

public Color getBkgColor() { return bkgColor; }

//возвращает ширину и высоту фигуры («стакана») в квадратиках

public abstract int getWidth();

public abstract int getHeight();

}

Далее создадим класс Figure. Этот класс будет описывать фигуру, падающую в «стакан». Фигура представляет собой массив 4х4 квадратных элемента со стороной WIDTH пикселей каждый. В классе, массив является массивом целых чисел, содержащим 0 или 1. Если элемент массива содержит 0, он не отрисовывается, если 1 – отображается заданным цветом. Например:

{{0,1,1,0},

{1,1,0,0},

{0,0,0,0},

{0,0,0,0}};

Однако, здесь есть одно ограничение: фигура в массиве всегда должна быть сдвинута максимально влево - вверх. Т.е. первой строкой, содержащей единицы должна быть строка с индексом 0. Первым столбцом, содержащим единицы, должен быть столбец с индексом 0. Фигура не обязательно должна занимать весь массив (как показано на примере выше).

Создайте новый класс Figure:

public class Figure extends Elem{

//максимальное количество квадратов по ширине и высоте

public static final int FIG_LEN = 4;

// конструктор с параметрами

public Figure(Point p, Color c, Color bkgc, int[][] a){

super(p, c, bkgc, a); //вызываем конструктор Elem

}

// метод, выполняющий поворот фигуры по часовой стрелке

public void rotateClockwise(){

//написать самим

}

// метод, выполняющий поворот фигуры против часовой стрелки

public void rotateAntiClockwise(){

//написать самим

}

// отрисовка фигуры

public void draw(Graphics2D g){

int i,j;

g.setColor(color);

Point p = coordToPixels(lu);

for(i=0; i<FIG_LEN; i++)

for(j=0; j<FIG_LEN; j++)

if(figArr[i][j]!= 0){

g.fillRect(p.x + j*widthElem, p.y +

i*widthElem, widthElem, widthElem);

}

}

// делаем фигуру невидимой (отрисовка цветом фона)

public void hide(Graphics2D g){

int i,j;

g.setColor(bkgColor);

Point p = coordToPixels(lu);

for(i=0; i<FIG_LEN; i++)

for(j=0; j<FIG_LEN; j++)

if(figArr[i][j]!= 0){

g.fillRect(p.x + j*widthElem, p.y +

i*widthElem, widthElem, widthElem);

}

}

// возвращаем координаты последней занятой строки и

// последнего занятого столбца в массиве figArr в элементах

// относительно начала массива

// например для массива, описанного выше в примере эта функция

// вернет: x==2, y==1

public Point getRBPoint(){

//написать самим

}

public int getWidth(){ return FIG_LEN;}

public int getHeight(){ return FIG_LEN;}

//метод создающий новый объект — точную копию текущей фигуры

@Override

public Figure clone(){

//написать самим

}

}

Теперь необходимо протестировать класс Figure и те методы, которые вы написали самостоятельно. Для этого выполним несколько шагов.

Вернемся к интерфейсу пользователя. В классе Tetris создайте закрытый метод

private void startNewGame(){

timer.start(); // запускаем таймер

}

В конструкторе класса Tetris в обработчике меню mNewGame раскомментируйте строку

//для «Новая игра»

mNewGame.addActionListener(new ActionListener() {

@Override

public void actionPerformed(ActionEvent e) {

startNewGame(); //<-- здесь

}

});

Теперь при выборе пункта меню «Новая игра» будет запускаться таймер.

Несколько слов о таймерах. Таймер это объект, который генерирует событие Action через равный промежуток времени (его мы задали при создании таймера он равен 400 милисек. (см. конструктор класса Tetris)). Все наши события в игре будут происходить синхронно по таймеру. Изменяя временной интервал можно увеличивать или уменьшать скорость падения фигур.

Создадим класс-обработчик события таймера, но для удобства сделаем это в классе TetrisPanel:

class TimerAct implements ActionListener{

@Override

public void actionPerformed(ActionEvent e) {

repaint();

}

}

Т.е. при наступлении события таймера у нас наше игровое поле будет перерисовываться.

Теперь нужно подключить этот обработчик к нашему таймеру. В конструкторе TetrisPanel добавим

public TetrisPanel(Timer ptm) {

super();

this.tm = ptm;

//добавляем здесь:

this.tm.addActionListener(new TimerAct());

}

Теперь поместим в «стакан» одну фигуру для тестирования. Для этого в классе TetrisPanel

добавим переменную уровня класса

private Figure fig;

В конструкторе TetrisPanel создадим фигуру

public TetrisPanel(Timer ptm) {

super();

this.tm = ptm;

this.tm.addActionListener(new TimerAct());

int[][] arr1={{1,1,1,0},

{0,1,0,0},

{0,0,0,0},

{0,0,0,0}};

fig = new Figure(new Point(4,4), Color.cyan, Color.darkGray, arr1);

}

а в конец обработчика paintComponent добавим

fig.draw(g2d);

Запустите программу. Фигура должна отображаться на игровом поле.

Поворот фигуры при нажатии на кнопки клавиатуры.

Для управления игрой с клавиатуры, необходимо написать класс-обработчик нажатия на клавиши клавиатуры.

Создайте новый класс

class TetrisKeyListener extends KeyAdapter {

//конструктор

public TetrisKeyListener(){ super();}

//метод обработки нажатия на клавишу клавиатуры

@Override

public void keyPressed(KeyEvent e){

int key = e.getKeyCode();

switch(key){

case KeyEvent.VK_LEFT:

break;

case KeyEvent.VK_RIGHT:

break;

case KeyEvent.VK_UP:

fig.rotateClockwise();

break;

case KeyEvent.VK_DOWN:

fig.rotateAntiClockwise();

break;

}

repaint();

}

//метод обработки нажатия на алфавитно-цифровую

//клавишу клавиатуры

@Override

public void keyTyped(KeyEvent e){

char ch = e.getKeyChar();

switch(ch){

case ' ': //пробел - пауза

if(timer.isRunning())

timer.stop();

else timer.start();

break;

case 'n': //n — новая игра

case 'N':

startNewGame();

break;

case 'q': //выход

System.exit(0);

break;

}

}

}

В конструкторе класса Tetris перед вызовом

...

pack();

добавьте строку подключающую наш обработчик событий клавиатуры

addKeyListener(new TetrisKeyListener());

Запустите программу. Теперь фигура должна вращаться при нажатии на клавиши «Стрелка вверх» и «Стрелка вниз».

Теперь приступим к созданию «стакана». Игровое поле состоит из двухмерного массива элементов (квадратиков) также как и фигура. В процессе игры, когда фигура падает, она представляет собой объект типа Figure. Однако, когда она достигает дна «стакана», то должна стать частью игрового поля т. е. набором элементов внутри массива элементов. Таким образом класс «стакан» выглядит следующим образом

public class Glass extends Elem {

public static final int GLASS_WIDTH = 12;

public static final int GLASS_HEIGHT = 20;

//массив элементов, описывающий игровое поле

//нам достался по наследству. Это figArr (см. класс Elem)

//массив указателей на фигуры, падающие в стакан

protected Figure[] figures;

protected int figCount; // количество фигур

//нужно ли бросать новую фигуру

//когда падающая фигура стала частью «стакана» и мы удалили

//пустые строки, нам нужен «сигнал» для запуска новой фигуры

//это и есть сигнальная переменная.

//когда фигура падает, она равна false, когда уже упала - true

protected boolean isNeedNew;

//возвращает истину, если заданная строка стакана полностью

//занята

protected boolean isStringFull(int i){

//i — номер строки в массиве figArr

//написать самим

}

//генератор случайных чисел

private Random rn;

//текущая падающая фигура

public Figure curFig;

//конструктор

public Glass(Color c, Color bkgc){

super(new Point(), c, bkgc, null);

rn = new Random();

//создаем массив элементов игрового поля

figArr = new int[24][12];

//создаем семь фигур и добавляем их в массив фигур figures

figures = new Figure[7];

int[][] arr1={

{1,1,1,0},

{0,1,0,0},

{0,0,0,0},

{0,0,0,0}};

figures[0] = new Figure(new Point(), c, bkgc, arr1);

int[][] arr2={

{1,1,0,0},

{0,1,1,0},

{0,0,0,0},

{0,0,0,0}};

figures[1] = new Figure(new Point(), c, bkgc, arr2);

int[][] arr3={

{0,1,1,0},

{1,1,0,0},

{0,0,0,0},

{0,0,0,0}};

figures[2] = new Figure(new Point(), c, bkgc, arr3);

int[][] arr4={

{1,1,0,0},

{1,1,0,0},

{0,0,0,0},

{0,0,0,0}};

figures[3] = new Figure(new Point(), c, bkgc, arr4);

int[][] arr5={

{1,1,1,0},

{0,0,1,0},

{0,0,0,0},

{0,0,0,0}};

figures[4] = new Figure(new Point(), c, bkgc, arr5);

int[][] arr6={

{1,1,1,0},

{1,0,0,0},

{0,0,0,0},

{0,0,0,0}};

figures[5] = new Figure(new Point(), c, bkgc, arr6);

int[][] arr7={

{1,0,0,0},

{1,0,0,0},

{1,0,0,0},

{1,0,0,0}};

figures[6] = new Figure(new Point(), c, bkgc, arr7);

figCount = 7;

}

//полная очистка стакана

public void clear()

{

int i,j;

for(i=0; i<figArr.length; i++)

for(j=0; j<figArr[0].length; j++)

figArr[i][j] = 0;

}

//методы проверки:можно ли повернуть, сдвинуть, опустить фигуру

//все методы проверки начинаются со слова try....

//можно ли повернуть фигуру по часовой стрелке

public boolean tryRotateClockwise(){

//я делал это так:

// создавал новую фигуру — клон падающей (для этого у нас есть метод

// clone() в классе фигуры

// поворачивал этот клон с помощью rotateClockwise();

// проверял: не выходит ли эта повернутая фигура за рамки «стакана»

}

//можно ли повернуть фигуру против часовой стрелки

public boolean tryRotateAntiClockwise(){

// аналогично

}

//можно ли сдвинуть фигуру на один элемент влево

public boolean tryShiftLeft(){

//сделать самим

}

//можно ли сдвинуть фигуру на один элемент вправо

public boolean tryShiftRight(){

//сделать самим

}

//можно ли сдвинуть фигуру на один элемент вниз

public boolean tryShiftDown(){

//сделать самим

}

//функции сдвигающие/поворачивающие фигуру:

// сначала проверяем: можно ли сдвинуть/повернуть,

// а потом сдвигаем/поворачиваем

public void stepLeft(){

if(tryShiftLeft()){

Point p=curFig.getLU();

curFig.setLU(p.x-1, p.y);

}

}

public void stepRight(){

if(tryShiftRight()){

Point p=curFig.getLU();

curFig.setLU(p.x+1, p.y);

}

}

public void stepDown(){

Point p = curFig.getLU();

p.y++;

curFig.setLU(p.x, p.y);

}

public void rotateClockwise(){

if(tryRotateClockwise()){

curFig.rotateClockwise();

}

}

public void rotateAntiClockwise(){

if(tryRotateAntiClockwise()){

curFig.rotateAntiClockwise();

}

}

//нужна ли новая фигура

public boolean isNeedNewFig() {return isNeedNew;}

//устанавливаем, что новая фигура нужна (или нет)

public void setIsNeedNew(boolean x){isNeedNew=x;}

//возвращает случайную фигуру из массива фигур

public Figure getRandomFig(){

int n = rn.nextInt(figCount);

curFig = figures[n];

return curFig;

}

//удаляет все заполненные строки

public void removeFullString(){

//проходим по массиву figArr по строкам и проверяем

// с помощью метода isStringFull() заполнена ли строка или нет

// и если заполнена, удаляем ее сдвигая верхние строки вниз

}

//переводит падающую фигуру в элементы массива figArr

public void moveFigToArr(){

Point p = curFig.getLU();

int i,j;

for(i=0; i<curFig.getHeight(); i++)

for(j=0; j<curFig.getWidth(); j++)

if(curFig.figArr[i][j]!= 0)

figArr[p.y+i][p.x+j]=1;

curFig = null;

}

//рисуем «стакан»

public void draw(Graphics2D g){

int i,j, w, h;

w = getWidth();

h = getHeight();

g.setColor(getBkgColor());

g.fillRect(0, 0, w*widthElem, h*widthElem);

g.setColor(getColor());

for(i=0; i<h; i++)

for(j=0; j<w; j++){

if(figArr[i][j]!= 0){

g.fillRect(j*widthElem, i*widthElem, widthElem, widthElem);

}

}

if(curFig!=null)

curFig.draw(g);

}

//метод (нам не нужен)

public void hide(Graphics2D g){

}

//возвращаем координаты правого нижнего угла стакана в элементах

public Point getRBPoint(){

return new Point(getWidth()-1, getHeight()-1);

}

@Override

public int getWidth() {

return figArr[0].length;

}

@Override

public int getHeight() {

return figArr.length;

}

}

И, в завершении, соберем все вместе.

В класс Tetris добавим переменную уровня класса

Glass glass;

В конструкторе класса Tetris перед вызовом метода pack() добавим создание стакана и подключение его к панели

glass = new Glass(Color.cyan, Color.darkGray);

drawPanel.setGlass(glass);

Вернемся к классу TetrisPanel и изменим метод

protected void paintComponent(Graphics g)

как показано ниже:

@Override

protected void paintComponent(Graphics g) {

super.paintComponent(g);

Graphics2D g2d = (Graphics2D) g;

Dimension dd = getSize();

if(glass == null){ //если стакана нет, рисуем серый квадрат

g.setColor(Color.darkGray);

g.fillRect(0, 0, dd.width, dd.height);

return;

}

// «стакан» рисует сам себя со всеми тонкостями и нюансами

glass.draw(g2d);

}

Чтобы все работало осталось написать метод таймера. Мы уже писали следующий код:

class TimerAct implements ActionListener{

@Override

public void actionPerformed(ActionEvent e) {

//здесь нам нужно описать управление игрой

//метод таймера вызывается периодически через равные промежутки

//времени. При этом игровое поле может иметь следующие состояния:

// нужна новая фигура: тогда мы

// - убираем пустые строки

// - получаем новую случайную фигуру

// - устанавливаем ее в центр стакана

// - проверяем: можно ли ее сдвинуть вниз

// если нельзя — конец игры

// если можно, то

// сбрасываем флаг «нужна новая фигура»

// вызываем метод repaint();

// завершаем метод таймера

// текущая фигура есть и она падает, тогда:

// - проверяем, можно ли ее сдвинуть вниз

// - если можно,

// - то сдвигаем

// - вызываем метод repaint();

// - завершаем метод таймера

// - если нельзя

// - превращаем падающую фигуру в массив эл. стакана

// - устанавливаем флаг «нужна новая фигура»

// - вызываем метод repaint();

// - завершаем метод таймера

}

}

Да, и в классе Tetris добавим метод

private void startNewGame(){

glass.clear();

glass.setIsNeedNew(true);

timer.start();

}

и раскомментируем соответствующую строку в обработчике меню.


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: