Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

How to combine Steam language with Unity Localization

Discussion in 'Localization Tools' started by tknippenberg, May 22, 2021.

  1. tknippenberg

    tknippenberg

    Joined:
    Feb 15, 2017
    Posts:
    12
    The Steamworks API returns the CurrentGameLanguage (which the user can set in a dropdown in the Steam client) as a string. In the OnEnable() function of my main GameController script I'm now calling a function DetermineLocale() to check the Steam language and I want to make sure that one is used.

    Code (CSharp):
    1.  
    2. string steamLocale = Steamworks.SteamApps.GetCurrentGameLanguage();
    3.  
    4. Debug.Log($"Loaded {LocalizationSettings.AvailableLocales.Locales.Count} locales. Active locale {LocalizationSettings.SelectedLocale}");
    5.  
    6.         switch (steamLocale)
    7.         {
    8.             case "dutch":
    9.                 LocalizationSettings.SelectedLocale = LocalizationSettings.AvailableLocales.GetLocale("nl");
    10.                 break;
    11.             case "german":
    12.                 Debug.Log(LocalizationSettings.AvailableLocales.GetLocale("DE-de"));
    13.                 Debug.Log(LocalizationSettings.AvailableLocales.GetLocale("DE"));
    14.                 Debug.Log(LocalizationSettings.AvailableLocales.GetLocale("de"));
    15.                 LocalizationSettings.SelectedLocale = LocalizationSettings.AvailableLocales.GetLocale("de");
    16.                 break;
    17.             default:
    18.                 LocalizationSettings.SelectedLocale = LocalizationSettings.AvailableLocales.GetLocale("en");
    19.                 break;
    20.         }
    21.  
    This switch statement approach doesn't seem to be working yet. The GetLocale() calls all return null and it seems 0 locales have been loaded at the time I do this.

    First of all, am I taking a sensible approach here of changing the locale, or is there a smarter way for Unity to work with the Steam language? And if my approach makes sense, any idea how to get my code working?
     
  2. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    7,845
    tknippenberg likes this.
  3. tknippenberg

    tknippenberg

    Joined:
    Feb 15, 2017
    Posts:
    12
    Ah that sounds like a better approach! I'll go check it out, thanks Karl!
     
    karl_jones likes this.
  4. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    7,845
    It should be simple to do. Here is an example

    Code (csharp):
    1.  
    2. using System;
    3. using UnityEngine;
    4. using UnityEngine.Localization;
    5. using UnityEngine.Localization.Settings;
    6.  
    7. [DisplayName("My Startup Selector")]
    8. [Serializable]
    9. public class StartupSelectorExample : IStartupLocaleSelector
    10. {
    11.     // Allow the user to select a specific language to use.
    12.     public SystemLanguage language;
    13.  
    14.     public Locale GetStartupLocale(ILocalesProvider availableLocales)
    15.     {
    16.         // Return the Locale that matches the language field or null if one does not exist.
    17.         return availableLocales.GetLocale(language);
    18.     }
    19. }

    StartupSelectorExample.png
     
    saukki and Vlad0sZ like this.
  5. bottledgalaxy

    bottledgalaxy

    Joined:
    Jul 19, 2019
    Posts:
    83
    Hey @karl_jones looks like the steam manager is not yet initialized by the time this method is called. Is there a way to call this manually after steam finishes initializing? thanks!
     
  6. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    7,845
  7. bottledgalaxy

    bottledgalaxy

    Joined:
    Jul 19, 2019
    Posts:
    83
    Hey Karl, I do have control of when steam initializes, but that link that you sent me went way over my head... any chance you can explain a bit more how to do what you are suggesting?

    Thank you
     
  8. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    7,845
    You need an editor class

    Code (csharp):
    1.  
    2. using System;
    3. using UnityEngine;
    4. using UnityEngine.AddressableAssets.Initialization;
    5. using UnityEngine.ResourceManagement.Util;
    6. using UnityEngine.Serialization;
    7.  
    8. /// <summary>
    9. /// Asset container for CacheInitializationData.
    10. /// </summary>
    11. [CreateAssetMenu(fileName = "SteamInitializationSettings.asset", menuName = "Addressables/Initialization/Steam Initialization Settings")]
    12. public class SteamInitializationSettings : ScriptableObject, IObjectInitializationDataProvider
    13. {
    14.     /// <summary>
    15.     /// Display name used in GUI for this object.
    16.     /// </summary>
    17.     public string Name
    18.     {
    19.         get { return "Steam Settings"; }
    20.     }
    21.  
    22.     /// <summary>
    23.     /// Create initialization data to be serialized into the Addressables runtime data.
    24.     /// </summary>
    25.     /// <returns>The serialized data for the initialization class and the data.</returns>
    26.     public ObjectInitializationData CreateObjectInitializationData()
    27.     {
    28.         return ObjectInitializationData.CreateSerializedInitializationData<SteamInitialization>(typeof(SteamInitialization).Name);
    29.     }
    30. }
    Use the menu to create the asset and add it to the addressables initialization objects in the settings.

    Do initialization here

    Code (csharp):
    1. using System;
    2. using UnityEngine;
    3. using UnityEngine.AddressableAssets.Initialization;
    4. using UnityEngine.ResourceManagement.Util;
    5. using UnityEngine.Serialization;
    6.  
    7. public class SteamInitialization : IInitializableObject
    8. {
    9.     public bool Initialize(string id, string data)
    10.     {
    11.         // Initialize steam
    12.     }
    13.  
    14.     public AsyncOperationHandle<bool> InitializeAsync(ResourceManager rm, string id, string data)
    15.     {
    16.         // Initialize steam
    17.     }
    18. }
    This code is rough and may have some errors.
     
  9. bottledgalaxy

    bottledgalaxy

    Joined:
    Jul 19, 2019
    Posts:
    83
    Mmmm the way the Steam API is initialized is by having this script on a GameObject in the first scene that your game loads...

    That's not compatible with this workflow is it?

    Code (CSharp):
    1. // The SteamManager is designed to work with Steamworks.NET
    2. // This file is released into the public domain.
    3. // Where that dedication is not recognized you are granted a perpetual,
    4. // irrevocable license to copy and modify this file as you see fit.
    5. //
    6. // Version: 1.0.12
    7.  
    8. #if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
    9. #define DISABLESTEAMWORKS
    10. #endif
    11.  
    12. using UnityEngine;
    13. #if !DISABLESTEAMWORKS
    14. using System.Collections;
    15. using Steamworks;
    16. #endif
    17.  
    18. //
    19. // The SteamManager provides a base implementation of Steamworks.NET on which you can build upon.
    20. // It handles the basics of starting up and shutting down the SteamAPI for use.
    21. //
    22. [DisallowMultipleComponent]
    23. public class SteamManager : MonoBehaviour {
    24. #if !DISABLESTEAMWORKS
    25.     protected static bool s_EverInitialized = false;
    26.  
    27.     protected static SteamManager s_instance;
    28.     protected static SteamManager Instance {
    29.         get {
    30.             if (s_instance == null) {
    31.                 return new GameObject("SteamManager").AddComponent<SteamManager>();
    32.             }
    33.             else {
    34.                 return s_instance;
    35.             }
    36.         }
    37.     }
    38.  
    39.     protected bool m_bInitialized = false;
    40.     public static bool Initialized {
    41.         get {
    42.             return Instance.m_bInitialized;
    43.         }
    44.     }
    45.  
    46.     protected SteamAPIWarningMessageHook_t m_SteamAPIWarningMessageHook;
    47.  
    48.     [AOT.MonoPInvokeCallback(typeof(SteamAPIWarningMessageHook_t))]
    49.     protected static void SteamAPIDebugTextHook(int nSeverity, System.Text.StringBuilder pchDebugText) {
    50.         Debug.LogWarning(pchDebugText);
    51.     }
    52.  
    53. #if UNITY_2019_3_OR_NEWER
    54.     // In case of disabled Domain Reload, reset static members before entering Play Mode.
    55.     [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    56.     private static void InitOnPlayMode()
    57.     {
    58.         s_EverInitialized = false;
    59.         s_instance = null;
    60.     }
    61. #endif
    62.  
    63.     protected virtual void Awake() {
    64.         // Only one instance of SteamManager at a time!
    65.         if (s_instance != null) {
    66.             Destroy(gameObject);
    67.             return;
    68.         }
    69.         s_instance = this;
    70.  
    71.         if(s_EverInitialized) {
    72.             // This is almost always an error.
    73.             // The most common case where this happens is when SteamManager gets destroyed because of Application.Quit(),
    74.             // and then some Steamworks code in some other OnDestroy gets called afterwards, creating a new SteamManager.
    75.             // You should never call Steamworks functions in OnDestroy, always prefer OnDisable if possible.
    76.             throw new System.Exception("Tried to Initialize the SteamAPI twice in one session!");
    77.         }
    78.  
    79.         // We want our SteamManager Instance to persist across scenes.
    80.         DontDestroyOnLoad(gameObject);
    81.  
    82.         if (!Packsize.Test()) {
    83.             Debug.LogError("[Steamworks.NET] Packsize Test returned false, the wrong version of Steamworks.NET is being run in this platform.", this);
    84.         }
    85.  
    86.         if (!DllCheck.Test()) {
    87.             Debug.LogError("[Steamworks.NET] DllCheck Test returned false, One or more of the Steamworks binaries seems to be the wrong version.", this);
    88.         }
    89.  
    90.         try {
    91.             // If Steam is not running or the game wasn't started through Steam, SteamAPI_RestartAppIfNecessary starts the
    92.             // Steam client and also launches this game again if the User owns it. This can act as a rudimentary form of DRM.
    93.  
    94.             // Once you get a Steam AppID assigned by Valve, you need to replace AppId_t.Invalid with it and
    95.             // remove steam_appid.txt from the game depot. eg: "(AppId_t)480" or "new AppId_t(480)".
    96.             // See the Valve documentation for more information: https://partner.steamgames.com/doc/sdk/api#initialization_and_shutdown
    97.             if (SteamAPI.RestartAppIfNecessary(AppId_t.Invalid)) {
    98.                 Application.Quit();
    99.                 return;
    100.             }
    101.         }
    102.         catch (System.DllNotFoundException e) { // We catch this exception here, as it will be the first occurrence of it.
    103.             Debug.LogError("[Steamworks.NET] Could not load [lib]steam_api.dll/so/dylib. It's likely not in the correct location. Refer to the README for more details.\n" + e, this);
    104.  
    105.             Application.Quit();
    106.             return;
    107.         }
    108.  
    109.         // Initializes the Steamworks API.
    110.         // If this returns false then this indicates one of the following conditions:
    111.         // [*] The Steam client isn't running. A running Steam client is required to provide implementations of the various Steamworks interfaces.
    112.         // [*] The Steam client couldn't determine the App ID of game. If you're running your application from the executable or debugger directly then you must have a [code-inline]steam_appid.txt[/code-inline] in your game directory next to the executable, with your app ID in it and nothing else. Steam will look for this file in the current working directory. If you are running your executable from a different directory you may need to relocate the [code-inline]steam_appid.txt[/code-inline] file.
    113.         // [*] Your application is not running under the same OS user context as the Steam client, such as a different user or administration access level.
    114.         // [*] Ensure that you own a license for the App ID on the currently active Steam account. Your game must show up in your Steam library.
    115.         // [*] Your App ID is not completely set up, i.e. in Release State: Unavailable, or it's missing default packages.
    116.         // Valve's documentation for this is located here:
    117.         // https://partner.steamgames.com/doc/sdk/api#initialization_and_shutdown
    118.         m_bInitialized = SteamAPI.Init();
    119.         if (!m_bInitialized) {
    120.             Debug.LogError("[Steamworks.NET] SteamAPI_Init() failed. Refer to Valve's documentation or the comment above this line for more information.", this);
    121.  
    122.             return;
    123.         }
    124.  
    125.         s_EverInitialized = true;
    126.     }
    127.  
    128.     // This should only ever get called on first load and after an Assembly reload, You should never Disable the Steamworks Manager yourself.
    129.     protected virtual void OnEnable() {
    130.         if (s_instance == null) {
    131.             s_instance = this;
    132.         }
    133.  
    134.         if (!m_bInitialized) {
    135.             return;
    136.         }
    137.  
    138.         if (m_SteamAPIWarningMessageHook == null) {
    139.             // Set up our callback to receive warning messages from Steam.
    140.             // You must launch with "-debug_steamapi" in the launch args to receive warnings.
    141.             m_SteamAPIWarningMessageHook = new SteamAPIWarningMessageHook_t(SteamAPIDebugTextHook);
    142.             SteamClient.SetWarningMessageHook(m_SteamAPIWarningMessageHook);
    143.         }
    144.     }
    145.  
    146.     // OnApplicationQuit gets called too early to shutdown the SteamAPI.
    147.     // Because the SteamManager should be persistent and never disabled or destroyed we can shutdown the SteamAPI here.
    148.     // Thus it is not recommended to perform any Steamworks work in other OnDestroy functions as the order of execution can not be garenteed upon Shutdown. Prefer OnDisable().
    149.     protected virtual void OnDestroy() {
    150.         if (s_instance != this) {
    151.             return;
    152.         }
    153.  
    154.         s_instance = null;
    155.  
    156.         if (!m_bInitialized) {
    157.             return;
    158.         }
    159.  
    160.         SteamAPI.Shutdown();
    161.     }
    162.  
    163.     protected virtual void Update() {
    164.         if (!m_bInitialized) {
    165.             return;
    166.         }
    167.  
    168.         // Run Steam client callbacks
    169.         SteamAPI.RunCallbacks();
    170.     }
    171. #else
    172.     public static bool Initialized {
    173.         get {
    174.             return false;
    175.         }
    176.     }
    177. #endif // !DISABLESTEAMWORKS
    178. }
     
  10. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    7,845
    That should still work.
    You should just need to call
    SteamManager.Instance
    which will create the GameObject and ensure its initialized in its Awake method.

    So maybe something like

    Code (csharp):
    1. if (!SteamManager.Instance.Initialized)
    2.     Debug.LogError("Something went wrong")
    See if that works
     
  11. bottledgalaxy

    bottledgalaxy

    Joined:
    Jul 19, 2019
    Posts:
    83
    So if I use that line, i'm getting an error `Cannot access protected property 'Instance' here`

    I tried calling
    if (!SteamManager.Initialized)
    directly, since that seems to internally call Instance, but doing that just logs out the "Something went wrong" line. If I put break points inside the
    Awake
    , I can see how that is being hit AFTER going to the
    Debug.LogError("Something went wrong")


    So doesn't look like
    Awake
    is being called before returning the results of
    Initialized


    Also, (not sure if related) i'm seeing this in the Console:
    But i'm not changing scenes or anything... this shows up just by hitting play
     
    Last edited: Sep 1, 2023
  12. bottledgalaxy

    bottledgalaxy

    Joined:
    Jul 19, 2019
    Posts:
    83
    BTW, I have this in the Addressable Assets Settings: Screenshot 2023-09-01 at 11.46.10 AM.png

    but
    GetStartupLocale
    is still being called BEFORE
    SteamInitialization
    's
    InitializeAsync
     
  13. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    7,845
    Oh Instance is protected. Can you change it to public? Otherwise you may be able to just create a GameObject and add a SteamManager component instead.
     
  14. bottledgalaxy

    bottledgalaxy

    Joined:
    Jul 19, 2019
    Posts:
    83
    I tried that and Awake is still not being called. I tried calling it manually, but then it throws an exception when trying to call DontDestroyOnLoad

    Creating a GameObject on the scene and adding the SteamManager component there instead is what I had initially, but then we are back to square 1 where GetStartupLocale is called before the steam manager gets initialized...

    Have you seen my last message though? Even when having an addressables initialization object, GetStartupLocale is still being called BEFORE InitializeAsync
     
  15. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    7,845
    It should not be calling GetStartupLocale until Addressables has initialized which includes the initialization objects.
    What version of the package are you using? Make sure its 1.4.4, if its not visible in the package manager you can force an update by editing the manifest.json file in the Packages folder.
     
  16. bottledgalaxy

    bottledgalaxy

    Joined:
    Jul 19, 2019
    Posts:
    83
    Screenshot 2023-09-01 at 2.29.51 PM.png

    Yes, i'm using 1.4.4
     
  17. bottledgalaxy

    bottledgalaxy

    Joined:
    Jul 19, 2019
    Posts:
    83
  18. bottledgalaxy

    bottledgalaxy

    Joined:
    Jul 19, 2019
    Posts:
    83
    I just tried building, in case it was an editor-only issue, and now i'm getting this error and can't build:
    That files is just what you shared with me earlier:
    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3. using UnityEngine.AddressableAssets.Initialization;
    4. using UnityEngine.ResourceManagement.Util;
    5. using UnityEngine.Serialization;
    6. /// <summary>
    7. /// Asset container for CacheInitializationData.
    8. /// </summary>
    9. [CreateAssetMenu(fileName = "SteamInitializationSettings.asset", menuName = "Addressables/Initialization/Steam Initialization Settings")]
    10. public class SteamInitializationSettings : ScriptableObject, IObjectInitializationDataProvider
    11. {
    12.     /// <summary>
    13.     /// Display name used in GUI for this object.
    14.     /// </summary>
    15.     public string Name
    16.     {
    17.         get { return "Steam Settings"; }
    18.     }
    19.     /// <summary>
    20.     /// Create initialization data to be serialized into the Addressables runtime data.
    21.     /// </summary>
    22.     /// <returns>The serialized data for the initialization class and the data.</returns>
    23.     public ObjectInitializationData CreateObjectInitializationData()
    24.     {
    25.         return ObjectInitializationData.CreateSerializedInitializationData<SteamInitialization>(typeof(SteamInitialization).Name);
    26.     }
    27. }
     
  19. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    7,845
    SteamInitializationSettings should be an editor only script.
    You can put it inside of an #if UNITY_EDITOR and #endif section.
     
  20. bottledgalaxy

    bottledgalaxy

    Joined:
    Jul 19, 2019
    Posts:
    83
    I went a different route and ended up doing this inside the SteamManager Monobehavior after it gets initialized:
    Code (CSharp):
    1.         if (!PlayerPrefs.HasKey("selected-locale"))
    2.         {
    3.             string languages = Steamworks.SteamApps.GetAvailableGameLanguages();
    4.             string steamLocale = Steamworks.SteamApps.GetCurrentGameLanguage();
    5.             switch (steamLocale)
    6.             {
    7.                 case "spanish":
    8.                 case "latam":
    9.                     LocalizationSettings.SelectedLocale =
    10.                         LocalizationSettings.AvailableLocales.GetLocale(SystemLanguage.Spanish);
    11.                     break;
    12.                 case "brazilian":
    13.                 case "portuguese":
    14.                     LocalizationSettings.SelectedLocale =
    15.                         LocalizationSettings.AvailableLocales.GetLocale(SystemLanguage.Portuguese);
    16.                     break;
    17.             }
    18.         }
    Seems to be working great and no more headaches
     
    karl_jones likes this.