Виммельбух, 3, перетяжка
Виммельбух, 3, перетяжка
Виммельбух, 3, перетяжка

Как написать своего сапёра на Java за 15 минут

Аватар Пётр Соковых
Отредактировано

67К открытий70К показов
Как написать своего сапёра на Java за 15 минут

Нам понадобятся:

  • 15 минут свободного времени;
  • Настроенная рабочая среда, т.е. JDK и IDE (например Eclipse);
  • Библиотека LWJGL (версии 2.x.x) для работы с Open GL. Обратите внимание, что для LWJGL версий выше 3 потребуется написать код, отличающийся от того, что приведён в статье;
  • Иконки для клеток, т.е. цифры, флаг, неверно поставленный флаг, мина, взорвавшаяся мина и закрытое поле. Можно чисто символически нарисовать самому, или скачать использовавшиеся при написании статьи.

Работа с графикой

Для работы с графикой создадим отдельный класс — GUI. От него нам потребуется хранение всех графических элементов управления (т.е. полей клеток), определение элемента, по которому пришёлся клик и передача ему управления, вывод графических элементов на экран и управление основными функциями OpenGL.

Благо класс GUI будет взаимодействовать с графическими элементами, нам нужно создать интерфейс (писать классы сетки, клеток и прочую механику пока рано), который определит, что это такое. Логика подсказывает, что у графического элемента должны быть:

  • Внешний вид (текстура);
  • Координаты;
  • Размеры (ширина и высота);
  • Метод, который по переданным координатам клика определит, попал ли клик по элементу;
  • Метод, который обработает нажатие на элемент.

Таким образом, пишем:

			public interface GUIElement {
	
	int getWidth();
	
	int getHeight();

	int getY();

	int getX();

        Sprite getSprite(); ///Создадим enum с таким именем, заполним позже
	
	int receiveClick(int x, int y, int button); /// Возвращаем результат клика
        ///Параметр button определяет кнопку мыши, которой был сделан щелчок.	

        /// Здесь используется фишка Java 8 --- default методы в интерфейсах.
        /// Если у вас более ранняя версия, вы можете использовать абстрактный класс
        /// вместо интерфейса.
	default boolean isHit(int xclick, int yclick){
		return ( (xclick > getX()) && (xclick < getX()+this.getWidth()) )
                    &&( (yclick > getY()) && (yclick < getY()+this.getHeight()) );
	}
}
		

В GUI должны храниться ячейки поля. Создадим для этих целей двумерный массив:

			///CELLS_COUNT_X и CELLS_COUNT_Y -- константы
//Cell -- класс, который реализует GUIElement; им займёмся немного позже
private static Cell[][] cells;
		

GUI должен передавать клики элементам, которые он содержит. Вычислить адрес клетки, по которой кликнули, нетрудно:

			public static int receiveClick(int x, int y, int button){
		int cell_x = x/CELL_SIZE;
		int cell_y = y/CELL_SIZE;
		
		return cells[cell_x][cell_y].receiveClick(x,y,button);
	}
		

Теперь разберёмся с основными функциями OpenGL. Во-первых, нам нужна инициализация.

initializeOpenGL()
			///Class GUI

private static void initializeOpenGL(){	
		try {
                       //Задаём размер будущего окна
			Display.setDisplayMode(new DisplayMode(SCREEN_WIDTH, SCREEN_HEIGHT));

                       //Задаём имя будущего окна
			Display.setTitle(NAME);

                       //Создаём окно
			Display.create();		
		} catch (LWJGLException e) {
			e.printStackTrace();
		}
		
		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
        glOrtho(0,SCREEN_WIDTH,0,SCREEN_HEIGHT,1,-1);
		glMatrixMode(GL_MODELVIEW);
		
		/*
		 * Для поддержки текстур
		 */
        glEnable(GL_TEXTURE_2D);
		
		/*
		 * Для поддержки прозрачности
		 */
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
		
                	
		/*
		 * Белый фоновый цвет
		 */
		glClearColor(1,1,1,1);
	}
		

На этом мы подробно останавливаться не будем, т.к. изучение LWJGL не входит в наши сегодняшние планы. Во-вторых, нам нужно обновлять изображение на экране:

			///Этот метод будет вызываться извне
public static void update() {
		updateOpenGL();
}

///А этот метод будет использоваться только локально,
/// т.к. базовым другие классы должны работать на более высоком уровне
private static void updateOpenGL() {
                Display.update();
                Display.sync(60);	
}
		

И, наконец, нам нужно это изображение вообще рисовать. Для этого пора закончить enum Sprite. Его элементы будут представлять из себя обёртку для текстуры с удобочитаемыми именами.

			///enum Sprite

///Файлы со всеми этими именами должны лежать по адресу
/// *папка проекта*/res/*имя текстуры*.png
ZERO("0"), ONE("1"), TWO("2"), THREE("3"), FOUR("4"), FIVE("5"), SIX("6"),
SEVEN("7"), EIGHT("8"), HIDEN("space"), BOMB("bomb"), EXPLOSION("explosion"),
FLAG("flag"), BROKEN_FLAG("broken_flag");
	
private Texture texture;

private Sprite(String texturename){
try {
        this.texture = TextureLoader.getTexture("PNG", new FileInputStream(new File("res/"+texturename+".png")));
} catch (IOException e) {
	e.printStackTrace();
}

}

public Texture getTexture(){
     return this.texture;
}
		

Теперь мы можем написать метод для GUI, который будет рисовать элементы:

			///Рисует все клетки
public static void draw(){
                ///Очищает экран от старого изображения
		glClear(GL_COLOR_BUFFER_BIT);
		
		for(GUIElement[] line:cells){
			for(GUIElement cell:line){
				drawElement(cell);
			}
		}
	}

///Рисует элемент, переданный в аргументе
private static void drawElement(GUIElement elem){


		elem.getSprite().getTexture().bind();
		
		glBegin(GL_QUADS);
			glTexCoord2f(0,0);
			glVertex2f(elem.getX(),elem.getY()+elem.getHeight());
			glTexCoord2f(1,0);
			glVertex2f(elem.getX()+elem.getWidth(),elem.getY()+elem.getHeight());
			glTexCoord2f(1,1);
			glVertex2f(elem.getX()+elem.getWidth(), elem.getY());
			glTexCoord2f(0,1);
			glVertex2f(elem.getX(), elem.getY());
		glEnd();
	}
		

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

			public static void init(){
	initializeOpenGL();
		
        ///Классом генератора мы займёмся чуть позже. Пока можно просто
        ///создать его, вместе с пустым методом generate
	this.cells = Generator.generate();
}
		

Ячейки

Создадим класс Cell, реализующий интерфейс GUIElement. В методах getWidth() и getHeight() вернём константу, для координат придётся создать поля, которые будут инициализироваться конструктором. Так же конструктором будем передавать состояние клетки: “-1”, если это мина, “-2”, если это взорванная мина, число мин поблизости в остальных случаях. Для этой цели можно было бы использовать enum, но число мин удобнее передавать как integer, имхо. Итак, конструктор:

			private int x;
	private int y;
	private int state;
	
	public Cell (int x, int y, int state){
		this.x=x;
		this.y=y;
		this.state=state;
	}
		

Ещё два поля — boolean isMarked и boolean isHidden будут отвечать за то, отметили ли клетку флажком, и открыли ли её. По умолчанию оба флага выставлены на false.

Разберёмся с методом getSprite().

			public Sprite getSprite() {
		if(this.isMarked){
			if(!this.isHidden && this.state!=-1){
				///Если эта клетка не скрыта, и на ней
				///ошибочно стоит флажок...
				return Sprite.BROKEN_FLAG;
			}
			///В другом случае --
			return Sprite.FLAG;				
		}else if(this.isHidden){
			///Если клетка не помечена, притом скрыта...
			return Sprite.HIDEN;
		}else{
			///Если не помечена и не скрыта, выводим как есть:
			switch (state){
			case -2:
				return Sprite.EXPLOSION;
			case -1:
				return Sprite.BOMB;
			default:
				assert (state>=0 && state<=8): "Some crap :c";
				///Сделал массив для удобства. Можете, конечно,
				///Писать 9 кейсов -- ваш выбор ;)
				return  skin_by_number[state];
			}
		}
	}
		

В случае, если на кнопку нажали, нам снова необходимо рассмотреть несколько простейших случаев:

			@Override
	public int receiveClick(int x, int y, int button) {
		if(isHidden){
			///Нет смысла обрабатывать клики по уже открытым полям
			if(button==0 && !this.isMarked){
				///Здесь обработаем щелчки левой кнопкой
				///Заметим, что щёлкать левой кнопкой по флагам
				///абсолютно бессмысленно
				
				///Открываем клетку
				this.isHidden = false;
				
				if(this.state==-1){
					///Если это была мина, меняем состояние
					///на взорванную и передаём сигнал назад
					this.state=-2;
					return -1;
				}
				
				if(this.state == 0){
					///Если мы попали в нолик, нужно открыть
					///Все соседние ячейки. Этим займётся GUI :)
					return 1;
				}
				
			}else if(button==1){
				///В любой ситуации, щелчок правой кнопкой
				///либо снимает отметку, либо ставит её
				this.isMarked = ! this.isMarked;
			}
		}
		
		return 0;
	}
		

Чтобы при поражении клетки можно было вскрыть, добавим метод:

			public void show() {
		this.isHidden=false;
	}
		

Для более удобной реализации генератора добавьте ещё и этот метод:

			public void incNearMines() {
		if(state<0){
			//ignore
		}else{		
			state++;
		}
	}
		

Обработка ответов от клеток

Вернёмся к методу GUI.receiveClick(). Теперь мы не можем просто вернуть результат назад, т.к. если результат выполнения — единица, то нам нужно открыть соседние ячейки, а в главный управляющий класс вернуть уже ноль, в знак того, что всё прошло корректно.

			public static int receiveClick(int x, int y, int button){
		int cell_x = x/CELL_SIZE;
		int cell_y = y/CELL_SIZE;
		
		int result =  cells[cell_x][cell_y].receiveClick(x,y,button);
		
		if(result==1){
			///Делаем вид, что тыкнули в клетки
			///Сверху, снизу, справа и слева
            ///Игнорируем выхождение за границы поля
            try{
				receiveClick(x+CELL_SIZE,y,button);
	                }catch(java.lang.ArrayIndexOutOfBoundsException e){
				//ignore
			}
			try{
				receiveClick(x-CELL_SIZE,y,button);
			}catch(java.lang.ArrayIndexOutOfBoundsException e){
				//ignore
			}
			try{
				receiveClick(x,y+CELL_SIZE,button);
			}catch(java.lang.ArrayIndexOutOfBoundsException e){
				//ignore
			}
			try{
				receiveClick(x,y-CELL_SIZE,button);
			}catch(java.lang.ArrayIndexOutOfBoundsException e){
				//ignore
			}
			
			return 0;
		}
		
		return result;
	}
		

Пишем генератор

Задачка эта не сложнее, чем создать массив случайных boolean-величин. Идея следующая — для каждой ячейки матрицы мы генерируем случайное число от 0 до 100. Если это число  меньше 15, то в этом месте записываем в матрицу мину (таким образом, шанс встретить мину — 15%). Записав мину, мы вызываем у всех клеток вокруг метод incNearMines(), а для тех ячеек, где клетка ещё не создана храним значение в специальном массиве.

			public static Cell[][] generate() {
		{
			Random rnd = new Random();
			
			///Карта, которую мы вернём
			Cell[][] map = new Cell[CELLS_COUNT_X][CELLS_COUNT_Y];
			
			///Матрица с пометками, указывается кол-во мин рядом с каждой клеткой
			int[][] counts = new int[CELLS_COUNT_X][CELLS_COUNT_Y];
			
			for(int x=0; x<CELLS_COUNT_X; x++){
				for(int y=0; y<CELLS_COUNT_Y; y++){
					boolean isMine = rnd.nextInt(100)<15;
					
					if(isMine){
						map[x][y] = new Cell(x*CELL_SIZE, y*CELL_SIZE, -1);
						
						for(int i=-1; i<2; i++){
							for(int j=-1; j<2; j++){
								try{
									if(map[x+i][y+j]==null){
										///Если клетки там ещё нет, записываем сведение
										///о мине в матрицу
										counts[x+i][y+j]+=1;
									}else{
										///Если есть, говорим ей о появлении мины
										map[x+i][y+j].incNearMines();
									}
								}catch(java.lang.ArrayIndexOutOfBoundsException e){
									//ignore
								}
							}
						}
					}else{
						///Если была сгенерирована обычная клетка, создаём её, со
						///state равным значению из матрицы
						map[x][y] = new Cell(x*CELL_SIZE, y*CELL_SIZE, counts[x][y]);
					}
				}
			}
			
			return map;
		}
	}
		

Главный управляющий класс и ввод

Создадим класс Main, в нём входной метод — public static void main(String[] args). Этот метод должен будет делать всего две вещи: вызывать инициализацию GUI и циклически вызывать рабочие методы (input(), GUI.draw() и GUI.update()), пока не получит сигнал закрытия.

			private static boolean end_of_game=false;

public static void main(String[] args) {
		GUI.init();
		
		while(!end_of_game){
			input();
			
			GUI.draw();
			
			GUI.update();
		}
	}
		

Здесь нам не хватает метода input(), займёмся им.

			///Если за последний такт произошли какие-то события с мышью,
///перебираем их по очереди
while(Mouse.next()){
                ///Если это было нажатие кнопки мыши, а не
                ///перемещение...
		if(Mouse.getEventButton()>=0 && Mouse.getEventButtonState()){
			int result;
	                ///Отсылаем это на обработку в GUI
			result = GUI.receiveClick(Mouse.getEventX(), Mouse.getEventY(), Mouse.getEventButton());
			
			switch(result){
			case 0:
				//отлично!
				break;
			case -1:
				//не очень :c				
				GUI.gameover();
				break;
			}
		}
	}
	
        ///То же самое с клавиатурой
	while(Keyboard.next()){
		if(Keyboard.getEventKeyState()){
			if(Keyboard.getEventKey()==Keyboard.KEY_ESCAPE){
				isExitRequested = true;
			}
		}
	}

        ///Обрабатываем клик по кнопке "закрыть" окна
        isExitRequested=isExitRequested || Display.isCloseRequested();
		

Метод GUI.gameover() будет просто вызывать метод show() у каждой клетки, показывая таким образом всё поле:

			public static void gameover() {
		for(Cell[] line:cells){
			for(Cell cell:line){
				cell.show();
			}
		}		
	}
		

Запускаем:

Готово!

UPD: исходники выложены на GitHub

Следите за новыми постами
Следите за новыми постами по любимым темам
67К открытий70К показов