Search Unity

Question How to manage dynamic buttons in a GUI panel

Discussion in 'Scripting' started by Kousen10, Feb 23, 2024.

  1. Kousen10

    Kousen10

    Joined:
    Jul 23, 2020
    Posts:
    23
    Hi,

    It is possible that the title is not totally accurate but I would like to hear some opinions on the different strategies to update a GUI panel in order to show different buttons in run time.

    So, let's say that I have a bunch of characters and each of them has different commands, skills, items, etc... This information might come from a ScriptableObject or a json file.
    At the end I have to execute the command (attack, skill, item, escape,...) and if the command has a subcommand (specific skill or item) attached, it shall be possible to select it and perform it as well.

    My doubt comes when I start the turn for a specific character. I have the pool of buttons and I need to update them with the character command, skills, etc.

    My initial code looks as follows:

    Code (CSharp):
    1.     public class BattleCommandButtonsDataFactory
    2.     {
    3.         // IMPORTANT: This method shall be modified for adding new Battle Commands!!
    4.         public List<(string, UnityAction)> CreateBattleCommandButtonsData(BattleMenuesController battleMenuesController, BattleCommandsPanel battleCommandsPanel, BattleCommandsPanel battleSubcommandsPanel,
    5.             GameObject targetSelector, BattleCharacter battleCharacter, BattleCharactersManager battleCharactersManager, SkillFactory skillFactory, ItemFactory itemFactory,
    6.             string[] items)
    7.         {
    8.             List<(string, UnityAction)> buttonsData = new List<(string, UnityAction)>();
    9.  
    10.             foreach (var battleCommand in battleCharacter.BattleCommands)
    11.             {
    12.                 switch (battleCommand)
    13.                 {
    14.                     case BattleCommandsIds.Attack:
    15.                         var attackNextBattleMenu = new BattleTargetSelectorMenu(new AttackBattleCommand(), battleCharacter, battleCharactersManager, targetSelector);
    16.                         buttonsData.Add((battleCommand.ToString(), () => OnCommandButtonPressed(battleMenuesController, attackNextBattleMenu, battleCommandsPanel, new ShowHidePanel(battleCommandsPanel.gameObject))));
    17.                         break;
    18.                     case BattleCommandsIds.Skills:
    19.                         var skillSubcommandButtonData = new SkillSubcommandButtonData(battleMenuesController, battleCommandsPanel, battleSubcommandsPanel, targetSelector, battleCharacter, battleCharactersManager, skillFactory);
    20.                         var skillNextBattleMenu = new BattleSubcommandsMenu(battleSubcommandsPanel, skillSubcommandButtonData.OnCommandButtonPressed, battleCharacter.CharacterConfig.Skills);
    21.                         buttonsData.Add((battleCommand.ToString(), () => OnCommandButtonPressed(battleMenuesController, skillNextBattleMenu, battleCommandsPanel, new FadePanel(battleCommandsPanel.CanvasGroup))));
    22.                         break;
    23.                     case BattleCommandsIds.Items:
    24.                         var itemSubcommandButtonData = new ItemSubcommandButtonData(battleMenuesController, battleCommandsPanel, battleSubcommandsPanel, targetSelector, battleCharacter, battleCharactersManager, itemFactory);
    25.                         var itemNextBattleMenu = new BattleSubcommandsMenu(battleSubcommandsPanel, itemSubcommandButtonData.OnCommandButtonPressed, items);
    26.                         buttonsData.Add((battleCommand.ToString(), () => OnCommandButtonPressed(battleMenuesController, itemNextBattleMenu, battleCommandsPanel, new FadePanel(battleCommandsPanel.CanvasGroup))));
    27.                         break;
    28.                     case BattleCommandsIds.Run:
    29.                         var runNextBattleMenu = new BattleTargetSelectorMenu(new RunBattleCommand(), battleCharacter, battleCharactersManager, targetSelector);
    30.                         buttonsData.Add((battleCommand.ToString(), () => OnCommandButtonPressed(battleMenuesController, runNextBattleMenu, battleCommandsPanel, new ShowHidePanel(battleCommandsPanel.gameObject))));
    31.                         break;
    32.                     default:
    33.                         throw new Exception("Battle command not available");
    34.                 }
    35.             }
    36.  
    37.             return buttonsData;
    38.         }
    39.  
    40.         private void OnCommandButtonPressed(BattleMenuesController battleMenuesController, IBattleMenu nextBattleMenu, BattleCommandsPanel battleCommandsPanel,
    41.             IBattleCommandsPanelTransition battleCommandsPanelTransition)
    42.         {
    43.             battleCommandsPanel.BattleCommandsPanelTransition = battleCommandsPanelTransition;
    44.             battleMenuesController.LoadMenu(nextBattleMenu);
    45.         }
    46.     }
    I decided to use a Factory (if this can be called like that) but I need to pass a lot of arguments to it... which somehow seems wrong to me.

    Do you follow any specific strategies when updating a GUI element with input parameters that can change in run time?

    Thank you in advance!

    Kind regards
     
    Last edited: Feb 23, 2024
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,656
    Attached is my example of handling dynamic callbacks on a dynamic number of buttons.
    '
    It is pretty factory-like I suppose.

    If you change the callbacks you pretty much need somewhere that changes, so if it's not the delegate on the button, it will have to be some other delegate.
     

    Attached Files:

    Kousen10 likes this.
  3. Nad_B

    Nad_B

    Joined:
    Aug 1, 2021
    Posts:
    703
    This is one of the few times I find Reflection really useful.

    Let's say you have an Enum of your commands, and sub items (this could also be SOs instead):
    Code (CSharp):
    1. public enum BattleCommandType
    2. {
    3.     Skills,
    4.     Items,
    5.     Run
    6. }
    7.  
    8. public enum BattleCommandSubItemType
    9. {
    10.     Skill_Magic,
    11.     Skill_Dash,
    12.     Skill_Block,
    13.     Skill_Items_Knife
    14. }
    Let's start writing a Command processing system consisting of one processor for each command. First let's declare an attribute we must add to all our command processors so we can link each one to a BattleCommandType enum value:

    Code (CSharp):
    1. [AttributeUsage(AttributeTargets.Class)]
    2. public class BattleCommandProcessorAttribute : Attribute
    3. {
    4.     public BattleCommandProcessorAttribute(BattleCommandType commandType)
    5.     {
    6.         CommandType = commandType;
    7.     }
    8.  
    9.     public BattleCommandType CommandType { get; }
    10. }
    Next let's write the processors interfaces, one for processors with sub items, and one for those without:

    Code (CSharp):
    1. public interface IBattleCommandProcessorWithSubItems
    2. {
    3.     BattleCommandSubItemType[] GetSubItems();
    4.  
    5.     void ExecuteCommand(BattleCommandSubItemType subItemType, Player player);
    6. }
    7.  
    8. public interface IBattleCommandProcessor : IBattleCommandProcessorWithSubItems
    9. {
    10.     void ExecuteCommand(Player player);
    11.  
    12.     // IBattleCommandProcessorWithSubItems implict implementation
    13.     // where we ignore sub items as they're not needed
    14.     void IBattleCommandProcessorWithSubItems.ExecuteCommand(BattleCommandSubItemType subItem, Player player) => ExecuteCommand(player);
    15.  
    16.     BattleCommandSubItemType[] IBattleCommandProcessorWithSubItems.GetSubItems() => Array.Empty<BattleCommandSubItemType>();
    17. }
    Now let's start writing our processors, one for each BattleCommandType:

    Code (CSharp):
    1. [BattleCommandProcessor(BattleCommandType.Run)]
    2. public class RunCommandProcessor : IBattleCommandProcessor
    3. {
    4.     public void ExecuteCommand(Player player)
    5.     {
    6.         // Make player run...
    7.         player.Run();
    8.     }
    9. }
    10.  
    11. [BattleCommandProcessor(BattleCommandType.Skills)]
    12. public class SkillsCommandProcessor : IBattleCommandProcessorWithSubItems
    13. {
    14.     private static readonly BattleCommandSubItemType[] SubItems = new[]
    15.     {
    16.         BattleCommandSubItemType.Skill_Magic,
    17.         BattleCommandSubItemType.Skill_Dash,
    18.         BattleCommandSubItemType.Skill_Block
    19.     };
    20.  
    21.     public BattleCommandSubItemType[] GetSubItems() => SubItems;
    22.  
    23.     public void ExecuteCommand(BattleCommandSubItemType skill, Player player)
    24.     {
    25.         // Execute the skill...
    26.         player.ExecuteSkill(skill);
    27.     }
    28. }
    And finally, our CommandsRegistry, the class responsible for populating the processors automatically via reflection, and exposing their functionality:

    Code (CSharp):
    1. public static class CommandsRegistry
    2. {
    3.     private static readonly Dictionary<BattleCommandType, IBattleCommandProcessorWithSubItems> ProcessorsDictionary = GenerateProcessorsDictionary();
    4.  
    5.     public static BattleCommandSubItemType[] GetCommandSubItems(BattleCommandType commandType)
    6.     {
    7.         var processor = GetProcessor(commandType);
    8.  
    9.         return processor.GetSubItems();
    10.     }
    11.  
    12.     public static void ExecuteCommand(BattleCommandType commandType, Player player)
    13.     {
    14.         var processor = GetProcessor(commandType);
    15.  
    16.         var subItems = GetCommandSubItems(commandType);
    17.  
    18.         // Check if this Processor does not required sub items...
    19.         if (subItems.Length > 0)
    20.             throw new Exception($"Processor '{processor.GetType().Name}' requires a sub item.");
    21.  
    22.         processor.ExecuteCommand(default, player);
    23.     }
    24.  
    25.     public static void ExecuteCommand(BattleCommandType commandType, BattleCommandSubItemType subItem, Player player)
    26.     {
    27.         var processor = GetProcessor(commandType);
    28.  
    29.         var subItems = GetCommandSubItems(commandType);
    30.  
    31.         // Check if SubItem is valid for this Processor.
    32.         if (subItems.Length == 0)
    33.             throw new Exception($"Processor '{processor.GetType().Name}' does not require a sub item.");
    34.  
    35.         if (!subItems.Contains(subItem))
    36.             throw new Exception($"SubItem '{subItem}' is not valid for Processor '{processor.GetType().Name}'. " +
    37.                 $"Valid values are: {string.Join(", ", subItems)}.");
    38.  
    39.         processor.ExecuteCommand(subItem, player);
    40.     }
    41.  
    42.     private static IBattleCommandProcessorWithSubItems GetProcessor(BattleCommandType commandType)
    43.     {
    44.         if (!ProcessorsDictionary.TryGetValue(commandType, out var processor))
    45.             throw new Exception($"Cannot find a Processor for command type '{commandType}'. " +
    46.                 "Did you forget to add [CommandProcessor] attribute to it?");
    47.  
    48.         return processor;
    49.     }
    50.  
    51.     private static Dictionary<BattleCommandType, IBattleCommandProcessorWithSubItems> GenerateProcessorsDictionary()
    52.     {
    53.         return typeof(IBattleCommandProcessorWithSubItems)
    54.             .Assembly.GetTypes()
    55.             .Where(x => x.IsClass && !x.IsAbstract && typeof(IBattleCommandProcessorWithSubItems).IsAssignableFrom(x) && x.GetCustomAttribute<BattleCommandProcessorAttribute>() != null)
    56.             .Select(x => new { Type = x, CommandType = x.GetCustomAttribute<BattleCommandProcessorAttribute>().CommandType })
    57.             .ToDictionary(x => x.CommandType, x => (IBattleCommandProcessorWithSubItems)Activator.CreateInstance(x.Type));
    58.     }
    59. }
    Now we can just call
    CommandsRegistry.GetCommandSubItems()
    to get sub items for a specific BattleCommandType (for UI display for eg.), and one of the two overloaded methods (one with a chosen sub item, and one without)
    CommandsRegistry.ExecuteCommand()
    to execute a command.

    This also can be rewritten using SOs without the need for an attribute and Reflection (or even interfaces), since all the metadata needed for a command is contained within the SO, but the basics are the same (a CommandProcessorSO for each BattleCommandType).
     
    Last edited: Feb 24, 2024
    Kousen10 likes this.