5.6 - Properties (Encapsulation)
In the previous section, we explored access modifiers as a way to implement encapsulation. Now, we'll look at another powerful feature of C# that enhances encapsulation: properties.
Properties provide a way to control access to a class's fields while adding additional logic for getting and setting their values. They're a key part of creating a clean, well-designed API for your classes.
The Problem with Public Fields
Let's start by understanding why we might want to use properties instead of public fields:
public class Player
{
// Public fields
public string name;
public int health;
public int level;
}
// Usage
Player player = new Player();
player.name = "Hero";
player.health = 100;
player.level = 1;
// Later in the code...
player.health = -50; // Oops! Health shouldn't be negative
player.level = 0; // Oops! Level should start at 1
The problem with public fields is that they offer no control over:
- Validation (ensuring values are within acceptable ranges)
- Side effects (updating other values or triggering events when a value changes)
- Read-only or write-only access
- Calculation of derived values
Traditional Getter and Setter Methods
Before properties, the standard approach to encapsulation was to use private fields with public getter and setter methods:
public class Player
{
// Private fields
private string name;
private int health;
private int level;
// Getter methods
public string GetName()
{
return name;
}
public int GetHealth()
{
return health;
}
public int GetLevel()
{
return level;
}
// Setter methods
public void SetName(string value)
{
name = value;
}
public void SetHealth(int value)
{
if (value < 0)
{
value = 0;
}
health = value;
if (health == 0)
{
Console.WriteLine($"{name} has been defeated!");
}
}
public void SetLevel(int value)
{
if (value < 1)
{
value = 1;
}
level = value;
Console.WriteLine($"{name} is now level {level}!");
}
}
// Usage
Player player = new Player();
player.SetName("Hero");
player.SetHealth(100);
player.SetLevel(1);
// Later in the code...
player.SetHealth(-50); // Health will be set to 0
player.SetLevel(0); // Level will be set to 1
This approach works, but it's verbose and doesn't feel as natural as using fields directly.
C# Properties: A Better Way
Properties in C# provide a more elegant solution. They look like fields to the code that uses them, but they're implemented with special accessor methods called get
and set
.
Basic Property Syntax
public class Player
{
// Private backing fields
private string name;
private int health;
private int level;
// Public properties
public string Name
{
get { return name; }
set { name = value; }
}
public int Health
{
get { return health; }
set
{
if (value < 0)
{
value = 0;
}
health = value;
if (health == 0)
{
Console.WriteLine($"{name} has been defeated!");
}
}
}
public int Level
{
get { return level; }
set
{
if (value < 1)
{
value = 1;
}
level = value;
Console.WriteLine($"{name} is now level {level}!");
}
}
}
// Usage - looks like fields, but with all the benefits of methods
Player player = new Player();
player.Name = "Hero";
player.Health = 100;
player.Level = 1;
// Later in the code...
player.Health = -50; // Health will be set to 0
player.Level = 0; // Level will be set to 1
// Reading the properties
Console.WriteLine($"{player.Name} is level {player.Level} with {player.Health} health.");
In this example:
Name
,Health
, andLevel
are properties- Each property has a
get
accessor that returns a value - Each property has a
set
accessor that assigns a value - The
value
keyword represents the value being assigned in theset
accessor - The
Health
andLevel
properties include validation logic
Auto-Implemented Properties (C# 3.0)
For simple properties that don't need additional logic, C# provides a shorthand syntax called auto-implemented properties:
public class Item
{
// Auto-implemented properties
public string Name { get; set; }
public int Value { get; set; }
public string Rarity { get; set; }
}
// Usage
Item sword = new Item();
sword.Name = "Steel Sword";
sword.Value = 100;
sword.Rarity = "Common";
Behind the scenes, the compiler creates a private backing field for each auto-implemented property. This syntax is concise and still maintains the flexibility to add logic later if needed.
Property Access Modifiers
You can apply different access modifiers to the get
and set
accessors:
public class Player
{
// Public getter, private setter
public string Name { get; private set; }
// Public getter, protected setter
public int Health { get; protected set; }
// Constructor to initialize properties with private/protected setters
public Player(string name, int health)
{
Name = name;
Health = health;
}
// Method that can use the protected setter
public void TakeDamage(int amount)
{
Health -= amount;
if (Health < 0)
Health = 0;
}
}
// Usage
Player player = new Player("Hero", 100);
Console.WriteLine(player.Name); // OK - public getter
Console.WriteLine(player.Health); // OK - public getter
// player.Name = "Villain"; // Error - private setter
// player.Health = 50; // Error - protected setter
player.TakeDamage(30); // OK - method in the same class can use protected setter
Console.WriteLine(player.Health); // Output: 70
This approach allows you to create read-only properties from outside the class while still allowing the class itself (or derived classes) to modify the values.
Read-Only Properties
You can create read-only properties by omitting the set
accessor:
public class Circle
{
private double radius;
public Circle(double radius)
{
this.radius = radius;
}
public double Radius
{
get { return radius; }
}
public double Diameter
{
get { return radius * 2; }
}
public double Circumference
{
get { return 2 * Math.PI * radius; }
}
public double Area
{
get { return Math.PI * radius * radius; }
}
}
// Usage
Circle circle = new Circle(5);
Console.WriteLine($"Radius: {circle.Radius}"); // Output: 5
Console.WriteLine($"Diameter: {circle.Diameter}"); // Output: 10
Console.WriteLine($"Circumference: {circle.Circumference:F2}"); // Output: 31.42
Console.WriteLine($"Area: {circle.Area:F2}"); // Output: 78.54
// circle.Radius = 10; // Error - no setter
Read-only properties are useful for:
- Immutable data that shouldn't change after initialization
- Calculated values derived from other fields
- Exposing internal state without allowing modification
Write-Only Properties
Though less common, you can also create write-only properties by omitting the get
accessor:
public class Logger
{
private string logFile;
public Logger(string logFile)
{
this.logFile = logFile;
}
public string LogMessage
{
set
{
// Append the message to the log file
File.AppendAllText(logFile, $"[{DateTime.Now}] {value}\n");
}
}
}
// Usage
Logger logger = new Logger("game.log");
logger.LogMessage = "Game started";
logger.LogMessage = "Player joined: Hero";
logger.LogMessage = "Level loaded: Forest";
// string message = logger.LogMessage; // Error - no getter
Write-only properties are useful for:
- Logging systems
- Password fields
- Any scenario where you want to accept input without allowing retrieval
Expression-Bodied Properties (C# 6.0)
For simple property implementations, you can use expression-bodied members:
public class Player
{
private int experience;
// Expression-bodied property with backing field
public int Level => experience / 1000 + 1;
// Expression-bodied property with calculation
public bool IsMaxLevel => Level >= 100;
// Expression-bodied property with string formatting
public string LevelDisplay => $"Level {Level}";
}
This syntax is concise and readable for simple computed properties.
Init-Only Setters (C# 9.0)
C# 9.0 (supported by Unity 6.x) introduced init-only setters, which allow properties to be set only during object initialization:
public class ImmutablePlayer
{
// Init-only properties
public string Name { get; init; }
public string Class { get; init; }
public DateTime CreationDate { get; init; }
// Regular property that can be modified
public int Experience { get; set; }
// Computed property based on Experience
public int Level => Experience / 1000 + 1;
}
// Usage
ImmutablePlayer player = new ImmutablePlayer
{
Name = "Hero",
Class = "Warrior",
CreationDate = DateTime.Now,
Experience = 0
};
// These are allowed
player.Experience = 1500;
Console.WriteLine($"{player.Name} is now level {player.Level}");
// These would cause errors
// player.Name = "Villain"; // Error - init-only
// player.Class = "Mage"; // Error - init-only
// player.CreationDate = DateTime.Now.AddDays(1); // Error - init-only
Init-only properties are useful for creating objects that are partially immutable—some properties can't be changed after initialization, while others can.
Properties vs. Fields in Unity
In Unity, properties work just like in standard C#, but there are some Unity-specific considerations:
Serialization
Unity's serialization system (which handles saving/loading and Inspector display) works with fields, not properties:
public class PlayerController : MonoBehaviour
{
// Will appear in Inspector
public float moveSpeed = 5f;
// Will NOT appear in Inspector
public float RunSpeed { get; set; } = 10f;
// Solution: use a backing field with [SerializeField]
[SerializeField] private float jumpForce = 8f;
public float JumpForce
{
get { return jumpForce; }
set { jumpForce = Mathf.Max(0, value); } // Ensure non-negative
}
}
Performance Considerations
For performance-critical code that runs every frame (like in Update
), consider the overhead of property accessors:
public class PerformanceExample : MonoBehaviour
{
// Option 1: Direct field access (fastest)
private Vector3 position;
// Option 2: Property with validation (slight overhead)
private float speed;
public float Speed
{
get { return speed; }
set { speed = Mathf.Max(0, value); }
}
// For frequently accessed values in tight loops, consider direct field access
private void Update()
{
// High-frequency access in performance-critical code
for (int i = 0; i < 1000; i++)
{
position += Vector3.forward * speed * Time.deltaTime;
}
}
}
For most cases, the performance difference is negligible, and the benefits of properties outweigh the tiny overhead.
Practical Examples
Example 1: Character Stats System
public class CharacterStats
{
// Private backing fields
private string name;
private int health;
private int maxHealth;
private int mana;
private int maxMana;
private int strength;
private int dexterity;
private int intelligence;
private int level;
private int experience;
// Properties with validation and derived values
public string Name
{
get { return name; }
set { name = value; }
}
public int Health
{
get { return health; }
set
{
health = Mathf.Clamp(value, 0, maxHealth);
if (health == 0)
OnDeath?.Invoke();
}
}
public int MaxHealth
{
get { return maxHealth; }
set
{
maxHealth = Mathf.Max(1, value);
health = Mathf.Min(health, maxHealth);
}
}
public int Mana
{
get { return mana; }
set { mana = Mathf.Clamp(value, 0, maxMana); }
}
public int MaxMana
{
get { return maxMana; }
set
{
maxMana = Mathf.Max(0, value);
mana = Mathf.Min(mana, maxMana);
}
}
public int Strength
{
get { return strength; }
set { strength = Mathf.Max(1, value); }
}
public int Dexterity
{
get { return dexterity; }
set { dexterity = Mathf.Max(1, value); }
}
public int Intelligence
{
get { return intelligence; }
set { intelligence = Mathf.Max(1, value); }
}
public int Level
{
get { return level; }
private set { level = Mathf.Max(1, value); }
}
public int Experience
{
get { return experience; }
set
{
experience = Mathf.Max(0, value);
CheckLevelUp();
}
}
// Calculated properties
public float HealthPercentage => (float)health / maxHealth;
public float ManaPercentage => maxMana > 0 ? (float)mana / maxMana : 0;
public int PhysicalDamage => strength * 2 + level;
public int MagicalDamage => intelligence * 2 + level;
public int CriticalChance => dexterity / 5;
// Event for death
public event Action OnDeath;
// Constructor
public CharacterStats(string name, int level = 1)
{
this.name = name;
this.level = level;
// Initialize stats based on level
maxHealth = 100 + (level - 1) * 20;
health = maxHealth;
maxMana = 50 + (level - 1) * 10;
mana = maxMana;
strength = 10;
dexterity = 10;
intelligence = 10;
experience = 0;
}
// Methods
public void TakeDamage(int amount)
{
Health -= amount;
Console.WriteLine($"{name} takes {amount} damage. Health: {health}/{maxHealth}");
}
public void Heal(int amount)
{
Health += amount;
Console.WriteLine($"{name} heals for {amount}. Health: {health}/{maxHealth}");
}
public bool UseMana(int amount)
{
if (mana >= amount)
{
Mana -= amount;
Console.WriteLine($"{name} uses {amount} mana. Mana: {mana}/{maxMana}");
return true;
}
Console.WriteLine($"{name} doesn't have enough mana!");
return false;
}
public void AddExperience(int amount)
{
Console.WriteLine($"{name} gains {amount} experience!");
Experience += amount;
}
private void CheckLevelUp()
{
int experienceNeeded = 1000 * level;
while (experience >= experienceNeeded)
{
experience -= experienceNeeded;
Level++;
// Increase stats on level up
MaxHealth += 20;
Health = MaxHealth; // Fully heal on level up
MaxMana += 10;
Mana = MaxMana; // Fully restore mana on level up
Strength += 2;
Dexterity += 2;
Intelligence += 2;
Console.WriteLine($"{name} leveled up to level {level}!");
// Calculate new experience needed for next level
experienceNeeded = 1000 * level;
}
}
}
// Usage in Unity
public class Player : MonoBehaviour
{
private CharacterStats stats;
private void Start()
{
stats = new CharacterStats("Hero");
stats.OnDeath += HandlePlayerDeath;
DisplayStats();
}
private void HandlePlayerDeath()
{
Debug.Log("Player has died! Game over.");
// Handle game over logic
}
public void TakeDamage(int amount)
{
stats.TakeDamage(amount);
UpdateHealthBar(stats.HealthPercentage);
}
public void CastSpell(int manaCost, int damage)
{
if (stats.UseMana(manaCost))
{
Debug.Log($"Spell cast for {stats.MagicalDamage + damage} damage!");
UpdateManaBar(stats.ManaPercentage);
}
}
public void DefeatEnemy(int experienceReward)
{
stats.AddExperience(experienceReward);
DisplayStats();
}
private void DisplayStats()
{
Debug.Log($"Name: {stats.Name}");
Debug.Log($"Level: {stats.Level}");
Debug.Log($"Health: {stats.Health}/{stats.MaxHealth}");
Debug.Log($"Mana: {stats.Mana}/{stats.MaxMana}");
Debug.Log($"Strength: {stats.Strength}");
Debug.Log($"Dexterity: {stats.Dexterity}");
Debug.Log($"Intelligence: {stats.Intelligence}");
Debug.Log($"Physical Damage: {stats.PhysicalDamage}");
Debug.Log($"Magical Damage: {stats.MagicalDamage}");
Debug.Log($"Critical Chance: {stats.CriticalChance}%");
}
private void UpdateHealthBar(float percentage)
{
// Update UI health bar based on percentage
Debug.Log($"Health bar updated: {percentage:P0}");
}
private void UpdateManaBar(float percentage)
{
// Update UI mana bar based on percentage
Debug.Log($"Mana bar updated: {percentage:P0}");
}
}
Example 2: Game Settings with Auto-Properties
public class GameSettings
{
// Auto-implemented properties with default values
public string PlayerName { get; set; } = "Player";
public float MusicVolume { get; set; } = 0.7f;
public float SfxVolume { get; set; } = 0.8f;
public bool FullScreen { get; set; } = true;
public int ResolutionIndex { get; set; } = 0;
public int QualityLevel { get; set; } = 2;
public bool InvertYAxis { get; set; } = false;
public float MouseSensitivity { get; set; } = 1.0f;
// Calculated property
public bool HasAudio => MusicVolume > 0 || SfxVolume > 0;
// Methods to save/load settings
public void SaveSettings()
{
PlayerPrefs.SetString("PlayerName", PlayerName);
PlayerPrefs.SetFloat("MusicVolume", MusicVolume);
PlayerPrefs.SetFloat("SfxVolume", SfxVolume);
PlayerPrefs.SetInt("FullScreen", FullScreen ? 1 : 0);
PlayerPrefs.SetInt("ResolutionIndex", ResolutionIndex);
PlayerPrefs.SetInt("QualityLevel", QualityLevel);
PlayerPrefs.SetInt("InvertYAxis", InvertYAxis ? 1 : 0);
PlayerPrefs.SetFloat("MouseSensitivity", MouseSensitivity);
PlayerPrefs.Save();
}
public void LoadSettings()
{
PlayerName = PlayerPrefs.GetString("PlayerName", "Player");
MusicVolume = PlayerPrefs.GetFloat("MusicVolume", 0.7f);
SfxVolume = PlayerPrefs.GetFloat("SfxVolume", 0.8f);
FullScreen = PlayerPrefs.GetInt("FullScreen", 1) == 1;
ResolutionIndex = PlayerPrefs.GetInt("ResolutionIndex", 0);
QualityLevel = PlayerPrefs.GetInt("QualityLevel", 2);
InvertYAxis = PlayerPrefs.GetInt("InvertYAxis", 0) == 1;
MouseSensitivity = PlayerPrefs.GetFloat("MouseSensitivity", 1.0f);
}
public void ApplySettings()
{
// Apply resolution
Resolution[] resolutions = Screen.resolutions;
if (ResolutionIndex >= 0 && ResolutionIndex < resolutions.Length)
{
Resolution resolution = resolutions[ResolutionIndex];
Screen.SetResolution(resolution.width, resolution.height, FullScreen);
}
// Apply quality level
QualitySettings.SetQualityLevel(QualityLevel);
// Other settings would be applied by the systems that use them
}
public void ResetToDefaults()
{
PlayerName = "Player";
MusicVolume = 0.7f;
SfxVolume = 0.8f;
FullScreen = true;
ResolutionIndex = 0;
QualityLevel = 2;
InvertYAxis = false;
MouseSensitivity = 1.0f;
}
}
// Usage in Unity
public class SettingsManager : MonoBehaviour
{
public static SettingsManager Instance { get; private set; }
private GameSettings settings;
private void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
settings = new GameSettings();
settings.LoadSettings();
settings.ApplySettings();
}
else
{
Destroy(gameObject);
}
}
public GameSettings GetSettings()
{
return settings;
}
public void SaveAndApplySettings()
{
settings.SaveSettings();
settings.ApplySettings();
}
}
// Usage in a settings UI
public class SettingsUI : MonoBehaviour
{
public Slider musicSlider;
public Slider sfxSlider;
public Toggle fullScreenToggle;
public Dropdown resolutionDropdown;
public Dropdown qualityDropdown;
public Toggle invertYAxisToggle;
public Slider sensitivitySlider;
private GameSettings settings;
private void Start()
{
settings = SettingsManager.Instance.GetSettings();
// Initialize UI elements with current settings
musicSlider.value = settings.MusicVolume;
sfxSlider.value = settings.SfxVolume;
fullScreenToggle.isOn = settings.FullScreen;
resolutionDropdown.value = settings.ResolutionIndex;
qualityDropdown.value = settings.QualityLevel;
invertYAxisToggle.isOn = settings.InvertYAxis;
sensitivitySlider.value = settings.MouseSensitivity;
// Add listeners
musicSlider.onValueChanged.AddListener(OnMusicVolumeChanged);
sfxSlider.onValueChanged.AddListener(OnSfxVolumeChanged);
// Add other listeners...
}
private void OnMusicVolumeChanged(float value)
{
settings.MusicVolume = value;
// Update audio immediately
AudioManager.Instance.SetMusicVolume(value);
}
private void OnSfxVolumeChanged(float value)
{
settings.SfxVolume = value;
// Update audio immediately
AudioManager.Instance.SetSfxVolume(value);
}
public void SaveSettings()
{
SettingsManager.Instance.SaveAndApplySettings();
}
public void ResetToDefaults()
{
settings.ResetToDefaults();
// Update UI elements
musicSlider.value = settings.MusicVolume;
sfxSlider.value = settings.SfxVolume;
// Update other UI elements...
// Apply changes
SettingsManager.Instance.SaveAndApplySettings();
}
}
Example 3: Immutable Game Object with Init-Only Properties (C# 9)
public class GameItem
{
// Init-only properties (C# 9)
public string Id { get; init; }
public string Name { get; init; }
public string Description { get; init; }
public ItemRarity Rarity { get; init; }
public ItemType Type { get; init; }
// Regular properties that can change
public int Quantity { get; set; } = 1;
public bool IsEquipped { get; set; } = false;
// Calculated properties
public int Value => CalculateValue();
public string DisplayName => $"{Name} ({Rarity})";
public Color RarityColor => GetRarityColor();
// Private methods for calculated properties
private int CalculateValue()
{
int baseValue = Rarity switch
{
ItemRarity.Common => 10,
ItemRarity.Uncommon => 25,
ItemRarity.Rare => 100,
ItemRarity.Epic => 500,
ItemRarity.Legendary => 2000,
_ => 5
};
// Apply type multiplier
float multiplier = Type switch
{
ItemType.Weapon => 1.5f,
ItemType.Armor => 1.2f,
ItemType.Consumable => 0.8f,
ItemType.Material => 0.5f,
ItemType.Quest => 0.0f, // Quest items have no value
_ => 1.0f
};
return (int)(baseValue * multiplier);
}
private Color GetRarityColor()
{
return Rarity switch
{
ItemRarity.Common => Color.white,
ItemRarity.Uncommon => Color.green,
ItemRarity.Rare => Color.blue,
ItemRarity.Epic => new Color(0.5f, 0, 0.5f), // Purple
ItemRarity.Legendary => Color.yellow,
_ => Color.gray
};
}
}
public enum ItemRarity
{
Common,
Uncommon,
Rare,
Epic,
Legendary
}
public enum ItemType
{
Weapon,
Armor,
Consumable,
Material,
Quest
}
// Usage in Unity
public class InventoryManager : MonoBehaviour
{
private List<GameItem> inventory = new List<GameItem>();
private void Start()
{
// Create items with init-only properties
GameItem sword = new GameItem
{
Id = "weapon_001",
Name = "Steel Sword",
Description = "A sturdy steel sword.",
Rarity = ItemRarity.Common,
Type = ItemType.Weapon
};
GameItem potion = new GameItem
{
Id = "consumable_001",
Name = "Health Potion",
Description = "Restores 50 health points.",
Rarity = ItemRarity.Common,
Type = ItemType.Consumable,
Quantity = 5 // Can set regular properties too
};
GameItem epicArmor = new GameItem
{
Id = "armor_005",
Name = "Dragon Scale Armor",
Description = "Armor forged from dragon scales.",
Rarity = ItemRarity.Epic,
Type = ItemType.Armor
};
// Add items to inventory
inventory.Add(sword);
inventory.Add(potion);
inventory.Add(epicArmor);
// Display inventory
DisplayInventory();
// We can change mutable properties
sword.IsEquipped = true;
potion.Quantity += 2;
// But we cannot change immutable properties
// sword.Name = "Iron Sword"; // Error - init-only property
// potion.Rarity = ItemRarity.Uncommon; // Error - init-only property
// Display updated inventory
DisplayInventory();
}
private void DisplayInventory()
{
Debug.Log("Inventory Contents:");
foreach (GameItem item in inventory)
{
string equippedStatus = item.IsEquipped ? "[Equipped]" : "";
string quantityText = item.Quantity > 1 ? $"x{item.Quantity}" : "";
Debug.Log($"{item.DisplayName} {quantityText} {equippedStatus}");
Debug.Log($" Type: {item.Type}, Value: {item.Value} gold");
Debug.Log($" {item.Description}");
}
}
}
Best Practices for Properties
-
Use Properties for Public Data: Instead of public fields, use properties to maintain encapsulation.
-
Keep Property Accessors Simple: Avoid complex or time-consuming operations in property accessors, especially if they'll be called frequently.
-
Consider Side Effects Carefully: Be cautious about side effects in property setters. They should be predictable and related to the property being set.
-
Use Auto-Implemented Properties for simple cases where no additional logic is needed.
-
Make Properties Read-Only when appropriate to prevent unintended modifications.
-
Use Init-Only Properties (C# 9) for immutable data that should only be set during initialization.
-
Consider Validation in setters to ensure data integrity.
-
Use Expression-Bodied Properties for simple computed properties to improve readability.
-
Remember Unity's Serialization limitations and use backing fields with
[SerializeField]
when needed. -
Document Properties with meaningful names and comments, especially for public APIs.
Conclusion
Properties are a powerful feature in C# that enhance encapsulation by providing controlled access to a class's data. They combine the accessibility of fields with the control of methods, allowing you to validate data, trigger side effects, and create read-only or computed values.
By using properties effectively, you can create more robust, maintainable, and user-friendly classes for your Unity games.
In the next section, we'll explore static members, which belong to the class itself rather than to instances of the class.
Practice Exercise
Exercise: Refactor the following class to use properties instead of public fields and direct access:
public class Enemy
{
public string name;
public int health;
public int maxHealth;
public int damage;
public float speed;
public bool isAlive;
public Enemy(string name, int health, int damage, float speed)
{
this.name = name;
this.health = health;
this.maxHealth = health;
this.damage = damage;
this.speed = speed;
this.isAlive = true;
}
public void TakeDamage(int amount)
{
health -= amount;
if (health <= 0)
{
health = 0;
isAlive = false;
Console.WriteLine($"{name} has been defeated!");
}
}
public void Heal(int amount)
{
health += amount;
if (health > maxHealth)
{
health = maxHealth;
}
}
public void Attack(Player player)
{
if (isAlive)
{
Console.WriteLine($"{name} attacks for {damage} damage!");
player.health -= damage;
}
}
}
public class Player
{
public string name;
public int health;
public int maxHealth;
public Player(string name, int health)
{
this.name = name;
this.health = health;
this.maxHealth = health;
}
}
Consider:
- Which fields should be exposed as properties?
- Which properties should have validation?
- Which properties could be calculated rather than stored?
- How can you improve encapsulation while maintaining functionality?