[ 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); } } Ничего сложного. Это «актер», который устанавливается по размеру экрана пользователя и делает нашу игру более похожей на звездное небо. Примерно так это должно выглядеть:
Главный экран игры Теперь добавим его в 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
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
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
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
// В этом методе мы будем парсить наши переводы. А вы как думали? Мы делаем многоязычную игру!
public HashMap
// В этом методе парсим звезды
public Array
// В этом парсим уровни
public Array
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
// Парсим позицию для звезд. Знаю знаю, можно было сделать это при парсинге уровня, но мне так легче потом читать этот код, если разбить его по задачам
public Map
Файл langs.xml
Файл stars.xml
Файл 1.xml
Файлы проекта и пример готовой игры.