我的最终目标是快读建立一个关卡数据自动读入储存功能:

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功能

10-25 00:23