Summary

A C# property is a class member that looks like a field from the outside — you read or write it with simple assignment syntax — but internally runs code in a get or set block. Properties combine the ease-of-use of public fields with the control of private data. They are ubiquitous across .NET and Unity: transform.position, rigidbody.velocity, Time.deltaTime, and Input.GetAxis are all properties. Understanding properties is necessary for reading Unity API documentation and for writing well-encapsulated game classes.


Key ideas

The problem properties solve:

Exposing a field as public allows any other script to set it to any value, including invalid ones. A public int health can be set to -99999 by accident. Using private data with public methods solves this but can be verbose:

// Verbose method approach
public int GetHealth()         { return currentHealth; }
public void SetHealth(int v)   { if (v >= 0) currentHealth = v; }

Properties provide equivalent control with cleaner syntax.

Property syntax:

public class Player : MonoBehaviour
{
    private int currentHealth;
 
    public int Health
    {
        get { return currentHealth; }
        set
        {
            if (value >= 0 && value <= 100)
                currentHealth = value;
        }
    }
}

From outside the class, this reads and writes exactly like a field:

player.Health = 50;          // calls the set block; value = 50
int h = player.Health;       // calls the get block
player.Health = -999;        // silently rejected by the set block

The keyword value inside a set block is the value being assigned.

Read-only property:

Omit the set block. External code can read but not write.

public float HealthPercent
{
    get { return (float)currentHealth / maxHealth; }
}

Expression-bodied read-only property (shorthand):

For a get-only property with a single expression, use =>:

public float HealthPercent => (float)currentHealth / maxHealth;
public bool IsAlive        => currentHealth > 0;
public int  Score          => baseScore + bonusScore;

This is the most common style in modern Unity C# for derived or read-only values.

Auto-properties:

When no validation or custom logic is needed, an auto-property generates the backing field automatically:

public string PlayerName { get; set; }     // readable and writable from outside
public int    Score      { get; private set; }  // readable outside, writable only from within this class

Score { get; private set; } is especially common in Unity scripts: other scripts can read the score but only this script can change it.

Properties in interfaces:

An interface can require a property:

public interface IHasHealth
{
    int MaxHealth { get; }
    int CurrentHealth { get; }
    bool IsAlive { get; }
}

Any class implementing IHasHealth must provide these properties (at minimum a getter for each).


In practice (Unity)

Combining a serialised backing field with a read-only property:

public class Enemy : MonoBehaviour
{
    [SerializeField] private int maxHealth = 100;
    private int currentHealth;
 
    // Read-only exposure — other scripts can check but not set health directly
    public int CurrentHealth => currentHealth;
    public bool IsAlive      => currentHealth > 0;
 
    private void Start()
    {
        currentHealth = maxHealth;
    }
 
    public void TakeDamage(int amount)
    {
        currentHealth = Mathf.Max(0, currentHealth - amount);
    }
}

External scripts check enemy.IsAlive and enemy.CurrentHealth without being able to set them.

Score with private setter:

public class ScoreManager : MonoBehaviour
{
    public int Score { get; private set; }
 
    public void AddPoints(int points)
    {
        Score += points;   // private set — only this class can change Score
    }
}

UI scripts read ScoreManager.Instance.Score freely but cannot accidentally overwrite it.


Evidence

From Miles (2019, §4.12): “Properties are a way of making the management of data like this slightly easier… The really nice thing about properties is that they are used just as the class member was [but with full control inside the get/set blocks].”

From Miles (2019, §4.12.5, Property Problems): “When you assign a value to a property you are actually calling a method. Unfortunately there is no way you can tell that from the code which does the assignment… it is possible to use this to advantage, in that an object can react to and track property changes.”


Gotchas

  • A set block cannot return a success/failure signal. If assignment can fail in a detectable way (e.g., a validation error the caller needs to know about), use a method instead: bool SetHealth(int v).
  • [SerializeField] does not work on properties — only on fields. To expose a property’s value in the Inspector, keep a separate [SerializeField] private backing field and reference it in the property.
  • Auto-properties with no setter are not the same as an expression-bodied property: public int X { get; } generates a readonly backing field (settable in constructor); public int X => someCalc; recomputes every time. Choose deliberately.
  • Properties that run significant code (database calls, heavy computation) are a common source of hidden performance bugs — the call site looks like a simple field read.