📜 ⬆️ ⬇️

Libgdx: Loading Screen and Loading Encrypted Resources

Very often, mobile games require a loading screen, since resource loading can take a long time on various devices. In this article I will show my implementation of the loader and file encryption using the Libgdx engine.

Simple gif animation loader to attract attention

In many applications, the boot screen is used only at startup (or not at all), while all images that may be needed are loaded at once. This is convenient if your game is “Flappy Bird” and has several dozen pictures packed in an atlas 1024x1024 in size. If the application is large, such as “Cut The Rope”, then it will not be possible to download all the resources at once, and there is no need to download “extra” resources, since the user may not even see them.

Part 1. Loader


The scheme of the loader is simple and transparent:
  1. Add a screen that needs some resources.
  2. If resources are not loaded, add a bootloader screen on top of it.
  3. After downloading the resources, remove the loader

When implementing this algorithm, there are difficulties. Consider the solution below.
')
Handling clicks when resources are not loaded yet.
This problem is solved by the screen manager, and each screen has an isActive () method inside it, which returns false if the screen is not ready.
public boolean isTouched(Rectangle rect, int pointer) { if (isActive()) { return Gdx.input.isTouched(pointer) && rect.contains(Gdx.input.getX(pointer), Gdx.input.getY(pointer)); } return false; } public boolean isPressed(int key) { if (isActive()) { return Gdx.input.isKeyPressed(key); } return false; } 

The loader has a beautiful animation with him, and you need to remove it after it has been completed.
All the logic of the animated bootloader will be built on methods that will return true if they are completed and you can go to the next. stage.
 @Override public void assetsReady() { super.assetsReady(); //   } @Override protected boolean isShowedScreen() { //  true,     } @Override protected void updateProgress() { //    } @Override protected void onAssetsLoaded() { //     } @Override protected boolean isHidedScreen() { //   ,  true } 

The improved loader logic looks like this (an example with the replacement of a loaded screen with a new one):
  1. Add a second screen that needs some resources.
  2. If resources are not loaded, add a bootloader screen on top of it.
  3. Loader show animation is displayed, resources start to load simultaneously.
  4. After downloading the resources, we want to display the second screen already, so we delete the first screen.
  5. Loader hiding animation is displayed.
  6. Hide the loader and change the clicker to the second screen.

When deleting downloaded resources from memory, resources that are used by other screens are deleted.
When deleting the screen, you need to unload resources from the memory that are no longer used. For this purpose, this method serves (it will unload only those resources that are not in demand by other screens)
 public void unload(boolean checkDependencies) { loaded = false; if (checkDependencies) { for (CoreScreen screen : coreManager.screens) { BaseAsset<?>[] screenAssets = screen.getAssets(); for (BaseAsset<?> screenAsset : screenAssets) { if (screenAsset != this && name.equals(screenAsset.name)) { return; // neededByOtherAsset } } } } coreManager.resources.unload(name); } 

This loader allows the application to spend less resources, and reduces the user expectation, thereby making the application more convenient.

Part 2. Encryption


Sometimes it becomes necessary to encrypt resources. Of course, if the application can decrypt them, then the user can, but it will not be so easy, especially if you did not forget to use ProGuard when you release the application (to protect the data stored in it). In Libgdx, when working with the file system, an abstraction is necessary so that there is a similar behavior on different operating systems. When working with internal resources, you can transfer your implementation of FileResolver to the AssetManager constructor, which in turn will return its implementation of FileHandle, which will return its implementation of InputStream.
In my case, the logic is simple, and the wrapper for the InputStream replaces the main read () method.
 @Override public int read() throws IOException { int one; if ((one = inputStream.read()) != -1) { one = CryptUtil.proceedByte(one, position); ++position; } return one; } // CryptUtil.java private static final int[] CRYPT_VALS = { 13, 177, 24, 36, 222, 89, 85, 56 }; static int proceedByte(int data, long position) { return (data ^ CRYPT_VALS[(int) (position % CRYPT_VALS.length)]); } 

When reading from a file, we only perform an XOR operation on the received byte with the value from the array. Of course, this method can be complicated by hammering the beginning of the file with the numbers of the array, and initialize this array when it is first read.

Unfortunately, these are not all the places where you need to wrap a FileHandle. When loading atlases, the loadable dependencies are obtained in a different way, because for encrypted files we will add a new type, with the permission ".atlascrypt", and also inform AssetManager that this type has a new loader:
 public class CryptTextureAtlasLoader extends TextureAtlasLoader { public CryptTextureAtlasLoader(FileHandleResolver resolver) { super(resolver); } @SuppressWarnings("rawtypes") @Override public Array<AssetDescriptor> getDependencies(String fileName, FileHandle atlasFile, TextureAtlasParameter parameter) { Array<AssetDescriptor> dependencies = super.getDependencies(fileName, atlasFile, parameter); for (AssetDescriptor descriptor : dependencies) { if (!(descriptor.file instanceof CryptFileHandle)) { descriptor.file = new CryptFileHandle(descriptor.file); } } return dependencies; } } 


Conclusion


To demonstrate the performance recorded video:


Of course, all this text would be useless without source.
Available on the GitHub link.

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


All Articles