7/24/2014

Singleton for Unity

Всем привет.  Меня зовут Марьян Ярома (в интернетах Maryan Yaroma). Знаете, давно хотел начать писать свои заметки в "электронном виде", а не на клочках бумаги, как обычно. Я же как-никак программист(хотя...), ну в общем суть остается прежней: мне просто необходимо что-то писать куда-нибудь. Так как это доставляет мне огромное удовольствие... А если мою писанину еще и прочитают, то я вообще буду безмерно рад, не говоря уж о комментариях.


Мои записи не претендуют на огромную практическую пользу и могут вводить кого-то в заблуждение, поэтому не стоит внимательно во все вчитываться, но кто знает... В общем первая запись совпала с поиском простейшего паттерна и, пожалуй, наиболее используемого в моей практике - Singleton'a(Одиночка).
Попробую сейчас немного рассказать, правда рассматривать его буду с прицелом на геймдев(Unity + C#). Итак, поехали!
Посмотрим википедию:
Одиночка (англ. Singleton) — порождающий шаблон проектирования, гарантирующий что в однопоточном приложении будет единственный экземпляр класса с глобальной точкой доступа.
Цель: Гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа. Существенно то, что можно пользоваться именно экземпляром класса, так как при этом во многих случаях становится доступной более широкая функциональность. Например, к описанным компонентам класса можно обращаться через интерфейс, если такая возможность поддерживается языком.
Простым языком: в разработке игр у нас часто возникает необходимость в классах, которые будут существовать в единственном экземпляре на протяжении всей игры. Допустим у нас есть аудио менеджер - класс отвечающий за воспроизведение и настройки звука и музыки в игре. А теперь применительно к Unity:
Кидаем скрипт (AudioManager.cs) на объект(любое название, например - Audio) сцены. Теперь нам необходимо, чтобы этот объект жил на протяжении всей игры, но если мы перейдем на другую сцену, то объект будет удален. Нас это никак не устраивает, поэтому воспользуемся функцией Unity защищающей от удаления: DontDestroyOnLoadи передадим в нее наш объект на котором висит скрипт (DontDestroyOnLoad(gameObject)). Вот и все задача почти выполнена! Теперь наш объект не удаляется при переходе от сцены к сцене. Но не все так хорошо как хотелось бы: когда мы переходим на ту сцену где изначально висел наш объект, появляется дубликат... С этим нужно как-то бороться. Вот тут нам и понадобится паттерн Singleton. Простейшим решением будет писать реализацию паттерна в нашем классе, примерно следующим образом:
       
/*
    AudioManager.cs
*/
using UnityEngine;

public class AudioManager : MonoBehaviour
{
    // какой-нибудь код
    public static AudioManager instance = null;
    
    void Awake()
    {
        if(instance != null)
        {
            Destroy(gameObject);
            return;
        }
        instance = this;
        DontDestroyOnLoad (gameObject);
    }
    // и далее какой-нибудь код
}
Впринципе просто, быстро и понятно. Но вставлять этот блок кода в каждый класс это не "по-программерски", мы же, ведь, ленивые...а повторять то, что однажды уже написали не в наших правилах. К тому же необходимо стараться избегать повторений кода, если это не сильно усложняет его понимание. Так, изменим немного то, что мы сделали, для этого напишем базовый класс для паттерна. Что-то вроде этого:
       
/*
    SingletonBehaviour.cs
*/
using System;
using UnityEngine;

public class SingletonBehaviour : MonoBehaviour
{
    private static T instance;
 private static object this_lock = new object();

 protected virtual void Awake () 
 {
  lock (this_lock) 
  {
            if(instance == null)
          instance = (T)(object)this;
   else
   {
    Destroy(gameObject);
    return;
   }
   DontDestroyOnLoad (gameObject);
  }
 }
}
Теперь наш аудио менеджер и другие классы, использующие этот паттерн не будут повторять его реализацию.
       
/*
    AudioManager.cs
*/
using UnityEngine;
using System.Collections;

public class AudioManager : SingletonBehaviour
{
    // какой-нибудь код 
    protected override void Awake()
    {
        base.Awake();
        // какой-нибудь код 
    }
    // какой-нибудь код 
}
Теперь проделаем нечто подобное для классов, которые не висят на объектах.
       
/*
    Singleton.cs
*/
using System;
using System.Reflection;

public class Singleton
{
    private static object init_lock = new object();
    private static T instance;
    
    public static T Instance()
    {
        if(instance == null)
        {
            CreateInstance();
        }
        return instance;
    }
    
    private static void CreateInstance()
    {
        lock(init_lock)
        {
            if(instance == null)
            {
                Type t = typeof(T);
                ConstructorInfo[] ctors = t.GetConstructors();
                if(ctors.Length > 0)
                {
                    throw new InvalidOperationException(
                    String.Format("{0} Singleton can not " +
                    "have constructors", t.Name));
                }
                instance = (T)Activator.CreateInstance(t, true);
            }
        }
    }
}
Получилось немного корявенько, но все же сути это не меняет. К тому же мы защищены от многопоточки -
       lock(init_lock){}
. Классы наследуемые от нащего базового Singleton будут удовлетворять всем критериям этого паттерна.
       
/*
    GameInfo.cs
*/
using System;

public class GameInfo : Singleton
{
    // какой-нибудь код
}
Используем его примерно следующим образом.
       
/*
    GameLog.cs
*/

using System;

public class GameLog
{
    // какой-нибудь код
    private GameInfo info;
    
    private void Init()
    {
        info = GameInfo.Instance();
        // нельзя использовать new()!
    }
}
Ну вот и все, пожалуй, на сегодня!

Комментариев нет:

Отправить комментарий