Нам понадобятся: 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()) ); } } Код 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; Код ///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); } Код 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); } Код ///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); } Нам нужно обновлять изображение на экране: ///Этот метод будет вызываться извне public static void update() { updateOpenGL(); } ///А этот метод будет использоваться только локально, /// т.к. базовым другие классы должны работать на более высоком уровне private static void updateOpenGL() { Display.update(); Display.sync(60); } Код ///Этот метод будет вызываться извне 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; } Код ///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 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(); } Код 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; } Код 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]; } } } Код 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; } Код @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 show() { this.isHidden=false; } Для более удобной реализации генератора добавьте ещё и этот метод: public void incNearMines() { if(state<0){ //ignore }else{ state++; } } Код 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; } Код 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; } } Код 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(); } } Код 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(); Код ///Если за последний такт произошли какие-то события с мышью, ///перебираем их по очереди 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(); } } } Код public static void gameover() { for(Cell[] line:cells){ for(Cell cell:line){ cell.show(); } } } Готово!