5.11 - Sealed Classes and Methods
Sealed classes and methods are features in C# that allow you to restrict inheritance and method overriding. They are essentially the opposite of abstract classes and virtual methods, which encourage extension through inheritance.
What are Sealed Classes?
A sealed class is a class that cannot be inherited from. Once you declare a class as sealed, no other class can use it as a base class. This is useful when you want to prevent further specialization of a class, either for security, design, or performance reasons.
Declaring Sealed Classes
To declare a sealed class in C#, you use the sealed
keyword:
public sealed class Logger
{
private static Logger instance;
// Private constructor to prevent instantiation
private Logger() { }
public static Logger Instance
{
get
{
if (instance == null)
{
instance = new Logger();
}
return instance;
}
}
public void Log(string message)
{
Debug.Log($"[Logger] {message}");
}
public void LogWarning(string message)
{
Debug.LogWarning($"[Logger] {message}");
}
public void LogError(string message)
{
Debug.LogError($"[Logger] {message}");
}
}
In this example, Logger
is a sealed class implementing the Singleton pattern. By making it sealed, we ensure that no one can create a derived class that might break the Singleton behavior.
What are Sealed Methods?
A sealed method is a method that cannot be overridden in derived classes. You can only seal a method that overrides a virtual method from a base class. This is useful when you want to provide a specific implementation of a virtual method and prevent further derived classes from changing that implementation.
Declaring Sealed Methods
To declare a sealed method in C#, you use the sealed
keyword along with the override
keyword:
public class Enemy
{
public virtual void Attack()
{
Debug.Log("Enemy attacks!");
}
}
public class Goblin : Enemy
{
public sealed override void Attack()
{
Debug.Log("Goblin attacks with a club!");
}
}
public class GoblinArcher : Goblin
{
// This would cause a compilation error
// public override void Attack()
// {
// Debug.Log("Goblin archer attacks with a bow!");
// }
}
In this example, the Goblin
class overrides the Attack()
method from the Enemy
class, but it seals it to prevent any derived classes (like GoblinArcher
) from further overriding it.
Why Use Sealed Classes and Methods?
There are several reasons to use sealed classes and methods:
1. Security
Sealing a class or method can prevent malicious or accidental misuse through inheritance. If you have a class that handles sensitive operations or maintains invariants that could be broken by derived classes, sealing it can help maintain security.
public sealed class SecurityManager
{
private string encryptionKey;
public SecurityManager(string key)
{
encryptionKey = key;
}
public string EncryptData(string data)
{
// Encryption implementation using encryptionKey
return "encrypted:" + data; // Simplified for example
}
public string DecryptData(string encryptedData)
{
// Decryption implementation using encryptionKey
return encryptedData.Replace("encrypted:", ""); // Simplified for example
}
}
By sealing this class, we prevent someone from creating a derived class that might expose the encryption key or modify the encryption/decryption algorithms in harmful ways.
2. Design Intent
Sealing a class or method clearly communicates your design intent: this class or method is not meant to be extended or overridden. It's a way of saying "this implementation is complete and should not be modified through inheritance."
public class Character
{
public virtual void Move() { /* ... */ }
public virtual void Attack() { /* ... */ }
}
public class Player : Character
{
public sealed override void Move()
{
// Player-specific movement implementation
// This is sealed because we don't want derived classes to change how the player moves
}
public override void Attack()
{
// Player-specific attack implementation
// This is not sealed, so derived classes can customize the attack
}
}
3. Performance Optimization
Sealed classes and methods can enable certain compiler optimizations. Since the compiler knows that a sealed class won't have any derived classes, it can perform optimizations like inlining method calls or devirtualizing virtual method calls.
public sealed class MathUtility
{
public static float CalculateDistance(Vector3 a, Vector3 b)
{
return Vector3.Distance(a, b);
}
public static float CalculateAngle(Vector3 from, Vector3 to)
{
return Vector3.Angle(from, to);
}
}
Since MathUtility
is sealed and its methods are static, the compiler can optimize calls to these methods more effectively.
4. Preventing Fragile Base Class Problem
The "fragile base class problem" occurs when changes to a base class can unintentionally break derived classes. By sealing methods that are critical to the correct functioning of your class, you can prevent derived classes from depending on implementation details that might change.
public class SaveSystem
{
public virtual void SaveGame()
{
PrepareData();
WriteToFile();
CleanUp();
}
protected virtual void PrepareData() { /* ... */ }
protected sealed void WriteToFile()
{
// Critical file writing logic that should not be overridden
// to prevent data corruption or security issues
}
protected virtual void CleanUp() { /* ... */ }
}
In this example, the WriteToFile()
method is sealed to prevent derived classes from changing the file writing logic, which could lead to data corruption or security issues.
Sealed Classes vs. Static Classes
Sealed classes and static classes are similar in that they both restrict inheritance, but they have different purposes and behaviors:
Feature | Sealed Class | Static Class |
---|---|---|
Instantiation | Can be instantiated | Cannot be instantiated |
Instance Members | Can have instance members | Cannot have instance members (only static) |
Inheritance | Cannot be inherited from | Cannot be inherited from |
Interfaces | Can implement interfaces | Cannot implement interfaces |
Constructor | Can have instance constructors | Can only have a static constructor |
When to use sealed classes:
- When you want to prevent inheritance but still need instance members
- When you need to implement interfaces
- When you need to create instances of the class
When to use static classes:
- When you want to group related static methods and properties
- When you don't need to create instances or maintain state
- When you're creating utility or helper classes
Sealed Classes in Unity
Unity doesn't use sealed classes extensively in its public API, but there are cases where sealing a class makes sense in Unity development:
1. Singleton Managers
Singleton managers are good candidates for sealed classes, as they should typically not be inherited from:
public sealed class GameManager : MonoBehaviour
{
private static GameManager instance;
public static GameManager Instance
{
get
{
if (instance == null)
{
GameObject gameManagerObject = new GameObject("GameManager");
instance = gameManagerObject.AddComponent<GameManager>();
DontDestroyOnLoad(gameManagerObject);
}
return instance;
}
}
private void Awake()
{
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(gameObject);
}
// Game management methods...
}
2. Utility Classes
Utility classes that provide helper methods are good candidates for sealed classes (or static classes if they don't need instance members):
public sealed class GameplayUtility
{
private static readonly int MaxPlayers = 4;
public static bool IsValidPlayerCount(int count)
{
return count > 0 && count <= MaxPlayers;
}
public static float CalculateDamage(int baseDamage, float multiplier, bool isCritical)
{
float damage = baseDamage * multiplier;
if (isCritical)
{
damage *= 2f;
}
return damage;
}
}
3. Final Implementations in a Class Hierarchy
When you have a deep class hierarchy and want to create a "final" implementation that shouldn't be extended further:
public abstract class Weapon : MonoBehaviour
{
public abstract void Fire();
}
public class Gun : Weapon
{
public override void Fire()
{
// Basic gun firing logic
}
}
public sealed class Shotgun : Gun
{
public override void Fire()
{
// Shotgun-specific firing logic
// This is sealed because we don't want any more specialized shotguns
}
}
Practical Examples
Example 1: Configuration System
// Base configuration class
public abstract class ConfigurationSection
{
protected string sectionName;
public ConfigurationSection(string name)
{
sectionName = name;
}
public abstract void Load();
public abstract void Save();
public virtual void Reset()
{
Debug.Log($"Resetting {sectionName} configuration");
// Default implementation
}
}
// Graphics configuration
public class GraphicsConfiguration : ConfigurationSection
{
private int resolution;
private int qualityLevel;
private bool fullscreen;
public GraphicsConfiguration() : base("Graphics")
{
// Default values
resolution = 1; // 0: Low, 1: Medium, 2: High
qualityLevel = 1; // 0: Low, 1: Medium, 2: High
fullscreen = true;
}
public override void Load()
{
Debug.Log("Loading graphics configuration");
resolution = PlayerPrefs.GetInt("Graphics_Resolution", 1);
qualityLevel = PlayerPrefs.GetInt("Graphics_Quality", 1);
fullscreen = PlayerPrefs.GetInt("Graphics_Fullscreen", 1) == 1;
}
public override void Save()
{
Debug.Log("Saving graphics configuration");
PlayerPrefs.SetInt("Graphics_Resolution", resolution);
PlayerPrefs.SetInt("Graphics_Quality", qualityLevel);
PlayerPrefs.SetInt("Graphics_Fullscreen", fullscreen ? 1 : 0);
PlayerPrefs.Save();
}
public sealed override void Reset()
{
Debug.Log("Resetting graphics configuration to defaults");
resolution = 1;
qualityLevel = 1;
fullscreen = true;
Save();
}
public void SetResolution(int value)
{
resolution = Mathf.Clamp(value, 0, 2);
}
public void SetQualityLevel(int value)
{
qualityLevel = Mathf.Clamp(value, 0, 2);
}
public void SetFullscreen(bool value)
{
fullscreen = value;
}
public void ApplySettings()
{
Debug.Log($"Applying graphics settings: Resolution={resolution}, Quality={qualityLevel}, Fullscreen={fullscreen}");
// Apply resolution
Resolution[] resolutions = Screen.resolutions;
int resIndex = 0;
switch (resolution)
{
case 0: // Low
resIndex = 0; // First (lowest) resolution
break;
case 1: // Medium
resIndex = resolutions.Length / 2; // Middle resolution
break;
case 2: // High
resIndex = resolutions.Length - 1; // Last (highest) resolution
break;
}
Resolution selectedResolution = resolutions[resIndex];
Screen.SetResolution(selectedResolution.width, selectedResolution.height, fullscreen);
// Apply quality level
QualitySettings.SetQualityLevel(qualityLevel);
}
}
// Audio configuration
public sealed class AudioConfiguration : ConfigurationSection
{
private float masterVolume;
private float musicVolume;
private float sfxVolume;
public AudioConfiguration() : base("Audio")
{
// Default values
masterVolume = 1.0f;
musicVolume = 0.8f;
sfxVolume = 1.0f;
}
public override void Load()
{
Debug.Log("Loading audio configuration");
masterVolume = PlayerPrefs.GetFloat("Audio_Master", 1.0f);
musicVolume = PlayerPrefs.GetFloat("Audio_Music", 0.8f);
sfxVolume = PlayerPrefs.GetFloat("Audio_SFX", 1.0f);
}
public override void Save()
{
Debug.Log("Saving audio configuration");
PlayerPrefs.SetFloat("Audio_Master", masterVolume);
PlayerPrefs.SetFloat("Audio_Music", musicVolume);
PlayerPrefs.SetFloat("Audio_SFX", sfxVolume);
PlayerPrefs.Save();
}
public override void Reset()
{
Debug.Log("Resetting audio configuration to defaults");
masterVolume = 1.0f;
musicVolume = 0.8f;
sfxVolume = 1.0f;
Save();
}
public void SetMasterVolume(float value)
{
masterVolume = Mathf.Clamp01(value);
}
public void SetMusicVolume(float value)
{
musicVolume = Mathf.Clamp01(value);
}
public void SetSFXVolume(float value)
{
sfxVolume = Mathf.Clamp01(value);
}
public void ApplySettings()
{
Debug.Log($"Applying audio settings: Master={masterVolume}, Music={musicVolume}, SFX={sfxVolume}");
// Find and set volume on audio sources
AudioSource[] audioSources = FindObjectsOfType<AudioSource>();
foreach (AudioSource source in audioSources)
{
if (source.CompareTag("Music"))
{
source.volume = musicVolume * masterVolume;
}
else
{
source.volume = sfxVolume * masterVolume;
}
}
}
}
// Configuration manager
public sealed class ConfigurationManager : MonoBehaviour
{
private static ConfigurationManager instance;
public static ConfigurationManager Instance
{
get
{
if (instance == null)
{
GameObject configObject = new GameObject("ConfigurationManager");
instance = configObject.AddComponent<ConfigurationManager>();
DontDestroyOnLoad(configObject);
}
return instance;
}
}
private GraphicsConfiguration graphicsConfig;
private AudioConfiguration audioConfig;
private void Awake()
{
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(gameObject);
// Initialize configurations
graphicsConfig = new GraphicsConfiguration();
audioConfig = new AudioConfiguration();
// Load saved settings
LoadAllSettings();
}
public void LoadAllSettings()
{
graphicsConfig.Load();
audioConfig.Load();
}
public void SaveAllSettings()
{
graphicsConfig.Save();
audioConfig.Save();
}
public void ApplyAllSettings()
{
graphicsConfig.ApplySettings();
audioConfig.ApplySettings();
}
public void ResetAllSettings()
{
graphicsConfig.Reset();
audioConfig.Reset();
ApplyAllSettings();
}
public GraphicsConfiguration GetGraphicsConfig()
{
return graphicsConfig;
}
public AudioConfiguration GetAudioConfig()
{
return audioConfig;
}
}
// Usage example
public class SettingsMenu : MonoBehaviour
{
[SerializeField] private Slider masterVolumeSlider;
[SerializeField] private Slider musicVolumeSlider;
[SerializeField] private Slider sfxVolumeSlider;
[SerializeField] private Dropdown resolutionDropdown;
[SerializeField] private Dropdown qualityDropdown;
[SerializeField] private Toggle fullscreenToggle;
private void Start()
{
// Set up UI elements
SetupAudioUI();
SetupGraphicsUI();
}
private void SetupAudioUI()
{
AudioConfiguration audioConfig = ConfigurationManager.Instance.GetAudioConfig();
// Set up sliders
masterVolumeSlider.value = PlayerPrefs.GetFloat("Audio_Master", 1.0f);
musicVolumeSlider.value = PlayerPrefs.GetFloat("Audio_Music", 0.8f);
sfxVolumeSlider.value = PlayerPrefs.GetFloat("Audio_SFX", 1.0f);
// Add listeners
masterVolumeSlider.onValueChanged.AddListener((value) => {
audioConfig.SetMasterVolume(value);
audioConfig.ApplySettings();
});
musicVolumeSlider.onValueChanged.AddListener((value) => {
audioConfig.SetMusicVolume(value);
audioConfig.ApplySettings();
});
sfxVolumeSlider.onValueChanged.AddListener((value) => {
audioConfig.SetSFXVolume(value);
audioConfig.ApplySettings();
});
}
private void SetupGraphicsUI()
{
GraphicsConfiguration graphicsConfig = ConfigurationManager.Instance.GetGraphicsConfig();
// Set up dropdowns and toggle
resolutionDropdown.value = PlayerPrefs.GetInt("Graphics_Resolution", 1);
qualityDropdown.value = PlayerPrefs.GetInt("Graphics_Quality", 1);
fullscreenToggle.isOn = PlayerPrefs.GetInt("Graphics_Fullscreen", 1) == 1;
// Add listeners
resolutionDropdown.onValueChanged.AddListener((value) => {
graphicsConfig.SetResolution(value);
});
qualityDropdown.onValueChanged.AddListener((value) => {
graphicsConfig.SetQualityLevel(value);
});
fullscreenToggle.onValueChanged.AddListener((value) => {
graphicsConfig.SetFullscreen(value);
});
}
public void ApplySettings()
{
ConfigurationManager.Instance.ApplyAllSettings();
ConfigurationManager.Instance.SaveAllSettings();
}
public void ResetSettings()
{
ConfigurationManager.Instance.ResetAllSettings();
// Update UI to reflect reset values
SetupAudioUI();
SetupGraphicsUI();
}
}
In this example, we have a configuration system with an abstract base class ConfigurationSection
and two derived classes: GraphicsConfiguration
and AudioConfiguration
. The AudioConfiguration
class is sealed to prevent further inheritance, as it represents a complete implementation that shouldn't be extended.
The GraphicsConfiguration
class overrides the Reset()
method and seals it, indicating that derived classes (if there were any) should not be able to change how graphics settings are reset.
The ConfigurationManager
class is also sealed, as it's a singleton that shouldn't be inherited from.
Example 2: Input System
// Input action interface
public interface IInputAction
{
void Execute(GameObject target);
string GetDescription();
}
// Base input handler
public abstract class InputHandler : MonoBehaviour
{
protected Dictionary<KeyCode, IInputAction> keyBindings = new Dictionary<KeyCode, IInputAction>();
protected virtual void Update()
{
CheckInput();
}
protected virtual void CheckInput()
{
foreach (var binding in keyBindings)
{
if (Input.GetKeyDown(binding.Key))
{
binding.Value.Execute(gameObject);
}
}
}
public abstract void SetupDefaultBindings();
public virtual void BindAction(KeyCode key, IInputAction action)
{
keyBindings[key] = action;
Debug.Log($"Bound {action.GetDescription()} to {key}");
}
public virtual void UnbindAction(KeyCode key)
{
if (keyBindings.ContainsKey(key))
{
Debug.Log($"Unbound {keyBindings[key].GetDescription()} from {key}");
keyBindings.Remove(key);
}
}
public virtual void ClearAllBindings()
{
keyBindings.Clear();
Debug.Log("Cleared all key bindings");
}
}
// Player input handler
public sealed class PlayerInputHandler : InputHandler
{
[SerializeField] private PlayerController playerController;
protected override void Awake()
{
if (playerController == null)
{
playerController = GetComponent<PlayerController>();
}
SetupDefaultBindings();
}
public override void SetupDefaultBindings()
{
ClearAllBindings();
// Movement
BindAction(KeyCode.W, new MoveAction(Vector3.forward));
BindAction(KeyCode.S, new MoveAction(Vector3.back));
BindAction(KeyCode.A, new MoveAction(Vector3.left));
BindAction(KeyCode.D, new MoveAction(Vector3.right));
// Actions
BindAction(KeyCode.Space, new JumpAction());
BindAction(KeyCode.E, new InteractAction());
BindAction(KeyCode.Q, new SwitchWeaponAction());
BindAction(KeyCode.R, new ReloadAction());
BindAction(KeyCode.F, new UseItemAction());
// Combat
BindAction(KeyCode.Mouse0, new AttackAction());
BindAction(KeyCode.Mouse1, new AimAction());
}
// This method is sealed to prevent derived classes from changing how input is checked
protected sealed override void CheckInput()
{
// First check for key bindings
base.CheckInput();
// Then check for continuous input for movement
Vector3 moveDirection = Vector3.zero;
if (Input.GetKey(KeyCode.W)) moveDirection += Vector3.forward;
if (Input.GetKey(KeyCode.S)) moveDirection += Vector3.back;
if (Input.GetKey(KeyCode.A)) moveDirection += Vector3.left;
if (Input.GetKey(KeyCode.D)) moveDirection += Vector3.right;
if (moveDirection != Vector3.zero && playerController != null)
{
playerController.Move(moveDirection.normalized);
}
// Check for continuous aiming
if (Input.GetKey(KeyCode.Mouse1) && playerController != null)
{
playerController.Aim(true);
}
else if (playerController != null)
{
playerController.Aim(false);
}
}
}
// Enemy input handler
public sealed class EnemyInputHandler : InputHandler
{
[SerializeField] private EnemyAI enemyAI;
protected override void Awake()
{
if (enemyAI == null)
{
enemyAI = GetComponent<EnemyAI>();
}
SetupDefaultBindings();
}
public override void SetupDefaultBindings()
{
// Enemies don't use key bindings, they use AI
ClearAllBindings();
}
// Override and seal the Update method to use AI instead of key input
protected sealed override void Update()
{
// Don't call base.Update() because we don't want to check for key input
// Instead, the EnemyAI component handles the enemy's behavior
}
}
// Input actions
public sealed class MoveAction : IInputAction
{
private Vector3 direction;
public MoveAction(Vector3 direction)
{
this.direction = direction;
}
public void Execute(GameObject target)
{
PlayerController controller = target.GetComponent<PlayerController>();
if (controller != null)
{
controller.Move(direction);
}
}
public string GetDescription()
{
return $"Move {direction}";
}
}
public sealed class JumpAction : IInputAction
{
public void Execute(GameObject target)
{
PlayerController controller = target.GetComponent<PlayerController>();
if (controller != null)
{
controller.Jump();
}
}
public string GetDescription()
{
return "Jump";
}
}
public sealed class InteractAction : IInputAction
{
public void Execute(GameObject target)
{
PlayerController controller = target.GetComponent<PlayerController>();
if (controller != null)
{
controller.Interact();
}
}
public string GetDescription()
{
return "Interact";
}
}
public sealed class SwitchWeaponAction : IInputAction
{
public void Execute(GameObject target)
{
PlayerController controller = target.GetComponent<PlayerController>();
if (controller != null)
{
controller.SwitchWeapon();
}
}
public string GetDescription()
{
return "Switch Weapon";
}
}
public sealed class ReloadAction : IInputAction
{
public void Execute(GameObject target)
{
PlayerController controller = target.GetComponent<PlayerController>();
if (controller != null)
{
controller.Reload();
}
}
public string GetDescription()
{
return "Reload";
}
}
public sealed class UseItemAction : IInputAction
{
public void Execute(GameObject target)
{
PlayerController controller = target.GetComponent<PlayerController>();
if (controller != null)
{
controller.UseItem();
}
}
public string GetDescription()
{
return "Use Item";
}
}
public sealed class AttackAction : IInputAction
{
public void Execute(GameObject target)
{
PlayerController controller = target.GetComponent<PlayerController>();
if (controller != null)
{
controller.Attack();
}
}
public string GetDescription()
{
return "Attack";
}
}
public sealed class AimAction : IInputAction
{
public void Execute(GameObject target)
{
PlayerController controller = target.GetComponent<PlayerController>();
if (controller != null)
{
controller.Aim(true);
}
}
public string GetDescription()
{
return "Aim";
}
}
// Simple player controller
public class PlayerController : MonoBehaviour
{
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private float jumpForce = 5f;
private Rigidbody rb;
private bool isGrounded;
private bool isAiming;
private void Awake()
{
rb = GetComponent<Rigidbody>();
}
public void Move(Vector3 direction)
{
// Apply movement in the specified direction
Vector3 movement = direction * moveSpeed * Time.deltaTime;
transform.position += movement;
}
public void Jump()
{
if (isGrounded)
{
rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
isGrounded = false;
Debug.Log("Player jumped");
}
}
public void Interact()
{
Debug.Log("Player interacting");
// Raycast to find interactable objects
Ray ray = new Ray(transform.position, transform.forward);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 2f))
{
IInteractable interactable = hit.collider.GetComponent<IInteractable>();
if (interactable != null)
{
interactable.Interact(this);
}
}
}
public void SwitchWeapon()
{
Debug.Log("Player switching weapon");
// Implementation...
}
public void Reload()
{
Debug.Log("Player reloading");
// Implementation...
}
public void UseItem()
{
Debug.Log("Player using item");
// Implementation...
}
public void Attack()
{
Debug.Log("Player attacking");
// Implementation...
}
public void Aim(bool aiming)
{
isAiming = aiming;
Debug.Log($"Player aiming: {isAiming}");
// Implementation...
}
private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("Ground"))
{
isGrounded = true;
}
}
}
// Simple enemy AI
public class EnemyAI : MonoBehaviour
{
[SerializeField] private float moveSpeed = 3f;
[SerializeField] private float detectionRange = 10f;
[SerializeField] private float attackRange = 2f;
private Transform player;
private bool isAttacking;
private void Start()
{
player = GameObject.FindGameObjectWithTag("Player")?.transform;
}
private void Update()
{
if (player == null) return;
float distanceToPlayer = Vector3.Distance(transform.position, player.position);
if (distanceToPlayer <= detectionRange)
{
if (distanceToPlayer <= attackRange)
{
Attack();
}
else
{
MoveTowardsPlayer();
}
}
}
private void MoveTowardsPlayer()
{
Vector3 direction = (player.position - transform.position).normalized;
transform.position += direction * moveSpeed * Time.deltaTime;
transform.LookAt(player);
}
private void Attack()
{
if (!isAttacking)
{
isAttacking = true;
Debug.Log("Enemy attacking");
// Implement attack logic...
// Reset attack flag after a delay
StartCoroutine(ResetAttackFlag());
}
}
private IEnumerator ResetAttackFlag()
{
yield return new WaitForSeconds(1f);
isAttacking = false;
}
}
// Interactable interface
public interface IInteractable
{
void Interact(PlayerController player);
}
In this example, we have an input system with an abstract base class InputHandler
and two sealed derived classes: PlayerInputHandler
and EnemyInputHandler
. These classes are sealed because they represent complete implementations for specific entity types.
The PlayerInputHandler
class overrides the CheckInput()
method and seals it, indicating that derived classes (if there were any) should not be able to change how input is checked.
All the input actions are sealed classes, as they represent simple, complete implementations that shouldn't be extended.
Example 3: Achievement System
// Achievement interface
public interface IAchievement
{
string ID { get; }
string Title { get; }
string Description { get; }
bool IsUnlocked { get; }
int ProgressCurrent { get; }
int ProgressRequired { get; }
float ProgressPercentage { get; }
void UpdateProgress(int amount);
void Unlock();
}
// Base achievement class
public abstract class Achievement : IAchievement
{
public string ID { get; protected set; }
public string Title { get; protected set; }
public string Description { get; protected set; }
public bool IsUnlocked { get; protected set; }
public int ProgressCurrent { get; protected set; }
public int ProgressRequired { get; protected set; }
public float ProgressPercentage => (float)ProgressCurrent / ProgressRequired;
protected Achievement(string id, string title, string description, int progressRequired)
{
ID = id;
Title = title;
Description = description;
ProgressRequired = progressRequired;
ProgressCurrent = 0;
IsUnlocked = false;
}
public virtual void UpdateProgress(int amount)
{
if (IsUnlocked) return;
ProgressCurrent += amount;
Debug.Log($"Achievement '{Title}' progress: {ProgressCurrent}/{ProgressRequired} ({ProgressPercentage:P0})");
if (ProgressCurrent >= ProgressRequired)
{
Unlock();
}
}
public virtual void Unlock()
{
if (IsUnlocked) return;
IsUnlocked = true;
ProgressCurrent = ProgressRequired;
Debug.Log($"Achievement unlocked: {Title}");
// Notify achievement manager
AchievementManager.Instance.OnAchievementUnlocked(this);
}
}
// Specific achievement types
public sealed class KillEnemiesAchievement : Achievement
{
public KillEnemiesAchievement(string id, string title, string description, int enemiesRequired)
: base(id, title, description, enemiesRequired)
{
}
public void EnemyKilled()
{
UpdateProgress(1);
}
}
public sealed class CollectItemsAchievement : Achievement
{
public CollectItemsAchievement(string id, string title, string description, int itemsRequired)
: base(id, title, description, itemsRequired)
{
}
public void ItemCollected()
{
UpdateProgress(1);
}
}
public sealed class CompleteLevelAchievement : Achievement
{
private string levelName;
public CompleteLevelAchievement(string id, string title, string description, string levelName)
: base(id, title, description, 1)
{
this.levelName = levelName;
}
public void LevelCompleted(string completedLevel)
{
if (completedLevel == levelName)
{
Unlock();
}
}
}
public sealed class SpeedRunAchievement : Achievement
{
private string levelName;
private float timeLimit;
public SpeedRunAchievement(string id, string title, string description, string levelName, float timeLimit)
: base(id, title, description, 1)
{
this.levelName = levelName;
this.timeLimit = timeLimit;
}
public void LevelCompleted(string completedLevel, float completionTime)
{
if (completedLevel == levelName && completionTime <= timeLimit)
{
Unlock();
}
}
}
// Achievement manager
public sealed class AchievementManager : MonoBehaviour
{
private static AchievementManager instance;
public static AchievementManager Instance
{
get
{
if (instance == null)
{
GameObject achievementObject = new GameObject("AchievementManager");
instance = achievementObject.AddComponent<AchievementManager>();
DontDestroyOnLoad(achievementObject);
}
return instance;
}
}
private Dictionary<string, IAchievement> achievements = new Dictionary<string, IAchievement>();
private void Awake()
{
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(gameObject);
// Initialize achievements
InitializeAchievements();
}
private void InitializeAchievements()
{
// Kill enemies achievements
RegisterAchievement(new KillEnemiesAchievement(
"KILL_10",
"Novice Hunter",
"Kill 10 enemies",
10
));
RegisterAchievement(new KillEnemiesAchievement(
"KILL_50",
"Experienced Hunter",
"Kill 50 enemies",
50
));
RegisterAchievement(new KillEnemiesAchievement(
"KILL_100",
"Master Hunter",
"Kill 100 enemies",
100
));
// Collect items achievements
RegisterAchievement(new CollectItemsAchievement(
"COLLECT_10",
"Collector",
"Collect 10 items",
10
));
RegisterAchievement(new CollectItemsAchievement(
"COLLECT_50",
"Hoarder",
"Collect 50 items",
50
));
// Level completion achievements
RegisterAchievement(new CompleteLevelAchievement(
"COMPLETE_LEVEL_1",
"First Steps",
"Complete Level 1",
"Level1"
));
RegisterAchievement(new CompleteLevelAchievement(
"COMPLETE_LEVEL_2",
"Moving Forward",
"Complete Level 2",
"Level2"
));
// Speed run achievements
RegisterAchievement(new SpeedRunAchievement(
"SPEED_LEVEL_1",
"Speed Demon",
"Complete Level 1 in under 2 minutes",
"Level1",
120f
));
}
private void RegisterAchievement(IAchievement achievement)
{
if (!achievements.ContainsKey(achievement.ID))
{
achievements.Add(achievement.ID, achievement);
Debug.Log($"Registered achievement: {achievement.Title}");
}
else
{
Debug.LogWarning($"Achievement with ID {achievement.ID} already exists!");
}
}
public IAchievement GetAchievement(string id)
{
if (achievements.TryGetValue(id, out IAchievement achievement))
{
return achievement;
}
Debug.LogWarning($"Achievement with ID {id} not found!");
return null;
}
public List<IAchievement> GetAllAchievements()
{
return new List<IAchievement>(achievements.Values);
}
public List<IAchievement> GetUnlockedAchievements()
{
return achievements.Values.Where(a => a.IsUnlocked).ToList();
}
public List<IAchievement> GetLockedAchievements()
{
return achievements.Values.Where(a => !a.IsUnlocked).ToList();
}
public void OnAchievementUnlocked(IAchievement achievement)
{
// Show notification
UIManager.Instance.ShowAchievementNotification(achievement);
// Save achievement progress
SaveAchievements();
}
public void EnemyKilled()
{
foreach (IAchievement achievement in achievements.Values)
{
KillEnemiesAchievement killAchievement = achievement as KillEnemiesAchievement;
if (killAchievement != null)
{
killAchievement.EnemyKilled();
}
}
}
public void ItemCollected()
{
foreach (IAchievement achievement in achievements.Values)
{
CollectItemsAchievement collectAchievement = achievement as CollectItemsAchievement;
if (collectAchievement != null)
{
collectAchievement.ItemCollected();
}
}
}
public void LevelCompleted(string levelName, float completionTime)
{
foreach (IAchievement achievement in achievements.Values)
{
CompleteLevelAchievement levelAchievement = achievement as CompleteLevelAchievement;
if (levelAchievement != null)
{
levelAchievement.LevelCompleted(levelName);
}
SpeedRunAchievement speedAchievement = achievement as SpeedRunAchievement;
if (speedAchievement != null)
{
speedAchievement.LevelCompleted(levelName, completionTime);
}
}
}
private void SaveAchievements()
{
// Save achievement progress to PlayerPrefs
foreach (IAchievement achievement in achievements.Values)
{
PlayerPrefs.SetInt($"Achievement_{achievement.ID}_Unlocked", achievement.IsUnlocked ? 1 : 0);
PlayerPrefs.SetInt($"Achievement_{achievement.ID}_Progress", achievement.ProgressCurrent);
}
PlayerPrefs.Save();
Debug.Log("Achievements saved");
}
private void LoadAchievements()
{
// Load achievement progress from PlayerPrefs
foreach (IAchievement achievement in achievements.Values)
{
bool isUnlocked = PlayerPrefs.GetInt($"Achievement_{achievement.ID}_Unlocked", 0) == 1;
int progress = PlayerPrefs.GetInt($"Achievement_{achievement.ID}_Progress", 0);
// Use reflection to set private fields (not ideal, but works for this example)
Achievement achievementBase = achievement as Achievement;
if (achievementBase != null)
{
typeof(Achievement).GetField("IsUnlocked", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)
?.SetValue(achievementBase, isUnlocked);
typeof(Achievement).GetField("ProgressCurrent", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)
?.SetValue(achievementBase, progress);
}
}
Debug.Log("Achievements loaded");
}
}
// Simple UI manager
public sealed class UIManager : MonoBehaviour
{
private static UIManager instance;
public static UIManager Instance
{
get
{
if (instance == null)
{
GameObject uiObject = new GameObject("UIManager");
instance = uiObject.AddComponent<UIManager>();
DontDestroyOnLoad(uiObject);
}
return instance;
}
}
[SerializeField] private GameObject achievementNotificationPrefab;
[SerializeField] private Transform notificationParent;
private void Awake()
{
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(gameObject);
}
public void ShowAchievementNotification(IAchievement achievement)
{
Debug.Log($"Showing achievement notification: {achievement.Title}");
// In a real game, you would instantiate a UI notification here
if (achievementNotificationPrefab != null && notificationParent != null)
{
GameObject notification = Instantiate(achievementNotificationPrefab, notificationParent);
AchievementNotification notificationComponent = notification.GetComponent<AchievementNotification>();
if (notificationComponent != null)
{
notificationComponent.SetAchievement(achievement);
}
}
}
}
// Achievement notification UI
public sealed class AchievementNotification : MonoBehaviour
{
[SerializeField] private Text titleText;
[SerializeField] private Text descriptionText;
[SerializeField] private float displayTime = 3f;
public void SetAchievement(IAchievement achievement)
{
if (titleText != null)
{
titleText.text = achievement.Title;
}
if (descriptionText != null)
{
descriptionText.text = achievement.Description;
}
// Destroy after display time
Destroy(gameObject, displayTime);
}
}
In this example, we have an achievement system with an abstract base class Achievement
and several sealed derived classes for specific achievement types: KillEnemiesAchievement
, CollectItemsAchievement
, CompleteLevelAchievement
, and SpeedRunAchievement
. These classes are sealed because they represent complete implementations for specific achievement types.
The AchievementManager
class is also sealed, as it's a singleton that shouldn't be inherited from.
Best Practices for Sealed Classes and Methods
-
Use Sealed Classes for Complete Implementations: Use the
sealed
keyword for classes that represent complete implementations that shouldn't be extended. -
Use Sealed Methods to Prevent Overriding: Use the
sealed
keyword for methods that provide a specific implementation that shouldn't be changed by derived classes. -
Seal Security-Critical Classes and Methods: Seal classes and methods that handle security-critical operations to prevent malicious or accidental misuse.
-
Consider Sealing Singletons: Singleton classes are good candidates for sealing, as they typically shouldn't be inherited from.
-
Document Why a Class or Method is Sealed: Clearly document the reason for sealing a class or method, so other developers understand your design intent.
-
Use Sealed Classes for Performance-Critical Code: Consider sealing classes that are performance-critical, as it can enable certain compiler optimizations.
-
Seal Methods That Maintain Invariants: If a method is responsible for maintaining class invariants, consider sealing it to prevent derived classes from breaking those invariants.
-
Balance Extensibility and Security: Carefully balance the need for extensibility through inheritance with the need for security and robustness.
-
Consider Alternatives to Inheritance: Before sealing a class, consider whether composition or interfaces might provide a better design than inheritance.
-
Don't Overuse Sealing: Don't seal classes or methods without a good reason, as it can unnecessarily restrict code reuse and extension.
Conclusion
Sealed classes and methods are powerful tools for designing robust class hierarchies in C#. They allow you to:
- Prevent inheritance and method overriding
- Communicate design intent
- Improve security by preventing malicious or accidental misuse
- Enable certain compiler optimizations
- Prevent the fragile base class problem
In Unity, sealed classes are particularly useful for singletons, utility classes, and final implementations in a class hierarchy. By using sealed classes and methods effectively, you can create more secure, maintainable, and performant code for your games.
In the next section, we'll explore interfaces, which provide a way to define contracts that classes must implement without specifying how they should be implemented.
Practice Exercise
Exercise: Design a simple game state system with the following requirements:
-
Create an abstract
GameState
class with:- Methods for entering, updating, and exiting the state
- Properties for the state name and description
-
Create at least three derived game state classes (e.g.,
MainMenuState
,GameplayState
,PauseState
) that:- Implement the abstract methods
- Add any state-specific functionality
-
Create a sealed
GameStateManager
class that:- Manages the current game state
- Provides methods for changing states
- Ensures only one instance exists (Singleton pattern)
-
Seal at least one of the derived game state classes and explain why you chose to seal it.
Think about how sealed classes help you enforce design constraints and prevent inappropriate inheritance in your game state system.