Загрузка...

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

Тема в разделе Программирование создана пользователем Hangman666 26 июн 2016. 830 просмотров

  1. Hangman666
    Hangman666 Автор темы 26 июн 2016 Хранитель Идей 342 10 мар 2016
    [IMG]
    Нам понадобятся:
    • 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);
    }

    Нам нужно обновлять изображение на экране:
    Код
    ///Этот метод будет вызываться извне
    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();
    }
    }
    }

    Готово!
     
    26 июн 2016 Изменено
Загрузка...
Top