from Singleton to Object Pool
Introduction
Design patterns are time-tested solutions to common problems that arise in software development. They help developers create more structured, understandable, and maintainable code. In this article, we'll look at a few key design patterns that can significantly improve the quality of your code in Unity. Let's start with one of the most well-known and commonly used patterns: Singleton.
Singleton
What is Singleton and why is it needed?
The Singleton pattern limits the instantiation of a class to a single object and provides a global point of access to that object. In the context of Unity, Singleton is often used for managers that control various aspects of the game, such as managing scenes, sounds, or game settings.
Example of using Singleton in Unity
Let's look at an example of creating a Singleton for a game manager (GameManager), which will manage the game state and provide access to shared resources.
Step 1: Create the GameManager class
Let's create a new GameManager script and add the following code to it:
using UnityEngine;
public class GameManager : MonoBehaviour
{
// Статическая переменная для хранения единственного экземпляра
private static GameManager _instance;
// Публичное статическое свойство для доступа к экземпляру
public static GameManager Instance
{
get
{
// Если экземпляр не существует, создаем его
if (_instance == null)
{
// Создаем новый объект и добавляем к нему компонент GameManager
_instance = new GameObject("GameManager").AddComponent<GameManager>();
}
return _instance;
}
}
// Метод Awake вызывается при инициализации объекта
private void Awake()
{
// Проверяем, существует ли уже экземпляр
if (_instance == null)
{
// Если экземпляр не существует, назначаем текущий объект и не уничтожаем его при загрузке новой сцены
_instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
// Если экземпляр уже существует, уничтожаем текущий объект, чтобы сохранить единственность
Destroy(gameObject);
}
}
// Пример метода для управления состоянием игры
public void StartGame()
{
// Логика старта игры
Debug.Log("Game Started");
}
}
Step 2: Using Singleton
Now that we have the GameManager class, let's see how we can use it in other scripts. For example, let's start a game using the StartGame method.
using UnityEngine;
public class GameController : MonoBehaviour
{
private void Start()
{
// Доступ к методу StartGame через Singleton
GameManager.Instance.StartGame();
}
}
Advantages and Disadvantages of the Singleton Pattern
Advantages:
1. Global hotspot: Singleton provides easy access to its methods and data from any part of the application.
2. Uniqueness of the copy: It is guaranteed that only one instance will be created, which prevents possible conflicts and errors.
Flaws:
1. Violation of the dependency inversion principle: Singleton creates a tight dependency between classes, which can make code difficult to test and extend.
2. Multithreading issues: In multithreaded applications, additional synchronization is required to ensure safe access to Singleton.
Conclusion
Singleton is a powerful tool that, when used correctly, can greatly simplify application state management. However, it is important to be aware of possible pitfalls and try to use this pattern only where it is really necessary. In the next part of the article, we will look at the Observer pattern, which will help you create more flexible and modular systems.
Observer
What is Observer and why is it needed?
The Observer pattern, also known as an “Observer,” defines a one-to-many dependency between objects, where a change in the state of one object (the observable) notifies and updates all objects related to it (the observers). In the context of Unity, an Observer is useful for managing events such as changes in the player’s health, UI updates, or level completion notifications.
Example of using Observer in Unity
Let's look at an example of implementing an event system using Observer to track changes in the player's health.
Step 1: Create a Health class
Let's create a new Health script that will contain the health management logic and events to notify observers.
using UnityEngine;
public class Health : MonoBehaviour
{
// Делегат для события изменения здоровья
public delegate void HealthChanged(int currentHealth);
public event HealthChanged OnHealthChanged;
// Приватное поле здоровья
private int _health;
public int HealthValue
{
get { return health; }
set
{
_health = value;
// Вызов события при изменении здоровья
OnHealthChanged?.Invoke(health);
}
}
// Метод для нанесения урона
public void TakeDamage(int damage)
{
HealthValue -= damage;
if (HealthValue < 0)
{
HealthValue = 0;
}
}
}
Step 2: Create a class to display health (HealthDisplay)
Let's create a new HealthDisplay script that will subscribe to health change events and update the UI.
using UnityEngine;
using UnityEngine.UI;
public class HealthDisplay : MonoBehaviour
{
[SerializeField] private Health _playerHealth; // Ссылка на объект здоровья игрока
[SerializeField] private Text _healthText; // UI элемент для отображения здоровья
private void OnEnable()
{
// Подписка на событие изменения здоровья
playerHealth.OnHealthChanged += UpdateHealthDisplay;
}
private void OnDisable()
{
// Отписка от события изменения здоровья
playerHealth.OnHealthChanged -= UpdateHealthDisplay;
}
// Метод для обновления UI при изменении здоровья
private void UpdateHealthDisplay(int currentHealth)
{
_healthText.text = $"Health: {_currentHealth}";
}
}
Step 3: Connecting the logic in Unity
Let's create a game object with a Health component and another object with a HealthDisplay component, connecting them through the inspector.
Advantages and Disadvantages of the Observer Pattern
Advantages:
1. Weak coupling: Observers do not directly depend on the object being observed, making it easy to add new features and components.
2. Scalability: Easily add new observers without changing the existing code of the observable object.
3. Flexibility: Allows you to implement reactive systems where components automatically respond to state changes.
Flaws:
1. Memory leaks: It is necessary to properly manage event subscriptions and unsubscriptions to avoid memory leaks.
2. Difficulty of debugging: It is difficult to track the sequence of events and cause and effect relationships, especially in complex systems.
Conclusion
Observer is a powerful tool for creating reactive systems in Unity. It allows you to easily manage events and interactions between components, providing flexibility and scalability of your code. In the next part of the article, we will look at the Factory Method pattern, which will help you centrally manage the creation of objects in your game.
Factory Method
What is Factory Method and why is it needed?
The Factory Method pattern provides an interface for creating objects, but allows subclasses to change the type of objects being created. This is useful when the system needs to create objects of different types, but it is not known in advance what type of object will be required. In Unity, this pattern is often used to create various game objects, such as enemies, NPCs, items, etc.
Example of using Factory Method in Unity
Let's look at an example of creating a factory to generate enemies in the game.
Step 1: Create an abstract Enemy class
Let's create an abstract class Enemy, which will be the base for all enemy types.
public abstract class Enemy
{
public abstract void Attack();
}
Step 2: Create Specific Enemy Classes
Let's create classes Orc and Troll that inherit from Enemy and implement the Attack method.
public class Orc : Enemy
{
public override void Attack()
{
// Реализация атаки орка
Debug.Log("Orc attacks!");
}
}
public class Troll : Enemy
{
public override void Attack()
{
// Реализация атаки тролля
Debug.Log("Troll attacks!");
}
}
public enum EnemyType
{
Orc,
Troll
}
Step 3: Create a factory for enemies
Let's create the EnemyFactory class, which will be responsible for creating enemies.
public class EnemyFactory
{
public Enemy CreateEnemy(EnemyType type)
{
switch (type)
{
case EnemyType.Orc:
return new Orc();
case EnemyType.Troll:
return new Troll();
default:
throw new ArgumentException("Unknown enemy type");
}
}
}
Step 4: Using the Factory in the Game
Now that we have an enemy factory, let's look at how it can be used to create enemies in the game.
using UnityEngine;
public class GameController : MonoBehaviour
{
private EnemyFactory _enemyFactory;
private void Start()
{
_enemyFactory = new EnemyFactory();
// Создание орка
Enemy orc = _enemyFactory.CreateEnemy(EnemyType.Orc);
orc.Attack();
// Создание тролля
Enemy troll = _enemyFactory.CreateEnemy(EnemyType.Troll);
troll.Attack();
}
}
Advantages and Disadvantages of the Factory Method Pattern
Advantages:
1. Centralized creation of objects: Simplifies the management of the object creation process and provides a centralized place to configure them.
2. Extensibility: Easily add new object types without changing existing factory code.
3. Decreasing dependence: Factory clients do not depend on the specific classes of objects they create, which improves the modularity and testability of the code.
Flaws:
1. Increase in code complexity: Introducing additional classes and methods can complicate the code structure.
2. Need to change factory when adding new object types: While adding new object types is simplified, it still requires changing the factory code.
Conclusion
Factory Method is a powerful pattern that allows you to centrally manage the creation of objects in your game. It improves the modularity and extensibility of your code, making the process of creating objects more flexible and manageable. In the next part of the article, we will look at the Object Pool pattern, which will help you effectively manage resources and improve the performance of your game.
Object Pool
What is Object Pool and why is it needed?
The Object Pool pattern provides a mechanism for reusing objects instead of constantly creating and destroying them. This pattern is especially useful in games where many similar objects are created and destroyed, such as bullets, enemies, or particle effects. Object pooling helps reduce the load on the garbage collector and improves overall game performance.
Example of using Object Pool in Unity
Let's look at an example of creating a pool of objects to control bullets in a shooter.
Step 1: Create the Bullet class
Let's create a Bullet class that will represent a bullet.
using UnityEngine;
public class Bullet : MonoBehaviour
{
private void OnEnable()
{
// Активируем пулю
Invoke(nameof(Deactivate), 2f); // Деактивируем пулю через 2 секунды
}
private void Deactivate()
{
gameObject.SetActive(false); // Деактивируем объект, возвращая его в пул
}
private void OnDisable()
{
CancelInvoke(); // Отменяем все запланированные вызовы
}
}
Step 2: Create a BulletPool object pool
Let's create a BulletPool class that will manage the pool of objects.
using UnityEngine;
using System.Collections.Generic;
public class BulletPool : MonoBehaviour
{
[SerializeField] private GameObject _bulletPrefab; // Префаб пули
private Queue<Bullet> _bulletPool = new Queue<Bullet>();
public static BulletPool Instance { get; private set; }
private void Awake()
{
Instance = this;
}
public Bullet GetBullet()
{
if (bulletPool.Count > 0)
{
Bullet bullet = _bulletPool.Dequeue();
bullet.gameObject.SetActive(true);
return bullet;
}
else
{
Bullet newBullet = Instantiate(_bulletPrefab).GetComponent<Bullet>();
return newBullet;
}
}
public void ReturnBullet(Bullet bullet)
{
bullet.gameObject.SetActive(false);
bulletPool.Enqueue(bullet);
}
}
Step 3: Using the Object Pool in the Game
Now that we have an object pool, let's look at how to use it to fire bullets.
using UnityEngine;
public class PlayerController : MonoBehaviour
{
[SerializeField] private Transform _firePoint; // Точка стрельбы
private void Update()
{
if (Input.GetButtonDown("Fire1"))
{
Shoot();
}
}
private void Shoot()
{
Bullet bullet = BulletPool.Instance.GetBullet();
bullet.transform.position = _firePoint.position;
bullet.transform.rotation = _firePoint.rotation;
// Добавьте сюда логику для запуска пули, например, задайте скорость
}
}
Advantages and Disadvantages of the Object Pool Pattern
Advantages:
1. Performance improvement: Reducing the frequency of object creation and destruction reduces the load on the garbage collector and improves game performance.
2. Reusing objects: Objects are reused, saving memory and resources.
3. Flexibility: The object pool can be configured to manage any type of object, making it a versatile solution for a variety of tasks.
Flaws:
1. Increased memory usage: Object pooling requires storing inactive objects in memory, which can increase the overall amount of memory used by the application.
2. Complexity of pool management: Managing large numbers of objects in a pool can become complex, especially if the objects have complex states or dependencies.
Conclusion
Object Pool is a powerful pattern that helps you manage resources efficiently and improve the performance of your game. It is especially useful in games that require frequent creation and destruction of similar objects. In the next part of the article, we will look at other useful design patterns that will help you create more structured and manageable code in Unity.