When we develop for android the IDE dynamically generates “R” class which contains constants for all assets. Needless to say, it’s very convenient.
But why is there no such thing in Unity?
How can I extend the build pipeline to create the “R” class along with asset bundles?
Strange, but I didn’t find anything.
namespace UnityEditor.AddressableAssets {
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.AddressableAssets.Settings;
using UnityEngine;
public static class AssetsSourceGenerator {
// Generate
public static void Generate(string path, string @namespace, string @class, AddressableAssetSettings settings) {
var builder = new StringBuilder();
var entries = settings.GetEntries().ToTreeList().Items.ToArray();
builder.AppendCompilationUnit( @namespace, @class, entries );
WriteText( path, builder.ToString() );
}
private static void WriteText(string path, string text) {
if (!File.Exists( path ) || File.ReadAllText( path ) != text) {
File.WriteAllText( path, text );
AssetDatabase.ImportAsset( path, ImportAssetOptions.Default );
}
}
// Append
private static void AppendCompilationUnit(this StringBuilder builder, string @namespace, string @class, KeyValueTreeList<AddressableAssetEntry>.Item[] items) {
builder.AppendLine( $"namespace {@namespace} {{" );
{
builder.AppendClass( 1, @class, items );
}
builder.AppendLine( "}" );
}
private static void AppendClass(this StringBuilder builder, int indent, string @class, KeyValueTreeList<AddressableAssetEntry>.Item[] items) {
builder.AppendIndent( indent ).AppendLine( $"public static class {@class} {{" );
foreach (var item in items.Sort_()) {
builder.AppendValueOrScope( indent + 1, item );
}
builder.AppendIndent( indent ).AppendLine( "}" );
}
private static void AppendValueOrScope(this StringBuilder builder, int indent, KeyValueTreeList<AddressableAssetEntry>.Item item) {
if (item is KeyValueTreeList<AddressableAssetEntry>.ValueItem value) {
var identifier = value.Key;
var address = value.Value.address;
if (value.Value.IsAsset()) {
builder.AppendIndent( indent ).AppendLine( $"public const string @{identifier} = \"{address}\";" );
} else {
throw Exceptions.Internal.NotSupported( $"Entry {value.Value} is not supported" );
}
} else
if (item is KeyValueTreeList<AddressableAssetEntry>.ScopeItem scope) {
var identifier = scope.Key;
var items = scope.Items;
builder.AppendIndent( indent ).AppendLine( $"public static class @{identifier} {{" );
foreach (var i in items.Sort_()) {
builder.AppendValueOrScope( indent + 1, i );
}
builder.AppendIndent( indent ).AppendLine( "}" );
}
}
// Helpers
private static IEnumerable<KeyValueTreeList<AddressableAssetEntry>.Item> Sort_(this IEnumerable<KeyValueTreeList<AddressableAssetEntry>.Item> items) {
return items
.OrderByDescending( i => i.Key.Equals( "EditorSceneList" ) )
.ThenByDescending( i => i.Key.Equals( "Resources" ) )
.ThenByDescending( i => i.Key.Equals( "UnityEngine" ) )
.ThenByDescending( i => i.Key.Equals( "UnityEditor" ) )
.ThenByDescending( i => i.Key.Equals( "Scenes" ) )
.ThenByDescending( i => i.Key.Equals( "Launcher" ) )
.ThenByDescending( i => i.Key.Equals( "LauncherScene" ) )
.ThenByDescending( i => i.Key.Equals( "Startup" ) )
.ThenByDescending( i => i.Key.Equals( "StartupScene" ) )
.ThenByDescending( i => i.Key.Equals( "Program" ) )
.ThenByDescending( i => i.Key.Equals( "ProgramScene" ) )
.ThenByDescending( i => i.Key.Equals( "MainScene" ) )
.ThenByDescending( i => i.Key.Equals( "GameScene" ) )
.ThenByDescending( i => i.Key.Equals( "Program" ) )
.ThenByDescending( i => i.Key.Equals( "Presentation" ) )
.ThenByDescending( i => i.Key.Equals( "UI" ) )
.ThenByDescending( i => i.Key.Equals( "App" ) )
.ThenByDescending( i => i.Key.Equals( "Game" ) )
.ThenByDescending( i => i.Key.Equals( "Core" ) )
.ThenByDescending( i => i.Key.Equals( "MainScreen" ) )
.ThenByDescending( i => i.Key.Equals( "GameScreen" ) )
.ThenByDescending( i => i.Key.Equals( "Objects" ) )
.ThenByDescending( i => i.Key.Equals( "Subjects" ) )
.ThenByDescending( i => i.Key.Equals( "World" ) )
.ThenByDescending( i => i.Key.Equals( "Levels" ) )
.ThenBy( i => i.Key );
}
}
}
namespace UnityEditor.AddressableAssets {
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor.AddressableAssets.Settings;
using UnityEngine;
internal static class AssetsSourceGeneratorHelper {
// GetEntries
public static List<AddressableAssetEntry> GetEntries(this AddressableAssetSettings settings) {
var entries = new List<AddressableAssetEntry>();
settings.GetAllAssets( entries, true );
return entries;
}
// ToTreeList
public static KeyValueTreeList<AddressableAssetEntry> ToTreeList(this IList<AddressableAssetEntry> entries) {
var treeList = new KeyValueTreeList<AddressableAssetEntry>();
foreach (var entry in entries) {
var (scope, identifier) = entry.GetPath();
treeList.AddValue( scope, identifier, entry );
}
return treeList;
}
// IsAsset
public static bool IsAsset(this AddressableAssetEntry entry) {
return !entry.IsFolder;
}
public static bool IsMainAsset(this AddressableAssetEntry entry) {
if (!entry.IsFolder) {
return entry.ParentEntry == null || entry.ParentEntry.IsFolder;
}
return false;
}
public static bool IsSubAsset(this AddressableAssetEntry entry) {
if (!entry.IsFolder) {
return entry.ParentEntry != null && !entry.ParentEntry.IsFolder;
}
return false;
}
public static bool IsFolder(this AddressableAssetEntry entry) {
return entry.IsFolder;
}
// Helpers
private static (string[] Scope, string Identifier) GetPath(this AddressableAssetEntry entry) {
if (entry.IsAsset()) {
if (entry.IsMainAsset()) {
var scope = entry.GetScope();
var identifier = entry.GetIdentifier();
return (scope, identifier);
} else {
var scope = entry.ParentEntry.GetScope();
var identifier = entry.ParentEntry.GetIdentifier();
return (scope.Append( identifier + "_" ).ToArray(), entry.TargetAsset.name.Escape());
}
} else {
throw Exceptions.Internal.NotSupported( $"Entry {entry} is not supported" );
}
}
private static string[] GetScope(this AddressableAssetEntry entry) {
var scope = Path.GetDirectoryName( entry.address );
return scope.Split( '/', '\\', '.' ).Select( Escape ).ToArray();
}
private static string GetIdentifier(this AddressableAssetEntry entry) {
var identifier = Path.GetFileNameWithoutExtension( entry.address );
if (identifier.Contains( " #" )) identifier = identifier.Substring( 0, identifier.IndexOf( " #" ) );
if (identifier.Contains( " @" )) identifier = identifier.Substring( 0, identifier.IndexOf( " @" ) );
return identifier.Escape();
}
private static string Escape(this string value) {
var chars = value.ToCharArray();
for (var i = 0; i < chars.Length; i++) {
if (!char.IsLetterOrDigit( chars[ i ] )) chars[ i ] = '_';
}
return new string( chars );
}
}
}
namespace System.Collections.Generic {
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
public partial class KeyValueTreeList<T> {
internal interface IScope {
List<KeyValueTreeList<T>.Item> Items { get; }
}
public abstract class Item {
public string Key { get; }
public Item(string key) {
Key = key;
}
}
public class ValueItem : Item {
public T Value { get; }
public ValueItem(string key, T value) : base( key ) {
Value = value;
}
}
public class ScopeItem : Item, IScope {
public List<KeyValueTreeList<T>.Item> Items { get; } = new List<KeyValueTreeList<T>.Item>( 0 );
public ScopeItem(string key) : base( key ) {
}
}
}
public partial class KeyValueTreeList<T> : KeyValueTreeList<T>.IScope {
public List<KeyValueTreeList<T>.Item> Items { get; } = new List<Item>();
// Constructor
public KeyValueTreeList() {
}
// GetValue
public T? GetValue(string[] keys, string key) {
var scope = GetScope( this, keys );
if (scope != null) {
return scope.Items.OfType<ValueItem>().Where( i => i.Key == key ).Select( i => i.Value ).FirstOrDefault();
}
return default;
}
public T[]? GetValues(string[] keys, string key) {
var scope = GetScope( this, keys );
if (scope != null) {
return scope.Items.OfType<ValueItem>().Where( i => i.Key == key ).Select( i => i.Value ).ToArray();
}
return default;
}
// AddValue
public void AddValue(string[] keys, string key, T value) {
var scope = GetOrAddScope( this, keys );
scope.Items.Add( new ValueItem( key, value ) );
}
public void AddValues(string[] keys, string key, params T[] values) {
var scope = GetOrAddScope( this, keys );
foreach (var value in values) {
scope.Items.Add( new ValueItem( key, value ) );
}
}
// RemoveAll
public int RemoveAll(string[] keys, string key) {
var scope = GetScope( this, keys );
if (scope != null) {
return scope.Items.RemoveAll( i => i.Key == key );
}
return 0;
}
// Utils
public override string ToString() {
var builder = new StringBuilder();
GetString( builder, this );
return builder.ToString();
}
// Helpers/GetScope
private static IScope? GetScope(IScope scope, IEnumerable<string> keys) {
var subScope = scope;
foreach (var key in keys) {
subScope = GetScope( subScope, key );
if (subScope == null) break;
}
return subScope;
}
private static IScope GetOrAddScope(IScope scope, IEnumerable<string> keys) {
var subScope = scope;
foreach (var key in keys) {
subScope = GetOrAddScope( subScope, key );
}
return subScope;
}
// Helpers/GetScope
private static ScopeItem? GetScope(IScope scope, string key) {
var subScope = scope.Items.OfType<ScopeItem>().Where( i => i.Key == key ).FirstOrDefault();
return subScope;
}
private static ScopeItem GetOrAddScope(IScope scope, string key) {
var subScope = scope.Items.OfType<ScopeItem>().Where( i => i.Key == key ).FirstOrDefault();
if (subScope == null) {
subScope = new ScopeItem( key );
scope.Items.Add( subScope );
}
return subScope;
}
// Helpers/GetString
private static void GetString(StringBuilder builder, KeyValueTreeList<T> list) {
builder.Append( "KeyValueTreeList:" );
foreach (var item in list.Items) {
GetString( builder, item.Key, item );
}
}
private static void GetString(StringBuilder builder, string path, Item item) {
if (item is ValueItem value) {
builder.AppendLine();
builder.Append( path ).Append( ": " ).Append( value.Value );
} else
if (item is ScopeItem scope) {
foreach (var i in scope.Items) {
GetString( builder, $"{path}/{i.Key}", i );
}
}
}
}
}