Unity, a powerful game engine, supports many modern C# features that can greatly enhance the efficiency, readability, and maintainability of your code. In this blog post, we'll explore these features and provide code samples to demonstrate their use in Unity. These features range from new syntax and patterns introduced in C# 8 to the latest enhancements from C# 12.
Switch expressions simplify and reduce the boilerplate code required by traditional switch statements.
public string GetModeTitle(GameMode mode)
{
return mode switch
{
.Points => "Points mode",
GameMode.Survive => "Survive mode",
GameMode.TimeAttack => "Time Attack mode",
GameMode=> "Unsupported game mode",
_ };
}
These patterns allow for more expressive and concise code when checking object properties and types.
public static float CalculateDamage(Enemy enemy) => enemy switch
{
{ IsVisible: true, HasArmor: true } => 1,
{ IsVisible: true, HasArmor: false } => 2,
=> 0
_ };
public static float GetEnemyStrength(Enemy enemy) => enemy switch
{
=> 1,
Minion => 2,
Troll => 3,
Vampire null => throw new ArgumentNullException(nameof(enemy)),
=> throw new ArgumentException("Unknown enemy", nameof(enemy)),
_ };
Default interface methods reduce the need for base classes by allowing interfaces to have implementations.
public interface IUpdatable
{
void Update();
void LateUpdate()
{
// Default implementation
.Log("Late Update");
Debug}
}
public class Player : IUpdatable
{
public void Update()
{
.Log("Player Update");
Debug}
}
Record types provide a concise way to define immutable data objects, which is useful for defining game data models.
public record GameConfig(string LevelName, int MaxPlayers, float Difficulty);
var config = new GameConfig("Forest", 4, 2.5f);
Collection expressions with spread elements make it easier to initialize collections concisely.
int[] row0 = { 1, 2, 3 };
int[] row1 = { 4, 5, 6 };
int[] row2 = { 7, 8, 9 };
int[] single = { ..row0, ..row1, ..row2 };
Raw string literals allow multi-line strings without needing escape sequences, useful for writing shaders or large text data.
string shaderCode = """
"Custom/MyShader" {
Shader {
Properties _Color ("Main Color", Color) = (1,1,1,1)
}
{
SubShader {
Pass // Shader code here
}
}
}
""";
Generic math support allows the use of mathematical operators in generic types, useful for implementing mathematical operations in a type-safe manner.
public interface IAdditionOperators<TSelf, TOther, TResult>
{
static abstract TResult operator +(TSelf left, TOther right);
}
public struct Vector2 : IAdditionOperators<Vector2, Vector2, Vector2>
{
public float X { get; set; }
public float Y { get; set; }
public static Vector2 operator +(Vector2 left, Vector2 right) => new Vector2
{
= left.X + right.X,
X = left.Y + right.Y
Y };
}
This feature improves performance and reduces memory overhead when using method group conversions.
public class EventPublisher
{
public event Action OnEvent;
public void TriggerEvent()
{
?.Invoke();
OnEvent}
}
public class EventSubscriber
{
private EventPublisher _publisher;
public EventSubscriber(EventPublisher publisher)
{
= publisher;
_publisher .OnEvent += HandleEvent;
_publisher}
private void HandleEvent()
{
// Handle the event
}
}
Async streams allow for asynchronous iteration over data streams, useful for handling data that is received over time.
public async IAsyncEnumerable<int> GenerateNumbersAsync()
{
for (int i = 0; i < 10; i++)
{
.Delay(1000);
await Taskyield return i;
}
}
public async void Start()
{
foreach (var number in GenerateNumbersAsync())
await {
.Log(number);
Debug}
}
Nullable reference types help in reducing null reference exceptions by explicitly defining whether a reference can be null or not.
#nullable enablepublic class Player
{
public string Name { get; set; }
public string? Nickname { get; set; } // Can be null
}
Ranges and indices provide a succinct syntax for slicing arrays and collections.
public void Start()
{
int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int[] slice = numbers[2..5]; // { 2, 3, 4 }
.Log(string.Join(", ", slice));
Debug
int last = numbers[^1]; // 9
.Log(last);
Debug}
Using declarations simplify resource management by ensuring resources are disposed of when they go out of scope.
public void ReadFile(string path)
{
using var reader = new StreamReader(path);
string content = reader.ReadToEnd();
.Log(content);
Debug} // reader is disposed here
Read-only members help enforce immutability for structs and classes.
public struct Vector
{
public float X { get; }
public float Y { get; }
public Vector(float x, float y)
{
= x;
X = y;
Y }
public readonly float Magnitude => MathF.Sqrt(X * X + Y * Y);
}
Enhanced stack allocation provides more efficient memory management for performance-critical code.
public unsafe void ProcessData()
{
<int> numbers = stackalloc int[100];
Spanfor (int i = 0; i < numbers.Length; i++)
{
[i] = i;
numbers}
// Process numbers...
}
In parameters allow passing parameters by reference without modifying the argument, improving performance for large structures.
public void Calculate(in Vector vector)
{
float magnitude = vector.Magnitude;
.Log(magnitude);
Debug}
These modern C# features provide powerful tools for writing cleaner, more efficient, and more maintainable code in Unity. By leveraging these features, you can improve your development workflow and create more robust and scalable Unity projects. For more detailed information and further examples, refer to the Unity documentation and the Microsoft C# language reference.