![image](https://habrastorage.org/web/0b6/1f8/dac/0b61f8dac9974bcd8558b67289285550.jpg)
Закончился конкурс от ВКонтакте и мой 2-х недельный марафон в интернете по поиску нужной информации. Хочу поделится небольшим опытом работы с графическим движком LibGDX. В интернете полно примеров, но большинство далеки от практики (нарисованный спрайт это далеко еще не игра) или уже устарели.
Честно, мне он понравился, потому что легко интегрируется с android studio, написан на удобном мне языке, отлично выполняет свою графическую задачу.
Вопросы, которые мне приходилось решать:
Вариант использования встроенного логгера
Я часто использую Timber из-за удобства. В core модуле он недоступен, потому написал простой вспомогательный класс с использованием имеющегося в LibGDX логгера, с которым приложение в релиз версии перестает писать логи
Плюс для удобства различные float значения 1.23456789 округляются
import com.badlogic.gdx.Gdx;
public class GdxLog {
public static boolean DEBUG;
@SuppressWarnings("all")
public static void print(String tag, String message) {
if (DEBUG) {
Gdx.app.log(tag, message);
}
}
@SuppressWarnings("all")
public static void d(String tag, String message, Integer...values) {
if (DEBUG) {
Gdx.app.log(tag, String.format(message, values));
}
}
@SuppressWarnings("all")
public static void f(String tag, String message, Float...values) {
if (DEBUG) {
Gdx.app.log(tag, String.format(message.replaceAll("%f", "%.0f"), values));
}
}
}
//... вызов
GdxLog.d(TAG, "worldWidth: %d", worldWidth);
Плюс для удобства различные float значения 1.23456789 округляются
E/libEGL: call to OpenGL ES API with no current context (logged once per thread)
Поначалу сильно огорчала такая ошибка, ничего не мог понять. Она происходит, потому что графика GLSurfaceView отрисовывается в своем отдельном потоке, когда проиcходит попытка извне вмешаться в этот процесс. Как исправить:
Впринципе аналогично, как и в случае, view.postInvalidate()
Я не любитель анонимных классов, поэтому написал такой простой метод для сокращения кода (иначе он просто становился не читаемым). Хотя с java 8 это уже не такая проблема, но из дополнительных плюсов то, что обрабатываются InvocationTargetException, когда, например, файл не найден, приложение уже не упадет по такой незначительной ошибке.
Важно, чтобы параметры не были примитивами, а наследовали Object. И плюс здесь упрощение с null параметром (только от класса String)
Gdx.app.postRunnable(new Runnable() {
@Override
public void run() {
// Здесь выполняется в самом потоке
}
});
Впринципе аналогично, как и в случае, view.postInvalidate()
Я не любитель анонимных классов, поэтому написал такой простой метод для сокращения кода (иначе он просто становился не читаемым). Хотя с java 8 это уже не такая проблема, но из дополнительных плюсов то, что обрабатываются InvocationTargetException, когда, например, файл не найден, приложение уже не упадет по такой незначительной ошибке.
// null may be only String params
public void postRunnable(final String name, final Object...params) {
Gdx.app.postRunnable(new Runnable() {
@Override
public void run() {
Method method = null;
Class[] classes = new Class[params.length];
for (int i = 0; i < params.length; i++) {
classes[i] = params[i] == null ? String.class : params[i].getClass();
}
try {
method = World.class.getMethod(name, classes);
} catch (SecurityException e) {
GdxLog.print(TAG, e.toString());
} catch (NoSuchMethodException e) {
GdxLog.print(TAG, e.toString());
}
if (method == null) {
return;
}
try {
method.invoke(WorldAdapter.this, params);
} catch (IllegalArgumentException e) {
GdxLog.print(TAG, e.toString());
} catch (IllegalAccessException e) {
GdxLog.print(TAG, e.toString());
} catch (InvocationTargetException e) {
GdxLog.print(TAG, e.toString());
}
}
});
}
Важно, чтобы параметры не были примитивами, а наследовали Object. И плюс здесь упрощение с null параметром (только от класса String)
Как использовать LibGDX с другими виджетами
Через фрагмент. Больше информации в wiki
Пример:
И сам фрагмент:
Пример:
public class ActivityMain extends AppCompatActivity
implements AndroidFragmentApplication.Callbacks {
protected FragmentWorld fragmentWorld;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ...
getSupportFragmentManager()
.beginTransaction()
.add(R.id.world, fragmentWorld, FragmentWorld.class.getSimpleName())
.commitAllowingStateLoss();
}
@Override
public void exit() {}
И сам фрагмент:
public class FragmentWorld extends AndroidFragmentApplication {
public World world;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
int worldWidth = getResources().getDimensionPixelSize(R.dimen.world_width);
int worldHeight = getResources().getDimensionPixelSize(R.dimen.world_height);
world = new World(BuildConfig.DEBUG, worldWidth, worldHeight);
return initializeForView(world);
}
}
Как вытащить рендер мира или Pixmap в Bitmap
Я не хотел сохранять pixmap в файл и потом средствами Android вытаскивать Bitmap
Поэтому придумал такой лайфхак с OutputStream классом. Работает прекрасно и не требует медленных r/w операций
Поэтому придумал такой лайфхак с OutputStream классом. Работает прекрасно и не требует медленных r/w операций
final Pixmap pixmap = getScreenshot();
Observable.fromCallable(new Callable <Boolean> () {
@Override
public Boolean call() throws Exception {
PixmapIO.PNG writer = new PixmapIO.PNG((int)(pixmap.getWidth() * pixmap.getHeight() * 1.5 f));
writer.setFlipY(false);
ByteArrayOutputStream output = new ByteArrayOutputStream();
try {
writer.write(output, pixmap);
} finally {
StreamUtils.closeQuietly(output);
writer.dispose();
pixmap.dispose();
}
byte[] bytes = output.toByteArray();
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
return true;
}
}).subscribeOn(Schedulers.io()).subscribe();
Как работать с colors совместно с Android
На самом деле можно int, наверное, (Color класс в LibGDX довольно специфичен как и вся графика, по-моему мнению, т.е. нужно разбираться на уровне битов) но для простоты я предпочел hex хранить и передавать в String
Соотвественно понадобился парсер:
Пример параметра «ffffff»
Соотвественно понадобился парсер:
protected Color parseColor(String hex) {
String s1 = hex.substring(0, 2);
int v1 = Integer.parseInt(s1, 16);
float f1 = 1 f * v1 / 255 f;
String s2 = hex.substring(2, 4);
int v2 = Integer.parseInt(s2, 16);
float f2 = 1 f * v2 / 255 f;
String s3 = hex.substring(4, 6);
int v3 = Integer.parseInt(s3, 16);
float f3 = 1 f * v3 / 255 f;
return new Color(f1, f2, f3, 1 f);
}
Пример параметра «ffffff»
Как определить нажатие на actorsticker.addListener() Все проще. Есть метод hit у сцены
Sticker sticker = (Sticker) stickersStage.hit(coordinates.x, coordinates.y, false);
Математика scale и rotation на событие pinch (два пальца)
Возможно это не лучшее решение, но работает хорошо. Поворот без резких скачков, плавный зум
Единственно необходимо перед этим событием запоминать текущий зум и поворот
И на время события pinch актер неподвижен в этом случае
@Override
public boolean pinch(Vector2 initialPointer1, Vector2 initialPointer2, Vector2 pointer1,
Vector2 pointer2) {
// initialPointer doesn't change
// all vectors contains device coordinates
Sticker sticker = getCurrentSticker();
if (sticker == null) {
return false;
}
Vector2 startVector = new Vector2(initialPointer1).sub(initialPointer2);
Vector2 currentVector = new Vector2(pointer1).sub(pointer2);
sticker.setScale(sticker.startScale * currentVector.len() / startVector.len());
float startAngle = (float) Math.toDegrees(Math.atan2(startVector.x, startVector.y));
float endAngle = (float) Math.toDegrees(Math.atan2(currentVector.x, currentVector.y));
sticker.setRotation(sticker.startRotation + endAngle - startAngle);
return false;
}
Единственно необходимо перед этим событием запоминать текущий зум и поворот
@Override
public boolean touchDown(float x, float y, int pointer, int button) {
if (pointer == FIRST_FINGER) {
Vector2 coordinates = stickersStage.screenToStageCoordinates(new Vector2(x, y));
Sticker sticker = (Sticker) stickersStage.hit(coordinates.x, coordinates.y, false);
if (sticker != null) {
// здесь
sticker.setPinchStarts();
currentSticker = sticker.index;
}
}
return false;
}
@Override
public void pinchStop() {
Sticker sticker = getCurrentSticker();
if (sticker != null) {
// здесь
sticker.setPinchStarts();
}
}
И на время события pinch актер неподвижен в этом случае
Почему не происходит анимация, но action к актеру добавлен
Ключевой метод act у сцены. Боль, когда этого не знаешь)
spriteBatch.begin();
stickersStage.act();
stickersStage.getRoot().draw(spriteBatch, 1);
spriteBatch.end();
Градиент в LibGDX
Насколько я понял, можно задать только левый верхний и нижний правый цвета. При этом есть не задавать остальные (transparent), то между ними будет пробел. Т.е. остальные определяются как сумма этих двух цветов на данном расстоянии, если речь идет о линейном градиенте. Сказать, что своеобразно, ничего не сказать
gradientTopLeftColor = parseColor(topLeftColor);
gradientBottomRightColor = parseColor(bottomRightColor);
gradientBlendedColor = new Color(gradientTopLeftColor).add(gradientBottomRightColor);
Хитрости обработки движения актера (событие pan)
Вот этот обработчик
worldDensity это разница между перемещением пальца в экранных координатах и актера в игровых. Без этого параметра актер будет отрываться от пальца
И если сделать привязку touch input через sticker.addListener, то поступающие координаты будут относительного самого актера к текущему положению пальца. Лучше так не делать, потому что при малом размере актера (зум) он задергается и вылетит из сцены (как было у меня)
@Override
public boolean pan(float x, float y, float deltaX, float deltaY) {
if (currentSticker != Sticker.INDEX_NONE) {
Sticker sticker = getCurrentSticker();
if (sticker != null) {
sticker.moveBy(deltaX * worldDensity, -deltaY * worldDensity);
}
}
return false;
}
worldDensity это разница между перемещением пальца в экранных координатах и актера в игровых. Без этого параметра актер будет отрываться от пальца
@Override
public void resize(int width, int height) {
if (height > width) {
worldDensity = 1f * worldWidth / width;
} else {
worldDensity = 1f * worldHeight / height;
}
viewport.update(width, height, true);
}
И если сделать привязку touch input через sticker.addListener, то поступающие координаты будут относительного самого актера к текущему положению пальца. Лучше так не делать, потому что при малом размере актера (зум) он задергается и вылетит из сцены (как было у меня)
Как лучше работать с анимациями актеров (Action)
Использовать Pool класс. В моем проекте есть пример более детально реализации дополнительной
public void onAppear() {
ScaleToAction scaleToAction = scaleToPool.obtain();
scaleToAction.setPool(scaleToPool);
scaleToAction.setScale(startScale);
scaleToAction.setDuration(ANIMATION_TIME_APPEAR);
addAction(scaleToAction);
}
Наверное все, что из интересного. Сам не нашел решение проблемы увеличения viewport. Камера zoom помогает только с приближением сцены, и получается, что сцена сокращается больше чем надо (область видимости неизменная).
Другой вопрос это сохранение рендера мира. На выходе он соотвествует размеру экрана, но мне нужен определенный размер. Пробовал с framebuffer, но не получилось вытащить с него pixmap (присутствуют какие-то баги с инициализацией класса Texture)
Еще недостаток в движке, что не позволяет, например, полностью отключить ввод с клавиатуры. Получалось так, что он перехватывал фокус с другого виджета (но он на это и не рассчитан собственно, хотя было бы неплохо. Go pull request, одним словом)
Но в целом, все очень даже хорошо. Развивайся дальше LibGDX)
→ Ссылка на проект