[ libGDX ] Пишем полноценную игру под Android. Часть 1

Здравствуйте! Я решил попробовать себя на поприще game-dev'а и заодно рассказать и показать как это было. Вторая часть здесь.

Игра представляет собой экран, на котором расположены созвездия. Каждая звезда этого созвездия имеет свой цвет (нота). Например, ноту «До» обычно представляют красным цветом, а «Ми» — желтым. Вот что получится в итоге:

image

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

Писать будем, используя фреймворк libGDX. Мне он больше всех понравился, как новичку в этом деле. Да и информации по нему я нашел больше. Итак, приступим.

Что нам понадобится:

  • Eclipse
  • Gradle
  • Android SDK
  • libGDX последней версии
  • Голова + руки + терпение


Я не буду создавать проект вручную. Мне проще воспользоваться gdx-setup. Итак, запускаем ее:

java -jar gdx-setup.jar


Далее вводим:

  • Name: «Songs of the Space»
  • Package: «ru.yoursite.songs_of_the_space»
  • Game Class: «MyGame»
  • Destination: path/to/your/workspace/songs_of_the_space
  • Android SDK: path/to/your/sdk
  • libGDX Version: Nightlies
  • Sub Projects: Desktop, Android
  • Extentions: «Freetype»


Нажимаем Generate и, после окончания процесса, идем в Eclipse.

В Eclipse выбираем Import -> Gradle -> Gradle Project. Затем Browse.. ищем наш проект и затем Build. После завершения выбираем все проекты и наживаем Finish. После завершения у вас в списке проектов появятся наши проекты. Сразу идем в основной проект (core) -> MyGame.java. Очищаем все, что создал gdx, а также наследуемся не от ApplicationAdapter, а от Game. В итоге, класс должен получить вид:

public class MyGame extends Game {

    @Override
    public void create() {
    }

    @Override
    public void render() {
        super.render();
    }
}


Далее. Создадим новый пакет, назовем его screens. И в нем три класса:
  • MainMenuScreen — будет отвечать за начальный экран приложения
  • LevelScreen — будет отвечать за экран выбора уровней
  • PlayScreen — будет отвечать за игровой экран


Все они должны наследоваться от интерфейса Screen. Добавляем все методы, которые требует наше наследование, закрываем все, кроме MainMenuScreen. В нем пишем следующее:

public class MainMenuScreen implements Screen {
    
    // наш основной класс
    final MyGame game;

    // Объявим все необходимые объекты
    private Stage stage;
    private TextButton play, exit;
    private Table table;
    private LabelStyle labelStyle;

    // Конструктор принимает объект нашего основого класса (объяснения позже)
    public MainMenuScreen(final MyGame gam) {
        game = gam;

        // Сцена -- она поможет существенно уменьшить количество кода и упростить нам жизнь
	stage = new Stage(new ScreenViewport());

        // Скин для кнопок. Изображения вы найдете по ссылке внизу статьи 
        Skin skin = new Skin();
        TextureAtlas buttonAtlas = new TextureAtlas(Gdx.files.internal("images/game/images.pack"));
        skin.addRegions(buttonAtlas);
        TextButtonStyle textButtonStyle = new TextButtonStyle();
        textButtonStyle.font = game.font;
        textButtonStyle.up = skin.getDrawable("button-up");
        textButtonStyle.down = skin.getDrawable("button-down");
        textButtonStyle.checked = skin.getDrawable("button-up");

        labelStyle = new LabelStyle();
        labelStyle.font = game.font;
        table = new Table();
        table.setFillParent(true);
        
        // Кнопка играть. Добавляем новый listener, чтобы слушать события касания. После касания, выбрирует и переключает на экран выбора уровней, а этот экран уничтожается
        play = new TextButton("Играть", textButtonStyle);
        play.addListener(new ClickListener() {
            @Override
            public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
                Gdx.input.vibrate(20);
                return true;
            };
            @Override
            public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
                game.setScreen(new LevelScreen(game));
                dispose();
            };
        });
        
        // Кнопка выхода. Вообще это не обязательно. Просто для красоты, ибо обычно пользователь жмет на кнопку телефона.
        exit = new TextButton("Выход", textButtonStyle);
        exit.addListener(new ClickListener() {
            @Override
            public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
                Gdx.input.vibrate(20);
                return true;
            };
            @Override
            public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
                Gdx.app.exit();
                dispose();
            };
        });
        table.add(play);
        table.row();
        table.add(exit);
        stage.addActor(table);

        Gdx.input.setInputProcessor(stage);  // Устанавливаем нашу сцену основным процессором для ввода (нажатия, касания, клавиатура etc.)
        Gdx.input.setCatchBackKey(true); // Это нужно для того, чтобы пользователь возвращался назад, в случае нажатия на кнопку Назад на своем устройстве
    }

    @Override
    public void render(float delta) {
        // Очищаем экран и устанавливаем цвет фона черным
        Gdx.gl.glClearColor(0, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        
        // Рисуем сцену
        stage.act(delta);
        stage.draw();
    }

    @Override
    public void resize(int width, int height) {}

    @Override
    public void show() {}

    @Override
    public void hide() {}

    @Override
    public void pause() {}

    @Override
    public void resume() {}

    @Override
    public void dispose() {
        // Уничтожаем сцену и объект game.
        stage.dispose();
        game.dispose();
    }
}


Далее. Делаем импорт зависимостей (Shift + Ctrl + O). И идем в основной класс MyGame.java. В него добавим следующее:

public class MyGame extends Game {

    // Объявляем наш шрифт и символы для него (чтобы нормально читались русские буковки)
    public BitmapFont font, levels;
    private static final String FONT_CHARACTERS = "абвгдеёжзийклмнопрстуфхцчшщъыьэюяАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789][_!$%#@|\\/?-+=()*&.;,{}\"´`'<>";

    @Override
    public void create() {
        // Я взял шрифт RussoOne с Google Fonts. Сконвертировал его в TTF. (как я понял, только ttf и поддерживается)
        FreeTypeFontGenerator generator = new FreeTypeFontGenerator(Gdx.files.internal("fonts/russoone.ttf"));
        FreeTypeFontParameter param = new FreeTypeFontParameter();
        param.size = Gdx.graphics.getHeight() / 18; // Размер шрифта. Я сделал его исходя из размеров экрана. Правда коряво, но вы сами можете поиграться, как вам угодно.
        param.characters = FONT_CHARACTERS; // Наши символы
        font = generator.generateFont(param); // Генерируем шрифт
        param.size = Gdx.graphics.getHeight() / 20;
        levels = generator.generateFont(param);
        font.setColor(Color.WHITE); // Цвет белый
        levels.setColor(Color.WHITE);
        generator.dispose(); // Уничтожаем наш генератор за ненадобностью.

	@Override
	public void render() {
		super.render();
	}
}


Делаем импорты и создаем новый package с именем managers (например). В нем класс XMLparse.java. Зачем это нужно? Затем, что уровни и многое другое мы будем брать из xml файлов. Кстати, создайте в папке assets (android проект -> assets) папку xml. В ней папки:

  • images
  • fonts
  • sounds
  • xml


В папку fonts положите шрифт. А в папке xml, создайте папку levels. Обновите Desktop проект ( F5 ), чтобы он подхватил все это. И теперь, давайте наполним наш класс XMLparse.java. В него пишем следующее:

public class XMLparse {
    public Array<String> XMLparseLevels() {
        Array<String> levels = new Array<String>();
        Array<Integer> int_levels = new Array<Integer>();

        FileHandle dirHandle;
        if (Gdx.app.getType() == ApplicationType.Android) {
            dirHandle = Gdx.files.internal("xml/levels");
        } else {
            dirHandle = Gdx.files.internal(System.getProperty("user.dir") + "/assets/xml/levels"); // хак для desktop проекта, так как он почему-то не видел этих файлов. Создайте символическую ссылку папки assets в в корне desktop-проекта на папку assets android-проекта
        }
        for (FileHandle entry : dirHandle.list()) {
            levels.add(entry.name().split(".xml")[0]);
        }
        
        // Эту жесть я сделал потому что сортировка строк немного не верно сортирует уровни. В комментариях подскажут как это сделать красивее. Я не особо Java программист. Я только учусь :)
        for (int i = 0; i < levels.size; i++) {
            int_levels.add(Integer.parseInt(levels.get(i)));
        }
        int_levels.sort();
        levels.clear();
		
        for (int i = 0; i < int_levels.size; i++) {
            levels.add(String.valueOf(int_levels.get(i)));
        }
        return levels;
    }
}


Ну что? Давайте наконец наполним класс LevelScreen.java. Но перед этим создайте пару xml файлов в папке assets -> xml -> levels с именами 1.xml, 2.xml и так далее. А в класс напишем следующее:

public class LevelScreen implements Screen {
    
    final MyGame game;
    
    private Stage stage;
    private Table table;
    private LabelStyle labelStyle;
    private TextButton level;
    
    private Array<String> levels;
    
    public LevelScreen(MyGame gam) {
        game = gam;
        
        stage = new Stage(new ScreenViewport());
        
        Skin skin = new Skin();
        TextureAtlas buttonAtlas = new TextureAtlas(Gdx.files.internal("images/game/images.pack"));
        skin.addRegions(buttonAtlas);
        TextButtonStyle textButtonStyle = new TextButtonStyle();
        textButtonStyle.font = game.levels;
        textButtonStyle.up = skin.getDrawable("level-up");
        textButtonStyle.down = skin.getDrawable("level-down");
        textButtonStyle.checked = skin.getDrawable("level-up");
        
        //Парсим наши уровни
        XMLparse parseLevels = new XMLparse();
        levels = parseLevels.XMLparseLevels();

        labelStyle = new LabelStyle();
        labelStyle.font = game.levels; // Берем размер шрифта из класса MyGame
        table = new Table();
        table.row().pad(20); // Новая строка + отступы
        table.center();
        table.setFillParent(true);

        for (int i = 0; i < levels.size; i++) {
            final String cur_level = levels.get(i);
            level = new TextButton(cur_level, textButtonStyle);
            level.addListener(new ClickListener() {
                @Override
                public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
                    Gdx.input.vibrate(20);
                    return true;
                };
                @Override
                public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
                    game.setScreen(new PlayScreen(game, cur_level)); // Передаем выбранный уровень в PlayScreen
                    dispose();
                };
            });
            table.add(level);

            // А эта жесть для того, чтобы переходить на новую строку при достижении количества в пять уровней в одной строке

            float indexLevel = Float.parseFloat(String.valueOf(i)) + 1;
            if (indexLevel % 5.0f == 0) table.row().padLeft(20).padRight(20).padBottom(20);
        }
        stage.addActor(table); // Добавляем нашу таблицу с уровнями на сцену
 
        Gdx.input.setInputProcessor(stage);
        Gdx.input.setCatchBackKey(true);
        // Это случится, когда пользователь нажмет на кнопку Назад на своем устройстве. Мы переведем его на прошлый экран.
        stage.setHardKeyListener(new OnHardKeyListener() {          
            @Override
            public void onHardKey(int keyCode, int state) {
                if (keyCode == Keys.BACK && state == 1){
                    game.setScreen(new MainMenuScreen(game));    
                }       
            }
        });
    }

    @Override
    public void render(float delta) {
        Gdx.gl.glClearColor(0, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        
        stage.act(delta);
        stage.draw();
    }

    @Override
    public void resize(int width, int height) {}

    @Override
    public void show() {}

    @Override
    public void hide() {}

    @Override
    public void pause() {}

    @Override
    public void resume() {}

    @Override
    public void dispose() {
        stage.dispose();
        game.dispose();
    }
}


Ух, много уже написал. В следующей части (если эта пройдет все этапы публикации) мы сделаем следующее:
  • Наполним класс PlayScreen
  • Добавим объекты звезд, нот и еще кое-чего


Файлы урока.

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

UPDATE

Я настолько спешил со статьей, что забыл описать один важный момент, из-за которого проект не запустится. Прошу прощения. Итак, нам также нужно создать свой класс сцены (Stage), который будет наследником libGDX Stage. Идем в наш package managers и создаем в нем класс с именем PlayStage.java. Его код:

public class PlayStage extends Stage {

    public PlayStage(ScreenViewport screenViewport) {
        super(screenViewport);
    }

    // Прослушивает события нажатия клавиш пользователем
    @Override
    public boolean keyDown(int keyCode) {
        if (keyCode == Keys.BACK) {
        	if (getHardKeyListener() != null)
                getHardKeyListener().onHardKey(keyCode, 1);
        }
        return super.keyDown(keyCode);
    }

    @Override
    public boolean keyUp(int keyCode) {
        if (keyCode == Keys.BACK){
            if (getHardKeyListener() != null)
                getHardKeyListener().onHardKey(keyCode, 0);
        }
        return super.keyUp(keyCode);
    }   
	
    public interface OnHardKeyListener {
        // Теперь, при нажатии пользователем кнопки Назад, будет выполняться код из этого метода (который мы пишем в теле экрана)
        public abstract void onHardKey(int keyCode, int state);
    }
    private OnHardKeyListener _HardKeyListener = null;  
    public void setHardKeyListener(OnHardKeyListener HardKeyListener) {
        _HardKeyListener = HardKeyListener;
    }       
    public OnHardKeyListener getHardKeyListener() {
        return _HardKeyListener;
    }
}


Теперь, в LevelScreen мы создаем объект класса PlayStage, а не Stage и ошибок больше быть не должно. И еще одно дополнение. Подготовим класс PlayScreen, так как ему требуется указать конструктор с параметрами. Напишем его и сгенерируем остальные методы, чтобы компилятор не ругался. Код:

public class PlayScreen implements Screen {
    final MyGame game;

    // В следующем уроке мы подробно рассмотрим этот класс, поэтому комментировать ничего не буду. Да тут и так понятно.
    public PlayScreen(final MyGame gam, String strLevel) {
        game = gam;
    }

    @Override
    public void render(float delta) {
        Gdx.gl.glClearColor(0, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
    }

    @Override
    public void resize(int width, int height) {}

    @Override
    public void show() {}

    @Override
    public void hide() {}

    @Override
    public void pause() {}

    @Override
    public void resume() {}

    @Override
    public void dispose() {
        game.dispose();
    }
}
Share post

Comments 20

    +1
    Спасибо за статью, может наконец-то в следующий раз я буду использовать все возможности LibGDX, а не городить велосипеды (кнопки, скины, etc) да костыли.

    Скрытый текст
    Не ради холивара, но почему не Android Studio/чистая IDEA? На своем опыте скажу, что для новичков она проще. Или статья не урочная, а просто показать?
      +1
      Не автор, но отвечу. У идеи есть свои недостатки — например производительность, особенно у 13 версии хромает. На достаточно мощном железе, приходится, например, выключать анимации окон потому что они ужасно лагают. Да и в целом она подтупляет. Эклипс же конечно стремный, но он летает.
        0
        Может анимации из-за JRE глючат? С осени пользуюсь AndroidStudio, «пережил» с ней все обновления, и как-то без каких-либо багов GUI (что вечно что-то ломается в системе сборки проекта — это другой разговор).
        По времени сборки — где-то 2-3 секунды на сборку проекта (clean+build). Если пересобирать не все, то быстрее. Под Eclipse как-то пробовал еще в конце прошлого года проект собирать, по времени примерно тоже самое выходит.
          +1
          Может еще от системы зависит, я на винде сижу. Версии 12 и младше по части GUI не лагали.
          2-3 секунды? У меня на HDD секунд 15-20 собирается. На SSD около 5. Инкрементальная сборка, не clean+build. Эклипс в тех же условиях мгновенно отрабатывает.
            0
            У меня на линухе с ssd и неплохим таким процессором. Сборка по времени упирается в процессор. Может у вас проект большой по размеру? У меня так, мелочь на пару десятков классов.
              0
              Если бы, просто hello world который по дефолту создается gdx-setup'ом. Я и удивляюсь, потому у меня проект на сотню классов в eclipse полностью пересобирается за те же 15-20 секунд. Ну может ноутбучного i5 gradle для счастья мало.
        • UFO just landed and posted this here
          0
          Android Studio вообще нельзя использовать с libGDX (по моим сведениям), ибо она заточена только на разработку под Android, а libGDX — кроссплатформенный. Я выбрал Eclipse, потому что на момент начала изучения Java и Android, большая часть статей и уроков начиналась с фразы: «Давайте сначала установим Eclipse и настроем его для работы.». Как-то так)
            0
            * настроим. И как я только опечатался.
            0
            И отвечая на вашу фразу про костыли и все возможности libGDX — очень сомневаюсь, что я настолько опытен. В целом, опыта работы с ним у меня не так много (как я об этом написал в начале статьи). Скорее всего за следующую (а возможно уже и за эту) статью я получу много щелбанов от бывалых «джавистов» и/или game dev специалистов.
            0
            Буквально на днях попробовал для проекта на libgdx использовать Gradle+IDEA. До этого сидел на Eclipse+Ant. Так вот, gradle собирает проект(по крайней мере десктопный) ужасно медленно. Hello world проект gradlом собирается медленнее чем полноценная игра antом. Плюс ко всему под эклипсом у меня за это время еще и упаковщик атласов успевал отработать. Пробовал перенести gradle проект на SSD — всеравно медленно.
              0
              Это правда, Gradle медленный, но мне наскучило руками настраивать все эти Java Build Path, а тут готовый вариант не делать этого. Вот я воспользовался.
              0
              Может я что-то делаю неправильно, но у меня нерешаемая проблема в коде:
              image
                +1
                Да, прошу прощения, это я немного поторопился и забыл рассказать об одном важном моменте. Обновил статью, надеюсь, ошибок больше не будет.
                0
                Спасибо за статью. А можно ссылку на Ваши игры в Google Play?
                  0
                  Рад стараться! Это мой дебют) но следующая игра не за горами, так что постараюсь вас уведомить.
                  +1
                  Прежде, чем кого-то чему-то учить и рассказывать про вашу «полноценную игру», я посоветовал бы сначала хоть почитать какие-либо основы, какие-либо общепринятые нормы, как, в конце-концов, правильно делать банальные вещи.
                  Вкратце:
                  1. В libgdx лучше объявлять все ресурсы в .json файле, который подавать в Skin(FileHandle), загружая через Skin.get(String, Class<?>).
                  2. Используете dispose() не там, где нужно, совсем не так ( «game.dispose()» в Screen'e — вы понимаете смысл dispose()? ), как нужно.
                  3. Не совсем понял прикол с наследованием Stage ради обработки нажатия одной кнопки, да еще и таким извращенным методом. Stage.addListener(EventListener) уже не в тренде?
                  4. XMLparse — я даже не могу придумать, как еще сложнее получить массив отсортированных чисел, которые зачем-то представлены в виде строк.
                  5. Мягко говоря, ваш код ужасен.
                  Если вы говорите, что «не особо Java программист» и «только учитесь», то не нужно публиковать такую, извиняюсь за прямоту, чушь. Это может кто-то подхватить и считать за норму — «На хабре опубликовано — значит, что правильно».
                    0
                    Благодарю за критику. Ее-то я как раз и ждал. Все замечания учту. Я знаю, что мой код ужасен, но все же и он кому-либо поможет, так как большая часть уроков по libGDX, которые встречались мне, просто цитировали документацию отдельными кусками, а здесь хотя бы есть некоторая целостность (на мой взгляд). Тем более, мы же говорим о программистах, а они думают головой, прежде вслепую копировать чужой код или алгоритм решения своей задачи. Но это, опять же мое мнение, а я могу ошибаться)
                    0
                    Сам изучаю сейчас LibGDX. Хотелось бы узнать по поводу метода dispose() в Ваших классах, наследуемых от Screen.

                    Разве надо для обьекта game делать dispose? Мы же его не создавали, в данном классе.
                      0
                      С dispose мне уже sperson написал)

                    Only users with full accounts can post comments. Log in, please.