[Android] Auto-Updater in game | Full Tutorial | Google Play Android 14

Hi everyone. Let me start by pointing out that I am a beginner and just learning.
I’ve been struggling with my project and many problems for a few weeks now - how to create an auto-updater in a mobile game? I succeeded! After many sleepless nights, clicking ADBs in the console and drinking a lot of coffee mixed with yerba-mate I created Auto - Updater directly in the game. And it works! Is it perfect? No. But the following tutorial I think will be perfect to show you the way from A to Z

What will be needed?
Unreal 5.4.3
Android Studio 2022.2.1 Patch 2
Java JDK 17
FTP Hosting
Google Play Console

Soooooo Let’s start!

  1. Open Android Studio and go to > Android SDK > check “Show Package Details” > Select:

    Android API 35

  • Android SDK Platform 35

  • Sources for Android 35

    Android API 34

  • Android SDK Platform 34

  1. Go to SDK Tools and select:
  • 34.0.0
  • 33.0.2
  • 33.0.1
  • 33.0.0
  • 32.0.0
  • 31.0.0
  • 30.0.3
  • 29.0.2
  • 28.0.3

  1. Go to Unreal Engine > Project Settings
  2. Uncheck Use Io Store
  3. Check Generate Chunks
  4. Build Configuration - Shipping
  5. Check - Full Rebuild (very important to clean cache!!!)
  6. Check - For Distribution

  1. Select package name and versions
  • Minimum SDK - 30
  • Target SDK - 34

  • Install Location - Auto
  • Package game data inside .apk? - Check (don’t worry, trust me)
  • Generate install files for all platforms - Uncheck
  • Disable verify OBB on first start/update. - Check
  • Force small OBB files.- Uncheck
  • Allow large OBB files. - Check
  • Allow patch OBB file.- Uncheck
  • Allow overflow OBB files.- Uncheck
  • Allow overflow OBB files.- Uncheck
  • Don’t bundle libraries into .apk for quicker iteration [Experimental] - Uncheck
  • Use ExternalFilesDir for UnrealGame files? - Check
  • Make log files always publicly accessible? - Check

  1. Go to Android SDK and select your SDK, NDK, JAVA location

  2. Back to Android and click Open Build Folder

  1. In this folder we need to create *jks file

image

How to make this file?
Open your CMD with Admin permission and paste:
keytool -genkeypair -v -keystore mykey.jks -alias myalias -keyalg RSA -keysize 2048 -storetype JKS -validity 10000
Press enter and set info:

  1. Go to the folder where we generated the file (in my case it is to system 32, my mistake, don’t do that) and move *jks file to Android folder.

  2. In Unreal go to Android > Distributin settings and set

  • Key Store (output of keytool, placed in /Build/Android) - mykey.jks (enter filename)
  • Key Alias (-alias parameter to keytool) -myalias
  • Key Store Password (-storepass parameter to keytool) - enter password from file
  • Key Password (leave blank to use Key Store Password) - enter password from file

Great! The android project is ready for compilation!

  1. Go to Your project folder > Source > Project_Name > and create xml file:

AndroidSanitizePermissions_UPL.xml
image

open with notepad and paste this:

<?xml version="1.0" encoding="utf-8"?>
<root xmlns:android="http://schemas.android.com/apk/res/android">
    <androidManifestUpdates>
        <removePermission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
        <removePermission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    </androidManifestUpdates>
</root>
  1. Open Build.cs
    Below “using UnrealBuildTool;” add:
using System.IO;

Below PublicDependencyModuleNames add:

PrivateDependencyModuleNames.AddRange(new string[] { "ChunkDownloader" });

		if(Target.Platform == UnrealTargetPlatform.Android)
		{
			string manifestFile = Path.Combine(ModuleDirectory, 
            "AndroidSanitizePermissions_UPL.xml");
			AdditionalPropertiesForReceipt.Add("AndroidPlugin", manifestFile);
		}

In PublicDependencyModuleNames add “HTTP”

Full code like this:

  1. Go to Yout Project > Config > DefaultEngine.ini and paste code:
[/Script/AndroidRuntimeSettings.AndroidRuntimeSettings]
+ObbFilters="-*pakchunk1*"
+ObbFilters="-*pakchunk2*"
+ObbFilters="-*pakchunk3*"
+ObbFilters="-*pakchunk4*"
+ObbFilters="-*pakchunk5*"
+ObbFilters="-*pakchunk6*"
+ObbFilters="-*pakchunk7*"
+ObbFilters="-*pakchunk8*"
+ObbFilters="-*pakchunk9*"
  1. Go toConfig folder and Open DefaultGame.ini and paste:
[/Script/Plugins.ChunkDownloader GamePatcher-Live]
+CdnBaseUrls="https://your_domain.com/PatchStaging/"

IMPORTANT!!! GamePatcher-Live - remember this name, we will need it later

  1. Go to your projecr folder > right click in .uproject file > Generate Visual Studio Prject Files > Open i VS > Build your project and Open your project

image

image

  1. Go to Unreal and Create GameInstance class
    image

In YourGameInstance.h under GENERATED_BODY() add:

public:
	// Overrides
	virtual void Init() override;
	virtual void Shutdown() override;

public:
	UFUNCTION(BlueprintPure, Category = "Patching|Stats")
	void GetLoadingProgress(int32& BytesDownloaded, int32& TotalBytesToDownload, float& DownloadPercent, int32& ChunksMounted, int32& TotalChunksToMount, float& MountPercent) const;

		 // Fired when the patching process succeeds or fails
	UPROPERTY(BlueprintAssignable, Category = "Patching");
	FPatchCompleteDelegate OnPatchComplete;

	// Starts the game patching process. Returns false if the patching manifest is not up to date. */
	UFUNCTION(BlueprintCallable, Category = "Patching")
	bool PatchGame();



protected:
	//Tracks if our local manifest file is up to date with the one hosted on our website
	bool bIsDownloadManifestUpToDate;

	//Called when the chunk download process finishes
	void OnManifestUpdateComplete(bool bSuccess);
	
	// List of Chunk IDs to try and download
	UPROPERTY(EditDefaultsOnly, Category = "Patching")
	TArray<int32> ChunkDownloadList;

	// Called when the chunk download process finishes
	void OnDownloadComplete(bool bSuccess);

	// Called whenever ChunkDownloader's loading mode is finished
	void OnLoadingModeComplete(bool bSuccess);

	// Called when ChunkDownloader finishes mounting chunks
	void OnMountComplete(bool bSuccess);

Go to In YourGameInstance.cpp and under

#include "YourGameInstance.h"

add:

#include "ChunkDownloader.h"
#include "Misc/CoreDelegates.h"

Paste this code, and change UYourGameInstance to Your name of Instance

void UYourGameInstance::Init()
{
    Super::Init();
    const FString DeploymentName = "GamePatcher-Live";
    const FString ContentBuildId = "GamePatcher-Live";

    // initialize the chunk downloader with chosen platform
    TSharedRef<FChunkDownloader> Downloader = FChunkDownloader::GetOrCreate();
    Downloader->Initialize("Android", 1);

    // load the cached build ID
    Downloader->LoadCachedBuild(DeploymentName);
    


    // update the build manifest file

    TFunction<void(bool bSuccess)> UpdateCompleteCallback = [&](bool bSuccess) 
        {
            //bIsDownloadManifestUpToDate = true; 
            bIsDownloadManifestUpToDate = bSuccess;
        };

    Downloader->UpdateBuild(DeploymentName, ContentBuildId, UpdateCompleteCallback);
    //GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, TEXT("called"));
}

void UYourGameInstance::Shutdown()
{
    Super::Shutdown();
    // Shut down ChunkDownloader
    FChunkDownloader::Shutdown();
}

void UYourGameInstance::GetLoadingProgress(int32& BytesDownloaded, int32& TotalBytesToDownload, float& DownloadPercent, int32& ChunksMounted, int32& TotalChunksToMount, float& MountPercent) const
{
    //Get a reference to ChunkDownloader
    TSharedRef<FChunkDownloader> Downloader = FChunkDownloader::GetChecked();

    //Get the loading stats struct
    FChunkDownloader::FStats LoadingStats = Downloader->GetLoadingStats();

    //Get the bytes downloaded and bytes to download
    BytesDownloaded = LoadingStats.BytesDownloaded;
    TotalBytesToDownload = LoadingStats.TotalBytesToDownload;

    //Get the number of chunks mounted and chunks to download
    ChunksMounted = LoadingStats.ChunksMounted;
    TotalChunksToMount = LoadingStats.TotalChunksToMount;

    //Calculate the download and mount percent using the above stats
    DownloadPercent = ((float)BytesDownloaded / (float)TotalBytesToDownload) * 100.0f;
    MountPercent = ((float)ChunksMounted / (float)TotalChunksToMount) * 100.0f;
}

bool UYourGameInstance::PatchGame()
{
    // make sure the download manifest is up to date
    if (bIsDownloadManifestUpToDate)
    {
        // get the chunk downloader
        TSharedRef<FChunkDownloader> Downloader = FChunkDownloader::GetChecked();

        // report current chunk status
        for (int32 ChunkID : ChunkDownloadList)
        {
            int32 ChunkStatus = static_cast<int32>(Downloader->GetChunkStatus(ChunkID));
            UE_LOG(LogTemp, Display, TEXT("Chunk %i status: %i"), ChunkID, ChunkStatus);
        }

        TFunction<void(bool bSuccess)> DownloadCompleteCallback = [&](bool bSuccess) {OnDownloadComplete(bSuccess); };
        Downloader->DownloadChunks(ChunkDownloadList, DownloadCompleteCallback, 1);

        // start loading mode
        TFunction<void(bool bSuccess)> LoadingModeCompleteCallback = [&](bool bSuccess) {OnLoadingModeComplete(bSuccess); };
        Downloader->BeginLoadingMode(LoadingModeCompleteCallback);
        return true;
    }

    // you couldn't contact the server to validate your Manifest, so you can't patch
    UE_LOG(LogTemp, Display, TEXT("Manifest Update Failed. Can't patch the game"));

    return false;
}

void UYourGameInstance::OnManifestUpdateComplete(bool bSuccess)
{
    bIsDownloadManifestUpToDate = bSuccess;
}

void UYourGameInstance::OnDownloadComplete(bool bSuccess)
{
    if (bSuccess)
    {
        UE_LOG(LogTemp, Display, TEXT("Download complete"));

        // get the chunk downloader
        TSharedRef<FChunkDownloader> Downloader = FChunkDownloader::GetChecked();
        FJsonSerializableArrayInt DownloadedChunks;

        for (int32 ChunkID : ChunkDownloadList)
        {
            DownloadedChunks.Add(ChunkID);
        }

        //Mount the chunks
        TFunction<void(bool bSuccess)> MountCompleteCallback = [&](bool bSuccess) {OnMountComplete(bSuccess); };
        Downloader->MountChunks(DownloadedChunks, MountCompleteCallback);

        OnPatchComplete.Broadcast(true);
    }
    else
    {
        UE_LOG(LogTemp, Display, TEXT("Load process failed"));

        // call the delegate
        OnPatchComplete.Broadcast(false);
    }
}

void UYourGameInstance::OnLoadingModeComplete(bool bSuccess)
{
    OnDownloadComplete(bSuccess);
}

void UYourGameInstance::OnMountComplete(bool bSuccess)
{
    OnPatchComplete.Broadcast(bSuccess);
}

We are mainly interested in these lines:

const FString DeploymentName = “GamePatcher-Live”; - Your deploy name
const FString ContentBuildId = “GamePatcher-Live”; - Your build ID
Downloader->Initialize(“Android”, 1); - Platform, and how many files you can downloading is same time

  1. Go to Unreal Editor and make folder with assets for auto-updating. In my case:
    image

  2. Create Data Asset and select Primary Asset Label


    image

  3. Priority set ABOVE 0 and select like my settings:

  4. Go to YourGameInstance > Right Click > Create Blueprint Class…
    image

  5. Open Your BP_YourGameInstance and add Index with Chunk ID


26. Go to Edit > Project Settings > Map and Modes > and Set Your BP_YourGameInstance

  1. Go to Content browser, create Game Mode and set in Map and Modes

26 Open your game mode and set blueprint like this:

  1. Create User Interface in Game Folder (not server content). We will need it to display the status of the FTP server download, so we create it in Game Content and not in Server Content.

Image - background
Progress bar - your status bar
2x textblock for check how many percent downloaded from ftp and megabits.

  1. Go to your main level > Open Level Blueprint and create Widget

  2. Set your main level in Map and Modes:


31. Go back to progress bar - Create binding
image

  1. Download Percent text:


    (in format text - {download} %)

  2. In download Text:


    (Format text - Downloading… {download.2f} MB / {Total.2f} MB)

Why divide by one million? because 1MB = 1 000 000 bytes.

  1. Go to Your Game Mode and you have to be creative now. Decide what should happen after the FTP data is correctly downloaded, for example, open a map

  2. Go to Platforms > Android > Package Your project

  3. Go to FTP and create folder PatchStaging
    image

  4. Inside PatchStaging create folder name from variable DeploymentName (GamePatcher-Live)
    folder and txt file ContentBuildId
    image

  5. In txt file enter folder name where game client will check content
    image

  6. go to Patcher folder and create “BuildManifest-Android.txt” and empty folder name Android

image
open empty BuildManifest-Android.txt and minimalize. We will back soon to this file.

  1. Go to [Project_Name]\Saved\StagedBuilds\Android_Multi\Project_Name\Content\Paks and copy-paste file to FTP (Android Folder)

  2. Go back to BuildManifest-Android.txt ane paste:

$NUM_ENTRIES = 1
$BUILD_ID = GamePatcher-Live
pakchunk1001-Android_Multi.pak	1240131	Version001	1001	/Android/pakchunk1001-Android_Multi.pak

$NUM_ENTRIES = 1 - how many pak you have in Android folder
$BUILD_ID - Your ID build drom c++ variable
pakchunk1001-Android_Multi.pak - name chunk file from Android FTP folder
1240131 - bytes from file without spaces!!!
Version001 - any variable name
1001 - ID Chunk
/Android/pakchunk1001-Android_Multi.pak - File location

All words are separated by tabs, not spaces!

How to test?
Method one: Connect your phone to PC via USB and install game

Method two:
Go to Google Play Console:
Application package explorer > Create new version > Upload AAB File
Next go to Internal Tests > Create new Version, select file from the previous step

Next Go to Internal Tests > Testers > Share link > open in new tab > download it on Google Play


Click Install

Open the installed application on your phone. You should see the bar loaded, above it how much % has been downloaded and below the bar the values in MB

In my case i have Open level after loading how the game will be downloaded from FTP and mounted:

4 Likes

Thank you for the experience , i will try it some day