Four More Techniques for Faster Unity 3D Development
By: Yvens Serpa
Tags: Gaming, Game Development, Unity, Game Design, Programming

Considering that a game might have many different merchants, a more traditional approach is to have each merchant a Prefab (or Prefab Variant). However, any variation is bound to create redundancy (same information stored more than once) and inconsistency (same information has different values across multiple appearances). Storing this data in a Scriptable Object detaches them from the Game Objects for easier reuse and consistency.

Instead of copying the data for all Merchants, each Merchant can have a Script that holds a reference to their MerchantInfo. Like the code below:

public class MerchantScript : MonoBehaviour

{

public MerchantInformation merchantInformation;



public void BuyItem(Item item)

{

//Some Code

}



public void SellItem(Item item)

{

//Some Code

}

}

Moreover, if you need to test a different set of Items or information of a certain Merchant, you can drag and drop a new MerchantInfo to the object at any time. This is faster than changing the properties manually and safer because the previous values are intact in the Scriptable Object.

Merchant Script with reference to a Scriptable Object. Source: Yvens Serpa.

4. Use LINQ (preferably with Lists)

This is surely a controversial point, but before any discussion, let me remind you that this article's point is not performance. It is about fast prototyping.

Performance is surely a crucial concern that must be addressed properly once it is necessary. Please do not spend time optimizing features that are not impacting your prototype before actually analyzing it both with the Unity Profiler and deploying your application to the target hardware. More importantly, do not focus your prototyping efforts on optimizing unless you are prototyping optimizations.

Back into the topic: LINQ is an acronym for Language-Integrated Query, a C# feature that allows for query-like statements. LINQ enables writing complex processes with only one or two lines of code. You can use LINQ with both Lists and Arrays, but Lists already have built-in LINQ functions that are often used and require less code to achieve than their Array.

Lists and Arrays behave the same for most of Unity’s mundane actions. They are displayed the same in the Inspector, and both allow resizing. If your code uses an Array, changing it to be a List should not have any immediate impact.

If you are not sure about the syntax difference between the two, an Array of Game Objects is written as such:

public GameObject[] gameObjectsArray;

While a List of Game Objects is written as:

public List<GameObject> gameObjectsList;

Suppose we want to iterate over a list of Game Objects and search which one (if any) has the Tag "Player." The solution for that using an Array could be:

public GameObject FindPlayer() {

foreach(GameObject go in gameObjectsArray) {

if(go.CompareTag("Player)) {

return go;

}

}

return null;

}

The same algorithm can be written using LINQ with a List as:

public GameObject FindPlayer() {

return gameObjectsList.Find(

(GameObject go) => {

return go.CompareTag("Player");

}

);

}

Find is one of the LINQ functions from Lists that is not available in Arrays. Still, the same behavior can be achieved using the Where function:

public GameObject FindPlayer() {

return gameObjectsArray.Where(

(GameObject go) => {

return go.CompareTag("Player");

}

).GetEnumerator().Current;

}

Particularly, Find requires less code and it is easier to read. Besides, it returns either one Game Object or null. In the case of using the Where function, the code still requires a function and a Property call to get either one Game Object or a null value. For that, I recommend the idea of simply using Lists instead.

The syntax for both cases can be off at the start, especially for how the query-statements are written (the predicate). An easier approach to understanding it is to read this code as expecting a boolean returning function. In the example, the function returns if the go variable has the Tag "Player."

Find and Where are not the only LINQ function we can use. In fact, there are many others with different caveats and functionalities. Below I list some of these functions I tend to use the most when prototyping in Unity:

FindAll

Different from Find, FindAll returns another List with all elements that return true to the statement. The code exempt bellow returns a List of all Game Objects tagged as "Enemy":

public List<GameObject> FindPlayer() {

return gameObjectsList.FindAll(

(GameObject go) => {

return go.CompareTag("Enemy");

}

);

}

Particularly, the main advantage of using FindAll is that it never returns a null value. In the worst case, if no element is found, an empty list is returned. Thus, you can avoid null checking in the code (smaller codebase!).

Any

Instead of returning the object, sometimes we only need to know if any object in a List attends to a certain condition. For example, to open a door in a game, the player must hold an item of type "Key." For that, Any is the way to go:

public bool HasKeyInTheInventory() {

return inventory.Any(

(Item item) =>

{

return item.Type.Equals("Key");

}

);

}

Notice that Any returns a boolean value, not the found object. If the intent is not only to assess if the inventory has an item of type "Key," but also to use the object for a certain action, it is better to use Find instead.

Sum

Often we need other sorts of information from our data, especially to display values or to calculate metrics. In this case, there are LINQ functions that allow for mathematical operations. Sum is one of them, and it returns the summation of all elements in a list given a query-statement.

The example below shows Sum being used to calculate the cost of all items in the inventory (a list of objects). This might be useful, for instance, to determine how much an inventory is worth.

public float CostOfAllItems()

{

return inventory.Sum(

(Item item) =>

{

return item.Cost;

}

);

}

Combining Queries

Finally, as some queries return other lists, it is possible to concatenate them. The example below shows an example of concatenating queries to calculate the cost of all items in the inventory of type "Potion":

public float CostOfAllPotions()

{

return inventory.FindAll(

(Item item) =>

{

return item.Type.Equals("Potion");

})

.Sum(

(Item item) =>

{

return item.Cost;

});

}

Bear in mind that there are other ways of achieving the same results. In fact, LINQ and Lists are comparatively less performant than using Arrays. But these discussions should only be addressed once the prototype is up and running. Unless your prototype's performance is way below interactive levels, you should continue prototyping.

On another view, concatenating queries might seem unnecessary since you can make the query functions more complex to achieve comparable results. However, concatenating makes the code easier to read and follow. Since most of the LINQ code is subject to be replaced in later stages, it is more productive to keep a readable and easy code first than the struggle for a more complex single-query alternative.

Conclusion

In short, the takeaway from these techniques are:

Using Icons to improve the visibility of objects in different contexts. Use the Reset method to revert to the desired state quickly. Use Scriptable Objects instead of Properties for consistency and backup. Use LINQ operations, preferably with Lists.

Even though some of these techniques are not very versatile, such as the Reset method, they should allow for quickly setting up the initial experiments before committing to a more complex approach. Specifically for the Reset method, a good alternative is to use a Custom Editor Script. Still, unless you need particular Reset behaviors and constantly reuse them, the effort and amount of added code might not initially compensate for the effort.

As for Scriptable Objects, they might actually require a bit more code than their counterpart (using member variables and properties), but the added effort is quite minimal. It is likely that your IDE already provides a shortcut to create Scriptable Object templates. Besides, they are easier to handle in source control for being separate objects and less likely to cause conflicts than a Prefab, or worse, the Game Scene.

Moreover, before insisting on the performance argument against LINQ operations, I invite you to try coding with them and assessing your code's performance. From my experience, it is more inclined to improve your productivity because the code is simpler to write than it is to cause major performance issues. Besides, many of the situations you might need a query are occasional and certainly outside your main game loop.

Landscape, by Albert Edelfelt (1896). Source: USEUM.

Thanks for reading :)