引言
尊敬的读者您好,在今天的文章中,我想谈谈在Unity3d环境中创建的游戏应用程序的设置和配置。
按照传统,我将从背景开始。 在游戏行业工作期间,我开发了各种具有不同复杂性的项目,并参观了游戏设计阵营和程序员阵营(直到今天)。 众所周知,任何应用程序都需要大量不同的配置数据和设置。 在相对经典的Unity3d格式中,此类设置被放置在检查器的可见部分中,在其中输入了一些数字,等等。 我认为不值得讨论这种方法的便利性,即使它被排除在外,在调优时
MonoBehaviour类所在的场景也被其他开发人员所更改,因此这一事实是不值得的。 因此,在经历了一系列的折磨之后,我决定编写一些简单有效的方法,使每个人的生活更加轻松,并简化与我共享的数据的使用。
注意 :
下述所有代码均适用于Unity版本2018.3+,并使用Roslyn编译器(C#7+语言版本)。内部设定
首先,请考虑项目的内部设置,其中包括各种常量,链接,外部SDK的标识符,键等。 包括全局和本地游戏设置。 通常,所有此类数据可以分为四种类型:
所有其他数据都可以轻松地存储在其中,并且考虑到字符串,您可以使用JSON序列化存储任何内容。 我们将以
ScriptableObject为基础,它非常适合解决这一任务。
public class Setting : ScriptableObject { public enum ParameterTypeEnum { Float, Int, String, Bool } [Serializable] public class ParameterData { public string Name => _name; public ParameterTypeEnum ParameterType => _parameterType; public string DefaultValue => _defaultValue; [SerializeField] private string _name; [SerializeField] private ParameterTypeEnum _parameterType; [SerializeField] private string _defaultValue; } [SerializeField] protected ParameterData[] Parameters; }
因此,在数据库中,我们有一个值数组:
注意 :
为什么要行? 在我看来,这比存储4个不同类型的变量更方便。为了在代码中使用,我们添加了辅助方法和一个字典,该字典将以盒装形式存储转换后的值。 protected readonly IDictionary<string, object> settingParameters = new Dictionary<string, object>(); [NonSerialized] protected bool initialized; private void OnEnable() { #if UNITY_EDITOR if (EditorApplication.isPlayingOrWillChangePlaymode) { Initialization(); } #else Initialization(); #endif } public virtual T GetParameterValue<T>(string name) { if (settingParameters.ContainsKey(name)) { var parameterValue = (T)settingParameters[name]; return parameterValue; } else { Debug.Log("[Setting]: name not found [{0}]".Fmt(name)); } return default; } protected virtual void Initialization() { if (initialized || Parameters == null) return; for (var i = 0; i < Parameters.Length; i++) { var parameter = Parameters[i]; object parameterValue = null; switch (parameter.ParameterType) { case ParameterTypeEnum.Float: { if (!float.TryParse(parameter.DefaultValue, out float value)) { value = default; } parameterValue = GetValue(parameter.Name, value); } break; case ParameterTypeEnum.Int: { if (!int.TryParse(parameter.DefaultValue, out int value)) { value = default; } parameterValue = GetValue(parameter.Name, value); } break; case ParameterTypeEnum.String: { parameterValue = GetValue(parameter.Name, parameter.DefaultValue); } break; case ParameterTypeEnum.Bool: { if (!bool.TryParse(parameter.DefaultValue, out bool value)) { value = default; } parameterValue = GetValue(parameter.Name, value); } break; } settingParameters.Add(parameter.Name, parameterValue); } initialized = true; } protected virtual object GetValue<T>(string paramName, T defaultValue) { return defaultValue; }
初始化在
OnEnable中完成。 为什么
不醒着呢 ? 对于存储为资产的实例,不会调用此方法(它在
CreateInstance时被调用,我们不需要)。
启动资产应用程序时,首先调用
ScriptableObject,首先调用
OnDisable (仅在编辑器中),然后再
调用OnEnable 。 另外,为了使编辑器在每次重新编译和打开项目的初始化期间均不起作用,您需要添加预编译指令,并在文件的开头插入:
#if UNITY_EDITOR using UnityEditor; #endif
我们将进一步需要
GetValue方法,并且对于内部设置,它仅返回默认值。
GetParameterValue方法
是我们访问参数的主要方法。 值得考虑的是,尽管取消了装箱值,但存储在
Setting中的参数在某种程度上还是常量,因此在初始化场景时应采用它们。 不要在
Update中调用该方法。
用法示例:
public class MyLogic : MonoBehaviour { [SerializeField] private Setting _localSetting; private string _localStrValue; private int _localIntValue; private float _localFloatValue; private bool _localBoolValue; private void Start() { _localStrValue = _localSetting.GetParameterValue<string>("MyStr"); _localIntValue = _localSetting.GetParameterValue<int>("MyInt"); _localFloatValue = _localSetting.GetParameterValue<float>("MyFloat"); _localBoolValue = _localSetting.GetParameterValue<bool>("MyBool"); } }
我们写了基础,现在我们需要一个编辑器,因为对我们而言,主要目标只是为那些使用这些设置的人员提供方便。
要添加菜单项以便能够创建资产,可以使用属性:
CreateAssetMenu(fileName = "New Setting", menuName = "Setting")
现在,我们将编写一个自定义检查器,使您可以在资产上显示数据并启动外部编辑器。 [CustomEditor(typeof(Setting), true)] public class SettingCustomInspector : Editor { private GUIStyle _paramsStyle; private GUIStyle _paramInfoStyle; private const string _parameterInfo = "<color=white>Name</color><color=grey> = </color><color=yellow>{0}</color> <color=white>Type</color><color=grey> = </color><color=yellow>{1}</color> <color=white>Defualt Value</color><color=grey> = </color><color=yellow>{2}</color>"; public override void OnInspectorGUI() { if (GUILayout.Button("Edit Setting")) { SettingEditorWindow.Show(serializedObject.targetObject as Setting); } EditorGUILayout.LabelField("Parameters:", _parametersStyle, GUILayout.ExpandWidth(true)); var paramsProp = serializedObject.FindProperty("Parameters"); for (var i = 0; i < paramsProp.arraySize; i++) { var paramProp = paramsProp.GetArrayElementAtIndex(i); var paramNameProp = paramProp.FindPropertyRelative("_name"); var paramTypeProp = paramProp.FindPropertyRelative("_parameterType"); var paramDefaultValueProp = paramProp.FindPropertyRelative("_defaultValue"); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField(_paramInfo.Fmt( paramNameProp.stringValue, paramTypeProp.enumDisplayNames[paramTypeProp.enumValueIndex], paramDefaultValueProp.stringValue), _paramInfoStyle); EditorGUILayout.EndHorizontal(); } } private void PrepareGUIStyle() { if (_parametersStyle == null) { _paramsStyle = new GUIStyle(GUI.skin.label); _paramsStyle.fontStyle = FontStyle.Bold; _paramsStyle.fontSize = 12; _paramsStyle.normal.textColor = Color.green; _paramInfoStyle = new GUIStyle(GUI.skin.label); _paramInfoStyle.richText = true; } } }
它是这样的:

现在我们需要一个参数本身及其值的编辑器,为此,我们使用了一个自定义窗口。 public class SettingEditorWindow : EditorWindow { public Setting SelectedAsset; private int _currentSelectedAsset = -1; private readonly List<string> _assetNames = new List<string>(); private readonly IList<SerializedObject> _settingSerializationObjects = new List<SerializedObject>(); private readonly IList<T> _assets = new List<T>(); private readonly IList<int> _editedNames = new List<int>();; private GUIContent _editNameIconContent; private GUIStyle _headerStyle; private GUIStyle _parametersStyle; private GUIStyle _parameterHeaderStyle; private GUIStyle _nameStyle; private Vector2 _scrollInspectorPosition = Vector2.zero; private Vector2 _scrollAssetsPosition = Vector2.zero; private const string _SELECTED_ASSET_STR = "SettingSelected"; public static void Show(Setting asset) { var instance = GetWindow<Setting>(true); instance.title = new GUIContent("Settings Editor", string.Empty); instance.SelectedAsset = asset; } private void OnEnable() { var assetGuids = AssetDatabase.FindAssets("t:{0}".Fmt(typeof(Setting).Name)); foreach (var guid in assetGuids) { var path = AssetDatabase.GUIDToAssetPath(guid); var asset = AssetDatabase.LoadAssetAtPath<T>(path); _assetNames.Add(path.Replace("Assets/", "").Replace(".asset", "")); _assets.Add(asset); _settingSerializationObjects.Add(new SerializedObject(asset)); } _currentSelectedAsset = PlayerPrefs.GetInt(_SELECTED_ASSET_STR, -1); _editNameIconContent = new GUIContent(EditorGUIUtility.IconContent("editicon.sml")); } private void OnDisable() { PlayerPrefs.SetInt(_SELECTED_ASSET_STR, _currentSelectedAsset); } private void PrepareGUIStyle() { if (_headerStyle == null) { _headerStyle = new GUIStyle(GUI.skin.box); _headerStyle.fontStyle = FontStyle.Bold; _headerStyle.fontSize = 14; _headerStyle.normal.textColor = Color.white; _headerStyle.alignment = TextAnchor.MiddleCenter; _parametersStyle = new GUIStyle(GUI.skin.label); _parametersStyle.fontStyle = FontStyle.Bold; _parametersStyle.fontSize = 12; _parametersStyle.normal.textColor = Color.green; } } private void OnGUI() { PrepareGUIStyle(); if (SelectedAsset != null) { _currentSelectedAsset = _assets.IndexOf(SelectedAsset); SelectedAsset = null; } EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginVertical(GUI.skin.box, GUILayout.MinWidth(350f), GUILayout.ExpandHeight(true)); _scrollAssetsPosition = EditorGUILayout.BeginScrollView(_scrollAssetsPosition, GUIStyle.none, GUI.skin.verticalScrollbar); _currentSelectedAsset = GUILayout.SelectionGrid(_currentSelectedAsset, _assetNames.ToArray(), 1); EditorGUILayout.EndScrollView(); EditorGUILayout.EndVertical(); EditorGUILayout.BeginVertical(GUILayout.ExpandWidth(true)); var assetSerializedObject = (_currentSelectedAsset >= 0) ? _settingSerializationObjects[_currentSelectedAsset] : null; EditorGUILayout.Space(); EditorGUILayout.LabelField((_currentSelectedAsset >= 0) ? _assetNames[_currentSelectedAsset] : "Select Asset...", _headerStyle, GUILayout.ExpandWidth(true)); EditorGUILayout.Space(); _scrollInspectorPosition = EditorGUILayout.BeginScrollView(_scrollInspectorPosition, GUIStyle.none, GUI.skin.verticalScrollbar); Draw(assetSerializedObject); EditorGUILayout.EndScrollView(); EditorGUILayout.EndVertical(); EditorGUILayout.EndHorizontal(); assetSerializedObject?.ApplyModifiedProperties(); } private void Draw(SerializedObject assetSerializationObject) { if (assetSerializationObject == null) return; EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Parameters:", _parametersStyle, GUILayout.Width(20f), GUILayout.ExpandWidth(true)); var parametersProperty = assetSerializationObject.FindProperty("Parameters"); if (GUILayout.Button("Add", GUILayout.MaxWidth(40f))) { if (parametersProperty != null) { parametersProperty.InsertArrayElementAtIndex(parametersProperty.arraySize); } } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); if (parametersProperty != null) { for (var i = 0; i < parametersProperty.arraySize; i++) { var parameterProperty = parametersProperty.GetArrayElementAtIndex(i); var parameterNameProperty = parameterProperty.FindPropertyRelative("_name"); var parameterTypeProperty = parameterProperty.FindPropertyRelative("_parameterType"); var parameterDefaultValueProperty = parameterProperty.FindPropertyRelative("_defaultValue"); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button(_editNameIconContent, GUILayout.MaxWidth(25f), GUILayout.MaxHeight(18f))) { if (_editedNames.Contains(i)) { _editedNames.Remove(i); } else { _editedNames.Add(i); } } EditorGUILayout.LabelField("Name", _parameterHeaderStyle, GUILayout.MaxWidth(40f)); if (_editedNames.Contains(i)) { parameterNameProperty.stringValue = EditorGUILayout.TextField(parameterNameProperty.stringValue, GUILayout.Width(175f)); var ev = Event.current; if (ev.type == EventType.MouseDown || ev.type == EventType.Ignore || (ev.type == EventType.KeyDown && ev.keyCode == KeyCode.Return)) { _editedNames.Remove(i); } } else { EditorGUILayout.LabelField(parameterNameProperty.stringValue, _nameStyle, GUILayout.Width(175f)); } EditorGUILayout.LabelField("Type", _parameterHeaderStyle, GUILayout.MaxWidth(40f)); parameterTypeProperty.enumValueIndex = EditorGUILayout.Popup(parameterTypeProperty.enumValueIndex, parameterTypeProperty.enumDisplayNames, GUILayout.Width(75f)); GUILayout.Space(20f); EditorGUILayout.LabelField("DefaultValue", _parameterHeaderStyle, GUILayout.Width(85f)); switch (parameterTypeProperty.enumValueIndex) { case 0: { if (!float.TryParse(parameterDefaultValueProperty.stringValue, out float value)) { value = default; } value = EditorGUILayout.FloatField(value, GUILayout.ExpandWidth(true)); parameterDefaultValueProperty.stringValue = value.ToString(); } break; case 1: { if (!int.TryParse(parameterDefaultValueProperty.stringValue, out int value)) { value = default; } value = EditorGUILayout.IntField(value, GUILayout.ExpandWidth(true)); parameterDefaultValueProperty.stringValue = value.ToString(); } break; case 2: parameterDefaultValueProperty.stringValue = EditorGUILayout.TextField(parameterDefaultValueProperty.stringValue, GUILayout.ExpandWidth(true)); break; case 3: { if (!bool.TryParse(parameterDefaultValueProperty.stringValue, out bool value)) { value = default; } value = EditorGUILayout.Toggle(value, GUILayout.ExpandWidth(true)); parameterDefaultValueProperty.stringValue = value.ToString(); } break; } if (GUILayout.Button("-", GUILayout.MaxWidth(25f), GUILayout.MaxHeight(18f))) { if (_editedNames.Contains(i)) { _editedNames.Remove(i); } parametersProperty.DeleteArrayElementAtIndex(i); } EditorGUILayout.EndHorizontal(); } } } }
我不会解释太多代码,这里的一切都很简单。 我只注意到编辑器使您可以选择编辑
Setting类型的所有资产。 为此,在打开窗口时,我们使用
AssetDatabase.FindAssets方法
(“ t:{0}”。Fmt(typeof(Setting).Name))在项目中找到它们。 通过按钮也可以编辑参数名称,以排除其意外更改。
这是编辑器的外观:

我们检查了应用程序内部使用的设置,现在我们将考虑一个更具体的情况。
外部设定
想象一个情况,在已经运行的游戏中,我们突然需要更改某些值以调整游戏玩法。 在原始版本中,我们在构建中进行更改,累积更改,进行更新并将其发送到商店,然后等待确认等。 但是那些不更新应用程序的人呢? 如果需要紧急进行更改怎么办? 为了解决此问题,存在诸如“
远程设置”之类的机制。 这不是一个新发明,并且在许多第三方SDK中用于分析等,例如在
Firebase ,
GameAnalytics和
Unity Analytics中使用 。 我们将使用后者。
注意 :
通常,所有这些系统之间没有区别,它们是相似的并且使用相同的原理。让我们来谈谈
Unity Analytics中的 远程设置 是什么以及它可以做什么。
为了使此功能在项目中可用,您需要在“
服务”选项卡上的项目中启用分析。

之后,您需要登录到Unity3d帐户并在其中找到您的项目,然后单击链接到“分析”部分,在菜单的左侧选择“
远程设置” 。

所有设置都分为在开发模式下使用的设置和将在已经发布的应用程序中使用的设置。

要添加参数,请选择适当的项目,然后输入参数的名称,类型和值。

添加完所有必需的参数后,我们需要代码中的支持才能使用它们。
注意 :“
同步”按钮可将设置与应用程序同步。 这个过程不会立即发生,但是,当应用程序中的参数更新时,将触发相应的事件,我们将在后面讨论 。
要使用“
远程设置”,您不需要任何其他SDK,只需打开分析功能即可,如我上面所述。
我们将编写一个用于远程设置的类,为此,我们将上述设置类用作基础。 public sealed class RemoteSetting : Setting { public IList<string> GetUpdatedParameter() { var updatedParameters = new List<string>(); for (var i = 0; i < Parameters.Length; i++) { var parameter = Parameters[i]; switch (parameter.ParameterType) { case ParameterTypeEnum.Float: { var currentValue = Get<float>(parameter.Name); var newValue = RemoteSettings.GetFloat(parameter.Name, currentValue); if (currentValue != newValue) { settingParameters[parameter.Name] = newValue; updatedParameters.Add(parameter.Name); } } break; case ParameterTypeEnum.Int: { var currentValue = Get<int>(parameter.Name); var newValue = RemoteSettings.GetInt(parameter.Name, currentValue); if (currentValue != newValue) { settingParameters[parameter.Name] = newValue; updatedParameters.Add(parameter.Name); } } break; case ParameterTypeEnum.String: { var currentValue = Get<string>(parameter.Name); var newValue = RemoteSettings.GetString(parameter.Name, currentValue); if (string.Compare(currentValue, newValue, System.StringComparison.Ordinal) != 0) { settingParameters[parameter.Name] = newValue; updatedParameters.Add(parameter.Name); } } break; case ParameterTypeEnum.Bool: { var currentValue = Get<bool>(parameter.Name); var newValue = RemoteSettings.GetBool(parameter.Name, currentValue); if (currentValue != newValue) { settingParameters[parameter.Name] = newValue; updatedParameters.Add(parameter.Name); } } break; } } return updatedParameters; } protected override object GetValue<T>(string paramName, T defaultValue) { switch(defaultValue) { case float f: return RemoteSettings.GetFloat(paramName, f); case int i: return RemoteSettings.GetInt(paramName, i); case string s: return RemoteSettings.GetString(paramName, s); case bool b: return RemoteSettings.GetBool(paramName, b); default: return default; } } }
如您所见,我们重新定义了
GetValue方法并添加了一个新方法,该方法使您可以获取已更改参数的列表,稍后我们将需要它。
上面,我们编写了一个在代码中使用
设置的示例,这很简单,但是没有考虑到远程设置的存在,因此,为了统一使用单个键访问所有设置,我们将编写一个经理来帮助您。
设置管理器代码 public class SettingsManager : MonoBehaviourSingleton<SettingsManager> { public Setting this[string index] => GetSetting(index); [SerializeField] private Setting[] _settings; private readonly IDictionary<string, Setting> _settingsByName = new Dictionary<string, Setting>(); public void ForceUpdate() { RemoteSettings.ForceUpdate(); } private void Start() { foreach(var setting in _settings) { _settingsByName.Add(setting.name, setting); } RemoteSettings.BeforeFetchFromServer += OnRemoteSettingBeforeUpdate; RemoteSettings.Updated += OnRemoteSettingsUpdated; RemoteSettings.Completed += OnRemoteSettingCompleted; } private Setting GetSetting(string name) { if(_settingsByName.ContainsKey(name)) { return _settingsByName[name]; }else { Debug.LogWarningFormat("[SettingManager]: setting name [{0}] not found", name); return null; } } private void OnRemoteSettingBeforeUpdate() { RemoteSettingBeforeUpdate.Call(); } private void OnRemoteSettingsUpdated() { foreach (var setting in _settingsByName.Values) { if (setting is RemoteSetting) { var updatedParameter = remoteSetting.GetUpdatedParameter(); foreach (var parameterName in updatedParameter) { RemoteSettingUpdated.Call(parameterName); } } } } private void OnRemoteSettingCompleted(bool wasUpdatedFromServer, bool settingsChanged, int serverResponse) { RemoteSettingsCompleted.Call(wasUpdatedFromServer, settingsChanged, serverResponse); } private void OnDestroy() { RemoteSettings.BeforeFetchFromServer -= OnRemoteSettingBeforeUpdate; RemoteSettings.Updated -= OnRemoteSettingsUpdated; RemoteSettings.Completed -= OnRemoteSettingCompleted; } }
经理以sigleton的形式出现,他只住在现场。 这样做是为了便于参考,并且为了轻松管理每个场景中的一组参数(排除逻辑不需要的参数)。
如您所见,
RemoteSettings具有三个事件:
- 从远程服务器接收参数值之前引发的事件
- 参数更新事件(仅由我们之前提到的“同步”按钮调用),以及通过ForceUpdate函数强制更新参数的情况
- 从服务器收到有关远程设置的数据时触发的事件。 如果发生任何错误,也将在此处发出服务器响应代码。
注意 :该
代码使用基于数据类型的事件系统,有关它的更多信息,请参见我的另一篇文章 。注意 :您
需要了解RemoteSettings的工作方式。 开始时,如果可以访问Internet,它将自动下载有关参数的数据并将其缓存,因此,下次启动时,如果没有Internet,则将从缓存中获取数据。 例外情况是在启动应用程序时关闭了对网络的访问,在这种情况下,用于获取参数值的功能将返回默认值。 在我们的情况下,这些是我们在编辑器中输入的内容。现在,考虑到上述情况,让我们更改使用代码中的设置的示例。
public class MyLogic : MonoBehaviour { private const string INGAME_PARAMETERS = "IngamgeParameters"; private const string REMOTE_RAPAMETERS = "RemoteParamteters"; private string _localStrValue; private int _localIntValue; private float _localFloatValue; private bool _localBoolValue; private string _remoteStrValue; private int _remoteIntValue; private float _remoteFloatValue; private bool _remoteBoolValue; private void Start() { var ingameParametes = SettingsManager.Instance[INGAME_PARAMETERS]; var remoteParametes = SettingsManager.Instance[REMOTE_RAPAMETERS]; _localStrValue = ingameParametes.GetParameterValue<string>("MyStr"); _localIntValue = ingameParametes.GetParameterValue<int>("MyInt"); _localFloatValue = ingameParametes.GetParameterValue<float>("MyFloat"); _localBoolValue = ingameParametes.GetParameterValue<bool>("MyBool"); _remoteStrValue = remoteParametes.GetParameterValue<string>("MyStr"); _remoteIntValue = remoteParametes.GetParameterValue<int>("MyInt"); _remoteFloatValue = remoteParametes.GetParameterValue<float>("MyFloat"); _remoteBoolValue = remoteParametes.GetParameterValue<bool>("MyBool"); } }
如您所见,从代码中可以看出,内部和外部设置之间的工作没有区别,但是,如果必要,如果逻辑要求,您可以订阅与远程设置相关的管理器事件。
注意 :
如果只需要远程参数,则可以从AssetStore下载特殊的插件 ,它允许您立即使用它们。结论
在本文中,我试图说明如何使用内部设置和远程设置简单地配置用Unity3d编写的应用程序。 我在项目中使用了类似的方法,并且证明了它的有效性。 我们甚至设法使用远程设置来实施
A / B测试系统 。 此外,这些设置被广泛用于存储与SDK,服务器内容以及游戏设置等相关的各种常量。 游戏设计师可以预先创建一组参数,并描述如何使用,使用什么参数以及在何处使用它们,同时他可以自定义游戏玩法而不会阻塞场景。 由于我们使用了
ScriptableObject并将这些参数存储为资产,因此可以通过
AssetBundle加载
它们 ,这进一步扩展了我们的功能。
文章中指定的链接habr.com/en/post/282524assetstore.unity.com/packages/add-ons/services/analytics/unity-analytics-remote-settings-89317