Libgdx: loading screen and downloading encrypted resources

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

    Simple gif animation downloader to attract attention

    In many applications, the loading screen is used only at startup (or is not used at all), while all the images that you may need are downloaded at once. This is convenient if your game is “Flappy Bird” and has several dozen pictures packed in an atlas of 1024x1024 size. If the application is large, such as “Cut The Rope”, then you won’t be able to download all the resources at once, and there’s no inappropriateness to download “extra” resources, as the user may not even see them.

    Part 1. Bootloader


    The bootloader operation scheme 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 loading resources, remove the bootloader

    When implementing this algorithm, difficulties arise. Let us consider their solution below.

    Handling clicks when resources are not already loaded.
    The screen manager solves this problem, and each screen has an isActive () method inside it, which will return 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 bootloader has a beautiful animation with it, and you need to remove it after it is completed.
    All the logic of the animated loader will be built on methods that will return true if they are completed and you can move on 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
    }
    

    Improved loader logic looks like this (example with replacing 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. An animation showing the bootloader is displayed, while the loading of resources begins
    4. After loading the resources, we want to display the second screen already, therefore we delete the first screen
    5. Displays bootloader hiding animation
    6. We hide the bootloader and change the click listener to the second screen

    When deleting loaded resources from memory, resources that are used by other screens are deleted.
    When deleting a screen, you need to unload from memory resources that are no longer in use. For this purpose, this method serves (it will unload only those resources that are not in demand by other screenshots)
    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 bootloader allows the application to consume less resources, and reduces the waiting time for the user, 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 simple, especially if you have not forgotten to use ProGuard when releasing the application (to protect the data stored in it). In Libgdx, when working with the file system, abstraction is needed so that on different operating systems there is a similar behavior. When working with internal resources, you can transfer your FileResolver implementation to the AssetManager constructor, which in turn will return its FileHandle implementation, which will return its InputStream implementation.
    In my case, the logic is simple, and the wrapper for 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 clogging the beginning of the file with the numbers of the array, and initialize this array on first reading.

    Unfortunately, these are not all the places where you need to wrap FileHandle. When loading atlases, the loaded dependencies are obtained in a different way, therefore 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 getDependencies(String fileName, FileHandle atlasFile, TextureAtlasParameter parameter) {
    		Array 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 operability, a video was recorded:


    Of course, all this text would be useless without source code.
    Available at GitHub .

    Also popular now: