Forum rules - please read before posting.

"Duplicate for each line?" not available for menuSource == MenuSource.UnityUiInScene

Hello!

I'm encountering an issue with subtitles during a cutscene created using Timeline in Unity.

Here's the situation: I have a non-player character (NPC) speaking, and just before the NPC's speech ends, the Player starts talking. I have both subtitles visible at the same time - that's okay.

During this cutscene, the Player character gameobject is within the camera's viewport but remains invisible, as the Player is just a First-Person Camera.
This leads to a problem where the last NPC's Speech Playable Clip displays the text above the Player's position instead of above the NPC.

Here's how I have things set up:

  • For NPCs, I've created a separate "Subtitles" menu with the position type set to "Above Speaking Character" and the speakers set to "All Except Player."
  • For the Player, I've established a "Subtitles Player" menu with the position type as "Manual," and it appears in the bottom center of the screen.

I attempted to fix the problem by using the "Duplicate for each line?" option but found that this choice is only available for menus where "menuSource != MenuSource.UnityUiInScene."
Since I must use "Unity In Scene" menus, I'm unable to access this option.

As I write this now, I checked, and it also turned out that I'm in general unable to make a few NPCs speak at the same time.

My questions are:

  • Why is the "Duplicate for each line?" option hidden for in-scene menus?
  • How can I correct this subtitle issue?

Thank you,
Barnim

Comments

  • In-scene Menus reference a specific UI Canvas in the scene, as opposed to a prefab that has a separate instance each time it is spawned in.

    Can you elaborate on the need for use of the In Scene option, as opposed to Prefab? I might be able to suggest a workaround if I understand the context.

  • I want my game to work on PC (flat screen) and VR - out of the box. For UI in VR, I'm using Canvas with render mode: World Space.

    In the AC manual, it is written:
    "If your Canvas' Render mode is World Space, you will need to use Unity Ui In Scene."

    For this reason, I have prepared two prefabs, each prefab contains a set of all menus, and each has the same ConstantID numbers for the relevant elements, so they are automatically detected by the MenuManager:

    • ScreenSpaceUI - all Canvas have render mode: "Screen Space - Overlay," which allows the "Always fit within screen" option to work
    • WorldSpaceUI - all Canvas have render mode: "World Space," which allows the menus to work within the Player's world, and I can control their behavior, such as the distance from the Player or whether they follow the Player

    When loading a scene, depending on whether I detect a VR device or not, I load the appropriate prefab into the scene via script.

  • "If your Canvas' Render mode is World Space, you will need to use Unity Ui In Scene."

    Perhaps the Manual statement should be revised, as it's not technically a restriction. It's more to do with if your Menu was in a fixed position in the world, e.g. a UI overlay on a computer screen object in the scene.

    Try using "Unity Ui Prefab" mode for your world-space Canvas - it should still work.

  • I tried using the "Unity Ui Prefab" mode for world-space Canvas and it actually works.

    But now I have a problem with replacing prefabs after launching the application.
    I wrote myself a class for replacing prefabs:

    using System.Collections.Generic;
    using UnityEngine;
    using AC;
    
    namespace JAGS
    {
        public class JagsMenuLoader
        {
            private Dictionary<string, Menu> menus;
    
            public JagsMenuLoader()
            {
                menus = new Dictionary<string, Menu>();
            }
    
            public void LoadMenu(string menuName)
            {
                //Menu menu = KickStarter.menuManager.GetMenuWithName(menuName);
                Menu menu = PlayerMenus.GetMenuWithName(menuName);
    
                if (menu != null)
                {
                    menus[menuName] = menu;
                }
                else
                {
                    Debug.LogWarning("No menu found with name: " + menuName);
                }
            }
    
            public Menu GetMenu(string menuName)
            {
                if (menus.ContainsKey(menuName))
                {
                    return menus[menuName];
                }
                else
                {
                    Debug.LogWarning("No menu found with name: " + menuName);
                    return null;
                }
            }
    
            public void LoadPrefabToMenu(string menuName, List<Canvas> uiPrefabs)
            {
                Menu menu = GetMenu(menuName);
                if (menu != null)
                {
                    Canvas prefab = uiPrefabs.Find(p => p.name == menu.PrefabCanvas.name);
                    if (prefab != null)
                    {
                        menu.PrefabCanvas = prefab;
                    }
                    else
                    {
                        Debug.LogWarning("No matching prefab found for menu: " + menu.title);
                    }
                }
            }
        }
    }
    

    I use it in my script, which I run in the scene.
    Here's an excerpt from my JagsGameEngine class:

    namespace JAGS
    {
        /// <summary>
        /// JAGS game and scene setup and UI management.
        /// </summary>
        public class JagsGameEngine : MonoBehaviour
        {
            [Header("UI ScreenSpace")]
            public List<Canvas> screenSpacePrefabs;
    
            [Header("UI WorldSpace")]
            [SerializeField] bool forceWorldSpaceUI = false;
            public List<Canvas> worldSpacePrefabs;
    
            bool vrEnabled;
    
            // Menus names      
            private string pauseMenuName = "Pause";
            private string optionsMenuName = "Options";
            private string saveMenuName = "Save";
            private string loadMenuName = "Load";
            private string profilesMenuName = "Profiles";
            private string inventoryMenuName = "Inventory";
            private string conversationMenuName = "Conversation";
            private string subtitlesNarratorMenuName = "Subtitles Narrator";
            private string subtitlesPlayerMenuName = "Subtitles Player";
            private string subtitlesMenuName = "Subtitles";
            private string interactionMenuName = "Interaction";
            private string inventoryInteractionMenuName = "Inventory Interaction";
            private string hotspotMenuName = "Hotspot";
    
            private List<Menu> menus;
            private JagsMenuLoader menuLoader;
    
            private void LoadMenus()
            {
                menuLoader = new JagsMenuLoader();
    
                string[] menuNames = new string[]
                {
                    pauseMenuName,
                    optionsMenuName,
                    saveMenuName,
                    loadMenuName,
                    profilesMenuName,
                    inventoryMenuName,
                    conversationMenuName,
                    subtitlesNarratorMenuName,
                    subtitlesPlayerMenuName,
                    subtitlesMenuName,
                    interactionMenuName,
                    inventoryInteractionMenuName,
                    hotspotMenuName
                };
    
                List<Canvas> uiPrefabs = (vrEnabled || forceWorldSpaceUI) ? worldSpacePrefabs : screenSpacePrefabs;
                foreach (var menuName in menuNames)
                {
                    menuLoader.LoadMenu(menuName);
                    menuLoader.LoadPrefabToMenu(menuName, uiPrefabs);
                }
            }
    
            private void Start()
            {   
                LoadMenus();
            }
        }
    }
    

    In the Inspector, I filled the screenSpacePrefabs and worldSpacePrefabs lists with prefabs from the project.
    And after running it, I noticed that the prefabs that are connected to MenuManager before launching the project are loaded into the game. During startup, they are replaced, but AC no longer takes them into account. Only after restarting the application are they loaded - for the obvious reason, which I wrote earlier.

    A few months earlier, I experimented with replacing the entire MenuManager config. I had two versions of MenuManagerPC and MenuManagerVR. While starting the application, I determined which MenuManager to load, but it similarly did not work, because the one currently loaded into AC Game Editor was taken into account at startup. Then I hacked the system and forced an AC restart right after launch if a menu change was required. This was supposed to be a temporary solution. And as a result, I came up with the idea that I would use In-Scene UI for both UI versions: ScreenSpace and WordSpace. And everything worked until I discovered that in this configuration, Subtitles have one instance.

    How can I have two separate UI versions, one for ScreenSpace and one for WorldSpace, using the Source mode: "Unity Ui Prefab", and load either one during the application startup depending on the needs?

  • At what point is the choice between the two made?

    Though you need to be careful when swapping Manager assets at runtime, you should be OK to do this so long as you make the change before AC itself boots up. You can do this by setting your game's first scene to one that just assigns the intended Manager to AC's References asset, and then open the first scene that uses AC.

    The alternative would be to have both variants of the Menu in the same Menu Manager, and pair their lock states so that only one is unlocked at a time.

    When a Menu is locked, it will not turn on even when its "Appear type" condition is met.

  • Thank you for all your answers.

    The first line of code in the OnEnable method of my class checks whether a VR device is detected. This class customizes the behavior of Input/UI/Menus according to the needs of my project and is loaded with each scene.

    The idea of swapping out the MenuManager in the startup scene generally seems like a good one. It's relatively safe in terms of the potential for errors. However, it does add some complexity to both the development and testing of the UI in both variants. Specifically, I would have to manually switch the MenuManager each time I put on or take off the VR device while working on a particular scene.

    For the sake of convenience, I've chosen to let menus adapt automatically or be manually toggled via a single checkbox. This comes with its own set of risks, but it's the path I'm currently on.
    To manage this, I've duplicated all my menus and appended a "VR" suffix to distinguish between standard and VR-specific menus. I've created a custom class to allow for easier and safer menu referencing in the code, eliminating the need for excessive string hardcoding. While this solution is functional, it demands careful attention moving forward, particularly when using AC built-in methods to retrieve menus.
    This approach also fills my logs with numerous "Cannot turn on menu XXXXXXXXX as it is locked" messages.

    I initially considered simplifying things by duplicating menus with identical names and toggling the 'isLocked' setting based on the menu.PrefabCanvas.renderMode. However, this idea fell through because PlayerMenus.GetMenuWithName("menuName") doesn't check the 'isLocked' status and just returns the first matching menu it finds.

    In summary, I'd prefer a more elegant solution that would allow for seamless swapping of Menus in MenuManager at runtime. It's unfortunate that this isn't possible, as it seems it should be - given that new menus can be constructed and registered with MenuManager in code during runtime, at least from what I've read in the documentation and code. I haven't delved deeply, so I may be mistaken.

    Do you think it is possible that future updates might allow us to selectively disable debugger Info Logs for those that say 'Cannot turn on menu XXXXXXXXX as it is locked'? I'm aware that the CanDisplay method can be used to disable all Info level logs, but could there be an option to mute logs for each individual locked menu, for example via MenuManager options?

  • PlayerMenus.GetMenuWithName("menuName") doesn't check the 'isLocked' status and just returns the first matching menu it finds.

    You can iterate through the GetMenus function to check for custom conditions:

    public Menu GetUnlockedMenu (string menuName)
    {
        List<Menu> menus = PlayerMenus.GetMenus (true);
        foreach (Menu menu in menus)
        {
            if (menu.title == menuName && !menu.isLocked)
            {
                return menu;
            }
        }
        return null;
    }
    

    I'd prefer a more elegant solution that would allow for seamless swapping of Menus in MenuManager at runtime

    Caution will need to be exercised - but the PlayerMenus component also has a RebuildMenus function that takes a MenuManager asset file as a parameter. This will destroy all existing runtime Menus, however. If you wanted to retain e.g. the on/off/locked states of the old Menus and apply them to the new, you'd need to do so manually.

    Do you think it is possible that future updates might allow us to selectively disable debugger Info Logs for those that say 'Cannot turn on menu XXXXXXXXX as it is locked'?

    I understand the intent here - though I feel a per-Menu option like that would be too cumbersome, particularly as it would chiefly be used for the 'locked' message and other messages would still be desirable to display.

  • After deep reflection and another experiment, I concluded that it's better to have two Menu Managers and switch them "on the fly."

    I managed to do this elegantly = without the need to restart AC when launching the application. I do it during the Start() method, invoking the following method:

    public void LoadMenuManager(MenuManager selectedMenuManager)
    {
        if (selectedMenuManager != null)
        {
            if (AdvGame.GetReferences().menuManager != selectedMenuManager)
            {
                AdvGame.GetReferences().menuManager = selectedMenuManager;
                KickStarter.playerMenus.RebuildMenus(selectedMenuManager);
                Debug.LogWarning("WARNING! Menu manager switched to:" + selectedMenuManager.name);          
            }
        }           
    }
    

    Like this:

    menuLoader.LoadMenuManager(vrEnabled ? menuManagerVR : menuManagerPC);
    

    The line KickStarter.playerMenus.RebuildMenus(selectedMenuManager); loads the Menu Manager at runtime.
    By setting AdvGame.GetReferences().menuManager = selectedMenuManager;, I not only load the Menu Manager into the editor but also ensure that changes made during runtime are saved back into that Manager.

    In my opinion, this looks good, and thanks to you, I have a better understanding of the potential consequences.
    I decided to use this more as a development tool. For the build, I can easily disable it and apply this mechanism for loading the appropriate Menu Manager in the startup scene before AC is loaded.
    This way, I eliminate all the headaches related to menu names - not only in the code but also in the Actions, keeping identical ConstantIDs, flood of logs about locked menus, and many other issues I had or could have had.

    Thanks again for your help and your exhaustive answers!

Sign In or Register to comment.

Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Welcome to the official forum for Adventure Creator.