[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
  •  
  • Додамо об'єкти зірок, нот і ще дечого
  •  
 
 Файли уроку.
 
Спасибі за увагу!

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.