Adding buttons via code

Started by Cerno, Sun 24/01/2016 08:07:48

Previous topic - Next topic

Cerno

Hello all.

Is there a way to add GUI buttons via scripting that have not been added in the AGS tool?

I am trying to assemble a GUI out of parts that can grow with the number of items in it.
So the background of the GUI consists of images that are stitched together (according to the number of items displayed)
Then the items themselves are placed on top of the background.

My plan is to do this by adding a button for each background piece and item.
Then I would use visibility/clickability/z-order to piece together the GUI as I need.

Now I think it would be cleaner if I didn't have to predefine the maximum number of buttons I need beforehand, but rather add them programmatically.

So, is it possible to add buttons on the fly without predefining them in the AGS tool?

Oh and if my approach is stupid, I'll be glad to hear about better solutions. ;)
123  Currently working on: Sibun - Shadow of the Septemplicon

monkey0506

You can accomplish this (WARNING: it's not "simple"!) if you're willing to render the GUI buttons yourself as an alternative to setting them up in the editor. This is similar to what you have to do to add controls to a custom dialog "GUI" (see custom dialog options rendering in the manual). Note that you lose a lot of the built-in GUIControl functionality with this method (like GUIControl.GetAtScreenXY), so you'll have to compensate for those factors as well. I would recommend setting up a GUI with whatever other controls you need. Create a DynamicSprite to use for the GUI background graphic, then you can use that sprite to draw the extra controls onto.

For each button, you'd need to know things like NormalGraphic, MouseOverGraphic, PushedGraphic, and possibly a DisabledGraphic. It's not officially released yet, but assuming you are using AGS 3.4.0.3 or later, you can use a managed struct for this. Otherwise, you'll have to create a dynamic array for each member separately (code below uses 3.4.* features).

Spoiler
Code: ags
// header
managed struct DynamicButton
{
  import static DynamicButton* GetAtScreenXY(int x, int y);
  GUI *OwningGUI; // TODO: protect me, probably using an attribute and a protected member
  int X;
  int Y;
  int NormalGraphic;
  int MouseOverGraphic;
  int PushedGraphic;
  int DisabledGraphic;
  bool Enabled;
};
[close]

Spoiler
Code: ags
// script -- basic definitions and GetAtScreenXY
DynamicSprite *guiBackground[]; // one per GUI that has dynamic buttons
DynamicButton *guiButtons[]; // one per dynamic button (regardless of owning GUI)
int guiButtonCapacity = 0; // total capacity of guiButtons array
int guiButtonCount = 0; // total dynamic buttons in game

DynamicButton* CreateDynamicButton(GUI *owningGUI)
{
  // TODO: check if guiBackground sprite exists for owningGUI, create it if not
  // TODO: check guiButtonCapacity, resize array if needed
  int i = guiButtonCount;
  guiButtonCount++;
  guiButtons[i] = new DynamicButton;
  guiButtons[i].OwningGUI = owningGUI; // TODO: write an extender method to set the protected data, instead of using a public member
  // etc., TODO: initialize other members?
  return guiButtons[i];
}

static DynamicButton* DynamicButton::GetAtScreenXY(int x, int y)
{
  // doesn't account for z-ordering, overlap, etc.
  for (int i = 0, x1, x2, y1, y2, graphic; i < guiButtonCount; i++)
  {
    if (!guiButtons[i].OwningGUI.Visible)
    {
      continue; // owning GUI not visible, ignore this button
    }
    x1 = guiButtons[i].OwningGUI.X + guiButtons[i].X;
    x2 = guiButtons[i].OwningGUI.X + guiButtons[i].OwningGUI.Width; // temporary x2
    y1 = guiButtons[i].OwningGUI.Y + guiButtons[i].Y;
    y2 = guiButtons[i].OwningGUI.Y + guiButtons[i].OwningGUI.Height; // temporary y2
    if (!guiButtons[i].Enabled) // disabled, there is only one graphic to check
    {
      x2 = x1 + Game.SpriteWidth[guiButtons[i].DisabledGraphic];
      y2 = y1 + Game.SpriteHeight[guiButtons[i].DisabledGraphic];
    }
    if ((x < x1) || (x > x2) || (y < y1) || (y > y2))
    {
      continue; // coords out of range for this button
    }
    // the button graphics might have different widths, we need to determine which one we're looking at
    graphic = guiButtons[i].MouseOverGraphic;
    if (mouse.IsButtonDown(eMouseLeft))
    {
      graphic = guiButtons[i].PushedGraphic;
    }
    x2 = x1 + Game.SpriteWidth[graphic];
    y2 = y1 + Game.SpriteHeight[graphic];
    if ((x <= x2) && (y <= y2))
    {
      return guiButtons[i]; // x1 and y1 were already validated earlier
    }
  } // end of for loop
  return null; // no matching button found!
}
[close]

Spoiler
Code: ags
// script -- render loop
function repeatedly_execute_always()
{
  // TODO: split this out to a separate function to avoid clutter
  // TODO: check paused state and interface enabled state?
  // TODO: clear GUI backgrounds and draw their appropriate background sprites, etc., for all visible GUIs with dynamic buttons
  DynamicButton *bat = DynamicButton.GetAtScreenXY(mouse.x, mouse.y);
  for (int i = 0, graphic; i < guiButtonCount; i++)
  {
    if (!guiButtons[i].OwningGUI.Visible)
    {
      continue; // owning gui not visible, ignore this button
    }
    DrawingSurface *surface = guiBackground[guiButtons[i].OwningGUI.ID].GetDrawingSurface(); // TODO: get appropriate background sprite if not just allocating one per GUI in the game
    // TODO: this is probably a terribly inefficient way of doing this, grabbing the surface just to draw one button, then releasing it
    // -- consider pooling DynamicButtons in relation to their owning GUI somehow
    graphic = guiButton[i].NormalGraphic;
    if (!guiButton[i].Enabled)
    {
      graphic = guiButton[i].DisabledGraphic;
    }
    else if (bat == guiButton[i]) // may need null check here? (I forget how AGS handles this)
    {
      if (mouse.IsButtonDown(eMouseLeft))
      {
        graphic = guiButton[i].PushedGraphic;
      }
      else
      {
        graphic = guiButton[i].MouseOverGraphic;
      }
    }
    surface.DrawImage(guiButton[i].X, guiButton[i].Y, graphic);
    surface.Release();
  }
}
[close]

Spoiler
Code: ags
// script -- click handlers

function gMyGui_OnClick(GUI *theGUI, MouseButton button)
{
  DynamicButton *bat = DynamicButton.GetAtScreenXY(mouse.x, mouse.y);
  if ((bat != null) && (bat.OwningGUI == theGUI))
  {
    // TODO: clicked on a dynamic button, handle that!
  }
  // TODO: handle other clicks on the GUI as normal
}
[close]

All that said, it's definitely much simpler, and probably more efficient in the long run to just stick to a fixed preset of buttons set up in the editor. The concept isn't totally flawed, but it would be a real beast to make this work properly in a manageable, efficient way. (I copied a fair amount of the concept from other code I'm working on with dialog options rendering, or else I may have omitted the code samples altogether)

Cerno

Thanks for the comprehensive reply.

I was hoping that you pulled the examples out of your codebase from somewhere, otherwise I would really feel bad for going with your final suggestion.

Realizing that it is more effort compared to what it's worth, I will stick to defining a maximum number of buttons and setting them to visible and invisible as needed.

However, I will try using a DynamicSprite for the background instead of using inactive buttons, as this seems to be the cleaner approach.

Thanks a lot for your input on the matter and the great examples.
123  Currently working on: Sibun - Shadow of the Septemplicon

Cerno

So I have the implementation almost working and am going to post my final solution when I'm done.

Just one follow-up question to make the code a bit nicer:
Can I get access to a given Sprite if I know the slot number? I am particularly interested in the sprite's size, since I feel hardcoding the size is redundant and not very nice.

I know that I could probably make a DynamicSprite out of the sprite and then access its Width and Height property, but creating a dynamic sprite will make a copy and I feel creating the overhead just for access to a sprite's size isn't particularly good style.

It would be cool if it was possible to do something like this:
Code: ags
static DynamicSprite* DynamicSprite.ReferenceToExistingSprite(int slot)

which would work similarly to DynamicSprite.CreateFromExistingSprite but instead of creating a copy it would just return a pointer to the original.

It may not work internally since I assume that predefined sprites are handled in a different manner, but having (read only?) access to a Sprite datatype feels natural to me.

Maybe there is something similar available?
123  Currently working on: Sibun - Shadow of the Septemplicon

Khris

If you just need the dimensions, you can use Game.SpriteWidth[slot] and Game.SpriteHeight[slot].

Cerno

That's what I needed, thanks!

I swear, one of these days I'm going to write an overview about what to find where. I bet every conceivable property of everything is accessible, but for some reason I can't seem to find them.
There is such a wealth of information in the manual yet I always have trouble finding stuff (although it's funny how the first finding after typing "sprite" is "SpriteHeight property" :-D).
123  Currently working on: Sibun - Shadow of the Septemplicon

SMF spam blocked by CleanTalk