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

Здравствуйте! Не прошло суток с момента публикации первой части статьи, а я не могу спать, так как есть незаконченное дело и нужно дописать статью. Приступим.Оговорюсь еще раз. Я шибкий не знаток Java и поэтому следующий далее код, может смутить многих, но игру я написал меньше, чем за неделю и работал скорее на результат, чем на красоту и порядочность кода. Надеюсь, в комментариях найдется тот, кто поможет сделать код и структуру проекта, если не совершенными, то хотя бы привести к хорошему виду и дать возможность мне и остальным стать более хорошими программистами. Ладно, хватит лирики, продолжим наш «хардкор».Создадим новый package и назовем его objects. В нем создадим класс фона, а в него добавим следующий код:

Файл BackgroundActor.java

package ru.habrahabr.songs_of_the_space.objects;

import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.g2d.Sprite; import com.badlogic.gdx.scenes.scene2d.Actor;

public class BackgroundActor extends Actor { private Texture backgroundTexture; private Sprite backgroundSprite;

public BackgroundActor () { backgroundTexture = new Texture («images/sky.jpg»); backgroundSprite = new Sprite (backgroundTexture); backgroundSprite.setSize (Gdx.graphics.getWidth (), Gdx.graphics.getHeight ()); }

@Override public void draw (Batch batch, float alpha) { backgroundSprite.draw (batch); } } Ничего сложного. Это «актер», который устанавливается по размеру экрана пользователя и делает нашу игру более похожей на звездное небо. Примерно так это должно выглядеть:

Главный экран игры image Теперь добавим его в MyGame.java и сделаем его доступным извне, для того, чтобы не создавать его на каждом следующем экране. Это избавит нас от мерцания.

Файл MyGame.java

// Перед методом create () public BackgroundActor background;

@Override public void create () { … background = new BackgroundActor (); background.setPosition (0, 0); … } Далее, мы должны в каждом новой экране добавлять его на сцену:

stage.addActor (game.background); Теперь, также в пакете objects создадим класс ноты. Он будет хранить все наши ноты в нужной нам последовательности.

Файл Note.java

package ru.habrahabr.songs_of_the_space.objects;

public class Note { private String note; private float delay; private Star star;

// Устанавливаем ноты. Ноты будем брать из xml файла уровня. public void setNote (String note) { this.note = note; }

public String getNote () { return this.note; }

// Устанавливаем задержку для ноты, чтобы можно было создавать мелодии разной сложности public void setDelay (String delay) { this.delay = Float.parseFloat (delay); }

public float getDelay () { return this.delay; }

// Наша красавица — звезда public void setStar (Star star) { this.star = star; }

public Star getStar () { return this.star; } } Теперь, когда мы создали ноту, нам нужно создать звезду, которая будет нашим основным актером в нашей космической сцене. Она будет мерцать и петь свою чудную мелодию для будущих пользователей.Перед тем, как продолжить немного поясню, зачем нам нужен отдельный класс для ноты и для звезды. Мелодия может повторять свои ноты, а каждая звезда должна быть в единственном экземпляре. Когда я только продумывал идею игры, я как раз хранил каждую ноту внутри звезды. В итоге, либо мелодия была слишком простой, либо звезд на небе становилось слишком много и было сложно пройти уровень даже с восемью повторяющимися нотами.Итак, создаем звезду.

Файл Star.java

package ru.sayakhov.songs_of_the_space.objects;

import com.badlogic.gdx.Gdx; import com.badlogic.gdx.audio.Sound; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.Texture.TextureFilter; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.g2d.Sprite; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;

public class Star extends Actor { // Звук, если пользователь ошибся private Sound sound, wrong;

// Ноты в строковом представлении private String note;

// Изображение звезды private Sprite img; private Texture img_texture; // Наш уровень. Он будет говорить, где должна находиться звезда private Level level; public Star (String str_img, String str_sound) { img_texture = new Texture («images/stars/» + str_img + ».png»); img_texture.setFilter (TextureFilter.Linear, TextureFilter.Linear); img = new Sprite (img_texture);

// Это я сделал для того, чтобы размер звезды менялся в зависимости от экрана пользователя

img.setSize (Gdx.graphics.getHeight () * 15 / 100, Gdx.graphics.getHeight () * 15 / 100); this.note = str_sound; this.sound = Gdx.audio.newSound (Gdx.files.internal («sounds/bells/» + str_sound + ».mp3»)); this.wrong = Gdx.audio.newSound (Gdx.files.internal («sounds/bells/wrong.mp3»));

// Слушает события касания пользователя и играет соответствующую ноту, а также создает эффект мерцания за счет увеличения звезды в размерах addListener (new ClickListener () { @Override public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) { img.setScale (1.2f); if (note.equals (level.getCurrentNoteStr ())) { level.setCurrentNote (); Gdx.input.vibrate (25); // Дадим пользователю понять, что он нажал немного вибрируя в момент касания getSound ().play (); } else {

// Если юзер ошибся, то начинаем сначала. Проигрываем первые четыре ноты и играем их. А также сильнее вибрируем, чтобы оповестить его об ошибке.

level.setCurrentNote (0); level.setEndNote (true); level.setPlayMusic (); getWrongSound ().play (); Gdx.input.vibrate (80); } return true; } @Override public void touchUp (InputEvent event, float x, float y, int pointer, int button) { img.setScale (1.0f); // Как только пользователь отпустил нашу звезду, делаем ее размер таким же, каким он был } }); setTouchable (Touchable.enabled); // Делаем нашу звезду активной для касания } public void setLevel (Level level) { this.level = level; } // Устанавливаем позицию изображения равной позиции актера, и делаем размеры актера равными размеру звезды @Override public void setBounds (float x, float y, float width, float height) { super.setBounds (x, y, this.img.getWidth (), this.img.getHeight ()); this.img.setPosition (x, y); } // В каждый момент исполнения, немного крутим нашу звезду. Пусть потанцует. @Override public void act (float delta) { img.rotate (0.05f); } // Рисуем звезду на сцене @Override public void draw (Batch batch, float alpha) { this.img.draw (batch); } public Sound getSound () { return this.sound; } public Sound getWrongSound () { return this.wrong; } public String getNote () { return this.note; } public Sprite getImg () { return this.img; } } Теперь создадим наш класс уровня. Он будет отвечать за создания всех актрис и актеров, а также играть мелодию и поздравлять в победой. Я добавил его в пакет objects, но он лучше подходит как менеджер, поэтому можете перенести его туда самостоятельно.

Файл Level.java

package ru.habrahabr.songs_of_the_space.objects;

import java.util.HashMap; import java.util.Map;

import com.badlogic.gdx.Gdx; import com.badlogic.gdx.audio.Sound; import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.utils.Array;

public class Level { private XMLparse xml_parse; private Array notes = new Array(); private Array stars = new Array(); private Map> starsPos = new HashMap>(); private int currentNote; private int endNote; private float delay; private boolean playMusic; private boolean win; private final Sound winner = Gdx.audio.newSound (Gdx.files.internal («sounds/win.mp3»)); // Победный звук аплодисментов public Level (String level) { xml_parse = new XMLparse (); Array xml_stars = xml_parse.XMLparseStars (); // парсим звезды из всего списка имеющихся notes = xml_parse.XMLparseNotes (level); // парсим ноты для уровня starsPos = xml_parse.getPos (level); // позиции звезд в текущем уровне endNote = 3; delay = 0; this.win = false; setPlayMusic (); for (Note n: this.notes) { for (Star s: xml_stars) { if (n.getNote ().equals (s.getNote ()) && ! this.stars.contains (s, true)) { // Поскольку в одном xml у нас хранятся все возможные варианты звезд, этот код отсеит лишние this.stars.add (s); } if (n.getNote ().equals (s.getNote ())) n.setStar (s); // А здесь мы устанавливаем для каждой ноты свою звезду } }

for (Star s: this.stars) { s.setLevel (this); s.setBounds (

// Это нужно для того, чтобы позицию звезды можно было описать в процентом от размера экрана пользователя отношении (так как скопления наших звезд будут стараться походить на настоящие созвездия реального космоса)

Gdx.graphics.getWidth () * Float.parseFloat (starsPos.get (s.getNote ()).get (0)) / 100, Gdx.graphics.getHeight () * Float.parseFloat (starsPos.get (s.getNote ()).get (1)) / 100 — s.getImg ().getHeight () / 2, s.getImg ().getWidth (), s.getImg ().getHeight () ); } } public boolean isWin () { return this.win; }

// Устанавливаем последнюю ноту public void setEndNote () { if (this.endNote < this.notes.size - 1) { this.endNote += 4; } } // Переопределяем метод для того, чтобы в случае, когда пользователь ошибся, сделать последней четвертую ноту. // Можно было обойтись и одним методом, но мне так понравилось больше. Переопределяй! Властвуй!

public void setEndNote (boolean begin) { if (begin) { this.endNote = 3; } } public void setCurrentNote (int note) { this.currentNote = note; }

// Устанавливаем текущую ноту public void setCurrentNote () { if (this.currentNote < this.notes.size - 1) { this.currentNote++; if (currentNote - 1 == endNote) { currentNote = 0; setEndNote(); // Увеличиваем значение на 4 для последней ноты setPlayMusic(); // Играем мелодию с большим количеством нот } } else {

// Если пользователь отыграл все ноты, играем победные аплодисменты

this.endNote = notes.size — 1; this.currentNote = 0; this.win = true; this.winner.play (); } } public int getCurrentNote () { return this.currentNote; } public String getCurrentNoteStr () { return this.notes.get (this.currentNote).getNote (); } public Array getNotes () { return this.notes; } public Array getStars () { return this.stars; } public void setPlayMusic () { if (playMusic) { playMusic = false; } else { playMusic = true; } } // Играем наши ноты для пользователя

public void playStars () { if (playMusic) { for (Star s: stars) { s.setTouchable (Touchable.disabled); // Не даем пользователю трогать наши звезды, пока играет мелодия } if (getCurrentNote () < notes.size) { if (getCurrentNote() <= endNote) { Note note = notes.get(getCurrentNote()); delay += note.getDelay(); // delay позволяет создавать задержку по времени между проигрыванием нот if (delay >= 0.9f) note.getStar ().getImg ().setScale (1.2f); // Увеличиваем активную в данный момент звезду для того, чтобы создать эффект мерцания if (delay >= 1.0f) { delay = 0; setCurrentNote (currentNote + 1); note.getStar ().getSound ().play (); note.getStar ().getImg ().setScale (1f); } } else { setPlayMusic (); setCurrentNote (0); } } else { delay = 0; setCurrentNote (0); setPlayMusic (); } } else { for (Star s: stars) { s.setTouchable (Touchable.enabled); // Делаем все наши звезды активными для касания } } } } Надеюсь, все понятно. Старался максимально комментировать код. Единственное, что может вызвать вопросы — это delay. Поясню немного. Метод playStars () будет вызываться в методе render () класса PlayScreen.java. Поскольку, он выполняется в потоке, каждый раз при совпадении всех условий, delay будет увеличиваться на заданное количество. Таким образом, будет имитироваться задержка в игре нот. Это лучше увидеть в коде. Давайте, наконец наполним наш класс PlayScreen.java. Поскольку, там много кода, я решил его спрятать под спойлер.

Файл PlayScreen.java package ru.habrahabr.songs_of_the_space.managers;

import ru.habrahabr.songs_of_the_space.MyGame; import ru.habrahabr.songs_of_the_space.objects.GamePreferences; import ru.habrahabr.songs_of_the_space.objects.Level; import ru.habrahabr.songs_of_the_space.objects.PlayStage; import ru.habrahabr.songs_of_the_space.objects.PlayStage.OnHardKeyListener; import ru.habrahabr.songs_of_the_space.objects.Star;

import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input.Keys; import com.badlogic.gdx.Screen; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.g2d.TextureAtlas; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.scenes.scene2d.ui.Label; import com.badlogic.gdx.scenes.scene2d.ui.Label.LabelStyle; import com.badlogic.gdx.scenes.scene2d.ui.Skin; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.scenes.scene2d.ui.TextButton; import com.badlogic.gdx.scenes.scene2d.ui.TextButton.TextButtonStyle; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.viewport.ScreenViewport;

public class PlayScreen implements Screen { final MyGame game; private GamePreferences pref; private Level level; private String sL, nL; private Array stars;

private PlayStage stage; private Table table, table2; public PlayScreen (final MyGame gam, String strLevel, String strNextLevel) { game = gam; this.sL = strLevel; this.nL = strNextLevel; stage = new PlayStage (new ScreenViewport ()); stage.addActor (game.background); // Добавляем фон pref = new GamePreferences (); level = new Level (strLevel); stars = level.getStars (); level.setCurrentNote (0); for (final Star s: stars) { stage.addActor (s); // Добавляем всех актрис (звезды) на сцену } LabelStyle labelStyle = new LabelStyle (); labelStyle.font = game.font; // Skin для кнопок, которые показываются в случае победы пользователя

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»); // Для всех кнопок лучше создать таблицу, так как она хорошо справляется с варавниванием

table = new Table (); table.padTop (20); table.center ().top (); table.setFillParent (true);

// label для показа названия созвездия Label label = new Label (game.langStr.get («Constellation»), labelStyle); table.add (label); table.row ().padBottom (30); label = new Label (game.langStr.get («level_» + strLevel), labelStyle); table.add (label); table.setVisible (false); stage.addActor (table); table2 = new Table (); table2.center ().bottom (); table2.setFillParent (true); table2.row ().colspan (2).padBottom (30); label = new Label (game.langStr.get («YouWin»), labelStyle); table2.add (label).bottom (); table2.row ().padBottom (20); TextButton button = new TextButton (game.langStr.get («Again»), textButtonStyle);

// Нужно не забыть заставить кнопки прослушивания событих клика (касания) // Эта кнопка, после нажатия, запустит уровень сначала button.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, sL, nL)); dispose (); }; }); table2.add (button);

// А эта перенесет пользователя обратно на экран выбора уровня

button = new TextButton (game.langStr.get («Levels»), textButtonStyle); button.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 (); }; }); table2.add (button); table2.setVisible (false); stage.addActor (table2); 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 LevelScreen (game)); } } }); }

@Override public void render (float delta) {

// Очистка экрана в каждый момент выполнения потока Gdx.gl.glClearColor (0, 0, 0, 1); Gdx.gl.glClear (GL20.GL_COLOR_BUFFER_BIT); // Рисуем сцену и вызываем метод act () для дополнительных действий актеров, описанных в одноименном методе каждого (в нашем случае, это вращение звезд) stage.act (delta); stage.draw (); level.playStars (); // Если пользователь выиграл, то показываем ему все наши кнопки, label’ы и прочее

if (level.isWin ()) { table.setVisible (true); table2.setVisible (true); pref.setLevel (nL); // Это для настроек игры. Объяснения ниже. for (Star s: stars) { s.setTouchable (Touchable.disabled); // Делаем все звезды неактивными для касания после победы пользователя } } }

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

@Override public void show () {}

@Override public void hide () {}

@Override public void pause () {}

@Override public void resume () {}

// На забываем уничтожить сцену и объект класса MyGame

@Override public void dispose () { stage.dispose (); game.dispose (); } } Наверное, код вызвал несколько вопросов, так как в нем можно заметить новый класс GamePreferences.java. Этот класс позволит нам хранить все настройки игры в удобном формате. Для Android приложения будет создан, так называемый «SharedPreferences». Подробнее здесь. В данном случае, в нем мы будем хранить пройденные пользователем уровни.Ну что? Давайте теперь создадим и наполним его.

Файл GamePreferences.java

package ru.habrahabr.songs_of_the_space.objects;

import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Preferences;

public class GamePreferences { private Preferences pref; private static final String PREFS_NAME = «SONGS_OF_THE_SPACE»; private static final String PREF_LEVEL = «LEVEL_»; public GamePreferences () { pref = Gdx.app.getPreferences (PREFS_NAME); } public boolean getLevel (String level) { pref.putBoolean (PREF_LEVEL + 1, true); pref.flush (); return pref.getBoolean (PREF_LEVEL + level, false); } public void setLevel (String level) { pref.putBoolean (PREF_LEVEL + level, true); pref.flush (); } } В нем нет ничего сложного. Не буду дублировать документацию, ссылку на нее я дал ниже. Теперь нам нужно немного обновить наш класс XMLparse.java. Так, мы еще не научили нашу парсить звезды и ноты. Сделаем это.

Файл XMLparse.java package ru.habrahabr.songs_of_the_space.objects;

import java.io.IOException; import java.util.HashMap; import java.util.Map;

import com.badlogic.gdx.Application.ApplicationType; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.XmlReader; import com.badlogic.gdx.utils.XmlReader.Element;

public class XMLparse { private Array stars = new Array(); private Array notes = new Array(); private Map> starsPos = new HashMap>();

// В этом методе мы будем парсить наши переводы. А вы как думали? Мы делаем многоязычную игру! public HashMap XMLparseLangs (String lang) { HashMap langs = new HashMap(); try { Element root = new XmlReader ().parse (Gdx.files.internal («xml/langs.xml»)); Array xml_langs = root.getChildrenByName («lang»); for (Element el: xml_langs) { if (el.getAttribute («key»).equals (lang)) { Array xml_strings = el.getChildrenByName («string»); for (Element e: xml_strings) { langs.put (e.getAttribute («key»), e.getText ()); } } else if (el.getAttribute («key»).equals («en»)) { Array xml_strings = el.getChildrenByName («string»); for (Element e: xml_strings) { langs.put (e.getAttribute («key»), e.getText ()); } } } } catch (IOException e) { e.printStackTrace (); } return langs; }

// В этом методе парсим звезды public Array XMLparseStars () { try { Element root = new XmlReader ().parse (Gdx.files.internal («xml/stars.xml»)); Array xml_stars = root.getChildrenByName («star»); for (Element el: xml_stars) { Star star = new Star ( el.getAttribute («files»), el.getAttribute («files») ); stars.add (star); } } catch (IOException e) { e.printStackTrace (); } return this.stars; }

// В этом парсим уровни public Array XMLparseLevels () { Array levels = new Array(); Array int_levels = new Array(); FileHandle dirHandle; if (Gdx.app.getType () == ApplicationType.Android) { dirHandle = Gdx.files.internal («xml/levels»); } else { // Это хак, так как libGDX почему-то не хотел видеть этот файл при тестировании Desktop приложения

dirHandle = Gdx.files.internal (System.getProperty («user.dir») + »/assets/xml/levels»); } for (FileHandle entry: dirHandle.list ()) { levels.add (entry.name ().split (».xml»)[0]); } 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; }

// Парсим ноты public Array XMLparseNotes (String strLevel) { try { Element root = new XmlReader ().parse (Gdx.files.internal («xml/levels/» + strLevel + ».xml»)).getChildByName («notes»); Array xml_notes = root.getChildrenByName («note»); for (Element el: xml_notes) { Note note = new Note (); note.setNote (el.getText ()); note.setDelay (el.getAttribute («delay»)); this.notes.add (note); } } catch (IOException e) { e.printStackTrace (); } return this.notes; }

// Парсим позицию для звезд. Знаю знаю, можно было сделать это при парсинге уровня, но мне так легче потом читать этот код, если разбить его по задачам public Map> getPos (String strLevel) { try { Element root = new XmlReader ().parse (Gdx.files.internal («xml/levels/» + strLevel + ».xml»)).getChildByName («positions»); Array xml_pos = root.getChildrenByName («position»); for (Element el: xml_pos) { Array xy = new Array(); xy.add (el.getAttribute («x»)); xy.add (el.getAttribute («y»)); this.starsPos.put (el.getAttribute («note»), xy); } } catch (IOException e) { e.printStackTrace (); } return this.starsPos; } } Осталось немного. Правда. Теперь, раз уж я заикнулся про многоязыковую поддержку, давайте создадим я немного поясню, как это будет. За основу берем локаль пользователя. Для нас она начинается с символом ru, для англичан с en и так далее. Я перевел приложение на два языка, поэтому языковой файл будет таким (и поэтому в коде метода XMLparseLangs немного странное условие):

Файл langs.xml Play Exit Again Levels You win! Constellation Canes Venatici Triangulum Equuleus Apus Sagitta Musca Ursa Minor Orion Ursa Major Eridanus Lacerta Играть Выход Повторить Уровни Вы победили! Созвездие Гончие псы Треугольник Малый Конь Райская Птица Стрела Муха Малая медведица Орион Большая медведица Эридан Ящерица Как видно, мы берем аттрибут и по нему определяем, что отдавать пользователю. Теперь нужно сделать еще кое-что. Создать XML файлы звезд, нот, уровней. Сделаем это.

Файл stars.xml Если бегло глянуть этот файл, то можно заметить, что немного слукавил, когда сказал, что для каждой ноты будет своя звезда. Я сделал разное представление звезд в разной тональности. Зачем? Для улучшения звучания, так как если взять более-менее интересное созвездие, то можно заметить, то оно состоит, как минимум из 8–9 звезд, а писать мелодию для 8–9 разных нот не очень-то хотелось, вот я и решил немного упростить себе жизнь, добавив еще одну октаву.Теперь приведу файл (для примера) уровня.

Файл 1.xml

d5 a6 d6 f#6 e5 a6 c#6 e6 d6 f#6 a6 d5 Как видно, сначала мы определяем последовательность нот и их задержку, а затем определяем позицию каждой уникальной ноты в процентом отношении. Кажется это все. Если что-то забыл, жду комментариев. Также, жду критики и советов. Если кому-нибудь будет интересно в следующей статье я могу описать процесс подключения AdMob к нашей игре, рассказать как и откуда я брал звуки для игры, и также рассказать о том, как я выкладывал игру в Google Play. Спасибо за внимание!

Файлы проекта и пример готовой игры.

© Habrahabr.ru