ota
This commit is contained in:
@@ -297,7 +297,7 @@ public class LoginManager : MonoBehaviour
|
||||
public static void LogoutAndReturnToLogin()
|
||||
{
|
||||
Logout();
|
||||
SceneManager.LoadScene(0);
|
||||
AddressableSceneLoader.Load(AddressableSceneLoader.IntroScene);
|
||||
}
|
||||
|
||||
static void ProceedToMainMenu()
|
||||
@@ -305,7 +305,7 @@ public class LoginManager : MonoBehaviour
|
||||
if (LevelLoadManager.instance != null)
|
||||
LevelLoadManager.LoadLevel("MainMenu");
|
||||
else
|
||||
SceneManager.LoadScene("MainMenu");
|
||||
AddressableSceneLoader.Load(AddressableSceneLoader.MainMenuScene);
|
||||
}
|
||||
|
||||
void SetBusy(bool busy)
|
||||
|
||||
@@ -83,7 +83,7 @@ public class MainMenuManager : MonoBehaviour
|
||||
void Awake()
|
||||
{
|
||||
if(LoginManager.instance==null){
|
||||
SceneManager.LoadScene(0);
|
||||
AddressableSceneLoader.Load(AddressableSceneLoader.IntroScene);
|
||||
}
|
||||
instance = this;
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 26c7172a22b6197419c452a40e4b22f9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
using UnityEngine.ResourceManagement.ResourceProviders;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
public static class AddressableSceneLoader
|
||||
{
|
||||
public const string IntroScene = "Intro";
|
||||
public const string MainMenuScene = "MainMenu";
|
||||
public const string GameScene = "Game";
|
||||
|
||||
public static void Load(string sceneName)
|
||||
{
|
||||
AddressableSceneLoadRunner.Instance.Begin(sceneName);
|
||||
}
|
||||
|
||||
public static IEnumerator LoadRoutine(string sceneName)
|
||||
{
|
||||
AsyncOperationHandle<SceneInstance> handle = Addressables.LoadSceneAsync(sceneName, LoadSceneMode.Single);
|
||||
yield return handle;
|
||||
if (handle.Status != AsyncOperationStatus.Succeeded)
|
||||
Debug.LogError("Failed to load addressable scene '" + sceneName + "': " + handle.OperationException);
|
||||
}
|
||||
}
|
||||
|
||||
public class AddressableSceneLoadRunner : MonoBehaviour
|
||||
{
|
||||
static AddressableSceneLoadRunner instance;
|
||||
|
||||
public static AddressableSceneLoadRunner Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (instance != null)
|
||||
return instance;
|
||||
|
||||
var go = new GameObject(nameof(AddressableSceneLoadRunner));
|
||||
DontDestroyOnLoad(go);
|
||||
instance = go.AddComponent<AddressableSceneLoadRunner>();
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (instance != null && instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
|
||||
public void Begin(string sceneName)
|
||||
{
|
||||
StartCoroutine(AddressableSceneLoader.LoadRoutine(sceneName));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 14a07839b30f1f34ab2d48cb46340278
|
||||
@@ -0,0 +1,238 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using UnityEngine.AddressableAssets.ResourceLocators;
|
||||
using UnityEngine.Networking;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
public class BootstrapUpdater : MonoBehaviour
|
||||
{
|
||||
public const string ContentReadyPrefsKey = "Ota.ContentReady";
|
||||
|
||||
[Header("Host")]
|
||||
[Tooltip("Apache folder that serves version.json and the Addressables catalog. Example: https://kkapi.playpoolstudios.com/ota")]
|
||||
[SerializeField] string otaBaseUrl = "https://kkapi.playpoolstudios.com/ota";
|
||||
[Tooltip("Opened when version.json has no storeUrl. Same role as the login screen Telegram link.")]
|
||||
[SerializeField] string fallbackStoreUrl = "https://t.me/+dfUhy4gmGXk0ZDRl";
|
||||
|
||||
[Header("Content")]
|
||||
[SerializeField] string introSceneAddress = AddressableSceneLoader.IntroScene;
|
||||
[SerializeField] string gameContentLabel = "game-content";
|
||||
|
||||
[Header("UI")]
|
||||
[SerializeField] TMP_Text statusText;
|
||||
[SerializeField] Slider progressSlider;
|
||||
[SerializeField] TMP_Text progressText;
|
||||
[SerializeField] GameObject retryButton;
|
||||
[SerializeField] GameObject updateButton;
|
||||
|
||||
string pendingStoreUrl = "";
|
||||
bool updateRunning;
|
||||
|
||||
void Start()
|
||||
{
|
||||
SetRetryVisible(false);
|
||||
SetUpdateVisible(false);
|
||||
SetProgress(0f, "");
|
||||
if (!updateRunning)
|
||||
StartCoroutine(RunUpdate());
|
||||
}
|
||||
|
||||
public void Retry()
|
||||
{
|
||||
if (updateRunning)
|
||||
return;
|
||||
SetRetryVisible(false);
|
||||
SetUpdateVisible(false);
|
||||
StartCoroutine(RunUpdate());
|
||||
}
|
||||
|
||||
public void OpenStore()
|
||||
{
|
||||
string url = string.IsNullOrWhiteSpace(pendingStoreUrl) ? fallbackStoreUrl : pendingStoreUrl;
|
||||
if (!string.IsNullOrWhiteSpace(url))
|
||||
Application.OpenURL(url.Trim());
|
||||
}
|
||||
|
||||
IEnumerator RunUpdate()
|
||||
{
|
||||
updateRunning = true;
|
||||
SetStatus("Checking for updates");
|
||||
|
||||
OtaVersionInfo version = null;
|
||||
bool versionFetched = false;
|
||||
yield return FetchVersion(info =>
|
||||
{
|
||||
version = info;
|
||||
versionFetched = info != null;
|
||||
});
|
||||
|
||||
if (!versionFetched)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
Debug.LogWarning("OTA version.json was not reachable. Editor play continues into " + introSceneAddress + ".");
|
||||
#else
|
||||
if (PlayerPrefs.GetInt(ContentReadyPrefsKey, 0) != 1)
|
||||
{
|
||||
Fail("Check your connection and try again.");
|
||||
yield break;
|
||||
}
|
||||
SetStatus("Offline. Loading saved game");
|
||||
#endif
|
||||
}
|
||||
else if (OtaVersionInfo.IsClientBelowRequiredVersion(version.minPlayerVersion))
|
||||
{
|
||||
pendingStoreUrl = version.storeUrl;
|
||||
string message = string.IsNullOrWhiteSpace(version.message)
|
||||
? "A new version is required."
|
||||
: version.message.Trim();
|
||||
SetStatus(message);
|
||||
SetUpdateVisible(true);
|
||||
updateRunning = false;
|
||||
yield break;
|
||||
}
|
||||
|
||||
AsyncOperationHandle<IResourceLocator> init = Addressables.InitializeAsync(false);
|
||||
yield return init;
|
||||
bool initOk = init.IsValid() && init.Status == AsyncOperationStatus.Succeeded;
|
||||
if (init.IsValid())
|
||||
Addressables.Release(init);
|
||||
if (!initOk)
|
||||
{
|
||||
Fail("Could not start content update.");
|
||||
yield break;
|
||||
}
|
||||
|
||||
AsyncOperationHandle<System.Collections.Generic.List<string>> check = Addressables.CheckForCatalogUpdates(false);
|
||||
yield return check;
|
||||
if (check.Status == AsyncOperationStatus.Succeeded && check.Result != null && check.Result.Count > 0)
|
||||
{
|
||||
SetStatus("Updating catalog");
|
||||
AsyncOperationHandle<System.Collections.Generic.List<IResourceLocator>> catalogs = Addressables.UpdateCatalogs(check.Result, false);
|
||||
yield return catalogs;
|
||||
if (catalogs.Status != AsyncOperationStatus.Succeeded)
|
||||
{
|
||||
Addressables.Release(catalogs);
|
||||
Addressables.Release(check);
|
||||
Fail("Could not update the content catalog.");
|
||||
yield break;
|
||||
}
|
||||
Addressables.Release(catalogs);
|
||||
}
|
||||
Addressables.Release(check);
|
||||
|
||||
long downloadBytes = 0;
|
||||
AsyncOperationHandle<long> sizeHandle = Addressables.GetDownloadSizeAsync(gameContentLabel);
|
||||
yield return sizeHandle;
|
||||
if (sizeHandle.Status == AsyncOperationStatus.Succeeded)
|
||||
downloadBytes = sizeHandle.Result;
|
||||
Addressables.Release(sizeHandle);
|
||||
|
||||
if (downloadBytes > 0)
|
||||
{
|
||||
AsyncOperationHandle download = Addressables.DownloadDependenciesAsync(gameContentLabel, false);
|
||||
while (!download.IsDone)
|
||||
{
|
||||
DownloadStatus status = download.GetDownloadStatus();
|
||||
float percent = status.TotalBytes > 0
|
||||
? (float)status.DownloadedBytes / status.TotalBytes
|
||||
: download.PercentComplete;
|
||||
SetProgress(percent, FormatMegabytes(status.DownloadedBytes) + " / " + FormatMegabytes(status.TotalBytes));
|
||||
SetStatus("Downloading update " + Mathf.RoundToInt(percent * 100f) + "%");
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (download.Status != AsyncOperationStatus.Succeeded)
|
||||
{
|
||||
Addressables.Release(download);
|
||||
Fail("Download failed. Check your connection and try again.");
|
||||
yield break;
|
||||
}
|
||||
Addressables.Release(download);
|
||||
}
|
||||
|
||||
PlayerPrefs.SetInt(ContentReadyPrefsKey, 1);
|
||||
PlayerPrefs.Save();
|
||||
SetStatus("Loading");
|
||||
SetProgress(1f, "");
|
||||
yield return AddressableSceneLoader.LoadRoutine(introSceneAddress);
|
||||
updateRunning = false;
|
||||
}
|
||||
|
||||
IEnumerator FetchVersion(Action<OtaVersionInfo> onResult)
|
||||
{
|
||||
string baseUrl = (otaBaseUrl ?? "").Trim().TrimEnd('/');
|
||||
if (string.IsNullOrEmpty(baseUrl))
|
||||
{
|
||||
onResult?.Invoke(null);
|
||||
yield break;
|
||||
}
|
||||
|
||||
string url = baseUrl + "/version.json";
|
||||
using (var request = UnityWebRequest.Get(url))
|
||||
{
|
||||
request.timeout = 20;
|
||||
yield return request.SendWebRequest();
|
||||
if (request.result != UnityWebRequest.Result.Success)
|
||||
{
|
||||
onResult?.Invoke(null);
|
||||
yield break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
onResult?.Invoke(JsonUtility.FromJson<OtaVersionInfo>(request.downloadHandler.text));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning("OTA version.json parse failed: " + e.Message);
|
||||
onResult?.Invoke(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Fail(string message)
|
||||
{
|
||||
SetStatus(message);
|
||||
SetRetryVisible(true);
|
||||
updateRunning = false;
|
||||
}
|
||||
|
||||
void SetStatus(string message)
|
||||
{
|
||||
if (statusText != null)
|
||||
statusText.text = message;
|
||||
}
|
||||
|
||||
void SetProgress(float normalized, string label)
|
||||
{
|
||||
if (progressSlider != null)
|
||||
{
|
||||
progressSlider.gameObject.SetActive(true);
|
||||
progressSlider.value = Mathf.Clamp01(normalized);
|
||||
}
|
||||
if (progressText != null)
|
||||
progressText.text = label ?? "";
|
||||
}
|
||||
|
||||
void SetRetryVisible(bool visible)
|
||||
{
|
||||
if (retryButton != null)
|
||||
retryButton.SetActive(visible);
|
||||
}
|
||||
|
||||
void SetUpdateVisible(bool visible)
|
||||
{
|
||||
if (updateButton != null)
|
||||
updateButton.SetActive(visible);
|
||||
}
|
||||
|
||||
static string FormatMegabytes(long bytes)
|
||||
{
|
||||
float mb = bytes / (1024f * 1024f);
|
||||
return mb.ToString("0.0", System.Globalization.CultureInfo.InvariantCulture) + " MB";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 095b27b1798839741b0d87da01566a79
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4a8936cb3c5731542a2e644e3ebd4b24
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,155 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEditor.AddressableAssets;
|
||||
using UnityEditor.AddressableAssets.Build;
|
||||
using UnityEditor.AddressableAssets.Settings;
|
||||
using UnityEditor.AddressableAssets.Settings.GroupSchemas;
|
||||
using UnityEngine;
|
||||
|
||||
public static class OtaAddressablesSetup
|
||||
{
|
||||
const string RemoteLoadUrl = "https://kkapi.playpoolstudios.com/ota/[BuildTarget]";
|
||||
const string GameContentLabel = "game-content";
|
||||
const string BootstrapScenePath = "Assets/Scenes/Bootstrap.unity";
|
||||
|
||||
static readonly string[] RemoteScenePaths =
|
||||
{
|
||||
"Assets/Scenes/Intro.unity",
|
||||
"Assets/Scenes/MainMenu.unity",
|
||||
"Assets/Scenes/Game.unity"
|
||||
};
|
||||
|
||||
[MenuItem("Tools/OTA/Configure Addressables")]
|
||||
public static void Configure()
|
||||
{
|
||||
AddressableAssetSettings settings = AddressableAssetSettingsDefaultObject.GetSettings(true);
|
||||
settings.BuildRemoteCatalog = true;
|
||||
settings.DisableCatalogUpdateOnStartup = true;
|
||||
settings.OverridePlayerVersion = "";
|
||||
|
||||
string profileId = settings.activeProfileId;
|
||||
settings.profileSettings.SetValue(profileId, AddressableAssetSettings.kRemoteBuildPath, "ServerData/[BuildTarget]");
|
||||
settings.profileSettings.SetValue(profileId, AddressableAssetSettings.kRemoteLoadPath, RemoteLoadUrl);
|
||||
settings.RemoteCatalogBuildPath.SetVariableByName(settings, AddressableAssetSettings.kRemoteBuildPath);
|
||||
settings.RemoteCatalogLoadPath.SetVariableByName(settings, AddressableAssetSettings.kRemoteLoadPath);
|
||||
|
||||
for (int i = 0; i < settings.DataBuilders.Count; i++)
|
||||
{
|
||||
if (settings.GetDataBuilder(i) != null && settings.GetDataBuilder(i).Name.Contains("Asset Database"))
|
||||
{
|
||||
settings.ActivePlayModeDataBuilderIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
AddressableAssetGroup local = GetOrCreateGroup(settings, "Local_Bootstrap");
|
||||
AddressableAssetGroup remote = GetOrCreateGroup(settings, "Remote_Game");
|
||||
ConfigureGroup(settings, local, true);
|
||||
ConfigureGroup(settings, remote, false);
|
||||
|
||||
MarkScene(settings, local, BootstrapScenePath, "Bootstrap", false);
|
||||
for (int i = 0; i < RemoteScenePaths.Length; i++)
|
||||
{
|
||||
string path = RemoteScenePaths[i];
|
||||
string address = Path.GetFileNameWithoutExtension(path);
|
||||
MarkScene(settings, remote, path, address, true);
|
||||
}
|
||||
|
||||
settings.SetDirty(AddressableAssetSettings.ModificationEvent.BatchModification, null, true, true);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
var scenes = new EditorBuildSettingsScene[1];
|
||||
scenes[0] = new EditorBuildSettingsScene(BootstrapScenePath, true);
|
||||
EditorBuildSettings.scenes = scenes;
|
||||
Debug.Log("OTA Addressables configured. Remote load path: " + RemoteLoadUrl);
|
||||
}
|
||||
|
||||
[MenuItem("Tools/OTA/Build Content")]
|
||||
public static void BuildContent()
|
||||
{
|
||||
Configure();
|
||||
AddressableAssetSettings.BuildPlayerContent(out AddressablesPlayerBuildResult result);
|
||||
if (!string.IsNullOrEmpty(result.Error))
|
||||
{
|
||||
Debug.LogError("OTA content build failed: " + result.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
string statePath = ContentUpdateScript.GetContentStateDataPath(false);
|
||||
Debug.Log("OTA content build finished. Keep this file for later patches: " + statePath);
|
||||
}
|
||||
|
||||
[MenuItem("Tools/OTA/Build Content Update")]
|
||||
public static void BuildContentUpdate()
|
||||
{
|
||||
AddressableAssetSettings settings = AddressableAssetSettingsDefaultObject.Settings;
|
||||
if (settings == null)
|
||||
{
|
||||
Debug.LogError("Addressables settings are missing. Run Tools/OTA/Configure Addressables first.");
|
||||
return;
|
||||
}
|
||||
|
||||
string statePath = ContentUpdateScript.GetContentStateDataPath(false);
|
||||
if (!File.Exists(statePath))
|
||||
{
|
||||
Debug.LogError("Missing content state at " + statePath + ". Run Tools/OTA/Build Content from the player build you shipped.");
|
||||
return;
|
||||
}
|
||||
|
||||
ContentUpdateScript.BuildContentUpdate(settings, statePath);
|
||||
Debug.Log("OTA content update built. Upload ServerData and keep " + statePath);
|
||||
}
|
||||
|
||||
static AddressableAssetGroup GetOrCreateGroup(AddressableAssetSettings settings, string name)
|
||||
{
|
||||
AddressableAssetGroup group = settings.FindGroup(name);
|
||||
if (group != null)
|
||||
return group;
|
||||
|
||||
return settings.CreateGroup(name, false, false, false, null, typeof(BundledAssetGroupSchema), typeof(ContentUpdateGroupSchema));
|
||||
}
|
||||
|
||||
static void ConfigureGroup(AddressableAssetSettings settings, AddressableAssetGroup group, bool local)
|
||||
{
|
||||
BundledAssetGroupSchema bundle = group.GetSchema<BundledAssetGroupSchema>();
|
||||
if (bundle == null)
|
||||
bundle = group.AddSchema<BundledAssetGroupSchema>();
|
||||
|
||||
if (local)
|
||||
{
|
||||
bundle.BuildPath.SetVariableByName(settings, AddressableAssetSettings.kLocalBuildPath);
|
||||
bundle.LoadPath.SetVariableByName(settings, AddressableAssetSettings.kLocalLoadPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
bundle.BuildPath.SetVariableByName(settings, AddressableAssetSettings.kRemoteBuildPath);
|
||||
bundle.LoadPath.SetVariableByName(settings, AddressableAssetSettings.kRemoteLoadPath);
|
||||
}
|
||||
|
||||
bundle.BundleMode = BundledAssetGroupSchema.BundlePackingMode.PackTogether;
|
||||
bundle.UseAssetBundleCrc = true;
|
||||
bundle.UseAssetBundleCache = true;
|
||||
|
||||
ContentUpdateGroupSchema update = group.GetSchema<ContentUpdateGroupSchema>();
|
||||
if (update == null)
|
||||
update = group.AddSchema<ContentUpdateGroupSchema>();
|
||||
update.StaticContent = local;
|
||||
}
|
||||
|
||||
static void MarkScene(AddressableAssetSettings settings, AddressableAssetGroup group, string assetPath, string address, bool gameContent)
|
||||
{
|
||||
string guid = AssetDatabase.AssetPathToGUID(assetPath);
|
||||
if (string.IsNullOrEmpty(guid))
|
||||
{
|
||||
Debug.LogWarning("OTA scene missing, skipped: " + assetPath);
|
||||
return;
|
||||
}
|
||||
|
||||
AddressableAssetEntry entry = settings.CreateOrMoveEntry(guid, group, false, false);
|
||||
entry.address = address;
|
||||
if (gameContent)
|
||||
entry.SetLabel(GameContentLabel, true, true, true);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 563b5612b6e2b074e91bc5b86be879e2
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class OtaVersionInfo
|
||||
{
|
||||
public string minPlayerVersion = "";
|
||||
public string contentVersion = "";
|
||||
public string message = "";
|
||||
public string storeUrl = "";
|
||||
|
||||
public static bool IsClientBelowRequiredVersion(string requiredVersion)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(requiredVersion))
|
||||
return false;
|
||||
if (!TryParseVersionFloat(requiredVersion, out float required))
|
||||
return false;
|
||||
if (!TryParseVersionFloat(Application.version, out float client))
|
||||
return false;
|
||||
return client < required;
|
||||
}
|
||||
|
||||
public static bool TryParseVersionFloat(string version, out float value)
|
||||
{
|
||||
value = 0f;
|
||||
if (string.IsNullOrWhiteSpace(version))
|
||||
return false;
|
||||
return float.TryParse(version.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e1e50b8157291245a0f59a9d20a194f
|
||||
@@ -1,10 +1,9 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
public class SceneManagerUIHelper : MonoBehaviour
|
||||
{
|
||||
public void LoadScene(string sceneName){
|
||||
SceneManager.LoadScene(sceneName);
|
||||
AddressableSceneLoader.Load(sceneName);
|
||||
}
|
||||
|
||||
public void Quit(){
|
||||
|
||||
@@ -66,8 +66,7 @@ public class LevelLoadManager : MonoBehaviour
|
||||
if (mainMenuDestination)
|
||||
BlocksMainMenuSettingsBootstrap = true;
|
||||
|
||||
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(levelName);
|
||||
yield return asyncLoad;
|
||||
yield return AddressableSceneLoader.LoadRoutine(levelName);
|
||||
|
||||
if (mainMenuDestination)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user