├── #01-GitVersionControl ├── README.md └── images │ ├── gitVersionControl1.png │ ├── gitVersionControl2.png │ └── gitVersionControl3.png ├── #02-ExtensionMethods └── README.md ├── #03-RichText ├── README.md ├── StringExtensions.cs └── images │ ├── richText1.png │ ├── richText2.png │ ├── richText3.png │ └── richText4.png ├── #04-MoreEfficientYieldStatements ├── README.md ├── WaitFor.cs └── images │ └── moreEfficientYieldStatements1.png ├── #05-CustomInvoke ├── DAMonoBehaviourExtensions.cs ├── README.md └── WaitFor.cs ├── #06-PlayerPreferences ├── PlayerPreferences.cs └── README.md ├── #07-BinarySerialization ├── BinarySerializer.cs └── README.md ├── #08-ScriptableObjects ├── LevelData.cs ├── ManualCreateMenuItem │ ├── LevelDataAsset.cs │ ├── README.md │ └── ScriptableObjectUtility.cs ├── README.md └── images │ └── scriptableObjects1.png ├── #09-JSONSerialization ├── JSONSerializer.cs └── README.md ├── #10-XMLSerialization ├── README.md └── XMLSerialization.cs ├── #11-OnScreenLogMessages ├── README.md └── images │ └── onScreenLogMessages1.png ├── #12-PreloadingMobileSigning ├── PreloadMobileSinging.cs ├── README.md └── images │ └── preloadMobileSinging1.png ├── #13-iOSLaunchScreen ├── LaunchScreen-iOS.xib ├── README.md └── images │ ├── iOSLaunchScreen1.png │ ├── iOSLaunchScreen2.png │ └── iOSLaunchScreen3.png ├── #14-CustomC#ScriptTemplate ├── 81-C# Script-NewBehaviourScript.cs.txt └── README.md ├── #15-PimpTheInspector ├── README.md └── images │ ├── pimpTheInspector1.png │ ├── pimpTheInspector2.png │ ├── pimpTheInspector3.png │ ├── pimpTheInspector4.png │ └── pimpTheInspector5.png ├── #16-OnValidate ├── README.md └── images │ ├── onValidate1.png │ └── onValidate2.png ├── #17-CustomEditor ├── README.md └── images │ ├── customEditorWindows1.png │ ├── customEditorWindows2.png │ └── customEditorWindows3.png ├── #18-CustomMenuItems ├── README.md └── images │ ├── customMenuItems1.png │ ├── customMenuItems2.png │ └── customMenuItems3.png ├── #19-LoadingArrayAssetsAtPath ├── FolderManager.cs └── README.md ├── #20-AssetPostprocessor └── README.md ├── #21-Singletons ├── MonoSingleton.cs ├── README.md └── SerializableSingleton.cs ├── #22-AndroidDeviceFilter ├── README.md └── images │ └── androidDeviceFilter1.png ├── #23-Pseudolocalization ├── English.json ├── GermanPseudo.json ├── PseudoLocalizationWindow.cs ├── README.md └── images │ └── pseudoLocalization1.png ├── #24-ContextMenus ├── README.md └── images │ ├── contextMenus1.png │ └── contextMenus2.png ├── #25-PropertyDrawers ├── README.md └── images │ └── propertyDrawers1.png ├── #26-DecoratorDrawers ├── README.md └── images │ └── decoratorDrawers1.png ├── #27-CustomFolderInspectors ├── DAFolderAsset.cs ├── DAFolderAssetPostprocessor.cs ├── DefaultAssetEditor.cs ├── MyFolderAsset.cs ├── README.md └── images │ ├── customFolderInspectors1.png │ ├── customFolderInspectors2.png │ └── customFolderInspectors3.png ├── .gitignore ├── LICENSE └── README.md /#01-GitVersionControl/README.md: -------------------------------------------------------------------------------- 1 | # 01 - Git Version Control 2 | 3 | Version control is something that I’m sure most are familiar with (if not check out this tutorial and this course), but something that you may not be familiar with is how to optimize a Unity Project for Git version control. 4 | 5 | One issue is that within a Unity Project there are many folders and files that can remain local and don’t need to be tracked. The Library folder, for instance, when not present is always constructed on load, while OS (Mac/Windows etc.) specific files don’t need to be synced across computers. 6 | 7 | Firstly, in Project Settings/Editor insure Version Control Mode is set to Visible Meta Files 8 | 9 | ![](images/gitVersionControl1.png) 10 | 11 | as this is required for version control. The benefit of this meta files is that unique settings for a file (such as import settings for a sprite etc.) are saved to an associated meta file, so syncing is easier and faster between projects. 12 | 13 | Now we could commit only the relevant files (that is, the Assets and ProjectSettings folders) 14 | ``` 15 | git add Assets 16 | git add ProjectSettings 17 | ``` 18 | but one issue is the annoyance of untracked files messages for files which we have no interest in tracking. 19 | 20 | ![](images/gitVersionControl2.png) 21 | 22 | Luckily by writing a custom .gitignore file (saved to the root project folder), we can specific the files that git should ignore tracking.

23 | 24 | ``` 25 | # Unity generated folders 26 | Temp/ 27 | Library/ 28 | 29 | # Custom Build Folder 30 | Builds/ 31 | 32 | # MonoDevelop generated files 33 | obj/ 34 | *.csproj 35 | *.unityproj 36 | *.sln 37 | *.userprefs 38 | 39 | # OS generated files 40 | .DS_Store 41 | .DS_Store? 42 | ._* 43 | .Spotlight-V100 44 | .Trashes 45 | ehthumbs.db 46 | Thumbs.db 47 | ``` 48 | 49 | ![](images/gitVersionControl3.png) 50 | 51 | One last point is the Editor setting Asset Serialization Mode to being Force Text or Mixed (between Text and Binary). Most Unity Projects will have scenes, prefabs and thus a lot of binary files. Force Text works better for version control in viewing the changes between commits, but Mixed means that binary files are imported faster into the project. -------------------------------------------------------------------------------- /#01-GitVersionControl/images/gitVersionControl1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#01-GitVersionControl/images/gitVersionControl1.png -------------------------------------------------------------------------------- /#01-GitVersionControl/images/gitVersionControl2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#01-GitVersionControl/images/gitVersionControl2.png -------------------------------------------------------------------------------- /#01-GitVersionControl/images/gitVersionControl3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#01-GitVersionControl/images/gitVersionControl3.png -------------------------------------------------------------------------------- /#02-ExtensionMethods/README.md: -------------------------------------------------------------------------------- 1 | # 02 - Extension Methods 2 | 3 | *Extension Methods* allow us to add functionality to existing classes. Say for instance we want to set the X position of a Transform. We could write a private method within our class MyGameObject but as this is probably something that we could use across multiple classes, it makes more sense to create an Extension Method 4 | 5 | ```C# 6 | public static void SetX(this Transform transform, float x) 7 | { 8 | Vector3 position = transform.position; 9 | position.x = x; 10 | transform.position = position; 11 | } 12 | ``` 13 | 14 | which can be called on any Transform in any class. 15 | 16 | ```C# 17 | Transform myTransform. 18 | myTransform.SetX(100); 19 | ``` 20 | 21 | As *transform.position* is a property and not a variable, Extension Methods like *SetX*, *SetY*, *MoveToInTime* etc. are extremely useful as they reduce duplicated lines of code, while making the code more understandable. 22 | 23 | One issue with Extension Methods is that there are no automatic null checks. So if myTransform happens to be null, then a **NullReferenceException** will be thrown when trying to call SetX. Thus we need to explicitly check for null, if applicable. 24 | 25 | ```C# 26 | public static void SetX(this Transform transform, float x) 27 | { 28 | if(transform == null) { Debug.LogError("transform is null"); return; } 29 | 30 | Vector3 position = transform.position; 31 | position.x = x; 32 | transform.position = position; 33 | } 34 | ``` 35 | 36 | ```C# 37 | public static bool IsNullOrEmpty(this string value) 38 | { 39 | return string.IsNullOrEmpty(value); 40 | } 41 | ``` 42 | 43 | If you are unfamiliar with Extension Methods, then I suggest you take a look [here](https://msdn.microsoft.com/pl-pl/library/windows/desktop/bb383977(v=vs.100).aspx), [here](https://unity3d.com/learn/tutorials/topics/scripting/extension-methods) and [here](http://www.alanzucconi.com/2015/08/05/extension-methods-in-c/), and start utilizing them within your projects! 44 | 45 | Note that Extension Methods can be written inside any static class. Some developers like to have a single *ExtensionMethods.cs* file, others like to break the methods out into separate files, for instance *StringExtentions.cs*, *TransformExtensions.cs* etc. -------------------------------------------------------------------------------- /#03-RichText/README.md: -------------------------------------------------------------------------------- 1 | # 03 - Rich Text 2 | 3 | You are probably familiar with the UI Text component in which text can be added to a UI canvas. 4 | 5 | ![](images/richText1.png) 6 | 7 | What is not so apparent is that this component supports rich text. 8 | 9 | Thus using markup tags like ``` , , and ``` , a single string can contain multiple font styles. 10 | 11 | ``` 12 | Text text; 13 | text.text = "This is green, 14 | and this is red. bold, italic"; 15 | ``` 16 | 17 | ![](images/richText2.png) 18 | 19 | Even *Debug.Log* supports these markup tags which can be useful when reporting warnings and errors. 20 | 21 | ![](images/richText3.png) 22 | 23 | For more information see the [Unity documentation](https://docs.unity3d.com/Manual/StyledText.html). 24 | 25 | It is, however, inconvenient and unnecessary to continually type these tags. Some Extension Methods would make everything easier now wouldn’t they?! 26 | 27 | ```C# 28 | public static string SetColor(this string value, RichTextColor color) 29 | { 30 | return string.Format("{1}", color.ToString(), value); 31 | } 32 | ``` 33 | 34 | where **RichTextColor** is a public enum of rich text tags 35 | 36 | ```C# 37 | public enum RichTextColor 38 | { 39 | aqua, black, blue, ... 40 | } 41 | ``` 42 | 43 | which can then be used as 44 | 45 | ```C# 46 | Debug.Log( "This is my message".SetColor(RichTextColor.red).SetBold() ); 47 | ``` 48 | 49 | The instantiation of two new string objects just to set the color and bold isn't efficient but then again it is also only used during debug mode. A better approach would be somthing like *SetColorAndBold* which concatenates the tags. 50 | 51 | Take a look at *StringExtentions.cs* for more functionality. *SetColorForWords* is particularly useful if one wants to highlight one or more words. 52 | 53 | ```C# 54 | Debug.Log( "This is my message".SetColorForWords(RichTextColor.red, 2) ); 55 | Debug.Log( "This is my message".SetSizeForWords(20, 0) ); 56 | Debug.Log( "This is my message".SetBoldForWords(1, 2) ); 57 | Debug.Log( "This is my message".SetItalicsForWords(0, 3) ); 58 | ``` 59 | 60 | ![](images/richText4.png) -------------------------------------------------------------------------------- /#03-RichText/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Written by James Leahy (c) 2017 DeFunc Art. 3 | * https://github.com/defuncart/ 4 | */ 5 | using System; 6 | using System.Text; 7 | using UnityEngine; 8 | 9 | /// A class of extention methods for string. 10 | public static class StringExtensions 11 | { 12 | /// Sets the color of each character of the string. 13 | /// The string. 14 | /// The RichTextColor color. 15 | public static string SetColor(this string value, RichTextColor color) 16 | { 17 | return value.SetColor(color.ToString()); 18 | } 19 | 20 | /// Sets the color of each character of the string. 21 | /// The string. 22 | /// The color as a HEX string. 23 | public static string SetColor(this string value, string color) 24 | { 25 | return string.Format("{1}", color, value); 26 | } 27 | 28 | /// Sets the color of a variable number of words. 29 | /// The string. 30 | /// The RichTextColor color. 31 | /// The variable number of word indicies. 32 | public static string SetColorForWords(this string value, RichTextColor color, params int[] highlightedIndices) 33 | { 34 | return value.SetColorForWords(color.ToString(), highlightedIndices); 35 | } 36 | 37 | /// Sets the color of a variable number of words. 38 | /// The string. 39 | /// The RichTextColor color. 40 | /// The variable number of word indicies. 41 | public static string SetColorForWords(this string value, string color, params int[] highlightedIndices) 42 | { 43 | string[] words = value.Split(' '); //split the string into an array of words 44 | StringBuilder sb = new StringBuilder(); 45 | for(int i=0; i < words.Length; i++) //and recombine the words into a string with tags for each highlightedIndex 46 | { 47 | if(Array.IndexOf(highlightedIndices, i) > -1) //highlightedIndices contains i 48 | { 49 | sb.Append(words[i].SetColor(color)); 50 | } 51 | else { sb.Append(words[i]); } 52 | sb.Append(' '); 53 | } 54 | return sb.ToString(); 55 | } 56 | 57 | /// Sets the size of each character of the string in pixels. 58 | /// The string. 59 | /// The size in pixels. 60 | public static string SetSize(this string value, int size) 61 | { 62 | return string.Format("{1}", size, value); 63 | } 64 | 65 | /// Sets the size in pixels of a variable number of words. 66 | /// The string. 67 | /// The size in pixels. 68 | /// The variable number of word indicies. 69 | public static string SetSizeForWords(this string value, int size, params int[] highlightedIndices) 70 | { 71 | string[] words = value.Split(' '); //split the string into an array of words 72 | StringBuilder sb = new StringBuilder(); 73 | for(int i=0; i < words.Length; i++) //and recombine the words into a string with tags for each highlightedIndex 74 | { 75 | if(Array.IndexOf(highlightedIndices, i) > -1) //highlightedIndices contains i 76 | { 77 | sb.Append(words[i].SetSize(size)); 78 | } 79 | else { sb.Append(words[i]); } 80 | sb.Append(' '); 81 | } 82 | return sb.ToString(); 83 | } 84 | 85 | /// Set the string to be boldface. 86 | /// The string. 87 | public static string SetBold(this string value) 88 | { 89 | return string.Format("{0}", value); 90 | } 91 | 92 | /// Sets a variable number of words to be boldface. 93 | /// The string. 94 | /// The variable number of word indicies. 95 | public static string SetBoldForWords(this string value, params int[] highlightedIndices) 96 | { 97 | string[] words = value.Split(' '); //split the string into an array of words 98 | StringBuilder sb = new StringBuilder(); 99 | for(int i=0; i < words.Length; i++) //and recombine the words into a string with tags for each highlightedIndex 100 | { 101 | if(Array.IndexOf(highlightedIndices, i) > -1) //highlightedIndices contains i 102 | { 103 | sb.Append(words[i].SetBold()); 104 | } 105 | else { sb.Append(words[i]); } 106 | sb.Append(' '); 107 | } 108 | return sb.ToString(); 109 | } 110 | 111 | /// Set the string to be italics. 112 | /// The string. 113 | public static string SetItalics(this string value) 114 | { 115 | return string.Format("{0}", value); 116 | } 117 | 118 | /// Sets a variable number of words to be italics. 119 | /// The string. 120 | /// The variable number of word indicies. 121 | public static string SetItalicsForWords(this string value, params int[] highlightedIndices) 122 | { 123 | string[] words = value.Split(' '); //split the string into an array of words 124 | StringBuilder sb = new StringBuilder(); 125 | for(int i=0; i < words.Length; i++) //and recombine the words into a string with tags for each highlightedIndex 126 | { 127 | if(Array.IndexOf(highlightedIndices, i) > -1) //highlightedIndices contains i 128 | { 129 | sb.Append(words[i].SetItalics()); 130 | } 131 | else { sb.Append(words[i]); } 132 | sb.Append(' '); 133 | } 134 | return sb.ToString(); 135 | } 136 | } 137 | 138 | /// A public enum of Colors for Rich Text tags built into Unity. 139 | public enum RichTextColor 140 | { 141 | /// aqua (same as cyan) #00ffffff 142 | aqua, 143 | /// black #000000ff 144 | black, 145 | /// blue #0000ffff 146 | blue, 147 | /// brown #a52a2aff 148 | brown, 149 | /// cyan (same as aqua) #00ffffff 150 | cyan, 151 | /// darkblue #0000a0ff 152 | darkblue, 153 | /// fuchsia (same as magenta) #ff00ffff 154 | fuchsia, 155 | /// green #008000ff 156 | green, 157 | /// grey #808080ff 158 | grey, 159 | /// lightblue #add8e6ff 160 | lightblue, 161 | /// lime #00ff00ff 162 | lime, 163 | /// magenta (same as fuchsia) #ff00ffff 164 | magenta, 165 | /// maroon #800000ff 166 | maroon, 167 | /// navy #000080ff 168 | navy, 169 | /// olive #808000ff 170 | olive, 171 | /// orange #ffa500ff 172 | orange, 173 | /// purple #800080ff 174 | purple, 175 | /// red #ff0000ff 176 | red, 177 | /// silver #c0c0c0ff 178 | silver, 179 | /// teal #008080ff 180 | teal, 181 | /// white #ffffffff 182 | white, 183 | /// yellow #ffff00ff 184 | yellow 185 | } 186 | -------------------------------------------------------------------------------- /#03-RichText/images/richText1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#03-RichText/images/richText1.png -------------------------------------------------------------------------------- /#03-RichText/images/richText2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#03-RichText/images/richText2.png -------------------------------------------------------------------------------- /#03-RichText/images/richText3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#03-RichText/images/richText3.png -------------------------------------------------------------------------------- /#03-RichText/images/richText4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#03-RichText/images/richText4.png -------------------------------------------------------------------------------- /#04-MoreEfficientYieldStatements/README.md: -------------------------------------------------------------------------------- 1 | # 04 - More Efficient Yield Statements 2 | 3 | When looking at Coroutine code online, one often sees something like 4 | 5 | ```C# 6 | private IEnumerator DoSomethingEveryFrame() 7 | { 8 | while(shouldDoSomething) 9 | { 10 | //do something 11 | yield return new WaitForEndOfFrame(); 12 | } 13 | } 14 | 15 | private IEnumerator DoSomethingAfterTime(float seconds) 16 | { 17 | yield return new WaitForSeconds(seconds); 18 | //do something 19 | } 20 | ``` 21 | 22 | Unbeknownst to many people, every instantiation of a new *WaitForSeconds* or *WaitForEndOfFrame* creates new objects of size ~20 bytes which must be subsequently removed by the garbage collector. As the garbage collector can collect garbage at any point, potentially resulting in a CPU spike and dropped frames, it is imperative to reduce garbage as much as possible. 23 | 24 | ## Null, WaitForFixedUpdate, WaitForEndOfFrame 25 | 26 | Consider the following update cycle (photo courtesy of [Wang Xuanyi](http://www.programering.com/a/MTN5gzMwATg.html)): 27 | 28 | ![](images/moreEfficientYieldStatements1.png) 29 | 30 | If one simply wants to run a block of code on the next frame, and it is irrelevant when this code will run, then **yield return null** is a good solution as no object needs to be created, resulting in roughly 9 bytes of garbage. Instead, if code must be ran in either the fixed update cycle, or at the end of the frame after all processing has occurred, then references to static *WaitForFixedUpdate* and *WaitForEndOfFrame* variables results in the creation of these variables only once, yet they can be used multiple times, even concurrently. 31 | 32 | ```C# 33 | /// A backing variable for FixedUpdate. 34 | static WaitForFixedUpdate _FixedUpdate; 35 | /// Waits until next fixed frame rate update function. 36 | public static WaitForFixedUpdate FixedUpdate 37 | { 38 | get{ return _FixedUpdate ?? (_FixedUpdate = new WaitForFixedUpdate()); } 39 | } 40 | 41 | /// A backing variable for EndOfFrame. 42 | private static WaitForEndOfFrame _EndOfFrame; 43 | /// Waits until the end of the frame after all cameras and GUI is rendered, just before displaying the frame on screen. 44 | public static WaitForEndOfFrame EndOfFrame 45 | { 46 | get{ return _EndOfFrame ?? (_EndOfFrame = new WaitForEndOfFrame()); } 47 | } 48 | ``` 49 | 50 | ## WaitForSeconds 51 | 52 | *WaitForSeconds* objects can be cached in a dictionary, whose keys is the wait time itself: 53 | 54 | ```C# 55 | /// A dictionary of WaitForSeconds whose keys are the wait time. 56 | private static Dictionary waitForSecondsDictionary = new Dictionary(); 57 | /// Suspends the coroutine execution for the given amount of seconds using scaled time. 58 | public static WaitForSeconds Seconds(float seconds) 59 | { 60 | //test if a WaitForSeconds with this wait time exists - if not, create one 61 | WaitForSeconds waitForSeconds; 62 | if(!waitForSecondsDictionary.TryGetValue(seconds, out waitForSeconds)) 63 | { 64 | waitForSecondsDictionary.Add(seconds, waitForSeconds = new WaitForSeconds(seconds)); 65 | } 66 | return waitForSeconds; 67 | } 68 | ``` 69 | 70 | By supplying a custom float comparer, we can avoid the values being boxed, inadvertently creating some garbage. 71 | 72 | ```C# 73 | /// A Float comparer used in the waitForSecondsDictionary. 74 | private class FloatComparer : IEqualityComparer 75 | { 76 | bool IEqualityComparer.Equals(float x, float y) { return x == y; } 77 | int IEqualityComparer.GetHashCode(float obj) { return obj.GetHashCode(); } 78 | } 79 | /// A dictionary of WaitForSeconds whose keys are the wait time. 80 | private static Dictionary waitForSecondsDictionary = new Dictionary(0, new FloatComparer()); 81 | ``` 82 | 83 | ## Summary 84 | 85 | By utilizing null in yield return statements when possible, and caching WaitForFixedFrame, WaitForSeconds and WaitForEndOfFrame variables, we can reduce the number of allocated objects, and help reduce the frequency of collected garbage. 86 | 87 | ## Further Reading 88 | 89 | [C# Coroutine WaitForSeconds Garbage Collection tip](https://forum.unity.com/threads/c-coroutine-waitforseconds-garbage-collection-tip.224878/) 90 | 91 | [Boxing and Unboxing](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/types/boxing-and-unboxing) -------------------------------------------------------------------------------- /#04-MoreEfficientYieldStatements/WaitFor.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Written by James Leahy. (c) 2017 DeFunc Art. 3 | * https://github.com/defuncart/ 4 | */ 5 | using System.Collections.Generic; 6 | using UnityEngine; 7 | 8 | /// Included in the DeFuncArt.Utilities namespace. 9 | namespace DeFuncArt.Utilities 10 | { 11 | /// A static class which contains cached values of WaitForSeconds, WaitForEndOfFrame, WaitForFixedUpdate. 12 | public static class WaitFor 13 | { 14 | /// A Float comparer used in the waitForSecondsDictionary. 15 | private class FloatComparer : IEqualityComparer 16 | { 17 | bool IEqualityComparer.Equals(float x, float y) 18 | { 19 | return x == y; 20 | } 21 | int IEqualityComparer.GetHashCode(float obj) 22 | { 23 | return obj.GetHashCode(); 24 | } 25 | } 26 | /// A dictionary of WaitForSeconds whose keys are the wait time. 27 | private static Dictionary waitForSecondsDictionary = new Dictionary(0, new FloatComparer()); 28 | /// Suspends the coroutine execution for the given amount of seconds using scaled time. 29 | public static WaitForSeconds Seconds(float seconds) 30 | { 31 | //test if a WaitForSeconds with this wait time exists - if not, create one 32 | WaitForSeconds waitForSeconds; 33 | if(!waitForSecondsDictionary.TryGetValue(seconds, out waitForSeconds)) 34 | { 35 | waitForSecondsDictionary.Add(seconds, waitForSeconds = new WaitForSeconds(seconds)); 36 | } 37 | return waitForSeconds; 38 | } 39 | 40 | /// A backing variable for FixedUpdate. 41 | static WaitForFixedUpdate _FixedUpdate; 42 | /// Waits until next fixed frame rate update function. 43 | public static WaitForFixedUpdate FixedUpdate 44 | { 45 | get{ return _FixedUpdate ?? (_FixedUpdate = new WaitForFixedUpdate()); } 46 | } 47 | 48 | /// A backing variable for EndOfFrame. 49 | private static WaitForEndOfFrame _EndOfFrame; 50 | /// Waits until the end of the frame after all cameras and GUI is rendered, just before displaying the frame on screen. 51 | public static WaitForEndOfFrame EndOfFrame 52 | { 53 | get{ return _EndOfFrame ?? (_EndOfFrame = new WaitForEndOfFrame()); } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /#04-MoreEfficientYieldStatements/images/moreEfficientYieldStatements1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#04-MoreEfficientYieldStatements/images/moreEfficientYieldStatements1.png -------------------------------------------------------------------------------- /#05-CustomInvoke/DAMonoBehaviourExtensions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Written by James Leahy. (c) 2017 DeFunc Art. 3 | * https://github.com/defuncart/ 4 | */ 5 | using DeFuncArt.Utilities; 6 | using System; 7 | using System.Collections; 8 | using UnityEngine; 9 | 10 | /// Included in the DeFuncArt.ExtensionMethods namespace. 11 | namespace DeFuncArt.ExtensionMethods 12 | { 13 | /// A collection of mono behaviour extention methods. 14 | public static class DAMonoBehaviourExtensions 15 | { 16 | /// Invokes an action after a given time. 17 | /// The action to invoke. 18 | /// The time in seconds. 19 | /// Whether cached yield values should be used. Defaults to true. 20 | public static Coroutine Invoke(this MonoBehaviour monoBehaviour, Action action, float time, bool useCachedYields = true) 21 | { 22 | return monoBehaviour.StartCoroutine(InvokeImplementation(action, time, useCachedYields)); 23 | } 24 | 25 | /// Coroutine which waits time seconds and then invokes the given action. 26 | private static IEnumerator InvokeImplementation(Action action, float time, bool useCachedYields) 27 | { 28 | //wait for time seconds then invoke the action. if useCachedYields is true, uses a cached WaitForSeconds, otherwise creates a new one 29 | yield return (useCachedYields ? WaitFor.Seconds(time) : new WaitForSeconds(time)); 30 | action(); 31 | } 32 | 33 | /// Invokes an action after a given time, then repeatedly every repeateRate seconds. 34 | /// The action to invoke. 35 | /// The time in seconds. 36 | /// The repeat rate in seconds. 37 | /// Whether cached yield values should be used. Defaults to true. 38 | public static Coroutine InvokeRepeating(this MonoBehaviour monoBehaviour, Action action, float time, float repeatRate, bool useCachedYields = true) 39 | { 40 | return monoBehaviour.StartCoroutine(InvokeRepeatingImplementation(action, time, repeatRate, useCachedYields)); 41 | } 42 | 43 | /// The coroutine implementation of InvokeRepeating. 44 | private static IEnumerator InvokeRepeatingImplementation(Action action, float time, float repeatRate, bool useCachedYields) 45 | { 46 | //wait for a given time then indefiently loop - if useCachedYields is true, uses a cached WaitForSeconds, otherwise creates a new one 47 | yield return (useCachedYields ? WaitFor.Seconds(time) : new WaitForSeconds(time)); 48 | while(true) 49 | { 50 | //invokes the action then waits repeatRate seconds - if useCachedYields is true, uses a cached WaitForSeconds, otherwise creates a new one 51 | action(); 52 | yield return (useCachedYields ? WaitFor.Seconds(repeatRate) : new WaitForSeconds(repeatRate)); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /#05-CustomInvoke/README.md: -------------------------------------------------------------------------------- 1 | # 05 - Custom Invoke 2 | 3 | ```public void Invoke(string methodName, float time)``` allows one to trigger a given method (via string methodName) after a delay of time seconds. However there are two notable issues: 4 | 5 | 1. Invoke uses reflection which can have a large overhead and should be avoided when possible. 6 | 2. Invoke methods are difficult to debug as methods called by name are hard to track in code. 7 | 8 | A better Invoke can be acomplished by using Coroutines and Actions: 9 | 10 | ```c# 11 | public static Coroutine Invoke(this MonoBehaviour monoBehaviour, Action action, float time) 12 | { 13 | return monoBehaviour.StartCoroutine(InvokeImplementation(action, time)); 14 | } 15 | 16 | private static IEnumerator InvokeImplementation(Action action, float time) 17 | { 18 | yield return new WaitForSeconds(time); 19 | action(); 20 | } 21 | ``` 22 | 23 | which can be easily called within any class that extends MonoBehaviour 24 | 25 | ```C# 26 | private void Test() 27 | { 28 | Debug.Log("Test using custom invoke"); 29 | } 30 | 31 | this.Invoke(Test, 3f); 32 | 33 | this.Invoke(() => { 34 | Debug.Log("Test using closure"); 35 | }, 4f); 36 | ``` 37 | 38 | This custom Invoke can easily be cancelled by holding a reference to its returned coroutine. 39 | 40 | ```C# 41 | public static void CancelInvoke(this MonoBehaviour monoBehaviour, Coroutine coroutine) 42 | { 43 | monoBehaviour.StopCoroutine(coroutine); 44 | } 45 | 46 | Coroutine coroutine = this.Invoke(Test, 10f); 47 | this.CancelInvoke(coroutine); 48 | ``` 49 | 50 | Custom Invoke could be further extended with the ability to include parameters too, although this isn't something I've found a use for yet. 51 | 52 | ```C# 53 | public static Coroutine Invoke(this MonoBehaviour monoBehaviour, Action action, T parameter, float time) where T : class 54 | { 55 | return monoBehaviour.StartCoroutine(InvokeImplementation(action, parameter, time)); 56 | } 57 | 58 | private static IEnumerator InvokeImplementation(Action action, T parameter, float time) where T : class 59 | { 60 | yield return new WaitForSeconds(time); 61 | action(parameter); 62 | } 63 | 64 | private void TestWithParameter(string param) 65 | { 66 | Debug.Log(param); 67 | } 68 | 69 | this.Invoke(TestWithParameter, "Test using custom invoke and param", 5f); 70 | ``` 71 | 72 | As discussed in [the previous tip](https://github.com/defuncart/50-unity-tips/tree/master/%2304-MoreEfficientYieldStatements), we should limit the number of created *WaitForSeconds* when possible. This can be achieved utilizing **WaitFor** and caching *WaitForSeconds* variables. 73 | 74 | ```C# 75 | public static Coroutine Invoke(this MonoBehaviour monoBehaviour, Action action, float time, bool useCachedWaits = true) 76 | { 77 | return monoBehaviour.StartCoroutine(InvokeImplementation(action, time, useCachedWaits)); 78 | } 79 | 80 | private static IEnumerator InvokeImplementation(Action action, float time, bool useCachedWaits) 81 | { 82 | //wait for time seconds then invoke the action. if useCachedWaits is true, uses a cached WaitForSeconds, otherwise creates a new one 83 | yield return (useCachedWaits ? WaitFor.Seconds(time) : new WaitForSeconds(time)); 84 | action(); 85 | } 86 | ``` 87 | 88 | The *useCachedWaits* variable can be set to false if it would be more desirable in creating a new *WaitForSeconds*, that would be subsequently removed by the garbage collector. 89 | 90 | ```C# 91 | this.Invoke(action: () => { 92 | Debug.Log("Some action with a unique wait time that will only be triggered once."); 93 | }, time: 4f, useCachedWaits: false); 94 | ``` 95 | 96 | Finally, ```public void InvokeRepeating(string methodName, float time, float repeatRate)``` can be similarly implemented using Coroutines. 97 | 98 | ```C# 99 | public static Coroutine InvokeRepeating(this MonoBehaviour monoBehaviour, Action action, float time, float repeatRate, bool useCachedWaits = true) 100 | { 101 | return monoBehaviour.StartCoroutine(InvokeRepeatingImplementation(action, time, repeatRate, useCachedWaits)); 102 | } 103 | 104 | /// The coroutine implementation of InvokeRepeating. 105 | private static IEnumerator InvokeRepeatingImplementation(Action action, float time, float repeatRate, bool useCachedWaits) 106 | { 107 | //wait for a given time then indefiently loop - if useCachedWaits is true, uses a cached WaitForSeconds, otherwise creates a new one 108 | yield return (useCachedWaits ? WaitFor.Seconds(time) : new WaitForSeconds(time)); 109 | while(true) 110 | { 111 | //invokes the action then waits repeatRate seconds - if useCachedYields is true, uses a cached WaitForSeconds, otherwise creates a new one 112 | action(); 113 | yield return (useCachedYields ? WaitFor.Seconds(repeatRate) : new WaitForSeconds(repeatRate)); 114 | } 115 | } 116 | ``` 117 | 118 | These custom invoke methods could also be added to a custom class inherited from MonoBehaviour which one would then always use as their base class. -------------------------------------------------------------------------------- /#05-CustomInvoke/WaitFor.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Written by James Leahy. (c) 2017 DeFunc Art. 3 | * https://github.com/defuncart/ 4 | */ 5 | using System.Collections.Generic; 6 | using UnityEngine; 7 | 8 | /// Included in the DeFuncArt.Utilities namespace. 9 | namespace DeFuncArt.Utilities 10 | { 11 | /// A static class which contains cached values of WaitForSeconds, WaitForEndOfFrame, WaitForFixedUpdate. 12 | public static class WaitFor 13 | { 14 | /// A Float comparer used in the waitForSecondsDictionary. 15 | private class FloatComparer : IEqualityComparer 16 | { 17 | bool IEqualityComparer.Equals(float x, float y) 18 | { 19 | return x == y; 20 | } 21 | int IEqualityComparer.GetHashCode(float obj) 22 | { 23 | return obj.GetHashCode(); 24 | } 25 | } 26 | /// A dictionary of WaitForSeconds whose keys are the wait time. 27 | private static Dictionary waitForSecondsDictionary = new Dictionary(0, new FloatComparer()); 28 | /// Suspends the coroutine execution for the given amount of seconds using scaled time. 29 | public static WaitForSeconds Seconds(float seconds) 30 | { 31 | //test if a WaitForSeconds with this wait time exists - if not, create one 32 | WaitForSeconds waitForSeconds; 33 | if(!waitForSecondsDictionary.TryGetValue(seconds, out waitForSeconds)) 34 | { 35 | waitForSecondsDictionary.Add(seconds, waitForSeconds = new WaitForSeconds(seconds)); 36 | } 37 | return waitForSeconds; 38 | } 39 | 40 | /// A backing variable for FixedUpdate. 41 | static WaitForFixedUpdate _FixedUpdate; 42 | /// Waits until next fixed frame rate update function. 43 | public static WaitForFixedUpdate FixedUpdate 44 | { 45 | get{ return _FixedUpdate ?? (_FixedUpdate = new WaitForFixedUpdate()); } 46 | } 47 | 48 | /// A backing variable for EndOfFrame. 49 | private static WaitForEndOfFrame _EndOfFrame; 50 | /// Waits until the end of the frame after all cameras and GUI is rendered, just before displaying the frame on screen. 51 | public static WaitForEndOfFrame EndOfFrame 52 | { 53 | get{ return _EndOfFrame ?? (_EndOfFrame = new WaitForEndOfFrame()); } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /#06-PlayerPreferences/PlayerPreferences.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Written by James Leahy (c) 2017 DeFunc Art. 3 | */ 4 | using UnityEngine; 5 | 6 | /// A simple extention of PlayerPrefs which allows the saving of boolean values. 7 | /// Extension methods are only valid on instances, hence the creation of this class. 8 | public static class PlayerPreferences 9 | { 10 | /// A public struct of keys for PlayerPreferences. 11 | public struct Keys 12 | { 13 | public static readonly string gameInitialized = "gameInitialized"; 14 | } 15 | 16 | /// Initializes notable key-value pairs to their initial values. 17 | public static void InitKeys() 18 | { 19 | SetBool(Keys.gameInitialized, false); 20 | } 21 | 22 | /// Determine whether a preference with the specified key exists. 23 | public static bool HasKey(string key) 24 | { 25 | return PlayerPrefs.HasKey(key); 26 | } 27 | 28 | /// Sets a new preference (or overwrites a previous) key-value pair. 29 | public static void SetInt(string key, int value) 30 | { 31 | PlayerPrefs.SetInt(key, value); PlayerPrefs.Save(); 32 | } 33 | 34 | /// Gets the value of a integer preference for a given key. 35 | public static int GetInt(string key) 36 | { 37 | if(!HasKey(key)) { Debug.LogError(string.Format("Key \"{0}\" not found!", key)); } 38 | return PlayerPrefs.GetInt(key); 39 | } 40 | 41 | /// Sets a new preference (or overwrites a previous) key-value pair. 42 | public static void SetBool(string key, bool value) 43 | { 44 | SetInt(key, value ? 1 : 0); 45 | } 46 | 47 | /// Gets the value of a boolean preference for a given key. 48 | public static bool GetBool(string key) 49 | { 50 | if(!HasKey(key)) { Debug.LogError(string.Format("Key \"{0}\" not found!", key)); } 51 | return GetInt(key) == 1; 52 | } 53 | 54 | /// Sets a new preference (or overwrites a previous) key-value pair. 55 | public static void SetFloat(string key, float value) 56 | { 57 | PlayerPrefs.SetFloat(key, value); PlayerPrefs.Save(); 58 | } 59 | 60 | /// Gets the value of a floating point preference for a given key. 61 | public static float GetFloat(string key) 62 | { 63 | if(!HasKey(key)) { Debug.LogError(string.Format("Key \"{0}\" not found!", key)); } 64 | return PlayerPrefs.GetFloat(key); 65 | } 66 | 67 | /// Sets a new deault (or overwrites a previous) key-value pair. 68 | public static void SetString(string key, string value) 69 | { 70 | PlayerPrefs.SetString(key, value); PlayerPrefs.Save(); 71 | } 72 | 73 | /// Gets the value of a string preference for a given key. 74 | public static string GetString(string key) 75 | { 76 | if(!HasKey(key)) { Debug.LogError(string.Format("Key \"{0}\" not found!", key)); } 77 | return PlayerPrefs.GetString(key); 78 | } 79 | 80 | /// Removes all preferences. 81 | public static void DeleteAll() 82 | { 83 | PlayerPrefs.DeleteAll(); 84 | } 85 | 86 | /// Remove a preference. 87 | public static void DeleteKey(string key) 88 | { 89 | if(!HasKey(key)) { Debug.LogError(string.Format("Key \"{0}\" not found!", key)); } 90 | PlayerPrefs.DeleteKey(key); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /#06-PlayerPreferences/README.md: -------------------------------------------------------------------------------- 1 | # 06 - Player Preferences 2 | 3 | [*PlayerPrefs*](https://docs.unity3d.com/ScriptReference/PlayerPrefs.html) is a static class which one can store and access player preferences between scenes and game sessions. The class is useful for storing basic values but has two main drawbacks: 4 | 5 | 1. it is severely limited in that it can only store strings, ints and floats. 6 | 2. the data is completely unsecure 7 | 8 | With regard to security, on Desktop the values are stored to a PLIST on macOS or registry on Windows, both of which are accessible and modifiable by the user. [*EncryptedPlayerPrefs*](https://gist.github.com/ftvs/5299600) by Sven Magnus is one approach to rectify this issue, while there are numerous paid assets available on the asset store. 9 | 10 | The lack of support for booleans can be easily solved by using 1 and 0, but for readability I would prefer *SetBool*, *GetBool* methods. As *PlayerPrefs* is called statically, it is impossible to add an Extention Method. Thus I created the *PlayerPreferences* class which, although guilty of being unadulterated syntactic sugar, is arguably more readable. 11 | 12 | ```C# 13 | public static void SetBool(string key, bool value) 14 | { 15 | SetInt(key, value ? 1 : 0); 16 | } 17 | 18 | public static bool GetBool(string key) 19 | { 20 | if(!HasKey(key)) { Debug.LogError(string.Format("Key \"{0}\" not found!", key)); } 21 | return GetInt(key) == 1; 22 | } 23 | ``` 24 | 25 | Also to be less error prone, I prefer the use of constant string keys 26 | 27 | ```C# 28 | public struct Keys 29 | { 30 | public static readonly string gameInitialized = "gameInitialized"; 31 | } 32 | ``` 33 | 34 | which makes the function calls much more readable 35 | 36 | ```C# 37 | if(!PlayerPreferences.GetBool(PlayerPreferences.Keys.gameInitialized)) 38 | { 39 | //do stuff 40 | PlayerPreferences.SetBool(PlayerPreferences.Keys.gameInitialized, true); 41 | } 42 | ``` 43 | 44 | So today's tip is not only somewhat subjective, but is also probably not too useful. Nevertheless it is something I ocassionally use so I felt it was worth mentioning. -------------------------------------------------------------------------------- /#07-BinarySerialization/BinarySerializer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Written by James Leahy. (c) 2017 DeFunc Art. 3 | */ 4 | using System; 5 | using System.IO; 6 | using System.Runtime.Serialization.Formatters.Binary; 7 | using UnityEngine; 8 | 9 | /// A Binary Serializer which serializes and deserializes classes into binary format. 10 | public class BinarySerializer 11 | { 12 | /// Load an instance of the class T from file. 13 | /// Filename of the file to load. 14 | /// The object type to be loaded. 15 | /// A loaded instance of the class T. 16 | public static T Load(string filename) where T: class 17 | { 18 | string path = PathForFilename(filename); 19 | if(BinarySerializer.PathExists(path)) 20 | { 21 | try 22 | { 23 | using(Stream stream = File.OpenRead(path)) 24 | { 25 | BinaryFormatter formatter = new BinaryFormatter(); 26 | return formatter.Deserialize(stream) as T; 27 | } 28 | } 29 | catch(Exception e) { Debug.LogWarning(e.Message); } 30 | } 31 | return default(T); 32 | } 33 | 34 | /// Save an instance of the class T to file. 35 | /// Filename of file to save. 36 | /// The class object to save. 37 | /// The object type to be saved. 38 | public static void Save(string filename, T data) where T: class 39 | { 40 | string path = PathForFilename(filename); 41 | using(Stream stream = File.OpenWrite(path)) 42 | { 43 | BinaryFormatter formatter = new BinaryFormatter(); 44 | formatter.Serialize(stream, data); 45 | } 46 | } 47 | 48 | /// Determine whether a file exists at a given filepath. 49 | /// Filepath of the file. 50 | /// True if the file exists, otherwise file. 51 | private static bool PathExists(string filepath) 52 | { 53 | return File.Exists(filepath); 54 | } 55 | 56 | /// Determine if a File with a given filename exists. 57 | /// Filename of the file. 58 | /// Bool if the file exists. 59 | public static bool FileExists(string filename) 60 | { 61 | return PathExists(PathForFilename(filename)); 62 | } 63 | 64 | /// Delete a File with a given filename. 65 | /// Filename of the file. 66 | public static void DeleteFile(string filename) 67 | { 68 | string filepath = PathForFilename(filename); 69 | if(PathExists(filepath)) 70 | { 71 | File.Delete(filepath); 72 | } 73 | } 74 | 75 | /// Determine the correct filepath for a filename. In UNITY_EDITOR this is in the project's root 76 | /// folder, on mobile it is in the persistent data folder while standalone is the data folder. 77 | /// Filename of the file. 78 | /// The filepath for a given file on the current device. 79 | private static string PathForFilename(string filename) 80 | { 81 | string path = filename; //for editor 82 | #if UNITY_STANDALONE 83 | path = Path.Combine(Application.dataPath, filename); 84 | #elif UNITY_IOS || UNITY_ANDROID 85 | path = Path.Combine(Application.persistentDataPath, filename); 86 | #endif 87 | return path; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /#07-BinarySerialization/README.md: -------------------------------------------------------------------------------- 1 | # 07 - Binary Serialization 2 | 3 | *Serialization* is the process of converting an object (or an entire graph of connected objects) into a stream of bytes so that it can be recreated again when needed (deserialization) [[MSDN](https://msdn.microsoft.com/en-us/library/7ay27kt9(v=vs.110).aspx)]. In Unity there are four predominate ways to serialize objects: 4 | 1. Binary Format 5 | 2. Scriptable Objects 6 | 3. JSON 7 | 4. XML 8 | 9 | As you might guess from the name, *Binary Serialization* serializes and deserializes an object into binary format. There are a few points to consider: 10 | 11 | - Any serializable Unity Object or class is automatically compatible. 12 | - Non serializable classes like *Vector*, *Color* etc. aren't compatible. One could instead store the individual values as an array and write helper methods, or investigate [ISerializationSurrogate](https://msdn.microsoft.com/en-us/library/system.runtime.serialization.surrogateselector). 13 | - The resulting data is binary and thus not human readable. 14 | - This works well for something that should be secure like game saves, but not anything that your team may need to edit like level variables, enemy properties etc. 15 | - One must code the process from scratch. 16 | - This isn't difficult and allows maximum versatility, but may seen more intimidating than JSON or XML. 17 | 18 | BinaryFormatter allows us to serialize/deserialze a class to binary 19 | 20 | ```C# 21 | 22 | public static void Save(string path, T data) where T: class 23 | { 24 | using(Stream stream = File.OpenWrite(path)) 25 | { 26 | BinaryFormatter formatter = new BinaryFormatter(); 27 | formatter.Serialize(stream, data); 28 | } 29 | } 30 | 31 | public static T Load(string path) where T: class 32 | { 33 | if(File.Exists(path)) 34 | { 35 | try 36 | { 37 | using(Stream stream = File.OpenRead(path)) 38 | { 39 | BinaryFormatter formatter = new BinaryFormatter(); 40 | return formatter.Deserialize(stream) as T; 41 | } 42 | } 43 | catch(Exception e) { Debug.LogWarning(e.Message); } 44 | } 45 | return default(T); 46 | } 47 | ``` 48 | 49 | Thus, given a class of player variables 50 | 51 | ```C# 52 | [System.Serializable] 53 | public class PlayerData 54 | { 55 | public string name { get; private set; } 56 | public int score { get; private set; } 57 | [System.NonSerialized] private int tempValue; 58 | } 59 | ``` 60 | 61 | we can easily serialize/deserialze this class to/from disk: 62 | 63 | ```C# 64 | string path = "PlayerData.sav"; 65 | PlayerData data; 66 | if(File.Exists(path)) 67 | { 68 | data = Serializer.Load(path); 69 | } 70 | else 71 | { 72 | data = new PlayerData(); 73 | Serializer.Save(path, data); 74 | } 75 | Debug.Log(data.ToString()); 76 | ``` 77 | 78 | In summary, 79 | - Any class marked *Serializable* can be serialized to disk. 80 | - Public, private and protected members will be saved. If you don't want a certain property to be saved, mark it as *NonSerialized*. 81 | - If the implementation of a class changes, then loading may fail. New properties can be appended to the class and saved, but the alteration of existing properties will stop the BinaryFormatter from loading the class. Maybe not a big deal when developing, but for a shipped product... -------------------------------------------------------------------------------- /#08-ScriptableObjects/LevelData.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Written by James Leahy (c) 2017 DeFunc Art. 3 | */ 4 | using UnityEngine; 5 | 6 | [CreateAssetMenu(fileName = "New LevelData", menuName = "LevelData", order = 1000)] 7 | public class LevelData : ScriptableObject 8 | { 9 | public int levelIndex; 10 | public int numberOfPlayerLives; 11 | public bool isTimed; 12 | public float timeLimit; 13 | 14 | public override string ToString () 15 | { 16 | return string.Format("[LevelData:levelIndex={0}, numberOfPlayerLives={1}, isTimed={2}, timeLimit={3}]", 17 | levelIndex, numberOfPlayerLives, isTimed, timeLimit); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /#08-ScriptableObjects/ManualCreateMenuItem/LevelDataAsset.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Written by James Leahy (c) 2017 DeFunc Art. 3 | */ 4 | using UnityEngine; 5 | using UnityEditor; 6 | 7 | public class LevelDataAsset 8 | { 9 | [MenuItem("Assets/Create/LevelData")] 10 | public static void CreateAsset() 11 | { 12 | ScriptableObjectUtility.CreateAsset(); 13 | } 14 | } -------------------------------------------------------------------------------- /#08-ScriptableObjects/ManualCreateMenuItem/README.md: -------------------------------------------------------------------------------- 1 | # Manually CreateMenuItem 2 | 3 | To turn a scriptable object into an asset, we could simply place a [*CreateAssetMenu*](https://docs.unity3d.com/ScriptReference/CreateAssetMenuAttribute.html) attribute above our class declaration: 4 | 5 | ``` 6 | [CreateAssetMenu(fileName = "New LevelData", menuName = "LevelData", order = 1000)] 7 | ``` 8 | 9 | Another option is to create an editor script that resides within an Editor folder: 10 | 11 | ```C# 12 | public class LevelDataAsset 13 | { 14 | [MenuItem("Assets/Create/LevelData")] 15 | public static void CreateAsset() 16 | { 17 | LevelData asset = ScriptableObject.CreateInstance(); 18 | AssetDatabase.CreateAsset(asset, "Assets/New LevelData.asset"); 19 | AssetDatabase.SaveAssets(); 20 | AssetDatabase.Refresh(); 21 | EditorUtility.FocusProjectWindow(); 22 | Selection.activeObject = asset; 23 | } 24 | } 25 | 26 | ``` 27 | 28 | However, it seems pointless to write the same code for different scriptable objects over and over again, while it would be nice that the asset is saved where we are clicking, as opposed to the root of the Assets folder. So using this utility 29 | 30 | ```C# 31 | public static class ScriptableObjectUtility 32 | { 33 | public static void CreateAsset() where T : ScriptableObject 34 | { 35 | //firstly we need to determine where to save the asset to. Try to get path of the editor's active object 36 | string path = AssetDatabase.GetAssetPath(Selection.activeObject); 37 | //if there is no selected object, set default path to Assets folder 38 | if(path.IsNullOrEmpty()) { path = "Assets"; } 39 | //else if the current object is a file, then remove the file's name from the path 40 | else if(Path.GetExtension(path) != string.Empty) { path = path.Replace(Path.GetFileName(path), string.Empty); } 41 | //add the asset's filename, e.g. "New LevelData.asset" 42 | path = AssetDatabase.GenerateUniqueAssetPath(string.Format("{0}/New {1}.asset", path, typeof(T).ToString())); 43 | 44 | //create a new instance of T, save it as an asset, and set it as active in the editor 45 | T asset = ScriptableObject.CreateInstance(); 46 | AssetDatabase.CreateAsset(asset, path); 47 | AssetDatabase.SaveAssets(); 48 | AssetDatabase.Refresh(); 49 | EditorUtility.FocusProjectWindow(); 50 | Selection.activeObject = asset; 51 | } 52 | } 53 | ``` 54 | 55 | we could easily create our asset as follows: 56 | 57 | ```C# 58 | public class LevelDataAsset 59 | { 60 | [MenuItem("Assets/Create/LevelData")] 61 | public static void CreateAsset() 62 | { 63 | ScriptableObjectUtility.CreateAsset(); 64 | } 65 | } 66 | 67 | ``` 68 | 69 | However, as we always need to write a new editor script for each scrictable object, I still feel that *CreateAssetMenu* is a better approach. -------------------------------------------------------------------------------- /#08-ScriptableObjects/ManualCreateMenuItem/ScriptableObjectUtility.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Written by James Leahy (c) 2017 DeFunc Art. 3 | */ 4 | using System.IO; 5 | using UnityEditor; 6 | using UnityEngine; 7 | 8 | /// Utilities for the ScriptableObject class. 9 | public static class ScriptableObjectUtility 10 | { 11 | /// A helper method which creates a new asset of a given type. 12 | public static void CreateAsset() where T : ScriptableObject 13 | { 14 | //firstly we need to determine where to save the asset to. Try to get path of the editor's active object 15 | string path = AssetDatabase.GetAssetPath(Selection.activeObject); 16 | //if there is no selected object, set default path to Assets folder 17 | if(path.IsNullOrEmpty()) { path = "Assets"; } 18 | //else if the current object is a file, then remove the file's name from the path 19 | else if(Path.GetExtension(path) != string.Empty) { path = path.Replace(Path.GetFileName(path), string.Empty); } 20 | //add the asset's filename, e.g. "New LevelData.asset" 21 | path = AssetDatabase.GenerateUniqueAssetPath(string.Format("{0}/New {1}.asset", path, typeof(T).ToString())); 22 | 23 | //create a new instance of T, save it as an asset, and set it as active in the editor 24 | T asset = ScriptableObject.CreateInstance(); 25 | AssetDatabase.CreateAsset(asset, path); 26 | AssetDatabase.SaveAssets(); 27 | AssetDatabase.Refresh(); 28 | EditorUtility.FocusProjectWindow(); 29 | Selection.activeObject = asset; 30 | } 31 | } -------------------------------------------------------------------------------- /#08-ScriptableObjects/README.md: -------------------------------------------------------------------------------- 1 | # 08 - Scriptable Objects 2 | 3 | A [*ScriptableObject*](https://docs.unity3d.com/ScriptReference/ScriptableObject.html) is a data container which 4 | - has similar functionality to MonoBehaviour but cannot be attached to a GameObject 5 | - supports Serialization and can be saved as an asset in the Unity project 6 | - can be loaded on runtime but not saved => not suitable for saving a game state 7 | - serialization/deserialization is built in and fully functional, unlike JSON, XML 8 | - persists settings changed during editor play mode to disk 9 | - optimizes the loading of data, by loading it only when needed 10 | 11 | Scriptable objects are very useful when one wishes to vary certain parameters during gameplay (e.g. level variables by the Game Designer) within Unity and persist those changes. So today lets consider that there's a game scene which is repurposed for many different levels. Each level would have it's own index, number of lives, certain enemies etc. 12 | 13 | ```C# 14 | public class LevelData : ScriptableObject 15 | { 16 | public int levelIndex; 17 | public int numberOfPlayerLives; 18 | public bool isTimed; 19 | public float timeLimit; 20 | } 21 | ``` 22 | 23 | To turn this scriptable object into an asset, we could write our own editor script (see the **ManualCreateMenuItem** subfolder) or simply place a [*CreateAssetMenu*](https://docs.unity3d.com/ScriptReference/CreateAssetMenuAttribute.html) attribute above our class declaration: 24 | 25 | ``` 26 | [CreateAssetMenu(fileName = "New LevelData", menuName = "LevelData", order = 1000)] 27 | ``` 28 | 29 | which adds the menu item LevelData to the create menu. 30 | 31 | ![](images/scriptableObjects1.png) 32 | 33 | I don't fully understand *order* but from what I've tested, 1 is up top, 100 is above *Scene* and 1000 is below *Legacy*. 34 | 35 | With our GameManager class containing a reference to a LevelData object, 36 | 37 | ```C# 38 | public class GameManager : MonoBehaviour 39 | { 40 | [SerializeField] private LevelData level; 41 | [SerializeField] private Text numberOfLivesText; 42 | 43 | private void Start() 44 | { 45 | //set up for level.levelIndex 46 | numberOfLivesText.text = level.numberOfPlayerLives.ToString(); 47 | } 48 | 49 | private void Update() 50 | { 51 | if(level.isTimed) 52 | { 53 | //update timer 54 | } 55 | } 56 | } 57 | ``` 58 | 59 | we can now tweak LevelData in the editor during play mode, and our settings will persist. Moreover, this is a workflow which is user friendly for non-programmers: yes, one could edit variables in an XML file but that is error prone and teadious. 60 | 61 | Scriptable objects are something I imagine that I'll start to rely more and more on, and as stored databases within the assets they are a solid design choice. 62 | 63 | For more information check out this [live training tutorial](https://unity3d.com/learn/tutorials/modules/beginner/live-training-archive/scriptable-objects) from Unity, or this [Saving Game Data](https://www.youtube.com/watch?v=ItZbTYO0Mnw) tutorial. Here is an [interesting script](https://gist.github.com/PachowStudios/2be3d01df2edcdf69116) by [Ryan Shea](https://github.com/PachowStudios) in which one can right click on any inspector field that accepts a *ScriptableObject* to automatically create an asset and assign it to that field. -------------------------------------------------------------------------------- /#08-ScriptableObjects/images/scriptableObjects1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#08-ScriptableObjects/images/scriptableObjects1.png -------------------------------------------------------------------------------- /#09-JSONSerialization/JSONSerializer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Written by James Leahy. (c) 2017 DeFunc Art. 3 | */ 4 | using System.IO; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using UnityEngine; 8 | 9 | public class JSONSerializer 10 | { 11 | #region Array/Dictionary Serialization 12 | 13 | /// Serializes an object of type T to JSON. 14 | /// A JSON representation of the object T. 15 | /// The object T. 16 | public static string ToJson(T data) where T : class 17 | { 18 | if(typeof(T) == typeof(string[])) { return ToJsonStringArray(data as string[]); } 19 | else if(typeof(T) == typeof(int[])) { return ToJsonIntArray(data as int[]); } 20 | else if(typeof(T) == typeof(float[])) { return ToJsonFloatArray(data as float[]); } 21 | else if(typeof(T) == typeof(Dictionary)) 22 | { 23 | return ToJsonStringStringDictionary(data as Dictionary); 24 | } 25 | else if(typeof(T) == typeof(Dictionary)) 26 | { 27 | return ToJsonStringIntDictionary(data as Dictionary); 28 | } 29 | else if(typeof(T) == typeof(Dictionary)) 30 | { 31 | return ToJsonStringFloatDictionary(data as Dictionary); 32 | } 33 | else 34 | { 35 | Debug.LogError(string.Format("Type {0} is not supported.", typeof(T).Name)); return null; 36 | } 37 | } 38 | 39 | /// Deserializes an object of type T from JSON. 40 | /// The object T. 41 | /// A JSON representation of the object T. 42 | public static T FromJson(string json) where T : class 43 | { 44 | if(typeof(T) == typeof(string[])) { return FromJsonStringArray(json) as T; } 45 | if(typeof(T) == typeof(int[])) { return FromJsonIntArray(json) as T; } 46 | if(typeof(T) == typeof(float[])) { return FromJsonFloatArray(json) as T; } 47 | else if(typeof(T) == typeof(Dictionary)) 48 | { 49 | return FromJsonStringStringDictionary(json) as T; 50 | } 51 | else if(typeof(T) == typeof(Dictionary)) 52 | { 53 | return FromJsonStringIntDictionary(json) as T; 54 | } 55 | else if(typeof(T) == typeof(Dictionary)) 56 | { 57 | return FromJsonStringFloatDictionary(json) as T; 58 | } 59 | else 60 | { 61 | Debug.LogError(string.Format("Type {0} is not supported.", typeof(T).Name)); return null; 62 | } 63 | } 64 | 65 | #endregion 66 | 67 | /* 68 | * Presently array types for top-level JSON deserialization isn't natively supported. 69 | * However, by wrapping the array in an object, it can be deserialized. 70 | * Each supported type implements ToJson and FromJson. 71 | */ 72 | 73 | #region Arrays 74 | 75 | /* string[] */ 76 | 77 | [System.Serializable] 78 | private class TopLevelStringArray 79 | { 80 | public string[] array; 81 | } 82 | 83 | private static string ToJsonStringArray(string[] array) 84 | { 85 | TopLevelStringArray topLevelArray = new TopLevelStringArray() { array = array }; 86 | return JsonUtility.ToJson(topLevelArray); 87 | } 88 | 89 | private static string[] FromJsonStringArray(string json) 90 | { 91 | TopLevelStringArray topLevelArray = JsonUtility.FromJson(json); 92 | return topLevelArray.array; 93 | } 94 | 95 | /* int[] */ 96 | 97 | [System.Serializable] 98 | private class TopLevelIntArray 99 | { 100 | public int[] array; 101 | } 102 | 103 | private static string ToJsonIntArray(int[] array) 104 | { 105 | TopLevelIntArray topLevelArray = new TopLevelIntArray() { array = array }; 106 | return JsonUtility.ToJson(topLevelArray); 107 | } 108 | 109 | private static int[] FromJsonIntArray(string json) 110 | { 111 | TopLevelIntArray topLevel = JsonUtility.FromJson(json); 112 | return topLevel.array; 113 | } 114 | 115 | /* float[] */ 116 | 117 | [System.Serializable] 118 | private class TopLevelFloatArray 119 | { 120 | public float[] array; 121 | } 122 | 123 | private static string ToJsonFloatArray(float[] array) 124 | { 125 | TopLevelFloatArray topLevelArray = new TopLevelFloatArray() { array = array }; 126 | return JsonUtility.ToJson(topLevelArray); 127 | } 128 | 129 | private static float[] FromJsonFloatArray(string json) 130 | { 131 | TopLevelFloatArray topLevel = JsonUtility.FromJson(json); 132 | return topLevel.array; 133 | } 134 | 135 | #endregion 136 | 137 | 138 | /* 139 | * Presently dictionary deserialization isn't natively support. However, by defining custom 140 | * objects (StringString, StringFloat, StringStringArray etc.), and objects contain arrays of 141 | * these objects, such an array can be natively deserialized as a custom object and then 142 | * converted into the required dictionary. 143 | * Each supported type implements ToJson and FromJson. 144 | */ 145 | 146 | #region Dictionaries 147 | 148 | /* Dictionary */ 149 | 150 | [System.Serializable] 151 | private class StringStringDictionaryArray 152 | { 153 | public StringStringDictionary[] items; 154 | } 155 | 156 | [System.Serializable] 157 | private class StringStringDictionary 158 | { 159 | public string key; 160 | public string value; 161 | } 162 | 163 | private static string ToJsonStringStringDictionary(Dictionary dictionary) 164 | { 165 | List dictionaryItemsList = new List(); 166 | foreach(KeyValuePair kvp in dictionary) 167 | { 168 | dictionaryItemsList.Add( new StringStringDictionary(){ key = kvp.Key, value = kvp.Value } ); 169 | } 170 | 171 | StringStringDictionaryArray dictionaryArray = new StringStringDictionaryArray(){ items = dictionaryItemsList.ToArray() }; 172 | return JsonUtility.ToJson(dictionaryArray); 173 | } 174 | 175 | private static Dictionary FromJsonStringStringDictionary(string json) 176 | { 177 | StringStringDictionaryArray loadedData = JsonUtility.FromJson(json); 178 | Dictionary dictionary = new Dictionary(); 179 | for(int i=0; i < loadedData.items.Length; i++) 180 | { 181 | dictionary.Add(loadedData.items[i].key, loadedData.items[i].value); 182 | } 183 | return dictionary; 184 | } 185 | 186 | /* Dictionary */ 187 | 188 | [System.Serializable] 189 | private class StringIntDictionaryArray 190 | { 191 | public StringIntDictionary[] items; 192 | } 193 | 194 | [System.Serializable] 195 | private class StringIntDictionary 196 | { 197 | public string key; 198 | public int value; 199 | } 200 | 201 | private static string ToJsonStringIntDictionary(Dictionary dictionary) 202 | { 203 | List dictionaryItemsList = new List(); 204 | foreach(KeyValuePair kvp in dictionary) 205 | { 206 | dictionaryItemsList.Add( new StringIntDictionary(){ key = kvp.Key, value = kvp.Value } ); 207 | } 208 | 209 | StringIntDictionaryArray dictionaryArray = new StringIntDictionaryArray(){ items = dictionaryItemsList.ToArray() }; 210 | return JsonUtility.ToJson(dictionaryArray); 211 | } 212 | 213 | private static Dictionary FromJsonStringIntDictionary(string json) 214 | { 215 | StringIntDictionaryArray loadedData = JsonUtility.FromJson(json); 216 | Dictionary dictionary = new Dictionary(); 217 | for(int i=0; i < loadedData.items.Length; i++) 218 | { 219 | dictionary.Add(loadedData.items[i].key, loadedData.items[i].value); 220 | } 221 | return dictionary; 222 | } 223 | 224 | /* Dictionary */ 225 | 226 | [System.Serializable] 227 | private class StringFloatDictionaryArray 228 | { 229 | public StringFloatDictionary[] items; 230 | } 231 | 232 | [System.Serializable] 233 | private class StringFloatDictionary 234 | { 235 | public string key; 236 | public float value; 237 | } 238 | 239 | private static string ToJsonStringFloatDictionary(Dictionary dictionary) 240 | { 241 | List dictionaryItemsList = new List(); 242 | foreach(KeyValuePair kvp in dictionary) 243 | { 244 | dictionaryItemsList.Add( new StringFloatDictionary(){ key = kvp.Key, value = kvp.Value } ); 245 | } 246 | 247 | StringFloatDictionaryArray dictionaryArray = new StringFloatDictionaryArray(){ items = dictionaryItemsList.ToArray() }; 248 | return JsonUtility.ToJson(dictionaryArray); 249 | } 250 | 251 | private static Dictionary FromJsonStringFloatDictionary(string json) 252 | { 253 | StringFloatDictionaryArray loadedData = JsonUtility.FromJson(json); 254 | Dictionary dictionary = new Dictionary(); 255 | for(int i=0; i < loadedData.items.Length; i++) 256 | { 257 | dictionary.Add(loadedData.items[i].key, loadedData.items[i].value); 258 | } 259 | return dictionary; 260 | } 261 | 262 | #endregion 263 | 264 | #region Class Instance 265 | 266 | /// Load an instance of the class T from file. 267 | /// Filename of the file to load. 268 | /// The object type to be loaded. 269 | /// A loaded instance of the class T. 270 | public static T Load(string filename) where T: class 271 | { 272 | string path = PathForFilename(filename); 273 | if(JSONSerializer.PathExists(path)) 274 | { 275 | return JsonUtility.FromJson(File.ReadAllText(path)); 276 | } 277 | return default(T); 278 | } 279 | 280 | /// Save an instance of the class T to file. 281 | /// Filename of file to save. 282 | /// The class object to save. 283 | /// The object type to be saved. 284 | public static void Save(string filename, T data) where T: class 285 | { 286 | string path = PathForFilename(filename); 287 | File.WriteAllText(path, JsonUtility.ToJson(data)); 288 | } 289 | 290 | /// Determine whether a file exists at a given filepath. 291 | /// Filepath of the file. 292 | /// True if the file exists, otherwise file. 293 | private static bool PathExists(string filepath) 294 | { 295 | return File.Exists(filepath); 296 | } 297 | 298 | /// Determine if a File with a given filename exists. 299 | /// Filename of the file. 300 | /// Bool if the file exists. 301 | public static bool FileExists(string filename) 302 | { 303 | Debug.Log(PathForFilename(filename)); 304 | return PathExists(PathForFilename(filename)); 305 | } 306 | 307 | /// Delete a File with a given filename. 308 | /// Filename of the file. 309 | public static void DeleteFile(string filename) 310 | { 311 | string filepath = PathForFilename(filename); 312 | if(PathExists(filepath)) 313 | { 314 | File.Delete(filepath); 315 | } 316 | } 317 | 318 | /// Determine the correct filepath for a filename. In UNITY_EDITOR this is in the project's root 319 | /// folder, on mobile it is in the persistent data folder while standalone is the data folder. 320 | /// Filename of the file. 321 | /// The filepath for a given file on the current device. 322 | private static string PathForFilename(string filename) 323 | { 324 | string path = filename; //for editor 325 | #if UNITY_STANDALONE 326 | path = Path.Combine(Application.dataPath, filename); 327 | #elif UNITY_IOS || UNITY_ANDROID 328 | path = Path.Combine(Application.persistentDataPath, filename); 329 | #endif 330 | return path; 331 | } 332 | 333 | #endregion 334 | } 335 | -------------------------------------------------------------------------------- /#09-JSONSerialization/README.md: -------------------------------------------------------------------------------- 1 | # 09 - JSON Serialization 2 | 3 | JSON is a human-readable and machine-parsable lightweight data-interchange format which serializes data objects as text strings. Unity's *JSONUtility* supports serializable classes but is somewhat limited in that **1)** it does not support dictionaries or top level arrays, and **2)** all properties must be public. 4 | 5 | Today we will discuss a couple of potential uses of JSON within a typical Unity Project: 6 | - saving/loading classes/structs to disk 7 | - saving/loading arrays to PlayerPrefs or pulling arrays from RemoteSettings 8 | - deserialization of a database 9 | - parsing web requests 10 | 11 | ## Saving and Loading Classes/Structs to Disk 12 | 13 | Considering our *PlayerData* class 14 | 15 | ```C# 16 | [System.Serializable] 17 | public class PlayerData 18 | { 19 | public string name; 20 | public int score; 21 | [System.NonSerialized] private int tempValue; 22 | private int somePrivateVariable; 23 | } 24 | ``` 25 | 26 | and a simple JSONSerializer 27 | 28 | ```C# 29 | public class JSONSerializer 30 | { 31 | public static T Load(string filename) where T: class 32 | { 33 | string path = PathForFilename(filename); 34 | if(JSONSerializer.PathExists(path)) 35 | { 36 | return JsonUtility.FromJson(File.ReadAllText(path)); 37 | } 38 | return default(T); 39 | } 40 | } 41 | 42 | public static void Save(string filename, T data) where T: class 43 | { 44 | string path = PathForFilename(filename); 45 | File.WriteAllText(path, JsonUtility.ToJson(data)); 46 | } 47 | ``` 48 | 49 | we could easily save our data to file and reload it as per the [BinarySerializer tip](https://github.com/defuncart/50-unity-tips/tree/master/%2307-BinarySerialization) 50 | 51 | ```C# 52 | string filename = "PlayerData.json"; 53 | PlayerData data; 54 | if(JSONSerializer.FileExists(filename)) 55 | { 56 | data = JSONSerializer.Load(filename); 57 | } 58 | else 59 | { 60 | data = new PlayerData(); 61 | JSONSerializer.Save(filename, data); 62 | } 63 | Debug.Log(data.ToString()); 64 | ``` 65 | 66 | As mentioned, JSON strings are human readable - for instance our PlayerData would simply be ```{"name":"Player", "score":100}``` - and thus without encryption, these strings would be player modifiable (if discovered). Moreover, as only public fields can be serialized, *somePrivateVariable* will thus always have a default value from the constructor - if one actually needs to serialize this variable, then Binary Serialization would be a better solution. 67 | 68 | ## Serializing Dictionaries and Top-Level Arrays 69 | 70 | *JSONUtility* does not support top-level arrays, but if the array is stored in an object 71 | 72 | ```json 73 | { "array": [ 0, 1, 2, 3 ] } 74 | ``` 75 | 76 | and a wrapper class is written to handle serialization 77 | 78 | ```C# 79 | [Serializable] 80 | public class TopLevelIntArray 81 | { 82 | public int[] array; 83 | } 84 | ``` 85 | 86 | then the array can be easily serialized and deserialized: 87 | 88 | ```C# 89 | public string ToJson(int[] array) 90 | { 91 | TopLevelIntArray topLevelArray = new TopLevelIntArray() { array = array }; 92 | return JsonUtility.ToJson(topLevelArray); 93 | } 94 | 95 | public static int[] FromJson(string json) 96 | { 97 | TopLevelIntArray topLevel = JsonUtility.FromJson(json); 98 | return topLevel.array; 99 | } 100 | ``` 101 | 102 | Similarly for dictionaries, we can define a custom object 103 | 104 | ```C# 105 | [System.Serializable] 106 | public class StringStringDictionary 107 | { 108 | public string key; 109 | public string value; 110 | } 111 | ``` 112 | 113 | and an additional object containing an array of our custom object 114 | 115 | ```C# 116 | [System.Serializable] 117 | private class StringStringDictionaryArray 118 | { 119 | public StringStringDictionary[] items; 120 | } 121 | ``` 122 | 123 | so that such an array can be natively serialized as a custom object and then converted into the required dictionary: 124 | 125 | ```C# 126 | private static string ToJson(Dictionary dictionary) 127 | { 128 | List dictionaryItemsList = new List(); 129 | foreach(KeyValuePair kvp in dictionary) 130 | { 131 | dictionaryItemsList.Add( new StringStringDictionary(){ key = kvp.Key, value = kvp.Value } ); 132 | } 133 | 134 | StringStringDictionaryArray dictionaryArray = new StringStringDictionaryArray(){ items = dictionaryItemsList.ToArray() }; 135 | return JsonUtility.ToJson(dictionaryArray); 136 | } 137 | 138 | private static Dictionary FromJson(string json) 139 | { 140 | StringStringDictionaryArray loadedData = JsonUtility.FromJson(json); 141 | Dictionary dictionary = new Dictionary(); 142 | for(int i=0; i < loadedData.items.Length; i++) 143 | { 144 | dictionary.Add(loadedData.items[i].key, loadedData.items[i].value); 145 | } 146 | return dictionary; 147 | } 148 | ``` 149 | 150 | By using *Generics* and *typeof* we can utilize *ToJson*, *FromJson* to call individual serializers/deserializers for a given type. See included code for details. 151 | 152 | ## Saving/loading arrays to/from PlayerPrefs, RemoteSettings 153 | 154 | By default *PlayerPrefs* does not support the saving/loading of arrays or dictionaries, but it does support strings, and using the above mentioned *JSONSerializer*, one could convert, for example, an array to JSON and then save to/load from PlayerPrefs. 155 | 156 | ```C# 157 | int[] myArray = new int[]{ 0, 1, 2, 3 }; 158 | PlayerPrefs.SetString("myArray", JSONSerializer.ToJson(myArray)); 159 | int[] loadedArray = JSONSerializer.FromJson(PlayerPrefs.GetString("myArray")); 160 | ``` 161 | 162 | Another potential use is Unity's new [*RemoteSettings*](https://blogs.unity3d.com/2017/06/02/introducing-remote-settings-update-your-game-in-an-instant/) feature which allows the real-time updating of ints, floats, booleans and strings values. Arrays presently aren't supported, but by storing them as JSON strings, one could easily pull them into the game. 163 | 164 | ## Deserialization of a Database 165 | 166 | Consider that the game supports localization and we wish to import a simple database. One approach would be to export the database to a custom json of the form 167 | 168 | ```json 169 | { "items" : [ 170 | { "key": "GameWon", "value": "You completed the level!"}, 171 | { "key": "GameLost", "value": "You failed to score enough points."} 172 | ]} 173 | ``` 174 | 175 | and then import it into our game using ```FromJson>``` as shown above. More on localization in a future tip! 176 | 177 | ## Parsing Web-Requests 178 | 179 | When one makes a web request, generally a JSON dictionary is returned. This could be deserialized to Dictionary using custom classes or probably more efficiently using the [*MiniJson*](https://gist.github.com/darktable/1411710) class 180 | 181 | ```C# 182 | using MiniJSON; 183 | 184 | Dictionary dict = Json.Deserialize(json) as Dictionary; 185 | ``` 186 | 187 | ## Conclusion 188 | 189 | *JSON Serialization* is a versatile serialization method that I find particularly useful for database deserialization and web requests. Although the built-in *JSONUtility* class isn't yet fully compatible with all objects, as we've seen in this post, one can easily add new functionality to enable the serialization of dictionaries and top-level arrays. -------------------------------------------------------------------------------- /#10-XMLSerialization/README.md: -------------------------------------------------------------------------------- 1 | # 10 - XML Serialization 2 | 3 | *XML* (Extensible Markup Language) is a human-readable and machine-parsable markup language which defines a set of rules for encoding a document. C# has built-in XML serialization with the following noteworthy points: 4 | 5 | - Need to import System.Xml.Serialization which adds about 1MB to the final build. 6 | - There are lightweight readers such as [TinyXML](http://www.grinninglizard.com/tinyxml2/index.html) and [XMLParser](https://forum.unity3d.com/threads/free-lightweight-xml-reader-needs-road-testing.77383/). 7 | - Like JSON, XML can only serialize public fields. 8 | - The serializable class must have a parameterless constructor. 9 | 10 | ## XML Format 11 | 12 | Like JSON strings, XML documents are human-readable and contain: 13 | 14 | - Tags, both start-tags and end-tags 15 | - Elements, the contents between two tags, which may contain child elements for instance <enemy>Big Boss</enemy> or <level><numberOfLives>3</numberOfLives></level> 16 | - Attributes, name-value pairs contained within the start tag, for instance <level index=0> 17 | 18 | ## XML Serialization Attributes 19 | 20 | **[XmlElement]** indicates that a field will be represented as an XML element. By default all public fields are treated as elements, with their name as the tag, so the only real practical use of [XmlElement] is to change the tag from the default value. 21 | 22 | 23 | 24 | 29 | 32 | 33 |
public class PlayerData
 25 | {
 26 |   [XmlElement("n")]
 27 |   public string name;
 28 | }
<PlayerData>
 30 |   <n>Gordon Freeman</n>
 31 | </PlayerData>
34 | 35 | **[XmlAttribute]** indicates that the field will be represented as an XML attribute 36 | 37 | 38 | 39 | 44 | 46 | 47 |
public class PlayerData
 40 | {
 41 |   [XmlAttribute("name")]
 42 |   public string name;
 43 | }
<PlayerData name="Gordon Freeman">
 45 | </PlayerData>
48 | 49 | **[XmlIgnore]** is used to skip the serialization of a public field. 50 | 51 | 52 | 53 | 58 | 61 | 62 |
public class PlayerData
 54 | {
 55 |   public string name;
 56 |   [XmlIgnore] public int somePublicUnserializedInt;
 57 | }
<PlayerData>
 59 |     <name>Gordon Freeman</name>
 60 | </PlayerData>
63 | 64 | **[XmlRoot]** is used to alter the document's root tag (which by default is the object's class name) 65 | 66 | 67 | 68 | 73 | 76 | 77 |
[XmlRoot("Player")]
 69 | public class PlayerData
 70 | {
 71 |   public string name;
 72 | }
<Player>
 74 |     <name>Gordon Freeman</name>
 75 | </Player>
78 | 79 | **[XmlArray]** is used to specify the array's tag, while **[XmlArrayItem]** is used to specify the individual element's tag. 80 | 81 | 82 | 83 | 89 | 97 | 98 |
public class PlayerData
 84 | {
 85 |   public string name;
 86 |   [XmlArray("scores"), XmlArrayItem("score")]
 87 |   public int[] levelScores;
 88 | }
<PlayerData>
 90 |     <name>Gordon Freeman</name>
 91 |     <scores>
 92 |         <score>50</score>
 93 |         <score>76</score>
 94 |         <score>19</score>
 95 |     </scores>
 96 | </PlayerData>
99 | 100 | ## Saving/Loading Classes/Structs to Disk 101 | 102 | Like the [Binary](https://github.com/defuncart/50-unity-tips/tree/master/%2307-BinarySerialization) and [JSON](https://github.com/defuncart/50-unity-tips/tree/master/%2309-JSONSerialization) Serialization tips, given the *PlayerData* class 103 | 104 | ```C# 105 | [System.Serializable] 106 | public class PlayerData 107 | { 108 | public string name; 109 | public int score; 110 | [System.NonSerialized] private int tempValue; 111 | private int somePrivateVariable; 112 | } 113 | ``` 114 | 115 | and using an XMLSerializer (which could be better named) 116 | 117 | ```C# 118 | using System.Xml.Serialization; 119 | 120 | public static T Load(string filename) where T: class 121 | { 122 | string path = PathForFilename(filename); 123 | if(XMLSerializer.PathExists(path)) 124 | { 125 | try 126 | { 127 | using(Stream stream = File.OpenRead(path)) 128 | { 129 | XmlSerializer serializer = new XmlSerializer(typeof(T)); 130 | return serializer.Deserialize(stream) as T; 131 | } 132 | } 133 | catch(Exception e) { Debug.LogWarning(e.Message); } 134 | } 135 | return default(T); 136 | } 137 | 138 | public static void Save(string filename, T data) where T: class 139 | { 140 | string path = PathForFilename(filename); 141 | using(Stream stream = File.OpenWrite(path)) 142 | { 143 | XmlSerializer serializer = new XmlSerializer(typeof(T)); 144 | serializer.Serialize(stream, data); 145 | } 146 | } 147 | ``` 148 | 149 | we can easily save our data to file and reload it 150 | 151 | ```C# 152 | string filename = "PlayerData.xml"; 153 | PlayerData data; 154 | if(XMLSerializer.FileExists(filename)) 155 | { 156 | data = XMLSerializer.Load(filename); 157 | } 158 | else 159 | { 160 | data = new PlayerData(); 161 | XMLSerializer.Save(filename, data); 162 | } 163 | Debug.Log(data.ToString()); 164 | ``` 165 | 166 | As per JSON, *somePrivateVariable* is not serializable and the resulting XML output is human readable and thus potentially player-modifiable. Nevertheless, XML is commonly used for saving data and game states - one approach is to encyrpt the file using [*System.Security.Cryptography*](https://support.microsoft.com/en-us/help/307010/how-to-encrypt-and-decrypt-a-file-by-using-visual-c). 167 | 168 | ## ToXML, FromXML 169 | 170 | Like JSON, we can easily convert an object to an XML string and back again using the following methods: 171 | 172 | ```C# 173 | public static string ToXML(T data) where T : class 174 | { 175 | using(StringWriter textWriter = new StringWriter()) 176 | { 177 | XmlSerializer serializer = new XmlSerializer(data.GetType()); 178 | serializer.Serialize(textWriter, data); 179 | return textWriter.ToString(); 180 | } 181 | } 182 | 183 | public static T FromXML(string xml) where T : class 184 | { 185 | //load the xml into an xml document to remove the header 186 | XmlDocument xmlDoc = new XmlDocument(); xmlDoc.LoadXml(xml); 187 | if(xmlDoc.FirstChild.NodeType == XmlNodeType.XmlDeclaration) { xmlDoc.RemoveChild(xmlDoc.FirstChild); } 188 | 189 | using(Stream stream = ToStream(xmlDoc.InnerXml)) 190 | { 191 | XmlSerializer serializer = new XmlSerializer(typeof(T)); 192 | return serializer.Deserialize(stream) as T; 193 | } 194 | } 195 | ``` 196 | 197 | This is something I haven't really tested, but it seems to work :p Could be useful saving an object to PlayerPrefs or pulling objects from RemoteSettings. 198 | 199 | ## XML vs JSON 200 | 201 | One could argue that JSON is more user-readable as it isn't cluttered with tags, which could benefit non-technical people who may need to edit the files. JSON built-in support isn't as encompasing as XML, so, for instance, dictionaries and top-level arrays will need their own custom serializers. XML requires no custom code or formatting, but System.Xml.Serialization will add 1MB to the final build. 202 | 203 | ## Example 204 | 205 | Scriptable Objects are a great way to vary certain parameters during gameplay and then persist to the project. However what if the Game Designer is not comfortable with Unity's interface? Then using a standalone build and an XML database is a one option - an XML file can be easily downloaded at runtime and deserialized into a game object. 206 | 207 | ## Conclusion 208 | 209 | Hopefully this series of five posts on serialization was useful. Next week we'll talk about UI tips, import settings, caching and more! -------------------------------------------------------------------------------- /#10-XMLSerialization/XMLSerialization.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Written by James Leahy. (c) 2017 DeFunc Art. 3 | */ 4 | using System; 5 | using System.IO; 6 | using System.Text; 7 | using System.Xml; 8 | using System.Xml.Serialization; 9 | using UnityEngine; 10 | 11 | /// An XML Serializer which serializes and deserializes classes into XML format. 12 | public class XMLSerializer 13 | { 14 | #region ToXML, FromXML 15 | 16 | /// Serializes an object of type T to XML. 17 | /// An XML representation of the object T. 18 | /// The object T. 19 | public static string ToXML(T data) where T : class 20 | { 21 | using(StringWriter textWriter = new StringWriter()) 22 | { 23 | XmlSerializer serializer = new XmlSerializer(data.GetType()); 24 | serializer.Serialize(textWriter, data); 25 | return textWriter.ToString(); 26 | } 27 | } 28 | 29 | /// Deserializes an object of type T from XML. 30 | /// The object T. 31 | /// An XML representation of the object T. 32 | public static T FromXML(string xml) where T : class 33 | { 34 | //load the xml into an xml document to remove the header 35 | XmlDocument xmlDoc = new XmlDocument(); xmlDoc.LoadXml(xml); 36 | if(xmlDoc.FirstChild.NodeType == XmlNodeType.XmlDeclaration) { xmlDoc.RemoveChild(xmlDoc.FirstChild); } 37 | 38 | using(Stream stream = ToStream(xmlDoc.InnerXml)) 39 | { 40 | XmlSerializer serializer = new XmlSerializer(typeof(T)); 41 | return serializer.Deserialize(stream) as T; 42 | } 43 | } 44 | 45 | /// Converts a string to a stream. 46 | private static Stream ToStream(string s) 47 | { 48 | MemoryStream stream = new MemoryStream(); 49 | StreamWriter writer = new StreamWriter(stream); 50 | writer.Write(s); 51 | writer.Flush(); 52 | stream.Position = 0; 53 | return stream; 54 | } 55 | 56 | #endregion 57 | 58 | #region Class Instance 59 | 60 | /// Load an instance of the class T from file. 61 | /// Filename of the file to load. 62 | /// The object type to be loaded. 63 | /// A loaded instance of the class T. 64 | public static T Load(string filename) where T: class 65 | { 66 | string path = PathForFilename(filename); 67 | if(XMLSerializer.PathExists(path)) 68 | { 69 | try 70 | { 71 | using(Stream stream = File.OpenRead(path)) 72 | { 73 | XmlSerializer serializer = new XmlSerializer(typeof(T)); 74 | return serializer.Deserialize(stream) as T; 75 | } 76 | } 77 | catch(Exception e) { Debug.LogWarning(e.Message); } 78 | } 79 | return default(T); 80 | } 81 | 82 | /// Save an instance of the class T to file. 83 | /// Filename of file to save. 84 | /// The class object to save. 85 | /// The object type to be saved. 86 | public static void Save(string filename, T data) where T: class 87 | { 88 | string path = PathForFilename(filename); 89 | using(Stream stream = File.OpenWrite(path)) 90 | { 91 | XmlSerializer serializer = new XmlSerializer(typeof(T)); 92 | serializer.Serialize(stream, data); 93 | } 94 | } 95 | 96 | /// Determine whether a file exists at a given filepath. 97 | /// Filepath of the file. 98 | /// True if the file exists, otherwise file. 99 | private static bool PathExists(string filepath) 100 | { 101 | return File.Exists(filepath); 102 | } 103 | 104 | /// Determine if a File with a given filename exists. 105 | /// Filename of the file. 106 | /// Bool if the file exists. 107 | public static bool FileExists(string filename) 108 | { 109 | return PathExists(PathForFilename(filename)); 110 | } 111 | 112 | /// Delete a File with a given filename. 113 | /// Filename of the file. 114 | public static void DeleteFile(string filename) 115 | { 116 | string filepath = PathForFilename(filename); 117 | if(PathExists(filepath)) 118 | { 119 | File.Delete(filepath); 120 | } 121 | } 122 | 123 | /// Determine the correct filepath for a filename. In UNITY_EDITOR this is in the project's root 124 | /// folder, on mobile it is in the persistent data folder while standalone is the data folder. 125 | /// Filename of the file. 126 | /// The filepath for a given file on the current device. 127 | private static string PathForFilename(string filename) 128 | { 129 | string path = filename; //for editor 130 | #if UNITY_STANDALONE 131 | path = Path.Combine(Application.dataPath, filename); 132 | #elif UNITY_IOS || UNITY_ANDROID 133 | path = Path.Combine(Application.persistentDataPath, filename); 134 | #endif 135 | return path; 136 | } 137 | 138 | #endregion 139 | } 140 | -------------------------------------------------------------------------------- /#11-OnScreenLogMessages/README.md: -------------------------------------------------------------------------------- 1 | # 11 - On Screen Log Messages 2 | 3 | Today’s tip is short and sweet but extremely useful for mobile devices: on screen log messages. 4 | 5 | [*Log viewer*](https://www.assetstore.unity3d.com/en/#!/content/12047) is a free asset which builds a overlay window of log messages, accessible by making a circle touch gesture. This window is a copy of the editor console and even supports [Rich Text](https://github.com/defuncart/unity-tips-tricks/tree/master/%2302-RichText). 6 | 7 | ![](images/onScreenLogMessages1.png) 8 | 9 | Whether debugging incorrect logic or catching bugs and crashing from in team testing, I have found this simple asset to be indispensable for mobile development. -------------------------------------------------------------------------------- /#11-OnScreenLogMessages/images/onScreenLogMessages1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#11-OnScreenLogMessages/images/onScreenLogMessages1.png -------------------------------------------------------------------------------- /#12-PreloadingMobileSigning/PreloadMobileSinging.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Written by James Leahy. (c) 2017 DeFunc Art. 3 | * https://github.com/defuncart/ 4 | */ 5 | #if UNITY_EDITOR 6 | using UnityEditor; 7 | 8 | /// A simple editor script which automatically sets up Android signing. 9 | [InitializeOnLoad] 10 | public class PreloadAndroidSigning 11 | { 12 | /// The keystone password. 13 | private const string KEYSTORE_PASS = "KEYSTORE_PASS"; 14 | /// The alias password. 15 | private const string ALIAS_PASSWORD = "ALIAS_PASSWORD"; 16 | 17 | /// Initializes the class. 18 | static PreloadAndroidSigning() 19 | { 20 | PlayerSettings.Android.keystorePass = KEYSTORE_PASS; 21 | PlayerSettings.Android.keyaliasPass = ALIAS_PASSWORD; 22 | } 23 | } 24 | 25 | /// A simple editor script which automatically sets up iOS signing. 26 | [InitializeOnLoad] 27 | public class PreloadiOSSigning 28 | { 29 | /// Whether the app should be automatically signed. 30 | private const bool AUTOMATICALLY_SIGN = true; 31 | /// The Apple Developer Team ID. 32 | private const string TEAM_ID = "TEAM_ID"; 33 | 34 | /// Initializes the class. 35 | static PreloadiOSSigning() 36 | { 37 | if(PlayerSettings.iOS.appleEnableAutomaticSigning != AUTOMATICALLY_SIGN) 38 | { 39 | PlayerSettings.iOS.appleEnableAutomaticSigning = AUTOMATICALLY_SIGN; 40 | } 41 | if(AUTOMATICALLY_SIGN && string.IsNullOrEmpty(PlayerSettings.iOS.appleDeveloperTeamID)) 42 | { 43 | PlayerSettings.iOS.appleDeveloperTeamID = TEAM_ID; 44 | } 45 | } 46 | } 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /#12-PreloadingMobileSigning/README.md: -------------------------------------------------------------------------------- 1 | # 12 - Preloading Mobile Signing 2 | 3 | To publish on Google Play, one needs to sign their app with a keystone signature file. Various aliases (i.e. users) can be authorized to use this file, each with their own password. In the Android Publishing section, we can select the keystone and alias, and enter their respective passwords. 4 | 5 | ![](images/preloadMobileSinging1.png) 6 | 7 | Eachtime that a Unity project loads, the developer must re-enter these passwords before building an APK. Wouldn't it be nice if we could avoid this? Editor script to the rescue! 8 | 9 | ```C# 10 | [InitializeOnLoad] 11 | public class PreloadAndroidSigning 12 | { 13 | private const string KEYSTORE_PASS = "KEYSTORE_PASS"; 14 | private const string ALIAS_PASSWORD = "ALIAS_PASSWORD"; 15 | 16 | static PreloadAndroidSigning() 17 | { 18 | PlayerSettings.Android.keystorePass = KEYSTORE_PASS; 19 | PlayerSettings.Android.keyaliasPass = ALIAS_PASSWORD; 20 | } 21 | } 22 | ``` 23 | 24 | This simply Editor script will automatically update both passwords on project load*, simply place it inside an Editor folder anywhere within your project. 25 | 26 | *As the class is marked *InitializeOnLoad*, the class's static constructor is guaranted to be called as the Editor launches. More info [here](https://docs.unity3d.com/Manual/RunningEditorCodeOnLaunch.html). 27 | 28 | **WARNING** As your keystone and passwords are [kinda important](https://support.google.com/googleplay/android-developer/answer/7384423?hl=en), you shouldn't commit them to a public repo! 29 | 30 | Unity does have Xcode default settings preferences, however if you need to update these settings on a per project basis, then that can be similarly achieved as above. 31 | 32 | ```C# 33 | [InitializeOnLoad] 34 | public class PreloadiOSSigning 35 | { 36 | private const bool AUTOMATICALLY_SIGN = true; 37 | private const string TEAM_ID = "TEAM_ID"; 38 | 39 | static PreloadiOSSigning() 40 | { 41 | if(PlayerSettings.iOS.appleEnableAutomaticSigning != AUTOMATICALLY_SIGN) 42 | { 43 | PlayerSettings.iOS.appleEnableAutomaticSigning = AUTOMATICALLY_SIGN; 44 | } 45 | if(AUTOMATICALLY_SIGN && string.IsNullOrEmpty(PlayerSettings.iOS.appleDeveloperTeamID)) 46 | { 47 | PlayerSettings.iOS.appleDeveloperTeamID = TEAM_ID; 48 | } 49 | } 50 | } 51 | ``` -------------------------------------------------------------------------------- /#12-PreloadingMobileSigning/images/preloadMobileSinging1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#12-PreloadingMobileSigning/images/preloadMobileSinging1.png -------------------------------------------------------------------------------- /#13-iOSLaunchScreen/LaunchScreen-iOS.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /#13-iOSLaunchScreen/README.md: -------------------------------------------------------------------------------- 1 | # 13 - iOS Launch Screen 2 | 3 | On Startup, a Unity game will display the splash screen (if enabled) before loading the first scene. However, if you launch the game on iOS, you will notice the following bluescreen before the splash screen/first scene. 4 | 5 | ![](images/iOSLaunchScreen1.png) 6 | 7 | This is an *iOS Launch Screen*, a screen that appears instantly when an iOS app lauches. As every app must supply a launch screen, when building an iOS Xcode project, Unity will supply a default as seen above. 8 | 9 | > *The launch screen is quickly replaced with the first screen of your app, giving the impression that your app is fast and responsive. The launch screen isn’t an opportunity for artistic expression. It’s solely intended to enhance the perception of your app as quick to launch and immediately ready for use.* [Apple](https://developer.apple.com/ios/human-interface-guidelines/icons-and-images/launch-screen/) 10 | 11 | So now that we know what a launch screen is, wouldn't it be nice to supply something better suited to the splash screen/first scene? This can be easily achieved in **Player Settings -> iOS -> Splash Screen** 12 | 13 | ![](images/iOSLaunchScreen2.png) 14 | 15 | There are two options: **1)** Supply static images of different sizes for different devices (iOS7+) or **2)** Generate an XIB file which is adapted to each device screen size (iOS8+). 16 | 17 | In the launch screen seen above, Unity automatically creates iPhone and iPad XIB files (*Default*). There are also *Image and Background (relative size)*, *Image and Background (constant size)* and *Custom XIB* options, the first two which create XIB files based on a background color and an overlay image, while the latter uses a supplied XIB file. 18 | 19 | For a simple, single-colored launch screen with no text or images, either of the three options will suffice, however I personally prefer to supply a custom XIB created in Xcode as **1)** The file size is smaller and **2)** Unity's created XIB uses a constrained UIImage which is unnecessary as the base UIView's background color can be assigned. 20 | 21 | ![](images/iOSLaunchScreen3.png) 22 | 23 | Amazingly I have seen the default Unity launch screen in games with more than a million downloads! A custom launch screen will not only look aesthetically better, it will give the impression that the app is loading faster, good for older devices. Simply 24 | * design a launch screen that’s as identical to the splash screen/first scene as possible 25 | * avoid including text as localization isn't posible -------------------------------------------------------------------------------- /#13-iOSLaunchScreen/images/iOSLaunchScreen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#13-iOSLaunchScreen/images/iOSLaunchScreen1.png -------------------------------------------------------------------------------- /#13-iOSLaunchScreen/images/iOSLaunchScreen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#13-iOSLaunchScreen/images/iOSLaunchScreen2.png -------------------------------------------------------------------------------- /#13-iOSLaunchScreen/images/iOSLaunchScreen3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#13-iOSLaunchScreen/images/iOSLaunchScreen3.png -------------------------------------------------------------------------------- /#14-CustomC#ScriptTemplate/81-C# Script-NewBehaviourScript.cs.txt: -------------------------------------------------------------------------------- 1 | /* 2 | * Written by James Leahy. (c) 2017 DeFunc Art. 3 | * https://github.com/defuncart/ 4 | */ 5 | using UnityEngine; 6 | 7 | /// The #SCRIPTNAME# class. 8 | public class #SCRIPTNAME# : MonoBehaviour 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /#14-CustomC#ScriptTemplate/README.md: -------------------------------------------------------------------------------- 1 | # 14 - Custom C# Script Template 2 | 3 | When one creates a new C# Script in Unity, they are presented with the following code automatically: 4 | 5 | ```C# 6 | using System.Collections; 7 | using System.Collections.Generic; 8 | using UnityEngine; 9 | 10 | public class #SCRIPTNAME# : MonoBehaviour { 11 | 12 | // Use this for initialization 13 | void Start () { 14 | 15 | } 16 | 17 | // Update is called once per frame 18 | void Update () { 19 | 20 | } 21 | } 22 | ``` 23 | 24 | Did you know that you can easily change this template? This is quite useful as 25 | 1. *Start* and *Update* methods, even when empty, are triggered once per frame for every game object. Often developers forget to remove unnecessary empty functions from their scripts - if it is never there in the first place, then that's one less thing to remove! 26 | 2. Can easily add #regions, ``````, custom code etc. 27 | 28 | Simply go to **/Applications/Unity/Unity.app/Contents/Resources/ScriptTemplates** (or *C:\Program Files\Unity\Editor\Data\Resources\ScriptTemplates* on *Windows*) and edit *81-C# Script-NewBehaviourScript.cs.txt*. 29 | 30 | ```C# 31 | /* 32 | * Written by James Leahy. (c) 2017 DeFunc Art. 33 | */ 34 | using UnityEngine; 35 | 36 | /// The #SCRIPTNAME# class. 37 | public class #SCRIPTNAME# : MonoBehaviour 38 | { 39 | } 40 | ``` 41 | 42 | Other templates like shaders can be edited too. For more information see [here](https://support.unity3d.com/hc/en-us/articles/210223733-How-to-customize-Unity-script-templates). It is worth remembering that this is a version-specific alteration that one needs to do repeat for each new version. -------------------------------------------------------------------------------- /#15-PimpTheInspector/README.md: -------------------------------------------------------------------------------- 1 | # 15 - Pimp the Inspector 2 | 3 | When a script inheriting from *MonoBehaviour* is attached to a *GameObject*, detailed information about that GameObject's properties are displayed in the inspector window. If the GameObject has a lot of properties, the inspector can seem cluttered, making it difficult to find specific properties. Moreover, when the script writer isn't the person actually viewing the inspector, it would be good to display additional information and verify input when possible. Luckily there are a number of attributes which can help us achieve this. 4 | 5 | ## SerializeField, HideInInspector 6 | 7 | When a public, serializable property is declared in a script, it is by default serialized and exposed to the inspector. A serialized property can be hidden from the inspector with **[HideInInspector]** attribute, while a private variable can be serialized and exposed to the inspector with the **[SerializeField]** attribute. 8 | 9 | ```C# 10 | public class MyComponent : MonoBehaviour 11 | { 12 | //serialized and exposed to the inspector by default, accessible by all objects with a reference of this instance 13 | public int myInt1; 14 | //serialized but not exposed to the inspector, accessible by all objects with a reference of this instance 15 | [HideInInspector] public int myInt2; 16 | //serialized and exposed to the inspector, not publicly accessible 17 | [SerializeField] private int myInt3; 18 | } 19 | ``` 20 | 21 | ![](images/pimpTheInspector1.png) 22 | 23 | Although Unity state that *"you will almost never need [SerializeField]"*, the **[SerializeField] private** combination is often used to adhere to encapsulation while serializing private variables. **[HideInInspector]** is very useful to hide serialized class members which don't need to be edited in the inspector. 24 | 25 | ## RequireComponent 26 | 27 | The **[RequireComponent]** attribute automatically adds required components as dependencies - when a script which uses this attribute is added to a GameObject, the required component will automatically be added to the GameObject. One example is *UI/Button* which automatically adds *UI/Image*, a required dependency. Consider the practical example of including a *Rigidbody2D* for a PlayerMove script: 28 | 29 | ```C# 30 | [RequireComponent(typeof(Rigidbody2D))] 31 | public class PlayerMove : MonoBehaviour 32 | { 33 | } 34 | ``` 35 | 36 | Note that **[RequireComponent]** only checks for missing dependencies when the component is added to a GameObject - existing instances which lack new dependencies will not have those dependencies automatically added. Thus this attribute is useful in insuring that the initial setup is correct. 37 | 38 | ## RangeAttribute 39 | 40 | The **[Range]** attribute is especially useful when an int or float should be constrained to a certain range. Not only does it force all input to be valid, but it supplies an intuitive method of quickly testing values values in play mode. 41 | 42 | ```C# 43 | public class MyComponent : MonoBehaviour 44 | { 45 | [Range(0, 100)] [SerializeField] private int myInt; 46 | [Range(0, 1)] [SerializeField] private float myFloat; 47 | } 48 | ``` 49 | 50 | ![](images/pimpTheInspector2.png) 51 | 52 | ## HeaderAttribute, SpaceAttribute 53 | 54 | The **[Header]** attribute adds a header above properties, while the **[Space]** attribute adds a vertical space between successive properties in the inspector, both of which are great ways of visually breaking the component's block of properties into various groups. 55 | 56 | ```C# 57 | public class MyComponent : MonoBehaviour 58 | { 59 | [Header("Health Settings")] 60 | [SerializeField] private int currentHealth = 0; 61 | [SerializeField] private int maxHealth = 100; 62 | [Space(10)] 63 | [Header("Speed Settings")] 64 | [SerializeField] private float speed = 10; 65 | [SerializeField] private int powerUpSpeed = 20; 66 | } 67 | ``` 68 | 69 | ![](images/pimpTheInspector3.png) 70 | 71 | ## ToolTipAttribute 72 | 73 | By default, the inspector displays property names as capitalized words. Although descriptive names should give a good idea about what a property does, additional information can be very useful for teammates who simply assign values to properties via the inspector without any familiarity with the script itself (i.e. Level Designer, UI Artist). The **[ToolTip]** attribute adds a tooltip when the mouse curser hovers over the property. 74 | 75 | ```C# 76 | public class MyComponent : MonoBehaviour 77 | { 78 | [Tooltip("The enemy's health, a value between 0 and 100.")] 79 | [Range(0, 100)] [SerializeField] private int enemyHealth = 0; 80 | } 81 | ``` 82 | 83 | ![](images/pimpTheInspector4.png) 84 | 85 | ## TextAreaAttribute, MultilineAttribute 86 | 87 | By default a string that is too long to be rendered inside the inspector window will be cut off. For long, editable strings that need to be displayed in the inspector, one approach is to use a **[TextArea]** which automatically wraps a string within an assignable height rectangle - if the string is too long, a scrollbar appears. **MultilineAttribute** allows a string to be edited in a multiline textfield, however doesn't automatically wrap the string. 88 | 89 | ```C# 90 | public class MyComponent : MonoBehaviour 91 | { 92 | [SerializeField] private string myString1 = "This is a long string, so long that it cannot fit within the inspector window."; 93 | [Multiline(2)] [SerializeField] private string myString2 = "This is a long string with the multiline attribute that still doesn't fit inside the inspector."; 94 | [TextArea] [SerializeField] private string myString3 = "This is a long string that is placed within a TextArea."; 95 | } 96 | ``` 97 | 98 | ![](images/pimpTheInspector5.png) 99 | 100 | ## HelpURLAttribute 101 | 102 | The **[HelpURLAttribute]** attribute provides a custom reference link which is triggered once the help **(?)** icon is clicked in the inspector (see any of the images above). 103 | 104 | ```C# 105 | [HelpURL("http://example.com/docs/MyComponent.html")] 106 | public class MyComponent : MonoBehaviour 107 | { 108 | } 109 | ``` 110 | 111 | ## Conclusion 112 | 113 | Today we have seen some quick yet powerful approaches in making a component's inspector window display relevant information in a tidy and intuitive manner. Over the next few tips I will be discussing how editor scripting can be immensely useful in building tools for the developer and their fellow teammates. 114 | 115 | ## Further Reading 116 | 117 | [Manual - The Inspector window](https://docs.unity3d.com/Manual/UsingTheInspector.html) 118 | 119 | [Scripting API - SerializeField](https://docs.unity3d.com/ScriptReference/SerializeField.html) 120 | 121 | [Scripting API - HideInInspector](https://docs.unity3d.com/ScriptReference/HideInInspector.html) 122 | 123 | [Scripting API - RequireComponent](https://docs.unity3d.com/ScriptReference/RequireComponent.html) 124 | 125 | [Scripting API - RangeAttribute](https://docs.unity3d.com/ScriptReference/RangeAttribute.html) 126 | 127 | [Scripting API - HeaderAttribute](https://docs.unity3d.com/ScriptReference/HeaderAttribute.html) 128 | 129 | [Scripting API - SpaceAttribute](https://docs.unity3d.com/ScriptReference/SpaceAttribute.html) 130 | 131 | [Scripting API - TextAreaAttribute](https://docs.unity3d.com/ScriptReference/TextAreaAttribute.html) 132 | 133 | [Scripting API - MultilineAttribute](https://docs.unity3d.com/ScriptReference/MultilineAttribute.html) 134 | 135 | [Scripting API - HelpURLAttribute](https://docs.unity3d.com/ScriptReference/HelpURLAttribute.html) 136 | -------------------------------------------------------------------------------- /#15-PimpTheInspector/images/pimpTheInspector1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#15-PimpTheInspector/images/pimpTheInspector1.png -------------------------------------------------------------------------------- /#15-PimpTheInspector/images/pimpTheInspector2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#15-PimpTheInspector/images/pimpTheInspector2.png -------------------------------------------------------------------------------- /#15-PimpTheInspector/images/pimpTheInspector3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#15-PimpTheInspector/images/pimpTheInspector3.png -------------------------------------------------------------------------------- /#15-PimpTheInspector/images/pimpTheInspector4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#15-PimpTheInspector/images/pimpTheInspector4.png -------------------------------------------------------------------------------- /#15-PimpTheInspector/images/pimpTheInspector5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#15-PimpTheInspector/images/pimpTheInspector5.png -------------------------------------------------------------------------------- /#16-OnValidate/README.md: -------------------------------------------------------------------------------- 1 | # 16 - OnValidate 2 | 3 | Yesterday we talked about the **[Range]** attribute which ensures that float/int values entered in the inspector are constrained to a certain range, thus ensuring valid input. However wouldn't it be great to validate data for all properties, not just those of type float and int? 4 | 5 | **OnValidate** is a callback when a script extending *MonoBehaviour* or *ScriptableObject* (not mentioned in the API) is loaded or the value of a property is changed in the inspector. Here we can use if statements or assertions to check that the inputted data is valid. 6 | 7 | ```C# 8 | public class MyComponent : MonoBehaviour 9 | { 10 | [SerializeField] private Button myButton; 11 | 12 | private void OnValidate() 13 | { 14 | Assert.IsNotNull(myButton, "Expected myButton to be not null"); 15 | } 16 | } 17 | ``` 18 | 19 | ![](images/onValidate1.png) 20 | ![](images/onValidate2.png) 21 | 22 | ## Further Reading 23 | 24 | [Scripting API - MonoBehaviour.OnValidate()](https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnValidate.html) 25 | 26 | [Scripting API - Assertions.Assert](https://docs.unity3d.com/ScriptReference/Assertions.Assert.html) 27 | -------------------------------------------------------------------------------- /#16-OnValidate/images/onValidate1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#16-OnValidate/images/onValidate1.png -------------------------------------------------------------------------------- /#16-OnValidate/images/onValidate2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#16-OnValidate/images/onValidate2.png -------------------------------------------------------------------------------- /#17-CustomEditor/README.md: -------------------------------------------------------------------------------- 1 | # 17 - Custom Editor Windows 2 | 3 | One of the most powerful features of the Unity editor is the ability to create custom windows and inspectors. We have already seen how we can use attributes such as **[Range]**, **[ToolTip]**, **[HideInInspector]**, **[HeaderAttribute]** etc. to customize a component's inspector. However, by writing a custom editor script for a component or asset, we can define exactly how the inspector should look, include useful buttons, visual the data etc. 4 | 5 | ## Custom Inspector 6 | 7 | One very simple but useful addition to an inspector is a button which can display information about the object, import assets from a folder or toggle pre-defined settings. Consider the following *EnemyData* scriptable object. 8 | 9 | ```C# 10 | using UnityEngine; 11 | 12 | /// An asset used to store data for an enemy. 13 | [CreateAssetMenu(fileName = "EnemyData", menuName = "EnemyData", order = 1000)] 14 | public class EnemyData : ScriptableObject 15 | { 16 | /// The enemy's health. 17 | [Tooltip("The enemy's health.")] 18 | [Range(0, 100)] public int health; 19 | /// The enemy's speed. 20 | [Tooltip("The enemy's speed.")] 21 | [Range(1, 10)] public float speed; 22 | 23 | /// Returns a string representation of the object. 24 | public override string ToString() 25 | { 26 | return string.Format("[EnemyData {0}: health={1}, speed={2}]", name, health, speed); 27 | } 28 | } 29 | ``` 30 | 31 | By default the editor renders the following inspector for this asset: 32 | 33 | ![](images/customEditorWindows1.png) 34 | 35 | By writing a script extending from *Editor* and overriding the callback **OnInspectorGUI**, we have complete control over the inspector's layout. These Editor scripts must be placed in the root or a subdirectory of folder named ```Editor```, generally ```Assets/Editor``` but ```Assets/Imported/DeFuncArt/Editor/Utilities/``` would also be valid. 36 | 37 | ```C# 38 | #if UNITY_EDITOR 39 | using UnityEditor; 40 | using UnityEngine; 41 | 42 | /// A custom editor script for the EnemyData asset. 43 | [CustomEditor(typeof(EnemyData))] 44 | public class EnemyDataEditor : Editor 45 | { 46 | /// Callback to draw the inspector. 47 | public override void OnInspectorGUI() 48 | { 49 | //get a reference to the target script 50 | EnemyData targetScript = (EnemyData)target; 51 | //draw default inspector for asset's properties 52 | DrawDefaultInspector(); 53 | //draw a button which prints info to the console 54 | if(GUILayout.Button(new GUIContent(text: "Print Info", tooltip: "Prints info about the asset to the console."))) 55 | { 56 | Debug.Log(targetScript.ToString()); 57 | } 58 | } 59 | } 60 | #endif 61 | ``` 62 | 63 | *DrawDefaultInspector* draws the same, default inspector that we saw above. However, underneath we draw a button, which if pressed, prints debug info to the console: 64 | 65 | ![](images/customEditorWindows2.png) 66 | 67 | This example may be quite trivial, however you can see the potential, for instance triggering an import method in EnemyData which imports from XLS file, or a LevelManager which imports an array of LevelData assets from a predefined path. 68 | 69 | ## Custom Window 70 | 71 | We can also easily create custom windows in the editor by writing a script extending from *EditorWindow* and customizing **OnGUI**. 72 | 73 | ```C# 74 | #if UNITY_EDITOR 75 | using UnityEditor; 76 | using UnityEngine; 77 | 78 | /// An EditorWindow to automate the creation of game data assets. 79 | class CustomWindow : EditorWindow 80 | { 81 | /// The current selected option. 82 | public static int option = 0; 83 | /// An array of strings used as toolbar texts. 84 | public static string[] optionNames = { "Yes", "No" }; 85 | 86 | //add a menu item named "Import Assets" to the Tools menu (shortcut ALT-CMD-B) 87 | [MenuItem("Tools/Custom Window")] 88 | public static void ShowWindow() 89 | { 90 | //show existing window instance - if one doesn't exist, create one 91 | EditorWindow.GetWindow(typeof(CustomWindow)); 92 | } 93 | 94 | /// Draws the window. 95 | private void OnGUI() 96 | { 97 | //draw a label 98 | GUILayout.Label("This is my custom window"); 99 | //draw a toolbar (array of buttons) and assign the selected button index as option 100 | option = GUILayout.Toolbar (option, optionNames); 101 | //draw a button which, if triggered, prints info to the console 102 | if(GUILayout.Button("Print Info")) { PrintToConsole(); } 103 | } 104 | 105 | /// Prints to console. 106 | private void PrintToConsole() 107 | { 108 | Debug.Log("Hello, World!"); 109 | } 110 | } 111 | #endif 112 | ``` 113 | 114 | ![](images/customEditorWindows3.png) 115 | 116 | ## Conclusion 117 | 118 | Custom inspectors and custom windows are quite powerful and can help not only your workflow, but that of your team. For more info, check google and the gazillion tutorials or start here with this [official one](https://unity3d.com/learn/tutorials/topics/interface-essentials/building-custom-inspector). 119 | 120 | ## Further Reading 121 | 122 | [Manual - Extending the Editor](https://docs.unity3d.com/Manual/ExtendingTheEditor.html) 123 | 124 | [Scripting API - Editor](https://docs.unity3d.com/ScriptReference/Editor.html) 125 | 126 | [Scripting API - Editor.OnInspectorGUI](https://docs.unity3d.com/ScriptReference/Editor.OnInspectorGUI.html) 127 | 128 | [Scripting API - EditorWindow](https://docs.unity3d.com/ScriptReference/EditorWindow.html) 129 | 130 | [Scripting API - EditorWindow.OnGUI](https://docs.unity3d.com/ScriptReference/EditorWindow.OnGUI.html) 131 | -------------------------------------------------------------------------------- /#17-CustomEditor/images/customEditorWindows1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#17-CustomEditor/images/customEditorWindows1.png -------------------------------------------------------------------------------- /#17-CustomEditor/images/customEditorWindows2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#17-CustomEditor/images/customEditorWindows2.png -------------------------------------------------------------------------------- /#17-CustomEditor/images/customEditorWindows3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#17-CustomEditor/images/customEditorWindows3.png -------------------------------------------------------------------------------- /#18-CustomMenuItems/README.md: -------------------------------------------------------------------------------- 1 | # 18 - Custom Menu Items 2 | 3 | The Unity editor allows adding custom menus which look and behave like built-in menus. This can be especially useful for adding commonly used functionality used throughout a project, for instance opening a scene in the editor, resetting all game data or triggering cheats to test gameplay etc. 4 | 5 | ## Adding Custom Menu Items 6 | 7 | To add a custom menu item, we simply need to create an editor script and write a static method marked with the **MenuItem** attribute. 8 | 9 | ```C# 10 | #if UNITY_EDITOR 11 | using UnityEditor; 12 | using UnityEngine; 13 | 14 | public class CustomMenus 15 | { 16 | [MenuItem("Tools/DeleteData")] 17 | public static void DeleteData() 18 | { 19 | PlayerPrefs.DeleteAll(); 20 | } 21 | } 22 | #endif 23 | ``` 24 | 25 | This creates a new editor menu *Tools* with the menu item *DeleteData*: 26 | 27 | ![](images/customMenuItems1.png) 28 | 29 | It is also possible to create new menu items under existing menus (i.e. Window), and also to create multiple levels of menus for better structuring and organization: 30 | 31 | ```C# 32 | [MenuItem("Tools/Data/Delete")] 33 | ``` 34 | 35 | ![](images/customMenuItems2.png) 36 | 37 | ## Hotkey shortcuts 38 | 39 | New menu items can be assigned hotkeys combinations that will automatically launch them. 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
HotkeyDescription
%CTRL on windows, CMD on Mac
#Shift
&Alt
#LEFT, #RIGHT, #UP, #DOWN
#HOME, #END, #PGUP, #PGDN
Navigation keys
#F1 ... #F12Function keys
67 | 68 | Hotkey character combinations are added to the end of the menu item path, preceded by a space. Character keys not part of a key-sequence are added by adding an underscore prefix to them (i.e. _d for shortcut key “D”). 69 | 70 | ```C# 71 | #if UNITY_EDITOR 72 | using UnityEditor; 73 | using UnityEngine; 74 | 75 | public class CustomMenus 76 | { 77 | ///A menu item with hotkey ALT-SHIFT-D 78 | [MenuItem("Tools/MenuItemOne #&d")] 79 | public static void MenuItemOne() {} 80 | 81 | ///A menu item with hotkey D 82 | [MenuItem("Tools/MenuItemTwo _d")] 83 | public static void MenuItemTwo() {} 84 | } 85 | #endif 86 | ``` 87 | 88 | Menu items with hotkeys will display the key-combination that is used to launch them. 89 | 90 | ![](images/customMenuItems3.png) 91 | 92 | ## Open Scenes in the Editor 93 | 94 | As opposed to searching in the [Project Window](https://docs.unity3d.com/Manual/ProjectView.html), one instance where I especially like to use custom hotkey menu items is for opening scenes in the editor. This can be easily achieved by using the scene's build index and a hot key combination, for instance ALT-SHIFT-0, and the *EditorSceneManager*. 95 | 96 | ```C# 97 | #if UNITY_EDITOR 98 | using UnityEditor; 99 | using UnityEditor.SceneManagement; 100 | using UnityEngine; 101 | 102 | public class CustomMenus 103 | { 104 | /// Opens the scene with build index 0 using a menu item or hotkey SHIFT-ALT-0. 105 | [MenuItem("Tools/Open Scene/Scene0 #&0")] 106 | public static void OpenScene0() 107 | { 108 | EditorSceneManager.OpenScene("Assets/Scenes/Scene0.unity"); 109 | } 110 | } 111 | #endif 112 | ``` 113 | 114 | ## Further Reading 115 | 116 | [Unity Editor Extensions – Menu Items](https://unity3d.com/learn/tutorials/topics/interface-essentials/unity-editor-extensions-menu-items) 117 | 118 | [Scripting Reference - MenuItem](https://docs.unity3d.com/ScriptReference/MenuItem.html) 119 | 120 | [Scripting Reference - EditorSceneManager](https://docs.unity3d.com/ScriptReference/SceneManagement.EditorSceneManager.html) 121 | -------------------------------------------------------------------------------- /#18-CustomMenuItems/images/customMenuItems1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#18-CustomMenuItems/images/customMenuItems1.png -------------------------------------------------------------------------------- /#18-CustomMenuItems/images/customMenuItems2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#18-CustomMenuItems/images/customMenuItems2.png -------------------------------------------------------------------------------- /#18-CustomMenuItems/images/customMenuItems3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#18-CustomMenuItems/images/customMenuItems3.png -------------------------------------------------------------------------------- /#19-LoadingArrayAssetsAtPath/FolderManager.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Written by James Leahy. (c) 2017-2018 DeFunc Art. 3 | * https://github.com/defuncart/ 4 | */ 5 | #if UNITY_EDITOR 6 | using System.IO; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using UnityEditor; 10 | using UnityEngine; 11 | 12 | // Part of the DeFuncArt.Utilities namespace. 13 | namespace DeFuncArtEditor 14 | { 15 | /// Performs operations on files and folders in the editor. 16 | public class FolderManager 17 | { 18 | /// Gets an array of assets of type T at a given path. This path is relative to /Assets. 19 | /// An array of assets of type T. 20 | /// The file path relative to /Assets. 21 | public static T[] GetAssetsAtPath(string path, bool recursively = false) where T : Object 22 | { 23 | //create a list to store results 24 | List returnList = new List(); 25 | //process the given filepath 26 | ProcessDirectory(path, ref returnList, recursively); 27 | //return results as an array 28 | return returnList.ToArray(); 29 | } 30 | 31 | /// Processes a directory to find files of type T. 32 | /// The file path relative to /Assets). 33 | /// A ref list to add results to. 34 | /// Whether subdirectories should be considered. 35 | /// The type parameter. 36 | private static void ProcessDirectory(string path, ref List returnList, bool recursively = false) where T : Object 37 | { 38 | //get the contents of the folder's full path (excluding any meta files) sorted alphabetically 39 | IEnumerable fullpaths = Directory.GetFiles(FullPathForRelativePath(path)).Where(x => !x.EndsWith(".meta")).OrderBy(s => s); 40 | //loop through the folder contents 41 | foreach (string fullpath in fullpaths) 42 | { 43 | //determine a path starting with Assets 44 | string assetPath = fullpath.Replace(Application.dataPath, "Assets"); 45 | //load the asset at this relative path 46 | Object obj = AssetDatabase.LoadAssetAtPath(assetPath); 47 | //and add it to the list if it is of type T 48 | if(obj is T) { returnList.Add(obj as T); } 49 | } 50 | 51 | if(recursively) 52 | { 53 | //path is relative to /Assets - to determine subdirectories, we need to query the full data 54 | string[] subdirectories = Directory.GetDirectories(FullPathForRelativePath(path)); 55 | //loop through all subdirectories and recursively call ProcessDirectory on a relative path 56 | foreach(string subdirectory in subdirectories) 57 | { 58 | string subPath = path + "/" + Path.GetFileName(subdirectory); 59 | ProcessDirectory(subPath, ref returnList); 60 | } 61 | } 62 | } 63 | 64 | /// Returns a full path for a given relative path. 65 | /// The relative path. 66 | /// The full path. 67 | private static string FullPathForRelativePath(string path) 68 | { 69 | return Application.dataPath + "/" + path; 70 | } 71 | } 72 | } 73 | #endif 74 | -------------------------------------------------------------------------------- /#19-LoadingArrayAssetsAtPath/README.md: -------------------------------------------------------------------------------- 1 | # 19 - Loading an Array of Assets at Path 2 | 3 | AssetDatabase allows us to easily load an asset in editor mode using the ```LoadAssetAtPath``` method: 4 | 5 | ```C# 6 | MyAsset myAsset = AssetDatabase.LoadAssetAtPath(pathToAsset); 7 | ``` 8 | 9 | Thus once would expect that ```LoadAllAssetsAtPath``` would return an object array of all assets of type T at *pathToAssets*: 10 | 11 | ```C# 12 | MyAsset[] myAssets = AssetDatabase.LoadAllAssetsAtPath(pathToAssets); 13 | ``` 14 | 15 | and is then somewhat bemused when they receive an empty array. 16 | 17 | So how come ```AssetDatabase.LoadAllAssetsAtPath("Assets/Data/")``` returns nothing when ```AssetDatabase.LoadAssetAtPath("Assets/Data/MyAsset.asset")``` returns a valid asset? 18 | 19 | Well it turns out that my (and potentially your) interpretation of this method signature (and description *Returns an array of all asset objects at assetPath*) is incorrect. ```LoadAllAssetsAtPath``` doesn't load all assets for a given directory, it loads all objects for a given asset. This is clear when visiting the API page, but not when coding in the IDE. 20 | 21 | ```C# 22 | Object[] data = AssetDatabase.LoadAllAssetsAtPath("Assets/MyMaterial.mat"); 23 | ``` 24 | 25 | > Some asset files may contain multiple objects (such as a Maya file which may contain multiple Meshes and GameObjects). This function returns all asset objects at a given path including hidden in the Project view. 26 | 27 | ## Rolling our own 28 | 29 | Luckily we can easily roll our own solution by using **System.IO.Directory** to get the contents of a folder, and using **System.Linq** to ignore all *.meta* files. 30 | 31 | ```C# 32 | /// Gets an array of assets of type T at a given path. This path is relative to /Assets. 33 | /// An array of assets of type T. 34 | /// The file path relative to /Assets. 35 | public static T[] GetAssetsAtPath(string path) where T : Object 36 | { 37 | List returnList = new List(); 38 | 39 | //get the contents of the folder's full path (excluding any meta files) sorted alphabetically 40 | IEnumerable fullpaths = Directory.GetFiles(FullPathForRelativePath(path)).Where(x => !x.EndsWith(".meta")).OrderBy(s => s); 41 | //loop through the folder contents 42 | foreach (string fullpath in fullpaths) 43 | { 44 | //determine a path starting with Assets 45 | string assetPath = fullpath.Replace(Application.dataPath, "Assets"); 46 | //load the asset at this relative path 47 | Object obj = AssetDatabase.LoadAssetAtPath(assetPath); 48 | //and add it to the list if it is of type T 49 | if(obj is T) { returnList.Add(obj as T); } 50 | } 51 | 52 | return returnList.ToArray(); 53 | } 54 | ``` 55 | 56 | This can then be used as follows: 57 | 58 | ```C# 59 | MyAsset[] myAssets = GetAssetsAtPath("/Data"); 60 | ``` 61 | 62 | where the path supplied is relative to */Assets*. 63 | 64 | ## Search Subdirectories 65 | 66 | The above solution works pretty well when we want to load an array of assets from a top-level directory, however, what if we would like to search subdirectories too? Again this is easily achieved using **System.IO.Directory** and recursion. 67 | 68 | ```C# 69 | /// Gets an array of assets of type T at a given path. This path is relative to /Assets. 70 | /// An array of assets of type T. 71 | /// The file path relative to /Assets. 72 | /// Whether subdirectories should be considered. 73 | public static T[] GetAssetsAtPath(string path, bool recursively = false) where T : Object 74 | { 75 | //create a list to store results 76 | List returnList = new List(); 77 | //process the given filepath 78 | ProcessDirectory(path, ref returnList, recursively); 79 | //return results as an array 80 | return returnList.ToArray(); 81 | } 82 | 83 | /// Processes a directory to find files of type T. 84 | /// The file path relative to /Assets). 85 | /// A ref list to add results to. 86 | /// Whether subdirectories should be considered. 87 | /// The type parameter. 88 | private static void ProcessDirectory(string path, ref List returnList, bool recursively = false) where T : Object 89 | { 90 | //get the contents of the folder's full path (excluding any meta files) sorted alphabetically 91 | IEnumerable fullpaths = Directory.GetFiles(FullPathForRelativePath(path)).Where(x => !x.EndsWith(".meta")).OrderBy(s => s); 92 | //loop through the folder contents 93 | foreach (string fullpath in fullpaths) 94 | { 95 | //determine a path starting with Assets 96 | string assetPath = fullpath.Replace(Application.dataPath, "Assets"); 97 | //load the asset at this relative path 98 | Object obj = AssetDatabase.LoadAssetAtPath(assetPath); 99 | //and add it to the list if it is of type T 100 | if(obj is T) { returnList.Add(obj as T); } 101 | } 102 | 103 | if(recursively) 104 | { 105 | //path is relative to /Assets - to determine subdirectories, we need to query the full data 106 | string[] subdirectories = Directory.GetDirectories(FullPathForRelativePath(path)); 107 | //loop through all subdirectories and recursively call ProcessDirectory on a relative path 108 | foreach(string subdirectory in subdirectories) 109 | { 110 | string subPath = path + "/" + Path.GetFileName(subdirectory); 111 | ProcessDirectory(subPath, ref returnList); 112 | } 113 | } 114 | } 115 | ``` 116 | 117 | ## Conclusion 118 | 119 | This is a short yet powerful editor script which I use all the time, especially when developing tools to automatically import assets from a given directory. 120 | 121 | ## Further Reading 122 | 123 | [Scripting Reference - AssetDatabase](https://docs.unity3d.com/ScriptReference/AssetDatabase.html) 124 | 125 | [Scripting Reference - AssetDatabase.LoadAssetAtPath](https://docs.unity3d.com/ScriptReference/AssetDatabase.LoadAssetAtPath.html) 126 | 127 | [Scripting Reference - AssetDatabase.LoadAllAssetsAtPath](https://docs.unity3d.com/ScriptReference/AssetDatabase.LoadAllAssetsAtPath.html) 128 | -------------------------------------------------------------------------------- /#20-AssetPostprocessor/README.md: -------------------------------------------------------------------------------- 1 | # 20 - Asset Postprocessor 2 | 3 | *AssetPostprocessor* is an Editor class which allows access to the import pipeline and the ability to run scripts prior or after importing assets. Each asset to import has an *assetImporter* and an *assetPath*, both of which are accessible in *Preprocess* and *Postprocess* callbacks. The *assetImporter* itself can either be an *AudioImporter*, *IHVImageFormatImporter*, *ModelImporter*, *MovieImporter*, *PluginImporter*, *SpeedTreeImporter*, *SubstanceImporter*, *TextureImporter*, *TrueTypeFontImporter* or *VideoClipImporter*, depending on the asset being imported. 4 | 5 | ## OnPreprocessAudio 6 | 7 | As a simple example, lets assume that all audio files that will be imported into the project are speech files that should always undergo the same import settings. In that case, we could write a simple script which has a callback before an audio file is imported, and adjust the importer's settings accordingly. 8 | 9 | ```C# 10 | #if UNITY_EDITOR 11 | using UnityEditor; 12 | using UnityEngine; 13 | 14 | /// An editor script which listens to import events. 15 | public class MyAssetPostprocessor : AssetPostprocessor 16 | { 17 | /// Callback before an audio clip is imported. 18 | private void OnPreprocessAudio() 19 | { 20 | AudioImporter audioImporter = assetImporter as AudioImporter; 21 | audioImporter.forceToMono = true; 22 | audioImporter.preloadAudioData = false; 23 | AudioImporterSampleSettings settings = new AudioImporterSampleSettings() { 24 | loadType = AudioClipLoadType.DecompressOnLoad, 25 | compressionFormat = AudioCompressionFormat.Vorbis, 26 | quality = 0, 27 | sampleRateSetting = AudioSampleRateSetting.OverrideSampleRate, 28 | sampleRateOverride = 22050 29 | }; 30 | audioImporter.defaultSampleSettings = settings; 31 | } 32 | } 33 | #end UNITY_EDITOR 34 | ``` 35 | 36 | ## OnPostprocessSprites 37 | 38 | As another simple example, lets assume that all UI sprites should have high quality compression. As all UI sprites will be imported into ```Assets/Sprites/UI```, we can use the *assetPath* to verify which textures are UI sprites, and **OnPostprocessSprites** to automatically set the asset's import settings. 39 | 40 | ```C# 41 | #if UNITY_EDITOR 42 | using UnityEditor; 43 | using UnityEngine; 44 | 45 | /// An editor script which listens to import events. 46 | public class MyAssetPostprocessor : AssetPostprocessor 47 | { 48 | /// Callback after a texture of sprites has completed importing. 49 | /// The texture. 50 | /// The array of sprites. 51 | private void OnPostprocessSprites(Texture2D texture, Sprite[] sprites) 52 | { 53 | if(System.IO.Path.GetDirectoryName(assetPath) == "Assets/Sprites/UI") 54 | { 55 | TextureImporter textureImporter = assetImporter as TextureImporter; 56 | textureImporter.textureCompression = TextureImporterCompression.CompressedHQ; 57 | textureImporter.crunchedCompression = true; 58 | textureImporter.compressionQuality = 100; 59 | } 60 | } 61 | } 62 | #end UNITY_EDITOR 63 | ``` 64 | 65 | ## Conclusion 66 | 67 | Although both these examples are simple and somewhat specific, one can appreciate the efficiency of correctly assigning settings on import as opposed to manually editing after import. Moreover, in combination with custom windows or menu items, the artist or sound engineer could define what the custom import settings should be, and use these as defaults. 68 | 69 | ## Further Reading 70 | 71 | [Scripting API - AssetPostprocessor](https://docs.unity3d.com/ScriptReference/AssetPostprocessor.html) 72 | 73 | [Scripting API - AssetPostprocessor.OnPreprocessAudio](https://docs.unity3d.com/ScriptReference/AssetPostprocessor.OnPreprocessAudio.html) 74 | 75 | [Scripting API - AssetPostprocessor.OnPostprocessSprites](https://docs.unity3d.com/ScriptReference/AssetPostprocessor.OnPostprocessSprites.html) 76 | -------------------------------------------------------------------------------- /#21-Singletons/MonoSingleton.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Written by James Leahy. (c) 2017-2018 DeFunc Art. 3 | * https://github.com/defuncart/ 4 | */ 5 | using UnityEngine; 6 | 7 | /// A base abstract class which can be extented to make a singleton component attachable to a game object. 8 | public abstract class MonoSingleton : MonoBehaviour where T : MonoSingleton 9 | { 10 | /// A static instance which is created on first lauch and thereafter never destroyed. 11 | public static T instance { get; private set; } 12 | 13 | /// Callback when the instance is awoken. 14 | /// Ensure that there is only one instance of the class and that it cannot be destroyed. 15 | private void Awake() 16 | { 17 | if(instance == null) { instance = this as T; DontDestroyOnLoad(gameObject); instance.Init(); } 18 | else if(instance != this) { Destroy(gameObject); } 19 | } 20 | 21 | /// Init the specific inherited class. 22 | protected virtual void Init() {} 23 | } 24 | -------------------------------------------------------------------------------- /#21-Singletons/README.md: -------------------------------------------------------------------------------- 1 | # 21 - Singletons 2 | 3 | The *singleton pattern* is a design pattern that restricts the instantiation of a class to a single, globally accessible instance. This is particularly useful when a single instance is needed to coordinate actions across an entire project. The benefits of this approach are clear: 4 | 1. we have a global pointer which we do not need to tediously pass to all classes who need to reference it. 5 | 2. as the class is initialized at runtime, it can utilize runtime information (unlike static classes). 6 | 3. the class can be lazily instantiated, that is, only created once the instance is first needed. This can be quite helpful for resource-heavy classes. Static classes are created when first loaded. 7 | 8 | The singleton pattern is often used in games as 'Managers', for instance GameManager, AudioManager, LocalizationManager, however it is often abused and overused due to lazyness, lack of OOP understanding or poor code design. There are tons of articles and discussions on this issue, with the general consensus of using the pattern *sparingly*. 9 | 10 | When deciding whether to use this pattern, it is worthwhile to consider if **1)** a static class could be instead utilized or **2)** if the code could be incorporated into another class. 11 | 12 | A class of constant variables that need to be global? Static members of a static class. An AnalyticsManager that sends custom analytic events? A static class with static methods. 13 | 14 | Moreover, in the following example adapted from Robert Nystrom, we have a *Bullet* class and *BulletManager*. As the game has many bullets, we probably need a single-instance *BulletManager* right? 15 | 16 | ```C# 17 | public class Bullet 18 | { 19 | public int x { get; set; } 20 | public int y { get; set; } 21 | } 22 | 23 | public class BulletManager 24 | { 25 | public Bullet Create(int x, int y) 26 | { 27 | Bullet bullet = new Bullet(); 28 | bullet.x = x; bullet.y = y; 29 | return bullet; 30 | } 31 | 32 | public bool IsOnScreen(Bullet bullet) 33 | { 34 | return bullet.x >= 0 && bullet.x < Screen.width && bullet.y >= 0 && bullet.y < Screen.height; 35 | } 36 | 37 | public void Move(Bullet bullet) 38 | { 39 | bullet.x += 5; 40 | } 41 | } 42 | ``` 43 | 44 | Actually no. *BulletManager* is simply a poorly designed helper class whose functionality could easily be incorporated into the *Bullet* class itself. 45 | 46 | ```C# 47 | public class Bullet 48 | { 49 | public int x { get; set; } 50 | public int y { get; set; } 51 | 52 | public bool isOnScreen 53 | { 54 | get { return x >= 0 && x < Screen.width && y >= 0 && y < Screen.height; } 55 | } 56 | 57 | public void Move() 58 | { 59 | x += 5; 60 | } 61 | } 62 | ``` 63 | 64 | There are sometimes, however, when I utilize the singleton pattern, for instance when saving/loading player data to disk and when classes need to reference data (LocalizationManager, AudioManager etc.). A *MonoBehaviour* class can easily be turned into a singleton by extending 65 | 66 | ```C# 67 | /// A base abstract class which can be extented to make a singleton component attachable to a game object. 68 | public abstract class MonoSingleton : MonoBehaviour where T : MonoSingleton 69 | { 70 | /// A static instance which is created on first lauch and thereafter never destroyed. 71 | public static T instance { get; private set; } 72 | 73 | /// Callback when the instance is awoken. 74 | /// Ensure that there is only one instance of the class and that it cannot be destroyed. 75 | private void Awake() 76 | { 77 | if(instance == null) { instance = this as T; DontDestroyOnLoad(gameObject); instance.Init(); } 78 | else if(instance != this) { Destroy(gameObject); } 79 | } 80 | 81 | /// Init the specific inherited class. 82 | protected virtual void Init() {} 83 | } 84 | ``` 85 | 86 | while a class saved to disk using [*BinarySerialization*](https://github.com/defuncart/50-unity-tips/tree/master/%2307-BinarySerialization) can be extended from 87 | 88 | ```C# 89 | [System.Serializable] 90 | public abstract class SerializableSingleton where T : class 91 | { 92 | /// The class's name. 93 | protected static string className 94 | { 95 | get { return typeof(T).Name; } 96 | } 97 | 98 | protected static T _instance; //backing variable for instance 99 | /// A computed property that returns a static instance of the class. 100 | /// If the instance hasn't already been loaded, then it is loaded from File. 101 | public static T instance 102 | { 103 | get { return _instance ?? (_instance = BinarySerializer.Load(className)); } 104 | } 105 | 106 | /// As the object's constructor is private, this method allows the creation of 107 | /// the object. Only creates the object if one isn't already saved to disk. 108 | public static void Create() 109 | { 110 | if(!BinarySerializer.FileExists(className)) 111 | { 112 | _instance = (T)System.Activator.CreateInstance(type: typeof(T), nonPublic: true); 113 | } 114 | } 115 | 116 | /// Saves the current instance to file. 117 | protected void Save() 118 | { 119 | BinarySerializer.Save(className, this); 120 | } 121 | } 122 | ``` 123 | 124 | In short, although singletons are generally overused and abused, they are still sometimes a viable design pattern. In future tips I will show how I utilize them within my projects. 125 | 126 | ## Further Reading 127 | [Singleton Pattern](https://en.wikipedia.org/wiki/Singleton_pattern) 128 | 129 | [Game Programming Patterns: Singleton](http://gameprogrammingpatterns.com/singleton.html) 130 | 131 | [Service Locator Pattern](https://en.wikipedia.org/wiki/Service_locator_pattern) 132 | 133 | [What is so bad about singletons?](https://stackoverflow.com/a/138012) 134 | 135 | [On Design Patterns: When to use the Singleton?](https://stackoverflow.com/a/228380) 136 | -------------------------------------------------------------------------------- /#21-Singletons/SerializableSingleton.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Written by James Leahy. (c) 2018 DeFunc Art. 3 | * https://github.com/defuncart/ 4 | */ 5 | using UnityEngine; 6 | 7 | /// A base abstract class which can be extented to make a serializable, singleton class. 8 | /// The class is saved to/loaded from disk using its class name. 9 | [System.Serializable] 10 | public abstract class SerializableSingleton where T : class 11 | { 12 | /// The class's name. 13 | protected static string className 14 | { 15 | get { return typeof(T).Name; } 16 | } 17 | 18 | protected static T _instance; //backing variable for instance 19 | /// A computed property that returns a static instance of the class. 20 | /// If the instance hasn't already been loaded, then it is loaded from File. 21 | public static T instance 22 | { 23 | get { return _instance ?? (_instance = BinarySerializer.Load(className)); } 24 | } 25 | 26 | /// As the object's constructor is private, this method allows the creation of 27 | /// the object. Only creates the object if one isn't already saved to disk. 28 | public static void Create() 29 | { 30 | if(!BinarySerializer.FileExists(className)) 31 | { 32 | Debug.Log(string.Format("{0} Creating...", className)); 33 | _instance = (T)System.Activator.CreateInstance(type: typeof(T), nonPublic: true); 34 | } 35 | } 36 | 37 | /// Saves the current instance to file. 38 | protected void Save() 39 | { 40 | BinarySerializer.Save(className, this); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /#22-AndroidDeviceFilter/README.md: -------------------------------------------------------------------------------- 1 | # 22 - Android Device Filter 2 | 3 | Tucked away in **Player Settings - Android - Other Settings**, there is a *Device Filter* setting with the options **FAT (ARMv7 + x86)**, **ARMv7** and **x86**. There are many approaches to reducing the build size for Android, but one sure approach is to change the default device filter from **FAT** (which builds a combined executable compatible with both ARMv7 and x86 architectures), and build separate builds for **ARMv7** and **x86**, where the **x86** build has a higher build number than **ARMv7**. Although this requires building two builds, you will reduce the actual install sizes by about 10MB. 4 | 5 | The Google Play Multiple APK Support state that: 6 | 7 | > - All APKs you publish for the same application must have the same package name and be signed with the same certificate key. 8 | > - Each APK must have a different version code, specified by the android:versionCode attribute. 9 | 10 | For an empty project, **FAT** has an install size of 41.88Mb, while **ARMv7** had an install size of 32.23Mb. On other projects I've notice roughly a 10Mb difference also. It's basically a free trick. 11 | 12 | Now you might be thinking that building multiple builds seems time consuming, however we can easy write a custom script to take care of that for us: 13 | 14 | ```c# 15 | /// Builds the game for the Android platform using a menu item. 16 | [MenuItem("Tools/Build/Android")] 17 | public static void BuildForAndroid() 18 | { 19 | ///arm 20 | BuildAndroidForDevice(AndroidTargetDevice.ARMv7); 21 | //x86 22 | BuildAndroidForDevice(AndroidTargetDevice.x86); 23 | } 24 | 25 | /// Builds the game for the Android platform using a given target device. 26 | private static void BuildAndroidForDevice(AndroidTargetDevice device) 27 | { 28 | PlayerSettings.Android.targetDevice = device; 29 | string androidPath = string.Format("{0}/Builds/{1} ({2}).apk", Path.GetDirectoryName(Application.dataPath), "My App", device.ToString()); 30 | BuildPipeline.BuildPlayer(EditorBuildSettings.scenes, androidPath, BuildTarget.Android, BuildOptions.None); 31 | } 32 | ``` 33 | 34 | The last thing that we need to do is assign different version codes to each build. One approach is to define a large integer whose components reflect the builds major version, minor version, path version, build number and target device: 35 | 36 | ![](images/androidDeviceFilter1.png) 37 | 38 | Thus version 1.2.3 with build version 17 for arm would be 10203170, while x86 would be 10203171. Notice that each x86 build has a higher version code than the corresponding arm build, while version 1.2.4 would have a higher version code than 1.2.3. 39 | 40 | ```c# 41 | /// Determines the correct versionCode for Android. 42 | /// The app's major version number (i.e. 1). 43 | /// The app's minor version number (i.e. 0). 44 | /// The app's patch version number (i.e. 0). 45 | /// The app's build version number (i.e. 99). 46 | /// Whether it is an x86 build. 47 | private static int AndroidVersionCode(int major, int minor, int patch, int build, bool x86) 48 | { 49 | return major*100000 + minor*10000 + patch*1000 + build*10 + (x86 ? 1 : 0); 50 | } 51 | ``` 52 | 53 | ## Further Reading 54 | 55 | [Android Developer - Multiple APK Support](https://developer.android.com/google/play/publishing/multiple-apks.html) 56 | -------------------------------------------------------------------------------- /#22-AndroidDeviceFilter/images/androidDeviceFilter1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#22-AndroidDeviceFilter/images/androidDeviceFilter1.png -------------------------------------------------------------------------------- /#23-Pseudolocalization/English.json: -------------------------------------------------------------------------------- 1 | { "items" : [ 2 | { "key": "hello", "value": "Hello World!"}, 3 | { "key": "test", "value": "The quick brown fox jumps over the lazy dog."} 4 | ]} -------------------------------------------------------------------------------- /#23-Pseudolocalization/GermanPseudo.json: -------------------------------------------------------------------------------- 1 | {"items":[{"key":"hello","value":"Hellö Wörld!|ßüüÜß"},{"key":"test","value":"The qüick bröwn föx jümpß över the lazy dög.|ÄßÖüÜüÜÜÖÜüäää"}]} -------------------------------------------------------------------------------- /#23-Pseudolocalization/PseudoLocalizationWindow.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Written by James Leahy. (c) 2017-2018 DeFunc Art. 3 | * https://github.com/defuncart/ 4 | */ 5 | #if UNITY_EDITOR 6 | using DeFuncArt.Serialization; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.IO; 10 | using System.Text; 11 | using UnityEditor; 12 | using UnityEngine; 13 | using UnityEngine.Assertions; 14 | 15 | /// An EditorWindow to gerate Pseudolocalizations. 16 | public class PseudoLocalizationWindow : EditorWindow 17 | { 18 | /// An enum representing the types of languages that can be rendered in Pseudotext. 19 | public enum Language 20 | { 21 | German, Polish, Russian 22 | } 23 | /// The chosen language to render. 24 | private Language langugage; 25 | 26 | /// The special characters for German. 27 | private string[] specialCharacters_DE = { "ä", "ö", "ü", "ß", "Ä", "Ö", "Ü" }; 28 | /// The special characters for Polish. 29 | private string[] specialCharacters_PL = { "ą", "ć", "ę", "ł", "ń", "ó", "ś", "ż", "ź", "Ą", "Ć", "Ę", "Ł", "Ń", "Ó", "Ś", "Ż", "Ź" }; 30 | /// The special characters for Russian. 31 | private string[] specialCharacters_RU = { "а", "б", "в", "г", "д", "е", "ё", "ж", "з", "и", "й", "к", "л", "м", "н", "о", "п", "р", "с", "т", "у", "ф", "х", "ц", "ч", "ш", "щ", "ъ", "ы", "ь", "э", "ю", "я", "А", "Б", "В", "Г", "Д", "Е", "Ё", "Ж", "З", "И", "Й", "К", "Л", "М", "Н", "О", "П", "Р", "С", "Т", "У", "Ф", "Х", "Ц", "Ч", "Ш", "Щ", "Ъ", "Ы", "Ь", "Э", "Ю", "Я" }; 32 | /// The special characters the selected language. 33 | private string[] specialCharacters 34 | { 35 | get 36 | { 37 | if (langugage == Language.German) { return specialCharacters_DE; } 38 | else if (langugage == Language.Polish) { return specialCharacters_PL; } 39 | else { return specialCharacters_RU; } 40 | } 41 | } 42 | /// A random special character for the selected language. 43 | private string randomSpecialCharacter 44 | { 45 | get { return specialCharacters[Random.Range(0, specialCharacters.Length)]; } 46 | } 47 | 48 | /// A dictionary of mapping characters for German. 49 | private Dictionary mappingCharacters_DE = new Dictionary(){ 50 | {"a" , new string[]{"ä"} }, {"A" , new string[]{"Ä"} }, 51 | {"o" , new string[]{"ö"} }, {"O" , new string[]{"Ö"} }, 52 | {"u" , new string[]{"ü"} }, {"U" , new string[]{"Ü"} }, 53 | {"s" , new string[]{"ß"} } 54 | }; 55 | /// A dictionary of mapping characters for Polish. 56 | private Dictionary mappingCharacters_PL = new Dictionary(){ 57 | {"a" , new string[]{"ą"} }, {"A" , new string[]{"Ą"} }, 58 | {"c" , new string[]{"ć"} }, {"C" , new string[]{"Ć"} }, 59 | {"e" , new string[]{"ę"} }, {"E" , new string[]{"Ę"} }, 60 | {"l" , new string[]{"ł"} }, {"L" , new string[]{"Ł"} }, 61 | {"n" , new string[]{"ń"} }, {"N" , new string[]{"Ń"} }, 62 | {"o" , new string[]{"ó"} }, {"O" , new string[]{"Ó"} }, 63 | {"s" , new string[]{"ś"} }, {"S" , new string[]{"Ś"} }, 64 | {"z" , new string[]{"ż", "ź"} }, {"Z" , new string[]{"Ż", "Ź"} } 65 | }; 66 | /// A dictionary of mapping characters for Russian. 67 | private Dictionary mappingCharacters_RU = new Dictionary(){ 68 | {"a" , new string[]{"а"} }, {"A" , new string[]{"А"} }, 69 | {"b" , new string[]{"ь", "в", "б", "ъ"} }, {"B" , new string[]{"Ь", "В", "Б", "Ъ"} }, 70 | {"c" , new string[]{"с"} }, {"C" , new string[]{"С"} }, 71 | {"d" , new string[]{"д"} }, {"D" , new string[]{"Д"} }, 72 | {"e" , new string[]{"е", "ё", "э"} }, {"E" , new string[]{"Е", "Ё", "Э"} }, 73 | {"f" , new string[]{"ф"} }, {"F" , new string[]{"Ф"} }, 74 | {"g" , new string[]{"г"} }, {"G" , new string[]{"Г"} }, 75 | {"h" , new string[]{"н"} }, {"H" , new string[]{"Н"} }, 76 | {"i" , new string[]{"и"} }, {"I" , new string[]{"И"} }, 77 | {"j" , new string[]{"й"} }, {"J" , new string[]{"Й"} }, 78 | {"k" , new string[]{"к"} }, {"K" , new string[]{"К"} }, 79 | {"l" , new string[]{"л"} }, {"L" , new string[]{"Л"} }, 80 | {"m" , new string[]{"м"} }, {"M" , new string[]{"М"} }, 81 | {"n" , new string[]{"п"} }, {"N" , new string[]{"П"} }, 82 | {"o" , new string[]{"о"} }, {"O" , new string[]{"О"} }, 83 | {"p" , new string[]{"р"} }, {"P" , new string[]{"Р"} }, 84 | {"q" , new string[]{"ч"} }, {"Q" , new string[]{"Ч"} }, 85 | {"r" , new string[]{"я"} }, {"R" , new string[]{"Я"} }, 86 | {"s" , new string[]{"з"} }, {"S" , new string[]{"З"} }, 87 | {"t" , new string[]{"т"} }, {"T" , new string[]{"Т"} }, 88 | {"u" , new string[]{"ц"} }, {"U" , new string[]{"Ц"} }, 89 | {"v" , new string[]{"ч"} }, {"V" , new string[]{"Ч"} }, 90 | {"w" , new string[]{"ш", "щ"} }, {"W" , new string[]{"Ш", "Щ"} }, 91 | {"x" , new string[]{"х", "ж"} }, {"X" , new string[]{"Х", "Ж"} }, 92 | {"y" , new string[]{"у"} }, {"Y" , new string[]{"У"} }, 93 | {"z" , new string[]{"з"} }, {"Z" , new string[]{"З"} } 94 | }; 95 | /// A dictionary of mapping characters for the selected language. 96 | private Dictionary mappingCharacters 97 | { 98 | get 99 | { 100 | if(langugage == Language.German) { return mappingCharacters_DE; } 101 | else if(langugage == Language.Polish) { return mappingCharacters_PL; } 102 | else { return mappingCharacters_RU; } 103 | } 104 | } 105 | 106 | /// A reference to the TextAsset JSON file with English strings. 107 | public Object englishJSONAsset; 108 | 109 | //add a menu item named "Pseudolocalization" to the Tools menu 110 | [MenuItem("Tools/Pseudolocalization")] 111 | public static void ShowWindow() 112 | { 113 | //show existing window instance - if one doesn't exist, create one 114 | GetWindow(typeof(PseudoLocalizationWindow)); 115 | } 116 | 117 | /// Draws the window. 118 | private void OnGUI() 119 | { 120 | //set the label's style to wrap words 121 | EditorStyles.label.wordWrap = true; 122 | 123 | //draw an info label 124 | EditorGUILayout.LabelField("Input English strings (as JSON):"); 125 | 126 | //draw the englishJSONAsset 127 | englishJSONAsset = EditorGUILayout.ObjectField(englishJSONAsset, typeof(TextAsset), true); 128 | 129 | //draw a space 130 | EditorGUILayout.Space(); 131 | 132 | //draw a language popup 133 | langugage = (Language) EditorGUILayout.EnumPopup("Language to render:", langugage); 134 | 135 | //draw an info label 136 | if(englishJSONAsset != null) 137 | { 138 | string relativeOutputFilepath = AssetDatabase.GetAssetPath(englishJSONAsset).Replace(Path.GetFileName(AssetDatabase.GetAssetPath(englishJSONAsset)), string.Format("{0}Pseudo.json", langugage.ToString())); 139 | EditorGUILayout.LabelField(string.Format("The output file will be saved to {0}", relativeOutputFilepath)); 140 | } 141 | 142 | //draw a space 143 | EditorGUILayout.Space(); 144 | 145 | //draw a button which, if triggered, render the Pseudolocalization for the selected language 146 | if(GUILayout.Button("Render Pseudolocalization")) { RenderPseudolocalization(); } 147 | } 148 | 149 | /// Render the Pseudolocalization for the selected language. 150 | private void RenderPseudolocalization() 151 | { 152 | //display an error if there is no input file 153 | if(englishJSONAsset == null) { Debug.LogErrorFormat("No input file."); return; } 154 | 155 | //load the json file as a dictionary 156 | TextAsset asset = englishJSONAsset as TextAsset; 157 | Dictionary inputJSON = JSONSerializer.FromJson>(asset.text); 158 | Dictionary outputJSON = new Dictionary(); 159 | 160 | //loop through each kvp 161 | foreach(KeyValuePair kvp in inputJSON) 162 | { 163 | //determine the pseudotranslation 164 | string englishText = kvp.Value; 165 | int numberOfRandomSpecialCharactersToGenerate = PseudotranslationLengthForText(englishText) - englishText.Length; 166 | string pseudoTranslation = string.Format("{0}|{1}", AddSpecialCharactersToText(englishText), GenerateXRandomSpecialCharacters(numberOfRandomSpecialCharactersToGenerate)); 167 | 168 | //add to output dictionary 169 | outputJSON[kvp.Key] = pseudoTranslation; 170 | } 171 | 172 | //convert the dictionary to json 173 | string outputJSONString = JSONSerializer.ToJson>(outputJSON); 174 | 175 | //determine a path for the output file 176 | string path = string.Format("{0}/{1}Pseudo.json", Path.GetFullPath(Directory.GetParent(AssetDatabase.GetAssetPath(englishJSONAsset)).FullName), langugage.ToString()); 177 | 178 | //save the file to disk 179 | File.WriteAllText(path, outputJSONString); 180 | 181 | //update asset database 182 | AssetDatabase.Refresh(); 183 | AssetDatabase.SaveAssets(); 184 | } 185 | 186 | /// Returns a string containing mapped special characters (a => ä) for the selected language. 187 | private string AddSpecialCharactersToText(string text) 188 | { 189 | StringBuilder sb = new StringBuilder(); 190 | char[] characters = text.ToCharArray(); 191 | string[] keys = mappingCharacters.Keys.ToArray(); 192 | foreach(char character in characters) 193 | { 194 | int index = System.Array.IndexOf(keys, character.ToString()); 195 | if(index > 0) 196 | { 197 | string[] possibleMappings = mappingCharacters[character.ToString()]; 198 | sb.Append(possibleMappings[Random.Range(0, possibleMappings.Length)]); 199 | } 200 | else { sb.Append(character); } 201 | } 202 | 203 | return sb.ToString(); 204 | } 205 | 206 | /// Returns a string contain X random special characters for the selected language. 207 | private string GenerateXRandomSpecialCharacters(int count) 208 | { 209 | StringBuilder sb = new StringBuilder(); 210 | for(int i=0; i < count; i++) 211 | { 212 | sb.Append(randomSpecialCharacter); 213 | } 214 | return sb.ToString(); 215 | } 216 | 217 | /// Determine the Pseudotranslation length for a given text string. 218 | private int PseudotranslationLengthForText(string text) 219 | { 220 | if(text.Length > 20) { return Mathf.CeilToInt(text.Length * 1.3f); } 221 | else if(text.Length > 10) { return Mathf.CeilToInt(text.Length * 1.4f); } 222 | else { return Mathf.CeilToInt(text.Length * 1.5f); } 223 | } 224 | } 225 | #endif 226 | -------------------------------------------------------------------------------- /#23-Pseudolocalization/README.md: -------------------------------------------------------------------------------- 1 | # 23 - Pseudolocalization 2 | 3 | *Internationalization* is the process of designing a software application so that it can easily be adapted to various other languages and regions without any programming changes. *Localization* is the process of adapting internationalized software for a specific region or language by adding locale-specific components (€2.99 => 2,99€) and translating text (Hello World! => Hallo Welt!). *Pseudolocalization* is a software testing method used before the localization process in which a fake (or pseudo) translations (with the region and language specific characters) are generated: Hello World! => Hellö Wörld!|ÜüäßÖ 4 | 5 | The benefits of pseudolocalization are three fold: 6 | 7 | 1. To test that all (special) characters of the target locale (i.e. German) are displayed correctly. 8 | 2. To test that text boxes can accommodate longer translations. If a pseduotranslation is cutoff or visually looks ugly on the screen, then there's a good chance that the real translation will also be. 9 | 3. To flag hardcoded strings or non-localized art. 10 | 11 | ## Text Expansion 12 | 13 | Considering English as the base language, after translation many languages will exhibit *Text Expansion* and have longer text strings. Generally German extends by 10-35%, Polish 20-30% and Russian by 15%. As a quick rule of thumb, I like to utilize IGDA Localization SIG's suggestions: 14 | 15 | | English Text Length | Pseduotranslation Length | 16 | | :-------------------|:-------------------------| 17 | | 1-10 characters | 150% | 18 | | 10-20 characters | 140% | 19 | | >20 characters | 130% | 20 | 21 | **Note:** some languages can actually have shorter text strings. In this post I will be considering languages that generally expand. 22 | 23 | ## Pseudo text 24 | 25 | From various examples I have seen on the web, there are many different ways to visualize these pseudo texts. Personally I prefer to generate separate pseudolocalizations for each target localization and test them separately to ensure that each target localization will be adequately rendered. My pseduotranslation style starts with the English text, replaces any Basic Latin characters (i.e. English letters) with similar special characters, uses pipe **|** as a divider, and then adds a few random characters at the end (depending on original text size.) So *Hello World!* becomes: 26 | 27 | | English | Hello World! | 28 | | :-------|:------------------------| 29 | | German | Hellö Wörld!|ÜüäßÖ | 30 | | Polish | Hęłłó Wórłd!|ꜿʌ | 31 | | Russian | Нёлло Шоялд!|ОТЧжт | 32 | 33 | It is important to remember that this pseduotranslation is non-sensical: it is not a real translation, instead merely a way to test that the game is ready for the translation stage. 34 | 35 | ## Unity Helper 36 | 37 | A pseudolocalization generator can easily be coded in Unity to enable quick testing. 38 | 39 | ![](images/pseudoLocalization1.png) 40 | 41 | This EditorWindow is given a JSON TextAsset reference (assumed to be English) of the form 42 | 43 | ``` 44 | { "items" : [ 45 | { "key": "hello", "value": "Hello World!"}, 46 | { "key": "test", "value": "The quick brown fox jumps over the lazy dog."} 47 | ]} 48 | ``` 49 | and saves, for instance, *GermanPseudo.json* to the save directory as the original file: 50 | 51 | ``` 52 | {"items":[ 53 | { "key": "hello", "value": "Hellö Wörld!|ßüüÜß"}, 54 | { "key": "test", "value": "The qüick bröwn föx jümpß över the lazy dög.|ÄßÖüÜüÜÜÖÜüäää"} 55 | ]} 56 | ``` 57 | 58 | This JSON is read from and written to disk using the [*JSON Serialization*](https://github.com/defuncart/50-unity-tips/tree/master/%2309-JSONSerialization) mentioned back in Tip #9. 59 | 60 | ## Other Languages 61 | 62 | This approach could be easily extended to other Latin script languages, for instance: 63 | 64 | | Language | Special Characters | 65 | | :--------|:-------------------------------| 66 | | French | àâæéèêëîïôœùûüçÀÂÆÉÈÊËÎÏÔŒÙÛÜÇ | 67 | | Czech | áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽ | 68 | | Spanish | áéíóúüñÁÉÍÓÚÜÑ¿¡ | 69 | 70 | I don't have any experience with languages that contract (when translated to from English) or languages that are written right-to-left, but I imagine a similar approach to Russian would work perfectly fine. 71 | 72 | ## Conclusion 73 | 74 | The earlier that localization issues are flagged, the less time required and more cost effective the solution will be. By investing a small amount of time in generating pseudotranslations and verifying that the game is ready for localization, one can be assured that everything is in order before actually beginning the localization phase. 75 | 76 | ## Further Reading 77 | 78 | [50 Unity Tips - JSON Serialization](https://github.com/defuncart/50-unity-tips/tree/master/%2309-JSONSerialization) 79 | 80 | [Pseudo-Localization – A Must in Video Gaming](http://www.gamasutra.com/blogs/IGDALocalizationSIG/20180504/317560/PseudoLocalization__A_Must_in_Video_Gaming.php) 81 | 82 | [Localization - Expansion and contraction factors](https://www.andiamo.co.uk/resources/expansion-and-contraction-factors) 83 | 84 | [What is Pseudo-Localization?](http://blog.globalizationpartners.com/what-is-pseudo-localization.aspx) 85 | -------------------------------------------------------------------------------- /#23-Pseudolocalization/images/pseudoLocalization1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#23-Pseudolocalization/images/pseudoLocalization1.png -------------------------------------------------------------------------------- /#24-ContextMenus/README.md: -------------------------------------------------------------------------------- 1 | # 24 - Context Menus 2 | 3 | In **#17-CustomEditorWindows** we discussed how we could create a *custom editor* displaying a button that once pressed, executes a function within the component's script. Personally I like the simplicity of a button inside the inspector, however if one only needs to execute a function from the editor, then writing a custom editor for the component is overkill. One alternative is to use the component's context menu. 4 | 5 | ## Context Menu 6 | 7 | When we click on the gear icon in the top-right corner of a component, we are present with a dropdown menu of options where the component can be copied, pasted, pasted as new etc. By using the **[ContextMenu]** attribute, we can add commands to this menu. 8 | 9 | ```c# 10 | public class MyComponent : MonoBehaviour 11 | { 12 | [SerializeField] private int myInt = 100; 13 | 14 | [ContextMenu("Print to Console")] 15 | public void PrintToConsole() 16 | { 17 | Debug.LogFormat("myInt = {0}", myInt); 18 | } 19 | } 20 | ``` 21 | 22 | Now when we select the component's context menu, the option *Print to Console* will be available, while clicking on this option will execute the associated function. Note that this function must be non-static. 23 | 24 | ![](images/contextMenus1.png) 25 | 26 | ## Context Menu Item 27 | 28 | Another approach is to add a context menu direct to a property field by using the **ContextMenuItemAttribute** to execute a named function. 29 | 30 | ```c# 31 | public class MyComponent : MonoBehaviour 32 | { 33 | [ContextMenuItem(name: "Reset this value", function: "Reset")] 34 | [SerializeField] private int myInt = 100; 35 | public void Reset() 36 | { 37 | myInt = 0; 38 | } 39 | } 40 | ``` 41 | 42 | Now when we right-click on the property, we are presented with the option to execute the custom command. 43 | 44 | ![](images/contextMenus2.png) 45 | 46 | ## Conclusion 47 | 48 | *Context Menus* are one approach when one needs to execute a method from the editor without resorting to write a custom editor. Although they are quite useful, they aren't as visible as a GUI button and the end user would need to be notified that they actually exist. 49 | 50 | ## Further Reading 51 | 52 | [50 Unity Tips - #15 Pimp the Inspector](https://github.com/defuncart/50-unity-tips/tree/master/%2315-PimpTheInspector) 53 | 54 | [50 Unity Tips - #17 Custom Editor](https://github.com/defuncart/50-unity-tips/tree/master/%2317-CustomEditor) 55 | 56 | [API - ContextMenu](https://docs.unity3d.com/ScriptReference/ContextMenu.html) 57 | 58 | [API - ContextMenuItemAttribute](https://docs.unity3d.com/ScriptReference/ContextMenuItemAttribute.html) 59 | -------------------------------------------------------------------------------- /#24-ContextMenus/images/contextMenus1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#24-ContextMenus/images/contextMenus1.png -------------------------------------------------------------------------------- /#24-ContextMenus/images/contextMenus2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#24-ContextMenus/images/contextMenus2.png -------------------------------------------------------------------------------- /#25-PropertyDrawers/README.md: -------------------------------------------------------------------------------- 1 | # 25 - Property Drawers 2 | 3 | *Property Drawers* are used to customize the look of certain controls in the Inspector window or the look of a Serializable class itself. Unlike *Custom Editors* which customize a single MonoBehavior or ScriptableObject, *Property Drawers* customize the look of every instance of the Serializable class. Moreover, by using custom *PropertyAttributes*, the look of all class properties with a specified attribute can be easily customized. 4 | 5 | ## Serializable Class 6 | 7 | The look of a Serializable class can be customized by writing a *Property Drawer*. Although this may seem identical to **#17-CustomEditor** where we customized how the EnemyData SerializedObject appeared in the inspector, there is a major difference: *Custom Editors* are used to draw a UnityEngine.Object class in full while *Property Drawers* are used to draw a nested class or struct inside a UnityEngine.Object. 8 | 9 | For more information check out the [Unity Manual](https://docs.unity3d.com/Manual/editor-PropertyDrawers.html) and this [forum post](https://forum.unity.com/threads/when-to-choose-a-custom-editor-vs-property-drawer.333306/#post-2302232). 10 | 11 | ## Custom Attributes 12 | 13 | In **#15-PimpTheInspector** we discussed the **[HideInInspector]**, **[Range]** and **[TextArea]** attributes which hide properties from the inspector, displayed a min/max slide for properties or display properties as a text box, all examples of built-in property drawers. In fact, we can define our own custom *PropertyAttributes* and state how they should draw associated properties in the inspector. 14 | 15 | Presently we can hide properties using **[HideInInspector]**, however what if we want to expose a property to the inspector but have it non-editable? As this is something that could be used throughout multiple classes and projects, one approach is to define a custom property attribute. 16 | 17 | Firstly we define the property attribute itself: 18 | 19 | ```c# 20 | /// An attribute to make a property non-editable (i.e. read only) in the inspector. 21 | public sealed class ReadOnlyAttribute : PropertyAttribute {} 22 | ``` 23 | 24 | and then define how the property attribute should be drawn: 25 | 26 | ```c# 27 | /// A custom property drawer for the ReadOnly attribute. 28 | [CustomPropertyDrawer(typeof(ReadOnlyAttribute))] 29 | public class ReadOnlyAttributeDrawer : PropertyDrawer 30 | { 31 | /// Callback to make a custom GUI for the property. 32 | /// Rectangle on the screen to use for the property GUI.. 33 | /// The SerializedProperty to make the custom GUI for.. 34 | /// The label of this property.. 35 | public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) 36 | { 37 | GUI.enabled = false; 38 | EditorGUI.PropertyField(position, property, label, true); 39 | GUI.enabled = true; 40 | } 41 | 42 | /// Callback to specify how tall the GUI for this field in pixels is. Default is 1 line high. 43 | /// The height in pixels. 44 | /// The SerializedProperty to make the custom GUI for.. 45 | /// The label of this property.. 46 | public override float GetPropertyHeight(SerializedProperty property, GUIContent label) 47 | { 48 | return EditorGUI.GetPropertyHeight(property, label, true); 49 | } 50 | } 51 | ``` 52 | 53 | This property attribute can then be easily used as follows: 54 | 55 | ```c# 56 | public class MyComponent : MonoBehaviour 57 | { 58 | [Header("Health Settings")] 59 | [SerializeField] private int currentHealth = 0; 60 | [ReadOnly, SerializeField] private int maxHealth = 100; 61 | } 62 | ``` 63 | 64 | ![](images/propertyDrawers1.png) 65 | 66 | Custom property attributes are a very powerful feature. To see their potential, take a look at [this list](https://fishtopher.github.io/UnityDrawers/) of open-source property drawers. 67 | 68 | ## Conclusion 69 | 70 | By using *Property Drawers* we can customize how classes or properties are drawn in the inspector. As *Property Drawers* customize the look of all instances of a class/all properties with a specified attribute, they can easily be re-used throughout multiple classes and projects, thus potentially limiting the number of *Custom Editors* that need to be written. 71 | 72 | ## Further Reading 73 | 74 | [50 Unity Tips - #15 Pimp the Inspector](https://github.com/defuncart/50-unity-tips/tree/master/%2315-PimpTheInspector) 75 | 76 | [50 Unity Tips - #17 Custom Editor](https://github.com/defuncart/50-unity-tips/tree/master/%2317-CustomEditor) 77 | 78 | [Forums - When to choose a custom editor vs. property drawer](https://forum.unity.com/threads/when-to-choose-a-custom-editor-vs-property-drawer.333306/) 79 | 80 | [API - PropertyAttribute](https://docs.unity3d.com/ScriptReference/PropertyAttribute.html) 81 | 82 | [API - PropertyDrawer](https://docs.unity3d.com/ScriptReference/PropertyDrawer.html) 83 | 84 | [Manual - PropertyDrawers](https://docs.unity3d.com/Manual/editor-PropertyDrawers.html) 85 | 86 | [Unity Drawers](https://fishtopher.github.io/UnityDrawers/) 87 | -------------------------------------------------------------------------------- /#25-PropertyDrawers/images/propertyDrawers1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#25-PropertyDrawers/images/propertyDrawers1.png -------------------------------------------------------------------------------- /#26-DecoratorDrawers/README.md: -------------------------------------------------------------------------------- 1 | # 26 - Decorator Drawers 2 | 3 | A *DecoratorDrawer* is similar to a *PropertyDrawer*, except that it doesn't draw a property but rather a decorative element based on a *PropertyAttribute*. Unlike property drawers, a decorator drawer isn't associated with property fields, however it still needs to be placed above a field. Another difference is that there can be multiple *DecoratorDrawer* attributes above the same field. Also, unlike property drawers, if a *DecoratorDrawer* attribute is placed above a List or an array property field, the decorator will only show up once before the array and not for every array element. 4 | 5 | In **#15-PimpTheInspector** we discussed the **[Header]**, **[Space]**, **[ToolTip]** attributes which are examples of built-in decorator drawers. In fact, we can define our own custom *PropertyAttributes* and state how they should be draw in the inspector by extending the **DecoratorDrawer** class. 6 | 7 | ## Color Line 8 | 9 | Lets say that we'd like to draw a line with a given width, height, vertical padding and color to visually separate parts of the inspector. Firstly we define the custom property attribute: 10 | 11 | ```c# 12 | /// A decorative attribute to draw a colored line in the inspector. 13 | public sealed class ColorLineAttribute : PropertyAttribute 14 | { 15 | /// The default width. 16 | private const float DEFAULT_WIDTH = 0.5f; 17 | /// The default height. 18 | private const float DEFAULT_HEIGHT = 3f; 19 | /// The default padding. 20 | private const float DEFAULT_PADDING = 5f; 21 | 22 | /// The line's width. This can either be a fixed value (i.e. 100) or relative to the inspector's width (i.e. 0.9). 23 | public float width { get; private set; } 24 | /// The line's height. This can either be a fixed value (i.e. 3) or relative to the inspector's width (i.e. 0.05). 25 | public float height { get; private set; } 26 | /// The vertical padding around the line. This can either be a fixed value (i.e. 5) or relative to the inspector's width (i.e. 0.1). 27 | public float padding { get; private set; } 28 | /// The line's color. 29 | public Color color { get; private set; } 30 | 31 | /// The attribute's constructor. 32 | /// The width. 33 | /// The height. 34 | /// The padding. 35 | /// The red value (between 0 and 1). 36 | /// The green value (between 0 and 1). 37 | /// The blue value (between 0 and 1). 38 | public ColorLineAttribute(float width=DEFAULT_WIDTH, float height=DEFAULT_HEIGHT, float padding=DEFAULT_PADDING, float r=1f, float g=0f, float b=0f) 39 | { 40 | this.width = width > 0 ? width : DEFAULT_WIDTH; 41 | this.height = height > 0 ? height : DEFAULT_HEIGHT; 42 | this.padding = padding > 0 ? padding : DEFAULT_PADDING; 43 | color = (r >= 0 && r <= 1 && g >= 0 && g <= 1 && b >= 0 && b <= 1 ? new Color(r, g, b) : Color.red); 44 | } 45 | } 46 | ``` 47 | 48 | and then define how this property attribute should be drawn: 49 | 50 | ```c# 51 | /// A custom property drawer for the ColorLine attribute. 52 | [CustomPropertyDrawer(typeof(ColorLineAttribute))] 53 | public class ColorLineAttributeDrawer : DecoratorDrawer 54 | { 55 | /// The ColorLine attribute. 56 | private ColorLineAttribute colorLine 57 | { 58 | get { return (ColorLineAttribute)attribute; } 59 | } 60 | 61 | /// The line's vertical padding. 62 | private float padding 63 | { 64 | get { return colorLine.padding > 1 ? colorLine.padding : colorLine.padding*Screen.height; } 65 | } 66 | 67 | /// Callback to specify how tall the GUI for this decorator in pixels is. Default is 1 line high. 68 | /// The height in pixels. 69 | public override float GetHeight() 70 | { 71 | return base.GetHeight() + 2*padding; 72 | } 73 | 74 | /// Callback to make a custom GUI for this decorator. 75 | /// Rectangle on the screen to use for the decorator GUI. 76 | public override void OnGUI(Rect position) 77 | { 78 | //firstly calculate the line's width and height (either fixed or proportional) 79 | float width = colorLine.width > 1 ? colorLine.width : colorLine.width*Screen.width; 80 | float height = colorLine.height > 1 ? colorLine.height : colorLine.height*Screen.height; 81 | 82 | //next calculate the line's start x, y positions 83 | float x = (position.x + (position.width / 2)) - width / 2; 84 | float y = position.y + padding + height / 2; 85 | 86 | //finally draw the line (by using the built in white pixel texture, tinted with GUI.color) 87 | Color currentGUIColor = GUI.color; //make a reference to the current color 88 | GUI.color = colorLine.color; 89 | EditorGUI.DrawPreviewTexture(new Rect(x, y, width, height), Texture2D.whiteTexture); 90 | GUI.color = currentGUIColor; //reset 91 | } 92 | } 93 | ``` 94 | 95 | This property attribute can then be easily used as follows: 96 | 97 | ```c# 98 | public class MyComponent : MonoBehaviour 99 | { 100 | [Header("Important")] 101 | [SerializeField] private int myInt = 100; 102 | 103 | [Space(15)] 104 | [ColorLine(width: 0.9f, height: 3, padding: 5, r: 0.5f, g: 0.5f, b: 0.5f)] 105 | 106 | [Header("Editor Only")] 107 | [SerializeField] private int someotherInt = 10; 108 | } 109 | ``` 110 | 111 | ![](images/decoratorDrawers1.png) 112 | 113 | ## Conclusion 114 | 115 | By using *DecoratorDrawer* attributes, we can customize how decorative elements are drawn in the inspector. These decorative elements are a quick and easy approach to visually organizing the inspector without having to define a *CustomEditor*. 116 | 117 | ## Further Reading 118 | 119 | [50 Unity Tips - #15 Pimp the Inspector](https://github.com/defuncart/50-unity-tips/tree/master/%2315-PimpTheInspector) 120 | 121 | [50 Unity Tips - #25 Property Drawers](https://github.com/defuncart/50-unity-tips/tree/master/%2325-PropertyDrawers) 122 | 123 | [API - DecoratorDrawer](https://docs.unity3d.com/ScriptReference/DecoratorDrawer.html) 124 | 125 | [API - PropertyAttribute](https://docs.unity3d.com/ScriptReference/PropertyAttribute.html) 126 | 127 | [API - PropertyDrawer](https://docs.unity3d.com/ScriptReference/PropertyDrawer.html) 128 | -------------------------------------------------------------------------------- /#26-DecoratorDrawers/images/decoratorDrawers1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#26-DecoratorDrawers/images/decoratorDrawers1.png -------------------------------------------------------------------------------- /#27-CustomFolderInspectors/DAFolderAsset.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Written by James Leahy. (c) 2018 DeFunc Art. 3 | * https://github.com/defuncart/ 4 | */ 5 | #if UNITY_EDITOR 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Text; 9 | using UnityEngine; 10 | 11 | /// Included in the DeFuncArtEditor namespace. 12 | namespace DeFuncArtEditor 13 | { 14 | /// A class of folder options. 15 | public class DAFolderAsset 16 | { 17 | /// A class which contains folder descriptions for given folder paths. 18 | public static class Descriptions 19 | { 20 | /// A placeholder stating that no explicit description was given. 21 | private const string PLACEHOLDER = "No description given."; 22 | 23 | /// A dictionary of folder paths and descriptions. 24 | private static Dictionary dict = new Dictionary 25 | { 26 | { "Assets/Animations", "A folder for animations." }, 27 | { "Assets/Audio", "A folder for audio files and audio databases. Music and SFX subfolders are expected." }, 28 | { "Assets/Editor", "A folder for custom editor scripts and color palettes." }, 29 | { "Assets/Fonts", "A folder for fonts." }, 30 | { "Assets/Prefabs", "A folder for prefabs." }, 31 | { "Assets/Scenes", "A folder for scenes." }, 32 | { "Assets/Scripts", "A folder for scripts." }, 33 | { "Assets/Sprites", "A folder for sprites." } 34 | }; 35 | 36 | /// Adds a param list of folder path, folder descriptions key-value pairs. 37 | /// The key-value pairs. 38 | public static void AddFolderDescriptions(params KeyValuePair[] kvps) 39 | { 40 | foreach(KeyValuePair kvp in kvps) 41 | { 42 | if(!dict.ContainsKey(kvp.Key)) { dict[kvp.Key] = kvp.Value; } 43 | } 44 | } 45 | 46 | /// Returns a description for a given folder path. 47 | /// The folder path. 48 | public static string DescriptionForFolder(string folder) 49 | { 50 | return dict.ContainsKey(folder) ? dict[folder] : PLACEHOLDER; 51 | } 52 | } 53 | 54 | /// A class which contains valid file types for given folder paths. 55 | public static class FileTypes 56 | { 57 | /// Placeholder text stating that all files are valid. 58 | private const string TEXT_ALL_FILE_TYPES = "all"; 59 | /// Text stating the valid file types. 60 | private const string TEXT_VALID_FILE_TYPES = "Valid file types: "; 61 | /// A string builder. 62 | private static StringBuilder sb; 63 | 64 | /// A dictionary of folder paths and valid filetypes. 65 | private static Dictionary> dict = new Dictionary> 66 | { 67 | { "Assets/Audio", new List{ "aif", "aiff", "mp3", "wav" } }, 68 | { "Assets/Editor", new List{ "cs", "colors" } }, 69 | { "Assets/Fonts", new List{ "tff" } }, 70 | { "Assets/Prefabs", new List{ "prefab" } }, 71 | { "Assets/Scenes", new List{ "unity" } }, 72 | { "Assets/Scripts", new List{ "cs" } }, 73 | { "Assets/Sprites", new List{ "png", "jpg", "psd" } } 74 | }; 75 | 76 | /// Adds a param list of folder path, list of valid file types key-value pairs. 77 | /// The key-value pairs. 78 | public static void AddFolderFileTypes(params KeyValuePair>[] kvps) 79 | { 80 | foreach(KeyValuePair> kvp in kvps) 81 | { 82 | if(!dict.ContainsKey(kvp.Key)) { dict[kvp.Key] = kvp.Value; } 83 | } 84 | } 85 | 86 | /// Returns a list of valid file types for a given folder path. 87 | /// The folder path. 88 | /// If the path has no explicit valid file types, whether the parent's should be considered. 89 | public static List FileTypesForFolder(string folder, bool checkParentFolders = true) 90 | { 91 | string key = folder; 92 | while(!dict.ContainsKey(key) && checkParentFolders) 93 | { 94 | DirectoryInfo info = Directory.GetParent(key); 95 | if(info == null) { break; } 96 | 97 | key = info.ToString(); 98 | if(key == "Assets") { break; } //this isn't necessary as Directory.GetParent("Assets") should return null 99 | } 100 | 101 | if (dict.ContainsKey(key)) { return dict[key]; } 102 | return null; 103 | } 104 | 105 | /// Returns a string representation of the valid folder types for a given folder path. 106 | /// The folder path. 107 | /// If the path has no explicit valid file types, whether the parent's should be considered. 108 | public static string FileTypesForFolderAsString(string folder, bool checkParentFolders = true) 109 | { 110 | //initialize the string builder 111 | if(sb == null) { sb = new StringBuilder(); } else { sb.Length = 0; sb.Append(TEXT_VALID_FILE_TYPES); } 112 | 113 | List fileTypes = FileTypesForFolder(folder, checkParentFolders); 114 | if(fileTypes == null) 115 | { 116 | sb.Append(TEXT_ALL_FILE_TYPES); 117 | } 118 | else //write the list to string with commas between elements 119 | { 120 | for(int i=0; i < fileTypes.Count; i++) 121 | { 122 | if(i != 0) { sb.Append(", "); } 123 | sb.Append(fileTypes[i]); 124 | } 125 | } 126 | //return the string builder contents 127 | return sb.ToString(); 128 | } 129 | } 130 | 131 | /// A class which contains file naming conventions for given folder paths. 132 | public static class FileNamingConventions 133 | { 134 | /// A placeholder stating that there are no explicit file naming conventions. 135 | private const string PLACEHOLDER = "None."; 136 | /// The file naming conventions text. 137 | private const string TEXT_FILE_NAMING_CONVENTIONS = "File naming conventions: "; 138 | 139 | /// A dictionary of folder paths and naming conventions. 140 | private static Dictionary dict = new Dictionary 141 | { 142 | }; 143 | 144 | /// Adds a param list of folder path, folder file naming conventions key-value pairs. 145 | /// The key-value pairs. 146 | public static void AddFolderFileNamingConventions(params KeyValuePair[] kvps) 147 | { 148 | foreach(KeyValuePair kvp in kvps) 149 | { 150 | if(!dict.ContainsKey(kvp.Key)) { dict[kvp.Key] = kvp.Value; } 151 | } 152 | } 153 | 154 | /// Returns a file naming convention for a given folder path. 155 | /// The folder path. 156 | public static string FileNamingConventionsForFolder(string folder) 157 | { 158 | return string.Format("{0}{1}", TEXT_FILE_NAMING_CONVENTIONS, dict.ContainsKey(folder) ? dict[folder] : PLACEHOLDER); 159 | } 160 | } 161 | } 162 | } 163 | #endif 164 | -------------------------------------------------------------------------------- /#27-CustomFolderInspectors/DAFolderAssetPostprocessor.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Written by James Leahy. (c) 2018 DeFunc Art. 3 | * https://github.com/defuncart/ 4 | */ 5 | #if UNITY_EDITOR 6 | using DeFuncArtEditor; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using UnityEditor; 10 | using UnityEngine; 11 | using UnityEngine.Assertions; 12 | 13 | /// A custom asset postprocessor which verifies that assets are imported into/moved to valid folders. 14 | public class DAFolderAssetPostprocessor : AssetPostprocessor 15 | { 16 | /// Callback after the importing of assets has completed. This call can occur after a manual reimport, or any time an asset/folder of assets 17 | /// are moved to a new location in the Project View. All string arrays are filepaths relative to the Project's root Assets folder. 18 | /// The imported assets. 19 | /// The deleted assets. 20 | /// The moved assets. 21 | /// The moved-from assets. 22 | static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) 23 | { 24 | Assert.AreEqual(movedAssets.Length, movedFromAssetPaths.Length); 25 | 26 | //check that all imported assets were imported into a valid folder 27 | foreach(string assetPath in importedAssets) 28 | { 29 | if(File.Exists(assetPath)) //file as opposed to a directory 30 | { 31 | string folder = Path.GetDirectoryName(assetPath); 32 | List fileTypes = DAFolderAsset.FileTypes.FileTypesForFolder(folder); 33 | if(fileTypes != null) 34 | { 35 | string extension = Path.GetExtension(assetPath).Replace(".", ""); 36 | if(!fileTypes.Contains(extension)) 37 | { 38 | Debug.LogErrorFormat("Asset {0} could not be imported into folder {1} because {2} is an invalid filetype.", Path.GetFileName(assetPath), folder, extension); 39 | AssetDatabase.DeleteAsset(assetPath); 40 | } 41 | } 42 | } 43 | } 44 | 45 | //check that all moved assets were moved into a valid folder 46 | for(int i=0; i < movedAssets.Length; i++) 47 | { 48 | string assetPath = movedAssets[i]; 49 | if(File.Exists(assetPath)) //file as opposed to a directory 50 | { 51 | string folder = Path.GetDirectoryName(assetPath); 52 | List fileTypes = DAFolderAsset.FileTypes.FileTypesForFolder(folder); 53 | if(fileTypes != null) 54 | { 55 | string extension = Path.GetExtension(assetPath).Replace(".", ""); 56 | if(!fileTypes.Contains(extension)) 57 | { 58 | Debug.LogErrorFormat("Asset {0} could not be moved to folder {1} because {2} is an invalid filetype.", Path.GetFileName(assetPath), folder, extension); 59 | AssetDatabase.MoveAsset(movedAssets[i], movedFromAssetPaths[i]); 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | #endif 67 | -------------------------------------------------------------------------------- /#27-CustomFolderInspectors/DefaultAssetEditor.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Written by James Leahy. (c) 2018 DeFunc Art. 3 | * https://github.com/defuncart/ 4 | */ 5 | #if UNITY_EDITOR 6 | using System.IO; 7 | using UnityEditor; 8 | using UnityEngine; 9 | 10 | /// Included in the DeFuncArtEditor namespace. 11 | namespace DeFuncArtEditor 12 | { 13 | /// Custom Editor for the DefaultAsset. 14 | [CustomEditor(typeof(DefaultAsset), true)] 15 | public class DefaultAssetEditor : Editor 16 | { 17 | /// A large font size. 18 | private const int FONT_SIZE_LARGE = 42; 19 | /// A medium font size. 20 | private const int FONT_SIZE_MEDIUM = 28; 21 | /// A small font size. 22 | private const int FONT_SIZE_SMALL = 18; 23 | 24 | /// Callback to draw the inspector. 25 | public override void OnInspectorGUI() 26 | { 27 | //draw the default inspector 28 | DrawDefaultInspector(); 29 | 30 | //get the asset's path 31 | string assetPath = AssetDatabase.GetAssetPath(target); 32 | 33 | //if the asset is a folder 34 | if(Directory.Exists(assetPath)) 35 | { 36 | //draw the folder description 37 | string description = DAFolderAsset.Descriptions.DescriptionForFolder(assetPath); 38 | DrawLabel(text: description, fontSize: FONT_SIZE_MEDIUM, height: Screen.height * 0.3f); 39 | 40 | //draw the folder's valid filetypes 41 | string validFileTypes = DAFolderAsset.FileTypes.FileTypesForFolderAsString(assetPath); 42 | DrawLabel(text: validFileTypes, fontSize: FONT_SIZE_MEDIUM, height: Screen.height * 0.2f); 43 | 44 | //draw the folder's naming conventions 45 | string namingConventions = DAFolderAsset.FileNamingConventions.FileNamingConventionsForFolder(assetPath); 46 | DrawLabel(text: namingConventions, fontSize: FONT_SIZE_MEDIUM); 47 | } 48 | 49 | } 50 | 51 | /// Draws a label with a given text, font size and height. 52 | /// The text. 53 | /// The font size. 54 | /// The height (defaults to required height). 55 | private void DrawLabel(string text, int fontSize, float height=-1) 56 | { 57 | if(height > 0) { EditorGUILayout.LabelField(text, new GUIStyle{ fontSize = fontSize, richText = true, wordWrap = true }, GUILayout.Height(height)); } 58 | else { EditorGUILayout.LabelField(text, new GUIStyle { fontSize = fontSize, richText = true, wordWrap = true }); } 59 | } 60 | } 61 | } 62 | #endif 63 | -------------------------------------------------------------------------------- /#27-CustomFolderInspectors/MyFolderAsset.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Written by James Leahy. (c) 2018 DeFunc Art. 3 | * https://github.com/defuncart/ 4 | */ 5 | #if UNITY_EDITOR 6 | using DeFuncArtEditor; 7 | using System.Collections.Generic; 8 | using UnityEngine; 9 | using UnityEditor; 10 | 11 | [InitializeOnLoad] 12 | public class MyFolderDescriptions 13 | { 14 | static MyFolderDescriptions() 15 | { 16 | DAFolderAsset.Descriptions.AddFolderDescriptions( 17 | new KeyValuePair("Assets/Localization", "A folder of localization files.") 18 | ); 19 | } 20 | } 21 | 22 | [InitializeOnLoad] 23 | public class MyFolderFileTypes 24 | { 25 | static MyFolderFileTypes() 26 | { 27 | DAFolderAsset.FileTypes.AddFolderFileTypes( 28 | new KeyValuePair>("Assets/Localization", new List { "json" }) 29 | ); 30 | } 31 | } 32 | 33 | [InitializeOnLoad] 34 | public class MyFolderFileNamingConventions 35 | { 36 | static MyFolderFileNamingConventions() 37 | { 38 | DAFolderAsset.FileNamingConventions.AddFolderFileNamingConventions( 39 | new KeyValuePair("Assets/Localization", "All files are expected to be named English, German etc., that is, the name of the language in English with the first letter capitalized.") 40 | ); 41 | } 42 | } 43 | #endif 44 | -------------------------------------------------------------------------------- /#27-CustomFolderInspectors/README.md: -------------------------------------------------------------------------------- 1 | # 27 - Custom Folder Inspectors 2 | 3 | By default, folders are rendered blank (left image) in the inspector. Wouldn't it be much better if we could customize these inspectors somehow, for instance, adding a short description, valid file types or file naming conventions (right image)? Not only could this be beneficial for future you, but also for your current teammates! 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | Just like any other asset, folders are imported into Unity, however they don't have any specific type and are assigned as *DefaultAssets*. By defining a custom editor, we can customize how this asset will appear in the inspector. 13 | 14 | ```c# 15 | /// Custom Editor for the DefaultAsset. 16 | [CustomEditor(typeof(DefaultAsset), true)] 17 | public class DefaultAssetEditor : Editor 18 | { 19 | /// Callback to draw the inspector. 20 | public override void OnInspectorGUI() 21 | { 22 | //draw the default inspector 23 | DrawDefaultInspector(); 24 | 25 | //get the asset's path 26 | string assetPath = AssetDatabase.GetAssetPath(target); 27 | 28 | //if the asset is a folder 29 | if(Directory.Exists(assetPath)) 30 | { 31 | //add custom code 32 | } 33 | } 34 | } 35 | ``` 36 | 37 | ## Folder Descriptions 38 | 39 | One approach to adding folder descriptions, is to create a static class with a simple dictionary. 40 | 41 | ```c# 42 | /// A class of folder options. 43 | public class DAFolderAsset 44 | { 45 | /// A class which contains folder descriptions for given folder paths. 46 | public static class Descriptions 47 | { 48 | /// A placeholder stating that no explicit description was given. 49 | public const string PLACEHOLDER = "No description given."; 50 | 51 | /// A dictionary of folder paths and descriptions. 52 | private static Dictionary dict = new Dictionary 53 | { 54 | { "Assets/Audio", "A folder for audio files and audio databases. Music and SFX subfolders are expected." }, 55 | { "Assets/Editor", "A folder for custom editor scripts and color palettes." }, 56 | { "Assets/Fonts", "A folder for fonts." }, 57 | { "Assets/Prefabs", "A folder for prefabs." }, 58 | { "Assets/Scenes", "A folder for scenes." }, 59 | { "Assets/Scripts", "A folder for scripts." }, 60 | { "Assets/Sprites", "A folder for sprites." } 61 | }; 62 | 63 | /// Returns a description for a given folder path. 64 | /// The folder path. 65 | public static string DescriptionForFolder(string folder) 66 | { 67 | return dict.ContainsKey(folder) ? dict[folder] : PLACEHOLDER; 68 | } 69 | } 70 | ``` 71 | 72 | Some folders are common between multiple projects, while others may be unique to the current project. As a static class cannot be abstract or inherited from, one simple approach to add additional descriptions is to include a method in **DAFolderAsset.Descriptions** 73 | 74 | ```c# 75 | /// Adds a param list of folder path, folder descriptions key-value pairs. 76 | /// The key-value pairs. 77 | public static void AddFolderDescriptions(params KeyValuePair[] kvps) 78 | { 79 | foreach(KeyValuePair kvp in kvps) 80 | { 81 | if(!dict.ContainsKey(kvp.Key)) { dict[kvp.Key] = kvp.Value; } 82 | } 83 | } 84 | ``` 85 | 86 | and create a project-only script with a custom static method to add these additional descriptions: 87 | 88 | ```c# 89 | [InitializeOnLoad] 90 | public class MyFolderDescriptions 91 | { 92 | static MyFolderDescriptions() 93 | { 94 | DAFolderAsset.Descriptions.AddFolderDescriptions( 95 | new KeyValuePair("Assets/Localization", "A folder of localization files.") 96 | ); 97 | } 98 | } 99 | ``` 100 | 101 | thus rendering a custom inspector for the folder only for the current project: 102 | 103 | ![](images/customFolderInspectors3.png) 104 | 105 | The same approach can be utilized for valid file types and file naming conventions: 106 | 107 | ```c# 108 | /// A class which contains valid file types for given folder paths. 109 | public static class FileTypes 110 | { 111 | /// A dictionary of folder paths and valid filetypes. 112 | private static Dictionary> dict = new Dictionary> 113 | { 114 | { "Assets/Audio", new List{ "aif", "aiff", "mp3", "wav" } }, 115 | { "Assets/Editor", new List{ "cs", "colors" } }, 116 | { "Assets/Fonts", new List{ "tff" } }, 117 | { "Assets/Prefabs", new List{ "prefab" } }, 118 | { "Assets/Scenes", new List{ "unity" } }, 119 | { "Assets/Scripts", new List{ "cs" } }, 120 | { "Assets/Sprites", new List{ "png", "jpg", "psd" } } 121 | }; 122 | } 123 | ``` 124 | 125 | ## Enforce Valid File Types 126 | 127 | If we wish to strictly enforce valid file types for certain folders, we can extend *AssetPostprocessor* to tap into the asset pipeline and verify that all imported and moved assets have valid types for their new folders. By comparing the asset's file type to what is valid for the directory, we can verify that the asset's type is correct. If not, an error message can be printed to the console, and the asset deleted, effectively reusing to import that asset into that folder. For moved assets, the asset can be moved back into it's original folder. 128 | 129 | ```c# 130 | /// A custom asset postprocessor which verifies that assets are imported into/moved to valid folders. 131 | public class DAFolderAssetPostprocessor : AssetPostprocessor 132 | { 133 | /// Callback after the importing of assets has completed. This call can occur after a manual reimport, or any time an asset/folder of assets 134 | /// are moved to a new location in the Project View. All string arrays are filepaths relative to the Project's root Assets folder. 135 | /// The imported assets. 136 | /// The deleted assets. 137 | /// The moved assets. 138 | /// The moved-from assets. 139 | static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) 140 | { 141 | Assert.AreEqual(movedAssets.Length, movedFromAssetPaths.Length); 142 | 143 | //check that all imported assets were imported into a valid folder 144 | foreach(string assetPath in importedAssets) 145 | { 146 | if(File.Exists(assetPath)) //file as opposed to a directory 147 | { 148 | string folder = Path.GetDirectoryName(assetPath); 149 | List fileTypes = DAFolderAsset.FileTypes.FileTypesForFolder(folder); 150 | if(fileTypes != null) 151 | { 152 | string extension = Path.GetExtension(assetPath).Replace(".", ""); 153 | if(!fileTypes.Contains(extension)) 154 | { 155 | Debug.LogErrorFormat("Asset {0} could not be imported into folder {1} because {2} is an invalid filetype.", Path.GetFileName(assetPath), folder, extension); 156 | AssetDatabase.DeleteAsset(assetPath); 157 | } 158 | } 159 | } 160 | } 161 | } 162 | } 163 | ``` 164 | 165 | Please see **DAFolderAssetPostprocessor.cs** for the full implementation. 166 | 167 | ## Conclusion 168 | 169 | By drawing a custom inspector for folders, we can visually display useful information that may otherwise be regelated to a documentation pdf. Moreover, by enforcing strict valid file types for relevant folders, we can assume that folders will always contain the correct value types. 170 | 171 | ## Further Reading 172 | 173 | [50 Unity Tips - #03 Rich Text](https://github.com/defuncart/50-unity-tips/tree/master/%2303-RichText) 174 | 175 | [50 Unity Tips - #17 Custom Editor](https://github.com/defuncart/50-unity-tips/tree/master/%2317-CustomEditor) 176 | 177 | [API - DefaultAsset](https://docs.unity3d.com/ScriptReference/DefaultAsset.html) 178 | -------------------------------------------------------------------------------- /#27-CustomFolderInspectors/images/customFolderInspectors1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#27-CustomFolderInspectors/images/customFolderInspectors1.png -------------------------------------------------------------------------------- /#27-CustomFolderInspectors/images/customFolderInspectors2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#27-CustomFolderInspectors/images/customFolderInspectors2.png -------------------------------------------------------------------------------- /#27-CustomFolderInspectors/images/customFolderInspectors3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defuncart/50-unity-tips/1357ab4fea4558f0be1c5a005e2c6bb2e7e60134/#27-CustomFolderInspectors/images/customFolderInspectors3.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS generated files 2 | .DS_Store 3 | .DS_Store? 4 | ._* 5 | .Spotlight-V100 6 | .Trashes 7 | ehthumbs.db 8 | Thumbs.db -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 James Leahy 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 50 Unity Tips 2 | 3 | A collection of 50 tips for Unity (focusing on Mobile) ranging from editor tools to serialization to UI shortcuts. 4 | 5 | [@defuncart](https://twitter.com/defuncart) 6 | --------------------------------------------------------------------------------