Настроенная рабочая среда, т.е. 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()), пока не получит сигнал закрытия.
///Если за последний такт произошли какие-то события с мышью,
///перебираем их по очереди
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() у каждой клетки, показывая таким образом всё поле:
Назвал практические советы, как начинающему IT-специлисты стать мидлом, как развить свои скиллы, претендовать на более высокую зарплату и спровоцировать повышение.