├── #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 | 
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 | 
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 | 
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 | 
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 | 
18 |
19 | Even *Debug.Log* supports these markup tags which can be useful when reporting warnings and errors.
20 |
21 | 
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 | 
--------------------------------------------------------------------------------
/#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 | 
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 | 
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 | public class PlayerData
25 | {
26 | [XmlElement("n")]
27 | public string name;
28 | }
|
29 | <PlayerData>
30 | <n>Gordon Freeman</n>
31 | </PlayerData> |
32 |
33 |
34 |
35 | **[XmlAttribute]** indicates that the field will be represented as an XML attribute
36 |
37 |
38 |
39 | public class PlayerData
40 | {
41 | [XmlAttribute("name")]
42 | public string name;
43 | }
|
44 | <PlayerData name="Gordon Freeman">
45 | </PlayerData> |
46 |
47 |
48 |
49 | **[XmlIgnore]** is used to skip the serialization of a public field.
50 |
51 |
52 |
53 | public class PlayerData
54 | {
55 | public string name;
56 | [XmlIgnore] public int somePublicUnserializedInt;
57 | }
|
58 | <PlayerData>
59 | <name>Gordon Freeman</name>
60 | </PlayerData> |
61 |
62 |
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 | [XmlRoot("Player")]
69 | public class PlayerData
70 | {
71 | public string name;
72 | }
|
73 | <Player>
74 | <name>Gordon Freeman</name>
75 | </Player> |
76 |
77 |
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 | public class PlayerData
84 | {
85 | public string name;
86 | [XmlArray("scores"), XmlArrayItem("score")]
87 | public int[] levelScores;
88 | }
|
89 | <PlayerData>
90 | <name>Gordon Freeman</name>
91 | <scores>
92 | <score>50</score>
93 | <score>76</score>
94 | <score>19</score>
95 | </scores>
96 | </PlayerData> |
97 |
98 |
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
20 | 
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 | 
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 | 
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 | 
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 | 
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 | 
36 |
37 | ## Hotkey shortcuts
38 |
39 | New menu items can be assigned hotkeys combinations that will automatically launch them.
40 |
41 |
42 |
43 | Hotkey |
44 | Description |
45 |
46 |
47 | % |
48 | CTRL on windows, CMD on Mac |
49 |
50 |
51 | # |
52 | Shift |
53 |
54 |
55 | & |
56 | Alt |
57 |
58 |
59 | #LEFT, #RIGHT, #UP, #DOWN #HOME, #END, #PGUP, #PGDN |
60 | Navigation keys |
61 |
62 |
63 | #F1 ... #F12 |
64 | Function keys |
65 |
66 |
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------