
Дізнайтесь більше про нові кар'єрні можливості в EchoUA. Цікаві проекти, ринкова оплата, гарний колектив. Надсилайте резюме та приєднуйтеся до нас.
Нам знадобляться:
- 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. По-перше, нам потрібна ініціалізація.
///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
Київ, Харків, Одеса, Дніпро, Запоріжжя, Кривий Ріг, Вінниця, Херсон, Черкаси, Житомир, Хмельницький, Чернівці, Рівне, Івано-Франківськ, Кременчук, Тернопіль, Луцьк, Ужгород, Кам'янець-Подільський, Стрий - за статистикою саме з цих міст програмісти найбільше переїжджають працювати до Львова. А Ви розглядаєте relocate?