Introduction
TypeSafe is a tool for reflecting changes made in the Unity Editor to your code. By scanning the project and generating static classes from your Resources, Layers, Tags and Scenes you no longer have to rely on hard-coded ‘naked-strings’ littered around your codebase. This has a number of benefits that improve the robustness of your code:
- Any changes made in the Editor that might introduce bugs in your code will cause compiler errors. (e.g. renaming a resource, which would break a hard-coded path at runtime, shows up as a compiler error immediately)
- Misspelling layers, scenes, etc is no longer possible.
- It is possible to iterate over the resources in your project. More on that later.
Resources
In Unity, a Resource is any asset that is contained within a folder called Resources. Usually to load such a resource you would pass a path into Resources.Load
, for example:
Traditional Method
var prefab = Resources.Load<PlayerBehaviour>("Player/MainPrefab");
var player = (PlayerBehaviour)Instantiate(prefab);
This has a number of drawbacks.
- If the path to the resource changes, or is typed incorrectly, this code will fail at runtime.
- The type of the resource may change in the future and cause this code to fail at runtime.
TypeSafe is designed to relocate this kind of error to compile-time instead of runtime, ensuring that you find out about bugs as soon as they are introduced.
TypeSafe Method
// If MainPrefab is missing, this will be a compile-error instead of a runtime error.
var player = Instantiate(SRResources.Player.MainPrefab.Load());
Resource Wrapper
Each resource in your project is wrapped in a simple Resource
object. This object contains the path to the resource, a cached copy of the resource once it has been loaded, and helper methods for asynchronous loading. Usually you won’t need any of this, as the object implicitly converts to the type of the resource when assigned to a field.
For example, a Material resource will be wrapped in a Resource<Material>
object. When this wrapper object is assigned to a renderer.sharedMaterial
field it will automatically load the resource and assign the result to the field.
// Assign alternate material. Resource is loaded and cached automatically.
player.GetComponent<Renderer>().material = SRResources.Materials.PlayerAlt;
The resource wrapper object automatically loads and caches the resource when assigned to a field. After the object is first assigned, any subsequent uses will return the cached object instead of loading it again. You don’t need to cache a copy in your scripts like you do with the traditional Resources.Load
method.
Class Overview
public class Resource<TResource> : IResource where TResource : Object
{
// Type of resource pointed to.
public Type Type { get; }
// Name of the resource (ie when calling unityObject.name)
public string Name { get; }
// Path string that is passed to Resources.Load(...) to load this resource object.
public string Path { get;}
// True if the resource has been loaded before.
public bool IsLoaded { get; }
// Get the object for this resource. If the resource is not loaded, this will block until it is.
// Subsequent calls to this method will return the cached reference to the resource.
public TResource Load();
// Returns a ResourceRequest object that can be used to yield in a coroutine.
public ResourceRequest LoadAsync();
// This is the magic. When this object is assigned to a field matching the Resource type
// it will automatically call Load() and return the result.
public static implicit operator TResource(Resource<TResource> resource);
// Unload this resource.
public void Unload();
}
Folders
The static class for resources generated by TypeSafe (named SRResources
by default, but this can be customized) has nested classes for each folder. For example, a resource located at Assets/Resources/Materials/Player/PlayerAlt can be accessed by SRResources.Materials.Player.PlayerAlt
.
Each folder has methods for getting a list of the contents, contents of a certain type, and matching recursive variants (which include resources in sub-folders).
Class Overview
// Return a list of all resources in this folder (does not include sub-folders)
// This method has a very low performance cost, so don't worry about caching the result.
public static IList<TypeSafe.IResource> GetContents();
// Return a list of all resources in this folder and all sub-folders.
// The result of this method is cached, so subsequent calls will have very low performance cost.
public static IList<TypeSafe.IResource> GetContentsRecursive();
// Return a list of all resources in this folder of type TResource (does not include sub-folders)
// You should cache the result of this method if used often.
public static List<TypeSafe.Resource<TResource>> GetContents<TResource>();
// Return a list of all resources in this folder of type TResource, including subfolders.
// You should cache the result of this method if used often.
public static List<TypeSafe.Resource<TResource>> GetContentsRecursive<TResource>();
// Calls Unload() on all resources in this folder.
public static void UnloadAll();
// Calls Unload() on all resources in this folder and all sub-folders.
// You should cache the result of this method if used often.
public static void UnloadAllRecursive();
Prefabs
TypeSafe handles prefabs as a special case. Instead of being wrapped in the generic Resource
object, they are wrapped in a specialized PrefabResource
object. This provides some syntax sugar for instantiating to save you some time.
Class Overview
public class PrefabResource : Resource<GameObject>
{
// Instantiate a new instance of this prefab and return the result.
public GameObject Instantiate();
// Instantiate a new instance of this prefab at position and rotation and return the result.
public GameObject Instantiate(Vector3 position, Quaternion rotation);
}
Unloading Resources
Resource folders have an UnloadAll and an UnloadAllRecursive method, that calls Unload() on each resource wrapper in the folder or folder & sub-folders respectively.
The resource wrapper holds the cached reference to the resource as a Weak Reference, which means that it does not prevent Unity’s Resources.UnloadUnusedResources()
method from working correctly on TypeSafe-loaded resources.
Layers and Tags
Both Layers and Sorting Layers in TypeSafe are wrapped in a struct which contains the layer ID and name. The Layer wrapper also contains the bitmask for the layer.
Layers
TypeSafe makes it simple to access and use layers. By using the SRLayers
class to reference layers you can be certain that if your layer configuration changes later you’ll discover any problems immediately. The Layer object implicitly converts to an integer, so can be assigned to a gameObject’s layer field automatically.
gameObject.layer = SRLayers.Characters;
Class Overview
public class Layer
{
// Name of the sorting layer, as might be passed to LayerMask.NameToLayer
public string name { get; }
// Layer number, as assigned to <c>gameObject.layer</c>
public int index { get; }
// Layer mask, as might be passed to Physics.Raycast
public int mask { get; }
}
The SRLayers
object also has an All
property that returns a list of Layer
objects containing all the layers in your project.
SortingLayers
By default, SortingLayers are accessed through the SRSortingLayers
class. The SortingLayer object implicitly converts to an integer or string, so can be assigned to a renderer’s sortingLayerID/sortingLayerName fields automatically.
GetComponent<Renderer>().sortingLayerID = SRSortingLayers.Characters;
Class Overview
public class SortingLayer
{
// Name of the sorting layer, as assigned to Renderer.sortingLayerName
public string name { get; }
//Unique ID, as assigned to Renderer.sortingLayerID<
public int id { get; }
}
The SRSortingLayers
object also has an All
property that returns a list of SortingLayer
objects containing all the sorting layers in your project.
Tags
Tags are accessed via the SRTags
class (by default. See Customize). It contains string properties generated from your tags, and an All
property that returns a list of strings with all your tags.
Scenes
Scenes are accessed via the SRScenes
class (by default. See Customize). This class has properties that return a Scene
wrapper object, that exposes the scene name and index. You can pass this object directly to Application.LoadScene(...)
or use one of the helper methods. Only Scenes listed in the Build Settings window will be included.
Application.LoadLevel(SRScenes.MainLevel);
SRScenes.MainLevel.Load();
Class Overview
// Name of the scene, as passed to Application.LoadLevel(string)
public string name { get; }
// Scene index, as passed to Application.LoadLevel(int)
public int index { get; }
// Calls Application.LoadLevel(...)
public void Load();
// Calls Application.LoadLevelAdditive(...)
public void LoadAdditive();
// Calls Application.LoadLevelAdditiveAsync(...)
public AsyncOperation LoadAdditiveAsync();
// Calls Application.LoadLevelAsync(...)
public AsyncOperation LoadAsync();
Input
Your Input axes (as defined in the Unity Input Manager) are accessed via the SRInput
class (by default. See Customize). This class has properties that return an InputAxis
wrapper object, that exposes the input name and state.
Class Overview
public class InputAxis
{
// Name of the input axis, as used by Input.GetAxis(string)
public string Name { get; }
// Equivalent to calling Input.GetAxis(Name);
public float Value { get; }
// Equivalent to calling Input.GetAxisRaw(Name);
public float RawValue { get; }
// Equivalent to calling Input.GetButton(Name);
public bool IsPressed { get; }
// Equivalent to calling Input.GetButtonDown(Name);
public bool Down { get; }
// Equivalent to calling Input.GetButtonUp(Name);
public bool Up { get; }
}
Audio Mixers
In the settings window, drag Audio Mixer assets to the Audio Mixers section on the Assets tab.
The parameters and snapshots of mixers in this list will be available in the SRAudioMixers
class (by default).
Parameters will be under SRAudioMixers.__MixerName__.Parameters
, and snapshots will be under SRAudioMixers.__MixerName__.Snapshots
Animators
In the settings window, drag Animator Controller assets to the Animator section on the Assets tab.
The parameters and layers of animators in this list will be available in the SRAnimators
class (by default).
Parameters
Parameters will be under SRAnimators.__AnimatorName__.Parameters
.
Parameters are integers, and correspond to the equavelent call to Animator.StringToHash.
// TypeSafe
animator.SetFloat(SRAnimators.SomeAnimator.Parameters.SomeParameter, 0.5f);
// Original
animator.SetFloat("SomeParameter", 0.5f);
// or
animator.SetFloat(_someParameterHash, 0.5f); // _someParameterHash populated elsewhere by Animator.StringToHash.
Layers
Layers are under SRAnimators.__AnimatorName__.Layers
. Layers are stored as an integer, and correspond to the equavelent call to Animator.GetLayerIndex.
// TypeSafe
animator.SetLayerWeight(SRAnimators.SomeAnimator.Layers.SomeLayer, 0.5f);
// Original
animator.SetLayerWeight(animator.GetLayerIndex("SomeLayer"), 0.5f);
Customize
Automatic Rebuild
TypeSafe automatically regenerates the classes whenever it detects a change in your project that might impact your code. This can be disabled if you want to only trigger scans manually. If no changes are detected it won’t trigger a script refresh.
The Automatic Rebuild Delay setting forces a minimum build time. This is helpful when you are rearranging resources as triggering a script refresh when you are busy rearranging can be frustrating. If another change is detected during a scan the scan is cancelled and re-queued.
Naming Scheme
The class naming scheme can be adjusted from the Settings panel. The namespace, prefix and suffix of the class can be changed.
- The namespace is the C# namespace that the generated classes will be contained in. By default this is empty and the classes are in the root namespace.
- The prefix is a string of characters added to the beginning of the class name (e.g., the default prefix SR is added to Resources to make SRResources.
- The suffix is the same as above, but added to the end of the class name.
The settings panel will provide a preview of your changes and ensure that the generated names don’t conflict with any built-in Unity classes or your own code.
Limitations
Conflicting Resources
Two resources cannot share the same path and name. If TypeSafe detects this, one will be discarded and an error printed to the console (clicking on this error will take you to the conflicting resources).
Nested Folder Names
Nested folders cannot have the same name as the parent folder. If TypeSafe detects this it will prefix the offending name with an underscore to prevent compiler errors and print a warning to the console. The same applies to resource names.
Unicode
TypeSafe allows most Unicode letter characters in resource names, except for non-ASCII numbers. This is due to a bug with Unity’s Mono compiler that incorrectly handles these characters.
Reserved Names
TypeSafe reserves a number of names that are used internally, or are part of the API. These names cannot be used for resources, layers or any other item. TypeSafe will ignore any resources with these names. A truncated list of these names is below:
- All
- None
- GetContents
- GetContentsRecursive
API
TypeSafeApi
The TypeSafeApi static class has a few exposed methods for invoking the TypeSafe compile process via script.
Member Name | Description |
---|---|
QueueRefresh() | Queue a new scan/compile process. Will do nothing and print a warning if IsBusy is true. |
Cancel() | If a TypeSafe scan is currently in progress, cancel it. |
IsBusy | True if TypeSafe is currently scanning or compiling. |
FormatTypeName(string) | Format a string with the class prefix/suffix specified in the Settings window. Useful when using Custom Data Sources |
TypeSafeApi is located in the TypeSafe.Editor namespace.
Custom Data Source
TypeSafe supports integrating into your own code to provide custom generated classes based on your own data. This is accomplished by creating a class that implements the ITypeSafeDataSource
interface.
namespace TypeSafe.Editor
{
public interface ITypeSafeDataSource
{
TypeSafeDataUnit GetTypeSafeDataUnit();
}
}
This interface has a single method, GetTypeSafeDataUnit()
, that returns a TypeSafeDataUnit
object. Any class that implements this interface will be automatically instantated and this method invoked during the TypeSafe scan process.
TypeSafeDataUnit
Property Name | Description |
---|---|
ClassName | The name of the generated class. You can use TypeSafeApi.FormatTypeName(…) to conform to the naming scheme specified in the Settings window. |
FileName | Used to create the generated file name ({0}.Generated.cs, placed in the TypeSafe usr folder). Uses ClassName by default. |
DataType | System.Type used for each data entry in the generated class. See type limitations. |
Data | Collection of TypeSafeDataEntry objects used to populate the generated class. |
NestedUnits | Collection of TypeSafeDataUnit objects. These will be added as nested classes inside this data unit. |
EnableAllProperty | Setting to true will enable the generation of a property named ‘All’ that returns a list of all entries in the data set. |
TypeSafeDataEntry
Property Name | Description |
---|---|
PropertyName | String used as property name of the generated entry. Will be filtered through reserved name and C# compliance checks. |
Parameters | The array of parameters passed to the data type constructor. If the data type is a primitive or string, this should have one entry of the exact same type. |
Type Limitations
The type of generated fields must fulfil one of the following criteria:
- Is a primitive (int, uint, byte, etc)
- Is a string
- Is a class with a public constuctor, with parameters matching the objects passed into the
TypeSafeDataEntry.Parameters
array.
FAQ
What’s up with global::?
In all code generated by TypeSafe classes are referenced by their absolute path, starting with global::
. This makes the code a bit bulky, but it ensures that there are no naming conflicts with existing code in your project. Users finding that a class in their project is interfering with code in an asset is a problem we’ve had reported in the past, so we took steps to ensure that this didn’t happen with TypeSafe. This is the same reason why we use a .NET assembly for all non-generated code.
Contact
If you have a problem which isn’t answered by this documentation or the FAQ, or you find an error in these docs, feel free to open an issue on the GitHub repository or post in the Unity Forum Thread.