我的最终目标是快读建立一个关卡数据自动读入储存功能:
1. 每个关卡有自己的编号,如果没有自定义该关卡,则读取默认编号的初始布局,如果有自定义该关卡,则读取新定义的关卡。
2.在游戏中如果对布局做出了更改,随时储存新的修改。
3.save和load系统与玩法系统耦合度低,无需管理。
先从一个简单的soundmanager开始学习。
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
-
- namespace BBG
- {
- public class SaveManager : SingletonComponent<SaveManager>
- {
- #region Member Variables
-
- private List
saveables; - private JSONNode loadedSave;
-
- #endregion
-
- #region Properties
-
- ///
- /// Path to the save file on the device
- ///
- public string SaveFilePath { get { return Application.persistentDataPath + "/save.json"; } }
-
- ///
- /// List of registered saveables
- ///
- private List
Saveables - {
- get
- {
- if (saveables == null)
- {
- saveables = new List
(); - }
-
- return saveables;
- }
- }
-
- #endregion
-
- #if UNITY_EDITOR
-
- [UnityEditor.MenuItem("Tools/Bizzy Bee Games/Delete Save Data")]
- public static void DeleteSaveData()
- {
- if (!System.IO.File.Exists(SaveManager.Instance.SaveFilePath))
- {
- UnityEditor.EditorUtility.DisplayDialog("Delete Save File", "There is no save file.", "Ok");
-
- return;
- }
-
- bool delete = UnityEditor.EditorUtility.DisplayDialog("Delete Save File", "Delete the save file located at " + SaveManager.Instance.SaveFilePath, "Yes", "No");
-
- if (delete)
- {
- System.IO.File.Delete(SaveManager.Instance.SaveFilePath);
-
- #if BBG_MT_IAP || BBG_MT_ADS
- System.IO.Directory.Delete(BBG.MobileTools.Utils.SaveFolderPath, true);
- #endif
-
- UnityEditor.EditorUtility.DisplayDialog("Delete Save File", "Save file has been deleted.", "Ok");
- }
- }
-
- #endif
-
- #region Unity Methods
-
- private void Start()
- {
- Debug.Log("Save file path: " + SaveFilePath);
- }
-
- private void OnDestroy()
- {
- Save();
- }
-
- private void OnApplicationPause(bool pause)
- {
- if (pause)
- {
- Save();
- }
- }
-
- #endregion
-
- #region Public Methods
-
- ///
- /// Registers a saveable to be saved
- ///
- public void Register(ISaveable saveable)
- {
- Saveables.Add(saveable);
- }
-
- ///
- /// Loads the save data for the given saveable
- ///
- public JSONNode LoadSave(ISaveable saveable)
- {
- return LoadSave(saveable.SaveId);
- }
-
- ///
- /// Loads the save data for the given save id
- ///
- public JSONNode LoadSave(string saveId)
- {
- // Check if the save file has been loaded and if not try and load it
- if (loadedSave == null && !LoadSave(out loadedSave))
- {
- return null;
- }
-
- // Check if the loaded save file has the given save id
- if (!loadedSave.AsObject.HasKey(saveId))
- {
- return null;
- }
-
- // Return the JSONNode for the save id
- return loadedSave[saveId];
- }
-
- #endregion
-
- #region Private Methods
-
- ///
- /// Saves all registered saveables to the save file
- ///
- private void Save()
- {
- Dictionary<string, object> saveJson = new Dictionary<string, object>();
-
- for (int i = 0; i < saveables.Count; i++)
- {
- saveJson.Add(saveables[i].SaveId, saveables[i].Save());
- }
-
- System.IO.File.WriteAllText(SaveFilePath, Utilities.ConvertToJsonString(saveJson));
- }
-
- ///
- /// Tries to load the save file
- ///
- private bool LoadSave(out JSONNode json)
- {
- json = null;
-
- if (!System.IO.File.Exists(SaveFilePath))
- {
- return false;
- }
-
- json = JSON.Parse(System.IO.File.ReadAllText(SaveFilePath));
-
- return json != null;
- }
-
- #endregion
- }
- }
以上代码中的Register函数很重要,其他的需要储存数据的模块,比如soundmanager,就需要继承Isavable,并且在初始化时register自己给savemanager:
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
-
- namespace BBG
- {
- public class SoundManager : SingletonComponent<SoundManager>, ISaveable
- {
- #region Classes
-
- [System.Serializable]
- private class SoundInfo
- {
- public string id = "";
- public AudioClip audioClip = null;
- public SoundType type = SoundType.SoundEffect;
- public bool playAndLoopOnStart = false;
-
- [Range(0, 1)] public float clipVolume = 1;
- }
-
- private class PlayingSound
- {
- public SoundInfo soundInfo = null;
- public AudioSource audioSource = null;
- }
-
- #endregion
-
- #region Enums
-
- public enum SoundType
- {
- SoundEffect,
- Music
- }
-
- #endregion
-
- #region Inspector Variables
-
- [SerializeField] private List
soundInfos = null; -
- #endregion
-
- #region Member Variables
-
- private List
playingAudioSources; - private List
loopingAudioSources; -
- public string SaveId { get { return "sound_manager"; } }
-
- #endregion
-
- #region Properties
-
- public bool IsMusicOn { get; private set; }
- public bool IsSoundEffectsOn { get; private set; }
-
- #endregion
-
- #region Unity Methods
-
- protected override void Awake()
- {
- base.Awake();
-
- SaveManager.Instance.Register(this);
-
- playingAudioSources = new List
(); - loopingAudioSources = new List
(); -
- if (!LoadSave())
- {
- IsMusicOn = true;
- IsSoundEffectsOn = true;
- }
- }
-
- private void Start()
- {
- for (int i = 0; i < soundInfos.Count; i++)
- {
- SoundInfo soundInfo = soundInfos[i];
-
- if (soundInfo.playAndLoopOnStart)
- {
- Play(soundInfo.id, true, 0);
- }
- }
- }
-
- private void Update()
- {
- for (int i = 0; i < playingAudioSources.Count; i++)
- {
- AudioSource audioSource = playingAudioSources[i].audioSource;
-
- // If the Audio Source is no longer playing then return it to the pool so it can be re-used
- if (!audioSource.isPlaying)
- {
- Destroy(audioSource.gameObject);
- playingAudioSources.RemoveAt(i);
- i--;
- }
- }
- }
-
- #endregion
-
- #region Public Methods
-
- ///
- /// Plays the sound with the give id
- ///
- public void Play(string id)
- {
- Play(id, false, 0);
- }
-
- ///
- /// Plays the sound with the give id, if loop is set to true then the sound will only stop if the Stop method is called
- ///
- public void Play(string id, bool loop, float playDelay)
- {
- SoundInfo soundInfo = GetSoundInfo(id);
-
- if (soundInfo == null)
- {
- Debug.LogError("[SoundManager] There is no Sound Info with the given id: " + id);
-
- return;
- }
-
- if ((soundInfo.type == SoundType.Music && !IsMusicOn) ||
- (soundInfo.type == SoundType.SoundEffect && !IsSoundEffectsOn))
- {
- return;
- }
-
- AudioSource audioSource = CreateAudioSource(id);
-
- audioSource.clip = soundInfo.audioClip;
- audioSource.loop = loop;
- audioSource.time = 0;
- audioSource.volume = soundInfo.clipVolume;
-
- if (playDelay > 0)
- {
- audioSource.PlayDelayed(playDelay);
- }
- else
- {
- audioSource.Play();
- }
-
- PlayingSound playingSound = new PlayingSound();
-
- playingSound.soundInfo = soundInfo;
- playingSound.audioSource = audioSource;
-
- if (loop)
- {
- loopingAudioSources.Add(playingSound);
- }
- else
- {
- playingAudioSources.Add(playingSound);
- }
- }
-
- ///
- /// Stops all playing sounds with the given id
- ///
- public void Stop(string id)
- {
- StopAllSounds(id, playingAudioSources);
- StopAllSounds(id, loopingAudioSources);
- }
-
- ///
- /// Stops all playing sounds with the given type
- ///
- public void Stop(SoundType type)
- {
- StopAllSounds(type, playingAudioSources);
- StopAllSounds(type, loopingAudioSources);
- }
-
- ///
- /// Sets the SoundType on/off
- ///
- public void SetSoundTypeOnOff(SoundType type, bool isOn)
- {
- switch (type)
- {
- case SoundType.SoundEffect:
-
- if (isOn == IsSoundEffectsOn)
- {
- return;
- }
-
- IsSoundEffectsOn = isOn;
-
- break;
- case SoundType.Music:
-
- if (isOn == IsMusicOn)
- {
- return;
- }
-
- IsMusicOn = isOn;
-
- break;
- }
-
- // If it was turned off then stop all sounds that are currently playing
- if (!isOn)
- {
- Stop(type);
- }
- // Else it was turned on so play any sounds that have playAndLoopOnStart set to true
- else
- {
- PlayAtStart(type);
- }
- }
-
- #endregion
-
- #region Private Methods
-
- ///
- /// Plays all sounds that are set to play on start and loop and are of the given type
- ///
- private void PlayAtStart(SoundType type)
- {
- for (int i = 0; i < soundInfos.Count; i++)
- {
- SoundInfo soundInfo = soundInfos[i];
-
- if (soundInfo.type == type && soundInfo.playAndLoopOnStart)
- {
- Play(soundInfo.id, true, 0);
- }
- }
- }
-
- ///
- /// Stops all sounds with the given id
- ///
- private void StopAllSounds(string id, List
playingSounds ) - {
- for (int i = 0; i < playingSounds.Count; i++)
- {
- PlayingSound playingSound = playingSounds[i];
-
- if (id == playingSound.soundInfo.id)
- {
- playingSound.audioSource.Stop();
- Destroy(playingSound.audioSource.gameObject);
- playingSounds.RemoveAt(i);
- i--;
- }
- }
- }
-
- ///
- /// Stops all sounds with the given type
- ///
- private void StopAllSounds(SoundType type, List
playingSounds ) - {
- for (int i = 0; i < playingSounds.Count; i++)
- {
- PlayingSound playingSound = playingSounds[i];
-
- if (type == playingSound.soundInfo.type)
- {
- playingSound.audioSource.Stop();
- Destroy(playingSound.audioSource.gameObject);
- playingSounds.RemoveAt(i);
- i--;
- }
- }
- }
-
- private SoundInfo GetSoundInfo(string id)
- {
- for (int i = 0; i < soundInfos.Count; i++)
- {
- if (id == soundInfos[i].id)
- {
- return soundInfos[i];
- }
- }
-
- return null;
- }
-
- private AudioSource CreateAudioSource(string id)
- {
- GameObject obj = new GameObject("sound_" + id);
-
- obj.transform.SetParent(transform);
-
- return obj.AddComponent
();; - }
-
- #endregion
-
- #region Save Methods
-
- public Dictionary<string, object> Save()
- {
- Dictionary<string, object> json = new Dictionary<string, object>();
-
- json["is_music_on"] = IsMusicOn;
- json["is_sound_effects_on"] = IsSoundEffectsOn;
-
- return json;
- }
-
- public bool LoadSave()
- {
- JSONNode json = SaveManager.Instance.LoadSave(this);
-
- if (json == null)
- {
- return false;
- }
-
- IsMusicOn = json["is_music_on"].AsBool;
- IsSoundEffectsOn = json["is_sound_effects_on"].AsBool;
-
- return true;
- }
-
- #endregion
- }
- }
如上所述的soundmanager,里面有两个内容是告知savemanager如何自动储存信息的
- public string SaveId { get { return "sound_manager"; } }
-
- public Dictionary<string, object> Save()
- {
- Dictionary<string, object> json = new Dictionary<string, object>();
-
- json["is_music_on"] = IsMusicOn;
- json["is_sound_effects_on"] = IsSoundEffectsOn;
-
- return json;
- }
-
-
-
另外,观察soundmanager可知,它在初始化时,去做了一次loadsave函数,也就是去找savemanager要数据,如果要到了,怎样设置,如果没有要到,怎样设置。
-
- public bool LoadSave()
- {
- JSONNode json = SaveManager.Instance.LoadSave(this);
-
- if (json == null)
- {
- return false;
- }
-
- IsMusicOn = json["is_music_on"].AsBool;
- IsSoundEffectsOn = json["is_sound_effects_on"].AsBool;
-
- return true;
- }
先看一下这个gamemanager中与储存相关的代码
- public Dictionary<string, object> Save()
- {
- Dictionary<string, object> json = new Dictionary<string, object>();
-
- json["num_stars_earned"] = SaveNumStarsEarned();
- json["last_completed"] = SaveLastCompleteLevels();
- json["level_statuses"] = SaveLevelStatuses();
- json["level_save_datas"] = SaveLevelDatas();
- json["star_amount"] = StarAmount;
- json["hint_amount"] = HintAmount;
- json["num_levels_till_ad"] = NumLevelsTillAd;
-
- return json;
- }
-
- private List<object> SaveNumStarsEarned()
- {
- List<object> json = new List<object>();
-
- foreach (KeyValuePair<string, int> pair in packNumStarsEarned)
- {
- Dictionary<string, object> packJson = new Dictionary<string, object>();
-
- packJson["pack_id"] = pair.Key;
- packJson["num_stars_earned"] = pair.Value;
-
- json.Add(packJson);
- }
-
- return json;
- }
-
- private List<object> SaveLastCompleteLevels()
- {
- List<object> json = new List<object>();
-
- foreach (KeyValuePair<string, int> pair in packLastCompletedLevel)
- {
- Dictionary<string, object> packJson = new Dictionary<string, object>();
-
- packJson["pack_id"] = pair.Key;
- packJson["last_completed_level"] = pair.Value;
-
- json.Add(packJson);
- }
-
- return json;
- }
-
- private List<object> SaveLevelStatuses()
- {
- List<object> json = new List<object>();
-
- foreach (KeyValuePair<string, Dictionary<int, int>> pair in packLevelStatuses)
- {
- Dictionary<string, object> packJson = new Dictionary<string, object>();
-
- packJson["pack_id"] = pair.Key;
-
- string levelStr = "";
-
- foreach (KeyValuePair<int, int> levelPair in pair.Value)
- {
- if (!string.IsNullOrEmpty(levelStr)) levelStr += "_";
- levelStr += levelPair.Key + "_" + levelPair.Value;
- }
-
- packJson["level_statuses"] = levelStr;
-
- json.Add(packJson);
- }
-
- return json;
- }
-
- private List<object> SaveLevelDatas()
- {
- List<object> savedLevelDatas = new List<object>();
-
- foreach (KeyValuePair<string, LevelSaveData> pair in levelSaveDatas)
- {
- Dictionary<string, object> levelSaveDataJson = pair.Value.Save();
-
- levelSaveDataJson["id"] = pair.Key;
-
- savedLevelDatas.Add(levelSaveDataJson);
- }
-
- return savedLevelDatas;
- }
-
- private bool LoadSave()
- {
- JSONNode json = SaveManager.Instance.LoadSave(this);
-
- if (json == null)
- {
- return false;
- }
-
- LoadNumStarsEarned(json["num_stars_earned"].AsArray);
- LoadLastCompleteLevels(json["last_completed"].AsArray);
- LoadLevelStatuses(json["level_statuses"].AsArray);
- LoadLevelSaveDatas(json["level_save_datas"].AsArray);
-
- StarAmount = json["star_amount"].AsInt;
- HintAmount = json["hint_amount"].AsInt;
- NumLevelsTillAd = json["num_levels_till_ad"].AsInt;
-
- return true;
- }
-
- private void LoadNumStarsEarned(JSONArray json)
- {
- for (int i = 0; i < json.Count; i++)
- {
- JSONNode childJson = json[i];
-
- string packId = childJson["pack_id"].Value;
- int numStarsEarned = childJson["num_stars_earned"].AsInt;
-
- packNumStarsEarned.Add(packId, numStarsEarned);
- }
- }
-
- private void LoadLastCompleteLevels(JSONArray json)
- {
- for (int i = 0; i < json.Count; i++)
- {
- JSONNode childJson = json[i];
-
- string packId = childJson["pack_id"].Value;
- int lastCompletedLevel = childJson["last_completed_level"].AsInt;
-
- packLastCompletedLevel.Add(packId, lastCompletedLevel);
- }
- }
-
- private void LoadLevelStatuses(JSONArray json)
- {
- for (int i = 0; i < json.Count; i++)
- {
- JSONNode childJson = json[i];
-
- string packId = childJson["pack_id"].Value;
- string[] levelStatusStrs = childJson["level_statuses"].Value.Split('_');
-
- Dictionary<int, int> levelStatuses = new Dictionary<int, int>();
-
- for (int j = 0; j < levelStatusStrs.Length; j += 2)
- {
- int levelIndex = System.Convert.ToInt32(levelStatusStrs[j]);
- int status = System.Convert.ToInt32(levelStatusStrs[j + 1]);
-
- levelStatuses.Add(levelIndex, status);
- }
-
- packLevelStatuses.Add(packId, levelStatuses);
- }
- }
-
- ///
- /// Loads the game from the saved json file
- ///
- private void LoadLevelSaveDatas(JSONArray savedLevelDatasJson)
- {
- // Load all the placed line segments for levels that have progress
- for (int i = 0; i < savedLevelDatasJson.Count; i++)
- {
- JSONNode savedLevelDataJson = savedLevelDatasJson[i];
- JSONArray savedPlacedLineSegments = savedLevelDataJson["placed_line_segments"].AsArray;
- JSONArray savedHints = savedLevelDataJson["hints"].AsArray;
-
- List
> placedLineSegments = new List>();
-
- for (int j = 0; j < savedPlacedLineSegments.Count; j++)
- {
- placedLineSegments.Add(new List
()); -
- for (int k = 0; k < savedPlacedLineSegments[j].Count; k += 2)
- {
- placedLineSegments[j].Add(new CellPos(savedPlacedLineSegments[j][k].AsInt, savedPlacedLineSegments[j][k + 1].AsInt));
- }
- }
-
- List<int> hintLineIndices = new List<int>();
-
- for (int j = 0; j < savedHints.Count; j++)
- {
- hintLineIndices.Add(savedHints[j].AsInt);
- }
-
- string levelId = savedLevelDataJson["id"].Value;
- int numMoves = savedLevelDataJson["num_moves"].AsInt;
-
- LevelSaveData levelSaveData = new LevelSaveData();
-
- levelSaveData.placedLineSegments = placedLineSegments;
- levelSaveData.numMoves = numMoves;
- levelSaveData.hintLineIndices = hintLineIndices;
-
- levelSaveDatas.Add(levelId, levelSaveData);
- }
- }
-
- #endregion
我们发现,因为数据较为复杂,无论是load还是save,都针对不同数据有自己的辅助函数。
gamemanager中,有一个startlevel,它需要一个packinfo(总关卡信息,可暂时忽略),以及一个leveldata.
这里拿到的leveldata,是制作者本身就默认写好的值,
如果这个leveldata的id,已经存在于levelsavedata的字典中,就说明这个leveldata经过了修改,因此要读取的是新的levelsavedata中的配置数据。
如果这个leveldata的id没有存在于levelsavedata的字典中,就说明这次是第一次打开这个level,那么需要新建一个savedata:
下面的代码记录了这个功能。
-
- ///
- /// Starts the level.
- ///
- public void StartLevel(PackInfo packInfo, LevelData levelData)
- {
- ActivePackInfo = packInfo;
- ActiveLevelData = levelData;
-
- // Check if the lvel has not been started and if there is loaded save data for it
- if (!levelSaveDatas.ContainsKey(levelData.Id))
- {
- levelSaveDatas[levelData.Id] = new LevelSaveData();
- }
-
- gameGrid.SetupLevel(levelData, levelSaveDatas[levelData.Id]);
-
- UpdateHintAmountText();
- UpdateLevelButtons();
-
- GameEventManager.Instance.SendEvent(GameEventManager.EventId_LevelStarted);
-
- ScreenManager.Instance.Show("game");
-
- // Check if it's time to show an interstitial ad
- if (NumLevelsTillAd <= 0)
- {
- NumLevelsTillAd = numLevelsBetweenAds;
-
- #if BBG_MT_ADS
- BBG.MobileTools.MobileAdsManager.Instance.ShowInterstitialAd();
- #endif
- }
- }
其他的功能基本上和soundmanager一样:
比如,在初始化时,注册自己,并试图loadsave.
比如,在游戏中断时,进行保存
- protected override void Awake()
- {
- base.Awake();
-
- GameEventManager.Instance.RegisterEventHandler(GameEventManager.EventId_ActiveLevelCompleted, OnActiveLevelComplete);
-
- SaveManager.Instance.Register(this);
-
- packNumStarsEarned = new Dictionary<string, int>();
- packLastCompletedLevel = new Dictionary<string, int>();
- packLevelStatuses = new Dictionary<string, Dictionary<int, int>>();
- levelSaveDatas = new Dictionary<string, LevelSaveData>();
-
- if (!LoadSave())
- {
- HintAmount = startingHints;
- NumLevelsTillAd = numLevelsBetweenAds;
- }
-
- gameGrid.Initialize();
-
- if (startingStars > 0)
- {
- StarAmount = startingStars;
- }
- }
-
- private void OnDestroy()
- {
- Save();
- }
-
- private void OnApplicationPause(bool pause)
- {
- if (pause)
- {
- Save();
- }
- }
至此,重点结束。
然后,其他功能的脚本,可以通过获得currentlevelsavedata的方式去修改其数据,方便在关闭界面时进行数据更新。
-
- ///
- /// Sets the numMoves and updates the Text UI
- ///
- private void SetNumMoves(int amount)
- {
- currentLevelSaveData.numMoves = amount;
-
- moveAmountText.text = currentLevelSaveData.numMoves.ToString();
- }
围绕这个功能,还可以方便设计undo/redo功能