Unity Architecture: Why You Should Stop Using Public Fields
TL;DR: Most Unity tutorials slap public on every field to save time, but it throws out encapsulation and lets any script stomp your health value. I build a small NPC example to show why private with controlled entry points protects invariants, and why Range alone is not a real fix.
In a lot of Unity tutorials you will notice that public is used everywhere. I am here to tell you that this is a bad practice. Most creators do this because they do not want to overcomplicate the video. The time required to record and explain the actual problems caused by public fields grows exponentially. It is easier to show a quick shortcut than to explain proper architecture. I want to take a direct approach to this problem instead. I will build an NPC with a health value to show the process and step through the exact logic behind every line of code.
If someone asks for a character with health you might start here.
public class NPC : MonoBehaviour
{
public int health = 100;
}
Technically the requirement is met. The character has health. It is public so I can set it to 100 and change it in the Inspector while the game is running. By making this field public I lose all control over the data. Any other script that references this NPC can change this value at any time.
Why not use public fields?
In a small prototype, a public field can be fine when the scope is tight and you control usage. The problem starts when public becomes a habit.
In a real project, a public field is an invitation for anyone else on your team to change that value without knowing the rules. If a teammate sees public int health, they will assume they can set it to whatever they want. They might set it to a negative number or bypass a death trigger because the rest of your code assumed the value would stay valid. That is where you end up with unsafe code practices, where it is hard to trust the integrity of your own variables.
So the practical framing is: do not use public fields everywhere. When you actually choose public, make it the ones that are meant to be read or edited by other systems. Anything that must stay within invariants should stay private, and you expose controlled entry points instead.
Why do public and private exist?
In early programming, most data was open by default because software was smaller and often written by one person. As projects got bigger and teams got larger, that model broke down fast. Access modifiers like private and public were added so classes could protect internal state and expose only safe entry points. That is the core of encapsulation, and it exists to keep code predictable when many systems and many people touch the same data.
Is [Range] enough?
The next requirement is usually a range limit. You do not want health to drop to negative values. To solve this in the Unity Inspector I often reach for an attribute.
[Range(0, 100)]
public int health = 100;
This gives me a nice slider in the Inspector. This is where many developers stop, thinking they have "fixed" the variable. They have not. Attributes are situational and baked in at compile time.
Understanding [Range]: think of this like a volume slider on a speaker. It turns your input box into a physical slider that prevents you from sliding below or above specific numbers. However, it is hard-coded. If your character levels up and needs more health, a [Range] attribute cannot change its limits while the game is running.
Most importantly, [Range] is editor-only. It only restricts what a human can do while dragging a slider in the Unity Inspector. It does nothing to protect the code. If another script says npc.health = -999;, C# will execute that line without any errors, completely bypassing your safety attribute. If you need a dynamic health upgrade that moves your max health from 100 to 150, this attribute becomes a problem. You cannot pass a variable into an attribute. You have used an editor-only tool to try and solve a runtime game rule.
Should I use OnValidate?
Since I cannot use attributes for dynamic ranges I can use the Unity callback for editor changes.
public class NPC : MonoBehaviour
{
public int health = 100;
public int maxHealth = 100;
private void OnValidate()
{
health = Mathf.Clamp(health, 0, maxHealth);
}
}
Understanding OnValidate: this is a security guard function. Every time you type a number or move a slider in the Unity Inspector, Unity automatically wakes this function up. It allows you to write code to fix or clean your data immediately. Unlike [Range], it can use variables, meaning it can check your maxHealth to see if your current health is still valid.
It is important to remember that OnValidate is meant for inspector debugging and editor-side data validation. It ensures that the values you set while designing your levels are logical and safe.
Now I can increase my max health to 200 and my health slider respects that new limit. This is an improvement, but I have introduced a hidden bug. This validation only runs in the editor. If an external script gets a reference to this NPC and sets the health to a negative number at runtime it will bypass my logic. Your game rules are useless because the field is public.
How do I protect health data?
The first real lesson in clean code is to protect the data. Do not use public fields. I make both values private, and keep Inspector visibility with [SerializeField].
[SerializeField] private int _health = 100;
[SerializeField] private int _maxHealth = 100;
Now the health data is guarded. External scripts cannot touch it directly, but I can still tune it in Unity. I still need a safe way for game code to change health from other scripts. This brings us to mutators.
A mutator is just a function that changes a value. It gives me one place to run validation.
public void SetHealth(int newHealth)
{
_health = Mathf.Clamp(newHealth, 0, _maxHealth);
}
By forcing every script to go through this function I ensure the health never goes out of bounds. I also clean up editor validation by calling this same function. This way my clamping rules live in one place.
A setter method gives me one safe place to change the health value. It does not tell anyone else in the game that the value changed. If the UI health bar or a kill tracker needs to react when health drops, they have to check it on every frame. That is what C# events solve, and most Unity devs use them wrong. I broke down the patterns I actually use in production in Most Unity devs still don't understand events.
Private fields or public properties?
If you have a UI script that needs to print the health value it cannot read a private field directly. I need an accessor. In standard C# development, you might use an auto-property like public int Health { get; set; }, but in Unity, the Inspector will not serialize normal properties.
This is a Unity limitation. You need the safety of a property, but properties are not directly serialized by Unity's default Inspector flow. So I use a private serialized field for persistence and a public property for controlled access.
[SerializeField] private int _health = 100;
[SerializeField] private int _maxHealth = 100;
public int Health
{
get
{
return _health;
}
set
{
_health = Mathf.Clamp(value, 0, _maxHealth);
}
}
When you write code to set the property, C# treats the incoming number as value inside that setter. It looks like a simple field from outside, but it behaves like a guarded function.
I use the underscore prefix for private fields for a reason. It tells me at a glance that this variable is a class-level field. When I see _health inside a long method I know exactly where it came from. I keep the public property capitalized as Health because that is standard C# naming.
Why can't Unity serialize properties?
A C# property is not a field. It is compiler-generated getter and setter methods around data access. Because Unity serialization is field-based, a plain property is not handled the same way as a serialized field in default Inspector workflows.
This creates a serialization conflict for the developer. You want property safety, but you also need Inspector visibility and serialized persistence. The practical solution is to combine both. Use a private serialized field for Unity serialization, and use a property or method for safe access.
What is the serialized property pattern?
Here is what the final version looks like when I protect both variables.
public class NPC : MonoBehaviour
{
[SerializeField] private int _health = 100;
[SerializeField] private int _maxHealth = 100;
public int Health
{
get { return _health; }
set { _health = Mathf.Clamp(value, 0, _maxHealth); }
}
public int MaxHealth
{
get { return _maxHealth; }
set { _maxHealth = Mathf.Max(1, value); }
}
}
I started with a single line of code and ended with properties backed by private fields. It takes more typing, but each addition solved a specific requirement. Visibility in the inspector. Safety from other scripts. Flexibility to change the max range at runtime. When you build features by solving exact requirements you end up writing systems that are much harder to break.
Should I use lambda properties?
If your property logic is simple and does not need multi-line validation, you can use the lambda operator => to shorten syntax. This keeps the same safety with less noise.
public class NPC : MonoBehaviour
{
[SerializeField] private int _health = 100;
[SerializeField] private int _maxHealth = 100;
// A shortcut for both get and set
public int Health
{
get => _health;
set => _health = Mathf.Clamp(value, 0, _maxHealth);
}
}
This single-line style removes curly-brace clutter while keeping encapsulation. It is a great way to keep scripts clean once this pattern is second nature.
Why I did not include [field: SerializeField] in this article
I noticed some of you were wondering why I did not include [field: SerializeField] here, and if I even know about it at all. I do know about it, and I made that call on purpose. There is a reason I do not use it in production gameplay architecture. If you want the full breakdown, read more here.
Unity public fields vs SerializeField short answer
If you reached this section because you searched for Unity public fields vs SerializeField, should I use public variables in Unity, or Unity C# encapsulation best practices, this is my short answer. I keep mutable gameplay values private, I serialize backing fields for Inspector workflow, and I expose controlled writes through methods or properties that clamp and validate data.
When people ask how to clamp health in Unity, Unity OnValidate vs property setter, Unity property with private backing field, or C# properties vs fields in Unity Inspector, I use one rule. I treat OnValidate as editor sanity checks, and I keep runtime protection in code paths that always execute in play mode.
Want to learn more about Architecture in Unity? Read What is Unity DOTS. Should you learn it in 2026? Is it useful?
Also, read how bad Architecture can destroy the game Cities Skylines 2 failed because of Unity? Is this true? I investigated!