[solved]dynamic sprite optimisation

Started by lafouine88, Mon 07/10/2024 08:04:56

Previous topic - Next topic

lafouine88

Hi guys
I'm working on a RPG game, and to allow multiple item skins to be show, I used a function with dynamic sprites. Each item has 10 views (walk, idle atk,death...) and the maximum number of pieces of equipment is 6(torso,legs,head,Right hand,left hand and shoulders).
So if you equip an item, it's associated with a number that corresponds to the first view(walk), the rest are logically found with+1,+2...
As so :
Code: ags
///loading function at start up, defines all the items
butin[5].name=sword;
//...items characteristics
 butin[5].viewskin=100;---->the run sword view. 101 is idle, 102 is atk...always in the same order for each item


There is also an array to store the item number for each piece of equipment.
Code: ags
int Skin_stuffed[6];

So when you equip a new item, it changes the Skin_stuffed
  • number and then redraws the wholes views(10 views of 8-15 sprites :/) with all different parts of equipment like this :

Code: ags
 DynamicSprite *MySprite[80];///idle 10*8
 DynamicSprite *MySprite2[64];///run 9*8
 DynamicSprite *MySprite3[128];///atk 16*8
 DynamicSprite *MySprite4[104];///kick 13*8
 DynamicSprite *MySprite5[104];///LH 13*8
 DynamicSprite *MySprite6[112];///sweep 14*8
 DynamicSprite *MySprite7[80];///aoe 10*8
 DynamicSprite *MySprite8[120];///cry 15*8
 DynamicSprite *MySprite9[240];///die 30*8
 ViewFrame *Runviewframe[6]; 
 ViewFrame *Idleviewframe[6]; 
 ViewFrame *Atkviewframe[6];
 ViewFrame *Kickviewframe[6];
 ViewFrame *LHviewframe[6];
 ViewFrame *Sweepviewframe[6];
 ViewFrame *Jumpviewframe[6];
 ViewFrame *Cryviewframe[6];
 ViewFrame *Dieviewframe[6];


function skin_limbs(int currentloop,  int currentframe, int move, int limb){
  if(Skin_stuffed[limb]!=0){
    if(move==0){//run
        Runviewframe[limb]= Game.GetViewFrame(Skin_stuffed[limb], currentloop, currentframe);
        }
    else if(move==1){ ///idle
        Idleviewframe[limb]= Game.GetViewFrame(Skin_stuffed[limb]+1, currentloop, currentframe); 
      }
    else if(move==2){ ///atk
        Atkviewframe[limb]= Game.GetViewFrame(Skin_stuffed[limb]+2, currentloop, currentframe); 
        }
    else if(move==3){ ///kick
        Kickviewframe[limb]= Game.GetViewFrame(Skin_stuffed[limb]+3, currentloop, currentframe); 
        }
    else if(move==4){ ///Latk
        LHviewframe[limb]= Game.GetViewFrame(Skin_stuffed[limb]+4, currentloop, currentframe); 
        }
    else if(move==5){ ///sweep
        Sweepviewframe[limb]= Game.GetViewFrame(Skin_stuffed[limb]+5, currentloop, currentframe); 
        }
    else if(move==6){ ///jump
        Jumpviewframe[limb]= Game.GetViewFrame(Skin_stuffed[limb]+6, currentloop, currentframe); 
        }
    else if(move==7){ ///cry
        Cryviewframe[limb]= Game.GetViewFrame(Skin_stuffed[limb]+7, currentloop, currentframe); 
        }
    else if(move==8){ ///die
        Dieviewframe[limb]= Game.GetViewFrame(Skin_stuffed[limb]+8, currentloop, currentframe); 
        }
  }
else{///empty slots,view 55 is blank frames

  Idleviewframe[limb]=Game.GetViewFrame(55, currentloop, currentframe); 
  Runviewframe[limb]=Game.GetViewFrame(55, currentloop, currentframe); 
  Atkviewframe[limb]=Game.GetViewFrame(55, currentloop, currentframe); 
  Kickviewframe[limb]=Game.GetViewFrame(55, currentloop, currentframe);
  LHviewframe[limb]=Game.GetViewFrame(55, currentloop, currentframe);
  Sweepviewframe[limb]=Game.GetViewFrame(55, currentloop, currentframe);
  Jumpviewframe[limb]=Game.GetViewFrame(55, currentloop, currentframe);
  Cryviewframe[limb]=Game.GetViewFrame(55, currentloop, currentframe);
  Dieviewframe[limb]=Game.GetViewFrame(55, currentloop, currentframe);
}
}

function AlterEgoNormalView() 
{
  int MySpriteNo=0;
  int currentloop=0; 
  int currentframe;


  while (currentloop <= 7)//7 views
  {
    currentframe = 0;
    while (currentframe < 8) ///run view number of sprites
    {
      MySprite2[MySpriteNo] = DynamicSprite.Create(256, 256, true);
      DrawingSurface *MySurface = MySprite2[MySpriteNo].GetDrawingSurface();
      
      
      skin_limbs(currentloop, currentframe, 0,0);
      MySurface.DrawImage(0, 0, Runviewframe[0].Graphic); 
      skin_limbs(currentloop, currentframe, 0,1);
      MySurface.DrawImage(0, 0, Runviewframe[1].Graphic); 
      skin_limbs(currentloop, currentframe, 0,5);
      MySurface.DrawImage(0, 0, Runviewframe[5].Graphic); 
      skin_limbs(currentloop, currentframe, 0, 2); ///r arm
      MySurface.DrawImage(0, 0, Runviewframe[2].Graphic); 
      skin_limbs(currentloop, currentframe, 0, 3); ///L arm
      MySurface.DrawImage(0, 0, Runviewframe[3].Graphic);
      skin_limbs(currentloop, currentframe, 0, 4); shoulder
      MySurface.DrawImage(0, 0, Runviewframe[4].Graphic);
      }

       MySurface.Release();

      ViewFrame *MyViewFrame = Game.GetViewFrame(8, currentloop, currentframe);
      MyViewFrame.Graphic = MySprite2[MySpriteNo].Graphic;
      MySpriteNo ++;
      currentframe ++;
    }
    currentloop ++;
  }
player.ChangeView(8);///view 8 is the run combined view, 9 is idle,10 atk...
}

///then pretty much the same for
//function AlterEgoIdleView();
//AlterEgoaoeView();
//AlterEgoatkView();
//AlterEgocryView();
//AlterEgodeathView();
//AlterEgokickView();
//AlterEgosweepView();
//AlterEgoLHView();


//and this function is called when the item is equipped :

function reloadskin(int base, int slot){ 

Skin_stuffed[slot]=base;
  
AlterEgoNormalView();
AlterEgoIdleView();
AlterEgoaoeView();
AlterEgoatkView();
AlterEgocryView();
AlterEgodeathView();
AlterEgokickView();
AlterEgosweepView();
AlterEgoLHView();
}

I know that s a lot of information. basically each time you equip a new item it redraws 80+64+128+104+104+112+80+120+240 dynamic sprites...it's too much for the engine and it entails a small freeze (around 1-2 seconds depending on the computer's performance).
So there's two questions :
1/I suppose the code is far from optimal, maybe there s things I could change to make it less greedy.
2/If not, do you have ideas of a clever way to smooth the calculating time, something like : When you change equipment it just changes idle and run(which you need straight away) but the atk death views will be calculated at loading screen. Which does'nt create this unpleasant slow in game.

Any ideas to lower the calculation time would be great :)

Thanks and enjoy your week



Crimson Wizard

#1
My suggestion is to combine a character of several objects, like characters, that follow the main body part, have each their own View, and switch their current Loop and Frame to match the main body in late-rep-exec-always. Then you won't have to create DynamicSprites for this, or redraw anything at all, but simply change Frame Graphic properties in the respective part's View. That solution will be faster, and require less memory too.

lafouine88

You're right, it seems way easier "^^
One thing though, "late_repeatedly execute" doesn't work for me. I had to put in in "repeatedly execute" to see the dummy characters actually follow the animation. ???

Crimson Wizard

Quote from: lafouine88 on Mon 07/10/2024 15:11:52One thing though, "late_repeatedly execute" doesn't work for me. I had to put in in "repeatedly execute" to see the dummy characters actually follow the animation. ???

It's "late_repeatedly_execute_always":
https://adventuregamestudio.github.io/ags-manual/RepExec.html


lafouine88

you confirm this kind of calculation would be transparent for the AGS engine? I remember one advice from Khris I think who said that most of the time there is a better way than to use 'repeatedly execute' to save RAM. The advantage of the previous method was that once it was calculated it's done (and you don't need to take into account limbs ordre of appearance according to current frame for instance).
Maybe I could go for a mix of both, like, until the next loading screen it follows using this method, and then it moves to dynamic sprites on the next loading screen? Or do you think it is irrelevant ?

Snarky

Quote from: lafouine88 on Mon 07/10/2024 15:48:57I remember one advice from Khris I think who said that most of the time there is a better way than to use 'repeatedly execute' to save RAM.

It's hard to judge the pros and cons without seeing some examples, but creating ~1000 DynamicSprites seems almost certainly like overkill.

I'd probably go with CW's suggestion, but if you ever find yourself in a position where you do need to generate a thousand DynamicSprites, my suggestion would be to not do it all in one go, but do a small number each game cycle. (Assuming that not all the sprites need to be immediately available.)

In this case, since you already have it broken into 10 different functions, I would start by calling them in subsequent game cycles:

Code: ags
int skinUpdateCounter;

void reloadskin(int base, int slot)
{ 
  Skin_stuffed[slot]=base;
  skinUpdateCounter=1;
}

void updateSkin()
{
  switch(skinUpdateCounter)
  {
    case 1: AlterEgoNormalView(); break;
    case 2: AlterEgoIdleView(); break;
    case 3: AlterEgoaoeView(); break;
    case 5: AlterEgoatkView(); break;
    case 6: AlterEgocryView(); break;
    case 7: AlterEgodeathView(); break;
    case 8: AlterEgokickView(); break;
    case 9: AlterEgosweepView(); break;
    case 10:
      AlterEgoLHView();
      skinUpdateCounter = -1;
      break;
  }
  skinUpdateCounter++;
}

function repeatedly_execute_always()
{
  if(skinUpdateCounter>0)
    updateSkin();
}

This will spread out the processing over 10 cycles (1/4 second). Since you say it takes 1-2 seconds, this would not be enough, and you would have to handle each of the AlterEgo functions in several steps as well (most easily using a second counter, with some modifications to the counter incrementing logic).

Crimson Wizard

#7
Quote from: lafouine88 on Mon 07/10/2024 15:48:57I remember one advice from Khris I think who said that most of the time there is a better way than to use 'repeatedly execute' to save RAM.

Using "repeatedly execute" on its own has nothing to do with RAM. RAM is memory, it's used by creating new sprites and objects. Maybe you mean CPU load?

Quote from: lafouine88 on Mon 07/10/2024 15:48:57The advantage of the previous method was that once it was calculated it's done (and you don't need to take into account limbs ordre of appearance according to current frame for instance).

Setting new graphics to the view frames needs to be done only once the player wears the item.

Setting the current loop, current frame to sync with the main body has to be done in rep-exec.

Right, I forgot about sorting order.
There has to be some way to make it work, like have arrays that tell relative z-order of each body part per frame in animation. You likely will need only 1 array per limb per loop (direction). Then in rep-exec you will have to adjust N body parts similar to this:

Code: ags
function late_repeatedly_execute_always()
{
    cLeftArm.Loop = player.Loop;
    cLeftArm.Frame = player.Frame;
    cLeftArm.Baseline = player.Baseline + RelativeOrder[ ... ];
    cRightArm.Loop = player.Loop;
    cRightArm.Frame = player.Frame;
    cRightArm.Baseline = player.Baseline + RelativeOrder[ ... ];
    ..... and so on
}

This should not affect game speed much.

Then, if sorting order remains same throughout animation, then it may be set only once a loop (direction) changes.

lafouine88

Quote from: Crimson Wizard on Mon 07/10/2024 16:52:20Using "repeatedly execute" on its own has nothing to do with RAM. RAM is memory, it's used by creating new sprites and objects. Maybe you mean CPU load?
Yeah sorry I always confuse those^^ I mean CPU load. basically my intention is to lighten this one as much as possible to have as fluid a game as possible. If this coasts extra memory (or ram :p) I feel this is fine.


Quote from: Crimson Wizard on Mon 07/10/2024 16:52:20Setting new graphics to the view frames needs to be done only once the player wears the item.

Setting the current loop, current frame to sync with the main body has to be done in rep-exec.
Yes, no problem with the new views. I was just thinking maybe adding 12 repeatedly execute could affect the speed. And planned to limit this as much as possible. But in this case it really seems smoother to do it your way.
I planned to use baseline for limbs, should work fine. But once again, what I liked with dynamic sprites is that you calculate the whole thing once (including limbs priority according to positions) and then you're all good until the next change. Requiring no CPU load at all. As @Snarky said, it's hard to know how it would go on the average computer. On my gamer pc, multiple repeteadly execute is supported without problem but I don't know for a weaker one.

@Snarky Thanks, I also thought of this trick at first but it didn't go too well. It slowed down just a bit, but 10 times. Which was really bad for game experience. It's hard to find the right balance between immersion and engine cumfort. I also thought of a place restriction (like you can just gear up in town) and the new views for all attack moves would be done when you exit town (thus going throught a loading screen that would be used to create the next views), but it seemed really too heavy for gameplay :/

eri0o

#9
You can do a dumb version just to test performance. Other alternative is to use Room Overlays, it's a bit more manual handling but they also allow for more control.

I haven't tried this specific case, but I did an experiment using characters some years ago and anything above ~150 characters visible on screen, all moving, resulted in significant slow downs - this was still in AGS 3.5.0 so there's a chance things are better nowadays. I don't know your game and maps, but just mentioning to experiment earlier to see how things work for you - using placeholder graphics.

Ah just mentioning, there's an open source engine that is for diablo like games called Flare, I have looked into in the past, I didn't end up liking it very much but it has some open source games with assets that are also interesting if you need something placeholdery to play with : https://github.com/igorko/flare-game/tree/master/art_src

Khris

Two cents regarding this:

Spoiler
Code: ags
if(move==0){//run
        Runviewframe[limb]= Game.GetViewFrame(Skin_stuffed[limb], currentloop, currentframe);
        }
    else if(move==1){ ///idle
        Idleviewframe[limb]= Game.GetViewFrame(Skin_stuffed[limb]+1, currentloop, currentframe); 
      }
    else if(move==2){ ///atk
        Atkviewframe[limb]= Game.GetViewFrame(Skin_stuffed[limb]+2, currentloop, currentframe); 
        }
    else if(move==3){ ///kick
        Kickviewframe[limb]= Game.GetViewFrame(Skin_stuffed[limb]+3, currentloop, currentframe); 
        }
    else if(move==4){ ///Latk
        LHviewframe[limb]= Game.GetViewFrame(Skin_stuffed[limb]+4, currentloop, currentframe); 
        }
    else if(move==5){ ///sweep
        Sweepviewframe[limb]= Game.GetViewFrame(Skin_stuffed[limb]+5, currentloop, currentframe); 
        }
    else if(move==6){ ///jump
        Jumpviewframe[limb]= Game.GetViewFrame(Skin_stuffed[limb]+6, currentloop, currentframe); 
        }
    else if(move==7){ ///cry
        Cryviewframe[limb]= Game.GetViewFrame(Skin_stuffed[limb]+7, currentloop, currentframe); 
        }
    else if(move==8){ ///die
        Dieviewframe[limb]= Game.GetViewFrame(Skin_stuffed[limb]+8, currentloop, currentframe); 
        }
[close]

If you only run a single command, you can skip the curly braces. They are used to group commands together and not needed here.
You can also create the view frame first:
Code: ags
    ViewFrame* vf = Game.GetViewFrame(Skin_stuffed[limb] + move, currentloop, currentframe);
    if (move == 0) Runviewframe[limb] = vf; // run
    else if (move == 1) Idleviewframe[limb] = vf; // idle
    else if (move == 2) Atkviewframe[limb] = vf; // attack

lafouine88

So, I confirm using characters seems much better than Dynamic Sprites in this case. Equipment changes are instant, and the CPU load is transparent.
Plus, you don t even need to bother with Sprites priority(meaning for instance that left arm is above the right one on left views, but below on right ones).
I just set :
Clegs.baseline=player.y;
Ctorso.baseline=player.y+1;
CLarm.baseline=player.y+2
CRarm.baseline=player.y+2;
Cshoulder.baseline=player.y+3;
Chead.baseline=player.y+4;
And it s all good👍👍

Thanks guys😁

Crimson Wizard

It's wonderful, glad to hear that it worked.

Right, I mentioned that you can only set priority once, but somehow I forgot that character's own baseline is dynamic, so that would not work here. If AGS supported relative zorder in parent->child relation, then things would be simpler, but oh well...

SMF spam blocked by CleanTalk