1. Introdução
Olá queridos leitores, no artigo de hoje, eu gostaria de falar sobre as configurações e aplicativos de jogos criados no ambiente Unity3d.
Por tradição, começarei com o plano de fundo. Enquanto trabalhava na indústria de jogos, desenvolvi vários projetos com complexidade variada e visitei o campo de design de jogos e o campo de programadores (onde estou até hoje). Não é segredo que qualquer aplicativo exija um grande número de dados e configurações diferentes. No formato clássico, relativamente do Unity3d, essas configurações são colocadas na parte visível do inspetor, onde são inseridos alguns números etc. Eu acho que não vale a pena falar sobre a conveniência dessa abordagem, mesmo que excluída, o fato de que, no momento do ajuste, a cena em que a classe
MonoBehaviour está localizada está bloqueada por alterações por outros desenvolvedores. Portanto, depois de uma série de vários tipos de provações no passado, decidi escrever algo simples e eficaz que tornasse a vida mais fácil para todos e simplificasse o trabalho com esses dados, que quero compartilhar com você.
Nota :
todo o código descrito abaixo é aplicável à versão 2018.3+ do Unity e usa o compilador Roslyn (versão de idioma C # 7+).Configurações internas
Para começar, considere as configurações internas do projeto, que incluem várias constantes, links, identificadores de SDKs externos, chaves etc. coisas, incluindo configurações de jogo globais e locais. Em geral, todos esses dados podem ser divididos em quatro tipos:
Todos os outros dados podem ser facilmente armazenados neles e, levando em consideração a sequência, você pode armazenar qualquer coisa usando a serialização JSON. Usaremos
ScriptableObject como base, o que é adequado para resolver esta tarefa como ninguém mais.
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; }
Portanto, no banco de dados, temos uma matriz de valores que são:
- Nome do parâmetro
- Tipo de Parâmetro
- Valores de parâmetro como uma sequência
Nota :
por que linhas? Pareceu-me mais conveniente do que armazenar 4 variáveis de tipos diferentes.Para uso no código, adicionamos métodos auxiliares e um dicionário que armazenará os valores convertidos em uma caixa. 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; }
A inicialização é feita no
OnEnable . Por que não no
Awake ? Este método não é chamado para instâncias armazenadas como ativos (é chamado no momento de
CreateInstance , do qual não precisamos). Quando o aplicativo para ativos é
iniciado, o ScriptableObject é chamado primeiro,
OnDisable (apenas no editor) e, em seguida,
OnEnable . Além disso, para que no editor durante cada recompilação e abertura da inicialização do projeto não funcione, você precisa adicionar diretivas de pré-compilação e inserir no início do arquivo:
#if UNITY_EDITOR using UnityEditor; #endif
Vamos precisar ainda mais do método
GetValue e, para configurações internas, ele simplesmente retorna o valor padrão.
O método
GetParameterValue é o nosso principal método para acessar parâmetros. Vale a pena considerar que, apesar do unboxing dos valores, os parâmetros armazenados em
Setting são de alguma forma constantes, portanto devem ser tomados ao inicializar as cenas. Não chame o método na
atualização .
Exemplo de uso:
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"); } }
Escrevemos a fundação e agora precisamos de um editor, pois o objetivo principal para nós era apenas a conveniência para quem trabalha com essas configurações.
Para adicionar um item de menu para poder criar um ativo, você pode usar o atributo:
CreateAssetMenu(fileName = "New Setting", menuName = "Setting")
Agora, escreveremos um inspetor personalizado que permitirá exibir dados no ativo e iniciar um editor externo. [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; } } }
É assim que ficará:

Agora precisamos de um editor dos próprios parâmetros e de seus valores, para isso usamos uma janela personalizada. 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(); } } } }
Não vou explicar muito o código, tudo é simples aqui. Observo apenas que o editor permite editar opcionalmente todos os ativos do tipo
Configuração . Para fazer isso, ao abrir a janela, nós os encontramos no projeto usando o método
AssetDatabase.FindAssets ("t: {0}". Fmt (typeof (Setting) .Name)) . E também a edição do nome do parâmetro é feita através do botão para excluir sua alteração acidental.
É assim que o editor se parece:

Examinamos as configurações usadas dentro do aplicativo, agora consideraremos um caso mais específico.
Configurações externas
Imagine uma situação que, em um jogo já em execução, de repente precisávamos alterar certos valores para ajustar a jogabilidade. Na versão primitiva, alteramos isso na compilação, acumulamos essas alterações, fazemos uma atualização e as enviamos para as lojas, após o que aguardamos a confirmação etc. Mas e aqueles que não atualizam o aplicativo? E se as mudanças precisarem ser feitas com urgência? Para resolver esse problema, existe um mecanismo como
Configurações remotas . Esta não é uma invenção nova e é usada em muitos SDKs de terceiros para análises, etc., por exemplo - está no
Firebase , no
GameAnalytics e no
Unity Analytics . É o último que vamos usar.
Nota :
em geral, não há diferença entre todos esses sistemas, eles são semelhantes e usam os mesmos princípios.Vamos analisar o que são as
Configurações remotas no
Unity Analytics e o que elas podem fazer.
Para que essa funcionalidade fique disponível no projeto, você precisa ativar a análise no projeto na guia
Serviços .

Depois disso, você precisa fazer login na sua conta do Unity3d, encontrar seu projeto e seguir o link para a seção de análise, onde, à esquerda no menu, selecione
Configurações remotas .

Todas as configurações são divididas nas que são usadas no modo de desenvolvimento e nas que serão usadas em um aplicativo já lançado.

Para adicionar um parâmetro, selecione o item apropriado e digite o nome, o tipo e o valor do parâmetro.

Depois de adicionar todos os parâmetros necessários, precisamos de suporte no código para trabalhar com eles.
Nota : O
botão Sincronizar sincroniza as configurações com o aplicativo. Esse processo não ocorre instantaneamente, no entanto, no momento em que os parâmetros no aplicativo são atualizados, os eventos correspondentes serão acionados, falaremos sobre eles mais tarde .
Para trabalhar com as
Configurações remotas, você não precisa de SDKs adicionais, basta ativar o analytics, como escrevi acima.
Vamos escrever uma classe para trabalhar com configurações remotas, para isso usamos a classe Setting descrita acima como base. 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; } } }
Como você pode ver, redefinimos o método
GetValue e adicionamos um novo método que permite obter uma lista de parâmetros alterados; precisaremos disso mais tarde.
Acima, escrevemos um exemplo do uso de
Configurações no código, é bastante simples, mas não leva em consideração a presença de configurações remotas; portanto, para unificar o acesso a todas as configurações em uma única chave, escreveremos um gerente que ajudará nisso.
Código do Gerenciador de configurações 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; } }
O gerente é apresentado na forma de um sigleton que vive apenas na cena. Isso foi feito para facilitar a referência e para gerenciar facilmente um conjunto de parâmetros em cada cena (excluir parâmetros que não são exigidos pela lógica).
Como você pode ver, o
RemoteSettings tem três eventos:
- Evento gerado antes que os valores dos parâmetros sejam recebidos do servidor remoto
- Evento de atualização de parâmetro (chamado apenas pelo botão Sync, sobre o qual escrevemos anteriormente), bem como no caso de atualização forçada de parâmetros por meio da função ForceUpdate
- Evento acionado quando dados sobre configurações remotas são recebidos do servidor. O código de resposta do servidor também é emitido aqui, caso ocorra algum erro.
Nota : o
código usa um sistema de eventos construído em tipos de dados, mais sobre isso está escrito em outro artigo meu .Nota : Você
precisa entender como o RemoteSettings funciona. No início, se houver acesso à Internet, ele baixa automaticamente os dados sobre os parâmetros e os armazena em cache; portanto, na próxima vez em que você iniciar, se não houver Internet, os dados serão retirados do cache. A exceção é a situação em que o aplicativo é iniciado inicialmente com o acesso à rede desativado; nesse caso, as funções para obter o valor do parâmetro retornarão o valor padrão. No nosso caso, esses são os que inserimos no editor.Agora vamos mudar o exemplo do uso das configurações do código, levando em consideração o acima.
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"); } }
Como você pode ver, no código, não há diferença no trabalho entre configurações internas e externas; no entanto, se necessário, se a lógica exigir, você pode assinar eventos do gerenciador relacionados a configurações remotas.
Nota :
se você precisar apenas de parâmetros remotos, poderá fazer o download de um plug-in especial do AssetStore , pois você poderá trabalhar com eles imediatamente.Conclusão
Neste artigo, tentei mostrar como você pode simplesmente configurar um aplicativo escrito no Unity3d usando configurações internas e remotas. Eu uso uma abordagem semelhante em meus projetos, e isso prova sua eficácia. Até conseguimos usar nossas configurações remotas para implementar nosso
sistema de teste A / B. Além disso, as configurações são amplamente usadas para armazenar várias constantes relacionadas ao SDK, itens do servidor, configurações de jogabilidade etc. O designer do jogo pode criar um conjunto de parâmetros com antecedência e descrever como, para quê e onde eles são usados, enquanto ele pode personalizar a jogabilidade sem bloquear a cena. E devido ao fato de termos usado o
ScriptableObject e armazenado parâmetros como ativos, eles podem ser carregados através do
AssetBundle , que expande ainda mais nossos recursos.
Links especificados no artigo :
habr.com/en/post/282524assetstore.unity.com/packages/add-ons/services/analytics/unity-analytics-remote-settings-89317