Главные игровые менеджеры. Управляем состояниями игры, настройками звука и графики

Всем привет! Сегодня я продолжаю цикл статей по разбору различных подходов в разработке игр на примерах. Сегодня хотел бы рассказать о возможных реализациях менеджеров для управления состояниями игры, а также настройками аудио и графики.

Главные игровые менеджеры. Управляем состояниями игры, настройками звука и графики

Итак, поехали.

Классы ядра. Работа с файловой системой и шифрованием.

Для начала создадим классы, которые помогут нам работать с файловой системой, а также шифровать наши данные. В дальнейшем с их помощью мы сможем сохранять наши игровые состояния.

Классы шифрования

Начнем с классов шифрования, к примеру Base64.cs:

namespace Framework.Core.Security { using System; using System.Text; using System.Security.Cryptography; public class Base64 { public static string Encode(string decodedText) { byte[] bytesToEncode = Encoding.UTF8.GetBytes (decodedText); string encodedText = Convert.ToBase64String (bytesToEncode); return encodedText; } public static string Decode(string encodedText) { byte[] decodedBytes = Convert.FromBase64String (encodedText); string decodedText = Encoding.UTF8.GetString (decodedBytes); return decodedText; } } }

Класс MD5.cs:

namespace Framework.Core.Security { using System; using System.Text; using System.Security.Cryptography; public class MD5 { public static string GetHash(string data) { using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create()) { byte[] inputBytes = System.Text.Encoding.UTF8.GetBytes(data); byte[] hashBytes = md5.ComputeHash(inputBytes); // Convert the byte array to hexadecimal string StringBuilder sb = new StringBuilder(); for (int i = 0; i < hashBytes.Length; i++) { sb.Append(hashBytes[i].ToString("X2")); } return sb.ToString(); } } public static string GetHash(byte[] data) { using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create()) { byte[] hashBytes = md5.ComputeHash(data); // Convert the byte array to hexadecimal string StringBuilder sb = new StringBuilder(); for (int i = 0; i < hashBytes.Length; i++) { sb.Append(hashBytes[i].ToString("X2")); } return sb.ToString(); } } } }

Класс RSA.cs:

namespace Framework.Core.Security { using System; using System.Collections.Generic; using System.Text; using System.Security.Cryptography; public class RSA { public static KeyValuePair<string, string> GenrateKeyPair(int keySize){ RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(keySize); string publicKey = rsa.ToXmlString(false); string privateKey = rsa.ToXmlString(true); return new KeyValuePair<string, string>(publicKey, privateKey); } public static string Encrypt(string plane, string publicKey) { byte[] encrypted = Encrypt(Encoding.UTF8.GetBytes(plane), publicKey); return Convert.ToBase64String(encrypted); } public static byte[] Encrypt(byte[] src, string publicKey) { using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider()) { rsa.FromXmlString(publicKey); byte[] encrypted = rsa.Encrypt(src, false); return encrypted; } } public static string Decrypt(string encrtpted, string privateKey) { byte[] decripted = Decrypt(Convert.FromBase64String(encrtpted), privateKey); return Encoding.UTF8.GetString(decripted); } public static byte[] Decrypt(byte[] src, string privateKey) { using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider()) { rsa.FromXmlString(privateKey); byte[] decrypted = rsa.Decrypt(src, false); return decrypted; } } } }

Класс SHA.cs:

namespace Framework.Core.Security { using System; using System.Text; using System.Security.Cryptography; public class SHA { public static string GetSHA1Hash(string input) { using (SHA1Managed sha1 = new SHA1Managed()) { var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(input)); var sb = new StringBuilder(hash.Length * 2); foreach (byte b in hash) { // can be "x2" if you want lowercase sb.Append(b.ToString("X2")); } return sb.ToString(); } } public static string GetSHA256Hash(string input) { using (SHA256Managed sha256 = new SHA256Managed()) { var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(input)); var sb = new StringBuilder(hash.Length * 2); foreach (byte b in hash) { // can be "x2" if you want lowercase sb.Append(b.ToString("X2")); } return sb.ToString(); } } public static string GetSHA512Hash(string input) { using (SHA512Managed sha256 = new SHA512Managed()) { var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(input)); var sb = new StringBuilder(hash.Length * 2); foreach (byte b in hash) { // can be "x2" if you want lowercase sb.Append(b.ToString("X2")); } return sb.ToString(); } } } }

Класс AES.cs:

namespace Framework.Core.Security { using System; using System.Collections.Generic; using System.Text; using System.Security.Cryptography; public class AES { // Private Params private static int _bufferKeySize = 32; private static int _blockSize = 256; private static int _keySize = 256; public static void UpdateEncryptionKeySize(int bufferKeySize = 32, int blockSize = 256, int keySize = 256) { _bufferKeySize = bufferKeySize; _blockSize = blockSize; _keySize = keySize; } public static string Encrypt(string plane, string password) { byte[] encrypted = Encrypt(Encoding.UTF8.GetBytes(plane), password); return Convert.ToBase64String(encrypted); } public static byte[] Encrypt(byte[] src, string password) { RijndaelManaged rij = SetupRijndaelManaged; // A pseudorandom number is newly generated based on the inputted password Rfc2898DeriveBytes deriveBytes = new Rfc2898DeriveBytes(password, _bufferKeySize); // The missing parts are specified in advance to fill in 0 length byte[] salt = new byte[_bufferKeySize]; // Rfc2898DeriveBytes gets an internally generated salt salt = deriveBytes.Salt; // The 32-byte data extracted from the generated pseudorandom number is used as a password byte[] bufferKey = deriveBytes.GetBytes(_bufferKeySize); rij.Key = bufferKey; rij.GenerateIV(); using (ICryptoTransform encrypt = rij.CreateEncryptor(rij.Key, rij.IV)) { byte[] dest = encrypt.TransformFinalBlock(src, 0, src.Length); // first 32 bytes of salt and second 32 bytes of IV for the first 64 bytes List<byte> compile = new List<byte>(salt); compile.AddRange(rij.IV); compile.AddRange(dest); return compile.ToArray(); } } public static string Decrypt(string encrtpted, string password) { byte[] decripted = Decrypt(Convert.FromBase64String(encrtpted), password); return Encoding.UTF8.GetString(decripted); } public static byte[] Decrypt(byte[] src, string password) { RijndaelManaged rij = SetupRijndaelManaged; List<byte> compile = new List<byte>(src); // First 32 bytes are salt. List<byte> salt = compile.GetRange(0, _bufferKeySize); // Second 32 bytes are IV. List<byte> iv = compile.GetRange(_bufferKeySize, _bufferKeySize); rij.IV = iv.ToArray(); Rfc2898DeriveBytes deriveBytes = new Rfc2898DeriveBytes(password, salt.ToArray()); byte[] bufferKey = deriveBytes.GetBytes(_bufferKeySize); // Convert 32 bytes of salt to password rij.Key = bufferKey; byte[] plain = compile.GetRange(_bufferKeySize * 2, compile.Count - (_bufferKeySize * 2)).ToArray(); using (ICryptoTransform decrypt = rij.CreateDecryptor(rij.Key, rij.IV)) { byte[] dest = decrypt.TransformFinalBlock(plain, 0, plain.Length); return dest; } } private static RijndaelManaged SetupRijndaelManaged { get { RijndaelManaged rij = new RijndaelManaged(); rij.BlockSize = _blockSize; rij.KeySize = _keySize; rij.Mode = CipherMode.CBC; rij.Padding = PaddingMode.PKCS7; return rij; } } } }

Классы для работы с файловой системой

Enum для Serialization Type:

namespace Framework.Core.ContentManagement { public enum SerializationType { JSON, EncryptedJSON, Binary, EncryptedBinary } }

Класс для работы с файлами и сериализацией FileReader.cs:

namespace Framework.Core.ContentManagement { using System; using System.Text; using System.Xml.Serialization; using System.IO; using UnityEngine; using Framework.Core.Security; using System.Runtime.Serialization.Formatters.Binary; public class FileReader { private static string _encryptionKey = "A&fv2hAD9jgkdf89^ASD2q89zsjdA"; // Default Data Encryption Key public static void SetEncryptionKey(string key){ _encryptionKey = key; } public static string GetEnryptionKey(string key){ return _encryptionKey; } #region Read Files public static string LoadText(string pathToFile, Encoding encoding = null) { string path = Application.persistentDataPath + pathToFile; if(File.Exists(path)){ if (encoding != null){ return File.ReadAllText(path, encoding); }else{ return File.ReadAllText(path); } }else{ return null; } } public static byte[] LoadBinary(string pathToFile) { string path = Application.persistentDataPath + pathToFile; if(File.Exists(path)){ return File.ReadAllBytes(path); }else{ return null; } } public static string LoadTextFromResources(string pathToFile) { TextAsset output = Resources.Load<TextAsset>(pathToFile); return output.text; } public static byte[] LoadBinaryFromResources(string pathToFile) { TextAsset output = Resources.Load<TextAsset>(pathToFile); return output.bytes; } public static void DeleteFile(string pathToFile) { if(File.Exists(pathToFile)) File.Delete(pathToFile); } #endregion #region Save Files public static void SaveText(string pathToFile, string content, Encoding encoding = null) { string path = Application.persistentDataPath + pathToFile; if (encoding != null){ File.WriteAllText(path, content, encoding); }else{ File.WriteAllText(path, content); } } public static void SaveBinary(string pathToFile, byte[] data) { string path = Application.persistentDataPath + pathToFile; File.WriteAllBytes(path, data); } #endregion #region Read Objects public static void ReadObjectFromFile(object referenceObject, string pathToFile, SerializationType serializationType = SerializationType.JSON) { string path = Application.persistentDataPath + pathToFile; if (File.Exists(path)){ if (serializationType == SerializationType.JSON || serializationType == SerializationType.EncryptedJSON){ string serializedData = LoadText(pathToFile, Encoding.UTF8); if(serializationType == SerializationType.EncryptedJSON) serializedData = AES.Decrypt(serializedData, _encryptionKey); if (serializedData != null){ JsonUtility.FromJsonOverwrite(serializedData, referenceObject); } }else if (serializationType == SerializationType.Binary || serializationType == SerializationType.EncryptedBinary){ BinaryFormatter converter = new BinaryFormatter(); FileStream inputStream = new FileStream(pathToFile, FileMode.Open); referenceObject = converter.Deserialize(inputStream); inputStream.Close(); } } } #endregion #region Save Objects public static void SaveObjectToFile<T>(string pathToFile, T serializationObject, SerializationType serializationType = SerializationType.JSON){ string path = Application.persistentDataPath + pathToFile; if (serializationType == SerializationType.JSON || serializationType == SerializationType.EncryptedJSON){ string serializedData = JsonUtility.ToJson(serializationObject); if (serializationType == SerializationType.EncryptedJSON) serializedData = AES.Encrypt(serializedData, _encryptionKey); SaveText(pathToFile, serializedData, Encoding.UTF8); }else if (serializationType == SerializationType.Binary || serializationType == SerializationType.EncryptedBinary){ BinaryFormatter converter = new BinaryFormatter(); FileStream outputStream = new FileStream(pathToFile, FileMode.Create); converter.Serialize(outputStream, serializationObject); outputStream.Close(); } } #endregion } }

Базовый игровой менеджер

Далее мы создадим класс, который будет управлять глобальным состоянием нашей игры. Он позволит нам реализовать сохранения в игре.

Начнем с IGameManager - общего интерфейса для всех менеджеров:

namespace Framework.Managers { public interface IGameManager { public void LoadState(); public void SaveState(); } }

Теперь добавим интерфейс для наших игровых состояний:

namespace Framework.Managers { public interface IGameState { // That's the way it has to be, don't touch it! } }

И, наконец, перейдем к классу GameManager.cs:

namespace Framework.Managers { using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; using UnityEngine.SceneManagement; using Framework.Core.ContentManagement; using Framework.Utils; public class GameManager : IGameManager { // Game Manager Events public UnityEvent<IGameState> OnGameStateChanged = new UnityEvent<IGameState>(); public UnityEvent<bool> OnGamePaused = new UnityEvent<bool>(); // Private Params private static GameManager _instance; private IGameState _config; #region Base Manager Logic private GameManager(IGameState config = null) { if (config != null) _config = config; } public static GameManager Instance(IGameState config = null) { if (_instance == null) _instance = new GameManager(config); return _instance; } public GameManager SetState(IGameState config) { _config = config; return _instance; } public IGameState GetCurrentState() { return _config; } public void LoadState() { string path = "/game_state.dat"; FileReader.ReadObjectFromFile(_config, path, SerializationType.EncryptedJSON); } public void SaveState() { string path = "/game_state.dat"; FileReader.SaveObjectToFile(path, _config, SerializationType.EncryptedJSON); if(OnGameStateChanged!=null) OnGameStateChanged.Invoke(_config); } #endregion #region Game Pause public void PauseGame() { if(OnGamePaused!=null) OnGamePaused.Invoke(true); } public void UnpauseGame() { if(OnGamePaused!=null) OnGamePaused.Invoke(false); } #endregion #region Scene Management private IEnumerator LoadSceneAsync(string sceneName, Action<float> onProgress = null, Action<AsyncOperation> onComplete = null) { yield return null; AsyncOperation asyncOperation = SceneManager.LoadSceneAsync(sceneName); asyncOperation.allowSceneActivation = false; while (!asyncOperation.isDone) { //Output the current progress if (onProgress != null) onProgress(asyncOperation.progress); // Check if the load has finished if (asyncOperation.progress >= 0.9f) { if (onComplete != null) { onComplete(asyncOperation); } else { asyncOperation.allowSceneActivation = true; } } yield return null; } } #endregion } }

Теперь можно создать наш класс для хранения игровых состояний:

namespace Framework.Models { using System; using System.Collections; using System.Collections.Generic; using Framework.Managers; [System.Serializable] public class GameStateModel : IGameState { // Base Game Params public bool IsFirstLaunch = false; // General Data public int DefaultCurrency = 0; public int VIPCurrency = 0; // Levels Data public int CurrentLevel = 0; public int CurrentLevelProgress = 0; } }

Здесь мы задали следующие параметры:

  • IsFirstLaunch - Запускается ли игра в первый раз;
  • DefaultCurrency - Обычная валюта;
  • VIPCurrency - VIP-валюта;
  • CurrentLevel - Текущий уровень, на котором мы остановились;
  • CurrentLevelProgress - Прогресс на уровне, на котором мы остановились.

Для инициализации менеджера используем:

GameManager.Instance(new GameStateModel()).LoadState();

Далее мы можем взаимодействовать с менеджером следующим образом:

GameStateModel ourData = (GameStateModel) GameManager.Instance().GetCurrentState(); ourData.IsFirstLaunch = false; GameManager.Instance().SaveState();

Аудио-менеджер. Работа с настройками звука

Теперь мы реализуем наш менеджер для настроек звука.

Для начала добавим Enum, позволяющий определять тип AudioSource:

namespace Framework.Managers { public enum AudioSourceType { Music, SoundFX, Voice } }

Теперь создадим класс конфигураций для работы с аудио:

namespace Framework.Managers { [System.Serializable] public class AudioManagerConfigs { public float MasterVolume = 1f; public float MusicVolume = 1f; public float SoundsVolume = 1f; public float VoicesVolume = 1f; } }

И реализуем наш аудио-менеджер:

namespace Framework.Managers { using System; using UnityEngine; using UnityEngine.Events; using Framework.Core.ContentManagement; public class AudioManager : IGameManager { // Audio Events public UnityEvent<AudioManagerConfigs> OnAudioSettingsChanged = new UnityEvent<AudioManagerConfigs>(); // Private Params private static AudioManager _instance; private AudioManagerConfigs _config = new AudioManagerConfigs(); #region Base Manager Logic private AudioManager(AudioManagerConfigs config = null) { if (config != null) _config = config; } public static AudioManager Instance(AudioManagerConfigs config = null) { if (_instance == null) _instance = new AudioManager(config); return _instance; } public AudioManager SetState(AudioManagerConfigs config) { _config = config; return _instance; } public AudioManagerConfigs GetCurrentState() { return _config; } public void LoadState() { string path = "/audio_settings.dat"; _config = FileReader.ReadObjectFromFile<AudioManagerConfigs>(path, SerializationType.EncryptedJSON); } public void SaveState() { string path = "/audio_settings.dat"; FileReader.SaveObjectToFile(path, _config, SerializationType.EncryptedJSON); if(OnAudioSettingsChanged!=null) OnAudioSettingsChanged.Invoke(_config); } #endregion #region Audio Manager Logic public AudioManager SetMasterVolume(float value) { _config.MasterVolume = value; SaveState(); return _instance; } public float GetMasterVolume() { return _config.MasterVolume; } public AudioManager SetSoundsVolume(float volume) { _config.SoundsVolume = volume; SaveState(); return _instance; } public float GetSoundsVolume() { return _config.SoundsVolume; } public AudioManager SetMusicVolume(float volume) { _config.MusicVolume = volume; SaveState(); return _instance; } public float GetMusicVolume() { return _config.MusicVolume; } public AudioManager SetVoicesVolume(float volume) { _config.VoicesVolume = volume; SaveState(); return _instance; } public float GetVoicesVolume() { return _config.VoicesVolume; } #endregion } }

Дополнительно, создадим компонент AudioSourceManager, который будет управлять нашим компонентом AudioSource:

namespace Framework.Components.Audio { using UnityEngine; using Framework.Managers; [RequireComponent(typeof(AudioSource))] [AddComponentMenu("Framework/Audio/AudioSource Volume")] internal class AudioSourceVolumeManager : MonoBehaviour { // Audio Type public AudioSourceType AudioSourceType = AudioSourceType.SoundFX; // Private Params private AudioSource _audioSource; private float _baseVolume; private void Awake() { _audioSource = GetComponent<AudioSource>(); _baseVolume = _audioSource.volume; } private void Start() { AudioManager.Instance().OnAudioSettingsChanged.AddListener(OnAudioSettingsUpdated); OnAudioSettingsUpdated(AudioManager.Instance().GetCurrentState()); } private void OnDestroy() { AudioManager.Instance().OnAudioSettingsChanged.RemoveListener(OnAudioSettingsUpdated); } private void OnAudioSettingsUpdated(AudioManagerConfigs configs) { float newVolume = _baseVolume * configs.MasterVolume; switch (AudioSourceType) { case AudioSourceType.SoundFX: newVolume *= configs.SoundsVolume; break; case AudioSourceType.Music: newVolume *= configs.MusicVolume; break; case AudioSourceType.Voice: newVolume *= configs.VoicesVolume; break; } _audioSource.volume = newVolume; } } }

И теперь можем инициализировать его и работать также, как с GameManager:

AudioManager.Instance(new AudioManagerConfigs()).LoadState();

Работа с графическими настройками

Здесь мы не будем расписывать глубокие настройки графики, а лишь создадим обычный менеджер для сохранения состояний QualityLevel.

Создадим класс конфигураций для нашего менеджера:

namespace Framework.Managers { [System.Serializable] public class GraphicsManagerConfigs { public int QualityLevel = 1; } }

И реализуем наш менеджер:

namespace PixelFramework.Managers { using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; using PixelFramework.Core.ContentManagement; public class GraphicsManager : IGameManager { // Graphics Events public UnityEvent<GraphicsManagerConfigs> OnGraphicsSettingsChanged = new UnityEvent<GraphicsManagerConfigs>(); // Private Params private static GraphicsManager _instance; private GraphicsManagerConfigs _config = new GraphicsManagerConfigs(); #region Base Manager Logic private GraphicsManager(GraphicsManagerConfigs config = null) { if (config != null) _config = config; } public static GraphicsManager Instance(GraphicsManagerConfigs config = null) { if (_instance == null) _instance = new GraphicsManager(config); return _instance; } public GraphicsManager SetState(GraphicsManagerConfigs config) { _config = config; return _instance; } public void LoadState() { string path = "/graphics_settings.dat"; _config = FileReader.ReadObjectFromFile<GraphicsManagerConfigs>(path, SerializationType.EncryptedJSON); } public void SaveState() { string path = "/graphics_settings.dat"; FileReader.SaveObjectToFile(path, _config, SerializationType.EncryptedJSON); if(OnGraphicsSettingsChanged!=null) OnGraphicsSettingsChanged.Invoke(_config); } public GraphicsManagerConfigs GetCurrentState() { return _config; } #endregion #region Graphics Manager Logic public GraphicsManager SetQualityLevel(int qualityLevel) { QualitySettings.SetQualityLevel(qualityLevel); _config.QualityLevel = qualityLevel; SaveState(); return _instance; } public int GetQualityLevel() { return _config.QualityLevel; } /* TODO: Extended Graphics Management */ #endregion } }

И его также можно использовать, как и другие менеджеры:

GraphicsManager.Instance(new GraphicsManagerConfigs()).LoadState();

Примечания

Вы можете использовать вместо UnityEvent более простые и быстрые конструкции. Здесь это показано для примера и используется в других целях.

Для менеджеров не обязательно использовать синглтон. Здесь это показано для примера.

Итого

Таким образом мы рассмотрели один из примеров реализации игровых менеджеров. С их помощью можно управлять глобальными состояниями игры, настройками графики, локализации, аудио и др.

Данная реализация менеджеров - лишь пример для быстрого прототипирования.

Надеюсь, для вас это было полезно и буду рад ответить на ваши вопросы.

30
16 комментариев