第八章 游戏中的数据(Data in Game)
在之前的章节中,我们进行地图对象的生成,移动等操作。
这一章本来可以进行战斗的编写,不过数据缺失是一个问题。
所以这一章我们先来建立一些数据,以及如何编辑它们,是否需要生成配置文件等。
文章目录
四 数据编辑器(Data Editor)
我们将进行一个EditorWindow
的编写,并将数据显示在上面。
要解决的主要问题是显示的数量(上一节中提过的,如果数量过多,会造成渲染卡顿)。
除了上一节内容,我没有进行更详细的编辑,你可以对每个数据文件都进行PropertyDrawer
的编写。
1 创建编辑器窗口(Create Editor Window)
我们先来创建一个最基本的窗口(EditorSrpgDataEditorWindow.cs
):
namespace DR.Book.SRPG_Dev.Models
{
public class EditorSrpgDataEditorWindow : EditorWindow
{
private static EditorSrpgDataEditorWindow s_Window;
public static EditorSrpgDataEditorWindow OpenEditorSrpgDataEditorWindow()
{
if (s_Window != null)
{
s_Window.Focus();
return s_Window;
}
s_Window = EditorWindow.GetWindow<EditorSrpgDataEditorWindow>(false, "SRPG Data");
s_Window.minSize = new Vector2(480, 480);
s_Window.Show();
return s_Window;
}
private EditorSrpgData m_SrpgData;
private SerializedObject m_SerializedObject;
public EditorSrpgData srpgData
{
get { return m_SrpgData; }
set
{
if (m_SrpgData == value)
{
return;
}
m_SrpgData = value;
// 删除以前的
if (m_SerializedObject != null)
{
m_SerializedObject.Dispose();
m_SerializedObject = null;
}
// 重新建立
if (m_SrpgData != null)
{
m_SerializedObject = new SerializedObject(m_SrpgData);
}
}
}
private void OnDestroy()
{
this.srpgData = null;
s_Window = null;
}
// TODO 其它需要添加的
}
}
你会看到我没有使用[MenuItem("Window/SRPG/SRPG Data Editor")]
,因为我不打算在菜单中打开它。
1.1 打开窗口(Open Editor Window)
将打开窗口和资源放在一起,建立文件EditorSrpgDataEditor.cs
:
using UnityEngine;
using UnityEditor;
namespace DR.Book.SRPG_Dev.Models
{
[CustomEditor(typeof(EditorSrpgData))]
public class EditorSrpgDataEditor : Editor
{
#region Property
public EditorSrpgData srpgData
{
get { return target as EditorSrpgData; }
}
#endregion
#region Unity Callback
public override void OnInspectorGUI()
{
EditorGUI.BeginDisabledGroup(true);
base.OnInspectorGUI();
EditorGUI.EndDisabledGroup();
if (GUILayout.Button("Edit Datas"))
{
EditorSrpgDataEditorWindow window = EditorSrpgDataEditorWindow.OpenEditorSrpgDataEditorWindow();
window.srpgData = srpgData;
}
}
#endregion
}
}
1.2 选择数据(Select Data)
在窗口中不要全部渲染,最好可按文件选择我们的数据。
在EditorSrpgData
中添加属性:
public enum ConfigType
{
MoveConsumption,
Class,
Character,
Item,
Text
}
[SerializeField]
public ConfigType currentConfig = ConfigType.MoveConsumption;
最终效果:
- 图 8.9 EditorSrpgData Inspector
2 功能接口(IEditorConfigSerializer.cs)
我们的编辑器至少具有的功能:
-
编辑数据;
-
保存配置文件;
-
读取配置文件。
除了这些功能外,最好还能排序(根据key
),还要能检测是否有重复的key
。
建立文件IEditorConfigSerializer.cs
:
using System;
namespace DR.Book.SRPG_Dev.Models
{
public interface IEditorConfigSerializer
{
Array EditorGetKeys();
void EditorSortDatas();
byte[] EditorSerializeToBytes();
void EditorDeserializeToObject(byte[] bytes);
}
}
有了接口,我们在EditorSrpgData.cs
中加入方法:
public IEditorConfigSerializer GetCurConfig()
{
switch (currentConfig)
{
case ConfigType.MoveConsumption:
return moveConsumptionConfig;
case ConfigType.Class:
return classConfig;
case ConfigType.Character:
return characterInfoConfig;
case ConfigType.Item:
return itemInfoConfig;
case ConfigType.Text:
return textInfoConfig;
default:
return null;
}
}
同时我们让所有配置文件都继承这个接口:
[Serializable]
public abstract class BaseXmlConfig<TKey, TData> : XmlConfigFile, IEditorConfigSerializer
where TData : class, IConfigData<TKey>
[Serializable]
public class BaseTxtConfig<TKey, TData> : TxtConfigFile, IEditorConfigSerializer
where TData : class, ITxtConfigData<TKey>, new()
获取key
与排序都是对datas
进行,所以应该是一样的:
/// <summary>
/// 获取所有Key
/// </summary>
/// <returns></returns>
Array IEditorConfigSerializer.EditorGetKeys()
{
if (datas == null)
{
return default(Array);
}
else
{
return datas.Select(data => data.GetKey()).ToArray();
}
}
void IEditorConfigSerializer.EditorSortDatas()
{
if (datas != null)
{
Array.Sort(datas, (data1, data2) =>
{
return data1.GetKey().GetHashCode().CompareTo(data2.GetKey().GetHashCode());
});
}
}
2.1 保存与读取Xml文件(Save or Load Xml File)
这个和我们的序列化是差不多,曾经我们已经写过了,这里不再阐述。
序列化:
public virtual byte[] EditorSerializeToBytes()
{
byte[] bytes;
using (MemoryStream ms = new MemoryStream())
{
using (StreamWriter sw = new StreamWriter(ms, Encoding.UTF8))
{
XmlSerializer xs = new XmlSerializer(GetType());
XmlSerializerNamespaces xsn = new XmlSerializerNamespaces();
xsn.Add("", "");
xs.Serialize(sw, this, xsn);
bytes = ms.ToArray();
}
}
return bytes;
}
反序列化:
public virtual void EditorDeserializeToObject(byte[] bytes)
{
XmlConfigFile config;
using (MemoryStream ms = new MemoryStream(bytes))
{
using (StreamReader sr = new StreamReader(ms, Encoding.UTF8))
{
XmlSerializer xs = new XmlSerializer(GetType());
config = xs.Deserialize(sr) as XmlConfigFile;
}
}
datas = (config as BaseXmlConfig<TKey, TData>).datas;
}
2.2 保存与读取Txt文件(Save or Load Txt File)
我们之前写过txt
文件的反序列化,但那是对字典的;这里我们要对保留的datas
进行填充。
也和之前说的一样, txt
文件可没有现成的序列化方法,每个文件都是不同的。 这样每个文件都重写一次方法显然是很麻烦, 所以我们采用反射方法,这样除了特殊的txt
外,都可以用这个通用的方法。
使用反射保存文件:
public virtual byte[] EditorSerializeToBytes()
{
if (datas == null)
{
datas = new TData[0];
}
StringBuilder builder = new StringBuilder();
// 反射获取所有public字段
Type dataType = typeof(TData);
FieldInfo[] fields = dataType.GetFields(BindingFlags.Instance
| BindingFlags.Public
| BindingFlags.GetField
| BindingFlags.SetField);
if (fields.Length != 0)
{
// 每一列的名字
string[] line = fields.Select(field => field.Name).ToArray();
builder.AppendLine(k_CommentingPrefix + string.Join("\t", line));
// 每一行数据
for (int i = 0; i < datas.Length; i++)
{
line = fields.Select(field => field.GetValue(datas[i]).ToString()).ToArray();
builder.AppendLine(string.Join("\t", line));
}
}
return Encoding.UTF8.GetBytes(builder.ToString().Trim());
}
读取文件:
public virtual void EditorDeserializeToObject(byte[] bytes)
{
string buffer = Encoding.UTF8.GetString(bytes).Trim();
// 分割行
string[] lines = buffer.Split(
new string[] { Environment.NewLine },
StringSplitOptions.RemoveEmptyEntries);
List<TData> loadedDatas = new List<TData>();
for (int i = 0; i < lines.Length; i++)
{
string line = lines[i].Trim();
// 如果是注释,直接下一条
if (line.StartsWith(k_CommentingPrefix))
{
continue;
}
TData data = new TData();
if (data.FormatText(line))
{
loadedDatas.Add(data);
}
}
datas = loadedDatas.ToArray();
}
3 绘制主函数(OnGUI)
回来EditorSrpgDataEditorWindow
中,我们来填充我们的OnGUI
方法:
private Vector2 m_Scroll;
private GUILayoutOption m_BtnWidth = GUILayout.MaxWidth(120);
private void OnGUI()
{
// TODO
}
首先,我们的对象不能为空,且能够选择绘制的类型:
EditorGUI.BeginDisabledGroup(true);
srpgData = (EditorSrpgData)EditorGUILayout.ObjectField("SRPG Data Editor", srpgData, typeof(EditorSrpgData), false);
EditorGUI.EndDisabledGroup();
if (srpgData == null || m_SerializedObject == null)
{
EditorGUILayout.HelpBox("Please re-open a SRPG Data Editor Window.", MessageType.Info);
return;
}
m_SerializedObject.Update();
// 绘制选择类型
SerializedProperty curConfigTypeProperty = m_SerializedObject.FindProperty("currentConfig");
EditorGUILayout.PropertyField(curConfigTypeProperty, true);
EditorGUILayout.Space();
// TODO
其次,是我们的功能按钮(方法之后再填充):
// 绘制按钮
if (!DoDrawButtons())
{
return;
}
最后,绘制我们当前的数据(方法之后再填充):
// 绘制数据
if (!DoDrawDatas())
{
return;
}
4 绘制按钮(Draw Buttons)
我们按钮的功能主要分为:
-
保存数据成配置文件:
SaveToFile
; -
读取配置文件:
LoadFromFile
; -
检测重复的
key
:CheckDumplicateKeys
; -
根据
key
排序数据:SortWithKeys
。
即,创建DoDrawButtons()
:
private GUILayoutOption m_BtnWidth = GUILayout.MaxWidth(120);
/// <summary>
/// 绘制按钮
/// </summary>
private bool DoDrawButtons()
{
IEditorConfigSerializer config = srpgData.GetCurConfig();
if (config == null)
{
EditorGUILayout.HelpBox(
string.Format("{0} Config is not found.", srpgData.currentConfig.ToString()),
MessageType.Error);
return false;
}
EditorGUILayout.BeginHorizontal();
{
if (GUILayout.Button("Save To File", m_BtnWidth))
{
SaveToFile(config);
}
if (GUILayout.Button("Load From File", m_BtnWidth))
{
LoadFromFile(config);
}
if (GUILayout.Button("Check Keys", m_BtnWidth))
{
CheckDuplicateKeys(config);
}
if (GUILayout.Button("Sort Datas", m_BtnWidth))
{
SortWithKeys(config);
}
}
EditorGUILayout.EndHorizontal();
return true;
}
我们让它们水平方向排列:
- 图 8.10 EditorSrpgDataWindow Buttons
4.1 保存文件(Save To File)
将配置文件转换成byte[]
的方法我们已经写过了,只需要获取相应的路径然后保存就可以了。 在UnityEditor中,获取保存文件路径的方法是 EditorUtility.SaveFilePanel 。而在保存之前,我们需要检测一下文件是否合法(是否有重复的key
)。
private void SaveToFile(IEditorConfigSerializer config)
{
string ext = (config is XmlConfigFile) ? "xml" : "txt";
string path = EditorUtility.SaveFilePanel(
"Save", Application.streamingAssetsPath, config.GetType().Name, ext);
if (!string.IsNullOrEmpty(path))
{
if (!CheckDuplicateKeys(config))
{
Debug.LogError("Config to save has some `Duplicate Keys`. Save Failure.");
return;
}
try
{
byte[] bytes = config.EditorSerializeToBytes();
File.WriteAllBytes(path, bytes);
AssetDatabase.Refresh();
}
catch (Exception e)
{
Debug.LogError("Save ERROR: " + e.ToString());
return;
}
}
}
4.2 读取文件(Load From File)
类似保存文件, 在UnityEditor中,获取读取文件路径的方法是 EditorUtility.OpenFilePanel 。而在读取之后,我们也检测一下文件是否合法(是否有重复的key
)。
private void LoadFromFile(IEditorConfigSerializer config)
{
string ext = (config is XmlConfigFile) ? "xml" : "txt";
string path = EditorUtility.OpenFilePanel(
"Load", Application.streamingAssetsPath, ext);
if (!string.IsNullOrEmpty(path))
{
try
{
byte[] bytes = File.ReadAllBytes(path);
config.EditorDeserializeToObject(bytes);
EditorUtility.SetDirty(srpgData);
Repaint();
}
catch (Exception e)
{
Debug.LogError("Load ERROR: " + e.ToString());
return;
}
if (!CheckDuplicateKeys(config))
{
Debug.LogError("Loaded File has some `Duplicate Keys`.");
return;
}
}
}
4.3 检测重复键值(Check Dumplicate Keys)
如果只是检测是否有重复的key
是非常简单的, 但我们需要知道是哪些数据重复了,这就需要我们保存重复的key
与其在datas
中相应的下标告知我们。
/// <summary>
/// 检查重复的Key
/// </summary>
/// <returns></returns>
private bool CheckDuplicateKeys(IEditorConfigSerializer config)
{
// 获取所有key
Array keys = config.EditorGetKeys();
// key : index
Dictionary<object, int> keySet = new Dictionary<object, int>();
// dumplicate [key : indexes]
Dictionary<object, HashSet<string>> duplicateKeys = new Dictionary<object, HashSet<string>>();
for (int i = 0; i < keys.Length; i++)
{
object key = keys.GetValue(i);
// 如果key重复了
if (keySet.ContainsKey(key))
{
// 如果重复key的set没有建立
if (!duplicateKeys.ContainsKey(key))
{
// 建立set,并加入最初的下标
duplicateKeys[key] = new HashSet<string>
{
keySet[key].ToString()
};
}
// 加入当前下标
duplicateKeys[key].Add(i.ToString());
}
else
{
keySet.Add(key, i);
}
}
if (duplicateKeys.Count != 0)
{
// 打印所有重复的keys
foreach (var kvp in duplicateKeys)
{
Debug.LogErrorFormat(
"Duplicate Keys \"{0}\": Index [{1}]",
kvp.Key.ToString(),
string.Join(", ", kvp.Value.ToArray()));
}
return false;
}
return true;
}
4.4 排序数据(Sort With Keys)
排序方法只需要直接调用:
private void SortWithKeys(IEditorConfigSerializer config)
{
config.EditorSortDatas();
}
5 绘制数据(Draw Datas)
我们绘制数据的主要目的是为了减轻渲染压力,不要一次性把列表中的所有数据都显示出来;并且可以选择绘制的范围。
为了简化,我没有自定义更多方法(目前够用了)。要自定义更多,可以创建更多的PropertyDrawer
。
要绘制数据,首先得先获取数据:
/// <summary>
/// 获取当前config
/// </summary>
/// <returns></returns>
private SerializedProperty GetConfigProperty()
{
switch (srpgData.currentConfig)
{
case EditorSrpgData.ConfigType.MoveConsumption:
return m_SerializedObject.FindProperty("moveConsumptionConfig");
case EditorSrpgData.ConfigType.Class:
return m_SerializedObject.FindProperty("classConfig");
case EditorSrpgData.ConfigType.Character:
return m_SerializedObject.FindProperty("characterInfoConfig");
case EditorSrpgData.ConfigType.Item:
return m_SerializedObject.FindProperty("itemInfoConfig");
case EditorSrpgData.ConfigType.Text:
return m_SerializedObject.FindProperty("textInfoConfig");
default:
return null;
}
}
我们要知道,我们编辑数据全部存储在配置文件中的datas
字段中,而这个字段也用于序列化,且它是一个数组;再加上之前的分析,我们目前需要:
-
获取我们的数组属性,使用一个
SerializedProperty
; -
显示的范围,使用一个
Vector2Int
,规定至少渲染20条数据; -
还有一个
ScrollView
滚动条,在UnityEditor中使用Vector2
。
所以建立DoDrawDatas()
private Vector2Int m_SelectedRange;
private Vector2 m_Scroll;
/// <summary>
/// 绘制具体信息
/// </summary>
private bool DoDrawDatas()
{
SerializedProperty curConfigProperty = GetConfigProperty();
if (curConfigProperty == null)
{
EditorGUILayout.HelpBox(
string.Format("{0} Config Property is not found.", srpgData.currentConfig.ToString()),
MessageType.Error);
return false;
}
SerializedProperty curArrayDatasProperty = curConfigProperty.FindPropertyRelative("datas");
EditorGUI.BeginChangeCheck();
// 设置数量
int arraySize = Mathf.Max(0, EditorGUILayout.DelayedIntField("Size", curArrayDatasProperty.arraySize));
curArrayDatasProperty.arraySize = arraySize;
if (arraySize != 0)
{
// 最少显示20个
m_SelectedRange = EditorGUILayout.Vector2IntField("Index Range", m_SelectedRange);
m_SelectedRange.x = Mathf.Max(0, Mathf.Min(m_SelectedRange.x, arraySize - 20));
m_SelectedRange.y = Mathf.Min(arraySize - 1, Mathf.Max(m_SelectedRange.x + 19, m_SelectedRange.y));
Vector2 range = m_SelectedRange;
EditorGUILayout.MinMaxSlider(
ref range.x,
ref range.y,
0,
arraySize - 1);
m_SelectedRange.x = (int)range.x;
m_SelectedRange.y = (int)range.y;
// 绘制数据
m_Scroll = EditorGUILayout.BeginScrollView(m_Scroll, "box");
{
for (int i = m_SelectedRange.x; i <= m_SelectedRange.y; i++)
{
SerializedProperty property = curArrayDatasProperty.GetArrayElementAtIndex(i);
EditorGUILayout.PropertyField(property, true);
}
}
EditorGUILayout.EndScrollView();
}
m_SerializedObject.ApplyModifiedProperties();
if (EditorGUI.EndChangeCheck())
{
EditorUtility.SetDirty(srpgData);
}
return true;
}
这样就完成了,打开窗口查看:
- 图 8.11 EditorSrpgDataWindow Datas