我的最终目标是快读建立一个关卡数据自动读入储存功能:
1. 每个关卡有自己的编号,如果没有自定义该关卡,则读取默认编号的初始布局,如果有自定义该关卡,则读取新定义的关卡。
2.在游戏中如果对布局做出了更改,随时储存新的修改。
3.save和load系统与玩法系统耦合度低,无需管理。
先从一个简单的soundmanager开始学习。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace BBG
{
public class SaveManager : SingletonComponent
{
#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 saveJson = new Dictionary();
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, 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 Save()
{
Dictionary json = new Dictionary();
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 Save()
{
Dictionary json = new Dictionary();
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 Save()
{
Dictionary json = new Dictionary();
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
我们发现,因为数据较为复杂,无论是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();
packLastCompletedLevel = new Dictionary();
packLevelStatuses = new Dictionary>();
levelSaveDatas = new Dictionary();
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功能