Ren'Py Variable for image names in animation

recreation

pure evil!
Respected User
Game Developer
Jun 10, 2018
6,327
22,777
I'm going to freak out soon, I can't think anymore, I'm sitting on this for hours now...

What I'm trying to do is to use a placeholder for the image names in ATL animations. I hacked something together, but it just refuses to work. No error whatsoever, the animation just refuses to play.
Code:
init python:
    def some_anim(aname):
        anumber = []
        anumber.append(aname % +1)
        return anumber

image gaknee:
    DynamicImage(some_anim("anim/g_stand%02d.png"))
    pause 0.10
    repeat 60
    
label start:
    show gaknee with hpunch
 

Epadder

Programmer
Game Developer
Oct 25, 2016
568
1,064
I tested your code and tried to customize it so I could get something out of it :unsure::
Python:
default anumber = []

init python:
    def some_anim(aname):
        if anumber is None:
            store.anumber.append(aname % +1)
        else:
            store.anumber.append(aname % (len(anumber) + 1))
        return anumber[-1]

image gaknee:
    DynamicImage(some_anim("anim %02d.webp"))
    pause 0.10
    repeat 12
    
label start:
    show gaknee
    pause
    "Trying to show an image."
I defined the list outside of the function, because the way I read it you would reset the list every-time when the function was run. I also changed the return to be the last item in the list instead of the whole list, DynamicImage only excepts a string anyway. :whistle:

I did get something to display, but the gaknee image doesn't recalculate the DynamicImage every-time it repeats... I only got the first frame of my test animation to show up.

So... this won't work as is, instead of DynamicImage you need to write a I haven't really explored making one though so I can't give an example. :confused:
 
  • Like
Reactions: recreation

xht_002

Member
Sep 25, 2018
342
353
just use or similar to convert a image sequence into a AVI movie then convert it to webm
 

recreation

pure evil!
Respected User
Game Developer
Jun 10, 2018
6,327
22,777
just use or similar to convert a image sequence into a AVI movie then convert it to webm
I usually give this advise to other people :LOL:
But no, in this specific case I need transparency. I tried QuickTime mov which works with transparency, but the filesize jumped to triple the size of what all images combined together have :/
 

recreation

pure evil!
Respected User
Game Developer
Jun 10, 2018
6,327
22,777
I defined the list outside of the function, because the way I read it you would reset the list every-time when the function was run
Good idea, I didn't see this.
I also changed the return to be the last item in the list instead of the whole list, DynamicImage only excepts a string anyway.
As far as I know this shouldn't be nessecary, I'll attach a working example at the bottom of this post. I didn't know that DynamicImage expects a string, so I made various attempts to convert it, but had no luck with the result either.

So... this won't work as is, instead of DynamicImage you need to write a I haven't really explored making one though so I can't give an example. :confused:
I try to avoid it in this case because DynamicDisplayable doesn't work well when its changed frequently and I need to change it almost all the time :/

Working example:
Code:
init python:
    def gen_anim_loop(fname, frange, delay):
        lst = []
        for i in frange:
            lst.append(fname % i)
            lst.append(delay)
        return lst

    renpy.image("ganime", Animation(*gen_anim_loop("anim/g_stand%02d.png", range(0, 59), 1/60.)))
    
label start:
    show ganime
This snipet is what my code is based on. As you can see, this works a bit different, but it works. Sadly it loops the animation indefinetly and I would need to define the function for every new animation, that's why I don't want to use it as is.
My knowledge of python is actually almost not existant, so I have to rely on what I know from other mostly web-based languages, which doesn't make it easier :(
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,236
I'm going to freak out soon, I can't think anymore, I'm sitting on this for hours now...
First thing, when you come to this just stop. Don't think of your problem anymore and do something completely different (read a book, play a game, clean the house, whatever) for some time.
The more you struggle on your problem, the less you'll find the solution.


I did get something to display, but the gaknee image doesn't recalculate the DynamicImage every-time it repeats... I only got the first frame of my test animation to show up.
:
A DynamicImage is a displayable that has text interpolation performed on it to yield a string giving a new displayable. Such interpolation is performed at the start of each interaction.

So... this won't work as is, instead of DynamicImage you need to write a I haven't really explored making one though so I can't give an example. :confused:
It can works without problem, but it's going too far for something this simple. One of the way to do it with DynamicDisplayable is to rely on a class ; something looking like this :
Python:
init python:

    class Some_anim( renpy.Python.RevertableObject ):
        def __init__( self, base, interval, nbFrames ):
            self.__base = base
            self.__interval = interval
            self.__nbFrames = nbFrames

        #   /st/ is the number of seconds (float) since the first time
        # this displayable started to be shown.
        #  /at/ is the number of seconds (float) since the first time
        # a displayable with the same tag started to be shown.
        def __call__( self, st, at ):
            #  The frame number is the displayed time modulo the
            # interval. /int/ is probably useless here, but we works
            # with float, so I'll be paranoid.
            frame = int( st % self.__interval )
            #  Then we do a modulo to represent the loop.
            frame %= self.__nbFrames
            #  Finally we return the right image, and ask to be called
            # for the next interval.
            return ( Image( self.__base.format( frame ) ), self.__interval )

image gaknee = DynamicDisplayable(Some_anim( "anim/g_stand{}.png", 0.10, 60 ) )
Or, relying on the fact that Ren'py will call the class with the right interval :
Python:
init python:

    class Some_anim( renpy.Python.RevertableObject ):
        def __init__( self, base, interval, nbFrames ):
            self.__base = base
            self.__interval = interval
            self.__nbFrames = nbFrames
            self.__frame = 0

        def __call__( self, st, at ):
            #  We are displayed since less that the interval, so
            # it's a new animation, restart from the first frame.
            if st < self.__interval:
                self.__frame = 0
            #  Loop if we reached the last frame.
            if self.__frame > self.__nbFrames :
                self.__frame = 0
            #  You need to increase the frame number after the
            # computation of the name, so keep the return
            # value...
            retVal = ( Image( self.__base.format( self.__frame ) ), self.__interval )
            #  increase the frame number...
            self.__frame += 1
            #  then finally return what was computed.
            return retVal

image gaknee = DynamicDisplayable(Some_anim( "anim/g_stand{}.png", 0.10, 60 ) )
It's possible to pass parameters with a DynamicDisplayable, but I never tried it, so I'm not sure if they are free or limited to properties, nor how they effectively are passed.

But here, the solution just need a change of point of view. Instead of a displayable, it's a screen that should be used :
Code:
screen some_anim( base, interval, nbFrames ):

    default frame = 0
    timer interval repeat True action If( frame >= nbFrames, SetScreenVariable( "frame", 0 ), SetScreenVariable( "frame", frame + 1 ) )
    add base

label start:

    show screen some_anim( "anim/g_stand[frame].png", 0.10, 60 )
    "Beautiful, isn't it ?"
    "Well, at least the animation I used as validation is beautiful."
    return
It's way simpler this way and really 100% on pure Ren'py.
 

recreation

pure evil!
Respected User
Game Developer
Jun 10, 2018
6,327
22,777
First thing, when you come to this just stop. Don't think of your problem anymore and do something completely different (read a book, play a game, clean the house, whatever) for some time.
The more you struggle on your problem, the less you'll find the solution.
I know, that's what I did :)

First thing, when you come to this just stop. Don't think of your problem anymore and do something completely different (read a book, play a game, clean the house, whatever) for some time.
The more you struggle on your problem, the less you'll find the solution.





:





It can works without problem, but it's going too far for something this simple. One of the way to do it with DynamicDisplayable is to rely on a class ; something looking like this :
Python:
init python:

    class Some_anim( renpy.Python.RevertableObject ):
        def __init__( self, base, interval, nbFrames ):
            self.__base = base
            self.__interval = interval
            self.__nbFrames = nbFrames

        #   /st/ is the number of seconds (float) since the first time
        # this displayable started to be shown.
        #  /at/ is the number of seconds (float) since the first time
        # a displayable with the same tag started to be shown.
        def __call__( self, st, at ):
            #  The frame number is the displayed time modulo the
            # interval. /int/ is probably useless here, but we works
            # with float, so I'll be paranoid.
            frame = int( st % self.__interval )
            #  Then we do a modulo to represent the loop.
            frame %= self.__nbFrames
            #  Finally we return the right image, and ask to be called
            # for the next interval.
            return ( Image( self.__base.format( frame ) ), self.__interval )

image gaknee = DynamicDisplayable(Some_anim( "anim/g_stand{}.png", 0.10, 60 ) )
Or, relying on the fact that Ren'py will call the class with the right interval :
Python:
init python:

    class Some_anim( renpy.Python.RevertableObject ):
        def __init__( self, base, interval, nbFrames ):
            self.__base = base
            self.__interval = interval
            self.__nbFrames = nbFrames
            self.__frame = 0

        def __call__( self, st, at ):
            #  We are displayed since less that the interval, so
            # it's a new animation, restart from the first frame.
            if st < self.__interval:
                self.__frame = 0
            #  Loop if we reached the last frame.
            if self.__frame > self.__nbFrames :
                self.__frame = 0
            #  You need to increase the frame number after the
            # computation of the name, so keep the return
            # value...
            retVal = ( Image( self.__base.format( self.__frame ) ), self.__interval )
            #  increase the frame number...
            self.__frame += 1
            #  then finally return what was computed.
            return retVal

image gaknee = DynamicDisplayable(Some_anim( "anim/g_stand{}.png", 0.10, 60 ) )
It's possible to pass parameters with a DynamicDisplayable, but I never tried it, so I'm not sure if they are free or limited to properties, nor how they effectively are passed.

But here, the solution just need a change of point of view. Instead of a displayable, it's a screen that should be used :
Code:
screen some_anim( base, interval, nbFrames ):

    default frame = 0
    timer interval repeat True action If( frame >= nbFrames, SetScreenVariable( "frame", 0 ), SetScreenVariable( "frame", frame + 1 ) )
    add base

label start:

    show screen some_anim( "anim/g_stand[frame].png", 0.10, 60 )
    "Beautiful, isn't it ?"
    "Well, at least the animation I used as validation is beautiful."
    return
It's way simpler this way and really 100% on pure Ren'py.
The last example works, but it's really slow and it loops indefinetly. Please see my post above yours.
Thx for you help.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,236
The last example works, but it's really slow and it loops indefinetly. Please see my post above yours.
Thx for you help.
Er, I fail to see how it can be slow :/ The timer screen statement can go down to 0.01 seconds without problems.
Anyway, making it not loop is really easy. Just change the part of the code that make it loop to hide the screen :
Code:
screen some_anim( base, interval, nbFrames ):

    default frame = 0
    timer interval repeat True action If( frame >= nbFrames, Hide( "some_anim" ), SetScreenVariable( "frame", frame + 1 ) )
    add base
 
  • Like
Reactions: recreation

recreation

pure evil!
Respected User
Game Developer
Jun 10, 2018
6,327
22,777
Er, I fail to see how it can be slow :/ The timer screen statement can go down to 0.01 seconds without problems.
Well there are 60 frames per second at 1080p to be displayed. It takes about 3 seconds for one loop with your example.
Anyway, making it not loop is really easy. Just change the part of the code that make it loop to hide the screen :
Code:
screen some_anim( base, interval, nbFrames ):

    default frame = 0
    timer interval repeat True action If( frame >= nbFrames, Hide( "some_anim" ), SetScreenVariable( "frame", frame + 1 ) )
    add base
Ah I see, I didn't expect it to be so easy. There is still so much to learn :cry:

Hmm... could you think of a way to stop the loop in my last example? It displays the animation as it should and seems to be faster, but I don't know how to get rid of the loop here.
This one:
Code:
init python:
    def gen_anim_loop(fname, frange, delay):
        lst = []
        for i in frange:
            lst.append(fname % i)
            lst.append(delay)
        return lst

    renpy.image("ganime", Animation(*gen_anim_loop("anim/g_stand%02d.png", range(0, 59), 1/60.)))
    
label start:
    show ganime
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,236
Well there are 60 frames per second
Er, no.
In your OP you defined a 0.10 second pause, that I used in my own example. And with a 0.10 second pause/interval, it's a 10 frames/second animation. If you want it to be 60 frames/second, just change the interval :
Code:
    show screen some_anim( "anim/g_stand[frame].png", 1/60, 60 )
I thought that this line was obvious, since I explicitly named the parameters, but I'll explain it :
  1. The base for the image, with '[frame]' being put at the exact place where the number representing the frame is in the name ;
  2. The interval between two frames ;
  3. The number of frames in the animation.
So, change the value of whatever parameter you want, and you'll change the animation displayed.
It's even possible, with a small change, to make the animation loop or not, and make this loop finite or infinite.


Hmm... could you think of a way to stop the loop in my last example?
You can't. The outdated Animation displayable only take displayable/pause couples. It was removed from the documentation probably when the ATL appeared, since it offered more possibilities, among which you find the repeat property.
 
  • Like
Reactions: recreation

recreation

pure evil!
Respected User
Game Developer
Jun 10, 2018
6,327
22,777
Er, no.
In your OP you defined a 0.10 second pause, that I used in my own example. And with a 0.10 second pause/interval, it's a 10 frames/second animation. If you want it to be 60 frames/second, just change the interval :
Welp, now I'm getting confused. Of course 0.10 was wrong. I changed that already yesterday. But I used 0.01 seconds instead. 1/60 is ~0.016, so the animation should play faster than 60fps o_O

Edit: if I use 1/60 instead of 0.01 I get an error stating that "a timers delay must be > 0"
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,236
Welp, now I'm getting confused. Of course 0.10 was wrong. I changed that already yesterday.
But you didn't changed OP, and that's what I used as base.


Edit: if I use 1/60 instead of 0.01 I get an error stating that "a timers delay must be > 0"
Use float(1)/60 and it will works. The way numbers are handled don't always return a float when it should, sometimes you need to enforce that you'll have one.
If you need it often, you can use :
Code:
define 60fps = float(1)/60
[...]
    show screen some_anim( "anim/g_stand[frame].png", 60fps, 60 )
 
  • Like
Reactions: recreation

recreation

pure evil!
Respected User
Game Developer
Jun 10, 2018
6,327
22,777
Use float(1)/60 and it will works. The way numbers are handled don't always return a float when it should, sometimes you need to enforce that you'll have one.
Okay, this works, but again its too slow :(

Code:
screen some_anim( base, interval, nbFrames ):

    default frame = 0
    timer interval repeat True action If( frame >= nbFrames, Hide( "some_anim" ), SetScreenVariable( "frame", frame + 1 ) )
    add base
    
...


show screen some_anim( "anim/g_knee[frame].png", float(1)/60, 60 )
It takes about 3 seconds again until the animation is finished.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,236
It takes about 3 seconds again until the animation is finished.
Not during my own test (1280x720 sprites, ~250KB each, with 75% full transparent background). Perhaps that it take a little more than 1 second (difficult to time it at the perfection), but it definitively took less than 2 seconds in all the loops.
So, if for you it take more than 3 seconds, then it's that the size of your images are way too big for this kind of things, or that Ren'py is already doing too many things as background task ; which is probably not the cause unless you have many timed things. Another possibility is that the screen have too much elements with a defined hovered property (or the new tooltip one), which is the only thing that can effectively slow down Ren'py.
 
  • Like
Reactions: recreation

recreation

pure evil!
Respected User
Game Developer
Jun 10, 2018
6,327
22,777
Okay I made a test and used both codes:
Code:
init python:
    def gen_anim_(aname, arange, delay):
        ast = []
        for i in arange:
            ast.append(aname % i)
            ast.append(delay)
        return ast
    renpy.image("ganimTest", Animation(*gen_anim_("anim/g_fist%d.png", range(0, 59), 1/60.)))

screen some_anim( base, interval, nbFrames ):

    default frame = 0
    timer interval repeat True action If( frame >= nbFrames, Hide( "some_anim" ), SetScreenVariable( "frame", frame + 1 ) )
    add base


label start:
    "Let's fight!"
    $ gWhatAttack = renpy.random.randint(1, 2)
    if gWhatAttack == 1:
        show ganimTest
    elif gWhatAttack == 2:
        show screen some_anim( "anim/g_knee[frame].png", float(1)/60, 59 )
You can test it yourself, the first one runs smooth at 60fps.
The second one (yours) sadly is laggy and too slow :/

My sprites are about ~1mb in filesize (totally unoptimised)
The only other timer I have is for delaying one initial animation.
I have 3 buttons but with no hovered property and 2 very simple bars.
The script is small with just about 100 lines. It's just a prove of concept thingy with nothing special or fancy.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,236
The second one (yours) sadly is laggy and too slow :/

My sprites are about ~1mb in filesize (totally unoptimised)
Well, it probably have to do with Ren'py prediction in this case. I mean that what make the screen version not being smooth is mostly the size of the images you use ; four times the one that give me an effectively smooth animation.
Put at True to see how Ren'py handle it here. You can also use the profiling features :
  • F3 profile the performance of Ren'py ;
  • F4 normally do the same that the debug_image_cache above ;
  • F7 profile the memory usage.

You can also try to optimize Ren'py, playing with configuration variables like :
, , , or .

Finally you can also see what happen when you trick the prediction with a screen like this one :
Code:
screen load_all( base, nbFrames ):
    for frame in range( 0, nbFrames ):
        add base:
            xpos config.screen_width + 100
            ypos config.screen_height + 100
    timer 1.0 action Hide( "load_all" )
It simply display all the frames outside of the screen, which imply that it will predict all the images and normally load them all in the cache. So, if you do something like this :
Code:
label start:
    show screen load_all( "anim/g_stand[frame].png", 60 )
    "some dialog line"
    show screen some_anim( "anim/g_stand[frame].png", float(1)/60, 60 )
It should solve the problem.

On a side note, optimizing your images will only lower the loading time ; once put in the active cache, they'll be stored using there effective size, so 4 Bytes by pixel.

Now the question is "do you really need a 60fps" animation ?
Human eyes see more or less at 25fps, so the effective difference you'll do will be to smooth the afterglow effect. But the said afterglow happen with real sudden move and is almost imperceptible with none CRT screens. There's 75% chances that with the same animation at 30fps (so you forget one over two frame), no one will see the difference.
 
  • Like
Reactions: recreation

recreation

pure evil!
Respected User
Game Developer
Jun 10, 2018
6,327
22,777
if you do something like this :
Code:
label start:
    show screen load_all( "anim/g_stand[frame].png", 60 )
    "some dialog line"
    show screen some_anim( "anim/g_stand[frame].png", float(1)/60, 60 )
It should solve the problem.
Perfect! I didn't think about something like this.

Now the question is "do you really need a 60fps" animation ?
Of course not. I was exaggerating on purpose here. I wanted to see if Renpy can handle 60fps.

Thanks for all the informations you posted. This will definitely become handy!