Skip to content

Commit

Permalink
OneAsset: Major API refactor (#13)
Browse files Browse the repository at this point in the history
* Added tests for attribute inheritance

* wip

* wip

* working tests

* Remove CreateAssetAutomaticallyAttribute

* added unsafe loading

* wip

* AssetLoadOptions

* wip

* test cleanup

* wip

* wip

* wip

* cr changes

* CR changes
  • Loading branch information
ErnSur authored Aug 10, 2023
1 parent d8c58df commit 5fd6c9a
Show file tree
Hide file tree
Showing 71 changed files with 1,025 additions and 470 deletions.
8 changes: 8 additions & 0 deletions Assets/Plugins.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 9 additions & 20 deletions Packages/com.quickeye.utility/OneAsset/Editor/AssetMaker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,21 @@ internal static class AssetMaker
[InitializeOnLoadMethod]
private static void RegisterCallback()
{
ScriptableObjectFactory.CreateAssetAction += CreateAsset;
OneAssetLoader.CreateAssetAction += CreateAsset;
Console.WriteLine("[AssetLoadTest] AssetMaker RegisterCallback");
}

private static void CreateAsset(ScriptableObject obj)
private static void CreateAsset(ScriptableObject obj, AssetLoadOptions options)
{
var fullAssetPath = GetFullAssetPath(obj.GetType());
var baseDir = Path.GetDirectoryName(fullAssetPath);
var path = options.Paths[0];
var baseDir = Path.GetDirectoryName(path);
if (baseDir != null)
Directory.CreateDirectory(baseDir);
AssetDatabase.CreateAsset(obj, fullAssetPath);
var assetPath = path;
if (!assetPath.EndsWith(".asset"))
assetPath = $"{assetPath}.asset";
AssetDatabase.CreateAsset(obj, assetPath);
AssetDatabase.SaveAssets();
}

private static string GetFullAssetPath(Type type)
{
var createAssetAtt = type.GetCustomAttribute<CreateAssetAutomaticallyAttribute>();
if (createAssetAtt == null)
throw new Exception($"{type.FullName} is missing {nameof(CreateAssetAutomaticallyAttribute)}.");
var loadFromAssetAttribute = LoadFromAssetUtils.GetFirstAttribute(type);
if (loadFromAssetAttribute == null)
throw new Exception($"{type.FullName} is missing {nameof(LoadFromAssetAttribute)}.");

var pathStart = PathUtility.EnsurePathStartsWith("Assets", createAssetAtt.ResourcesFolderPath);
pathStart = PathUtility.EnsurePathEndsWith("Resources", pathStart);
var pathEnd = loadFromAssetAttribute.GetResourcesPath(type) + ".asset";
return $"{pathStart}/{pathEnd}";
}
}
}
38 changes: 18 additions & 20 deletions Packages/com.quickeye.utility/OneAsset/Editor/UI/AssetMetadata.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using System;
using System.IO;
using System.Linq;
using UnityEditor;
using Object = UnityEngine.Object;
Expand All @@ -12,12 +10,9 @@ internal class AssetMetadata

public readonly string TypeName;

public readonly LoadFromAssetAttribute[] LoadFromAssetAttributes;
public string[] ResourcesPaths { get; }
public string FirstResourcesPath => ResourcesPaths[0];

public LoadFromAssetAttribute FirstLoadFromAssetAttribute =>
ResourcesPaths.Length == 0 ? null : LoadFromAssetAttributes[0];
public readonly AssetLoadOptions LoadOptions;
public string FirstLoadPath =>
LoadOptions.Paths.Length == 0 ? null : LoadOptions.Paths[0];

/// <summary>
/// Metadata about the object that has <see cref="LoadFromAssetAttribute"/>
Expand All @@ -28,32 +23,35 @@ public AssetMetadata(Object asset)
Asset = asset;
var type = asset.GetType();
TypeName = type.Name;
LoadFromAssetAttributes = LoadFromAssetUtils.GetAttributesInOrder(type);
ResourcesPaths = LoadFromAssetAttributes.Select(a => a.GetResourcesPath(type)).ToArray();
LoadOptions = AssetLoadOptionsUtility.GetLoadOptions(type);
}

public bool IsInLoadablePath(out string attributeLoadPath)
public bool IsInLoadablePath(out AssetPath loadPath)
{
var assetPath = AssetDatabase.GetAssetPath(Asset);
if (string.IsNullOrEmpty(assetPath))
{
attributeLoadPath = null;
loadPath = null;
return false;
}

var extension = Path.GetExtension(assetPath);
// the ResourcesPaths need to use forward slashes
foreach (var resourcesPath in ResourcesPaths)
foreach (var loadablePath in LoadOptions.AssetPaths)
{
var pathEnding = $"Resources/{resourcesPath}{extension}";
if (assetPath.EndsWith(pathEnding, StringComparison.InvariantCulture))
if (loadablePath.IsInResourcesFolder &&
assetPath.EndsWith(loadablePath.ResourcesPath + loadablePath.Extension))
{
loadPath = loadablePath;
return true;
}

if (loadablePath.OriginalPath == assetPath)
{
attributeLoadPath = resourcesPath;
loadPath = loadablePath;
return true;
}
}

attributeLoadPath = null;
loadPath = null;
return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace OneAsset.Editor.UI
{
using static LoadableAssetGUI;

// TODO: add an icon in the header drawer to indicate if the asset is loadable in runtime or just editor. It should have an icon of "?" in a circle. like help button. it could also display Path formatting rules for all of the options enabled
internal class AssetPathHeaderDrawer : PostHeaderDrawer
{
[InitializeOnLoadMethod]
Expand All @@ -23,7 +24,7 @@ private static bool ShouldDrawHeader(UnityEditor.Editor editor, out AssetMetadat
return editor.targets.Length == 1 &&
EditorUtility.IsPersistent(editor.target) &&
LoadFromAssetCache.TryGetEntry(editor.serializedObject.targetObject, out metadata) &&
metadata.LoadFromAssetAttributes.Length > 0;
metadata.LoadOptions.Paths.Length > 0;
}

private readonly AssetMetadata _metadata;
Expand All @@ -36,30 +37,30 @@ private AssetPathHeaderDrawer(UnityEditor.Editor editor, AssetMetadata metadata)

public override void OnGUI()
{
var hasCorrectPath = _metadata.IsInLoadablePath(out var resPath);
var hasCorrectPath = _metadata.IsInLoadablePath(out var loadPath);
if (!hasCorrectPath)
resPath = _metadata.FirstResourcesPath;
loadPath = _metadata.LoadOptions.AssetPaths[0];

using (new GUILayout.HorizontalScope())
{
var path = $"Resources/{resPath}";
var labelContent = new GUIContent(GetGuiContent(hasCorrectPath, resPath, _metadata.TypeName));
var pathText = loadPath.ToString();
var labelContent = new GUIContent(GetGuiContent(hasCorrectPath, pathText, _metadata.TypeName));
var labelStyle = new GUIStyle(EditorStyles.label);
labelStyle.margin.left += 2;
labelStyle.margin.right += 2;
labelContent.text = "Load Path";
labelContent.tooltip = path;
labelContent.tooltip = pathText;
var labelRect = GUILayoutUtility.GetRect(labelContent, labelStyle, GUILayout.ExpandWidth(false));
var textFieldRect = GUILayoutUtility.GetRect(GUIContent.none, EditorStyles.textField);

GUI.enabled = false;
EditorGUI.TextField(textFieldRect, $"*/{path}");
EditorGUI.TextField(textFieldRect, pathText);
GUI.enabled = true;

if (GUI.RepeatButton(textFieldRect, GUIContent.none, GUIStyle.none))
{
labelContent.text = "Copied";
GUIUtility.systemCopyBuffer = path;
GUIUtility.systemCopyBuffer = pathText;
}

using (new EditorGUIUtility.IconSizeScope(new Vector2(16, 16)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ private static bool TryGetOneGameObjectPrefab(string path, out Object component)

private static bool HasLoadFromAssetAttribute(Object asset)
{
return LoadFromAssetUtils.HasAttribute(asset.GetType());
return AssetLoadOptionsUtility.HasAttribute(asset.GetType());
}

private static Object GetLoadableAssetOrNull(Object obj)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ internal static class LoadableAssetGUI
private const string UnlinkedIcon =
"Packages/com.quickeye.utility/OneAsset/Editor/UI/Icons/Unlinked.png";

public static GUIContent GetGuiContent(bool isCorrectPath, string resourcesPath, string typeName)
public static GUIContent GetGuiContent(bool isCorrectPath, string loadPath, string typeName)
{
var iconContent = isCorrectPath
? EditorGUIUtility.IconContent(LinkedIcon)
: EditorGUIUtility.IconContent(UnlinkedIcon);
iconContent.tooltip = isCorrectPath
? $"{typeName} can be loaded from this path"
: $"{typeName} won't load from this path. Load path:\n\"Resources/{resourcesPath}\"";
: $"{typeName} won't load from this path. Load path:\n\"{loadPath}\"";
return iconContent;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ private static void ProjectWindowItemOnGUI(string guid, Rect rect)
try
{
if (!LoadFromAssetCache.TryGetEntry(guid, out var metadata) ||
metadata.FirstLoadFromAssetAttribute == null)
metadata.FirstLoadPath == null)
return;
if (rect.height > EditorGUIUtility.singleLineHeight)
DrawProjectGridItem(rect, metadata);
Expand All @@ -54,15 +54,16 @@ private static void DrawProjectItem(Rect rect, string path, AssetMetadata meta)
{
var projectItemLabelContent = new GUIContent(Path.GetFileNameWithoutExtension(path));
var linkedIconRect = CalculateRectAfterLabelText(rect, projectItemLabelContent, true);

var isInLoadablePath = meta.IsInLoadablePath(out _);
var linkedIcon = GetGuiContent(isInLoadablePath, meta.FirstResourcesPath, meta.TypeName);
var linkedIcon = GetGuiContent(isInLoadablePath, meta.FirstLoadPath, meta.TypeName);
using (new EditorGUIUtility.IconSizeScope(new Vector2(16, 16)))
GUI.Label(linkedIconRect, linkedIcon, IconLabelStyle);
}

private static void DrawProjectGridItem(Rect rect, AssetMetadata meta)
{
var content = GetGuiContent(meta.IsInLoadablePath(out _), meta.FirstResourcesPath, meta.TypeName);
var content = GetGuiContent(meta.IsInLoadablePath(out _), meta.FirstLoadPath, meta.TypeName);
var iconRect = new Rect(rect)
{
size = new Vector2(rect.size.y, rect.size.y) / 3
Expand Down
74 changes: 47 additions & 27 deletions Packages/com.quickeye.utility/OneAsset/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,47 @@
A set of classes and editor UI improvements aimed to improve workflows that require asset loading.

**Package contains**:
- `ScriptableObjectFactory` Creates or loads Scriptable Objects by respecting the rules and options of following attributes:
- `LoadFromAssetAttribute` - Load asset from path
- `CreateAssetAutomaticallyAttribute` - Create an asset if it doesn't exist
- `OneAssetLoader` Loads or creates objects with options from `AssetLoadOptions`
- Abstract class that can be inherited to get a singleton behaviour
- `OneGameObject<T>`
- `OneScriptableObject<T>`
- Customize a singleton implementation with options like:
- Customize a singleton implementation with `AssetLoadOptions` like:
- Load instance from a prefab or `ScriptableObject` asset
- Create an asset automatically if it doesn't exist already
- Extended editor UI for singleton assets:
- Custom icons and tooltips in the project browser
- Asset load path field in the inspector header
<img src="./Documentation~/SingletonUI.png" align="center" width="70%">


## `OneAssetLoader`

**Example usage**
```csharp
var loadPaths = new[]
{
"Assets/Resources/OneAssetSamples1/SampleAsset.asset",
"Assets/Resources/LegacyPath/SampleAsset.asset"
};
var loadOptions = new AssetLoadOptions(loadPaths)
{
// Enables a system that will create scriptable object file if it cannot be loaded from loadPaths.
// Asset will always be created at the first path provided.
// Works only in editor
CreateAssetIfMissing = true,

// If set to true a exception will be thrown when `OneAssetLoader` wont find asset at any of the provided paths
// if set to false a new instance of ScriptableObject will be created
AssetIsMandatory = true,

// Use the `UnityEditorInternal.InternalEditorUtility.LoadSerializedFileAndForget` as a fallback load option.
// Use with caution!
// Works only in editor
LoadAndForget = true
};
var asset = OneAssetLoader.LoadOrCreateScriptableObject<SampleScriptableObject>(loadOptions);
```

## UnityEngine.Object and a Singleton pattern (Disclaimer)

By definition, a singleton is a class that:
Expand All @@ -37,19 +64,16 @@ UnityEngine.Object.Destroy(singletonInstance);
var i = ScriptableObject.CreateInstance<MySingleton>();
```
> For more details on exact behavior of singleton loading look at the XML documentation and tests.
> **See**: _OneScriptableObjectTests.cs_ and _ScriptableObjectFactory.cs_
> **See**: _OneScriptableObjectTests.cs_ and _OneAssetLoader.cs_
## `OneGameObject<T>`

`MonoBehaviour` Singleton implementation.
Takes into account some common problems of many singleton implementations that are out there.

Options:
- Load instance from a prefab asset by adding a `LoadFromAssetAttribute` attribute

Example:
```c#
[LoadFromAsset("Popup View")]
[LoadFromAsset("Resources/Popup View.prefab")]
public class PopupView : OneGameObject<PopupView> { }
void UseExample()
{
Expand All @@ -58,33 +82,29 @@ void UseExample()
}
```


## `OneScriptableObject<T>`

`ScriptableObject` Singleton implementation.

Options:
- Automatically create scriptable object asset (if it doesn't exists already) when used with `LoadFromAssetAttribute` and `CreateAssetAutomaticallyAttribute`
Optional features:
- Create a [SettingsProvider](https://docs.unity3d.com/ScriptReference/SettingsProvider.html) just by adding `SettingsProviderAttribute` and `LoadFromAssetAttribute`


Example:
```c#
// LoadFromAssetAttribute will make the `SuperSdkSettings.Instance` load the scriptable object from
// resources path: "*Resources/Super SDK Settings"
// `Mandatory = true` option will make sure to show error messages if the asset is missing at this path.
[LoadFromAsset("Super SDK Settings", Mandatory = true)]
// `CreateAssetAutomatically` Attribute turns on a system that will create scriptable object file at specific path
// if it cannot be loaded from path specified in `LoadFromAsset` attribute
// in this example it will create asset with path: "Assets/Settings/Super SDK Settings"
[CreateAssetAutomatically("Assets/Settings/")]
// The `SettingsProviderAsset` will create a new UI settings tab with name "Super SDK" in the Project Settings window
// where users can edit this asset
[SettingsProviderAsset("Project/Super SDK")]
public class SuperSdkSettings : OneScriptableObject<SuperSdkSettings>
{
public string AppKey;
}
// LoadFromAssetAttribute will make the `SuperSdkSettings.Instance` load the scriptable object from
// resources path: "*Resources/Super SDK Settings"
// `CreateAssetIfMissing` turns on a system that will create scriptable object file at specific path
// if it cannot be loaded from path specified in `LoadFromAsset` attribute
// in this example it will create asset with path: "Assets/Settings/Resources/Super SDK Settings"
[LoadFromAsset("Assets/Settings/Resources/Super SDK Settings.asset", CreateAssetIfMissing = true)]
// The `SettingsProviderAsset` will create a new UI settings tab with name "Super SDK" in the Project Settings window
// where users can edit this asset
[SettingsProviderAsset("Project/Super SDK")]
public class SuperSdkSettings : OneScriptableObject<SuperSdkSettings>
{
public string AppKey;
}
```

## Samples
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace OneAsset
public class AssetIsMissingException : Exception
{
internal AssetIsMissingException(Type assetType, string assetPath) : base(
$"Asset of type {assetType} is missing from path: Resources/{assetPath}")
$"Asset of type {assetType} is missing from path: {assetPath}")
{
}
}
Expand Down
Loading

0 comments on commit 5fd6c9a

Please sign in to comment.