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

в 15:46, , рубрики: Без рубрики

Здравствуйте! Я решил попробовать себя на поприще 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
  • Добавим объекты звезд, нот и еще кое-чего

Файлы урока.

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

Автор: newca9h

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js