8. Использование Singleton в Unity3D
Организация любого, хотя-бы малость серьезного проекта требует хорошей организации кода. Проекты, разрабатываемые в среде Unity3D не являются исключением и, по мере роста проекта, его организация может сыграть не малую роль в качестве исходного продукта.
Как работает Singleton
Рассмотрим для примера организацию работы в мобильной игре:
В нашем случае Singleton — это объект
переходящий от сцене к сцене, служащий для управления всеми объектами
определенного типа в рамках игровой сцены (игры в целом).
На схеме ниже мы изобразили схему работы на
примере мобильной пошаговой онлайн-игры:
Чтобы иметь полную картину, рассмотрим архитектуру этой игры. В данном случае помимо объектов Singleton у нас будут присутствовать следующие элементы:
- Объекты для подгрузки Singleton (Так
называемый Bootstrap-класс)
- Объекты игровой
логики (объекты управления сценариями)
- Контроллеры
(Например: контроллер игрока)
- Модели данных
(объекты для сериализации данных, получаемых с сервера)
- Объекты
интерфейса
- Прочие статические игровые объекты
Таким образом мы сможем создать удобную и чистую
архитектуру проекта с которой в дальнейшем не возникнет сложностей при
масштабировании.
Реализация Singleton в Unity3D
Для более легкого восприятия мы продолжим
рассматривать архитектуру мобильной онлайн игры и посмотрим, как все что мы
описали выше, будет выглядеть на практике.
Классы-Менеджеры
Основа всего метода проектирования — собственно
сами классы менеджеры, которые находятся в игре в единственном экземпляре и
могут быть вызваны в любой момент. Для создания такого класса менеджера мы
можем описать следующий код:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//=============================================
// Audio Manager
//=============================================
public class AudioManager: MonoBehaviour {
public static AudioManager instance = null; // Экземпляр объекта
// Метод,
выполняемый при старте игры
void Awake() {
// Теперь, проверяем существование
экземпляра
if (instance == null)
{ // Экземпляр менеджера был найден
instance = this;
// Задаем ссылку на экземпляр объекта
} else if(instance
== this){ // Экземпляр объекта уже
существует на сцене
Destroy(gameObject); // Удаляем объект
}
// Теперь нам нужно указать,
чтобы объект не уничтожался
// при переходе на другую
сцену игры
DontDestroyOnLoad(gameObject);
// И запускаем собственно инициализатор
InitializeManager();
}
// Метод инициализации менеджера
private void InitializeManager(){
/*
TODO: Здесь мы будем проводить инициализацию */
}
}
На примере выше мы создали основу для одного из
игровых менеджеров (в нашем случае это менеджер Audio). Не обязательно
проводить инициализацию через метод Start(). Вы также можете использовать для
этого метод Awake(), чтобы ваш объект был готов еще до старта сцены.
Теперь мы допишем наш класс, чтобы он умел
загружать и сохранять параметры звука и музыки в игре:
using
System.Collections;
using
System.Collections.Generic;
using
UnityEngine;
//=============================================
// Audio Manager
//=============================================
public class AudioManager:
MonoBehaviour {
public static
AudioManager instance = null; //
Экземпляр объекта
public static bool
music = true; // Параметр доступности музыки
public static bool
sounds = true; // Параметр доступности звуков
// Метод, выполняемый при старте игры
void Awake () {
// Теперь, проверяем существование
экземпляра
if (instance == null) { // Экземпляр менеджера был найден
instance = this; // Задаем ссылку на экземпляр объекта
} else if(instance == this){
// Экземпляр объекта
уже существует на сцене
Destroy(gameObject); // Удаляем объект
}
// Теперь нам нужно указать, чтобы
объект не уничтожался
// при переходе на другую сцену игры
DontDestroyOnLoad(gameObject);
// И запускаем собственно
инициализатор
InitializeManager();
}
// Метод инициализации менеджера
private void InitializeManager(){
// Здесь мы загружаем и конвертируем настройки из PlayerPrefs
music
= System.Convert.ToBoolean (PlayerPrefs.GetString ("music",
"true"));
sounds = System.Convert.ToBoolean (PlayerPrefs.GetString ("sounds",
"true"));
}
// Метод для сохранения текущих настроек
public static void saveSettings(){
PlayerPrefs.SetString ("music",
music.ToString ()); // Применяем параметр музыки
PlayerPrefs.SetString ("sounds",
sounds.ToString ()); // Применяем параметр звуков
PlayerPrefs.Save();
// Сохраняем настройки
}
}
using
System.Collections;
using
System.Collections.Generic;
using
UnityEngine;
//=============================================
// Audio Muter Class
//=============================================
public class AudioMuter
: MonoBehaviour {
// Публичные параметры
public bool
is_music = false; // Это объект с музыкой?
// Приватные параметры
private
AudioSource _as; // Audio Source
private float
base_volume = 1F; // Базовая громкость
// Инициализация компонента при старте игры
void Awake() {
_as
= this.gameObject.GetComponent<AudioSource>
(); // Получаем компонент
AS
base_volume
= _as.volume; //
Получаем базовую громкость из AS
}
// Каждый кадр мы проверяем
параметры и устанавливаем громкость
void Update () {
// Для начала проверим, музыка это
или нет
if
(is_music) {
_as.volume =
(AudioManager.music)?base_volume:0F;
} else {
_as.volume =
(AudioManager.sounds)?base_volume:0F;
}
}
}
Bootstrap-класс
Рассмотрим наш класс Boostrap-а:
using
System.Collections;
using
System.Collections.Generic;
using
UnityEngine;
//=============================================
// Game Classes Loader
//=============================================
public class GameLoader
: MonoBehaviour {
// Ссылки на менеджеров
public
GameObject game_manager; // Game Base Manager
public
GameObject audio_manager; // Audio Manager
public
GameObject lang_manager; // Language Manager
public
GameObject net_manager; // Network Manager
// Метод пробуждения объекта (перед стартом игры)
void Awake () {
// Инициализация игровой базы
if (GameBase.instance == null) {
Instantiate
(game_manager);
}
// Инициализация аудио менеджера
if
(AudioManager.instance == null) {
Instantiate
(audio_manager);
}
// Инициализация менеджера языков
if
(LangManager.instance == null) {
Instantiate
(lang_manager);
}
// Инициализация сетевого менеджера
if
(NetworkManager.instance == null) {
Instantiate (net_manager);
}
}
}
Модели данных
using
System.Collections;
using
System.Collections.Generic;
using
UnityEngine;
[System.Serializable]
public class responceModel{
public bool
complete = false; // Статус операции
public string message = ""; // Сообщение об ошибке (в случае если complete = false)
}
{
complete: true, //
Статус операции
data: {} // Объект с запрошенными данными
}
А при ошибке мы получаем ответ следующего вида:
{
complete: false, // Статус операции
message: "" // Сообщение об ошике
}
Таким образом мы можем парсить ответ сервера при
помощи JSON десериализации и нашей модели данных:
responceModel responce =
JsonUtility.FromJson<responceModel>(request.text); //
Парсинг
JSON
if(responce.complete){
/* TODO: Делаем что-то с
полученными данными */
Debug.Log(responce.data);
}else{
/* TODO: Выводим ошибку */
Debug.Log(responce.message);
}
Контроллеры
Контроллеры будут служить нам для работы
множественных объектов в игре (к примеру, противники в игре, либо контроллер
игрока). Контроллеры создаются самым обычным способом и цепляются на объекты в
игре в качестве компонентов.
Пример простого контроллера игрока:
using
System.Collections;
using
System.Collections.Generic;
using
UnityEngine;
//=============================================
// PLAYER CONTROLLER
//=============================================
public class PlayerController
: MonoBehaviour {
// Публичные объекты
[Header ("Player
Body Parts")]
public
GameObject[] hairs;
public
GameObject[] faces;
public
GameObject[] special;
// Инициализация компонента
void Start
() {
}
// Апдейт фрейма
void Update () {
}
// Обновить на игроке его части тела
public void updateParts (){
// Работа с волосами
for
(int i = 0;
i < hairs.Length; i++) {
if (i ==
NetworkManager.instance.auth.player_data.profile_data.body.hairs) {
hairs [i].SetActive (true);
} else
{
hairs [i].SetActive (false);
}
}
/* TODO: Тоже самое для
других частей тела */
}
}
На примере выше в части кода, где мы обновляем
части тела игрока, мы используем модель данных с информацией о профиле игрока,
которая была непосредственно подключена в менеджере сети.
Рассмотрим данную строку:
if (i
== NetworkManager.instance.auth.player_data.profile_data.body.hairs){
Здесь мы видим, что идет сравнение индекса в
цикле с идентификатором волос в модели данных игрока. Данная модель
представлена в экземпляре объекта менеджера сети (NetworkManager), где
был инициализирован объект для работы с авторизацией (auth), внутри
которого размещены модели данных (player_data => profile_data => body).
Взаимодействие
с Singleton
Для взаимодействия с менеджерами мы будем
использовать либо экземпляр объекта (instance), либо прямое обращение для
статических параметров.
Пример работы с instance:
public bool
_hair = NetworkManager.instance.auth.player_data.profile_data.body.hairs;
На примере выше мы использовали свойство instance для
получения данных о волосах игрока в менеджере NetworkManager.
Пример прямого взаимодействия со
static-параметрами:
public
bool _sounds = AudioManager.sounds;
На примере выше мы обратились напрямую к
статичному свойству sounds в менеджере AudioManager.
О плюсах и минусах Singleton
Плюсы:
+ Нет необходимости постоянной настройки и
описаний полей скриптов в инспекторе
+ К менеджерам можно обращаться через свойство
instance
+ Удобный рефакторинг кода
+ Компактность кода
Минусы:
— Сильная зависимость кода
— Доступ только к скриптам-менеджерам в
единственном экземпляре
Немного практических примеров
Использование делегатов
Рассмотрим данный пример:
// Задаем функции-делегаты
public
delegate void OnComplete();
public delegate void OnError(string
message);
// Создаем наш метод, использующий делегаты
public void checkNumber(int number,
OnComplete success, OnError fail){
if(number<10){
success(); // Вызываем метод OnComplete
}else{
fail("Вы ввели число большее
10!"); // Вызываем метод с ошибкой
}
}
На простом примере выше мы создали метод,
который вызываем функцию success, если параметр number был меньше 10 и функцию
error, когда параметр был больше или равен 10 соответственно.
Использовать данный метод можно следующим
способом:
public
void testMethod(){
int _number = Random.Range(0,50); // Случайное число
// Вызываем созданный нами метод
проверки числа
checkNumber(_number,
(()=>{ // Здесь
вызывается метод Success
/* TODO: Делаем что-то при
успешном выполнении */
Debug.Log("Все хорошо!");
}), ((string text)=>{ // Здесь вызывается метод Fail
Debug.Log(text); // Выводим текст, полученный в
аргументе Callback функции
testMethod(); // Перезапускаем метод до тех пор,
пока число не станет <10
}));
}
Таким образом мы можем создавать код с управляемым
результатом. Теперь мы плавно переходим к примеру использования вместе с
Singleton.
Делегаты в связке с Coroutine в Singleton
Для наиболее удобного и правильного
взаимодействия с сервером мы можем использовать связку Coroutine-функций и
делегатов, тем самым получая возможность отправлять асинхронные запросы и
обрабатывать ответ сервера. Ниже мы подготовили пример NetworkManager-а
с использованием Coroutine-функций и делегатов.
Рассмотрим данный пример NetworkManager-а:
using
System.Collections;
using
System.Collections.Generic;
using
UnityEngine;
//=============================================
// Network Manager
//=============================================
public class NetworkManager
: MonoBehaviour {
// Публичные параметры
public static
NetworkManager instance = null; //
Экземпляр менеджера
public static string
server = "https://mysite.com/api";
// URL сервера
// Публичные ссылки на подобъекты менеджера
public APIAuth auth; // Объект авторизации
public APIUtils utils; // Объект утилит
// Инициализация менеджера
void Awake () {
// Проверяем экземпляр объекта
if (instance == null) {
instance = this;
} else if(instance
== this){
Destroy(gameObject);
}
// Даем понять движку, что его не
нужно уничтожать
DontDestroyOnLoad(gameObject);
// Инициализируем нашего менеджера
InitializeManager();
}
//
Инициализация менеджера
public void InitializeManager(){
auth = new APIAuth
(server + "/auth/"); //
Подключаем подобъект авторизации
utils = new APIUtils
(server + "/utils/"); //
Подключаем подобъект утилит
}
}
//=============================================
// API Auth Manager
//=============================================
public class APIAuth{
// Приватные параметры
private string
controllerURL = ""; //
Controller URL
//=============================================
// Конструктор объекта
//=============================================
public APIAuth(string
controller){
controllerURL = controller;
}
//=============================================
// Метод для авторизации
//=============================================
public delegate void OnLoginComplete();
public delegate void OnLoginError(string
message);
public
IEnumerator SingIn(string
login, string password, OnLoginComplete
complete, OnLoginError error){
// Формируем данные для отправки
WWWForm data = new WWWForm();
data.AddField("login",
login);
data.AddField("password",
password);
data.AddField("lang",
LangManager.language);
// Отправляем запрос на сервер
WWW request = new
WWW(controllerURL + "/login/",
data);
yield return request;
// Обрабатываем ответ сервера
if (request.error != null) { //
Ошибка отправки запроса
error ("Не удалось отправить запрос на сервер");
} else { // Ошибок при отправке не было
try{
responceModel responce =
JsonUtility.FromJson<responceModel>(request.text);
if(responce.complete){
complete(); // Вызываем Success Callback
}else{
error
(responce.message); // Do error
Debug.Log("API
Error: " + responce.message);
}
}catch{
error ("Не удалось обработать ответ
сервера");
Debug.Log("Ошибка обработки ответа
сервера. Данные ответа: " + request.text);
}
}
}
/* TODO: Здесь будут
остальные методы для работы с авторизацией */
}
//=============================================
// Теперь создаем
подобъект утилит по образу и подобию авторизации
//=============================================
public class APIUtils{
private string
controllerURL = "";
// Аналогичный конструктор класса
public APIUtils(string
controller){
controllerURL = controller;
}
//=============================================
// Проверка версии клиента игры
//=============================================
public delegate void OnClientVersionChecked();
public delegate void OnClientVersionError(string
message);
public
IEnumerator CheckClientVersion(string version,
OnClientVersionChecked complete, OnClientVersionError error){
// Создаем данные
WWWForm data = new
WWWForm();
data.AddField("version",
version);
data.AddField("lang",
LangManager.language);
// Отправляем запрос
WWW request = new
WWW(controllerURL + "/checkVersion/",
data);
yield return
request;
// Обрабатываем ответ
if
(request.error != null) {
error ("Не удалось отправить запрос на
сервер");
}
else {
try{
responceModel responce = JsonUtility.FromJson<responceModel>(request.text);
if(responce.complete){
complete();
}else{
error
(responce.message);
Debug.Log("API
Error: " + responce.message);
}
}catch{
error ("Не удалось обработать ответ сервера");
Debug.Log("Ошибка обработки ответа сервера. Данные ответа:
" + request.text);
}
}
}
}
Теперь мы можем использовать это по
назначению:
// Простая функция для вызова проверки
public
void checkMyGame(){
StartCoroutine(NetworkManager.instance.utils.CheckClientVersion(Application.version,
(()=>{ //
Если все прошло успешно
/* TODO: Здесь мы выполняем
загрузку игры после успешной проверки версии игры */
}), ((string msg) => { // Если возникла ошибка
/* TODO: Здесь мы просим
пользователя обновить версию клиента игры */
Debug.Log(msg);
})));
}
Таким образом, вы можете выполнять код
NetworkManager и управлять его методами при помощи Callback-функций из любой
сцены игры.
Источник: https://habr.com/ru/post/341830/
Универсальный скрипт создания:
public class GenericSingletonClass<T> : MonoBehaviour where T : Component
{
private static T instance;
public static T Instance {
get {
if (instance == null) {
instance = FindObjectOfType<T> ();
if (instance == null) {
GameObject obj = new GameObject ();
obj.name = typeof(T).Name;
instance = obj.AddComponent<T>();
}
}
return instance;
}
}
public virtual void Awake ()
{
if (instance == null) {
instance = this as T;
DontDestroyOnLoad (this.gameObject);
} else {
Destroy (gameObject);
}
}
}
Объявление экземпляра объекта
public class MyAudioController : GenericSingletonClass<MyAudioController>;
{
}
Комментарии
Отправить комментарий