Как написать игру на java intellij idea

Время на прочтение
8 мин

Количество просмотров 528K

В любом деле самое сложное — это начало. Часто бывает тяжело войти в контекст, с чем столкнулся и я, решив разработать свое первое Android-приложение. Настоящая статья для тех, кто хочет начать, но не знает с чего.

Статья затронет весь цикл разработки приложения. Вместе мы напишем простенькую игру “Крестики-Нолики” с одним экраном (в ОС Android это называется Activity).

Отсутствие опыта разработки на языке Java не должно стать препятствием в освоении Android. Так, в примерах не будут использоваться специфичные для Java конструкции (или они будет минимизированы на столько, на сколько это возможно). Если Вы пишете, например, на PHP и знакомы с основополагающими принципами в разработке ПО, эта статья будет вам наиболее полезна. В свою очередь так как, я не являюсь экспертом по разработке на Java, можно предположить, что исходный код не претендует на лейбл “лучшие практики разработки на Java”.

Установка необходимых программ и утилит

Перечислю необходимые инструменты. Их 3:

  1. JDK — набор для разработки на языке Java;
  2. Android SDK and AVD Manager — набор утилит для разработки + эмулятор;
  3. IDE c поддержкой разработки для Android:
    • Eclipse + ADT plugin;
    • IntelliJ IDEA Community Edition;
    • Netbeans + nbandroid plugin;

Утилиты устанавливаются в определенном выше порядке. Ставить все перечисленные IDE смысла нет (разве только если Вы испытываете затруднения с выбором подходящей). Я использую IntelliJ IDEA Community Edition, одну из самых развитых на данный момент IDE для Java.

Запуск виртуального устройства

Запустив AVD Manager и установив дополнительные пакеты (SDK различных версий), можно приступить к созданию виртуального устройства с необходимыми параметрами. Разобраться в интерфейсе не должно составить труда.

Список устройств

Создание проекта

Мне всегда не терпится приступить к работе, минимизируя подготовительные мероприятия, к которым относится создание проекта в IDE, особенно, когда проект учебный и на продакшн не претендует.

Итак, File->New Project:

По нажатию кнопки F6 проект соберется, откомпилируется и запустится на виртуальном девайсе.

Структура проекта

На предыдущем скриншоте видна структура проекта. Так как в этой статье мы преследуем сугубо практические цели, заострим внимание лишь на тех папках, которые будем использовать в процессе работы. Это следующие каталоги: gen, res и src.

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

Папка res предназначена для хранения ресурсов, таких как картинки, тексты (в том числе переводы), значения по-умолчанию, макеты (layouts).

src — это папка в которой будет происходить основная часть работы, ибо тут хранятся файлы с исходными текстами нашей программы.

Первые строки

Как только создается Activity (экран приложения), вызывается метод onCreate(). IDE заполнила его 2 строчками:

super.onCreate(savedInstanceState);
setContentView(R.layout.main);

Метод setContentView (равносильно this.setContentView) устанавливает xml-макет для текущего экрана. Далее xml-макеты будем называть «layout», а экраны — «Activity». Layout в приложении будет следующий:

<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:id="@+id/main_l"
    android:gravity="center"
    >
</TableLayout>

Для этого приложения идеально подойдет TableLayout. Id можно присвоить любому ресурсу. В данном случае, TableLayout присвоен id = main_l. При помощи метода findViewById() можно получить доступ к виду:


    private TableLayout layout; // это свойство класса KrestikinolikiActivity

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        layout = (TableLayout) findViewById(R.id.main_l);
        buildGameField();
    }

Теперь необходимо реализовать метод buildGameField(). Для этого требуется сгенерировать поле в виде матрицы. Этим будет заниматься класс Game. Сначала нужно создать класс Square для ячеек и класс Player, объекты которого будут заполнять эти ячейки.

Square.java

package com.example;

public class Square {
    private Player player = null;

    public void fill(Player player) {
        this.player = player;
    }

    public boolean isFilled() {
        if (player != null) {
            return true;
        }
        return false;
    }

    public Player getPlayer() {
        return player;
    }
}

Player.java

package com.example;


public class Player {
    private String name;

    public Player(String name) {
        this.name = name;
    }

    public CharSequence getName() {
        return (CharSequence) name;
    }
}

Все классы нашего приложения находятся в папке src.

Game.java

package com.example;

public class Game {
 /**
     * поле
     */
    private Square[][] field;
 
 /**
     * Конструктор
     *
     */
    public Game() {
        field = new Square[3][3];
        squareCount = 0;
        // заполнение поля
        for (int i = 0, l = field.length; i < l; i++) {
            for (int j = 0, l2 = field[i].length; j < l2; j++) {
                field[i][j] = new Square();
                squareCount++;
            }
        }
    }
 
 public Square[][] getField() {
        return field;
    }
}

Инициализация Game в конструкторе KrestikinolikiActivity.

public KrestikinolikiActivity() {
    game = new Game();
 game.start(); // будет реализован позже
}

Метод buildGameField() класса KrestikinolikiActivity. Он динамически добавляет строки и колонки в таблицу (игровое поле):

private Button[][] buttons = new Button[3][3];
 //(....)
    private void buildGameField() {
        Square[][] field = game.getField();
        for (int i = 0, lenI = field.length; i < lenI; i++ ) {
            TableRow row = new TableRow(this); // создание строки таблицы
            for (int j = 0, lenJ = field[i].length; j < lenJ; j++) {
                Button button = new Button(this);
                buttons[i][j] = button;
                button.setOnClickListener(new Listener(i, j)); // установка слушателя, реагирующего на клик по кнопке
                row.addView(button, new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT,
                        TableRow.LayoutParams.WRAP_CONTENT)); // добавление кнопки в строку таблицы
                button.setWidth(107);
                button.setHeight(107);
            }
            layout.addView(row, new TableLayout.LayoutParams(TableLayout.LayoutParams.WRAP_CONTENT,
                    TableLayout.LayoutParams.WRAP_CONTENT)); // добавление строки в таблицу
        }
    }

В строке 8 создается объект, реализующий интерфейс View.OnClickListener. Создадим вложенный класс Listener. Он будет виден только из KrestikinolikiActivity.

public class Listener implements View.OnClickListener {
        private int x = 0;
        private int y = 0;

        public Listener(int x, int y) {
            this.x = x;
            this.y = y;
        }

        public void onClick(View view) {
            Button button = (Button) view;
        }
    }

Осталось реализовать логику игры.

public class Game {
    /**
     * игроки
     */
    private Player[] players;
    /**
     * поле
     */
    private Square[][] field;
    /**
     * начата ли игра?
     */
    private boolean started;
    /**
     * текущий игрок
     */
    private Player activePlayer;
    /**
     * Считает колличество заполненных ячеек
     */
    private int filled;
    /**
     * Всего ячеек
     */
    private int squareCount;

    /**
     * Конструктор
     *
     */
    public Game() {
        field = new Square[3][3];
        squareCount = 0;
        // заполнение поля
        for (int i = 0, l = field.length; i < l; i++) {
            for (int j = 0, l2 = field[i].length; j < l2; j++) {
                field[i][j] = new Square();
                squareCount++;
            }
        }
        players = new Player[2];
        started = false;
        activePlayer = null;
        filled = 0;
    }

    public void start() {
        resetPlayers();
        started = true;
    }

    private void resetPlayers() {
        players[0] = new Player("X");
        players[1] = new Player("O");
        setCurrentActivePlayer(players[0]);
    }

    public Square[][] getField() {
        return field;
    }

    private void setCurrentActivePlayer(Player player) {
        activePlayer = player;
    }

    public boolean makeTurn(int x, int y) {
        if (field[x][y].isFilled()) {
            return false;
        }
        field[x][y].fill(getCurrentActivePlayer());
        filled++;
        switchPlayers();
        return true;
    }

    private void switchPlayers() {
        activePlayer = (activePlayer == players[0]) ? players[1] : players[0];
    }

    public Player getCurrentActivePlayer() {
        return activePlayer;
    }

    public boolean isFieldFilled() {
        return squareCount == filled;
    }

    public void reset() {
        resetField();
        resetPlayers();
    }

    private void resetField() {
        for (int i = 0, l = field.length; i < l; i++) {
            for (int j = 0, l2 = field[i].length; j < l2; j++) {
                field[i][j].fill(null);
            }
        }
        filled = 0;
    }
}

Определение победителя

К. О. подсказывает, что в крестики-нолики выирывает тот, кто выстроет X или O в линию длиной, равной длине поля по-вертикали, или по-горизонтали, или по-диагонали. Первая мысль, которая приходит в голову — это написать методы для каждого случая. Думаю, в этом случае хорошо подойдет паттерн Chain of Responsobility. Определим интерфейс

package com.example;

public interface WinnerCheckerInterface {
    public Player checkWinner();
}

Так как Game наделен обязанностью выявлять победителя, он реализует этот интерфейс. Настало время создать виртуальных «лайнсменов», каждый из которых будет проверять свою сторону. Все они реализует интерфейс WinnerCheckerInterface.

WinnerCheckerHorizontal.java

package com.example;

public class WinnerCheckerHorizontal implements WinnerCheckerInterface {
    private Game game;

    public WinnerCheckerHorizontal(Game game) {
        this.game = game;
    }

    public Player checkWinner() {
        Square[][] field = game.getField();
        Player currPlayer;
        Player lastPlayer = null;
        for (int i = 0, len = field.length; i < len; i++) {
            lastPlayer = null;
            int successCounter = 1;
            for (int j = 0, len2 = field[i].length; j < len2; j++) {
                currPlayer = field[i][j].getPlayer();
                if (currPlayer == lastPlayer && (currPlayer != null && lastPlayer !=null)) {
                    successCounter++;
                    if (successCounter == len2) {
                        return currPlayer;
                    }
                }
                lastPlayer = currPlayer;
            }
        }
        return null;
    }
}

WinnerCheckerVertical.java

package com.example;

public class WinnerCheckerVertical implements WinnerCheckerInterface {
    private Game game;

    public WinnerCheckerVertical (Game game) {
        this.game = game;
    }
    public Player checkWinner() {
        Square[][] field = game.getField();
        Player currPlayer;
        Player lastPlayer = null;
        for (int i = 0, len = field.length; i < len; i++) {
            lastPlayer = null;
            int successCounter = 1;
            for (int j = 0, len2 = field[i].length; j < len2; j++) {
                currPlayer = field[j][i].getPlayer();
                if (currPlayer == lastPlayer && (currPlayer != null && lastPlayer !=null)) {
                    successCounter++;
                    if (successCounter == len2) {
                        return currPlayer;
                    }
                }
                lastPlayer = currPlayer;
            }
        }
        return null;
    }
}

WinnerCheckerDiagonalLeft.java

package com.example;

public class WinnerCheckerDiagonalLeft implements WinnerCheckerInterface {
    private Game game;

    public WinnerCheckerDiagonalLeft(Game game) {
        this.game = game;
    }

    public Player checkWinner() {
        Square[][] field = game.getField();
        Player currPlayer;
        Player lastPlayer = null;
        int successCounter = 1;
        for (int i = 0, len = field.length; i < len; i++) {
            currPlayer = field[i][i].getPlayer();
            if (currPlayer != null) {
                if (lastPlayer == currPlayer) {
                    successCounter++;
                    if (successCounter == len) {
                        return currPlayer;
                    }
                }
            }
            lastPlayer = currPlayer;
        }
        return null;
    }
}

WinnerCheckerDiagonalRight.java

package com.example;

public class WinnerCheckerDiagonalRight implements WinnerCheckerInterface {
    private Game game;

    public WinnerCheckerDiagonalRight(Game game) {
        this.game = game;
    }

    public Player checkWinner() {
        Square[][] field = game.getField();
        Player currPlayer;
        Player lastPlayer = null;
        int successCounter = 1;
        for (int i = 0, len = field.length; i < len; i++) {
            currPlayer = field[i][len - (i + 1)].getPlayer();
            if (currPlayer != null) {
                if (lastPlayer == currPlayer) {
                    successCounter++;
                    if (successCounter == len) {
                        return currPlayer;
                    }
                }
            }
            lastPlayer = currPlayer;
        }
        return null;
    }
}

Проинициализируем их в конструкторе Game:

//(....)
 /**
     * "Судьи" =). После каждого хода они будут проверять,
     * нет ли победителя
     */
    private WinnerCheckerInterface[] winnerCheckers;
 //(....)
    public Game() {
        //(....)
        winnerCheckers = new WinnerCheckerInterface[4];
        winnerCheckers[0] = new WinnerCheckerHorizontal(this);
        winnerCheckers[1] = new WinnerCheckerVertical(this);
        winnerCheckers[2] = new WinnerCheckerDiagonalLeft(this);
        winnerCheckers[3] = new WinnerCheckerDiagonalRight(this);
        //(....)
    }

Реализация checkWinner():

public Player checkWinner() {
        for (WinnerCheckerInterface winChecker : winnerCheckers) {
            Player winner = winChecker.checkWinner();
            if (winner != null) {
                return winner;
            }
        }
        return null;
    }

Победителя проверяем после каждого хода. Добавим кода в метод onClick() класса Listener

public void onClick(View view) {
            Button button = (Button) view;
            Game g = game;
            Player player = g.getCurrentActivePlayer();
            if (makeTurn(x, y)) {
                button.setText(player.getName());
            }
            Player winner = g.checkWinner();
            if (winner != null) {
                gameOver(winner);
            }
            if (g.isFieldFilled()) {  // в случае, если поле заполнено
                gameOver();
            }
        }

Метод gameOver() реализован в 2-х вариантах:

private void gameOver(Player player) {
        CharSequence text = "Player "" + player.getName() + "" won!";
        Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
        game.reset();
        refresh();
    }

    private void gameOver() {
        CharSequence text = "Draw";
        Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
        game.reset();
        refresh();
    }

Для Java, gameOver(Player player) и gameOver() — разные методы. Воспользовавшись Builder’ом Toast.makeText, можно быстро создать и показать уведомление. refresh() обновляет состояние поля:

private void refresh() {
        Square[][] field = game.getField();

        for (int i = 0, len = field.length; i < len; i++) {
            for (int j = 0, len2 = field[i].length; j < len2; j++) {
                if (field[i][j].getPlayer() == null) {
                    buttons[i][j].setText("");
                } else {
                    buttons[i][j].setText(field[i][j].getPlayer().getName());
                }
            }
        }
    }

Готово! Надеюсь, эта статья помогла Вам освоиться в мире разработки под OS Android. Благодарю за внимание!

Видео готового приложения

Исходники .

PS: статья была опубликована по просьбе комментаторов этого поста.

Немного о Java

Java — это не только язык, это целая экосистема, включающая в себя средства разработки, платформу для запуска готовых приложений, огромный свод документации и активное сообщество. Одним из преимуществ Java на начальном этапе была кросс-платформенность (принцип — «написано один раз — запускается везде»).

Дело в том, что программа на Java исполняется не на прямую процессором компьютера, а виртуальной машиной Java (JVM). Это позволяет абстрагироваться от многих нюансов конкретных платформ. Программу, написанную на Java, можно без изменений кода запускать на Windows, Linux, MacOS и других операционных системах (если, конечно, программа не использует специфичные для ОС функции).

Кто застал начало 2000х, наверное помнит огромное количество мобильных телефонов (тогда еще они не были смартфонами), на каждом телефоне была по сути своя маленькая ОС, но при этом почти на каждом можно было запустить Java игру или приложение.

На сегодняшний день Java по-прежнему входит в топ языков для изучения, а Java как платформа — в топ используемых технологий в мире IT и смежных областях.

Создание проекта, первые шаги

Сегодня мы начнем изучать Java, причем сразу с примера игры «Крестики-нолики».

Итак, поехали. Надеюсь как установить java SDK ты уже разобрался. Мы будем писать код в IDE IntelliJ IDEA, но если у вас какая-то другая, например Eclipse, то разницы большой не будет.

Итак, создаем новый проект: нажимаем «create new project», выбираем java и щелкаем «next» до окна, где требуется ввести имя проекта, вводим TicTacToe (крестики-нолики). В некоторых случаях на этапе создания потребуется выбрать шаблон проекта, тогда смело выбирай что-либо похожее на JavaConsoleApplication.

После этого нажимаем «Finish». Idea немного подумает и сгенерирует нам проект с классом Main, в котором определена функция main().

Давайте разберемся, что здесь что. Слева открыто окно структуры проекта «Project», как мы видим в папке src в пакете com.company находится единственный java-файл нашей программы с именем Main. Справа показано его содержимое. Тут надо сделать небольшое отступление, дело в том, что в Java почти все представлено классами. В том числе и файлы программы описывают классы, причем имя файла должно совпадать с классом, который описывается в этом файле (например, у нас файл Main.java описывает класс Main). Пусть слово «класс» не смущает на первом этапе. Пока лишь отмечу, что для глубокого изучения Java так или иначе придется познакомиться с объектно-ориентированным подходом. В двух словах, класс можно воспринимать как шаблон, идею, а экземпляры класса — как реализацию этой идеи. Экземпляры класса называются его объектами. Например, вы можете представить идею стола (нечто, на что можно класть предметы), однако конкретных экземпляров такого стола огромное множество (на одной ножке, на четырех, круглые, квадратные, из разных материалов). Примерно так соотносятся классы и объекты в объектно-ориентированном программировании.

Внутри нашего класса Main описана функция main(), в Java с этой функции начинается исполнение программы, это точка входа в наше приложение. Сейчас там написан только автоматический комментарий (комментарии в Java начинаются с двух символов //). Попробуем кое-что добавить в наш код и проверить работоспособность приложения. Внутри функции main() допишем две строки:

Встроенная функция println() просто выводит на экран текстовую информацию. Запустим наше приложение (нажимаем shift-F10 или зеленый треугольник). Внизу, во вкладке run появится вывод нашей программы:

Функция main() отработала и закончилась, вместе с ней закончилась наша программа.

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

Смысл большинства строк понятен из комментариев к ним, отдельно отмечу строку window.setLayout() — здесь устанавливается менеджер расположения, который будет применяется к компонентам, добавляемым в наше окно. Менеджер BorderLayout может располагать новые компоненты относительно сторон света (North(верх), West(слева), East(справа), South(низ)), Center (центр)). По умолчанию он располагает компоненты по центру. Подробнее с менеджерами расположения можно познакомиться в документации.

Теперь, если запустить нашу программу, мы увидим окно:

Пока в этом окне ничего нет. Создадим свой компонент, который и будет отрисовывать графику игры.

Свой компонент для рисования

Очевидно, что рисовать в консоли у нас не получится, нужен какой-то компонент для более продвинутого взаимодействия с пользователем. Для этой цели создадим еще один класс, назовем его TicTacToe. Щелкаем правой клавишей мыши на имени пакета приложения (в данном случае это com.company)

И в появившемся меню выбираем пункт «New» → «Java Class». В окне создания класса набираем его имя «TicTacToe» и нажимаем «Enter».

У нас в проекте появился еще один класс. В главное окно можно добавлять только объекты класса JComponent, кроме того, нам нужна область для рисования. Поэтому наследуем наш класс TicTacToe от JComponent. Ой сколько непонятных слов! Сейчас постараюсь пояснить.

Наследование классов — это как создание уточненного шаблона на базе существующего. Например, есть класс Стол, описывающий идею стола вообще. Но нам очень часто приходится создавать столы на четырех ногах с деревянной столешницей, поэтому для удобства мы можем уточнить идею класса Стол и создать шаблон ДеревянныйСтол — он будет иметь все те же основные свойства, что и родительская идея, но зато часть свойств у него уже определены и понятны — это число ног, равное четырем и материал столешницы — дерево.

С JComponent то же самое — данный класс реализует идею некоторого графического компонента пользовательского интерфейса. Такой компонент можно добавить в окно и он умеет как-то себя отрисовывать. Например, класс JButton — наследник класса JComponent, это компонент, который выглядит, как кнопка и умеет показывать анимацию клика мышкой.

Здесь же, наследуя класс JComponent, мы создадим свой компонент, в котором сможем рисовать то, что нам нужно.

Итак дописываем extends JComponent в строку описания класса:

Слово extends говорит о том, что наш класс TicTacToe расширяет (наследует) класс JComponent.

У всех компонентов есть метод paintComponent(), который отвечает за их отрисовку. В параметры этого метода приходит объект Graphics, с помощью которого мы и будем рисовать то, что нам необходимо. Давайте переопределим метод paintComponent так, чтобы он рисовал окружность (это необязательно, но для проверки, что у нас все работает как надо, будет хорошим тоном это сделать).

Переопределим метод paintComponent() в классе TicTacToe следующим образом:

метод setColor() объекта graphics, как очевидно из названия, устанавливает цвет, которым мы будем рисовать, а метод drawOval(x, y, w, h) — в общем случае рисует овал с координатами центра x, y, шириной — w и высотой h. В данном случае рисуется окружность, так как ширина и высота заданы одинаковые — 100. Замечу, что экранные координаты отсчитываются от левого верхнего угла. То есть 0 по вертикали находится вверху.

Чтобы проверить, как выглядит наш объект класса TicTacToe надо создать его экземпляр и добавить в главное окно в качестве дочернего компонента. Создание новых объектов в Java осуществляется с помощью ключевого слова new.

Например, если у нас есть класс Стол и мы хотим создать объект этого класса (настоящий конкретный стол), то мы должны написать что-то такое: стол = new Стол(). Здесь «стол» имя, по которому мы будем обращаться к нашему объекту (взаимодействовать с ним), а Стол — имя класса, объект которого мы создаем.

Замечу сразу, что вместо «стол» мы могли написать любое имя, например «fdgdgdgfd», но программисты обычно стараются давать «говорящие» имена объектам, чтобы код было легче читать. Чтобы создать экземпляр класса TicTacToe мы можем также написать game = new TicTacToe(), а потом добавить его в окно методом add().

Теперь код класса Main выглядит вот так:

Если теперь запустить нашу программу, то мы увидим окно с окружностью:

Ну что ж. Рисовать в базе мы научились. Время приступать к созданию игры.

Создание игрового поля

Вернемся к классу TicTacToe. Для начала необходимо нарисовать игровое поле, состоящее из девяти клеточек. Для этого давайте нарисуем две горизонтальные и две вертикальные линии на нашем поле. Чтобы это сделать, воспользуемся методом drawLine(x1,y1,x2,y2) объекта Graphics, который приходит к нам в метод paintComponent() в качестве параметра. Метод drawLine() рисует линию от точки с координатами x1,y1 до точки x2,y2. Давайте подумаем как нарисовать игровое поле.

Если мы разобьем высоту поля на три (у нас же три клетки в ряду), то получим высоту одной клетки (назовем ее dh). Узнать высоту всего компонента можно методом getHeight(). Значит, мы должны нарисовать первую горизонтальную линию от точки 0,dh до точки w, dh, где w — ширина поля. Но это только одна горизонтальная линия, вторую рисуем также, но координаты будут уже: начало — 0, 2*dh, конец w, 2*dh. По аналогии, если высота поля равна h, а ширина одной клетки равна dw, то вертикальные линии рисуются в координатах d, 0 — d, h и dw*2, 0 — dw*2, h.

Теперь давайте немного поговорим о переменных. Если помните — в алгебре за буквой могло скрываться какое-то значение, например выражение x = 2*a, подразумевало, что на место буквы а можно подставить любое значение и вычислить x.

Примерно то же самое происходит с переменными в программировании. Имя переменной (идентификатор) сопоставлен с некоторым значением и «хранит» его «в себе» (на самом деле, с объектами классов все несколько сложнее, там мы имеем дело со ссылками, но это пока за рамками данного материала).

Помимо этого, в программах есть разные типы данных. Наверное, вы согласитесь, что строку и целое число надо хранить в памяти как-то по-разному? Даже целые и дробные числа требуют разного подхода, поэтому в программах данные соответствуют определенным типам. В нашем примере нам уже понадобились значения ширины и высоты ячейки игрового поля dw и dh. Чтобы вычислить и сохранить их значения в памяти, воспользуемся следующими выражениями:

Здесь int — означает тип данных «целое число». Выражение int a = 10 объявляет переменную с именем a и задает ей сразу значение 10. В нашем примере создаются четыре переменных, значения w и h получаются из методов самого компонента TicTacToe, а dw и dh вычисляются. Обратите внимание, что при делении w / 3 получается целый тип данных. В Java, как и во многих других языках, деление целого на целое дает в результате целое. При этом дробная часть просто отбрасывается (округления нет). Заметьте, что здесь не используется слово «new», так как создаются не объекты, а переменные простых (скалярных) типов данных, в данном случае типа int.

Мы могли бы уже написать код для рисования всех линий, но мы же программисты, а программисты любят все упрощать, правда для этого они пишут много дополнительного кода. Представим, что у нас было бы поле не 3 на 3 клетки а, например, 15х15. Как бы мы его разлиновали? Вручную набирать код для рисования 28 линий это уж слишком.

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

Разберем, как автоматизировать рисование линий, заметим, что все горизонтальные линии содержат одни и те же значения координат по горизонтали (от начала до конца ширины игрового поля), при этом их координаты по вертикали различаются на dh. У первой линии высота dh, у второй 2*dh, и так далее.

Для вертикальных линий рассуждения аналогичны, только в рассуждении приведенном выше надо поменять вертикальные и горизонтальные координаты местами.

Попробуем рисовать линии с помощью цикла, в классе TicTacToe создадим свой метод с названием drawGrid(), он будет у нас отвечать за рисование линий сетки игрового поля:

Еще раз пробежимся по коду. Первые четыре строки метода — необходимые нам значения ширины, высоты игрового поля и ширины, высоты одной ячейки. Цикл начинается с ключевого слова for, в скобках после него указывается переменная, которая будет счетчиком (у нас она еще и объявляется сразу int i = 1), условие при ложности которого цикл прервется и выражение изменяющее переменную-счетчик (i++ увеличивает i каждую итерацию цикла на единицу).

Внутри цикла каждую итерацию рисуются очередные горизонтальная и вертикальная линии поля.

Добавим вызов нашего метода drawGrid() в метод отрисовки всего компонента paintComponent():

Запускаем программу и видим разрисованное поле:

Скажи мне, куда ты кликнул?

Итак, наше игровое поле выглядит готовым к игре, но теперь нам надо узнать в какой из квадратов кликнул пользователь. Для этого давайте немного настроим наш компонент TicTacToe, чтобы он смог принимать события от мыши. Во-первых, нам необходимо включить получение таких событий. Делается это с помощью метода enableEvents(), но где его вызвать?

Конечно можно было бы добавить его вызов в наш метод drawGrid() или даже в paintComponent(), но эти методы по логике работы игры будут вызываться каждый раз, когда мы захотим что-то нарисовать. А включить события надо лишь один раз. Где бы найти метод, который вызывается у компонента единожды, например при его создании?

На самом деле такой метод есть у каждого класса и называется он конструктором. Именно конструктор вызывается при попытке создания нового объекта. Конструкторов может быть несколько, он так же как обычный метод может принимать параметры, но вот возвращаемого значения у него нет. Конструктор класса имеет то же имя, что и класс. Создадим конструктор в классе TicTacToe:

Как видим — ничего сложного, просто еще один метод. А как же наш компонент создавался до этого? Ведь в классе Main мы его уже создавали. Помните, game = new TicTacToe()? Тут тоже никакой магии — если конструктор не задан явно, используется конструктор по умолчанию.

Именно здесь мы включим получение событий от мыши:

Хорошо! Получение событий мы включили, а где же мы их будем получать? В методе processMouseEvent() конечно, именно он будет срабатывать каждый раз, когда указатель мыши каким-либо образом взаимодействует с нашим игровым полем.

Приведу на всякий случай полный код класса TicTacToe на текущий момент:

Мозг игры

Ну не то чтобы уж мозг, но некоторую начинку нам создать придется. Итак, давайте подумаем, как хранить состояние игры? Неплохо бы было хранить состояние каждой клетки игрового поля, для этой цели хорошо подойдет двумерный массив целых чисел размером 3х3. Создается он просто int[][] field = new int[3][3].

Массив это уже целый объект, на который выделяется память в отдельной области (куче), поэтому тут мы используем слово new. Создадим в классе TicTacToe новый метод под названием initGame(), он будет отвечать у нас за сброс игры к начальному состоянию, именно здесь мы будем «очищать» игровое поле.

Для хранения состояния ячейки поля создадим три константы со значениями 0, 10 и 200. Ноль будет соответствовать пустой ячейке, 10 — крестику, а 200 — нолику. Первоначально заполним массив нулями.

Теперь добавим немного кода в метод processMouseEvent(), который отвечает за клики мышью на игровом поле. Также заведем логическую переменную isXturn, которая будет показывать чей сейчас ход.

Пробежимся по коду. Переход от координат к индексам довольно прост: мы делим текущую координату на размер одной ячейки и получаем сколько целых ячеек укладывается до текущей (если совсем непонятно, то поясню: разделить на w/3 это то же самое, что умножить на 3/w).

В 42й строке кода стоит условный оператор (также называемый ветвлением), пора с ним познакомиться. Если условие в скобках истинно (в нашем случае если поле пустое), то мы заходим «внутрь» условного оператора (строки 43-46), если же условие ложно (ячейка уже занята), то мы пройдем дальше. Что же происходит если кликнутая ячейка пуста?

В 44й строке после «=» стоит еще один интересный оператор — тернарный, он дает возможность записать в строку ветвление, если в результате него присваивается значение. Записывают его так: isXturn? — это проверка, чей сейчас ход (ходит крестик, если значение «истина»), далее следует определенная нами константа FIELD_X, именно она будет результатом выражения, если isXturn — true.

После FIELD_X стоит двоеточии и константа FIELD_O — ее значение станет результатом выражения, если ход «нолика». После изменения значения в ячейке массива, меняем очередность хода: isXturn =! isXturn изменит значение переменной на противоположное. В конце всех действий — вызываем перерисовку компонента, так как теперь нужно нарисовать крестик или нолик, там где его не было раньше.

Теперь осталось научиться рисовать крестики или нолики. Создадим два метода: drawX() и draw0(). На данном этапе мы уже умеем рисовать линии и круги, поэтому обойдусь комментариями в коде:

Комментарии в коде достаточно очевидны. Коротко поясню, что крестик мы рисуем как пересечение двух линий из угла в угол ячейки, а нолик — как овал чуть вытянутый по вертикали. Теперь у нас есть методы, рисующие крестик и нолик по заданным индексам ячейки поля. Как же мы будем рисовать процесс игры? Пробежимся еще раз по коду. Игроки кликают мышкой на наш компонент, при этом срабатывает метод processMouseEvent(), в котором мы определяем, какое событие произошло, пуста ли ячейка, в которую кликнули и вызываем перерисовку компонента (repaint()). На момент перерисовки в массиве field содержатся актуальные данные о поставленных крестиках и ноликах, остается пробежаться циклами по всему массиву и если встречается нолик — рисовать нолик, а если крестик — рисовать крестик. Поехали, создаем метод drawXO(). Именно в нем будем «просматривать» массив:

Осталось вызвать данный метод в методе painComponent():

Теперь, если запустить нашу программу можно понаслаждаться постановкой крестиков и ноликов:

Определяем победителя

Все бы хорошо, но сейчас игра никак не отслеживает свое состояние, то есть крестики и нолики успешно ставятся, но выигрыш никак не определяется. Придется еще немного потрудиться! Как же нам определить, что игра закончилась? Давайте присмотримся к массиву field. Если в ряду будут стоять одни крестики, значит, там значения 10, 10, 10.

Если нолики — 200, 200, 200. Эврика! Давайте проверять сумму всех ячеек в ряду по горизонтали и вертикали, а также по двум диагоналям. Создаем еще один метод checkState(), он будет каждый ход проверять сложившуюся на поле ситуацию и возвращать -1, если ходов не осталось, 3*FIELD_X если выиграли крестики и 3*FIELD_O, если выиграли нолики, в случае продолжения игры — метод пусть вернет 0.

Основные комментарии даны в коде. Элементы, стоящие на главной диагонали вычисляются просто — их индексы равны ([0][0], [1][1] и т. д.). Побочная диагональ содержит элементы с индексами [0][N-1], [1][N-2] и так далее (N — длина массива).

Часть кода с 142й по 160ю строку отвечает за подсчет суммы значений в ячейках по вертикальным и горизонтальным рядам: каждую «большую» итерацию по i фиксируется вертикальный (горизонтальный) ряд с индексом i и запускается малый цикл с перебором всех ячеек в данном ряду (цикл по j).

Кроме того, проверяется наличие на поле хотя бы одной не занятой ячейки (hasEmpty=true), это нужно чтобы определить ситуацию, когда все ячейки заняты, но никто не выиграл (ничья). Наконец, если нигде ранее не произошел выход из метода мы проверяем значение hasEmpty, если пустые ячейки есть — возвращаем 0, а если нет, то -1 (ничья).

Осталось использовать данный метод. Немного подправим обработку нажатий.

Здесь добавилось получение результата из метода checkState() и его обработка. Метод showMessageDialog() показывает сообщение с заданным текстом и заголовком. В ветвлении проверяем, какое значение вернул метод checkState() и показываем соответствующие сообщения (или продолжаем игру, если результат 0).

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

Задание на дом: наша игра все-таки ленивая, и почему-то не зачеркивает выигрышный ряд. Подумайте, как можно его зачеркнуть, ведь рисовать линии вы уже умеете (подсказка, обратите внимание на метод checkState() — там уже сделана почти вся работа по нахождению выигрышного ряда).

С помощью нашего шестимесячного курса «Профессия: Разработчик» вы научитесь писать в Java не только это! 👉 Узнать подробности!

Всем доброго времени. На связи Алексей Гулынин. На досуге решил немного изучить Java. В результате решил написать игру «Крестики-нолики» в процедурном стиле, чтобы изучить базовые возможности языки, такие как типы переменных, циклы, операторы, методы, условный оператор, работа с буфером и т.д. В качестве IDE решил использовать «IntelliJ IDEA». Далее последует много кода, каждый метод будет кратко прокомментирован.

Скачать уже готовый проект полностью можно по ссылке.

Объявим переменные, которые будем использовать в наших методах. «field» — это наше игровое поле, которое является двумерным массивом типа String (будет инициализироваться «*»). «DIMENSION» — это размерность поля. «aiLevel» — это некое подобие искусственного интеллекта (0 в данном случае означает, что никакого интеллекта нет, компьютер будет случайно делать ход). Далее мы реализуем логику интеллекта.

public static final String USER_SIGN = "X";
public static final String USER_SIGN_SECOND = "O";
public static final String AI_SIGN = "O";
public static final String NOT_SIGN = "*";
public static int aiLevel = 0;
public static final int DIMENSION = 3;
public static String[][] field = new String[DIMENSION][DIMENSION];

В методе main(), который является точкой входа в программу, вызывается «mainMenu()», в котором будет определен режим игры

public static void main(String[] args) {
    mainMenu();
}

Далее мы реализуем метод «mainMenu()», в котором научимся вводить и считывать значения с консоли и рассмотрим конструкцию «switch — case»

public static void mainMenu() {
    System.out.println("Выберите режим игры: ");
    System.out.println("1. Игра против компьютера.");
    System.out.println("2. 2 игрока.");
    System.out.println("3. Выход.");
    int i = 0;
    Scanner sc = new Scanner(System.in);
    // Считываем значение с консоли и сохраняем его
    // в переменную i
    i = sc.nextInt();
    switch (i) {
	case 1: {
	    aiLevel();
	    break;
	}
	case 2: {
	    modeTwoPlayers();
	    break;
	}
	case 3: {
	    System.exit(0);
	    break;
	}
	default: {
	    System.out.println("Было введено неверное значение!");
	}
    }
}

Далее напишем метод modeTwoPlayers(), в котором инкапсулирована логика для игры двух игроков. Здесь мы рассмотрим такие конструкции, как цикл «while», условный оператор. Что здесь написано: «while (true)» — это бесконечный цикл. Выход из бесконечного цикла будет осуществляться по определенным условиям: победа одного из участников, либо когда все ячейки заполнены (ничья). «userShot()» — это ход игрока. «checkWin()» — проверка победы.

public static void modeTwoPlayers() {
    int count = 0;
    initField();
    while (true) {
        printField();
        userShot(USER_SIGN, 1);
        count++;
        if (checkWin(USER_SIGN)) {
            System.out.println("USER 1 WIN!!!");
            printField();
            break;
        }
        userShot(USER_SIGN_SECOND, 2);
        count++;
        if (checkWin(USER_SIGN_SECOND)) {
            System.out.println("USER 2 WIN!!!");
            printField();
            break;
        }
        if (count == Math.pow(DIMENSION, 2)) {
            printField();
            break;
        }
     }
}

В методе modeAgainstAI() мы реализуем режим игры против компьютера. Данный метод является практически копией предыдущего метода. Отличие заключается в том, что здесь реализован метод aiShot(), который отвечает за логику хода компьютера:

public static void modeAgainstAI() {
int count = 0;
initField();
while (true) {
    printField();
    userShot(USER_SIGN, 0);
    count++;
    if (checkWin(USER_SIGN)) {
        System.out.println("USER WIN!!!");
        printField();
        break;
    }
    aiShot();
    count++;
    if (checkWin(AI_SIGN)) {
        System.out.println("AI WIN!!!");
        printField();
        break;
     }
     if (count == Math.pow(DIMENSION, 2)) {
         printField();
         break;
     }
  }
}

Далее напишем методы инициализации поля и вывод поля в консоль. Здесь мы рассмотрим цикл «for»:

public static void initField() {
    for (int i = 0; i < DIMENSION; i++) {
        for (int j = 0; j < DIMENSION; j++) {
            field[i][j] = NOT_SIGN;
        }
    }
}

public static void printField() {
    for (int i = 0; i <= DIMENSION; i++) {
        System.out.print(i + " ");
    }
    System.out.println();
    for (int i = 0; i < DIMENSION; i++) {
        System.out.print((i + 1) + " ");
            for (int j = 0; j < DIMENSION; j++) {
                System.out.print(field[i][j] + " ");
            }
        System.out.println();
    }
}

В данной первой статье, посвященной реализации крестиков-ноликов в процедурном стиле мы изучили некоторые возможности языка Java. В следующей статье мы разберем самое интересное: реализацию хода компьютера и определение победителя.

На связи был Алексей Гулынин, оставляйте свои комментарии, увидимся в следующих статьях.

Крестики-нолики

Коты против собак

Вместо предисловия

Статья за 2011 год. Интересно, работает ли код сейчас? Спустя 9 лет я решил заново пройтись по проекту. Заодно кое-что поправил в коде.

Однажды я написал статью на Хабре. Один из читателей сказал, что тоже написал одну статью про создание игры «Крестики-нолики», но боится её выкладывать на всеобщее обозрение. Я и несколько других комментаторов сумели убедить его опубликовать свой материал, который вы можете прочитать здесь. В статье речь шла о создании игры в IntelliJ IDEA Community Edition. Если кому-то интересно, то ознакомьтесь с оригиналом статьи, а также почитайте комментарии. Я в свою очередь немного отредактировал статью и сам код игры.


Статья затронет весь цикл разработки приложения. Вместе мы напишем простенькую игру “Крестики-Нолики” с одним экраном.

Для нашего приложения идеально подойдёт макет TableLayout с идентификатором android:id=»@+id/tableLayout».


<?xml version="1.0" encoding="utf-8"?>
<TableLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/tableLayout"
    android:gravity="center">
</TableLayout>

Программный доступ к TableLayout:


package ru.alexanderklimov.tictactoe;

import android.os.Bundle;
import android.widget.TableLayout;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    private TableLayout tableLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        tableLayout = findViewById(R.id.tableLayout);
        buildGameField(); // создание игрового поля
    }
}

Теперь необходимо реализовать метод buildGameField(). Для этого требуется создать поле в виде матрицы. Этим будет заниматься класс Game. Сначала нужно создать класс Player, объекты которого будут заполнять ячейки игрового поля и класс Square для самих ячеек.

Player.java

package ru.alexanderklimov.tictactoe;

public class Player
{
    private String name;

    public Player(String name)
    {
        this.name = name;
    }

    public CharSequence getName()
    {
        return name;
    }
}

Square.java


package ru.alexanderklimov.tictactoe;

public class Square
{
    private Player player = null;

    public void fill(Player player)
    {
        this.player = player;
    }

    public boolean isFilled()
    {
        return player != null;
    }

    public Player getPlayer()
    {
        return player;
    }
}

Game.java


package ru.alexanderklimov.tictactoe;

public class Game
{
    private Square[][] field;
    private int squareCount;

    public Game()
    {
        field = new Square[3][3];
        squareCount = 0;
        // заполнение поля
        for (int i = 0, l = field.length; i < l; i++)
        {
            for (int j = 0, l2 = field[i].length; j < l2; j++)
            {
                field[i][j] = new Square();
                squareCount++;
            }
        }
    }

    public Square[][] getField()
    {
        return field;
    }
}

Метод buildGameField() динамически добавляет строки и колонки в таблицу (игровое поле):


private Button[][] buttons = new Button[3][3];

private void buildGameField()
{
    Square[][] field = game.getField();
    for (int i = 0, lenI = field.length; i < lenI; i++ ) {
        TableRow row = new TableRow(this); // создание строки таблицы
        for (int j = 0, lenJ = field[i].length; j < lenJ; j++)
        {
            Button button = new Button(this);
            buttons[i][j] = button;
            button.setOnClickListener(new Listener(i, j)); // установка слушателя, реагирующего на клик по кнопке
            row.addView(button, new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT,
                    TableRow.LayoutParams.WRAP_CONTENT)); // добавление кнопки в строку таблицы
            button.setWidth(160);
            button.setHeight(160);
        }
        tableLayout.addView(row,
                new TableLayout.LayoutParams(TableLayout.LayoutParams.WRAP_CONTENT,
                TableLayout.LayoutParams.WRAP_CONTENT)); // добавление строки в таблицу
    }
}

В коде создаётся объект, реализующий интерфейс View.OnClickListener. Создадим вложенный класс Listener. Он будет виден только из активности.


public class Listener implements View.OnClickListener
{
    private int x = 0;
    private int y = 0;

    Listener(int x, int y)
    {
        this.x = x;
        this.y = y;
    }

    public void onClick(View view)
    {
        Button button = (Button) view;
    }
}

Осталось реализовать логику игры. Возвращаемся к Game.java:


package ru.alexanderklimov.tictactoe;

public class Game {
    // игроки
    private Player[] players;

    // поле
    private Square[][] field;

    // начата ли игра?
    private boolean started;

    // текущий игрок
    private Player activePlayer;

    // Считает колличество заполненных ячеек
    private int filled;

    // Всего ячеек
    private int squareCount;

    public Game() {
        field = new Square[3][3];
        squareCount = 0;
        // заполнение поля
        for (int i = 0, l = field.length; i < l; i++) {
            for (int j = 0, l2 = field[i].length; j < l2; j++) {
                field[i][j] = new Square();
                squareCount++;
            }
        }
        players = new Player[2];
        started = false;
        activePlayer = null;
        filled = 0;
    }

    public void start() {
        resetPlayers();
        started = true;
    }

    private void resetPlayers() {
        players[0] = new Player("X");
        players[1] = new Player("O");
        setCurrentActivePlayer(players[0]);
    }

    private void setCurrentActivePlayer(Player player) {
        activePlayer = player;
    }

    public Square[][] getField() {
        return field;
    }

    public boolean makeTurn(int x, int y) {
        if (field[x][y].isFilled()) {
            return false;
        }
        field[x][y].fill(getCurrentActivePlayer());
        filled++;
        switchPlayers();
        return true;
    }

    private void switchPlayers() {
        activePlayer = (activePlayer == players[0]) ? players[1] : players[0];
    }

    public Player getCurrentActivePlayer() {
        return activePlayer;
    }

    public boolean isFieldFilled() {
        return squareCount == filled;
    }

    public void reset() {
        resetField();
        resetPlayers();
    }

    private void resetField() {
        for (int i = 0, l = field.length; i < l; i++) {
            for (int j = 0, l2 = field[i].length; j < l2; j++) {
                field[i][j].fill(null);
            }
        }
        filled = 0;
    }
}

Определение победителя

Очевидно, что в крестики-нолики выигрывает тот, кто построит X или O в линию длиной, равной длине поля по вертикали, по горизонтали или по диагонали. Первая мысль, которая приходит в голову — это написать методы для каждого случая. Думаю, в этом случае хорошо подойдёт паттерн Chain of Responsobility. Определим интерфейс:


package ru.alexanderklimov.tictactoe;

public interface WinnerCheckerInterface 
{
	public Player checkWinner();
}

Так как Game наделён обязанностью выявлять победителя, он реализует этот интерфейс. Настало время создать виртуальных «лайнсменов», каждый из которых будет проверять свою сторону. Все они реализует интерфейс WinnerCheckerInterface.

WinnerCheckerHorizontal.java


package ru.alexanderklimov.tictactoe;

public class WinnerCheckerHorizontal implements WinnerCheckerInterface {
    private Game game;

    public WinnerCheckerHorizontal(Game game) {
        this.game = game;
    }

    public Player checkWinner() {
        Square[][] field = game.getField();
        Player currPlayer;
        Player lastPlayer = null;
        for (int i = 0, len = field.length; i < len; i++) {
            lastPlayer = null;
            int successCounter = 1;
            for (int j = 0, len2 = field[i].length; j < len2; j++) {
                currPlayer = field[i][j].getPlayer();
                if (currPlayer == lastPlayer && (currPlayer != null && lastPlayer != null)) {
                    successCounter++;
                    if (successCounter == len2) {
                        return currPlayer;
                    }
                }
                lastPlayer = currPlayer;
            }
        }
        return null;
    }
}

WinnerCheckerVertical.java


package ru.alexanderklimov.tictactoe;

public class WinnerCheckerVertical implements WinnerCheckerInterface {
    private Game game;

    public WinnerCheckerVertical(Game game) {
        this.game = game;
    }

    public Player checkWinner() {
        Square[][] field = game.getField();
        Player currPlayer;
        Player lastPlayer = null;
        for (int i = 0, len = field.length; i < len; i++) {
            lastPlayer = null;
            int successCounter = 1;
            for (int j = 0, len2 = field[i].length; j < len2; j++) {
                currPlayer = field[j][i].getPlayer();
                if (currPlayer == lastPlayer && (currPlayer != null && lastPlayer != null)) {
                    successCounter++;
                    if (successCounter == len2) {
                        return currPlayer;
                    }
                }
                lastPlayer = currPlayer;
            }
        }
        return null;
    }
}

WinnerCheckerDiagonalLeft.java


package ru.alexanderklimov.tictactoe;

public class WinnerCheckerDiagonalLeft implements WinnerCheckerInterface {
    private Game game;

    public WinnerCheckerDiagonalLeft(Game game) {
        this.game = game;
    }

    public Player checkWinner() {
        Square[][] field = game.getField();
        Player currPlayer;
        Player lastPlayer = null;
        int successCounter = 1;
        for (int i = 0, len = field.length; i < len; i++) {
            currPlayer = field[i][i].getPlayer();
            if (currPlayer != null) {
                if (lastPlayer == currPlayer) {
                    successCounter++;
                    if (successCounter == len) {
                        return currPlayer;
                    }
                }
            }
            lastPlayer = currPlayer;
        }
        return null;
    }
}

WinnerCheckerDiagonalRight.java


package ru.alexanderklimov.tictactoe;

public class WinnerCheckerDiagonalRight implements WinnerCheckerInterface {
    private Game game;

    public WinnerCheckerDiagonalRight(Game game) {
        this.game = game;
    }

    public Player checkWinner() {
        Square[][] field = game.getField();
        Player currPlayer;
        Player lastPlayer = null;
        int successCounter = 1;
        for (int i = 0, len = field.length; i < len; i++) {
            currPlayer = field[i][len - (i + 1)].getPlayer();
            if (currPlayer != null) {
                if (lastPlayer == currPlayer) {
                    successCounter++;
                    if (successCounter == len) {
                        return currPlayer;
                    }
                }
            }
            lastPlayer = currPlayer;
        }
        return null;
    }
}

Проинициализируем их в конструкторе Game:


// "Судьи". После каждого хода они будут проверять,
// нет ли победителя
private WinnerCheckerInterface[] winnerCheckers;

// Инициализация "судей"
winnerCheckers = new WinnerCheckerInterface[4];
winnerCheckers[0] = new WinnerCheckerHorizontal(this);
winnerCheckers[1] = new WinnerCheckerVertical(this);
winnerCheckers[2] = new WinnerCheckerDiagonalLeft(this);
winnerCheckers[3] = new WinnerCheckerDiagonalRight(this);

Реализация checkWinner():


public Player checkWinner() {
    for (WinnerCheckerInterface winChecker : winnerCheckers) {
        Player winner = winChecker.checkWinner();
        if (winner != null) {
            return winner;
        }
    }
    return null;
}

Возвращаемся в класс активности. Победителя проверяем после каждого хода. Добавим код в метод onClick() класса Listener


public void onClick(View view) 
{
    Button button = (Button) view;
    Game g = game;
    Player player = g.getCurrentActivePlayer();
    if (makeTurn(x, y)) 
    {
        button.setText(player.getName());
    }
    Player winner = g.checkWinner();
    if (winner != null) 
    {
        gameOver(winner);
    }
    if (g.isFieldFilled()) 
    {  // в случае, если поле заполнено
        gameOver();
    }
}

Метод gameOver() реализован в 2-х вариантах.

Метод makeTurn() проверяет очерёдность хода игрока, а метод refresh() обновляет состояние поля:

Весь код активности.


// Если этот код работает, его написал Александр Климов,
// а если нет, то не знаю, кто его писал.
package ru.alexanderklimov.as34;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    private TableLayout tableLayout;

    private Button[][] buttons = new Button[3][3];

    private Game game;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        tableLayout = findViewById(R.id.tableLayout);


        game = new Game();
        buildGameField(); // создание игрового поля
        game.start(); // будет реализован позже
    }

    private void buildGameField() {
        Square[][] field = game.getField();
        for (int i = 0, lenI = field.length; i < lenI; i++) {
            TableRow row = new TableRow(this); // создание строки таблицы
            for (int j = 0, lenJ = field[i].length; j < lenJ; j++) {
                Button button = new Button(this);
                buttons[i][j] = button;
                button.setOnClickListener(new Listener(i, j)); // установка слушателя, реагирующего на клик по кнопке
                row.addView(button, new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT,
                        TableRow.LayoutParams.WRAP_CONTENT)); // добавление кнопки в строку таблицы
                button.setWidth(160);
                button.setHeight(160);
            }
            tableLayout.addView(row,
                    new TableLayout.LayoutParams(TableLayout.LayoutParams.WRAP_CONTENT,
                            TableLayout.LayoutParams.WRAP_CONTENT)); // добавление строки в таблицу
        }
    }

    public class Listener implements View.OnClickListener {
        private int x;
        private int y;

        Listener(int x, int y) {
            this.x = x;
            this.y = y;
        }

        public void onClick(View view) {
            Button button = (Button) view;
            Game g = game;
            Player player = g.getCurrentActivePlayer();
            if (makeTurn(x, y)) {
                button.setText(player.getName());
            }
            Player winner = g.checkWinner();
            if (winner != null) {
                gameOver(winner);
            }
            if (g.isFieldFilled()) {  // в случае, если поле заполнено
                gameOver();
            }
        }
    }

    private void gameOver(Player player) {
        CharSequence text = "Player "" + player.getName() + "" won!";
        Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
        game.reset();
        refresh();
    }

    private void gameOver() {
        CharSequence text = "Draw";
        Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
        game.reset();
        refresh();
    }

    private boolean makeTurn(int x, int y) {
        return game.makeTurn(x, y);
    }

    private void refresh() {
        Square[][] field = game.getField();

        for (int i = 0, len = field.length; i < len; i++) {
            for (int j = 0, len2 = field[i].length; j < len2; j++) {
                if (field[i][j].getPlayer() == null) {
                    buttons[i][j].setText("");
                } else {
                    buttons[i][j].setText(field[i][j].getPlayer().getName());
                }
            }
        }
    }
}

Видео готового приложения

Оригинальные исходники от автора статьи уже недоступны, но по оставленным записям я восстановил работоспособность приложения, хотя, конечно, его можно ещё улучшить.

Игра Крестики-нолики

Дополнительное чтение

Ещё один вариант игры

SavvaVyatkin/CatDogToe — правильный вариант игры с участием котов. Картинка для статьи взята из этого проекта.

Крестики-нолики (Custom View)

Реклама

Привет хаброжители. Данный пост является «рерайтом» моего поста для песочницы. На этот раз я постараюсь охватить больше тем, чем тогда.

Почему Java?

Ничего объективного я тут не скажу, а скажу лишь то, что я люблю этот язык, и мне нравиться писать на нем. Да, на Java нет игр AAA-класса, но Java предоставляет огромные возможности, больше кол-во встроенных средств и быстроту написания кода.

Начнем с выбора IDE. Я являюсь фанатом Eclipse и посоветую вам его.
Если же почему-то вам он не понравился, вы можете использовать NetBeans, Intellij IDEA или командную строку и ваш любимый редактор.

И скачаем JDK последней версии: JDK 7u4

Скорее всего проблем с установкой IDE у вас не возникнет, а если у вас 64-битная система, все же посоветую устанавливать 32-битный Eclipse, так как иногда бывают ошибки и Eclipse у вас просто не запустится.

Под катом мы приступим к созданию игры.

Класс Game

Итак, создаем проект, в нем класс Game(попутно создав в нем точку входа). Данный класс должен наследовать класс Canvas и реализовать интерфейс Runnable:

Создадим переменную running типа Boolean, которая, как вы уже догадались будет показывать нам запущена ли игра, или нет.

Создадим функцию start() и в ней мы будем создавать новый поток и переводить running в true:

Создадим три функции — update(long delta), render() и init(). Я надеюсь что их значение вам понятно. В функции run() создадим главный игровой цикл, перед ним будем вызывать init(), а в нем самом render() и update(). Так же мы будем вычислять разницу между кадрами(delta time).

Пока поработаем над функцией render().

Вам наверное уже не терпится запустить и попробовать, но не спешите. Мы должны создать фрейм и добавить наш холст на него. Заодно и объявим три переменных.

Примерно вот так выглядит наш класс Game сейчас.

Класс Sprite

Создадим новый класс Sprite. Поскольку этот класс небольшой, я сразу приведу весь его код с комментариями:

Сразу же проверим работоспособность. Возьмем эту картинку и скопируем ее в папку с нашим классом Sprite. Добавим функцию getSprite() в класс Game(временно).

Добавим нашу картинку в папку assets(папку создать в корне проекта), саму папку надо добавить в build path.

Далее создаем переменную hero типа Sprite. В функции init() инициализируем ее. В Функции render() рисуем:

Input

Для обработки инпута мы создадим класс, наследующий KeyAdapter:

Тут же и объявим две переменных в шапке класса Game:

Внутри класса KeyInputHandler создадим две функции:

Теперь в функции init() добавим следующее:

Создадим переменные x и y для героя(так как пока что мы еще не написали класс Entity). Сделаем чтобы герой всегда рисовался на этих координатах.

А теперь в функции update() будем проверять нажаты ли клавиши и изменять x-координату.

Создание простых игр на Java

Как написать свой Тетрис на Java за полчаса

В предыдущих статьях этой серии мы уже успели написать сапёра, змейку и десктопный клон игры 2048. П.

  • 0
  • 0
  • 0
  • 0
  • 0

Как написать свою 2048 на Java за 15 минут

В предыдущих статьях этой серии мы уже писали сапёра и змейку на Java, а теперь попробуем написать десктопный клон игры 2048.

  • 0
  • 0
  • 0
  • 0
  • 0

Как написать свою змейку на Java за 15 минут

В предыдущей статье мы писали сапёра за 15 минут, теперь займёмся классической змейкой.В предыдущей .

Раздел «Игры» на JavaRush: Игровой движок

Раздел

void setCellColor(int x, int y, Color color) — устанавливает клетке с координатами (x, y) цвет color:

Color getCellColor(int x, int y) — возвращает цвет клетки с координатами (x, y):

void setCellValue(int x, int y, String value) — помещает в клетку с координатами (x, y) текст value:

String getCellValue(int x, int y) — возвращает текст, содержащийся в клетке с координатами (x, y):

void setCellTextSize(int x, int y, int size) — устанавливает размер контента в клетке с координатами (x, y). size – высота текста в процентах от высоты клетки:

int getCellTextSize(int x, int y) — возвращает размер контента в клетке с координатами (x, y):

void setCellNumber(int x, int y, int value) — помещает в клетку с координатами (x, y) число value:

int getCellNumber(int x, int y) — возвращает число, содержащееся в клетке с координатами (x, y). Если в клетке содержится не число, возвращает 0:

void setCellTextColor(int x, int y, Color color) — устанавливает цвет контента(текста) в клетке с координатами (x, y):

Color getCellTextColor(int x, int y) — возвращает цвет контента (текста) в клетке с координатами (x, y):

void setCellValueEx(int x, int y, Color cellColor, String value) — устанавливает клетке с координатами (x, y) цвет фона cellColor и контент value:

void setCellValueEx(int x, int y, Color cellColor, String value, Color textColor) — устанавливает клетке с координатами (x, y) цвет фона cellColor, контент value и цвет контента textColor:

void setCellValueEx(int x, int y, Color cellColor, String value, Color textColor, int textSize); — устанавливает клетке с координатами (x, y) цвет фона cellColor, контент value, цвет контента textColor и размер контента textSize:

Время на прочтение
4 мин

Количество просмотров 266K

Введение и подготовка

Привет хаброжители. Данный пост является «рерайтом» моего поста для песочницы. На этот раз я постараюсь охватить больше тем, чем тогда.

Почему Java?

Ничего объективного я тут не скажу, а скажу лишь то, что я люблю этот язык, и мне нравиться писать на нем. Да, на Java нет игр AAA-класса, но Java предоставляет огромные возможности, больше кол-во встроенных средств и быстроту написания кода.

IDE

Начнем с выбора IDE. Я являюсь фанатом Eclipse и посоветую вам его.
Если же почему-то вам он не понравился, вы можете использовать NetBeans, Intellij IDEA или командную строку и ваш любимый редактор.

JDK

И скачаем JDK последней версии: JDK 7u4

Скорее всего проблем с установкой IDE у вас не возникнет, а если у вас 64-битная система, все же посоветую устанавливать 32-битный Eclipse, так как иногда бывают ошибки и Eclipse у вас просто не запустится.

Под катом мы приступим к созданию игры.

Класс Game

Итак, создаем проект, в нем класс Game(попутно создав в нем точку входа). Данный класс должен наследовать класс Canvas и реализовать интерфейс Runnable:

public class Game extends Canvas implements Runnable {
	private static final long serialVersionUID = 1L;

	public void run() { //функция run появляется после того, как мы добавили "implements Runnable"
	}

	public static void main(String[] args) {
	}
}

Создадим переменную running типа Boolean, которая, как вы уже догадались будет показывать нам запущена ли игра, или нет.

Создадим функцию start() и в ней мы будем создавать новый поток и переводить running в true:

public void start() {
	running = true;
	new Thread(this).start();
}

Создадим три функции — update(long delta), render() и init(). Я надеюсь что их значение вам понятно. В функции run() создадим главный игровой цикл, перед ним будем вызывать init(), а в нем самом render() и update(). Так же мы будем вычислять разницу между кадрами(delta time).

public void run() {
	long lastTime = System.currentTimeMillis();
	long delta;
	
	init();
		
	while(running) {
		delta = System.currentTimeMillis() - lastTime;
		lastTime = System.currentTimeMillis();	
		update(delta);
		render();
	}
}
	
public void init() {
		
}
	
public void render() {

}
	
public void update(long delta) {
		
}

Пока поработаем над функцией render().

public void render() {
	BufferStrategy bs = getBufferStrategy(); 
	if (bs == null) {
		createBufferStrategy(2); //создаем BufferStrategy для нашего холста
		requestFocus();
		return;
	}
		
	Graphics g = bs.getDrawGraphics(); //получаем Graphics из созданной нами BufferStrategy
	g.setColor(Color.black); //выбрать цвет
	g.fillRect(0, 0, getWidth(), getHeight()); //заполнить прямоугольник 
	g.dispose();
	bs.show(); //показать
}

Вам наверное уже не терпится запустить и попробовать, но не спешите. Мы должны создать фрейм и добавить наш холст на него. Заодно и объявим три переменных.

public static int WIDTH = 400; //ширина
public static int HEIGHT = 300; //высота
public static String NAME = "TUTORIAL 1"; //заголовок окна

public static void main(String[] args) {
	Game game = new Game();
	game.setPreferredSize(new Dimension(WIDTH, HEIGHT));

	JFrame frame = new JFrame(Game.NAME);
	frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //выход из приложения по нажатию клавиши ESC
	frame.setLayout(new BorderLayout());
	frame.add(game, BorderLayout.CENTER); //добавляем холст на наш фрейм
	frame.pack();
	frame.setResizable(false);
	frame.setVisible(true);

	game.start();
}

Примерно вот так выглядит наш класс Game сейчас.

Класс Sprite

Создадим новый класс Sprite. Поскольку этот класс небольшой, я сразу приведу весь его код с комментариями:

import java.awt.Graphics;
import java.awt.Image;

public class Sprite {
	private Image image; //изображение
	
	public Sprite(Image image) {
		this.image = image;
	}
	
	public int getWidth() { //получаем ширину картинки
		return image.getWidth(null);
	}

	public int getHeight() { //получаем высоту картинки
		return image.getHeight(null);
	}
	
	public void draw(Graphics g,int x,int y) { //рисуем картинку
		g.drawImage(image,x,y,null);
	}
}

Сразу же проверим работоспособность. Возьмем эту картинку и скопируем ее в папку с нашим классом Sprite. Добавим функцию getSprite() в класс Game(временно).

public Sprite getSprite(String path) {
	BufferedImage sourceImage = null;
		
	try {
		URL url = this.getClass().getClassLoader().getResource(path);
		sourceImage = ImageIO.read(url);
	} catch (IOException e) {
		e.printStackTrace();
	}

	Sprite sprite = new Sprite(Toolkit.getDefaultToolkit().createImage(sourceImage.getSource()));
		
	return sprite;
}

Добавим нашу картинку в папку assets(папку создать в корне проекта), саму папку надо добавить в build path.

Далее создаем переменную hero типа Sprite. В функции init() инициализируем ее. В Функции render() рисуем:

//в "шапку"
public static Sprite hero;

//в init()
hero = getSprite("man.png");

//в render() после g.fillRect(0, 0, getWidth(), getHeight());
hero.draw(g, 20, 20);

Результат:

Весь код Game.java

Input

Для обработки инпута мы создадим класс, наследующий KeyAdapter:

private class KeyInputHandler extends KeyAdapter {

}

Тут же и объявим две переменных в шапке класса Game:

private boolean leftPressed = false;
private boolean rightPressed = false; 

Внутри класса KeyInputHandler создадим две функции:

public void keyPressed(KeyEvent e) { //клавиша нажата
	if (e.getKeyCode() == KeyEvent.VK_LEFT) {
		leftPressed = true;
	}
	if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
		rightPressed = true;
	}	
} 	
public void keyReleased(KeyEvent e) { //клавиша отпущена
	if (e.getKeyCode() == KeyEvent.VK_LEFT) {
		leftPressed = false;
	}
	if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
		rightPressed = false;
	}
}

Теперь в функции init() добавим следующее:

addKeyListener(new KeyInputHandler());

Создадим переменные x и y для героя(так как пока что мы еще не написали класс Entity). Сделаем чтобы герой всегда рисовался на этих координатах.

private static int x = 0;
private static int y = 0;

hero.draw(g, x, y);

А теперь в функции update() будем проверять нажаты ли клавиши и изменять x-координату.

public void update(long delta) {
	if (leftPressed == true) {
		x--;
	}
	if (rightPressed == true) {
		x++;
	}
}

Он двигается!

Спасибо за внимание.

P.S. Ссылка на github

Понравилась статья? Поделить с друзьями:
  • Как написать игру на html5
  • Как написать игру на html
  • Как написать игру на delphi
  • Как написать игру на c visual studio
  • Как написать игру морской бой на python