├── .editorconfig ├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── CHANGELOG.md.meta ├── Editor.meta ├── Editor ├── AssetBundleBuilder.cs ├── AssetBundleBuilder.cs.meta ├── Synapse.AssetBundleBuilder.Editor.asmdef └── Synapse.AssetBundleBuilder.Editor.asmdef.meta ├── LICENSE ├── LICENSE.meta ├── README.md ├── README.md.meta ├── Runtime.meta ├── Runtime ├── AssetBundleDescription.cs ├── AssetBundleDescription.cs.meta ├── Json.meta ├── Json │ ├── AssetBundleDescriptionConverter.cs │ ├── AssetBundleDescriptionConverter.cs.meta │ ├── Synapse.AssetBundleBuilder.Runtime.Json.asmdef │ └── Synapse.AssetBundleBuilder.Runtime.Json.asmdef.meta ├── Synapse.AssetBundleBuilder.Runtime.asmdef └── Synapse.AssetBundleBuilder.Runtime.asmdef.meta ├── Tests.meta ├── Tests ├── Editor.meta └── Editor │ ├── JsonTests.cs │ ├── JsonTests.cs.meta │ ├── Unity.AssetBundleBuilder.Tests.Runtime.asmdef │ └── Unity.AssetBundleBuilder.Tests.Runtime.asmdef.meta ├── package.json └── package.json.meta /.editorconfig: -------------------------------------------------------------------------------- 1 | ############################### 2 | # Core EditorConfig Options # 3 | ############################### 4 | 5 | root = true 6 | 7 | # All files 8 | [*] 9 | indent_style = space 10 | 11 | # Code files 12 | [*.{cs,csx,vb,vbx}] 13 | indent_size = 4 14 | insert_final_newline = true 15 | charset = utf-8 16 | 17 | ############################### 18 | # .NET Coding Conventions # 19 | ############################### 20 | 21 | [*.{cs,vb}] 22 | # Organize usings 23 | dotnet_sort_system_directives_first = true 24 | dotnet_separate_import_directive_groups = false 25 | 26 | # this. preferences 27 | dotnet_style_qualification_for_field = false:silent 28 | dotnet_style_qualification_for_property = false:silent 29 | dotnet_style_qualification_for_method = false:silent 30 | dotnet_style_qualification_for_event = false:silent 31 | 32 | # Language keywords vs BCL types preferences 33 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 34 | dotnet_style_predefined_type_for_member_access = true:silent 35 | 36 | # Parentheses preferences 37 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 38 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 39 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 40 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 41 | 42 | # Modifier preferences 43 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 44 | dotnet_style_readonly_field = true:suggestion 45 | 46 | # Expression-level preferences 47 | dotnet_style_object_initializer = true:suggestion 48 | dotnet_style_collection_initializer = true:suggestion 49 | dotnet_style_explicit_tuple_names = true:suggestion 50 | dotnet_style_null_propagation = true:suggestion 51 | dotnet_style_coalesce_expression = true:suggestion 52 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent 53 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 54 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 55 | dotnet_style_prefer_auto_properties = true:silent 56 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 57 | dotnet_style_prefer_conditional_expression_over_return = true:silent 58 | 59 | ############################### 60 | # Naming Conventions # 61 | ############################### 62 | 63 | # Style Definitions 64 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 65 | 66 | # Use PascalCase for constant fields 67 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion 68 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields 69 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style 70 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 71 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = * 72 | dotnet_naming_symbols.constant_fields.required_modifiers = const 73 | 74 | ############################### 75 | # C# Code Style Rules # 76 | ############################### 77 | 78 | [*.cs] 79 | # var preferences 80 | csharp_style_var_for_built_in_types = true:silent 81 | csharp_style_var_when_type_is_apparent = true:silent 82 | csharp_style_var_elsewhere = true:silent 83 | 84 | # Expression-bodied members 85 | csharp_style_expression_bodied_methods = false:silent 86 | csharp_style_expression_bodied_constructors = false:silent 87 | csharp_style_expression_bodied_operators = false:silent 88 | csharp_style_expression_bodied_properties = true:silent 89 | csharp_style_expression_bodied_indexers = true:silent 90 | csharp_style_expression_bodied_accessors = true:silent 91 | 92 | # Pattern-matching preferences 93 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 94 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 95 | 96 | # Null-checking preferences 97 | csharp_style_throw_expression = true:suggestion 98 | csharp_style_conditional_delegate_call = true:suggestion 99 | 100 | # Modifier preferences 101 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 102 | 103 | # Expression-level preferences 104 | csharp_prefer_braces = true:silent 105 | csharp_style_deconstructed_variable_declaration = true:suggestion 106 | csharp_prefer_simple_default_expression = true:suggestion 107 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 108 | csharp_style_inlined_variable_declaration = true:suggestion 109 | 110 | ############################### 111 | # C# Formatting Rules # 112 | ############################### 113 | 114 | # New line preferences 115 | csharp_new_line_before_open_brace = all 116 | csharp_new_line_before_else = true 117 | csharp_new_line_before_catch = true 118 | csharp_new_line_before_finally = true 119 | csharp_new_line_before_members_in_object_initializers = true 120 | csharp_new_line_before_members_in_anonymous_types = true 121 | csharp_new_line_between_query_expression_clauses = true 122 | 123 | # Indentation preferences 124 | csharp_indent_case_contents = true 125 | csharp_indent_switch_labels = true 126 | csharp_indent_labels = flush_left 127 | 128 | # Space preferences 129 | csharp_space_after_cast = false 130 | csharp_space_after_keywords_in_control_flow_statements = true 131 | csharp_space_between_method_call_parameter_list_parentheses = false 132 | csharp_space_between_method_declaration_parameter_list_parentheses = false 133 | csharp_space_between_parentheses = false 134 | csharp_space_before_colon_in_inheritance_clause = true 135 | csharp_space_after_colon_in_inheritance_clause = true 136 | csharp_space_around_binary_operators = before_and_after 137 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 138 | csharp_space_between_method_call_name_and_opening_parenthesis = false 139 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 140 | csharp_space_after_comma = true 141 | csharp_space_after_dot = false 142 | 143 | # Wrapping preferences 144 | csharp_preserve_single_line_statements = true 145 | csharp_preserve_single_line_blocks = true 146 | 147 | ################################## 148 | # Visual Basic Code Style Rules # 149 | ################################## 150 | 151 | [*.vb] 152 | # Modifier preferences 153 | visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion 154 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | *.cs diff=csharp text 5 | *.cginc text 6 | *.shader text 7 | 8 | # Unity-generated text assets always use Unix-style line endings. 9 | *.mat eol=lf 10 | *.anim eol=lf 11 | *.unity eol=lf 12 | *.prefab eol=lf 13 | *.physicsMaterial2D eol=lf 14 | *.physicsMaterial eol=lf 15 | *.asset eol=lf 16 | *.meta eol=lf 17 | *.controller eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Ll]ibrary/ 2 | [Tt]emp/ 3 | [Oo]bj/ 4 | [Bb]uild/ 5 | [Bb]uilds/ 6 | Assets/AssetStoreTools* 7 | 8 | # Visual Studio cache directory 9 | .vs/ 10 | 11 | # Autogenerated VS/MD/Consulo solution and project files 12 | ExportedObj/ 13 | .consulo/ 14 | *.csproj 15 | *.unityproj 16 | *.sln 17 | *.suo 18 | *.tmp 19 | *.user 20 | *.userprefs 21 | *.pidb 22 | *.booproj 23 | *.svd 24 | *.pdb 25 | *.opendb 26 | *.VC.db 27 | 28 | # Unity3D generated meta files 29 | *.pidb.meta 30 | *.pdb.meta 31 | 32 | # Unity3D Generated File On Crash Reports 33 | sysinfo.txt 34 | 35 | # Builds 36 | *.apk 37 | *.unitypackage 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [v0.2.3] 11 | 12 | ### Fixed 13 | 14 | * Updated `isHttpError` and Linux `BuildTargets` to recommended uses in 2024 15 | 16 | ### Added 17 | 18 | * `PrepareBundlesForUpload()` can now use a short delay between requests 19 | * Simple retry system for requests in `PrepareBundlesForUpload()`. This allows differentiating between cases where the server could not be reached 20 | and where the bundle is not uploaded, throwing an exception in the former case, which helps resolve that ambiguity. Previously, if the server was unresponsive 21 | the asset bundle would be marked for upload but the actual upload would fail. 22 | * `PrepareBundlesForUpload()` logging 23 | 24 | ## [v0.2.2] 25 | 26 | ### Fixed 27 | 28 | * `NormalizePlatform()` no longer throws an exception for "unsupported" platforms. ([#7]) 29 | 30 | [#7]: https://github.com/kongregate/asset-bundle-builder/pull/7 31 | 32 | ## [v0.2.1] 33 | 34 | ### Added 35 | 36 | * `ManifestBundlePath`, `CurrentBuildPlatform`, and `GetManifestBundlePathForTarget()`. ([#6]) 37 | 38 | [#6]: https://github.com/kongregate/asset-bundle-builder/pull/6 39 | 40 | ## [v0.2.0] 41 | 42 | ### Added 43 | 44 | * `MergePlatformManifests()` as a more robust way of generating the `AssetBundleDescription` objects after building asset bundles. ([#4]) 45 | 46 | ### Fixed 47 | 48 | * Build asset bundles are now placed in folders based on their corresponding `RuntimePlatform`, rather than their `BuildTarget`. ([#4]) 49 | 50 | ### Breaking Changes 51 | 52 | * `AssetBundleBuilder.GenerateBundleDescriptions()` has been removed and replaced with `MergePlatformManifests()`. You will need to update your build process to provide the `AssetBundleManifest` for each platform to `MergePlatformManifests()`. This can be done by either directly providing the `AssetBundleManifest` asset for each platform, or providing the path to each platform's manifest bundle. See the "Building Bundle Descriptions" section of the README for more information. 53 | 54 | [#4]: https://github.com/kongregate/asset-bundle-builder/pull/4 55 | 56 | ## [v0.1.0] - 2019-11-08 57 | 58 | Initial release :tada: Provides basic workflow for building, distributing, and loading asset bundles. 59 | 60 | [Unreleased]: https://github.com/kongregate/asset-bundle-builder/compare/v0.2.3...master 61 | [v0.2.3]: https://github.com/kongregate/asset-bundle-builder/compare/v0.2.2...v0.2.3 62 | [v0.2.2]: https://github.com/kongregate/asset-bundle-builder/compare/v0.2.1...v0.2.2 63 | [v0.2.1]: https://github.com/kongregate/asset-bundle-builder/compare/v0.2.0...v0.2.1 64 | [v0.2.0]: https://github.com/kongregate/asset-bundle-builder/compare/v0.1.0...v0.2.0 65 | [v0.1.0]: https://github.com/kongregate/asset-bundle-builder/compare/56f87b9...v0.1.0 66 | -------------------------------------------------------------------------------- /CHANGELOG.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 14c84ee6e6e38674396e18daafd36168 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 73812214a50263c4badbf654c8d48633 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/AssetBundleBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using Unity.EditorCoroutines.Editor; 8 | using UnityEditor; 9 | using UnityEngine; 10 | using UnityEngine.Networking; 11 | 12 | namespace SynapseGames.AssetBundle 13 | { 14 | /// 15 | /// Functionality for building and preparing asset bundles. 16 | /// 17 | public static class AssetBundleBuilder 18 | { 19 | /// 20 | /// The path to the asset bundle build folder, relative to the root directory 21 | /// of the Unity project. 22 | /// 23 | public static readonly string RootBuildPath = "AssetBundles"; 24 | 25 | /// 26 | /// The path to the asset bundle staging directory, relative to the root 27 | /// directory of the Unity project. 28 | /// 29 | public static readonly string StagingArea = Path.Combine(RootBuildPath, "Staging"); 30 | 31 | /// 32 | /// The path to the asset bundle upload directory, relative to the root directory 33 | /// of the Unity project. 34 | /// 35 | public static readonly string UploadArea = Path.Combine(RootBuildPath, "Upload"); 36 | 37 | /// 38 | /// The path to the directory where embedded asset bundles are stored within the 39 | /// Unity project. 40 | /// 41 | /// 42 | /// 43 | /// This path is for use in the editor. For accessing embedded bundles at runtime, 44 | /// use . 45 | /// 46 | public static readonly string EmbeddedBundlePath = 47 | "Assets/StreamingAssets/EmbeddedAssetBundles"; 48 | 49 | /// 50 | /// The runtime platform that asset bundles will build for based on the active 51 | /// build target. 52 | /// 53 | public static RuntimePlatform CurrentBuildPlatform => 54 | GetPlatformForTarget(EditorUserBuildSettings.activeBuildTarget); 55 | 56 | /// 57 | /// The path to the manifest bundle for the currently-active build target. 58 | /// 59 | /// 60 | /// 61 | /// The manifest bundle is an additional bundle generated by the Unity build 62 | /// process that contains the asset generated 63 | /// as part of the asset bundle build process. The manifest bundle will not 64 | /// exist until after asset bundles have been built. 65 | /// 66 | public static string ManifestBundlePath => 67 | GetManifestBundlePathForTarget(EditorUserBuildSettings.activeBuildTarget); 68 | 69 | /// 70 | /// Builds asset bundles for the specified build target. 71 | /// 72 | /// 73 | /// 74 | /// The build target to build asset bundles for. Defaults to the active build 75 | /// target as specified by . 76 | /// Note that this will be normalized according to the rules specified by 77 | /// . 78 | /// 79 | /// 80 | /// 81 | /// Build options for the asset bundles. Defaults to . 82 | /// 83 | /// 84 | /// The asset bundle manifest for the built bundles. 85 | /// 86 | /// 87 | /// 88 | /// The resulting bundles will be output to a directory under 89 | /// named after . Due to build target normalization, 90 | /// this may not be the exact build target specified. 91 | /// 92 | /// 93 | /// 94 | /// After bundles are built, the set of bundles for the target platform are 95 | /// copied to the staging area and renamed according to the file naming 96 | /// convention. Note that the staging area is cleared at the beginning of the 97 | /// build, so after the build finishes it will only contain bundles for the 98 | /// specified platform. 99 | /// 100 | /// 101 | public static AssetBundleManifest BuildAssetBundles( 102 | BuildTarget? buildTarget = null, 103 | BuildAssetBundleOptions options = BuildAssetBundleOptions.None) 104 | { 105 | var target = buildTarget ?? EditorUserBuildSettings.activeBuildTarget; 106 | target = NormalizeBuildTarget(target); 107 | 108 | // Generate asset bundles in a directory named based on the build target. 109 | var buildDirectory = GetBuildPathForBuildTarget(target); 110 | Directory.CreateDirectory(buildDirectory); 111 | 112 | // Invoke Unity's asset bundle build process. 113 | var manifest = BuildPipeline.BuildAssetBundles(buildDirectory, options, target); 114 | 115 | // TODO: Delete any old bundles that have since been deleted from the project. 116 | 117 | // Clear out the staging area, if it already exists. This prevents a buildup 118 | // of old bundles over time. 119 | ResetDirectory(StagingArea); 120 | 121 | // Copy bundles to the staging area, renaming them based on the target 122 | // platform and asset hash. 123 | CopyBundlesToStagingArea(target, manifest); 124 | 125 | return manifest; 126 | } 127 | 128 | /// 129 | /// Builds asset bundles for all specified build targets. 130 | /// 131 | /// 132 | /// The set of build targets to build bundles for. 133 | /// 134 | /// 135 | /// Build options for the asset bundles. Defaults to . 136 | /// 137 | /// 138 | /// 139 | /// The set of manifests generated for each platform. Note that the keys of the 140 | /// dictionary are the normalized build targets, and so may not exactly match 141 | /// the list of targets in . 142 | /// 143 | /// 144 | /// 145 | /// Behavior is the same as , 146 | /// but will generate bundles for all specified platforms. The staging folder 147 | /// will be reset before the build, and will contain bundles for all specified 148 | /// platforms after the build finishes. 149 | /// 150 | public static Dictionary BuildAssetBundles( 151 | BuildTarget[] buildTargets, 152 | BuildAssetBundleOptions options = BuildAssetBundleOptions.None) 153 | { 154 | // Normalize the list of build targets by normalizing the individual targets 155 | // specified and then removing any duplicates. For example, if the user 156 | // specifies both StandaloneWindows and StandaloneWindows64, we only want to 157 | // generate output for StandaloneWindows once. 158 | buildTargets = buildTargets 159 | .Select(NormalizeBuildTarget) 160 | .Distinct() 161 | .ToArray(); 162 | 163 | // Clear out the staging area, if it already exists. This prevents a buildup 164 | // of old bundles over time. 165 | ResetDirectory(StagingArea); 166 | 167 | var manifests = new Dictionary(); 168 | foreach (var buildTarget in buildTargets) 169 | { 170 | var target = NormalizeBuildTarget(buildTarget); 171 | 172 | // Generate asset bundles in a directory named based on the build target. 173 | var buildDirectory = GetBuildPathForBuildTarget(target); 174 | Directory.CreateDirectory(buildDirectory); 175 | 176 | // Invoke Unity's asset bundle build process. 177 | var manifest = BuildPipeline.BuildAssetBundles(buildDirectory, options, target); 178 | 179 | // TODO: Delete any old bundles that have since been deleted from the project. 180 | 181 | // Copy bundles to the staging area, renaming them based on the target 182 | // platform and asset hash. 183 | CopyBundlesToStagingArea(buildTarget, manifest); 184 | 185 | manifests.Add(target, manifest); 186 | } 187 | 188 | return manifests; 189 | } 190 | 191 | /// 192 | /// Copies built bundles to StreamingAssets to be embedded in the built player. 193 | /// 194 | /// 195 | /// 196 | /// A list of bundle names to copy. These should be the names of the asset 197 | /// bundles as defined in the Unity project. Will generate a warning if any of 198 | /// the specified names are invalid. 199 | /// 200 | /// 201 | /// 202 | /// The build target to build asset bundles for. Defaults to the active build 203 | /// target as specified by . 204 | /// Note that this will be normalized according to the rules specified by 205 | /// . 206 | /// 207 | /// 208 | /// 209 | /// is reset each time this function is called. 210 | /// This is to ensure that asset bundles from another platform are not 211 | /// accidentally included when switching build targets. Only one platform's 212 | /// bundles may be embedded at a time since only one set of platform bundles 213 | /// will be valid for a built player. 214 | /// 215 | public static void CopyEmbeddedBundles( 216 | string[] embeddedBundles, 217 | BuildTarget? buildTarget = null) 218 | { 219 | var target = buildTarget ?? EditorUserBuildSettings.activeBuildTarget; 220 | target = NormalizeBuildTarget(target); 221 | 222 | var buildPath = GetBuildPathForBuildTarget(target); 223 | 224 | // Fully reset the embedded bundle path in order to clear out removed 225 | // bundles or bundles from other platforms. 226 | ResetDirectory(EmbeddedBundlePath); 227 | 228 | // Gather the set of valid bundle names in order to validate the names 229 | // passed by the user. 230 | var validBundleNames = new HashSet(AssetDatabase.GetAllAssetBundleNames()); 231 | 232 | foreach (var bundleName in embeddedBundles) 233 | { 234 | if (!validBundleNames.Contains(bundleName)) 235 | { 236 | Debug.LogWarning($"Unable to copy unknown embedded bundle {bundleName}"); 237 | continue; 238 | } 239 | 240 | var srcPath = Path.Combine(buildPath, bundleName); 241 | if (!File.Exists(srcPath)) 242 | { 243 | Debug.LogWarning( 244 | $"Asset bundle {bundleName} has not been built for target {target}, " + 245 | $"make sure to build bundles before calling CopyEmbeddedBundles()"); 246 | continue; 247 | } 248 | 249 | var destPath = Path.Combine(EmbeddedBundlePath, bundleName); 250 | File.Copy(srcPath, destPath, true); 251 | } 252 | } 253 | 254 | public static bool CheckForMissingBundles(BuildTarget[] targets, out string error) 255 | { 256 | StringBuilder log = new(); 257 | var filesInStaging = Directory.GetFiles(StagingArea); 258 | var assetBundles = AssetDatabase.GetAllAssetBundleNames(); 259 | 260 | foreach (var bundle in assetBundles) 261 | { 262 | foreach (var target in targets) 263 | { 264 | var platform = GetPlatformForTarget(target); 265 | var prefix = $"{bundle}_{platform}_"; 266 | if (!filesInStaging.Any(f => Path.GetFileName(f).StartsWith(prefix))) { 267 | log.AppendLine($"Missing {platform} for {bundle}"); 268 | } 269 | } 270 | } 271 | 272 | error = log.ToString(); 273 | return string.IsNullOrEmpty(error); 274 | } 275 | 276 | private const int PREPARE_FOR_UPLOAD_MAX_RETRIES = 5; 277 | private const float PREPARE_FOR_UPLOAD_DELAY = 0.1f; 278 | 279 | /// 280 | /// Checks to see which asset bundles are already hosted, and copies any 281 | /// new/updated bundles to a separate folder for easy upload. 282 | /// 283 | /// 284 | /// 285 | /// The address of the public server to check for the bundles. Server must be 286 | /// public (i.e. not require any authentication) and must accept HEAD requests. 287 | /// 288 | /// 289 | /// 290 | /// Add a short delay between HEAD requests in case the CDN can't handle 291 | /// so many successive requests. 292 | /// 293 | /// 294 | /// 295 | /// Retry the HEAD requests up to 5 times to confirm the bundle is not uploaded 296 | /// rather than the CDN could not be reached. If the retry count exceeds 5 for 297 | /// any bundle, will throw an 298 | /// 299 | /// 300 | /// 301 | /// An that will finish once all bundles have 302 | /// been copied. 303 | /// 304 | /// 305 | /// 306 | /// In order to detect which bundles have already been uploaded we make a HEAD 307 | /// request to where the file would be stored relative to . 308 | /// If the response code is 200 (or otherwise indicates success) then the bundle 309 | /// is considered to already be uploaded. If the response is 404 (or otherwise 310 | /// an error) then the bundle is considered not yet uploaded. 311 | /// 312 | public static void PrepareBundlesForUpload(string baseUri, bool addDelay = false, bool useRetries = true) 313 | { 314 | // Clear out the upload area, if it already exists. This prevents a buildup 315 | // of bundles over time, and ensures that after a build the upload area will 316 | // only contain the latest bundles that need to be uploaded. 317 | ResetDirectory(UploadArea); 318 | 319 | // Start all of the web requests at the same time and wait for them to complete. 320 | var bundleRoutines = Directory 321 | .GetFiles(StagingArea) 322 | .Select(PrepareBundle) 323 | .ToArray(); 324 | 325 | if (Application.isBatchMode) 326 | { 327 | // HACK: We have to block while waiting for the web requests to complete 328 | // otherwise this function doesn't work when running Unity from the command 329 | // line. When running with the -quit argument (which is necessary when doing 330 | // build automation on a remote server) Unity won't wait for coroutines to 331 | // complete before exiting, so the upload operations never complete. 332 | // 333 | // NOTE: We have to manually drive the coroutines to completion by calling 334 | // the MoveNext() method on each of them until they all are done. The 335 | // WaitForCoroutines() helper method handles this for us. At runtime Unity 336 | // would handle this for us, but it doesn't support blocking on coroutines 337 | // in the editor. 338 | while (WaitForCoroutines(bundleRoutines)) { } 339 | } 340 | else 341 | { 342 | // When running in the editor normally, we run the coroutines in the 343 | // background to avoid blocking the main thread. 344 | foreach (var coroutine in bundleRoutines) 345 | { 346 | EditorCoroutineUtility.StartCoroutineOwnerless(coroutine); 347 | } 348 | } 349 | 350 | IEnumerator PrepareBundle(string bundlePath) 351 | { 352 | var fileName = Path.GetFileName(bundlePath); 353 | var uri = $"{baseUri}/{fileName}"; 354 | var retries = 0; 355 | 356 | do { 357 | // Short wait between requests to avoid overloading the CDN 358 | if (addDelay) yield return new WaitForSeconds(PREPARE_FOR_UPLOAD_DELAY); 359 | 360 | // TODO: Execute network requests in parallel. This may be easier to 361 | // do with async/await than with coroutines. 362 | Debug.Log($"Checking if bundle is already uploaded: {uri}"); 363 | var request = UnityWebRequest.Head(uri); 364 | request.SendWebRequest(); 365 | 366 | // HACK: Manually poll the web request until it finishes. This is 367 | // necessary because yielding on the request doesn't work in the editor, 368 | // and so the coroutine will never resume after yielding. The web 369 | // request is still being processed in the background, though, so we can 370 | // manually poll the request to find out when it has finished. 371 | while (!request.isDone) 372 | { 373 | yield return null; 374 | } 375 | 376 | // Could not connect to server, try again 377 | if (request.result == UnityWebRequest.Result.ConnectionError) 378 | { 379 | retries++; 380 | continue; 381 | } 382 | 383 | // Not found, which means it's not uploaded yet 384 | if (request.result == UnityWebRequest.Result.ProtocolError || request.result == UnityWebRequest.Result.DataProcessingError) 385 | { 386 | var destPath = Path.Combine(UploadArea, fileName); 387 | File.Copy(bundlePath, destPath, true); 388 | } 389 | break; 390 | } while (useRetries && retries < PREPARE_FOR_UPLOAD_MAX_RETRIES); 391 | 392 | if (useRetries) { 393 | // Don't allow the ambiguous case where a bundle wasn't marked for upload because the CDN could not be reached 394 | if (retries == PREPARE_FOR_UPLOAD_MAX_RETRIES) { 395 | throw new Exception($"Could not connect to server while checking {uri}"); 396 | } else if (retries > 0) { 397 | Debug.LogWarning($"Had issues connecting to {uri}, succeeded after {retries} attempts"); 398 | } 399 | } 400 | } 401 | 402 | bool WaitForCoroutines(IEnumerator[] routines) 403 | { 404 | var workRemaining = false; 405 | foreach (var routine in routines) 406 | { 407 | workRemaining |= routine.MoveNext(); 408 | } 409 | 410 | return workRemaining; 411 | } 412 | } 413 | 414 | /// 415 | /// Generates the list of asset bundle descriptions from the set of 416 | /// platform-specific manifests. 417 | /// 418 | /// 419 | /// 420 | /// The paths to each of the platforms-specific manifest bundles. This should 421 | /// include a bundle for every platform you build bundles for in order to ensure 422 | /// that the generated objects contain 423 | /// correct information. Paths should be either an absolute path, or a relative 424 | /// path that is relative to the root folder of the Unity project. 425 | /// 426 | /// 427 | /// 428 | /// The bundle description for each of the bundles defined in the project. The 429 | /// bundle descriptions will include asset hashes for the platforms specified in 430 | /// . 431 | /// 432 | /// 433 | /// 434 | /// This function will handle the work of loading the manifest bundle for each 435 | /// specified platform, loading the manifest from the bundle, and then unloading 436 | /// the bundle to avoid loading conflicts. If you already have the bundle 437 | /// manifests loaded, you can instead use 438 | /// directly. 439 | /// 440 | public static AssetBundleDescription[] MergePlatformManifests( 441 | Dictionary paths) 442 | { 443 | var manifests = paths 444 | .Select(pair => (pair.Key, Value: ExtractManifest(pair.Value))) 445 | .ToDictionary(pair => pair.Key, pair => pair.Value); 446 | return MergePlatformManifests(manifests); 447 | 448 | AssetBundleManifest ExtractManifest(string path) 449 | { 450 | // Load the bundle from the file, skipping any files in the directory 451 | // that aren't valid asset bundles. 452 | var bundle = UnityEngine.AssetBundle.LoadFromFile(path); 453 | if (bundle == null) 454 | { 455 | throw new ArgumentException($"No bundle loaded from {path}"); 456 | } 457 | 458 | // Load the manifest from the bundle. 459 | var manifest = bundle.LoadAsset("assetbundlemanifest"); 460 | if (manifest == null) 461 | { 462 | throw new ArgumentException($"Failed to load AssetBundleManifest from bundle at path {path}"); 463 | } 464 | 465 | // Unload the bundle immediately without unloading the manifest that was 466 | // loaded from it. This prevents issues with trying to load the same 467 | // bundle multiple times, which can mostly come up when testing the 468 | // bundle build process locally. 469 | bundle.Unload(false); 470 | 471 | return manifest; 472 | } 473 | } 474 | 475 | /// 476 | /// Generates the list of asset bundle descriptions from the set of 477 | /// platform-specific manifests. 478 | /// 479 | /// 480 | /// 481 | /// The set of objects generated when 482 | /// building bundles. This should include a manifest for every platform you 483 | /// build bundles for in order to ensure that the generated 484 | /// objects contain correct information. 485 | /// 486 | /// 487 | /// 488 | /// The bundle description for each of the bundles defined in the project. The 489 | /// bundle descriptions will include asset hashes for the platforms specified in 490 | /// . 491 | /// 492 | public static AssetBundleDescription[] MergePlatformManifests( 493 | Dictionary manifests) 494 | { 495 | var descriptions = new Dictionary hashes, HashSet dependencies)>(); 496 | foreach (var keyValue in manifests) 497 | { 498 | var platform = keyValue.Key; 499 | var manifest = keyValue.Value; 500 | 501 | foreach (var bundleName in manifest.GetAllAssetBundles()) 502 | { 503 | // Get the existing description object for the current bundle, or 504 | // create a new one and add it to the description dictionary. 505 | if (!descriptions.TryGetValue(bundleName, out var description)) 506 | { 507 | // The first time we create the list of description objects, 508 | // populate the set of dependencies. 509 | var dependencies = new HashSet( 510 | manifest.GetDirectDependencies(bundleName)); 511 | 512 | description = (new Dictionary(), dependencies); 513 | descriptions.Add(bundleName, description); 514 | } 515 | 516 | // Set the hash for the current build target. 517 | description.hashes[platform] = manifest.GetAssetBundleHash(bundleName); 518 | } 519 | } 520 | 521 | return descriptions 522 | .Select(pair => new AssetBundleDescription(pair.Key, pair.Value.hashes, pair.Value.dependencies)) 523 | .ToArray(); 524 | } 525 | 526 | /// 527 | /// Gets the corresponding for the specified . 528 | /// 529 | public static RuntimePlatform GetPlatformForTarget(BuildTarget target) 530 | { 531 | switch (target) 532 | { 533 | case BuildTarget.StandaloneWindows: 534 | case BuildTarget.StandaloneWindows64: 535 | return RuntimePlatform.WindowsPlayer; 536 | 537 | case BuildTarget.StandaloneOSX: 538 | return RuntimePlatform.OSXPlayer; 539 | 540 | case BuildTarget.StandaloneLinux64: 541 | return RuntimePlatform.LinuxPlayer; 542 | 543 | case BuildTarget.Android: 544 | return RuntimePlatform.Android; 545 | 546 | case BuildTarget.iOS: 547 | return RuntimePlatform.IPhonePlayer; 548 | 549 | case BuildTarget.WebGL: 550 | return RuntimePlatform.WebGLPlayer; 551 | 552 | // TODO: Support for other platforms. 553 | 554 | default: 555 | throw new ArgumentException($"Cannot determine asset bundle target for unsupported build target {target}"); 556 | } 557 | } 558 | 559 | /// 560 | /// Normalizes the target and appends it to . 561 | /// 562 | private static string GetBuildPathForBuildTarget(BuildTarget target) 563 | { 564 | var platform = GetPlatformForTarget(NormalizeBuildTarget(target)); 565 | return Path.Combine(RootBuildPath, platform.ToString()); 566 | } 567 | 568 | /// 569 | /// Returns the normalized equivalent of the specified build target. 570 | /// 571 | /// 572 | /// 573 | /// There are a handful of cases where two or more variants of 574 | /// use the same asset bundles at runtime, e.g. 575 | /// and . In these cases, we define 576 | /// a single variant to be the "normalized" variant that is used when building 577 | /// bundles for all equivalent platforms. See the package documentation for the 578 | /// full set of conversions performed. 579 | /// 580 | public static BuildTarget NormalizeBuildTarget(BuildTarget target) 581 | { 582 | switch (target) 583 | { 584 | case BuildTarget.StandaloneWindows64: 585 | return BuildTarget.StandaloneWindows; 586 | 587 | case BuildTarget.StandaloneLinux64: 588 | return BuildTarget.StandaloneLinux64; 589 | 590 | default: 591 | return target; 592 | } 593 | } 594 | 595 | /// 596 | /// The path to the manifest bundle for the specified build target. 597 | /// 598 | /// 599 | /// 600 | /// The manifest bundle is an additional bundle generated by the Unity build 601 | /// process that contains the asset generated 602 | /// as part of the asset bundle build process. The manifest bundle will not 603 | /// exist until after asset bundles have been built for the specified platform. 604 | /// 605 | public static string GetManifestBundlePathForTarget(BuildTarget target) 606 | { 607 | target = NormalizeBuildTarget(target); 608 | var targetBuildPath = GetBuildPathForBuildTarget(target); 609 | var platformName = GetPlatformForTarget(target); 610 | return Path.Combine(targetBuildPath, platformName.ToString()); 611 | } 612 | 613 | /// 614 | /// Ensures that a directory exists and is empty. 615 | /// 616 | private static void ResetDirectory(string path) 617 | { 618 | if (Directory.Exists(path)) 619 | { 620 | Directory.Delete(path, true); 621 | } 622 | 623 | Directory.CreateDirectory(path); 624 | } 625 | 626 | /// 627 | /// Copies all bundles for the specified target to the staging area. 628 | /// 629 | /// 630 | /// 631 | /// The build target to use. Assumed to already be normalized. 632 | /// 633 | /// 634 | /// 635 | /// The asset bundle manifest resulting from building asset bundles for the 636 | /// specified target. 637 | /// 638 | private static void CopyBundlesToStagingArea( 639 | BuildTarget buildTarget, 640 | AssetBundleManifest manifest) 641 | { 642 | var buildDirectory = GetBuildPathForBuildTarget(buildTarget); 643 | var target = GetPlatformForTarget(buildTarget); 644 | 645 | foreach (var bundle in manifest.GetAllAssetBundles()) 646 | { 647 | var sourceFile = Path.Combine(buildDirectory, bundle); 648 | var fileName = AssetBundleDescription.BuildFileName( 649 | bundle, 650 | target, 651 | manifest.GetAssetBundleHash(bundle)); 652 | 653 | File.Copy( 654 | sourceFile, 655 | Path.Combine(StagingArea, fileName), 656 | true); 657 | } 658 | } 659 | } 660 | } -------------------------------------------------------------------------------- /Editor/AssetBundleBuilder.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 051bce8569c72e74192339d5eb806df1 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Synapse.AssetBundleBuilder.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Synapse.AssetBundleBuilder.Editor", 3 | "references": [ 4 | "Unity.EditorCoroutines.Editor", 5 | "Synapse.AssetBundleBuilder.Runtime" 6 | ], 7 | "optionalUnityReferences": [], 8 | "includePlatforms": [ 9 | "Editor" 10 | ], 11 | "excludePlatforms": [], 12 | "allowUnsafeCode": false, 13 | "overrideReferences": false, 14 | "precompiledReferences": [], 15 | "autoReferenced": true, 16 | "defineConstraints": [] 17 | } -------------------------------------------------------------------------------- /Editor/Synapse.AssetBundleBuilder.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6b7d3b67e29ae364e9610ecc683c66d7 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kongregate 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2421c1dc21342de45a3291740791c748 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Asset Bundle Builder 2 | 3 | This tool provides a workflow for managing and automating the process of building, uploading, and deploying asset bundles for Unity 3D games. It extends the built-in Unity tools be defining a convention for how bundles are named, where they are stored, how they are tracked, and how they are downloaded, such that it's easy to continually release updates to asset bundles for a live game. 4 | 5 | ## Motivation and Use Cases 6 | 7 | The goal of this system is to make it easy to continuously update and deploy asset bundles for a live game. The default naming convention and build process provided by Unity is generic and doesn't impose many restrictions on how you manage asset bundles for your game, but that also means that common usage patterns have to be re-invented for each new project. This package attempts to provide a streamlined process that covers some common use cases: 8 | 9 | - You want to iterate on and release updates to asset bundles without having to release an update to your game. 10 | - Your game has a server component which can provide your game client with an updated list of asset bundle definitions. 11 | - You would like to be able to automate the build and upload process for asset bundles as part of your CI process. 12 | - Storage for built bundles isn't an issue (i.e. you're using Amazon S3 or some other cloud storage service), but you would like to limit when new bundles are deployed to minimize how often players need to download updated bundles. 13 | 14 | > NOTE: If you are starting a new Unity project and are considering using asset-bundle-builder, consider using Unity's newer [Addressable Asset System](https://blogs.unity3d.com/2019/07/15/addressable-asset-system/) instead. This system is mainly intended for projects that are already using asset bundles and are heavily invested in the asset bundle workflow. 15 | 16 | ## Setup and Usage 17 | 18 | To include asset-bundle-builder as a Unity package, you'll need to be on Unity 2018.3 or later. Open `Packages/manifest.json` in your project and add "com.synapse-games.asset-bundle-builder" to the `dependencies` object: 19 | 20 | ```json 21 | { 22 | "dependencies": { 23 | "com.synapse-games.asset-bundle-builder": "https://github.com/kongregate/asset-bundle-builder.git#v0.2.3" 24 | } 25 | } 26 | ``` 27 | 28 | > NOTE: You'll need to have Git installed on your development machine for Unity to be able to download the dependency. See https://git-scm.com/ for more information. Alternatively, you can clone the project directly into your `Packages` folder in order to vendor the package with your project. 29 | 30 | > NOTE: If you're using an older version of Unity, you can still use bundle-builder by copying the contents of `Plugins` into your project's `Plugins` folder. 31 | 32 | ### Building and Uploading Bundles 33 | 34 | Asset bundles are defined as normal in Unity, i.e. by tagging assets as part of a bundle. To build bundles, invoke `AssetBundleBuilder.BuildAssetBundles()`. This will build asset bundles to a folder `AssetBundles/` in your project directory, then copy and rename the bundles into `AssetBundles/Staging`. The `Staging` directory will contain the current set of asset bundles for any built platforms. 35 | 36 | Once you have built all of your asset bundles, call `AssetBundleBuilder.PrepareBundlesForUpload()`, passing it the URL of the web server where you store your built bundles. This will check for the presence of each bundle, and copy any bundles that haven't already been uploaded to `AssetBundles/Upload`. You should then upload any bundles in `AssetBundles/Upload` to your storage server or CDN as appropriate. 37 | 38 | > NOTE: If you do not have your asset bundles in a publicly-accessible server, or are otherwise not able to check for the presence of bundles at build time, you can skip using `PrepareBundlesForUpload()`. In this case, you can upload bundles from `AssetBundles/Staging` directly, or perform any necessary custom logic to determine which bundles need to be uploaded. 39 | 40 | Diagram of the `AssetBundles` directory: 41 | 42 | ```txt 43 | AssetBundles/ 44 | ├── / 45 | │ └── 46 | ├── Staging/ 47 | │ └── 48 | └── Upload/ 49 | └── 50 | ``` 51 | 52 | ### Generating the Bundle Descriptions 53 | 54 | To generate the list of asset bundle descriptions used on the platform server, call `AssetBundleBuilder.GenerateBundleDescriptions()`. This will return a list of `AssetBundleDescription` objects which contain the information needed to load each bundle at runtime. This information needs to then be exported in some format that can be used by your game's server to tell your game's client which asset bundles to download. 55 | 56 | While this package doesn't enforce any specific format for this data, it does provide support for using Json.NET to serialize the bundle descriptions to a JSON file. To set this up, you'll need to do the following: 57 | 58 | * Add the [jillejr.newtonsoft.json-for-unity](https://www.npmjs.com/package/jillejr.newtonsoft.json-for-unity) package to your project. 59 | * Add a `JSON_NET` definition to your project's [custom scripts defines](https://docs.unity3d.com/Manual/PlatformDependentCompilation.html) in order to enable the Json.NET compatibility features in asset-bundle-builder. 60 | * Register the `AssetBundleDescriptionConverter` class when performing serialization/deserialization: 61 | 62 | ```c# 63 | var descriptions = AssetBundleBuilder 64 | .GenerateBundleDescriptions(new BuildTarget[] { ... }); 65 | 66 | var bundleJson = JsonConvert.SerializeObject( 67 | descriptions, 68 | Formatting.Indented, 69 | new AssetBundleDescriptionConverter()); 70 | 71 | File.WriteAllText( 72 | Path.Combine(AssetBundleBuilder.BundleOutputPath, "assetbundles.json"), 73 | bundleJson); 74 | ``` 75 | 76 | If not using JSON encoding (or not using Json.NET for the encoding), you will need to implement serialization for `AssetBundleDescription` using whatever system is appropriate for your game. 77 | 78 | ### Preparing Embedded Bundles 79 | 80 | If you would like to distribute any asset bundles with your client directly, you can use `AssetBundleBuilder.CopyEmbeddedBundles()` to copy a set of bundles to `StreamingAssets/EmbeddedAssetBundles`. Only asset bundles for the specified target platform (defaulting to the currently-selected platform) will be copied over, to avoid bloating the built player with unused files. 81 | 82 | Notes that you will have to manually rebuild asset bundles before calling `CopyEmbeddedBundles()`. Additionally, you'll need to make sure to build asset bundles and then call `CopyEmbeddedBundles()` before building your player in order to ensure that you are including the right version of any embedded bundles in your player. 83 | 84 | ### Downloading Bundles 85 | 86 | To determine which asset bundles need to be downloaded at runtime, your game client should load the generated bundle description list (usually as a JSON document). Once loaded, the bundle description list will define the full set of bundles available to the client, including the information needed to identify the current version of a bundle for a given platform. 87 | 88 | In general, the easiest way to determine the name of the file to download is the `FileNameForCurrentTarget` property. If a bundle was baked into your client build (i.e. by specifying it when calling `CopyEmbeddedBundles()`), you should instead use the `EmbeddedPath` property to get the full path to the embedded copy of the bundle. 89 | 90 | > NOTE: This package doesn't provided any utilities for determining the full URL of your hosted bundles, nor does it provide any specific utilities for downloading, loading, or managing your asset bundles at runtime. 91 | 92 | ## Additional Documentation 93 | 94 | This section more thoroughly explains the specifics of how asset-bundle-builder manages the asset bundles in your project. 95 | 96 | ### Asset Bundle File Naming 97 | 98 | The generated bundle files are named according to the pattern `{name}_{platform}_{hash}.unity3d`, where: 99 | 100 | * `{name}` is the bundle name, as defined in the Unity project. 101 | * `{platform}` is the target platform string, matching the normalized variant names for [`RuntimePlatform`](https://docs.unity3d.com/ScriptReference/RuntimePlatform.html) (see [Platform Support](#platform-support) below). 102 | * `{hash}` is the asset hash of the built bundle, determined when the bundle is built. 103 | 104 | This naming conventions serves a number of purposes: 105 | 106 | * Including the platform name in the file name allows the different platform-specific versions of a bundle to exist in the same folder. This simplifies the deployment of bundles and reduces complications when uploading/downloading bundles. 107 | * Including the hash in the file name allows the different historical versions of the same bundle to be stored in the same directory in the CDN, which is necessary to support iteration and re-deployment of new versions of an asset bundle over time. 108 | * Adding an explicit file extension makes the bundles play better with applications that expect all files to have extensions, and makes it easier for humans to tell what purpose the files serve. 109 | 110 | ### Platform Support 111 | 112 | Unity uses the `BuildTarget` enum when building asset bundles and the `RuntimePlatform` enum to specify the current platform at runtime. Unfortunately, neither of these accurately reflects the set of platform-specific asset bundles that need to be built. To address this, asset-bundle-builder uses a normalized subset of both `BuildTarget` and `RuntimePlatform` to identify the target platform for built asset bundles. 113 | 114 | When building asset bundles, the specified `BuildTarget` will be normalized to account for cases where Unity differentiates between build targets at build time but not runtime. Specifically, when building for Windows there are the `StandaloneWindows` and `StandaloneWindows64` targets, however both will use the `WindowsPlayer` runtime platform. As such, asset-bundle-builder will only ever use the `StandaloneWindows` target when building asset bundles. 115 | 116 | At runtime, the "Editor" variants for Windows, OSX, and Linux are normalized to the corresponding platform player variants, e.g. `WindowsEditor` will load bundles for `WindowsPlayer`. This is because the editor uses asset bundles for the corresponding platform's player. 117 | 118 | Additionally, obsolete build targets and runtime platforms are not supported. 119 | 120 | > NOTE: Not all platforms are currently supported, and there are some outstanding questions that need to be answered in order to properly handle asset bundles on all platforms. See [this thread on the Unity forums](https://forum.unity.com/threads/do-macos-and-windows-need-different-asset-bundles.670510/) for more details. 121 | 122 | ### Hosting Asset Bundles 123 | 124 | Built bundles are expected to be hosted in a single folder on a remote server or CDN. This includes both the live version of any given bundle as well as any unpublished versions still in development. At any given time, the full set of asset bundles generated from the project can be uploaded to your hosting server without risking existing bundles being overwritten or broken. 125 | 126 | > NOTE: This package doesn't strictly require or enforce this convention for hosting your asset bundles, but it does provide a default workflow to support it. As such, following this convention will ensure a smoother build and deployment process for your game's asset bundles. 127 | 128 | ### Bundle Description List 129 | 130 | This package supports generating a list of "bundle description" objects which specify the necessary information for loading your asset bundles at runtime. It also provides out-of-the-box functionality for converting this list to and from a JSON document for use at runtime. 131 | 132 | For each asset bundle defined in your project, the bundle description will contain: 133 | 134 | * The name of the bundle. 135 | * The set of supported platforms and the corresponding asset hash for each one. 136 | * The list of direct dependencies for each bundle. 137 | 138 | This is the information needed at runtime to determine which bundles your game's client should download and to determine the correct filename to download for a given bundle. The `AssetBundleDescription` class provides utilities for accessing this information. 139 | 140 | #### Building Bundle Descriptions 141 | 142 | When building bundle descriptions, you need to provide the [`AssetBundleManifest`](https://docs.unity3d.com/ScriptReference/AssetBundleManifest.html) for each platform that you built bundles for. There are two ways to get the manifest after building bundles: 143 | 144 | * `AssetBundleBuilder.BuildAssetBundles()` will return the manifest for each bundle built. If you are building all bundles at once using `BuildAssetBundles()`, you can pass the returned bundles directly into `MergePlatformManifests()`. 145 | * Load the manifest from the manifest bundle generated by Unity. When building asset bundles for a platform, Unity produces an additional asset bundle that only contains the `AssetBundleManifest` for the build. You can either load the manifest from the bundle directly, or you can pass `MergePlatformManifests()` the paths to the bundles and it will load the manifests for you. 146 | 147 | If you are building all of your asset bundles at the same time with a single call to `BuildAssetBundles()`, the first approach is recommended. 148 | 149 | When using a build automation server, though, it is common to have multiple platforms build in parallel with separate copies of the project. In this case, no one workspace will have access to all generated bundle manifests. To generated the asset bundle descriptions, you'll have to setup your build system to copy the manifest bundles to a single workspace after all bundles finish building, and have a single workspace generate the bundle descriptions in a single build step. 150 | 151 | #### JSON Conversion 152 | 153 | Written out to JSON using the provided Json.NET conversion it would looks like this: 154 | 155 | ```json 156 | [ 157 | { 158 | "name": "bundle-1", 159 | "hashes": { 160 | "Android": "84dd10474d639ecc804d8fa3088887a2", 161 | "iOS": "4c92bf45a5c9e0281254cc7ad07690e9" 162 | }, 163 | "dependencies": [] 164 | }, 165 | { 166 | "name": "cool-stuff", 167 | "hashes": { 168 | "Android": "301c62868060808f10b236e1de1dfaa8", 169 | "iOS": "90c94041de4ee425eb9ab23b9aa96021" 170 | }, 171 | "dependencies": ["bundle-1"] 172 | } 173 | ] 174 | ``` 175 | 176 | #### Differences From `AssetBundleManifest` 177 | 178 | Unity already provides the `AssetBundleManifest` asset to determine the hash and dependencies for each bundle, so why provide a separate system for providing that data at runtime? The primary reason for this is that `AssetBundleManifest` always represents the full set of asset bundles as contained in your Unity project when you build your bundles. This means that you can't choose when individual bundles are deployed: If you push the latest bundle manifest, all of your asset bundles are now live. Using a less opaque data format like JSON allows you to maintain a separate list of live bundles and manually choose when to move each bundle into production. Keeping information for all platforms in one file, rather than having a separate file per platform, further eases this process. 179 | 180 | ### Compatibility with Addressables 181 | 182 | In July of 2019 Unity moved their [Addressable Asset System](https://blogs.unity3d.com/2019/07/15/addressable-asset-system/) out of preview status. This system is designed to replace asset bundles, and seeks to address many of the same use cases as asset-bundle-builder. Unfortunately, this package is not currently compatible with addressables, and it is not clear if compatibility is possible. If you believe you have a solution for migrating projects using asset-bundle-builder to the addressable asset system, please [open an issue](https://github.com/kongregate/asset-bundle-builder/issues/new) with your suggestion so that we can discuss integrating that functionality in the package! 183 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4ed7c0d077e5ddc43bb2c53206b8dab2 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c245ecf7470356b49b97b38dfe189221 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/AssetBundleDescription.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using UnityEngine; 6 | 7 | namespace SynapseGames.AssetBundle 8 | { 9 | /// 10 | /// Data needed to load an asset bundle at runtime. 11 | /// 12 | /// 13 | /// 14 | /// This type is intended to be loaded from JSON (or another intermediate data 15 | /// format) at runtime and used to determine what asset bundles need to be loaded. 16 | /// It provides enough information to determine the file name for a given asset 17 | /// bundle for any supported platform. The values are generated at build time using 18 | /// 19 | /// 20 | public struct AssetBundleDescription : IEquatable 21 | { 22 | private Dictionary _hashes; 23 | private HashSet _dependencies; 24 | 25 | /// 26 | /// The name of the asset bundle, as specified in the Unity editor. 27 | /// 28 | public string Name { get; } 29 | 30 | /// 31 | /// The set of supported platforms and their corresponding asset hashes. 32 | /// 33 | public IReadOnlyDictionary Hashes => _hashes; 34 | 35 | public IReadOnlyCollection Dependencies => _dependencies; 36 | 37 | /// 38 | /// The file name for the current platform, if any. Will be null if there is no 39 | /// hash for the current platform. 40 | /// 41 | public string FileName => GetFileNameForPlatform(NormalizedPlatform); 42 | 43 | /// 44 | /// The asset hash for the current platform, if any. Will be null if there is 45 | /// no hash for the current platform. 46 | /// 47 | public Hash128? Hash => GetHashForPlatform(NormalizedPlatform); 48 | 49 | /// 50 | /// The path to the asset bundle when it is distributed as an embedded bundle. 51 | /// 52 | /// 53 | /// 54 | /// This path will be inside the streaming assets path, which may require 55 | /// platform-specific considerations when loading. This also does not guarantee 56 | /// that the bundle is actually embedded in the player, it only provides the 57 | /// path where the file would be stored if it were embedded. 58 | /// 59 | public string EmbeddedPath => Path.Combine( 60 | Application.streamingAssetsPath, 61 | "EmbeddedAssetBundles", 62 | Name); 63 | 64 | /// 65 | /// Initializes a new . 66 | /// 67 | /// 68 | /// 69 | /// The name of the asset bundle, as defined in the Unity project. 70 | /// 71 | /// 72 | /// 73 | /// The set of asset hashes for each supported platform. Only the specified 74 | /// platforms will be considered supported, such that the bundle cannot be 75 | /// loaded on any platforms not specified. 76 | /// 77 | public AssetBundleDescription( 78 | string name, 79 | Dictionary hashes, 80 | HashSet dependencies) 81 | { 82 | Name = name; 83 | _dependencies = dependencies; 84 | _hashes = hashes; 85 | } 86 | 87 | /// 88 | /// Initializes a new , copying the name 89 | /// and hashes from . 90 | /// 91 | public AssetBundleDescription(AssetBundleDescription other) 92 | : this(other.Name, other._hashes, other._dependencies) 93 | { 94 | } 95 | 96 | /// 97 | /// Gets the filename for the bundle on the specified target platform, if supported. 98 | /// 99 | /// 100 | /// 101 | /// The file name for the specified target platform, or null if no hash is 102 | /// present for that platform. 103 | /// 104 | /// 105 | /// 106 | /// Automatically normalizes with . 107 | /// 108 | public string GetFileNameForPlatform(RuntimePlatform platform) 109 | { 110 | platform = NormalizePlatform(platform); 111 | if (Hashes.TryGetValue(platform, out var hash)) 112 | { 113 | return BuildFileName(Name, platform, hash); 114 | } 115 | 116 | return null; 117 | } 118 | 119 | /// 120 | /// Returns the hash for the specified target platform, if any. 121 | /// 122 | /// 123 | /// 124 | /// The asset hash for the specified platform, or null if no hash is present 125 | /// for that platform. 126 | /// 127 | /// 128 | /// 129 | /// Automatically normalizes with . 130 | /// 131 | public Hash128? GetHashForPlatform(RuntimePlatform platform) 132 | { 133 | platform = NormalizePlatform(platform); 134 | if (Hashes.TryGetValue(platform, out var hash)) 135 | { 136 | return hash; 137 | } 138 | 139 | return null; 140 | } 141 | 142 | public override string ToString() 143 | { 144 | // TODO: Include more information in the string representation of the bundle. 145 | return $"{{ Name = {Name} }}"; 146 | } 147 | 148 | public override bool Equals(object obj) 149 | { 150 | if (obj is AssetBundleDescription other) 151 | { 152 | return Equals(other); 153 | } 154 | 155 | return false; 156 | } 157 | 158 | public override int GetHashCode() 159 | { 160 | int hash = Name.GetHashCode(); 161 | 162 | // NOTE: We iterate over the variants of RuntimePlatform, rather than 163 | // directly iterating over the contents of Hashes, in order to hash the 164 | // values in the dictionary in a deterministic order. 165 | var variants = Enum.GetValues(typeof(RuntimePlatform)).Cast(); 166 | foreach (var variant in variants) 167 | { 168 | Hash128 platformHash; 169 | if (Hashes.TryGetValue(variant, out platformHash)) 170 | { 171 | hash = (hash, variant, platformHash).GetHashCode(); 172 | } 173 | } 174 | 175 | return hash; 176 | } 177 | 178 | public bool Equals(AssetBundleDescription other) 179 | { 180 | if (Name != other.Name) 181 | { 182 | return false; 183 | } 184 | 185 | if (Hashes == other.Hashes) 186 | { 187 | return true; 188 | } 189 | 190 | if (Hashes.Count != other.Hashes.Count) 191 | { 192 | return false; 193 | } 194 | 195 | foreach (var pair in Hashes) 196 | { 197 | if (!other.Hashes.TryGetValue(pair.Key, out var otherHash) 198 | || otherHash != pair.Value) 199 | { 200 | return false; 201 | } 202 | } 203 | 204 | return _dependencies.SetEquals(other.Dependencies); 205 | } 206 | 207 | /// 208 | /// Returns the appropriate asset bundle target for the current platform. 209 | /// 210 | /// 211 | /// 212 | /// Uses to determine the current runtime platform. 213 | /// 214 | public static RuntimePlatform NormalizedPlatform => NormalizePlatform(Application.platform); 215 | 216 | /// 217 | /// Returns the appropriate asset bundle target for the specified platform. 218 | /// 219 | /// 220 | /// 221 | /// For editor platforms, this will return corresponding platform's player 222 | /// since the editor does not use different asset bundles from the 223 | /// corresponding platform. 224 | /// 225 | public static RuntimePlatform NormalizePlatform(RuntimePlatform platform) 226 | { 227 | switch (platform) 228 | { 229 | case RuntimePlatform.WindowsEditor: 230 | case RuntimePlatform.WindowsPlayer: 231 | return RuntimePlatform.WindowsPlayer; 232 | 233 | case RuntimePlatform.OSXEditor: 234 | case RuntimePlatform.OSXPlayer: 235 | return RuntimePlatform.OSXPlayer; 236 | 237 | case RuntimePlatform.LinuxEditor: 238 | case RuntimePlatform.LinuxPlayer: 239 | return RuntimePlatform.LinuxPlayer; 240 | 241 | default: return platform; 242 | } 243 | } 244 | 245 | /// 246 | /// Canonical method for determining the final file name of a built asset bundle. 247 | /// 248 | /// 249 | /// 250 | /// The name of the asset bundle, as configured in the editor. 251 | /// 252 | /// The platform the bundle was built for. 253 | /// The hash of the built bundle. 254 | /// 255 | /// 256 | /// The full file name for the asset bundle, including file extension. 257 | /// 258 | public static string BuildFileName( 259 | string bundleName, 260 | RuntimePlatform platform, 261 | Hash128 hash) 262 | { 263 | return $"{bundleName}_{platform}_{hash}.unity3d"; 264 | } 265 | 266 | public static bool operator ==(AssetBundleDescription left, AssetBundleDescription right) 267 | { 268 | return left.Equals(right); 269 | } 270 | 271 | public static bool operator !=(AssetBundleDescription left, AssetBundleDescription right) 272 | { 273 | return !left.Equals(right); 274 | } 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /Runtime/AssetBundleDescription.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0a515f78168bca84ebd389c20b9cb003 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 782788af2f0efb44dac75fbc3bb2742e 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/Json/AssetBundleDescriptionConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | using UnityEngine; 6 | 7 | namespace SynapseGames.AssetBundle.Json 8 | { 9 | public class AssetBundleDescriptionConverter : JsonConverter 10 | { 11 | public override AssetBundleDescription ReadJson( 12 | JsonReader reader, 13 | Type objectType, 14 | AssetBundleDescription existingValue, 15 | bool hasExistingValue, 16 | JsonSerializer serializer) 17 | { 18 | var jsonObject = JObject.Load(reader); 19 | 20 | var name = jsonObject.Value("name"); 21 | 22 | var hashes = new Dictionary(); 23 | foreach (var property in jsonObject.Value("hashes")) 24 | { 25 | if (!Enum.TryParse(property.Key, out RuntimePlatform platform)) 26 | { 27 | throw new JsonSerializationException($"Could not parse unknown asset bundle target {property.Key}"); 28 | } 29 | 30 | var hash = Hash128.Parse((string)property.Value); 31 | if (!hash.isValid) 32 | { 33 | throw new JsonSerializationException($"Invalid hash {property.Value} found in bundle {name} for platform {platform}"); 34 | } 35 | 36 | hashes.Add(platform, hash); 37 | } 38 | 39 | var dependencies = new HashSet(); 40 | serializer.Populate(jsonObject.Value("dependencies").CreateReader(), dependencies); 41 | 42 | return new AssetBundleDescription(name, hashes, dependencies); 43 | } 44 | 45 | public override void WriteJson( 46 | JsonWriter writer, 47 | AssetBundleDescription value, 48 | JsonSerializer serializer) 49 | { 50 | writer.WriteStartObject(); 51 | 52 | // Serialize the name of the bundle. 53 | writer.WritePropertyName("name"); 54 | writer.WriteValue(value.Name); 55 | 56 | // Serialize the set of platforms and hashes. 57 | writer.WritePropertyName("hashes"); 58 | writer.WriteStartObject(); 59 | foreach (var pair in value.Hashes) 60 | { 61 | writer.WritePropertyName(pair.Key.ToString()); 62 | writer.WriteValue(pair.Value.ToString()); 63 | } 64 | writer.WriteEndObject(); 65 | 66 | // Serialize the list of dependencies. 67 | writer.WritePropertyName("dependencies"); 68 | serializer.Serialize(writer, value.Dependencies); 69 | 70 | writer.WriteEndObject(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Runtime/Json/AssetBundleDescriptionConverter.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 82995a0215c66fc43bc944e3335a97b6 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Runtime/Json/Synapse.AssetBundleBuilder.Runtime.Json.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Synapse.AssetBundleBuilder.Runtime.Json", 3 | "references": [ 4 | "Synapse.AssetBundleBuilder.Runtime" 5 | ], 6 | "optionalUnityReferences": [], 7 | "includePlatforms": [], 8 | "excludePlatforms": [], 9 | "allowUnsafeCode": false, 10 | "overrideReferences": false, 11 | "precompiledReferences": [], 12 | "autoReferenced": true, 13 | "defineConstraints": [ 14 | "JSON_NET" 15 | ] 16 | } -------------------------------------------------------------------------------- /Runtime/Json/Synapse.AssetBundleBuilder.Runtime.Json.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 525006528faa72545b0b595168e2ebd6 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime/Synapse.AssetBundleBuilder.Runtime.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Synapse.AssetBundleBuilder.Runtime", 3 | "references": [], 4 | "optionalUnityReferences": [], 5 | "includePlatforms": [], 6 | "excludePlatforms": [], 7 | "allowUnsafeCode": false, 8 | "overrideReferences": false, 9 | "precompiledReferences": [], 10 | "autoReferenced": true, 11 | "defineConstraints": [] 12 | } -------------------------------------------------------------------------------- /Runtime/Synapse.AssetBundleBuilder.Runtime.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d1dabd9245c7da04bac1d452b8c88b97 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Tests.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b53f4eec91007d5438b2381df1a224c1 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 42f838b7e85aa0549adbbfd7b15e4e0d 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/Editor/JsonTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | using NUnit.Framework; 5 | using SynapseGames.AssetBundle; 6 | using SynapseGames.AssetBundle.Json; 7 | using UnityEngine; 8 | 9 | public class JsonTests 10 | { 11 | public static readonly string SampleJson = @"[ 12 | { 13 | ""name"": ""bundle-1"", 14 | ""hashes"": { 15 | ""Android"": ""982415458cdaf4e60c420f75fe6c8e8b"", 16 | ""IPhonePlayer"": ""bd7a32acc8931e77eb1174baff409f21"", 17 | ""WindowsPlayer"": ""5425682f11f1eeb1b0ac7e4580cd2237"", 18 | ""OSXPlayer"": ""4a8250d93de151328fe1651096ec8bc1"", 19 | ""WebGLPlayer"": ""bc341e351d4813630d417d33558a51ed"" 20 | }, 21 | ""dependencies"": [] 22 | }, 23 | { 24 | ""name"": ""bundle-10"", 25 | ""hashes"": { 26 | ""Android"": ""7de37f7cd9eca4b2a6c9216c6cda8d0c"", 27 | ""IPhonePlayer"": ""e3fe65f3b2b19e4d1550cd7210ca5e0d"", 28 | ""WindowsPlayer"": ""18a5260b52e33fa6e54ff67e14b6c979"", 29 | ""OSXPlayer"": ""25296e04f8fac4a875b4f774b7bea1ba"", 30 | ""WebGLPlayer"": ""7a9b673950467554cad5f25f0ec70da5"" 31 | }, 32 | ""dependencies"": [""bundle-1""] 33 | } 34 | ]"; 35 | 36 | public static readonly string SampleJsonExtended = @"{ 37 | ""metadata"": ""Some cool stuff over here"", 38 | ""bundle"": { 39 | ""name"": ""bundle-1"", 40 | ""hashes"": { 41 | ""Android"": ""982415458cdaf4e60c420f75fe6c8e8b"", 42 | ""IPhonePlayer"": ""bd7a32acc8931e77eb1174baff409f21"", 43 | ""WindowsPlayer"": ""5425682f11f1eeb1b0ac7e4580cd2237"", 44 | ""OSXPlayer"": ""4a8250d93de151328fe1651096ec8bc1"", 45 | ""WebGLPlayer"": ""bc341e351d4813630d417d33558a51ed"" 46 | }, 47 | ""dependencies"": [] 48 | } 49 | }"; 50 | 51 | public static readonly AssetBundleDescription[] SampleDescriptions = new AssetBundleDescription[] 52 | { 53 | new AssetBundleDescription( 54 | "bundle-1", 55 | new Dictionary() 56 | { 57 | { RuntimePlatform.Android, Hash128.Parse("982415458cdaf4e60c420f75fe6c8e8b") }, 58 | { RuntimePlatform.IPhonePlayer, Hash128.Parse("bd7a32acc8931e77eb1174baff409f21") }, 59 | { RuntimePlatform.WindowsPlayer, Hash128.Parse("5425682f11f1eeb1b0ac7e4580cd2237") }, 60 | { RuntimePlatform.OSXPlayer, Hash128.Parse("4a8250d93de151328fe1651096ec8bc1") }, 61 | { RuntimePlatform.WebGLPlayer, Hash128.Parse("bc341e351d4813630d417d33558a51ed") }, 62 | }, 63 | new HashSet()), 64 | 65 | new AssetBundleDescription( 66 | "bundle-10", 67 | new Dictionary() 68 | { 69 | { RuntimePlatform.Android, Hash128.Parse("7de37f7cd9eca4b2a6c9216c6cda8d0c") }, 70 | { RuntimePlatform.IPhonePlayer, Hash128.Parse("e3fe65f3b2b19e4d1550cd7210ca5e0d") }, 71 | { RuntimePlatform.WindowsPlayer, Hash128.Parse("18a5260b52e33fa6e54ff67e14b6c979") }, 72 | { RuntimePlatform.OSXPlayer, Hash128.Parse("25296e04f8fac4a875b4f774b7bea1ba") }, 73 | { RuntimePlatform.WebGLPlayer, Hash128.Parse("7a9b673950467554cad5f25f0ec70da5") }, 74 | }, 75 | new HashSet() { "bundle-1" }), 76 | }; 77 | 78 | [Test] 79 | public void TestSerialize() 80 | { 81 | var json = JsonConvert.SerializeObject(SampleDescriptions, new AssetBundleDescriptionConverter()); 82 | 83 | var data = JsonConvert.DeserializeObject(json); 84 | var bundle1Name = (string)data[0]["name"]; 85 | var bundle2AndroidHash = (string)data[1]["hashes"]["Android"]; 86 | var bundle2Dependency = (string)data[1]["dependencies"][0]; 87 | 88 | Assert.AreEqual(bundle1Name, "bundle-1"); 89 | Assert.AreEqual(bundle2AndroidHash, "7de37f7cd9eca4b2a6c9216c6cda8d0c"); 90 | Assert.AreEqual(bundle2Dependency, "bundle-1"); 91 | } 92 | 93 | [Test] 94 | public void TestDeserialize() 95 | { 96 | var descriptions = JsonConvert.DeserializeObject(SampleJson, new AssetBundleDescriptionConverter()); 97 | Assert.AreEqual(descriptions, SampleDescriptions); 98 | } 99 | 100 | [Test] 101 | public void TestRoundTrip() 102 | { 103 | var json = JsonConvert.SerializeObject(SampleDescriptions, new AssetBundleDescriptionConverter()); 104 | var descriptions = JsonConvert.DeserializeObject(json, new AssetBundleDescriptionConverter()); 105 | Assert.AreEqual(descriptions, SampleDescriptions); 106 | } 107 | 108 | [Test] 109 | public void TestDeserializeExtended() 110 | { 111 | var description = JsonConvert.DeserializeObject(SampleJsonExtended, new AssetBundleDescriptionConverter()); 112 | 113 | Assert.AreEqual("bundle-1", description.Bundle.Name); 114 | Assert.AreEqual("Some cool stuff over here", description.CustomData); 115 | Assert.AreEqual(Hash128.Parse("982415458cdaf4e60c420f75fe6c8e8b"), description.Bundle.GetHashForPlatform(RuntimePlatform.Android)); 116 | } 117 | } 118 | 119 | public struct ExtendedBundleDescription 120 | { 121 | [JsonProperty("bundle")] 122 | public AssetBundleDescription Bundle; 123 | 124 | [JsonProperty("metadata")] 125 | public string CustomData; 126 | } 127 | -------------------------------------------------------------------------------- /Tests/Editor/JsonTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c7b2adff63f7fac4abe70871cc7710b0 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Editor/Unity.AssetBundleBuilder.Tests.Runtime.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Synapse.AssetBundleBuilder.Tests.Editor", 3 | "references": [ 4 | "Synapse.AssetBundleBuilder.Runtime", 5 | "Synapse.AssetBundleBuilder.Runtime.Json" 6 | ], 7 | "optionalUnityReferences": [ 8 | "TestAssemblies" 9 | ], 10 | "includePlatforms": [ 11 | "Editor" 12 | ], 13 | "excludePlatforms": [], 14 | "allowUnsafeCode": false, 15 | "overrideReferences": false, 16 | "precompiledReferences": [], 17 | "autoReferenced": false, 18 | "defineConstraints": [ 19 | "JSON_NET" 20 | ] 21 | } -------------------------------------------------------------------------------- /Tests/Editor/Unity.AssetBundleBuilder.Tests.Runtime.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d85790538624c1f46ad84d6ad4de7681 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.synapse-games.asset-bundle-builder", 3 | "version": "0.2.3", 4 | "displayName": "Asset Bundle Builder", 5 | "description": "A system for automatically building, managing, and publishing asset bundles.", 6 | "unity": "2018.4", 7 | "author": { 8 | "name": "David LeGare", 9 | "email": "dlegare.1001@gmail.com", 10 | "url": "https://randompoison.github.io" 11 | }, 12 | "dependencies": { 13 | "com.unity.editorcoroutines": "0.0.2-preview.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 91d94bf7e13e1f34b9566124d6dc57af 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------