我的最终目标是快读建立一个关卡数据自动读入储存功能:
1. 每个关卡有自己的编号,如果没有自定义该关卡,则读取默认编号的初始布局,如果有自定义该关卡,则读取新定义的关卡。
2.在游戏中如果对布局做出了更改,随时储存新的修改。
3.save和load系统与玩法系统耦合度低,无需管理。
小试牛刀-soundmanager
先从一个简单的soundmanager开始学习。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace BBG
{
public class SaveManager : SingletonComponent<SaveManager>
{
#region Member Variables
private List<ISaveable> saveables;
private JSONNode loadedSave;
#endregion
#region Properties
/// <summary>
/// Path to the save file on the device
/// </summary>
public string SaveFilePath { get { return Application.persistentDataPath + "/save.json"; } }
/// <summary>
/// List of registered saveables
/// </summary>
private List<ISaveable> Saveables
{
get
{
if (saveables == null)
{
saveables = new List<ISaveable>();
}
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
/// <summary>
/// Registers a saveable to be saved
/// </summary>
public void Register(ISaveable saveable)
{
Saveables.Add(saveable);
}
/// <summary>
/// Loads the save data for the given saveable
/// </summary>
public JSONNode LoadSave(ISaveable saveable)
{
return LoadSave(saveable.SaveId);
}
/// <summary>
/// Loads the save data for the given save id
/// </summary>
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
/// <summary>
/// Saves all registered saveables to the save file
/// </summary>
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));
}
/// <summary>
/// Tries to load the save file
/// </summary>
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<SoundInfo> soundInfos = null;
#endregion
#region Member Variables
private List<PlayingSound> playingAudioSources;
private List<PlayingSound> 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<PlayingSound>();
loopingAudioSources = new List<PlayingSound>();
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
/// <summary>
/// Plays the sound with the give id
/// </summary>
public void Play(string id)
{
Play(id, false, 0);
}
/// <summary>
/// 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
/// </summary>
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);
}
}
/// <summary>
/// Stops all playing sounds with the given id
/// </summary>
public void Stop(string id)
{
StopAllSounds(id, playingAudioSources);
StopAllSounds(id, loopingAudioSources);
}
/// <summary>
/// Stops all playing sounds with the given type
/// </summary>
public void Stop(SoundType type)
{
StopAllSounds(type, playingAudioSources);
StopAllSounds(type, loopingAudioSources);
}
/// <summary>
/// Sets the SoundType on/off
/// </summary>
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
/// <summary>
/// Plays all sounds that are set to play on start and loop and are of the given type
/// </summary>
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);
}
}
}
/// <summary>
/// Stops all sounds with the given id
/// </summary>
private void StopAllSounds(string id, List<PlayingSound> 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--;
}
}
}
/// <summary>
/// Stops all sounds with the given type
/// </summary>
private void StopAllSounds(SoundType type, List<PlayingSound> 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<AudioSource>();;
}
#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);
}
}
/// <summary>
/// Loads the game from the saved json file
/// </summary>
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<List<CellPos>> placedLineSegments = new List<List<CellPos>>();
for (int j = 0; j < savedPlacedLineSegments.Count; j++)
{
placedLineSegments.Add(new List<CellPos>());
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:
下面的代码记录了这个功能。
/// <summary>
/// Starts the level.
/// </summary>
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的方式去修改其数据,方便在关闭界面时进行数据更新。
/// <summary>
/// Sets the numMoves and updates the Text UI
/// </summary>
private void SetNumMoves(int amount)
{
currentLevelSaveData.numMoves = amount;
moveAmountText.text = currentLevelSaveData.numMoves.ToString();
}
围绕这个功能,还可以方便设计undo/redo功能