Well, damn, looks like I just run into one of those obscure AGS bugs. Took me several hours to, first, realize that could not be a mistake in my script, and then find out why is it happening exactly, staring into the engine code.
Following example is based on default game template.
Room 1 script:
// room script file
DynamicSprite *Sprite;
function room_Load()
{
Sprite = DynamicSprite.CreateFromExistingSprite(2000);
ViewFrame *f = Game.GetViewFrame(player.View, 0, 0);
f.Graphic = Sprite.Graphic;
}
function on_key_press(eKeyCode key)
{
// These lines cause the bug ======
//if (Sprite != null)
//Sprite.Delete();
// ================================
DynamicSprite *new_sprite = DynamicSprite.CreateFromExistingSprite(2000 + Random(9));
Sprite = new_sprite;
ViewFrame *f = Game.GetViewFrame(player.View, 0, 0);
f.Graphic = Sprite.Graphic;
}
So, this code is very simple. What it does:
- It takes certain View frame, and replaces original sprite with dynamic sprite, which in turn is a copy of some character sprite.
- On every key press it creates NEW dynamic sprite from the random range of character sprites, and puts it to same View frame.
Don't bother wondering why would I need such script, that's just an example of certain actions.
Run the game and hold any key (e.g. space bar). Character's image will beging to cycle randomly.
Now, the interesting part. See these commented lines where the previous sprite gets explicitly DELETED before assigning new sprite to the same pointer? Thinking logically, that should be right thing to do, and should be safe to assume that nothing bad is going to happen?
Well, uncomment these lines:
if (Sprite != null)
Sprite.Delete();
and run the game again. Hold space bar, and... you see that character's looks do not change anymore.
HOWEVER, if you walk a little and then make Roger take original direction, you will see, that the frame 0 sprite actually is different. So it DOES change after all, it's just that AGS did not redraw it in time.
So, why is this happening, and how can DynamicSprite.Delete make any difference?AGS has character cache, where it stores last character's image with additional effects (tint, lighting, area zoom) applied. On every update it checkes whether anything has changed and if not - it keeps cached sprite.
When you create NEW dynamic sprite, and assign it to the view frame, the number of sprite is now
different from old one, so AGS knows that it needs to reset cache.
But when you delete an old sprite FIRST, its SLOT number frees, and the next dynamic sprite gets created on the SAME SLOT, having same index as the old one has had! And AGS cannot detect that the change happened!
To prove this point, change the code:
function on_key_press(eKeyCode key)
{
// Temporarily keep an old sprite in another pointer
DynamicSprite *old_sprite = Sprite;
DynamicSprite *new_sprite = DynamicSprite.CreateFromExistingSprite(2000 + Random(9));
Sprite = new_sprite;
ViewFrame *f = Game.GetViewFrame(player.View, player.Loop, player.Frame);
f.Graphic = Sprite.Graphic;
// Now delete the old sprite by referencing the temp pointer
if (old_sprite != null)
old_sprite.Delete();
}
If you run this code and hold space bar, the character will be animating again.
Now, the above is just a dummy script example. If you have a more complicated script, with multitude of dynamic sprites, where you cannot reliably keep track of when the old slots are freed (or rather do not want to do that), for that case I found another workaround to force AGS reset character cache:
DrawingSurface *ds = Sprite.GetDrawingSurface();
ds.DrawPixel(-1, -1);
ds.Release();
Thing is that when DrawinSurface.Release is called, it checks all (supposedly) places where this sprite could be used, and releases all related caches. But some drawing operations MUST be complete to make it do so! Hence we just draw one pixel outside of the sprite. While sprite physically stays unchanged, DrawingSurface object registers modification.
Now this will work too:
function on_key_press(eKeyCode key)
{
if (Sprite != null)
Sprite.Delete();
DynamicSprite *new_sprite = DynamicSprite.CreateFromExistingSprite(2000 + Random(9));
Sprite = new_sprite;
ViewFrame *f = Game.GetViewFrame(player.View, player.Loop, player.Frame);
f.Graphic = Sprite.Graphic;
// Poke AGS to make it reset sprite cache
DrawingSurface *ds = Sprite.GetDrawingSurface();
ds.DrawPixel(-1, -1);
ds.Release();
}
PS.The actual reason for this bug is that when DynamicSprite gets deleted, it does not clears related CHARACTER caches. Which is very strange, because it clears GUIs and Room object caches. This makes me think that there could be a simple oversight.
PPS.
Also, that bothers me, but I have a suspicion that if you don't call DynamicSprite.Delete, but simply overwrite pointer with new sprite, the actual bitmap does not get deleted and stays in system memory (aka "memory leak"). This is explicitly coded so in AGS engine, and I cannot understand why. This might require a separate research.