📜 ⬆️ ⬇️

Libgdx Practical questions and answers

image Hi Habr!

The contest from VKontakte and my 2-week marathon on the Internet for finding the necessary information ended. I want to share some experience with the graphics engine LibGDX. There are plenty of examples on the Internet, but most of them are far from practice (a drawn sprite is far from a game) or outdated.

Honestly, I liked it, because it easily integrates with android studio, it is written in a convenient language, it perfectly performs its graphical task. It's scary to think about using android graphics to solve this problem.

Questions that I had to solve:

Use option of the built-in logger
I often use Timber for convenience. It is not available in the core module, so I wrote a simple auxiliary class using the logger available in LibGDX, with which the application will stop writing logs in the release version
')
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); 

Plus, for convenience, different float values ​​of 1.23456789 are rounded

E / libEGL: call to OpenGL ES API with no current context (logged once per thread)
At first I was very upset by such a mistake, I could not understand anything. It happens because the GLSurfaceView graphics are rendered in their own separate stream when an outside attempt to interfere with this process occurs. How to fix:

 Gdx.app.postRunnable(new Runnable() { @Override public void run() { //      } }); 

The principle is the same as in the case view.postInvalidate ()

I am not a fan of anonymous classes, so I wrote such a simple method to shorten the code (otherwise it just became unreadable). Although with java 8 this is no longer such a problem, but of the additional advantages that InvocationTargetException is handled, when, for example, the file is not found, the application will not fall due to such a minor error.

 // 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()); } } }); } 

It is important that the parameters are not primitives, and inherit Object. And plus here is a simplification with a null parameter (only from the String class)

How to use LibGDX with other widgets
Through the fragment. More information on the wiki

Example:

 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() {} 

And the fragment itself:

 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); } } 


How to pull the render world or Pixmap in Bitmap
I did not want to save the pixmap to a file and then use Android tools to pull out a Bitmap
Therefore, I came up with such a life hacking with an OutputStream class. Works great and does not require slow r / w operations.

 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(); 


How to work with colors with Android
Actually, you can int, probably (the Color class in LibGDX is quite specific as the entire graphics, in my opinion, that is, you need to understand at the bit level), but for simplicity, I preferred to store and transfer the hex to String

Appropriately needed a parser:

 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); } 

Example of the "ffffff" parameter

How to determine the click on actor
sticker.addListener () Everything is easier. There is a hit method at the scene.

 Sticker sticker = (Sticker) stickersStage.hit(coordinates.x, coordinates.y, false); 


Math scale and rotation for pinch event (two fingers)
Perhaps this is not the best solution, but it works well. Rotate without sudden jumps, smooth zoom

 @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; } 

The only thing necessary before this event is to memorize the current zoom and rotation.

 @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(); } } 

And at the time of the pinch event, the actor is still in this case

Why does not the animation, but the action to the actor added
The key act method is in the scene. The pain when you do not know)

 spriteBatch.begin(); stickersStage.act(); stickersStage.getRoot().draw(spriteBatch, 1); spriteBatch.end(); 


Gradient in libgdx
As I understand it, you can only set the left upper and lower right colors. If there is no setting of the rest (transparent), then there will be a space between them. Those. the rest are defined as the sum of these two colors at a given distance, in the case of a linear gradient. To say that is peculiar, to say nothing

 gradientTopLeftColor = parseColor(topLeftColor); gradientBottomRightColor = parseColor(bottomRightColor); gradientBlendedColor = new Color(gradientTopLeftColor).add(gradientBottomRightColor); 


The tricks of handling the movement of an actor (event pan)
Here is the handler

 @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 is the difference between moving the finger in screen coordinates and the actor in the game. Without this parameter, the actor will break away from the finger.

 @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); } 

And if you bind the touch input through the sticker.addListener, then the incoming coordinates will be relative to the actor itself to the current position of the finger. It’s better not to do that, because with a small actor’s size (zoom), he will twitch and fly out of the scene (as I did)

How best to work with animations of actors (Action)
Use the Pool class . In my project there is an example of more detailed implementation of additional

 public void onAppear() { ScaleToAction scaleToAction = scaleToPool.obtain(); scaleToAction.setPool(scaleToPool); scaleToAction.setScale(startScale); scaleToAction.setDuration(ANIMATION_TIME_APPEAR); addAction(scaleToAction); } 


Probably all that interesting. I myself have not found a solution to the problem of increasing the viewport. The zoom camera helps only with the approach of the scene, and it turns out that the scene is reduced more than necessary (the scope is unchanged).

Another issue is the preservation of the rendering world. At the exit, it matches the screen size, but I need a certain size. I tried it with framebuffer, but I couldn’t get a pixmap from it (there are some bugs with the initialization of the Texture class)

Another drawback in the engine, which does not allow, for example, to completely disable keyboard input. It turned out that he intercepted the focus from another widget (but he wasn’t designed for this, although it would be nice. Go pull request, in one word)

But in general, everything is very good. Grow on LibGDX)

Link to project

Source: https://habr.com/ru/post/338398/


All Articles