Unity 分数统计系统
CoderZ1010 人气:0项目中经常遇到分数统计的需求,例如我们执行了某项操作或做了某个题目,操作正确则计分,相反则不计分失去该项分数,为了应对需求需要一个分数统计系统。
首先定义一个分数信息的数据结构,使用Serializable特性使其可序列化:
using System; using UnityEngine; namespace SK.Framework { /// <summary> /// 分数信息 /// </summary> [Serializable] public class ScoreInfo { /// <summary> /// ID /// </summary> public int id; /// <summary> /// 描述 /// </summary> [TextArea] public string description; /// <summary> /// 分值 /// </summary> public float value; } }
ScoreInfo类可序列化后,创建ScoreProfile类继承ScriptableObject使其作为可通过菜单创建的Asset资产:
using UnityEngine; namespace SK.Framework { /// <summary> /// 分数配置文件 /// </summary> [CreateAssetMenu] public class ScoreProfile : ScriptableObject { public ScoreInfo[] scores = new ScoreInfo[0]; } }
使用ScoreIDConstant类编写所有分数项ID常量,创建ScoreID特性并使用PropertyDrawer使其可在面板选择:
namespace SK.Framework { public sealed class ScoreIDConstant { public const int INVALID = -1; } }
using UnityEngine; #if UNITY_EDITOR using UnityEditor; using System; using System.Reflection; using System.Collections; #endif namespace SK.Framework { public class ScoreIDAttribute : PropertyAttribute { } #if UNITY_EDITOR [CustomPropertyDrawer(typeof(ScoreIDAttribute))] public class ScoreIDPropertyAttributeDrawer : PropertyDrawer { private int[] scoreIDArray; private GUIContent[] scoreIDConstArray; public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { return base.GetPropertyHeight(property, label); } public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { if (scoreIDConstArray == null) { ArrayList constants = new ArrayList(); FieldInfo[] fieldInfos = typeof(ScoreIDConstant).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy); for (int i = 0; i < fieldInfos.Length; i++) { var fi = fieldInfos[i]; if (fi.IsLiteral && !fi.IsInitOnly) constants.Add(fi); } FieldInfo[] fieldInfoArray = (FieldInfo[])constants.ToArray(typeof(FieldInfo)); scoreIDArray = new int[fieldInfoArray.Length]; scoreIDConstArray = new GUIContent[fieldInfoArray.Length]; for (int i = 0; i < fieldInfoArray.Length; i++) { scoreIDConstArray[i] = new GUIContent(fieldInfoArray[i].Name); scoreIDArray[i] = (int)fieldInfoArray[i].GetValue(null); } } var index = Array.IndexOf(scoreIDArray, property.intValue); index = Mathf.Clamp(index, 0, scoreIDArray.Length); index = EditorGUI.Popup(position, label, index, scoreIDConstArray); property.intValue = scoreIDArray[index]; } } #endif }
有了ScoreID特性后,用于ScoreInfo中的id字段:
using System; using UnityEngine; namespace SK.Framework { /// <summary> /// 分数信息 /// </summary> [Serializable] public class ScoreInfo { /// <summary> /// ID /// </summary> [ScoreID] public int id; /// <summary> /// 描述 /// </summary> [TextArea] public string description; /// <summary> /// 分值 /// </summary> public float value; } }
数据可配置后,创建分数项Score类,声明以下字段:Flag表示该分数项的标识,注册分数项时返回该标识,用于后续获取或取消该分数项分值;Description即分数项的描述;Value表示该分数项的分值;IsObtained用于标记该分数项的分值是否已经获得。
namespace SK.Framework { /// <summary> /// 分数项 /// </summary> public class Score { /// <summary> /// 标识 /// </summary> public string Flag { get; private set; } /// <summary> /// 描述 /// </summary> public string Description { get; private set; } /// <summary> /// 分值 /// </summary> public float Value { get; private set; } /// <summary> /// 是否已经获得分值 /// </summary> public bool IsObtained { get; set; } public Score(string flag, string description, float value) { Flag = flag; Description = description; Value = value; } } }
为了实现一个分数组合,例如某项操作,通过A操作方式可获得5分,通过B操作方式可获得3分,它们之间是互斥的,即获得了前者的5分,就不会获得后者的3分,创建ScoreGroup类:
using System.Collections.Generic; namespace SK.Framework { /// <summary> /// 分数组合 /// </summary> public class ScoreGroup { /// <summary> /// 组合描述 /// </summary> public string Description { get; private set; } /// <summary> /// 计分模式 /// Additive表示组合内分值进行累加 /// MutuallyExclusive表示组内各分数项互斥 获得其中一项分值 则取消其它项分值 /// </summary> public ValueMode ValueMode { get; private set; } public List<Score> Scores { get; private set; } public ScoreGroup(string description, ValueMode valueMode, params Score[] scores) { Description = description; ValueMode = valueMode; Scores = new List<Score>(scores); } public bool Obtain(string flag) { var target = Scores.Find(m => m.Flag == flag); if (target != null) { switch (ValueMode) { case ValueMode.Additive: target.IsObtained = true; break; case ValueMode.MutuallyExclusive: for (int i = 0; i < Scores.Count; i++) { Scores[i].IsObtained = Scores[i] == target; } break; default: break; } if (ScoreMaster.DebugMode) { ScoreMaster.LogInfo($"获取分数组合 [{Description}] 中标识为 [{flag}] 的分值 [{target.Description}]"); } return true; } if (ScoreMaster.DebugMode) { ScoreMaster.LogError($"分数组合 [{Description}] 中不存在标识为 [{flag}] 的分数项."); } return false; } public bool Cancle(string flag) { var target = Scores.Find(m => m.Flag == flag); if (target != null) { if (ScoreMaster.DebugMode) { ScoreMaster.LogInfo($"取消分数组合 [{Description}] 中标识为 [{flag}] 的分数项分值 [{target.Description}]"); } target.IsObtained = false; return true; } if (ScoreMaster.DebugMode) { ScoreMaster.LogError($"分数组合 [{Description}] 中不存在标识为 [{flag}] 的分数项."); } return false; } } }
namespace SK.Framework { /// <summary> /// 计分方式 /// </summary> public enum ValueMode { /// <summary> /// 累加的 /// </summary> Additive, /// <summary> /// 互斥的 /// </summary> MutuallyExclusive, } }
最终编写分数管理类,封装Create、Obtain、Cancle、GetSum函数,分别用于创建分数组合、获取分数、取消分数、获取总分,实现Editor类使分数信息在Inspector面板可视化:
using System; using UnityEngine; using System.Collections.Generic; #if UNITY_EDITOR using UnityEditor; using System.Reflection; #endif namespace SK.Framework { public class ScoreMaster : MonoBehaviour { #region NonPublic Variables private static ScoreMaster instance; [SerializeField] private ScoreProfile profile; private readonly Dictionary<string, ScoreGroup> groups = new Dictionary<string, ScoreGroup>(); #endregion #region Public Properties public static ScoreMaster Instance { get { if (instance == null) { instance = FindObjectOfType<ScoreMaster>(); } if (instance == null) { instance = new GameObject("[SKFramework.Score]").AddComponent<ScoreMaster>(); instance.profile = Resources.Load<ScoreProfile>("Score Profile"); if (instance.profile == null && DebugMode) { LogError("加载分数信息配置表失败."); } } return instance; } } #endregion #region NonPublic Methods private string[] CreateScore(string description, ValueMode valueMode, params int[] idArray) { Score[] scores = new Score[idArray.Length]; string[] flags = new string[idArray.Length]; for (int i = 0; i < idArray.Length; i++) { var info = Array.Find(profile.scores, m => m.id == idArray[i]); if (info != null) { var flag = Guid.NewGuid().ToString(); flags[i] = flag; scores[i] = new Score(flag, info.description, info.value); if (DebugMode) LogInfo($"创建分数ID为 [{idArray[i]}] 的分数项 [{info.description}] flag: {flag}"); } else if (DebugMode) { LogError($"配置中不存在ID为 [{idArray[i]}] 的分数信息."); } } ScoreGroup group = new ScoreGroup(description, valueMode, scores); groups.Add(description, group); if (DebugMode) { LogInfo($"创建分数组合 [{description}] 计分模式[{valueMode}]"); } return flags; } private bool ObtainValue(string groupDescription, string flag) { if (groups.TryGetValue(groupDescription, out ScoreGroup target)) { return target.Obtain(flag); } if (DebugMode) { LogError($"不存在分数组合 [{groupDescription}]."); } return false; } private bool CancleValue(string groupDescription, string flag) { if (groups.TryGetValue(groupDescription, out ScoreGroup target)) { return target.Cancle(flag); } if (DebugMode) { LogError($"不存在分数组合 [{groupDescription}]."); } return false; } private float GetSumValue() { float retV = 0f; foreach (var kv in groups) { var scores = kv.Value.Scores; for (int i = 0; i < scores.Count; i++) { var score = scores[i]; if (score.IsObtained) { retV += score.Value; } } } return retV; } #endregion #region Public Methods /// <summary> /// 创建分数组合 /// </summary> /// <param name="description">分数组合描述</param> /// <param name="valueMode">分数组计分方式</param> /// <param name="idArray">分数信息ID组合</param> /// <returns>返回分数项标识符组合</returns> public static string[] Create(string description, ValueMode valueMode, params int[] idArray) { return Instance.CreateScore(description, valueMode, idArray); } /// <summary> /// 获取分数组合中指定标识分数项的分值 /// </summary> /// <param name="groupDescription">分数组合</param> /// <param name="flag">分数项标识</param> /// <returns>获取成功返回true 否则返回false</returns> public static bool Obtain(string groupDescription, string flag) { return Instance.ObtainValue(groupDescription, flag); } /// <summary> /// 取消分数组合中指定标识分数项的分值 /// </summary> /// <param name="groupDescription">分数组合</param> /// <param name="flag">分数项标识</param> /// <returns></returns> public static bool Cancle(string groupDescription, string flag) { return Instance.CancleValue(groupDescription, flag); } /// <summary> /// 获取总分值 /// </summary> /// <returns>总分值</returns> public static float GetSum() { return Instance.GetSumValue(); } #endregion #region Debugger public static bool DebugMode = true; public static void LogInfo(string info) { Debug.Log($"<color=cyan><b>[SKFramework.Score.Info]</b></color> --> {info}"); } public static void LogWarn(string warn) { Debug.Log($"<color=yellow><b>[SKFramework.Score.Warn]</b></color> --> {warn}"); } public static void LogError(string error) { Debug.Log($"<color=red><b>[SKFramework.Score.Error]</b></color> --> {error}"); } #endregion } #if UNITY_EDITOR [CustomEditor(typeof(ScoreMaster))] public class ScoreMasterInspector : Editor { private SerializedProperty profile; private Dictionary<string, ScoreGroup> groups; private Dictionary<ScoreGroup, bool> groupFoldout; private void OnEnable() { profile = serializedObject.FindProperty("profile"); } public override void OnInspectorGUI() { EditorGUILayout.PropertyField(profile); if (GUI.changed) { serializedObject.ApplyModifiedProperties(); EditorUtility.SetDirty(target); } if (!Application.isPlaying) return; Color color = GUI.color; GUI.color = Color.cyan; OnRuntimeGUI(); GUI.color = color; } private void OnRuntimeGUI() { if (groupFoldout == null) { groups = typeof(ScoreMaster).GetField("groups", BindingFlags.Instance | BindingFlags.NonPublic) .GetValue(ScoreMaster.Instance) as Dictionary<string, ScoreGroup>; groupFoldout = new Dictionary<ScoreGroup, bool>(); } foreach (var kv in groups) { if (!groupFoldout.ContainsKey(kv.Value)) { groupFoldout.Add(kv.Value, false); } ScoreGroup group = kv.Value; groupFoldout[group] = EditorGUILayout.Foldout(groupFoldout[group], group.Description); if (groupFoldout[group]) { GUILayout.Label($"计分模式: {(group.ValueMode == ValueMode.Additive ? "累加" : "互斥")}"); for (int i = 0; i < group.Scores.Count; i++) { Score score = group.Scores[i]; GUILayout.BeginVertical("Box"); GUI.color = score.IsObtained ? Color.green : Color.cyan; GUILayout.Label($"描述: {score.Description}"); GUILayout.Label($"标识: {score.Flag}"); GUILayout.BeginHorizontal(); GUILayout.Label($"分值: {score.Value} {(score.IsObtained ? "√" : "")}"); GUI.color = Color.cyan; GUILayout.FlexibleSpace(); GUI.color = Color.yellow; if (GUILayout.Button("Obtain", "ButtonLeft", GUILayout.Width(50f))) { ScoreMaster.Obtain(group.Description, score.Flag); } if (GUILayout.Button("Cancle", "ButtonRight", GUILayout.Width(50f))) { ScoreMaster.Cancle(group.Description, score.Flag); } GUI.color = Color.cyan; GUILayout.EndHorizontal(); GUILayout.EndVertical(); } } } GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); GUILayout.Label($"总分: {ScoreMaster.GetSum()}", "LargeLabel"); GUILayout.Space(50f); GUILayout.EndHorizontal(); } } #endif }
测试:
namespace SK.Framework { public sealed class ScoreIDConstant { public const int INVALID = -1; public const int TEST_A = 0; public const int TEST_B = 1; public const int TEST_C = 2; public const int TEST_D = 3; } }
using UnityEngine; using SK.Framework; public class Foo : MonoBehaviour { private string[] flags; private void Start() { flags = ScoreMaster.Create("测试", ValueMode.MutuallyExclusive, ScoreIDConstant.TEST_A, ScoreIDConstant.TEST_B, ScoreIDConstant.TEST_C, ScoreIDConstant.TEST_D); } private void OnGUI() { if (GUILayout.Button("A", GUILayout.Width(200f), GUILayout.Height(50f))) { ScoreMaster.Obtain("测试", flags[0]); } if (GUILayout.Button("B", GUILayout.Width(200f), GUILayout.Height(50f))) { ScoreMaster.Obtain("测试", flags[1]); } if (GUILayout.Button("C", GUILayout.Width(200f), GUILayout.Height(50f))) { ScoreMaster.Obtain("测试", flags[2]); } if (GUILayout.Button("D", GUILayout.Width(200f), GUILayout.Height(50f))) { ScoreMaster.Obtain("测试", flags[3]); } GUILayout.Label($"总分: {ScoreMaster.GetSum()}"); } }
加载全部内容