Ben Traje
← Back to unity

Unity C# Best Practices: Component Fetching, Input Systems, and Clean Utility Classes

22 Sep 26 (1mo ago)

When developing in Unity, small architectural decisions—like how you fetch components, handle player inputs, or structure your math functions—can dramatically impact both performance and project scalability.

Whether you are transitioning to modern Unity workflows or just looking to clean up your codebase, here is a definitive guide to fixing common scripting pitfalls and adopting C# best practices.

The Truth About GetComponent and Robustness

A common misconception among newer Unity developers is that calling GetComponent<T>() is unsafe or searches the entire scene.

In reality, GetComponent is highly localized and robust. When you call player.GetComponent<MapMovementController>(), Unity only queries that specific player GameObject. It does not iterate through the scene hierarchy.

What to avoid: The true performance killers are scene-wide searches like FindFirstObjectByType<T>() or GameObject.Find(). These should be avoided whenever possible, especially at runtime.

Modern Best Practice: While GetComponent is safe, calling it continuously inside an Update() loop creates unnecessary overhead.

  • Cache it: Call it once in Start() or Awake() and store the reference in a variable.
  • Use TryGetComponent: Instead of checking for nulls manually, use if (player.TryGetComponent(out MapMovementController controller)) to safely retrieve components without throwing errors or allocating extra memory.

Fixing the "Active Input Handling" InvalidOperationException

If you have recently imported an older asset pack or script, you might run into this console error:

"InvalidOperationException: You are trying to read Input using the UnityEngine.Input class, but you have switched active Input handling to Input System package in Player Settings."

This happens because Unity 6 (and modern LTS versions) defaults to the newer, modular Input System Package, while legacy scripts still rely on the old UnityEngine.Input manager (e.g., Input.GetKeyDown).

How to resolve this:

  • The Quick Fix (Run Both): Navigate to Edit > Project Settings > Player > Other Settings. Find Active Input Handling and change the dropdown to Both. This runs the legacy Input Manager alongside the new Input System, allowing old assets to work instantly with negligible performance overhead. Unity will require a restart to apply this.
  • The Best Practice (Upgrade): Rewrite legacy scripts to use the new Input System APIs (e.g., Keyboard.current[Key.Space].wasPressedThisFrame). The modern system is highly recommended because it natively supports complex hardware layouts—like instantly recognizing and mapping XInput or Switch Pro controllers (such as 8BitDo)—without requiring custom mapping scripts.

Writing Clean Utility Classes (Ditching MonoBehaviour)

Not every script in Unity needs to be a MonoBehaviour. If you are writing a script purely for mathematical calculations or data formatting (like converting grid coordinates to an array index), attaching it to a GameObject is unnecessary bloat.

Instead, use standard C# static classes.

public static class GridUtil
{
    public static int CalculateIndex(int x, int y, int width)
    {
        return x + y * width;
    }
}

Why this is superior for utilities:

  • Global Access: Any script can call GridUtil.CalculateIndex() instantly without needing a reference or GetComponent.
  • No Instantiation: By marking the class itself as static, C# prevents anyone from accidentally creating an instance (new GridUtil()), keeping your memory footprint light.
  • Skips the Singleton: You do not need to rely on the clunky "Instance/Awake" Singleton pattern unless you are actively storing state data or require Unity lifecycle methods like Update().

4. Return Types vs. out Parameters

While C# supports out parameters seamlessly, standard coding conventions dictate when they should be used.

  • Single Values: If your method calculates a single value (like an integer index), always use a standard return statement. This allows you to evaluate the method inline, such as array[GridUtil.CalculateIndex(x, y, width)] = someValue;.
  • Multiple Values: Traditionally, out parameters were the standard for returning two or more values (e.g., out int x, out int y). They are also standard for the "Try" pattern (int.TryParse).

The Modern Upgrade for Multiple Values: If you need to return multiple values (like an X and Y coordinate), modern C# (7.0+) favors Tuples over out parameters. Tuples provide a much cleaner syntax:

// Defining the method with a Tuple return type
public static (int x, int y) CalculatePos(int index, int width)
{
    return (index % width, index / width);
}

// Calling the method
var (x, y) = GridUtil.CalculatePos(myIndex, gridWidth);

Summary

Writing robust Unity code is about using the right tool for the job. Cache your components locally, utilize the power of the modern Input System for plug-and-play controller support, strip MonoBehaviour from static math utilities, and leverage standard C# return types to keep your scripts clean and performant.