chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
86
Assets/Editor/AddressablePreBuildCheck.cs
Normal file
86
Assets/Editor/AddressablePreBuildCheck.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using UnityEditor;
|
||||
using UnityEditor.Build;
|
||||
using UnityEditor.Build.Reporting;
|
||||
using UnityEngine;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
/// <summary>
|
||||
/// Pre-build check to ensure Addressables bundles are built and up-to-date
|
||||
/// before creating an Android build. Prevents "Invalid serialized file version" errors.
|
||||
/// </summary>
|
||||
public class AddressablePreBuildCheck : IPreprocessBuildWithReport
|
||||
{
|
||||
public int callbackOrder => -100; // Run early in the build process
|
||||
|
||||
public void OnPreprocessBuild(BuildReport report)
|
||||
{
|
||||
if (report.summary.platform != BuildTarget.Android)
|
||||
return;
|
||||
|
||||
CheckAddressablesBundles();
|
||||
}
|
||||
|
||||
private void CheckAddressablesBundles()
|
||||
{
|
||||
string bundlePath = "ServerData/Android";
|
||||
string fullBundlePath = Path.Combine(Application.dataPath, "..", bundlePath);
|
||||
|
||||
// Check if bundles directory exists and has content
|
||||
if (!Directory.Exists(fullBundlePath))
|
||||
{
|
||||
LogWarningAndPromptBuild("Addressables bundle directory not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
var bundles = Directory.GetFiles(fullBundlePath, "*.bundle");
|
||||
|
||||
if (bundles.Length == 0)
|
||||
{
|
||||
LogWarningAndPromptBuild("No Addressables bundles found!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check bundle age - warn if older than 1 day
|
||||
var oldestBundle = bundles
|
||||
.Select(f => new FileInfo(f))
|
||||
.OrderBy(f => f.LastWriteTime)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (oldestBundle != null)
|
||||
{
|
||||
var age = System.DateTime.Now - oldestBundle.LastWriteTime;
|
||||
|
||||
if (age.TotalHours > 24)
|
||||
{
|
||||
Debug.LogWarning($"⚠️ [Addressables Check] Bundles are {age.TotalHours:F1} hours old.");
|
||||
Debug.LogWarning($" Oldest bundle: {Path.GetFileName(oldestBundle.FullName)}");
|
||||
Debug.LogWarning($" Last modified: {oldestBundle.LastWriteTime}");
|
||||
Debug.LogWarning(" Consider rebuilding Addressables to ensure version compatibility.");
|
||||
Debug.LogWarning(" Go to: Window > Asset Management > Addressables > Groups > Build > New Build");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"✓ [Addressables Check] Bundles are recent ({age.TotalMinutes:F0} minutes old)");
|
||||
}
|
||||
}
|
||||
|
||||
// Check catalog files
|
||||
var catalogs = Directory.GetFiles(fullBundlePath, "catalog_*.bin");
|
||||
if (catalogs.Length == 0)
|
||||
{
|
||||
Debug.LogWarning("⚠️ [Addressables Check] No catalog files found! Addressables may not load properly.");
|
||||
}
|
||||
}
|
||||
|
||||
private void LogWarningAndPromptBuild(string message)
|
||||
{
|
||||
Debug.LogWarning($"⚠️ [Addressables Check] {message}");
|
||||
Debug.LogWarning(" Addressables content must be built before building the Android app.");
|
||||
Debug.LogWarning(" Go to: Window > Asset Management > Addressables > Groups > Build > New Build");
|
||||
Debug.LogWarning(" Or the app will fail to initialize with 'Invalid serialized file version' error!");
|
||||
|
||||
// Optionally stop the build - uncomment if you want to enforce this
|
||||
// throw new BuildFailedException("Addressables bundles not found. Please build Addressables first.");
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/AddressablePreBuildCheck.cs.meta
Normal file
2
Assets/Editor/AddressablePreBuildCheck.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b497cb2e406b1ffba3d66e65c7c757c
|
||||
8
Assets/Editor/AndroidTemplates.meta
Normal file
8
Assets/Editor/AndroidTemplates.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 26392c925a05ffd1a88d7d1b79ebdfd0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
214
Assets/Editor/AndroidTemplates/AppUIActivity.java.txt
Normal file
214
Assets/Editor/AndroidTemplates/AppUIActivity.java.txt
Normal file
@@ -0,0 +1,214 @@
|
||||
package com.unity3d.player.appui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
|
||||
import com.unity3d.player.UnityPlayer;
|
||||
import com.unity3d.player.UnityPlayerActivity;
|
||||
|
||||
public class AppUIActivity extends UnityPlayerActivity {
|
||||
|
||||
int m_CurrentUiMode = Configuration.UI_MODE_NIGHT_UNDEFINED;
|
||||
float m_CurrentFontScale = 1;
|
||||
float m_CurrentScaledDensity = 1;
|
||||
float m_CurrentDensity = 1;
|
||||
int m_CurrentDensityDpi = DisplayMetrics.DENSITY_DEFAULT;
|
||||
Vibrator m_Vibrator;
|
||||
|
||||
public boolean IsNightModeDefined() {
|
||||
return m_CurrentUiMode != Configuration.UI_MODE_NIGHT_UNDEFINED;
|
||||
}
|
||||
|
||||
public boolean IsNightModeEnabled() {
|
||||
return m_CurrentUiMode == Configuration.UI_MODE_NIGHT_YES;
|
||||
}
|
||||
|
||||
public float FontScale() {
|
||||
return m_CurrentFontScale;
|
||||
}
|
||||
|
||||
public float ScaledDensity() {
|
||||
return m_CurrentScaledDensity;
|
||||
}
|
||||
|
||||
public float Density() {
|
||||
return m_CurrentDensity;
|
||||
}
|
||||
|
||||
public int DensityDpi() {
|
||||
return m_CurrentDensityDpi;
|
||||
}
|
||||
|
||||
void Vibrate(long[] timings, int repeat) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
m_Vibrator.vibrate(VibrationEffect.createWaveform(timings, repeat));
|
||||
} else {
|
||||
m_Vibrator.vibrate(timings, repeat);
|
||||
}
|
||||
}
|
||||
|
||||
void Vibrate(long[] timings) {
|
||||
Vibrate(timings, -1);
|
||||
}
|
||||
|
||||
void Vibrate(long timing, int amplitude) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
m_Vibrator.vibrate(VibrationEffect.createOneShot(timing, amplitude));
|
||||
}
|
||||
else {
|
||||
m_Vibrator.vibrate(timing);
|
||||
}
|
||||
}
|
||||
|
||||
void Vibrate(long timing) {
|
||||
Vibrate(timing, -1);
|
||||
}
|
||||
|
||||
public void RunHapticFeedback(int hapticFeedbackType) {
|
||||
|
||||
switch (hapticFeedbackType) {
|
||||
case HapticFeedback.LIGHT: {
|
||||
Vibrate(HapticFeedback.lightTiming, HapticFeedback.lightAmplitude);
|
||||
break;
|
||||
}
|
||||
case HapticFeedback.MEDIUM: {
|
||||
Vibrate(HapticFeedback.mediumTiming, HapticFeedback.mediumAmplitude);
|
||||
break;
|
||||
}
|
||||
case HapticFeedback.HEAVY: {
|
||||
Vibrate(HapticFeedback.heavyTiming, HapticFeedback.heavyAmplitude);
|
||||
break;
|
||||
}
|
||||
case HapticFeedback.SUCCESS: {
|
||||
Vibrate(HapticFeedback.successTimings);
|
||||
break;
|
||||
}
|
||||
case HapticFeedback.ERROR: {
|
||||
Vibrate(HapticFeedback.errorTimings);
|
||||
break;
|
||||
}
|
||||
case HapticFeedback.WARNING: {
|
||||
Vibrate(HapticFeedback.warningTimings);
|
||||
break;
|
||||
}
|
||||
case HapticFeedback.SELECTION: {
|
||||
Vibrate(HapticFeedback.selectionTiming, HapticFeedback.lightAmplitude);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Resources resources = getResources();
|
||||
|
||||
Configuration configuration = resources.getConfiguration();
|
||||
|
||||
m_CurrentUiMode = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
m_CurrentFontScale = configuration.fontScale;
|
||||
|
||||
m_Vibrator = resolveVibrator();
|
||||
|
||||
DisplayMetrics displayMetrics = resources.getDisplayMetrics();
|
||||
m_CurrentScaledDensity = displayMetrics.scaledDensity;
|
||||
m_CurrentDensity = displayMetrics.density;
|
||||
m_CurrentDensityDpi = displayMetrics.densityDpi;
|
||||
|
||||
|
||||
Log.d("APP UI", "Initial Night Mode: " + m_CurrentUiMode);
|
||||
Log.d("APP UI", "Initial Font Scale: " + m_CurrentFontScale);
|
||||
Log.d("APP UI", "Initial Density: " + displayMetrics.density);
|
||||
Log.d("APP UI", "Initial Scaled Density: " + displayMetrics.scaledDensity);
|
||||
Log.d("APP UI", "Initial Density DPI: " + displayMetrics.densityDpi);
|
||||
}
|
||||
|
||||
private Vibrator resolveVibrator() {
|
||||
Vibrator fallback = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
try {
|
||||
Object manager = getSystemService("vibrator_manager");
|
||||
if (manager != null) {
|
||||
java.lang.reflect.Method method = manager.getClass().getMethod("getDefaultVibrator");
|
||||
Object result = method.invoke(manager);
|
||||
if (result instanceof Vibrator) {
|
||||
return (Vibrator) result;
|
||||
}
|
||||
}
|
||||
} catch (Throwable throwable) {
|
||||
Log.w("APP UI", "Unable to access VibratorManager", throwable);
|
||||
}
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
|
||||
boolean configurationChanged = false;
|
||||
|
||||
int currentNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
if (m_CurrentUiMode != currentNightMode) {
|
||||
m_CurrentUiMode = currentNightMode;
|
||||
configurationChanged = true;
|
||||
|
||||
switch (m_CurrentUiMode) {
|
||||
case Configuration.UI_MODE_NIGHT_NO:
|
||||
Log.d("APP UI", "Light Mode enabled");
|
||||
break;
|
||||
case Configuration.UI_MODE_NIGHT_YES:
|
||||
Log.d("APP UI", "Dark Mode enabled");
|
||||
break;
|
||||
case Configuration.UI_MODE_NIGHT_UNDEFINED:
|
||||
Log.d("APP UI", "Dark Mode is Undefined");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
float currentFontScale = newConfig.fontScale;
|
||||
if (m_CurrentFontScale != currentFontScale) {
|
||||
m_CurrentFontScale = currentFontScale;
|
||||
configurationChanged = true;
|
||||
Log.d("APP UI", "Changed Font Scale : " + m_CurrentFontScale);
|
||||
}
|
||||
|
||||
float currentScaledDensity = displayMetrics.scaledDensity;
|
||||
if (m_CurrentScaledDensity != currentScaledDensity) {
|
||||
m_CurrentScaledDensity = currentScaledDensity;
|
||||
configurationChanged = true;
|
||||
Log.d("APP UI", "Changed Scaled Density : " + m_CurrentScaledDensity);
|
||||
}
|
||||
|
||||
float currentDensity = displayMetrics.density;
|
||||
if (m_CurrentDensity != currentDensity) {
|
||||
m_CurrentDensity = currentDensity;
|
||||
configurationChanged = true;
|
||||
Log.d("APP UI", "Changed Density : " + m_CurrentDensity);
|
||||
}
|
||||
|
||||
int currentDensityDpi = newConfig.densityDpi;
|
||||
if (m_CurrentDensityDpi != currentDensityDpi) {
|
||||
m_CurrentDensityDpi = currentDensityDpi;
|
||||
configurationChanged = true;
|
||||
Log.d("APP UI", "Changed Density DPI : " + m_CurrentDensityDpi);
|
||||
}
|
||||
|
||||
if (configurationChanged) {
|
||||
UnityPlayer.UnitySendMessage("AppUIUpdater", "OnAndroidNativeMessageReceived", "configurationChanged");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9c5699037016407d5a4ec18d7f0f5f83
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
86
Assets/Editor/AppUIActivityVibratorPatch.cs
Normal file
86
Assets/Editor/AppUIActivityVibratorPatch.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DeviantMobile.Editor.Android
|
||||
{
|
||||
[InitializeOnLoad]
|
||||
internal static class AppUIActivityVibratorPatch
|
||||
{
|
||||
private const string PackageName = "com.unity.dt.app-ui";
|
||||
private const string RelativeJavaPath = "Runtime/Core/Platform/Android/Plugins/Android/AppUIActivity.java";
|
||||
private const string PackageAssetPath = "Packages/" + PackageName + "/" + RelativeJavaPath;
|
||||
|
||||
static AppUIActivityVibratorPatch()
|
||||
{
|
||||
EditorApplication.update += ApplyOnceWhenEditorReady;
|
||||
}
|
||||
|
||||
private static void ApplyOnceWhenEditorReady()
|
||||
{
|
||||
EditorApplication.update -= ApplyOnceWhenEditorReady;
|
||||
TryApplyPatch();
|
||||
}
|
||||
|
||||
[MenuItem("Tools/Android/App UI/Reapply Vibrator Compatibility Patch")]
|
||||
private static void ReapplyPatchFromMenu()
|
||||
{
|
||||
if (TryApplyPatch(force: true))
|
||||
{
|
||||
Debug.Log("Reapplied App UI vibrator compatibility patch.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("App UI vibrator compatibility patch could not be applied. See console for details.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryApplyPatch(bool force = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath("Packages/" + PackageName);
|
||||
if (packageInfo == null)
|
||||
{
|
||||
Debug.LogWarning($"Package '{PackageName}' could not be located. Skipping App UI vibrator patch.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var javaFilePath = Path.Combine(packageInfo.resolvedPath, RelativeJavaPath);
|
||||
if (!File.Exists(javaFilePath))
|
||||
{
|
||||
Debug.LogWarning($"AppUIActivity.java not found at '{javaFilePath}'.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentSource = File.ReadAllText(javaFilePath);
|
||||
if (!force && currentSource.Contains("Unable to access VibratorManager"))
|
||||
{
|
||||
return true; // already patched
|
||||
}
|
||||
|
||||
var templatePath = Path.Combine(Application.dataPath, "Editor/AndroidTemplates/AppUIActivity.java.txt");
|
||||
if (!File.Exists(templatePath))
|
||||
{
|
||||
Debug.LogWarning($"Template Java source missing at '{templatePath}'.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var patchedSource = File.ReadAllText(templatePath);
|
||||
File.WriteAllText(javaFilePath, patchedSource);
|
||||
|
||||
AssetDatabase.ImportAsset(PackageAssetPath);
|
||||
Debug.Log("Applied App UI vibrator compatibility patch.");
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
2
Assets/Editor/AppUIActivityVibratorPatch.cs.meta
Normal file
2
Assets/Editor/AppUIActivityVibratorPatch.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d0d69b3c420214735a5d14802dee4af9
|
||||
292
Assets/Editor/AssetBundleBuilder.cs
Normal file
292
Assets/Editor/AssetBundleBuilder.cs
Normal file
@@ -0,0 +1,292 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using CustomAssetBundleSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Unity Editor tool to build AssetBundles and generate catalog.json
|
||||
/// Menu: Tools > AssetBundles > Build for Android
|
||||
/// </summary>
|
||||
public class AssetBundleBuilder : EditorWindow
|
||||
{
|
||||
private static string outputPath = "ServerData/AssetBundles/Android";
|
||||
private static BuildTarget buildTarget = BuildTarget.Android;
|
||||
|
||||
[MenuItem("Tools/AssetBundles/Build for Android")]
|
||||
public static void BuildAssetBundles()
|
||||
{
|
||||
Debug.Log("=== Custom AssetBundle Builder ===");
|
||||
|
||||
// Create output directory
|
||||
string fullOutputPath = Path.Combine(Application.dataPath, "..", outputPath);
|
||||
if (!Directory.Exists(fullOutputPath))
|
||||
{
|
||||
Directory.CreateDirectory(fullOutputPath);
|
||||
Debug.Log($"Created output directory: {fullOutputPath}");
|
||||
}
|
||||
|
||||
// Build options
|
||||
BuildAssetBundleOptions options = BuildAssetBundleOptions.ChunkBasedCompression |
|
||||
BuildAssetBundleOptions.StrictMode;
|
||||
|
||||
// Build the bundles
|
||||
Debug.Log($"Building AssetBundles to: {fullOutputPath}");
|
||||
Debug.Log($"Target platform: {buildTarget}");
|
||||
|
||||
AssetBundleManifest manifest = BuildPipeline.BuildAssetBundles(
|
||||
fullOutputPath,
|
||||
options,
|
||||
buildTarget
|
||||
);
|
||||
|
||||
if (manifest == null)
|
||||
{
|
||||
Debug.LogError("✗ AssetBundle build failed!");
|
||||
EditorUtility.DisplayDialog("Build Failed", "AssetBundle build failed! Check console for errors.", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log("✓ AssetBundles built successfully!");
|
||||
|
||||
// Generate the JSON catalog
|
||||
GenerateCatalog(fullOutputPath, manifest);
|
||||
|
||||
Debug.Log("=== Build Complete ===");
|
||||
EditorUtility.DisplayDialog("Build Complete",
|
||||
$"AssetBundles built successfully!\n\nOutput: {fullOutputPath}\n\nNext: Upload to VPS",
|
||||
"OK");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate catalog.json from built AssetBundles
|
||||
/// </summary>
|
||||
private static void GenerateCatalog(string outputPath, AssetBundleManifest manifest)
|
||||
{
|
||||
Debug.Log("Generating catalog.json...");
|
||||
|
||||
var catalog = new RemoteAssetCatalog
|
||||
{
|
||||
version = Application.version,
|
||||
timestamp = System.DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||
baseUrl = "https://assetbundle.uncaptured.co",
|
||||
bundles = new List<RemoteAssetCatalog.BundleEntry>()
|
||||
};
|
||||
|
||||
// Get all bundle names
|
||||
string[] allBundles = manifest.GetAllAssetBundles();
|
||||
|
||||
foreach (string bundleName in allBundles)
|
||||
{
|
||||
string bundlePath = Path.Combine(outputPath, bundleName);
|
||||
|
||||
if (!File.Exists(bundlePath))
|
||||
{
|
||||
Debug.LogWarning($"Bundle file not found: {bundlePath}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get file info
|
||||
FileInfo fileInfo = new FileInfo(bundlePath);
|
||||
|
||||
// Calculate hash
|
||||
string hash = CalculateMD5(bundlePath);
|
||||
|
||||
// Get scenes in this bundle
|
||||
var scenePaths = AssetDatabase.GetAssetPathsFromAssetBundle(bundleName);
|
||||
var sceneNames = new List<string>();
|
||||
|
||||
foreach (var scenePath in scenePaths)
|
||||
{
|
||||
if (scenePath.EndsWith(".unity"))
|
||||
{
|
||||
string sceneName = Path.GetFileNameWithoutExtension(scenePath);
|
||||
sceneNames.Add(sceneName);
|
||||
}
|
||||
}
|
||||
|
||||
// Get dependencies
|
||||
var dependencies = new List<string>();
|
||||
string[] bundleDeps = manifest.GetAllDependencies(bundleName);
|
||||
dependencies.AddRange(bundleDeps);
|
||||
|
||||
// Create bundle entry
|
||||
var entry = new RemoteAssetCatalog.BundleEntry
|
||||
{
|
||||
bundleName = Path.GetFileNameWithoutExtension(bundleName),
|
||||
fileName = bundleName,
|
||||
hash = hash,
|
||||
size = fileInfo.Length,
|
||||
scenes = sceneNames,
|
||||
dependencies = dependencies
|
||||
};
|
||||
|
||||
catalog.bundles.Add(entry);
|
||||
|
||||
Debug.Log($" ✓ Added bundle: {bundleName}");
|
||||
Debug.Log($" - Size: {fileInfo.Length / 1024f / 1024f:F2} MB");
|
||||
Debug.Log($" - Hash: {hash}");
|
||||
Debug.Log($" - Scenes: {sceneNames.Count}");
|
||||
Debug.Log($" - Dependencies: {dependencies.Count}");
|
||||
}
|
||||
|
||||
// Save catalog as JSON
|
||||
string catalogPath = Path.Combine(outputPath, "catalog.json");
|
||||
string json = JsonUtility.ToJson(catalog, true);
|
||||
File.WriteAllText(catalogPath, json);
|
||||
|
||||
Debug.Log($"✓ Catalog generated: {catalogPath}");
|
||||
Debug.Log($" Total bundles: {catalog.bundles.Count}");
|
||||
Debug.Log($" Catalog size: {json.Length / 1024f:F2} KB");
|
||||
|
||||
// Also save a pretty-printed version for debugging
|
||||
string prettyPath = Path.Combine(outputPath, "catalog_readable.json");
|
||||
File.WriteAllText(prettyPath, json);
|
||||
Debug.Log($"✓ Readable catalog: {prettyPath}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculate MD5 hash of a file
|
||||
/// </summary>
|
||||
private static string CalculateMD5(string filePath)
|
||||
{
|
||||
using (var md5 = MD5.Create())
|
||||
using (var stream = File.OpenRead(filePath))
|
||||
{
|
||||
byte[] hash = md5.ComputeHash(stream);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
foreach (byte b in hash)
|
||||
{
|
||||
sb.Append(b.ToString("x2"));
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
[MenuItem("Tools/AssetBundles/Open Output Folder")]
|
||||
public static void OpenOutputFolder()
|
||||
{
|
||||
string fullPath = Path.Combine(Application.dataPath, "..", outputPath);
|
||||
if (Directory.Exists(fullPath))
|
||||
{
|
||||
EditorUtility.RevealInFinder(fullPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"Output folder doesn't exist: {fullPath}");
|
||||
}
|
||||
}
|
||||
|
||||
[MenuItem("Tools/AssetBundles/Clear Cache")]
|
||||
public static void ClearCache()
|
||||
{
|
||||
if (EditorUtility.DisplayDialog("Clear AssetBundle Cache?",
|
||||
"This will delete all cached AssetBundles. Continue?",
|
||||
"Yes", "Cancel"))
|
||||
{
|
||||
Caching.ClearCache();
|
||||
Debug.Log("✓ AssetBundle cache cleared");
|
||||
}
|
||||
}
|
||||
|
||||
[MenuItem("Tools/AssetBundles/Settings")]
|
||||
public static void ShowSettings()
|
||||
{
|
||||
var window = GetWindow<AssetBundleBuilder>("AssetBundle Builder");
|
||||
window.Show();
|
||||
}
|
||||
|
||||
// Editor Window GUI
|
||||
void OnGUI()
|
||||
{
|
||||
GUILayout.Label("Custom AssetBundle Builder", EditorStyles.boldLabel);
|
||||
GUILayout.Space(10);
|
||||
|
||||
EditorGUILayout.HelpBox(
|
||||
"This tool builds AssetBundles and generates a JSON catalog for remote loading.\n\n" +
|
||||
"1. Mark your scenes/assets with AssetBundle names\n" +
|
||||
"2. Click 'Build for Android'\n" +
|
||||
"3. Upload ServerData/AssetBundles/Android/ to your VPS",
|
||||
MessageType.Info);
|
||||
|
||||
GUILayout.Space(10);
|
||||
|
||||
// Output path
|
||||
GUILayout.Label("Output Path:", EditorStyles.boldLabel);
|
||||
outputPath = EditorGUILayout.TextField(outputPath);
|
||||
|
||||
GUILayout.Space(5);
|
||||
|
||||
// Build target
|
||||
GUILayout.Label("Build Target:", EditorStyles.boldLabel);
|
||||
buildTarget = (BuildTarget)EditorGUILayout.EnumPopup(buildTarget);
|
||||
|
||||
GUILayout.Space(10);
|
||||
|
||||
// Build button
|
||||
if (GUILayout.Button("Build AssetBundles", GUILayout.Height(40)))
|
||||
{
|
||||
BuildAssetBundles();
|
||||
}
|
||||
|
||||
GUILayout.Space(5);
|
||||
|
||||
// Utility buttons
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
if (GUILayout.Button("Open Output Folder"))
|
||||
{
|
||||
OpenOutputFolder();
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Clear Cache"))
|
||||
{
|
||||
ClearCache();
|
||||
}
|
||||
}
|
||||
|
||||
GUILayout.Space(20);
|
||||
|
||||
// Info section
|
||||
GUILayout.Label("Current Settings:", EditorStyles.boldLabel);
|
||||
EditorGUILayout.LabelField("Unity Version:", Application.unityVersion);
|
||||
EditorGUILayout.LabelField("App Version:", Application.version);
|
||||
EditorGUILayout.LabelField("Company:", Application.companyName);
|
||||
|
||||
GUILayout.Space(10);
|
||||
|
||||
// Quick guide
|
||||
if (GUILayout.Button("Show Quick Guide"))
|
||||
{
|
||||
ShowQuickGuide();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ShowQuickGuide()
|
||||
{
|
||||
EditorUtility.DisplayDialog("AssetBundle Quick Guide",
|
||||
"STEP 1: Mark Assets for Bundling\n" +
|
||||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" +
|
||||
"• Select your Game scene in Project window\n" +
|
||||
"• In Inspector, find 'AssetBundle' dropdown (bottom)\n" +
|
||||
"• Select 'New...' and name it 'gamescene'\n\n" +
|
||||
|
||||
"STEP 2: Build AssetBundles\n" +
|
||||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" +
|
||||
"• Tools → AssetBundles → Build for Android\n" +
|
||||
"• Wait for build to complete\n" +
|
||||
"• Files created in ServerData/AssetBundles/Android/\n\n" +
|
||||
|
||||
"STEP 3: Upload to VPS\n" +
|
||||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" +
|
||||
"• scp -r ServerData/AssetBundles/Android/* user@server:/var/www/AssetBundles/Android/\n\n" +
|
||||
|
||||
"STEP 4: Update Your Code\n" +
|
||||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" +
|
||||
"• Replace Addressables calls with CustomAssetBundleSceneLoader\n" +
|
||||
"• Build APK and test!",
|
||||
"Got it!");
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/AssetBundleBuilder.cs.meta
Normal file
2
Assets/Editor/AssetBundleBuilder.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8bb3b29337992a0e180b70df6883c57e
|
||||
2382
Assets/Editor/CharacterPrefabTransferTool.cs
Normal file
2382
Assets/Editor/CharacterPrefabTransferTool.cs
Normal file
File diff suppressed because it is too large
Load Diff
2
Assets/Editor/CharacterPrefabTransferTool.cs.meta
Normal file
2
Assets/Editor/CharacterPrefabTransferTool.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4316661c7d34ec384b8b9b9f0bb914a0
|
||||
128
Assets/Editor/ForceJsonCatalog.cs
Normal file
128
Assets/Editor/ForceJsonCatalog.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using UnityEditor;
|
||||
using UnityEditor.AddressableAssets;
|
||||
using UnityEditor.AddressableAssets.Settings;
|
||||
using UnityEngine;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
/// <summary>
|
||||
/// WORKAROUND for Unity 6 bug where m_EnableJsonCatalog is ignored
|
||||
/// This script patches settings.json to force JSON catalog loading
|
||||
/// </summary>
|
||||
public class ForceJsonCatalogWorkaround
|
||||
{
|
||||
[MenuItem("Addressables/Fix/Patch settings.json for JSON Catalogs")]
|
||||
public static void PatchSettingsForJsonCatalog()
|
||||
{
|
||||
Debug.Log("[ForceJsonCatalog] WORKAROUND: Patching settings.json to use .json catalogs instead of .bin");
|
||||
|
||||
// Find all settings.json files
|
||||
string[] settingsFiles = Directory.GetFiles("Library/com.unity.addressables", "settings.json", SearchOption.AllDirectories);
|
||||
|
||||
if (settingsFiles.Length == 0)
|
||||
{
|
||||
Debug.LogWarning("[ForceJsonCatalog] No settings.json found. Build Addressables content first!");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (string settingsPath in settingsFiles)
|
||||
{
|
||||
PatchSettingsFile(settingsPath);
|
||||
}
|
||||
|
||||
// Also patch ServerData if it exists
|
||||
string[] serverSettingsFiles = Directory.GetFiles("ServerData", "settings.json", SearchOption.AllDirectories);
|
||||
foreach (string settingsPath in serverSettingsFiles)
|
||||
{
|
||||
PatchSettingsFile(settingsPath);
|
||||
}
|
||||
|
||||
// Rename .bin catalogs to .json
|
||||
RenameBinCatalogsToJson();
|
||||
|
||||
Debug.Log("[ForceJsonCatalog] ✓ Patch complete! settings.json now references .json catalogs");
|
||||
Debug.Log("[ForceJsonCatalog] Next steps:");
|
||||
Debug.Log(" 1. Rebuild your APK");
|
||||
Debug.Log(" 2. Upload ServerData/Android/ contents to your server");
|
||||
Debug.Log(" 3. Test on device");
|
||||
}
|
||||
|
||||
private static void PatchSettingsFile(string settingsPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
string content = File.ReadAllText(settingsPath);
|
||||
|
||||
// Replace all references from .bin to .json in catalog URLs
|
||||
string patchedContent = content.Replace("catalog.bin", "catalog.json");
|
||||
patchedContent = patchedContent.Replace("catalog_2.0.0.bin", "catalog_2.0.0.json");
|
||||
|
||||
// Also patch the m_IsLocalCatalogInBundle if needed
|
||||
patchedContent = Regex.Replace(patchedContent,
|
||||
@"""m_IsLocalCatalogInBundle"":\s*true",
|
||||
"\"m_IsLocalCatalogInBundle\":false");
|
||||
|
||||
File.WriteAllText(settingsPath, patchedContent);
|
||||
Debug.Log($"[ForceJsonCatalog] ✓ Patched: {settingsPath}");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"[ForceJsonCatalog] Error patching {settingsPath}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RenameBinCatalogsToJson()
|
||||
{
|
||||
// Rename catalog.bin files to catalog.json
|
||||
string[] paths = new string[] { "Library/com.unity.addressables", "ServerData" };
|
||||
|
||||
foreach (string basePath in paths)
|
||||
{
|
||||
if (!Directory.Exists(basePath)) continue;
|
||||
|
||||
string[] binFiles = Directory.GetFiles(basePath, "catalog*.bin", SearchOption.AllDirectories);
|
||||
|
||||
foreach (string binFile in binFiles)
|
||||
{
|
||||
string jsonFile = binFile.Replace(".bin", ".json");
|
||||
|
||||
try
|
||||
{
|
||||
// Copy (don't delete .bin yet for safety)
|
||||
File.Copy(binFile, jsonFile, true);
|
||||
Debug.Log($"[ForceJsonCatalog] ✓ Created: {Path.GetFileName(jsonFile)}");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"[ForceJsonCatalog] Error renaming {binFile}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
[MenuItem("Addressables/Fix/Build + Patch (Complete Fix)")]
|
||||
public static void BuildAndPatchForJsonCatalog()
|
||||
{
|
||||
Debug.Log("[ForceJsonCatalog] Starting complete build with JSON catalog fix...");
|
||||
|
||||
// 1. Build Addressables normally
|
||||
Debug.Log("[ForceJsonCatalog] Step 1/3: Building Addressables content...");
|
||||
AddressableAssetSettings.BuildPlayerContent();
|
||||
|
||||
// 2. Patch settings.json files
|
||||
Debug.Log("[ForceJsonCatalog] Step 2/3: Patching settings.json files...");
|
||||
PatchSettingsForJsonCatalog();
|
||||
|
||||
// 3. Done
|
||||
Debug.Log("[ForceJsonCatalog] ============================================");
|
||||
Debug.Log("[ForceJsonCatalog] ✓ BUILD COMPLETE WITH JSON CATALOG FIX!");
|
||||
Debug.Log("[ForceJsonCatalog] ============================================");
|
||||
Debug.Log("[ForceJsonCatalog] Next steps:");
|
||||
Debug.Log("[ForceJsonCatalog] 1. Rebuild APK in Unity (File → Build Settings → Build)");
|
||||
Debug.Log("[ForceJsonCatalog] 2. Upload ServerData/Android/ to server");
|
||||
Debug.Log("[ForceJsonCatalog] 3. Install APK and test");
|
||||
Debug.Log("[ForceJsonCatalog] ============================================");
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/ForceJsonCatalog.cs.meta
Normal file
2
Assets/Editor/ForceJsonCatalog.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a75aa305a40bff91ab51e23adc8a79a2
|
||||
Reference in New Issue
Block a user