Unity is a great tool for building games, it offers a ton of features and makes it almost painless to export your game to multiple platforms. However it it still has some shortcomings, and you can’t expect it to do everything you need right out of the box. Today we’ll discuss C# extensions and how they can be used in Unity to cut your design time and make your code cleaner. We’ll discuss some of the most useful ones we’ve used during the development of Rogue Star Rescue.
What is an extension?
Simply put, an extensions adds a public function to a class you didn’t write. In Unity this is especially useful because we don’t want to override the GameObject or Transform class every time we need some extra functionality. They are defined as static functions and use the ‘this’ keyword to reference the instanced object the function is applied to. The declaration looks like this:
public static ReturnClass extensionFunctionName(this ExtendedClass object) {}
Vector Extensions
In 2d games Vector2s are used almost everywhere. The problem is that they don’t always ‘fit’ with a lot of the 3d functions of GameObject, causing a lot of compiler errors/warnings from casts. We can use some extensions to make life easier.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | public static Vector3Int toVector3Int(this Vector2Int v) { return new Vector3Int(v.x, v.y, 0); } public static Vector2Int toVector2Int(this Vector3Int v) { return new Vector2Int(v.x, v.y); } public static Vector2 tileCenter(this Vector2Int v) { return new Vector2(v.x + 0.5f, v.y + 0.5f); } public static Vector2 tileCenter(this Vector3Int v) { return new Vector2(v.x + 0.5f, v.y + 0.5f); } public static Vector2Int toVector2IntRound(this Vector3 v) { return new Vector2Int(Mathf.RoundToInt(v.x), Mathf.RoundToInt(v.y)); } public static Vector3Int toVector3IntRound(this Vector3 v) { return new Vector3Int(Mathf.RoundToInt(v.x), Mathf.RoundToInt(v.y), Mathf.RoundToInt(v.z)); } public static Vector3Int toVector3IntFloor(this Vector3 v) { return new Vector3Int(Mathf.FloorToInt(v.x), Mathf.FloorToInt(v.y), Mathf.FloorToInt(v.z)); } public static Vector2 toVector2(this Vector2Int v) { return v; } public static Vector2 toVector2(this Vector3 v) { return v; } public static Vector2 Rotate(this Vector2 v, float degrees) { float radians = degrees * Mathf.Deg2Rad; float sin = Mathf.Sin(radians); float cos = Mathf.Cos(radians); float tx = v.x; float ty = v.y; return new Vector2(cos * tx - sin * ty, sin * tx + cos * ty); } public static Vector3 toVector3(this Vector2Int v) { return new Vector3(v.x, v.y); } public static Vector2Int toVector2IntFloor(this Vector2 v) { return new Vector2Int(Mathf.FloorToInt(v.x), Mathf.FloorToInt(v.y)); } |
Furthermore if you’re using Tilemaps you’ll need a lot of Vector3Ints. These neighbour functions are very helpful for procedural tile generation logic.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | public static List getNeighbors(this Vector3Int center, int degree =1) { List neighbors = new List(); for (int x = center.x - degree; x <= center.x + degree; x++) for (int y = center.y - degree; y <= center.y + degree; y++) { if (x == center.x && y == center.y) continue; //skip center point neighbors.Add(new Vector3Int(x, y, 0)); } return neighbors; } public static List get4PointNeighbors(this Vector3Int center, int degree = 1) { List neighbors = new List(); for (int x = center.x - degree; x <= center.x + degree; x++) for (int y = center.y - degree; y <= center.y + degree; y++) { if (x == center.x && y == center.y) continue; //skip center point if (x == center.x || y == center.y) //make sure it shares one axis, this ensures 4 point values only { neighbors.Add(new Vector3Int(x, y, 0)); } } return neighbors; } public static Vector3Int getNeighbor(this Vector3Int center, CCDirection direction) { return center + direction.toVector2Int().toVector3Int(); } |
List and Enumerable Extensions
Any game is going to have a lot of List and Enumerable objects. Below are some useful extensions.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | public static bool containsAll(this List a, List b) { foreach (var bItem in b) { if (!a.Contains(bItem)) return false; } return true; } public static void Shuffle(this IList ts) { var count = ts.Count; var last = count - 1; for (var i = 0; i < last; ++i) { var r = UnityEngine.Random.Range(i, count); var tmp = ts[i]; ts[i] = ts[r]; ts[r] = tmp; } } public static List cutIndexes(this IList list) { var indexes = new List(); int cutPoint = Random.Range(0, list.Count); for(int i = cutPoint; i<list.Count; i++) //up { indexes.Add(i); } for(int i =cutPoint; i>=0; i--) //down { if (i == cutPoint) continue; indexes.Add(i); } return indexes; } public static void removeNull(this IList list) where T:Object { for (var i = list.Count - 1; i > -1; i--) { if (list[i] == null) list.RemoveAt(i); } } public static T getRandom(this IList ts, IEnumerable exclusionList = null) { if (ts.Count == 0) return default(T); if (exclusionList != null) { //remove exclusion list first var tempList = new List(); tempList.AddRange(ts); foreach (var exclude in exclusionList) tempList.Remove(exclude); if (tempList.Count == 0) return default(T); return tempList[Random.Range(0, tempList.Count)];//exclusive end } return ts[Random.Range(0, ts.Count)];//exclusive end } public static T circularToggleStruct(this IList ts, T currentSelection, bool toggleUp = true) where T : struct { if (ts.Count == 0) return default(T); int index = ts.IndexOf(currentSelection); if (toggleUp) index++; else index--; //circular indexing if (index < 0) index = ts.Count - 1; if (index == ts.Count) index = 0; return ts[index]; } //string is IEnumerable, so doesn't work with object or struct public static string circularToggleString(this IList ts, string currentSelection, bool toggleUp = true) { if (ts.Count == 0) return null; //null case if (currentSelection == null) return ts[0]; int index = ts.IndexOf(currentSelection); if (toggleUp) index++; else index--; //circular indexing if (index < 0) index = ts.Count - 1; if (index == ts.Count) index = 0; return ts[index]; } public static T circularToggle(this IList ts, T currentSelection, bool toggleUp = true) where T : Object { if (ts.Count == 0) return null; //null case if (currentSelection == null) return ts[0]; int index = ts.IndexOf(currentSelection); if (toggleUp) index++; else index--; //circular indexing if (index < 0) index = ts.Count - 1; if (index == ts.Count) index = 0; return ts[index]; } |
GameObject and Transform Extensions
GameObjects and Transforms are at the core of any Unity game. Unfortunately they are still missing some conveniences that makes them hard to work with. The following extensions can make working with them much smoother.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | public static bool isPlayer(this GameObject go) { return go.CompareTag(Player.tagIdentifier); } public static bool HasComponent(this GameObject go) where T : Component { return go.GetComponent() != null; } //maintains the current z public static void setXYPosition(this Transform transform, Vector2 pos) { transform.position = new Vector3(pos.x, pos.y, transform.position.z); } public static void setZPosition(this Transform transform, float zPos) { transform.position = new Vector3(transform.position.x, transform.position.y, zPos); } //maintains the current z public static void setXYLocalPosition(this Transform transform, Vector2 pos) { transform.localPosition = new Vector3(pos.x, pos.y, transform.localPosition.z); } public static void setYLocalPosition(this Transform transform, float yPos) { transform.localPosition = new Vector3(transform.localPosition.x, yPos, transform.localPosition.z); } public static void setZLocalPosition(this Transform transform, float zPos) { transform.localPosition = new Vector3(transform.localPosition.x, transform.localPosition.y, zPos); } |
The great thing about these extensions is that they can be reused in your future projects! I like to put them all in one file but you can just as well use multiple files, depending on how complicated your game is. Rogue Star Rescue’s code base would be much larger without them, so use them wisely to keep your project more maintainable.