Написать пост

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

Аватар Пётр Соковых

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

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

В этот раз нам снова понадобятся:

  • 15 минут свободного времени;
  • Настроенная рабочая среда, т.е. JDK и IDE (например Eclipse);
  • Библиотека LWJGL (версии 2.x.x) для работы с Open GL. Обратите внимание, что для LWJGL версий выше 3 потребуется написать код, отличающийся от того, что приведён в статье;
  • Спрайты, т.е. картинки самой змеи и фрукта, который она будет есть. Можно чисто символически нарисовать самому, или скачать использовавшиеся при написании статьи.

Подключение библиотек

В прошлый раз у многих возникли с этим вопросом проблемы, поэтому мне показалось уместным посвятить этому немного времени. Во-первых, выше я дал ссылку на скачивание архива с библиотеками, которые использую я, чтобы не было путаницы с версиями и вопросов, где найти что-то. Папку из архива требуется поместить в папку проекта и подключить через вашу IDE.

Во-вторых, у многих пользователей InteliJ IDEA возникли проблемы как раз с их подключением. Я нашёл в сети следующий видеогайд:

После того, как я сделал всё в точности по нему, у меня библиотеки подключились корректно и всё заработало.

Работа с графикой

С этой стороны наша задача мало отличается от той, что мы выполняли при написании Сапёра. Снова создаём класс GUI, который будет хранить и обновлять состояние всех графических элементов. Если точнее:

  • Класс будет выполнять инициализацию OpenGL:
initializeOpenGL()
			///Class GUI

private static void initializeOpenGL(){	
		try {
                       //Задаём размер будущего окна
			Display.setDisplayMode(new DisplayMode(SCREEN_WIDTH, SCREEN_HEIGHT));

                       //Задаём имя будущего окна
			Display.setTitle(SCREEN_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);
	}
		
  • Должен хранить текущие состояния ячеек://Класс Cells напишем несколько позже private static Cell[][] cells;
  • Должен отрисовывать эти самые ячейки:///Рисует все клетки public static void draw(){ ///Очищает экран от старого изображения glClear(GL_COLOR_BUFFER_BIT); for(Cell[] line:cells){ for(Cell cell:line){ drawElement(cell); } } }private static void drawElement(Cell elem){ ///Если у ячейки нет спрайта, то рисовать её не нужно if(elem.getSprite() == null) return; ///Собственно, рисуем. Подробно не останавливаюсь, так как нам интересна сама логика игры, а не LWJGL 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 update(boolean have_to_decrease) { updateOpenGL(); for(Cell[] line:cells){ for(Cell cell:line){ cell.update(have_to_decrease); } } } ///А этот метод будет использоваться только локально, /// т.к. базовым другие классы должны работать на более высоком уровне private static void updateOpenGL() { Display.update(); Display.sync(FPS); }

Как вы можете видеть, здесь я уже использовал несколько констант. Для них был создан отдельный класс Constants с public static полями. Вот он целиком:

			public class Constants {
    ///Размер игровой ячейки
    public static final int CELL_SIZE = 32;

    ///Размеры игрового поля в ячейках
    public static final int CELLS_COUNT_X = 20;
    public static final int CELLS_COUNT_Y = 20;

    ///Шанс появления ягод на старте в процентах. 
    ///При выставленном значении спавнится 3-5 ягод.
    ///Не беспокойтесь, что значение слишком низкое, как минимум одна ягода создаётся отдельно.
    public static final int INITIAL_SPAWN_CHANCE = 1;//%

    ///В нашем случае змея проходит одну клетку за один фрейм.
    ///Значение 5 мне показалось оптимальным, но вы можете экспериментировать.
    public static final int FPS = 5;

    ///Константы для создания окна, названия достаточно говорящие.
    public static final int SCREEN_WIDTH =CELLS_COUNT_X*CELL_SIZE;
    public static final int SCREEN_HEIGHT = CELLS_COUNT_Y*CELL_SIZE;
    public static final String SCREEN_NAME = "Tproger's Snake";
}
		

Enum Sprite, который отвечает за подгрузку текстур, полностью идентичен тому, что мы писали для Сапёра, за исключением того, что нам нужно только две текстуры — для змеи и для ягод. Вот код:

			public enum Sprite {
    ///Файлы с именами circle и cherries должны лежать по адресу
    /// %папка проекта%/res/ в расширении .png
    BODY("circle"), CHERRIES("cherries");

    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;
    }
}
		

Механика игры

Самое время поговорить о том, как наша змея будет, собственно, перемещаться. Вам наверняка доводилось видеть вывески, вокруг которых по кругу бегают огоньки? Разумеется, сами лампочки в них не перемещаются, просто каждый тик последняя гаснет, а первая зажигается. Таким же образом будет перемещаться и наша змея.

Несложно подсчитать, что каждая лампочка должна гореть столько тиков, какова длина “змеи”. Значит, мы должны сообщить клетке, в которую попадает змея, что она должна гореть определённое количество секунд, а каждый тик уменьшать это число у каждой клетки с ненулевым таймером, и менять спрайт, если змея из клетки уже выползла (т.е. таймер стал равен нулю). В случае же необходимости удлинить цепочку, достаточно просто не уменьшать время “горения” клеток на каком-то тике. Именно поэтому метод update() у классов Cell и GUI принимает параметр — если он равен false, значит, змея что-то съела.

Пишем класс клетки

			public class Cell {

    private int x; 
    private int y;
    private int state;/* 0 -> ячейка пуста 
                        >0 -> в ячейке тело змеи, которое будет там ещё N фреймов
                        <0 -> Что-то необычное:
                            -1: Ягоды
                        */

    ///Конструктор просто выставляет начальные значения координат и состояния
    public Cell (int x, int y, int state){
        this.x=x;
        this.y=y;
        this.state=state;
    }
    
    ///==== Ничем не примечательные геттеры и сеттеры

    public int getX(){
        return x;
    }

    public int getY(){
        return y;
    }

    public int getHeight(){
        return CELL_SIZE;
    }

    public int getWidth(){
        return CELL_SIZE;
    }

    public int getState(){
        return this.state;
    }

    public void setState(int state){
        this.state = state;
    }
    
    ///====

    ///Метод обновления клетки. Уменьшаем время "горения", если это необходимо
    public void update(boolean have_to_decrease){
        if (have_to_decrease && this.state > 0){
            this.state--;
        }
    }

    ///Ячейка "думает" как она должна выглядеть
    public Sprite getSprite(){
        if(this.state > 0){
            ///Если в ней тело змеи -- как змея
            return Sprite.BODY;
        }else if(this.state==0){
            ///Если в ней нет ничего -- никак выглядеть и не должна
            return null;
        }else{
            ///Иначе проходимся свитчем по возможным объектам.
            ///Так как это демо -- я добавил только ягоды 
            switch(this.state){
                default: return Sprite.CHERRIES;
            }
        }
    }


}
		

Добавляем геттер и сеттер для состояния клетки поля в GUI

			getState(x,y){
return cells[x][y].getState();
}

setState(x,y,state){
cells[x][y].setState(state);
}
		

Добавляем метод, создающий начальное поле в GUI

Просто инициализируем OpenGL, затем массив Cell[][] cells и заполняем последний клетками со случайным полем state.

			public static void init(){
        initializeOpenGL();

        cells = new Cell[CELLS_COUNT_X][CELLS_COUNT_Y];

        Random rnd = new Random();

        for(int i=0; i
		

Главный управляющий класс

			class Main{

    ///Переменная, при обращении которой в true приложение закрывается
    private static boolean isExitRequested=false;

    ///Данные о нашей змее. Выползать она будет из нижнего левого угла,
    ///Вправо (направления посчитаны по часовой стрелки от севера, т.е.
    /// 0 -- вверх, 1 -- вправо, 2 -- вниз, 3 -- влево
    private static int x=-1,y=0, direction=1, length=3;

    ///Флаг, который обращается в false, если на данном тике змея что-то съела
    private static boolean have_to_decrease = true;

    ///Входной класс
    public static void main(String[] args) {
        ///Инициализируем графический интерфейс
        GUI.init();

        ///Создаём ягодку в случайном месте
        generate_new_obj();

        ///Пока не получим сигнал на закрытие, в цикле...
        while(!isExitRequested){
            ///Проверяем ввод данных
            input();

            ///Двигаем змею
            move();

            ///Обновляем и рисуем графические элементы
            GUI.draw();
            GUI.update(have_to_decrease);

        }
    }

    private static void move() {
        /// Если на прошлом тике мы что-то съели, то на этом должны вернуть значение на true
        have_to_decrease=true;

        ///Меняем координаты змеи в зависимости от направления
        switch(direction){
            case 0:
                y++; break;
            case 1:
                x++; break;
            case 2:
                y--; break;
            case 3:
                x--; break;
        }

        ///Проверяем, не вышла ли змея за границы
        if(x < 0 || x >= CELLS_COUNT_X || y < 0 || y >= CELLS_COUNT_Y){
            //TODO gameover
            System.exit(1);
        }

        ///Смотрим состояние ячейки, куда зашла змея
        int next_cell_state = GUI.getState(x,y);

        ///Если там змея, то это проигрыш
        if(next_cell_state>0){
            //TODO gameover
            System.exit(1);

        }else{
            ///Если там еда, то 
            if(next_cell_state < 0){
                length++; ///Увеличиваем длину на единицу
                generate_new_obj(); ///Создаём новую еду
                have_to_decrease=false; ///Выставляем флаг того, что мы съели что-то
            }

            ///"Зажигаем" клетку
            GUI.setState(x,y,length);
        }
    }



/*Алгоритм генерации новой еды следующий.
Мы высчитываем количество клеток, которые не заполнены змеёй, по формуле:
CELLS_COUNT_X*CELLS_COUNT_Y-length
И выбираем случайную такую клетку (сохраняем её номер в point).
Потом проходимся по всем клеткам, и, если в клетке не змея, уменьшаем счётчик.
Как только счётчик равен нулю, создаём в этой клетке еду и выходим из цикла.

ВНИМАНИЕ! При таком методе ягоды могут создаваться поверх других ягод, т.е.
Их общее количество будет уменьшаться со временем. Чтобы избежать этого можно 
при уничтожении одной ягоды создавать случайное число (1-3) ягод.

*/

    private static void generate_new_obj() {
        int point = new Random().nextInt(CELLS_COUNT_X*CELLS_COUNT_Y-length);

        for(int i=0; i
		

Готово!
P.S. Исходники можно скачать здесь (архив всей папки проекта).

Следите за новыми постами
Следите за новыми постами по любимым темам
99К открытий100К показов