Ren'Py DynamicDisplayable with Dissolve

Night Mirror

Well-Known Member
Modder
Jun 2, 2018
1,716
9,218
I've been working on this for a few days now and feel like I'm going around in circles. Some outside perspective would be really helpful.

Here is the quick gist of the issue. I'd like to implement blinks all through the project I'm working on. This has turned out to be very straight forward using ATL:
Python:
image morty_main_blink_open:
    "chars/morty/main/blank.png"
    choice:
        5
    choice:
        4
    choice:
        3
    choice:
        2
    "chars/morty/main/l/1.png" with Dissolve(0.1)
    0.1
    "chars/morty/main/l/0.png" with Dissolve(0.1)
    0.1
    "chars/morty/main/l/1.png" with Dissolve(0.1)
    0.1

    repeat
The problem is, this code isn't very compact, is hugely repetitive (do not repeat yourself), and feels like I should be able to create a factory to generate it. Additionally, since every character model (using layeredimage) and every full screen image (mostly using layeredimage as well) needs this code it is littered everywhere, and it just feels... bad.

After much googling, I found a helpful post on this site that pointed me towards DynamicDisplayable as a possible option, and damn did it get me close. Now the blink code looks more like this:
Code:
image morty_main_blink_open = DynamicBlink("chars/morty/main/blank.png", ["1.png", "0.png"], ["chars", "morty", "main", "l"])
Much more compact and when I get to scene work, it is WAY easier to implement for each image.

Just, one, tiny, little, hardly worth mentioning but absolutely a pretty big issue, thing. The images don't dissolve, they just hard switch. And when dealing with blinks, this looks awful, taking away any immersion the blinks otherwise added.

Here is the sub class I'm working with (based on the DynamicBlink code in this post)
Python:
    class DynamicBlink(renpy.display.layout.DynamicDisplayable):
        def __init__(self, first_image, images, dir_list, **kwargs):
            self.closing = True
            self.used_images = [first_image]
            self.dir_setup(images, dir_list)
            self.current_image_index = 0
            self.image_count = len(images) - 1
            self.current_image = images[0]
            self.blink_timer = -1.0

            kwargs.update( { '_predict_function' : self.predict_images } )

            super(DynamicBlink, self).__init__( self.get_current_blink_image )

        def get_current_blink_image(self, timer, at):
            if timer < 1.0 and self.blink_timer > 6.0:
                self.get_new_random_time(timer)
            if self.closing == True:
                if timer > self.blink_timer:
                    self.current_image_index += 1
                    self.current_image = self.used_images[self.current_image_index]
                    self.blink_timer = timer + 0.1
                    if self.current_image_index == self.image_count:
                        self.closing = False
                        self.blink_timer = timer + 0.1
            else:
                if timer > self.blink_timer:
                    self.current_image_index -= 1
                    self.current_image = self.used_images[self.current_image_index]
                    self.blink_timer = timer + 0.1
                    if self.current_image_index == 0:
                        self.closing = True
                        self.get_new_random_time(timer)

            return self.current_image, 0

        def dir_setup(self, images, dir_list):
            for i in images:
                img = ""
                for n in dir_list:
                    img += n + "/"
                img += i
                self.used_images.append(img)

        def get_new_random_time(self, timer):
            self.blink_timer = timer + 2.0 + ( renpy.python.rng.random() * 3.0 )

        def predict_images(self):
            return self.used_images
And the code does work, it just doesn't dissolve. I tried returning a Dissolve object at the end of get_current_blink_image, but that doesn't work (throwing an error that Dissolve is not a Displayable, even though it looks like it should be?)

After spending (I have no idea how many hours) a lot of time looking at the renpy engine classes for DynamicDisplayable and Dissolve, I just can't figure out how to get them to work with each other. I think I might ultimately be going about this from the wrong end. Maybe I just need to write a whole new implementation of something like DynamicDissolveable or something... Or, I'm looking at the wrong class entirely

Anyhow, thanks for any input anyone can share! Worst case I just stick with ATL, which works, but is a bit ugly (and no one but me will ever care).
 

Night Mirror

Well-Known Member
Modder
Jun 2, 2018
1,716
9,218
Alright, well, I found a solution! Short answer is that DynamicDisplayable is absolutely the wrong way to go for this. I couldn't find any posts talking about TransitionAnimation (seriously, none, thanks google, you failed me) on this or any other site, but it seems to be what I really needed. So, here is the solution I came up with:
Python:
    class DynamicBlink(renpy.display.anim.TransitionAnimation):
        def __init__(self, first_image, images, dir_list):

            ## Setup default image for the animation [Image, Delay, Transition]
            self.used_images = [first_image, self.get_random_delay(), Dissolve(0.1)]

            ## Append a list of animations going forward
            self.dir_setup(images, dir_list)

            ## Append a list of animations going in reverse
            self.dir_setup(list(reversed(images[:-1])), dir_list)

            ## Set a default timer
            self.time_step = -1.0

            ## Call Super
            super(DynamicBlink, self).__init__( *self.used_images )

        def dir_setup(self, images, dir_list):
            for i in images:
                img = ""
                for n in dir_list:
                    img += n + "/"
                img += i

                ## Image
                self.used_images.append(img)

                ## Delay
                self.used_images.append(0.1)

                ## Transition
                self.used_images.append(Dissolve(0.1))

        def get_random_delay(self):
            ## Get random delay between blinks
            return 3.0 + ( renpy.python.rng.random() * 3.0 )


        def render(self, width, height, st, at):
            orig_t = at if self.anim_timebase else st
            t = orig_t % sum(self.delays)

            ## Check if timer has reset
            if t < self.time_step:

                ## Get new Delay for first animation slot.
                self.delays[0] = self.get_random_delay()

                ## Force Reset for New Starting Delay to prevent Double Blinks
                st = at = 0.0
            self.time_step = t

            ## Call Super
            return super(DynamicBlink, self).render( width, height, st, at )
While I'm sure I'll make it a bit cleaner as I go forward (or more likely never touch it again), this appears to be working as expected. If you decide to use this in your game, it should be pretty easy to modify for your needs (as most of it is commented). But I'll go over the function call a bit.

Here is how to make new "blink" images for it:
Python:
image s0_1_0_m_f = DynamicBlink("bgs/blank.png", ["mbh.png", "mbc.png"], ["scenes", "s0", "1", "0"])
The function call takes 3 arguments. The first image (with a full path to that image), a list of images to animate (as this is a blink, you only need to list the images once, then it will reverse them to play the images back to the start), and finally a list of the path to the images. This last part is specifically useful for me to be broken up into each folder, but writing the whole thing will work just as well ["scenes/s0/1/0"] (be careful not to add that last slash if you do it this way!)

And, that's it! I hope this helps someone else with this issue. If you have any questions just ask here and I'll do my best to elaborate.
 
Last edited: