how to 'update' String data from struct array?

Started by lafouine88, Tue 18/06/2024 18:52:44

Previous topic - Next topic

lafouine88

Hi guys

So here is my new situation...I swear I'm almost done with my game settings so soon there shouldn't be more questions^^hopefully "^^

I'm working on a rpg game. To make things more cumfortable for me I stored all my struct data in one single script. I set a function that I call on game start so all my spells,inventory items etc. are loaded at the beginning.
As so :

Code: ags
#loadscript.asc

function dataload(){
////loads the data for items...
  loot[0].sprite=2184;
  loot[0].Name="Casque rare de la Chouette";
  loot[0].bonus_agi=10;
loot[1].sprite=2185;
//.....

///and also for skills. Like a template that evolves through a skilltree

  skill[1].Nom="Hard skin"; 
  skill[1].Description=String.Format("Increases armor by %d%",(skill[1].lvlcurrent+1)*2);
  skill[1].sprite=403;
//skill[1].lvlcurrent=0;
skill[2].Name="Advanced strenght";
//.....


}


function game_start(){
dataload();
}

It's great for items and spells because there are 'frozen', they don't change with time. But the skilltree part evolves. When you grow to the next level you get a skill point that you can spend in any of those skills.

Then the corresponding int evolves : skill.lvlcurrent++;

And here is the problem. I use a repeatedly execute function to display the current level of progression of the selected spell. As so:

Code: ags

function mouse_over(){
   GUIControl*lab=GUIControl.GetAtScreenXY(mouse.x, mouse.y);
   GUI*gi=GUI.GetAtScreenXY(mouse.x, mouse.y);

 if(lab!=null){ ////there are a bunch of other conditions here but I removed them to stay focused on the issue

  Lblinfotitre.Text=String.Format("%s",skill[u].Nom ); ///this first label gives the skill name. Works perfectly
  Lblinfo.Text=String.Format("%s",skill[u].Description); ////this one calls for the description given before. Here is the problem
}

function repeatedly_execute(){
mouse_over();
}

I hope I'm making things clear enough "^^ basically when you pass the mouse over the buttons that represent each skill, a gui with two labels appears, and the labels change to give the right names and description of each spell. I link a video to show better. What should happen is the description should change from 2% to 4%,6%,8% and 10% with each click.

https://streamable.com/t0k82d

The problem is skill.description stays 'frozen'. I guess it loads at the start but 'prints' the current data to the string, and then doesn't evolve anymore. So if I add points to this skill, the description doesn't change as I would like it to.
I guess one of the solution would be to hard code every description in my mouse_over function but I find this way too heavy, and if there is a clever way to do that I would gladly take it.

Thanks in advance :)




Snarky

#1
Yes, the string is created when the dataload() function runs, and it won't update by itself. It's not like the String.Format() function is stored as part of the string or anything: String.Format() runs once as part of dataload(), and only the resulting output is stored.

To make this work, you'll need to use a totally different approach. I can think of two possibilities:

  • Don't store these descriptions in the struct array at all. Instead, have a function called something like GetSkillDescription(int skillIndex) that is basically a big switch statement, and dynamically generates the right string each time it is called.
  • Write some kind of string parser that can extract tokens from your strings and do basic calculations. For example, this string might look something like "Increases armor by $$(%SKILLLVL+1)*2$$", and before you display the string you would check for "$$"-delimited sections, decode the %-token and look up the value, and parse and perform the calculation. Overall, I'd say this is pretty challenging. (Perhaps suprisingly, the last part is probably the most difficult bit.)

I would probably recommend the first approach as much easier to do. However, I assume that somewhere this effect also has to be applied? In that case, shouldn't there be a function that returns this number? If so, that reduces the difficulty of option 2 considerably, since you no longer have to parse the structure of a mathematical expression and perform a calculation, just call the right function. (E.g. store the string as "Increases armor by %ARMOREFFECT"; scan the string for %-tokens, recognize %ARMOREFFECT and replace it by the output of skill[ i ].GetArmorEffect() or whatever the function is called.)

This actually points towards a third possible approach: Instead of storing these descriptions anywhere, generate them dynamically from the data in the struct. If somewhere in the struct there is a variable (or combination of variables) that gives it this effect, you should be able to derive the description according to some rules. That way you can be sure that the description and the actual effect match.

Whichever approach you choose, regenerating the string every game cycle is probably unnecessary, and could be somewhat processor-heavy (especially if you regenerate every string every cycle). You should try to detect when things change, and only update the description when that happens.

lafouine88

Ok thanks @Snarky. I guess the first option would be the easiest so I'll try and go for this one. I thought about the third option at some point but could not figure out how to make this work "^^, I'll stick to simple then.
Special thanks for the advice about the repeatedly execute string. I didn't really realize how memory consuming this could be, but you're right, it's stupid to keep generating it, I'll add a way to change this only when infos change.

I'll keep you informed, thanks again :)

Snarky

Quote from: lafouine88 on Tue 18/06/2024 20:43:34Special thanks for the advice about the repeatedly execute string. I didn't really realize how memory consuming this could be, but you're right, it's stupid to keep generating it, I'll add a way to change this only when infos change.

Well, using the first solution, the task of regenerating one string over and over is not going to be super-taxing, but it's still unnecessary. It potentially becomes a problem if you're regenerating MANY strings every cycle, and especially if you were going to use one of the other solutions that involved parsing the string. Or also if regenerating the string means having to re-render the GUI graphics. I'm not sure if AGS is smart enough to recognize that a new string is the same as the old one and it doesn't need to render it from scratch.

One simple thing that cuts down on the regeneration by a lot is to only generate it on the first turn after you've moved the mouse over the button.

Good luck!

lafouine88

@Snarky

So, the first part works perfectly (and I discovered the switch tool that I never used before"^^). That adds a lot of code but since it's a separate function it's quite readable.

For the optimisation, I did something quite simple that seems to work and seems logic, but i can't reaaly check if it is more optimized.
I modified like this :

Code: ags
int mo=0;

function mouse_over(){
///the same as before except at the very end...
mo=2;
}

function repeatedly execute(){
if(GUIControl.GetAtScreenXY(mouse.x, mouse.y)!=null){ ///||GUI.Getaatscreen||object... for all the type of objects that trigger mouse_over
mo=1;
}
else{
mo=0;
}


if(mo==1){
mouse_over();
}

}

Does that seem good?

Snarky

#5
Nice work!

I see what you're trying to do, but I don't think your solution is quite correct. As long as the mouse is over a GUIControl, mo will be set to 1 every time repeatedly_execute() runs, and then it will call mouse_over().

You have to put another test inside the check for whether the mouse is over anything. I think the logic also becomes clearer if you name the states:

Code: ags
// You could also set these up as an enum, but let's leave that for now
#define MOUSEOVER_NONE 0
#define MOUSEOVER_TRIGGERED 1
#define MOUSEOVER_HOLDING 2

int mo = MOUSEOVER_NONE;

function mouse_over()
{
  ///the same as before except at the very end...
  mo = MOUSEOVER_HOLDING;
}

function repeatedly execute()
{
  if(GUIControl.GetAtScreenXY(mouse.x, mouse.y)!=null) ///||GUI.Getaatscreen||object... for all the type of objects that trigger mouse_over
  {
    if(mo == MOUSEOVER_NONE)
      mo=MOUSEOVER_TRIGGERED;
  }
  else
    mo=MOUSEOVER_NONE;

  if(mo==MOUSEOVER_TRIGGERED)
    mouse_over();
}

If you were only dealing with GUI Controls there is a slightly more elegant way to do this, by keeping track of which Control the mouse was over in the last game cycle, but since you have to deal with GUIs and other interactive things all at once, I think it would end up being more complicated. But here's the basic idea:

Code: ags
GUIControl* lastControl;

function repeatedly execute()
{
  GUIControl* newControl = GUIControl.GetAtScreenXY(mouse.x, mouse.y);
  if(newControl != lastControl && newControl != null)
    mouse_over();
  lastControl = newControl;
}

(Edit: Comparing this to the code above, I also see how that version could be simplified to something very similar, but I'll leave that.)

(I know @Crimson Wizard has long wanted to create a type hierarchy where different entities inherit from a few base types. I haven't really seen the point of it myself, but this would be a clear use-case for it, since having all of these be sub-types of e.g. "Clickable" would allow you to compare GUIControls, GUIs, Characters, Objects and Hotspots all in one test.)


lafouine88

Hi again guys.
Actually, I still have problems with the simplification of my function. The solution I tried and that was corrected by @Snarky doesn't work well :s The info don't show up most of the time. Thought sometimes it does !??
I give you the code I'm using a bit more accurately hoping that someone could find the glitch. I couldn't :/

Code: ags
#define MOUSEOVER_NONE 0
#define MOUSEOVER_TRIGGERED 1
#define MOUSEOVER_HOLDING 2

function Mouse_over(){


  InventoryItem*item=InventoryItem.GetAtScreenXY(mouse.x, mouse.y);
  Character*perso=Character.GetAtScreenXY(mouse.x, mouse.y);
  GUIControl*lab=GUIControl.GetAtScreenXY(mouse.x, mouse.y);
  GUI*gi=GUI.GetAtScreenXY(mouse.x, mouse.y);
  Object *objet=Object.GetAtScreenXY(mouse.x, mouse.y);

if(lab!=null){
  
  if(lab.OwningGUI.ID==54){  ///////specific infos for specific gui54
int s=lab.ID-10;///specific link between my controls numbers and my array
///set the placement and dimensions for the info gui 'ginfoloot'
  gInfoloot.Visible=true;
  gInfoloot.X=41;
gInfoloot.Height=(stuff[s].lignes+5)*10;
  gInfoloot.Width=150;

  Infonom.Text=String.Format("%s",stuff[s].Name); ////this label is part of the info gui

  }
  //-------------------------------------------------------
    else if(lab==Btnbuffennemi1){
//here goes the same kind of infos, but for another type of control
//and so on for all the possible guicontrols
}
}
///------------------------------then we go in case the mouse is over a gui------------------
 if(gi!=null){
if(gi==ginventory){
///for some reason I use another gui to display info here, called 'ginfo_aide'
  ginfo_aide.Visible=true; 
  ginfo_aide.X=49;
  ginfo_aide.Y=329;
  Lblinfotitre.TextColor=65184;
  Lblinfotitre.Text="Backpack (B)"; 
  ginfo_aide.Height=20;
  ginfo_aide.Width=110;
}
else if(gi==gSpellbook){
//same for the other guis
//...
}
}
//-------------------
if(item!=null){
//same for items, characters and objects
}
mo = MOUSEOVER_HOLDING;
}

  void repeatedly_execute() {

    if(InventoryItem.GetAtScreenXY(mouse.x, mouse.y)!=null||Character.GetAtScreenXY(mouse.x, mouse.y)!=null||GUIControl.GetAtScreenXY(mouse.x, mouse.y)!=null||GUI.GetAtScreenXY(mouse.x, mouse.y)!=null||Object.GetAtScreenXY(mouse.x, mouse.y)!=null){
  if(mo==MOUSEOVER_NONE){mo=MOUSEOVER_TRIGGERED;}  
    }
    else{mo=MOUSEOVER_NONE;}

    if(mo==MOUSEOVER_TRIGGERED){
      Mouse_over();
      }
}

It's not the exact code but the structure is here and I couldn't find why this wouldn't work :( seems logic to me '^^
Do you guys have any ideas what could be going wrong? or suggestions for other methods to display my strings without refreshing all the time (see previous posts)

Thanks <3

Khris

I don't have time to debug this right now, just wanted to point out something:

Code: ags
  Lblinfo.Text = String.Format("%s", skill[u].Description);

can be written as

Code: ags
  Lblinfo.Text = skill[u].Description;

SMF spam blocked by CleanTalk