Привет!
Закончился конкурс от ВКонтакте vk.com/wall-104669514_37 и мой 2-х недельный марафон в интернете по поиску нужной информации
Хочу поделится небольшим опытом работы с графическим движком LibGDX. В интернете полно примеров, но большинство далеки от практики (нарисованный спрайт это далеко еще не игра) или уже устарели.
Честно, мне он понравился, потому что легко интегрируется с android studio, написан на удобном мне языке, отлично выполняет свою графическую задачу. Даже страшно подумать об использовании android graphics для решения такой задачи.
Вопросы, которые мне приходилось решать:
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 округляются
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)
Пример:
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);
}
}
Поэтому придумал такой лайфхак с 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();
Соотвественно понадобился парсер:
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»
Sticker sticker = (Sticker) stickersStage.hit(coordinates.x, coordinates.y, false);
@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 актер неподвижен в этом случае
spriteBatch.begin();
stickersStage.act();
stickersStage.getRoot().draw(spriteBatch, 1);
spriteBatch.end();
gradientTopLeftColor = parseColor(topLeftColor);
gradientBottomRightColor = parseColor(bottomRightColor);
gradientBlendedColor = new Color(gradientTopLeftColor).add(gradientBottomRightColor);
@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, то поступающие координаты будут относительного самого актера к текущему положению пальца. Лучше так не делать, потому что при малом размере актера (зум) он задергается и вылетит из сцены (как было у меня)
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)
Ссылка на проект: github.com/androidovshchik/VKAdvancedPosting
Автор: Влад