Diego Giacomelli Programmer since 2001. Aspiring game developer since childhood. I write about C#, Unity 3D, game development and web development, and more. Currently interested in anything that I can code with C#.

6 modern C# features for cleaner Unity code

5 min read 1508

Unity Logo

Since C# 7.0, many code improvements that help us write less code have been added to the language. This tutorial will focus on six new features that can help us write more concise and readable code and how we can use these features on our C# for Unity.

These are the tutorial sections:

Prerequisites

The following prerequisites are required to follow along with this tutorial:

  • Basic knowledge of Unity
  • Previous experience writing C# scripts in Unity

Setting up our Unity project

First, we need to create our Unity project. For this tutorial, we’ll use the version 2021.3.4f1, which, at the moment I’m writing, is the newest LTS Unity version.

On the project templates list, choose 2D(core) (the simplest of all), give it a name, and click the Create project button.

Create Project Screenshot

With the project started, create a folder called Scripts inside the Assets folder. We’ll use them to keep our project organized during the tutorial.

Assets Project Folder

Tutorial structure

For each sample of how to use the new C# feature, we will first look at how it was done before and then how we could write less and more readable code with the new feature.

The classes below are just stubs that are used on all samples throughout the tutorial. You can add them to a script inside the Scripts folder:



// GAME MODE.
public enum GameMode
{
    TimeAttack,
    Survive,
    Points
}

// ENEMIES.
public abstract class Enemy
{
    public bool IsVisible { get; set; }
    public bool HasArmor { get; set; }
}
 
public class Minion : Enemy { }
public class Troll : Enemy { }
public class Vampire : Enemy { }

public class Zombie : Enemy { }


// WEAPONS.
public abstract class Weapon { }
public class Stake : Weapon { }
public class Shotgun : Weapon { }

C# features support in Unity

In C# versions 8 and 9, a lot of new features were added to the language. You can read the full features list for each version in the links below:

C# 8 and 9 features in Unity: What’s missing?

Unity support for C# 8 has started on version 2020.2 and C# 9 has started on version 2021.2.

Be aware that not every C# 8 and 9 feature is supported by Unity, like:

  • default interface methods
  • indices and ranges
  • asynchronous streams
  • asynchronous disposable
  • suppress emitting locals init flag
  • covariant return types
  • module initializers
  • extensible calling conventions for unmanaged function pointers
  • init only setters

Most of these unsupported features are used in very specific scenarios, like extensible calling conventions for unmanaged function pointers, and some aren’t, like indices and ranges.

Because of this, features like indices and ranges and init only setters will likely be supported in future versions of Unity. However, the chance of an unsupported feature for a very specific scenario gaining Unity support in the future is smaller than a feature like indices and ranges.

Maybe you can find some workarounds to use these unsupported features in Unity, but I discourage you from doing this because Unity is a cross-platform game engine. A workaround in a new feature could lead you to problems quite hard to understand, debug, and resolve.

Fortunately, Unity supports some of the more common patterns and expressions from C# 8 and 9. Let’s review some of the most helpful ones below and see how they can enable us to write cleaner code.

Switch expression

The switch expression can dramatically simplify and reduce the LOC (Lines Of Code) to make a switch, because we can avoid a bunch of boilerplate code, like the case and return statements.

Doc tip: the switch expression provides for switch-like semantics in an expression context. It provides a concise syntax when the switch arms produce a value.


More great articles from LogRocket:


Often, a switch statement produces a value in each of its case blocks. Switch expressions enable you to use more concise expression syntax. There are fewer repetitive case and break keywords and fewer curly braces.

Before

public string GetModeTitleOld(GameMode mode)
{
    switch (mode)
    {
        case GameMode.Points:
            return "Points mode";

        case GameMode.Survive:
            return "Survive mode";

        case GameMode.TimeAttack:
            return "Time Attack mode";

        default:
            return "Unsupported game mode";
    }
}

After

public string GetModeTitleNew(GameMode mode)
{
    return mode switch
    {
        GameMode.Points => "Points mode",
        GameMode.Survive => "Survive mode",
        GameMode.TimeAttack => "Time Attack mode",
        _ => "Unsupported game mode",
    };
}

Property pattern

The property pattern enables you to match on properties of the object examined in a switch expression.

As you can see in the sample below, using a property pattern, we can transform a series of if statements into a simple list of properties that the object on the switch statement should match.

The _ => has the same meaning as the default on a classic switch.

Doc tip: a property pattern matches an expression when an expression result is non-null and every nested pattern matches the corresponding property or field of the expression result.

Before

public float CalculateDamageOld(Enemy enemy)
{
    if (enemy.IsVisible)
        return enemy.HasArmor ? 1 : 2;

    return 0;
}

After

public static float CalculateDamageNew(Enemy enemy) => enemy switch
{
    { IsVisible: true, HasArmor: true } => 1,
    { IsVisible: true, HasArmor: false } => 2,
    _ => 0
};

Type pattern

We can use type patterns to check if the runtime type of an expression is compatible with a given type.

The type pattern is almost the same logic as a property pattern but is now used in a context of an object type. We can transform a series of if statements that check an object type into a list of types that the object on the switch statement should match.

Before

public static float GetEnemyStrengthOld(Enemy enemy)
{
    if (enemy is Minion)
        return 1;

    if (enemy is Troll)
        return 2;

    if (enemy is Vampire)
        return 3;

    if (enemy == null)
        throw new ArgumentNullException(nameof(enemy));

    throw new ArgumentException("Unknown enemy", nameof(enemy));
}

After

public static float GetEnemyStrengthNew(Enemy enemy) => enemy switch
{
    Minion => 1,
    Troll => 2,
    Vampire => 3,
    null => throw new ArgumentNullException(nameof(enemy)),
    _ => throw new ArgumentException("Unknown enemy", nameof(enemy)),
};

Using the type pattern, we go from 16 lines of code to only 8 that have the same result and are quite clear to read and understand.

Constant pattern

A constant pattern can be used to test if an expression result equals a specified constant.

Probably the simplest pattern match, it just matches a constant value — for instance, a string — and then returns the result.

Before

public Enemy CreateEnemyByNameOld(string name)
{
    if(name == null)
        throw new ArgumentNullException(nameof(name));

    if (name.Equals("Minion"))
        return new Minion();

    if (name.Equals("Troll"))
        return new Troll();

    if (name.Equals("Vampire"))
        return new Vampire();

    throw new ArgumentException($"Unknown enemy: {name}", nameof(name));
}

After

public Enemy CreateEnemyByNameNew(string name) => name switch
{
    "Minion" => new Minion(),
    "Troll" => new Troll(),
    "Vampire" => new Vampire(),
    null => throw new ArgumentNullException(nameof(name)),
    _ => throw new ArgumentException($"Unknown enemy: {name}", nameof(name)),
};

A constant pattern can be used with any constant expression, like int, float, char, string, bool, and enum.

Relational pattern

A relational pattern will compare an expression result with a constant.

This one could seem the most complex pattern match, but at its core it’s not that complicated. What we can do with a Relational Pattern is directly use logical operators as <, >, <=, or >= to evaluate the object and then provide a result for the switch.

Doc tip: the right-hand part of a relational pattern must be a constant expression.

Before

public string GetEnemyEnergyMessageOld(float energy)
{
    if(energy < 0 || energy > 1)
        throw new ArgumentException("Energy should be between 0.0 and 1.0", nameof(energy));

    if (energy >= 1f)
        return "Healthy";

    if (energy > .5f)
        return "Injured";

        return "Very hurt";
}

After

public string GetEnemyEnergyMessageNew(float energy) => energy switch
{
    < 0 or > 1 => throw new ArgumentException("Energy should be between 0.0 and 1.0", nameof(energy)),
    >= 1 => "Healthy",
    > .5f => "Injured",
    _ => "Very hurt"
};

Any of the relational operators <, >, <=, or >= can be used on a relational pattern.

Logical pattern

We can use the not, and, and or pattern combinators to create logical expressions.

This is like an extension of the relational pattern where you can combine the logical operators not, and, and or to create a more complex and elaborate pattern match.

Doc tip: you use the not, and, and or pattern combinators to create the following logical patterns:

  • Negation not pattern that matches an expression when the negated pattern doesn’t match the expression
  • Conjunctive and pattern that matches an expression when both patterns match the expression
  • Disjunctive or pattern that matches an expression when either pattern matches the expression

Before

public float CalculateEnergyLossByStakeOld(Enemy enemy)
{
    if (enemy == null)
        throw new ArgumentNullException(nameof(enemy));

    if (enemy is not Vampire)
        return .1f;

    return 1f;
}

After

public float CalculateEnergyLossByStakeNew(Enemy enemy) => enemy switch
{
    null => throw new ArgumentNullException(nameof(enemy)),
    not Vampire => .1f,
    _ => 1f
};

Conclusion

In this tutorial, we’ve learned how to use the switch expression, property pattern, type pattern, constant pattern, relational pattern, and logical pattern to write less and more modern C# code on Unity.

Hopefully, you can use some of these in your next project to spare yourself time while writing cleaner code.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Diego Giacomelli Programmer since 2001. Aspiring game developer since childhood. I write about C#, Unity 3D, game development and web development, and more. Currently interested in anything that I can code with C#.

Leave a Reply