About optimization

Started by Baguettator, Tue 26/08/2025 15:45:00

Previous topic - Next topic

Baguettator

Hi,

I don't have really deep knowledge in optimization for my game. And I would like to ask about what I (or we) could do for it when time to optimize the game comes.

For my own situation, my game uses lots of data (strings, int and some custom structs with many variables inside) because it is a simulation of a tabletop game.

I already made that the game doesn't "memorize" all the data of all the maps, instead the game reads txt files with data stored in. This replaces big arrays of structs containing hundreds of data with only 1 instance of the same struct.

It was a first step.

Now, my game has many many texts. Because, many things to tweak, many things to explain, many GUIs... and it's needed. Do texts (strings) have to be optimized ? To have a "lighter" game while running ? I thought that many texts could be used like that :

Code: ags
String Givetxt(int n)
{
  if (n==e_eventFood) return "This event gives food !";
  else if (n==e_eventTools) return "This event gives tools !";
}

void DrawEvent(int n)
{
  Labelevent.Text=Givetxt(n);
}

Instead of :

Code: ags
String events[2];
events[0]="This event gives food !";
events[1]="This event gives tools !";

void DrawEvent(n)
{
  Labelevent.Text=events[n];
}

If my texts don't need to be accessed for any reason (for example, if I need to know which event has been drawn, I can just memorize the "index" of the event with a single variable taking its value "n" in the DrawEvent function), it can avoid memorizing an array of strings, without beeing "laggy".

I think strings are loudest data in AGS, int are generally needed and can't be optimized I presume.

Am I right in this point of view ? Should I optimize like I said ? Or is it near no difference at all ?

eri0o

I only look into optimizing things that run many times in a frame.

About strings for custom things, as data, if you can have them in a separate file (that's packaged with the game using the $DATA$ token for reading), this can make it easier for you to change and update things ignoring the data structure in your game, which may also help you play with different things.

Crimson Wizard

#2
Optimization is commonly done for:
- speed
- runtime memory
- disk space
- convenience of coding

Sometimes optimizing for one thing makes it worse in another. Sometimes it's okay, sometimes it is not.

But you should not optimize unless you have a proof that the current state is problematic.

Quote from: Baguettator on Tue 26/08/2025 15:45:00I think strings are loudest data in AGS, int are generally needed and can't be optimized I presume.

If by "loudest" you mean "taking most memory", then that would be graphics.
You may put an effort optimizing memory taken by strings in script only to find out later that your game's memory requirement went down by 1%.
So before optimizing, try to have a good idea about how much do they take, and if that's really is a problem.

EDIT: also, strictly speaking, "ints" can be "optimized" by packing several values in one. Although nobody does this today, unless there are millions of values maybe.

Danvzare

From my own understanding, generally when it comes down to optimization, memory being used by variables such as strings or ints should be pretty low on your priority list. You can optimize things a bit there, use shorts instead of ints when the value doesn't go above 30,000, that sort of stuff. But any benefit will mostly be negligible.

What you actually want to do, is optimize any functions that are repeatedly executed.
For example, let's say you have a function that checks if the cursor is on the left side of the screen, the right side of the screen, or the center.
Code: ags
if(cursor==left) do this;
else if(cursor==right) do this;
else do that;
Now let's say this function runs every time you click. You expect people to click a lot, and for some reason, you also expect the cursor to be in the center 90% of the time. You'd want to optimize the function because it's running a lot, and the best way to optimize it would be to check if the cursor is in the center first, so it can skip the other checks.
Code: ags
if(cursor==center) do this;
else if(cursor==left) do this;
else do that;

This is a vast over simplification. But that's one thing can do when optimizing your game.
Another would be to look over your code and see where things can be simplified and made more elegant. Once again, the benefits are usually negligible, but if it's a function that runs a lot, it could add up.

In general though, you usually don't have to do optimizations unless you encounter an issue.
Doing so will also help with testing. Since it's a bit hard to know if what you've changed has made things better or worse, when you literally can't see a difference (because there isn't a problem with which to see a difference in).

Crimson Wizard

#4
Quote from: Danvzare on Wed 27/08/2025 13:19:11Now let's say this function runs every time you click. You expect people to click a lot

"Often" in human's perspective is not so often in CPU perspective. If the game is run in 60fps, then humans clicking is considerably less frequent, and usually not a problem. Something to also keep in mind: CPU can potentially do alot of things in a split second. Even if your operation is not so optimal, it may still be a fraction of what CPU can do, so it will do your inefficient operation and then rest, waiting for the next event, and nobody will notice anything. It becomes a real problem when the total work that CPU has to do at once exceeds a duration of game frame regularly.

I'm saying this, because in the past I've seen people trying to optimize things that are never going to be a problem (or asking questions about how to do that), but miss things that really would be nice to get fixed. I probably did the same...

Of course, your example of checking for the highest probable variant first is a valid optimization example on its own. I doubt that it will improve much in the described case, but such thing may improve alot in certain other situations, when the condition checking itself is slow. That is more relevant when hundreds or thousands of similar checks are done in a loop.

I remember a real case when switching an order of condition checks inside a AI logic improved fps in a game from 4 to 40 (not making this up).

Baguettator

Hmm understood. Thanks a lot for all the interesting replies !

Indeed, I think my game has not any big problem of optimization. I thought I could make things a bit better because my PC is really slow, but it's mostly due to graphic card (its graphic card can lag while watching a video if it has too much quality...).

I will think later about optimizing ints and string. I think it will be more a thing of "cleaner, better ordered and less emcumbered scripts files" than "faster script running".

Baguettator

Hmm, and now I see something in the "Default set up" window :

Sound cache : 32MB
Sprite cache size : 128
Texture cache size : 128

Is it very low for a 1920*1080 game ? Although I don't use any animation (it's a tabletop game, nothing moves except by SetPosition functions, but I use a lot of sprites because lots of tokens etc...), perhaps I should increase that ? How much could I set ?

Crimson Wizard

Quote from: Baguettator on Yesterday at 15:22:52Sound cache : 32MB
Sprite cache size : 128
Texture cache size : 128

Is it very low for a 1920*1080 game ? Although I don't use any animation (it's a tabletop game, nothing moves except by SetPosition functions, but I use a lot of sprites because lots of tokens etc...), perhaps I should increase that ? How much could I set ?

Add a debugging counter to your game that prints sprite and texture cache stats. You can use this function to get them:
https://adventuregamestudio.github.io/ags-manual/System.html#systemgetengineinteger

Play through your game and see how fast the cache fills up.

The cache matters when same images repeat through the game, and when it takes a long time to load ones necessary for the scene.

Texture cache is more useful when you play in OpenGL/Direct3D, Sprite cache is more useful if you play using Software renderer.

Baguettator

Thanks a lot ! So no problem from here, as I'm not higher than 30% for sprite cache (and about 4% for texture cache !).

I saw the AGS bug with the fps drop in the AGS 4.0 release thread. I'm not sure if it could be related, but I too encounter FPS drops when several GUIs (and especially if I use a semi-transparent white color for their background) are on screen. I have a pretty poor computer now so I don't know if it's linked. My repeatedly codes are :

Code: ags
// global script
function repeatedly_execute() 
{
if (gCompetencesPerso.Visible==true)
{
  Button *c[];
  c=new Button [0];
  c=DynamicArrays_AddButton(c, Compalt1);
  c=DynamicArrays_AddButton(c, Compalt2a);
  c=DynamicArrays_AddButton(c, Compalt2b);
  c=DynamicArrays_AddButton(c, Compprinc1);
  c=DynamicArrays_AddButton(c, Compprinc2);
  c=DynamicArrays_AddButton(c, Compprinc3a);
  c=DynamicArrays_AddButton(c, Compprinc3b);
  c=DynamicArrays_AddButton(c, Compprinc4);
  c=DynamicArrays_AddButton(c, Compsurv1);
  c=DynamicArrays_AddButton(c, Compsurv2a);
  c=DynamicArrays_AddButton(c, Compsurv2b);
  c=DynamicArrays_AddButton(c, Compsurv3a);
  c=DynamicArrays_AddButton(c, Compsurv3b);
  c=DynamicArrays_AddButton(c, Compsurv4);
  GUIControl *b=GUIControl.GetAtScreenXY(mouse.x, mouse.y);
  if (b!=null && b.AsButton!=null && b!=Fondcompetenceperso && b!=Retourcompetencesperso)
  {
    for (int i=0 ; i<c.Length ; i++)
    {
      if (b.AsButton==c[i])
      {
        String s;
        int t1=3, t2=8, t3=14;
        if (i<t1) s=comp_perso_desc[personnage[playerID].comp_alt[i]];
        else if (i>=t1 && i<t2) s=comp_perso_desc[personnage[playerID].comp_princ[i-t1]];
        else if (i>=t2) s=comp_perso_desc[personnage[playerID].comp_surv[i-t2]];
        Labelcomppersodesc.Text=s;
        break;
      }
    }
  }
  else Labelcomppersodesc.Text="";
}

if (gAffichageRessourcesColonie.Visible==true && parammunitionsdifferenciees==1)
{
  GUI *g=GUI.GetAtScreenXY(mouse.x, mouse.y);
  GUIControl *b=GUIControl.GetAtScreenXY(mouse.x, mouse.y);
  if (gDetailsMunitions.Visible==false && ((g!=null && g==gDetailsMunitions) || (b!=null && b==AffichageColoMun) || (g!=null && g==gAffichageRessourcesColonie && mouse.x>=gDetailsMunitions.X && mouse.x<gDetailsMunitions.X+gDetailsMunitions.Width)))
  {
      Scoreballesdetail.Text=String.Format("%d", ressource[munition][colo]);
      Scorecartouchesdetail.Text=String.Format("%d", ressource[cartouche][colo]);
      Scoreflechesdetail.Text=String.Format("%d", ressource[fleche][colo]);
      gDetailsMunitions.SetPosition(AffichageColoMun.X-15, gAffichageRessourcesColonie.Y + gAffichageRessourcesColonie.Height);
      gDetailsMunitions.Visible=true;
  }
  else if (g==null || b==null || g!=gAffichageRessourcesColonie || b!=AffichageColoMun) gDetailsMunitions.Visible=false;
}

if (IsKeyPressed(eKeyTab) && player.Room==ROOM_EXP)
{
  if (o_racc_fouille==null) // On crée l'overlay
  {
    racc_fouille=0;
    gAffichageexpedition.Transparency=50;
    b_racc_fouille=null;
    ds_racc_fouille=DynamicSprite.Create(1920, 1080);
    int fnt=eFontEurostyle45;
    DrawingSurface *d=ds_racc_fouille.GetDrawingSurface();
    d.DrawingColor=COULEUR_SURVOL_PLUSMOINS;
    int n=1;
    for (int i=0 ; i<NB_PIONS_FOUILLES ; i++)
    {
      if (pionfouille[i].Visible==true)
      {
        int x=pionfouille[i].X + gAffichageexpedition.X;
        int y=pionfouille[i].Y + gAffichageexpedition.Y;
        d.DrawString(x, y, fnt, String.Format("%d", n));
        b_racc_fouille=DynamicArrays_AddButton(b_racc_fouille, pionfouille[i]);
        n++;
      }
    }
    d.Release();
    o_racc_fouille=Overlay.CreateGraphical(0, 0, ds_racc_fouille.Graphic);
    o_racc_fouille.ZOrder=600;
    
    // On prépare la ds pour sélection
    ds_racc_fouille_select=DynamicSprite.Create(pionfouille[0].Width + 4, pionfouille[0].Height + 4);
    DrawingSurface *d_s=ds_racc_fouille_select.GetDrawingSurface();
    d_s.Clear(COULEUR_COMPCOLOACQUISE);
    d_s.Release();
  }
}
else if (player.Room==ROOM_EXP && o_racc_fouille!=null)
{
  o_racc_fouille=null;
  o_racc_fouille_select=null;
  ds_racc_fouille=null;
  ds_racc_fouille_select=null;
  racc_fouille=0;
  b_racc_fouille=null;
  gAffichageexpedition.Transparency=0;
}


}

function repeatedly_execute_always() 
{
  if (debug==1)
  {
    int maxSpriteCache = System.GetEngineInteger(ENGINE_VALUE_I_SPRCACHE_MAXNORMAL);
    int curSpriteCache = System.GetEngineInteger(ENGINE_VALUE_I_SPRCACHE_NORMAL);
    int maxTextureCache = System.GetEngineInteger(ENGINE_VALUE_I_TEXCACHE_MAXNORMAL);
    int curTextureCache = System.GetEngineInteger(ENGINE_VALUE_I_TEXCACHE_NORMAL);
    o_debug=Overlay.CreateTextual(5, 5, 1920, eFontEurostyle28, 0xFF4AFC41, String.Format("Sprite cache filled: %d / %d (%d %%)\nTexture cache filled: %d / %d (%d %%)", curSpriteCache, maxSpriteCache, curSpriteCache * 100 / maxSpriteCache, curTextureCache, maxTextureCache, curTextureCache * 100 / maxTextureCache));
    o_debug.ZOrder=1000;
  }
  else if (o_debug!=null) o_debug.Remove();
  
  
  GUIControl *bouton=GUIControl.GetAtScreenXY(mouse.x, mouse.y);
  GUI *guicheck=GUI.GetAtScreenXY(mouse.x, mouse.y);
  if (bouton!=null && bouton!=survol_souris && bouton.AsButton!=null && guicheck!=gTextesExpedition && bouton.AsButton!=Zoneblocnotes && bouton.AsButton.Text!= "New Button" && bouton.AsButton.TextColor!=COULEUR_SURVOL_PLUSMOINS && bouton.AsButton.TextColor!=COULEUR_COMPCOLOACQUISE && bouton.AsButton.Clickable==true) // C'est un bouton et il a du texte
  {
    if (survol_souris!=null) survol_souris.AsButton.TextColor=COULEUR_NOIR;
    survol_souris=bouton;
    if (survol_souris.AsButton.Text=="+1" || survol_souris.AsButton.Text=="-1" || survol_souris.AsButton.Text=="+5" || survol_souris.AsButton.Text=="-5") survol_souris.AsButton.TextColor=COULEUR_SURVOL_PLUSMOINS;
    else if (survol_souris.AsButton.Graphic==35 || survol_souris.AsButton.Graphic==42 || survol_souris.AsButton.Graphic==45 || survol_souris.AsButton.Graphic==48) survol_souris.AsButton.TextColor=COULEUR_SURVOL_COMP;
    else survol_souris.AsButton.TextColor=COULEUR_BLANC;
  }
  else if (survol_souris!=null)
  {
    if (bouton==null || (bouton!=null && bouton!=survol_souris))
    {
      survol_souris.AsButton.TextColor=COULEUR_NOIR;
      survol_souris=null;
    }
  }
  
  Hotspot *hot=Hotspot.GetAtScreenXY(mouse.x, mouse.y);
  Object *obj=Object.GetAtScreenXY(mouse.x, mouse.y);
  if (bouton!=null || (hot!=null && hot!=hotspot[0]) || (obj!=null && obj.Clickable==true))
  {
    String s="null";
    if (bouton!=null) s=bouton.GetTextProperty("Description");
    else if (guicheck==null && hot!=null && hot!=hotspot[0]) s=hot.GetTextProperty("Description");
    else if (guicheck==null && obj!=null) s=obj.GetTextProperty("Description");
    
    if (s!="null")
    {
      if (gDescGUI.Visible==false || gDescGUI.GetTextProperty("Description")!=s)
      {
        int fnt=eFontEurostyle28;
        int w=GetTextWidth(s, fnt) + 40;
        int h=GetTextHeight("A", fnt, w) + 20;
        ds_desc=DynamicSprite.Create(w, h);
        int e=4; // épaisseur des bordures noires
        DynamicSprite *fond=DynamicSprite.CreateFromFile("$INSTALLDIR$/Sprites/Boutons/Fond_Bouton_Sansbordure.bmp");
        DrawingSurface *d=ds_desc.GetDrawingSurface();
        d.DrawingColor=COULEUR_NOIR;
        d.DrawRectangle(0, 0, w, h);
        d.DrawImage(e, e, fond.Graphic, 0, w-(e*2), h-(e*2));
        d.DrawString((w-GetTextWidth(s, fnt))/2, (h-GetTextHeight("A", fnt, w))/2, fnt, s);
        d.Release();
        gDescGUI.SetSize(w, h);
        gDescGUI.BackgroundGraphic=ds_desc.Graphic;
        gDescGUI.SetTextProperty("Description", s);
      }
      
      DynamicSprite *temp=DynamicSprite.CreateFromExistingSprite(Mouse.GetModeGraphic(Mouse.Mode));
      int x=mouse.x;
      if (x+gDescGUI.Width>1920) x=1920-gDescGUI.Width;
      int y=mouse.y + temp.Height + 2;
      if (y + gDescGUI.Height > 1080) y=mouse.y - gDescGUI.Height - 5;
      gDescGUI.SetPosition(x, y);
      gDescGUI.Visible=true;
      if (bouton!=null) gDescGUI.ZOrder=bouton.OwningGUI.ZOrder + 1;
    }
    else
    {
      gDescGUI.Visible=false;
      gDescGUI.SetTextProperty("Description", "");
    }
  }
  else if (bouton==null || hot==null || obj==null)
  {
    gDescGUI.Visible=false;
    gDescGUI.SetTextProperty("Description", "");
  }

  if (gChoixdifficulte.Visible==true) SurvolParametres();

  if (gPauseExpedition.Visible==true)
  {
    pauseloop++;
    if (pauseloop==40 && Labelpause.Visible==true)
    {
      Labelpause.Visible=false;
      pauseloop=0;
    }
    else if (pauseloop==10 && Labelpause.Visible==false)
    {
      Labelpause.Visible=true;
      pauseloop=0;
    }
  }
}


// in the room
function room_RepExec()
{
  if (Labellistemusiques.Visible==false) VerifPlayList();

 tempsactuel = tempsactuel % 1440;
 
if (IsGamePaused() ==0 && parameventhorairepartour==0 && gAffichageexpedition.Visible==true && gEventobjectif.Visible==false && gEventvoitures.Visible==false && gCustomDisplay.Visible==false && gReglesspeciales.Visible==false && gChoixActionsEquipe.Visible==false
&& gInterfacepersos.Visible==false && gInterfaceVehicules.Visible==false && gPhasezombie.Visible==false && gDeckZombie.Visible==false && scene!=en_retourexp)
{
if (looping == 10) // Ne sera validée que tous les 10 cycles de jeu
{
MiseajourHorlogeExp();
looping = 0; // On remet le compteur à 0
}
else looping++; // On incrémente looping à chaque cycle de jeu

tempsaccel++;
 
 if (tempsaccel >= 40*60/vitessetemps)
 {
  tempsexp++;
  tempsactuel++;
  tempsaccel=0;
    
  if (paramdiffjournuit>0)
  {  
  if (tempsactuel==60) Eventhoraire(6);
  if (tempsactuel==120) Eventhoraire(6);
  if (tempsactuel==180) Eventhoraire(6);
  if (tempsactuel==240) Eventhoraire(6);
  if (tempsactuel==300) Eventhoraire(1);
  if (tempsactuel==360) Eventhoraire(1);
  if (tempsactuel==420) Eventhoraire(1);
  if (tempsactuel==480) Eventhoraire(1);
  if (tempsactuel==540) Eventhoraire(2);
  if (tempsactuel==600) Eventhoraire(2);
  if (tempsactuel==660) Eventhoraire(2);
  if (tempsactuel==720) Eventhoraire(2);
  if (tempsactuel==780) Eventhoraire(3);
  if (tempsactuel==840) Eventhoraire(3);
  if (tempsactuel==900) Eventhoraire(3);
  if (tempsactuel==960) Eventhoraire(3);
  if (tempsactuel==1020) Eventhoraire(4);
  if (tempsactuel==1080) Eventhoraire(4);
  if (tempsactuel==1140) Eventhoraire(4);
  if (tempsactuel==1200) Eventhoraire(4);
  if (tempsactuel==1260) Eventhoraire(5);
  if (tempsactuel==1320) Eventhoraire(5);
  if (tempsactuel==1380) Eventhoraire(5);
  if (tempsactuel==1440) Eventhoraire(5);
  }
  
  if (tempsactuel==60 || tempsactuel==780) aClocher_1h.Play(eAudioPriorityNormal, eOnce);
  if (tempsactuel==120 || tempsactuel==840) aClocher_2h.Play(eAudioPriorityNormal, eOnce);
  if (tempsactuel==180 || tempsactuel==900) aClocher_3h.Play(eAudioPriorityNormal, eOnce);
  if (tempsactuel==240 || tempsactuel==960) aClocher_4h.Play(eAudioPriorityNormal, eOnce);
  if (tempsactuel==300 || tempsactuel==1020) aClocher_5h.Play(eAudioPriorityNormal, eOnce);
  if (tempsactuel==360 || tempsactuel==1080) aClocher_6h.Play(eAudioPriorityNormal, eOnce);
  if (tempsactuel==420 || tempsactuel==1140) aClocher_7h.Play(eAudioPriorityNormal, eOnce);
  if (tempsactuel==480 || tempsactuel==1200) aClocher_8h.Play(eAudioPriorityNormal, eOnce);
  if (tempsactuel==540 || tempsactuel==1260) aClocher_9h.Play(eAudioPriorityNormal, eOnce);
  if (tempsactuel==600 || tempsactuel==1320) aClocher_10h.Play(eAudioPriorityNormal, eOnce);
  if (tempsactuel==660 || tempsactuel==1380) aClocher_11h.Play(eAudioPriorityNormal, eOnce);
  if (tempsactuel==720 || tempsactuel==1440) aClocher_12h.Play(eAudioPriorityNormal, eOnce);
 }
}
}

It doesn't seem to be a loud scripting ? Especially because many things trigger only if a specific GUI is on screen, and my experience of FPS drop occurs even if they are not visible. Also, I have a blocking function WaitMouseKey(-1) used for my own custom display function, the game is slower while the blocking function is waiting (looping until key or mouseclick).

I can share my game if needed (or Crimson perhaps you already have it)

Crimson Wizard

#9
Quote from: Baguettator on Yesterday at 20:16:37I saw the AGS bug with the fps drop in the AGS 4.0 release thread. I'm not sure if it could be related, but I too encounter FPS drops when several GUIs (and especially if I use a semi-transparent white color for their background) are on screen. I have a pretty poor computer now so I don't know if it's linked.

That bug was related specifically to dialog options shown on screen. But that does not mean that there cannot be other issues in parallel. Or it may be something with the game itself...


Khris

What is the c=DynamicArrays_AddButton(c, Compalt1); part doing?
You're creating an array of buttons of size 1, then keep overwriting it? Every frame? No idea what's happening there.

The other thing you can do is use a bit of maths to cut down on those if lines:
Code: ags
  int hours = tempsactuel / 60; // 0 - 23
  if (hours * 60 == tempsactuel) { // full hour
    Eventhoraire(hours / 4); // rewrite this function to use 0 instead of 6
  }

If you create an array and put the hour sounds inside, you can also use similar code to play these based on hours % 12.

Crimson Wizard

#11
Yeah, there's suspicious dynamic sprite creation and/or loading from file, and other drawing in "repeatedly_execute_always". It will take a while to understand under which conditions do they act, but just having them inside "rep-exec" is already a "red flag".

SMF spam blocked by CleanTalk