[Solved] Dynamic character portraits and animations.

Discuss how to use the Ren'Py engine to create visual novels and story-based games. New releases are announced in this section.
Forum rules
This is the right place for Ren'Py help. Please ask one question per thread, use a descriptive subject like 'NotFound error in option.rpy' , and include all the relevant information - especially any relevant code and traceback messages. Use the code tag to format scripts.
Post Reply
Message
Author
User avatar
Tayruu
Regular
Posts: 141
Joined: Sat Jul 05, 2014 7:57 pm

[Solved] Dynamic character portraits and animations.

#1 Post by Tayruu »

I'm using a blink/lip-flap setup for the character portraits in my project. Given the amount of code involved just for a single portrait, I would like to make it that everything is based on one definition. It seemed like a dynamic displayable was the way to go, but it later seemed a creator-defined displayable worked better.

So uh, here's an example of a portrait that's not dynamic:

Code: Select all

    image akane casual idle = Flatten(LiveComposite(
        (384, 640),
        (0, 0), im.Crop("graphics/char/akane/casual/idle.png", (0,0,384,640)),
        (128, 144), "aci eyes",
        (128, 208), WhileSpeaking("akane", "aci mouth")
        ))

    image aci eyes:
        animation
        Null()
        choice:
            pause 3.8
        choice:
            pause 2.8
        choice:
            pause 1.5
        choice:
            pause 0.8
        im.Crop("graphics/char/akane/casual/idle.png", (384, 0, 128, 64))
        pause 0.1
        im.Crop("graphics/char/akane/casual/idle.png", (384, 64, 128, 64))
        pause 0.1
        im.Crop("graphics/char/akane/casual/idle.png", (384, 0, 128, 64))
        pause 0.1
        repeat
            
    image aci mouth:
        Null()
        pause 0.13
        im.Crop("graphics/char/akane/casual/idle.png", (384, 160, 128, 64))
        pause 0.13
        im.Crop("graphics/char/akane/casual/idle.png", (384, 224, 128, 64))
        pause 0.13
        repeat  
This has a lot of code I think I could reduce to single variables, hence I looked to a dynamic displayable.

It went well, until I wanted to work with animations - the eyes and the mouth. I don't know the equivalents for the functions used in the latter two blocks, for use in python. The dynamic system I have currently looks like this, based on some of the code from the documentation:

Code: Select all

    class Portrait(renpy.Displayable):
        def __init__(self, image, width, eyepos, moupos, **kwargs):
            # Pass additional properties on to the constructor.
            super(Portrait, self).__init__(**kwargs)
            # Values
            self.image = ("graphics/char/%s" % image)
            self.width = width
            self.eyepos = eyepos
            self.moupos = moupos

        def render(self, width, height, st, at):
            # Display the portrait
            portrait = Flatten(LiveComposite(
            (self.width, 640),
            (0, 0), im.Crop(self.image, (0, 0, self.width,640)),
            (self.eyepos[0], self.eyepos[1]), self.eyes(self.image, self.width),
            ))
            
            # Process the portrait to render it
            child_render = renpy.render(portrait, self.width, 640, st, at)
            return child_render
                       
        def eyes(self, image, x):
            return im.Crop(image, (x, 64, 128, 64))
Right now, the eyes definition only returns a single frame because I can't just put the frames and pauses in as they are, as well as other lines.

The code can be called with an image like image akane_test = Portrait("akane/casual/idle.png", width=384, eyepos=(128, 144), moupos=(128,208)). Portrait defines the image, width of the portrait, and the x/y of the eyes and mouth elements.

The image in question looks like this. I use im.Crop to slice the image up as necessary.

So my question remains; how do I perform what I do with the first lot of code in a dynamic system, so that I simply have to refer to that Portrait() for all character art? What is the equivalent of animation, choice, pause, repeat, and how do I get it to work just like the non-dynamic code? (It seems like it works so far though, sans the animation.)
Attachments
script.rpy
Demonstration.
(6.94 KiB) Downloaded 159 times
Last edited by Tayruu on Tue Feb 03, 2015 6:38 pm, edited 1 time in total.

User avatar
bvtsang
Newbie
Posts: 1
Joined: Sun Oct 19, 2014 4:16 am
Contact:

Re: Dynamic character portraits and animations.

#2 Post by bvtsang »

I was personally interested in the answer for this, so I decided to give this a shot.

Code: Select all

init python:
    class Portrait(renpy.Displayable):
        def __init__(self, image, width, eyepos, moupos, **kwargs):
            super(Portrait, self).__init__(**kwargs)
            self.image = ("graphics/char/%s" % image)
            self.portrait_width = width
            self.eyepos = eyepos
            self.moupos = moupos
            self.eye_width = 128
            self.eye_height = 64
            self.mouth_width = 128
            self.mouth_height = 64

        def render(self, width, height, st, at):
            # This is the base portrait displayable
            portrait = im.Crop(self.image, (0, 0, self.portrait_width, 640))

            # This is the render of the base portrait displayable
            # Remember, this render() method must return a renpy.Render object
            # For now, you can think of this as a layer
            portrait_render = renpy.render(portrait, self.portrait_width, 640, st, at)

            # Now we're going to render the eyes
            # The reason why we're using a DynamicDisplayable here is that it forces us to rerender the eyes later
            # Specifically, we call redraw_eyes() every interval (0.1 second in this case)
            eye_render = renpy.render(DynamicDisplayable(self.redraw_eyes), self.eye_width, self.eye_height, st, at)
            # After rendering the eyes, we draw (blit) it onto the portrait render
            # This is like placing a layer that contains eyes on top of the bottom (portrait) layer
            portrait_render.blit(eye_render, (self.eyepos[0], self.eyepos[1]))

            # This is a render of the mouth, and it follows the same pattern as the eyes
            mouth_render = renpy.render(DynamicDisplayable(self.redraw_mouth), self.eye_width, self.eye_height, st, at)
            portrait_render.blit(mouth_render, (self.moupos[0], self.moupos[1]))

            # Finally show the render of the base portrait with the eyes and mouth drawn on top
            return portrait_render

        # This is the function we pass into a DynamicDisplayable
        # According to the documentation, this function should return a (d, redraw) tuple
        #   - d is the displayble to show
        #   - redraw is the amount of time to wait before calling the function again,
        #     or None to not call the function again before the start of the next interaction
        # http://www.renpy.org/doc/html/displayables.html#DynamicDisplayable
        def redraw_eyes(self, st, at):
            # I'm sure there's a way to make these displayable definitions defined only once, but I'm too lazy
            opened = im.Crop(self.image, (self.eyepos[0], self.eyepos[1], self.eye_width, self.eye_height))
            half_opened = im.Crop(self.image, (self.portrait_width, 0, self.eye_width, self.eye_height))
            closed = im.Crop(self.image, (self.portrait_width, 64, self.eye_width, self.eye_height))

            # The main way we can tell how time passes is through the st argument,
            # which is the amount of time the displayable has been shown for
            #
            # Because of this, I still don't know how to implement the equivalents of
            # choice, pause, and repeat
            #
            # So I've done my best below
            # Perhaps someone more knowledgable than me can do better
            time = st % 6
            if time < 2:
                return opened, 0.1
            elif time < 4:
                return half_opened, 0.1
            else:
                return closed, 0.1

        def redraw_mouth(self, st, at):
            closed = im.Crop(self.image, (self.moupos[0], self.moupos[1], self.mouth_width, self.mouth_height))
            half_opened = im.Crop(self.image, (self.portrait_width, 160, self.mouth_width, self.mouth_height))
            opened = im.Crop(self.image, (self.portrait_width, 224, self.mouth_width, self.mouth_height))

            time = st % 9
            if time < 3:
                return closed, 0.1
            elif time < 6:
                return half_opened, 0.1
            else:
                return opened, 0.1

init:
    image akane_d idle = Portrait("akane/casual/idle.png", width=384, eyepos=(128, 144), moupos=(128,208))

label start:
    show akane_d idle at center
    "I am a dynamic character."
    return
I still don't have a clean implementation of choice, pause, and repeat, but I've done my best.
Attachments
script.rpy
(4.21 KiB) Downloaded 157 times

User avatar
Tayruu
Regular
Posts: 141
Joined: Sat Jul 05, 2014 7:57 pm

Re: Dynamic character portraits and animations.

#3 Post by Tayruu »

Woah, this looks impressive. It seems like it works too. It takes a portrait and generates as expected with two different poses.

There's just getting it to animate as expected and to speak as expected. I actually forgot one more variable with regards to the non-dynamic portrait - the "speaker". The function WhileSpeaking as per the lip-flap script is used to check if a portrait should animate its mouth.

Since WhileSpeaking deals with a dynamic displayable too, I'm sure it could be altered to fit, but as far as I can see something can simply be done with the "speaking" variable.
... maybe. I'm going to perhaps just toy around to see what happens/break it a few times. As far as displaying the portraits goes, it works.

You say you don't know how to implement choice, pause, and repeat in the comments, but as far as I can tell that's not a worry. Choice as I was using it set one of a few random pause times between blinks, so it could be created a different way. You're already controlling the time and pace already, as well as repetition.

EDIT: Dynamic mouth was easy. Added a "speaker" argument in and referred to it here. (Plus the following timing.) I could probably clear away some unused code from the original lip-flap...

Code: Select all

            # This is a render of the mouth, and it follows the same pattern as the eyes
            global speaking
            if speaking == self.speaker:
                mouth_render = renpy.render(DynamicDisplayable(self.redraw_mouth), self.eye_width, self.eye_height, st, at)
                portrait_render.blit(mouth_render, (self.moupos[0], self.moupos[1]))

Code: Select all

            p = .4
            time = st % p
            if time < .1:
                return half_opened, 0.1
            elif time < .2:
                return opened, 0.1
            elif time < .3:
                return half_opened, 0.1
            else:
                return closed, 0.1   
EDIT: Hmm, seems that now I have to wrap the image in Flatten, since it's not in the class. I wonder if this works okay. Code looks a mess, but the visual weirdness has stopped.

Code: Select all

            # Return the render.
            flatten_portrait = renpy.display.render.Render(self.portrait_width, 640)
            flatten_portrait.blit(portrait_render.render_to_texture(True), (0, 0))
            flatten_portrait.depends_on(portrait_render, focus=True)
            return flatten_portrait
########################################

EDIT: http://lemmasoft.renai.us/forums/viewto ... 51&t=30279

Post Reply

Who is online

Users browsing this forum: Ocelot