[SOLVED] Picking random idle poses

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
zmook
Veteran
Posts: 421
Joined: Wed Aug 26, 2020 6:44 pm
Contact:

[SOLVED] Picking random idle poses

#1 Post by zmook » Sat Jul 02, 2022 7:53 pm

I have the following set of requirements, and I'm having trouble finding a clean solution:
  1. I have a sprite with several poses, some active (walking, kissing), and a set of three standing idle poses (idle1, idle2, idle3).
  2. The idle poses are layered images with attributes for facial expressions, so there are more attributes than just 'pose' to keep track of.
  3. I am using config.speaking_attribute for talking, so sprites are reshown frequently with "speaking" and "-speaking" attributes.
  4. When I specify a pose explicitly, like 'show eileen idle1 happy', I want that pose and expression to continue until I overwrite them, like normal. (When she talks, this will be reshown as 'show eileen idle1 happy speaking' and when someone else talks 'show eileen idle1 happy -speaking', of course.)
  5. When I do *not* specify a pose, I want one of the three idle poses picked randomly.
  6. I want this to be as close to transparent to the script writer as possible. That is, the least possible amount of extra cruft that has to be specified when showing characters, moving them around, and writing dialog. As much as possible, all the machinery for updating poses should be encapsulated in a characters.rpy file, away from the story.
I've been experimenting with config.adjust_attributes, but can't quite get it to deliver all my requirements. It has to be safe against prediction, so the select_pose function can't write any state information other than the list of attributes it returns.

I've been trying to figure out a system for keeping track via image attributes of which idle pose is currently displayed and whether it was randomized or explicitly set, but have not managed to make it work. Does anyone have a better plan?
Last edited by zmook on Tue Jul 05, 2022 2:15 pm, edited 1 time in total.
colin r
➔ if you're an artist and need a bit of help coding your game, feel free to send me a PM

philat
Eileen-Class Veteran
Posts: 1853
Joined: Wed Dec 04, 2013 12:33 pm
Contact:

Re: Picking random idle poses

#2 Post by philat » Sun Jul 03, 2022 11:01 am

Not super clear on what you're trying to do (when would the poses be picked? When would they be refreshed? etc.), but in general I would look into hooking the show statement with config.show.

User avatar
Kia
Eileen-Class Veteran
Posts: 1008
Joined: Fri Aug 01, 2014 7:49 am
Deviantart: KiaAzad
Discord: Kia#6810
Contact:

Re: Picking random idle poses

#3 Post by Kia » Tue Jul 05, 2022 11:01 am

I recently helped a friend toggle between censored and uncensored. It might not be the exact solution you're looking for but I had to mention it just in case.

Code: Select all

default censor = "dirty"


label start:

    $ censor  = "clean"
    show image "test [censor]"
    "This is the clean version."
    $ censor = "dirty"
    show image "test [censor]"
    "This is dirty."

    return

User avatar
zmook
Veteran
Posts: 421
Joined: Wed Aug 26, 2020 6:44 pm
Contact:

[SOLVED] Re: Picking random idle poses

#4 Post by zmook » Tue Jul 05, 2022 2:13 pm

I spent a long time trying to figure out how much of this I could get renpy to do for me. config.adjust_attributes looked really promising, and I had hoped for a design that looked something like this:

Code: Select all

init python:
    # register a callback to select on the fly which pose gets shown for a registered tag.
    # return value: a new name to use instead of the supplied name
    def select_pose(name):      
        # figure out what pose to show...
        return rv            

# called every time the image is reshown, including for updating the 'speaking' attribute
define config.adjust_attributes["eileen"] = select_pose

init 512:
    # use the optional attribute of layeredimage to keep each one simple
    layeredimage eileen idle1:
        group outfit auto:
            attribute moodiv default
        group face if_any "speaking" auto variant "speaking":
            attribute neutral default
        group face if_not "speaking" auto:
            attribute neutral null default 

    layeredimage eileen idle2:
        # like idle1

    layeredimage eileen idle3:
        # like idle1
But... it turns out there are a lot of unexpected corner cases where the adjust_attributes function does not receive the input I'd expect. And with the amount of noise caused by prediction calls submitting all kinds of alternate input, debugging it is just a big pain in the ass. So I ended up pushing most of the variant-pose selection into the layeredimage attribute_function.

And here we are:

Code: Select all

init 400 python:
    # programmatically generate some simple blinking animated images
    # iterate image definitions equivalent to;
    #   image eileen_idle_blink_idle1_neutral = blinker("eileen_idle1_closedeyes_neutral")
    # by the docs, the standard image statement runs at priority 500, and we want to 
    # get these done before the layeredimage statement needs them (I think).
    
    blinks = {"idle1": "neutral happy concern".split(),
                "idle2": "neutral happy concern sad mischievous laughing".split(),
                "idle3": "neutral happy concern sad mischievous laughing".split(),
                }
    for pose in blinks.keys():
        for mood in  blinks[pose]:
            renpy.image("eileen_idle_blink_%s_%s" % (pose, mood),
                blinker("eileen_%s_closedeyes_%s" % (pose, mood)))


    def idle_selector_eileen(attrs):
#         renpy.log("idle_selector_eileen: received attrs=%r" % attrs)
        tag = 'eileen'
        pose = attrs & {'idle1','idle2','idle3'}
        if not pose:
            pose = g_idle_pose_current.get(tag, "idle1")
            attrs.add(pose)
#         renpy.log("idle_selector_eileen: returning attrs=%r" % attrs)
        return attrs
        
    
    # this function is used to locate images *at init time*. Images must be enumerated with
    # explicit attributes in the layeredimage definition; format_function is incompatible 
    # with using an 'auto' declaration
#     def idle_format_eileen(what, name, group, variant, attribute, image, image_format):
#         renpy.log("image_format: what=%r, name=%r, group=%r, variant=%r, attribute=%r, image=%r, image_format=%r"
#                     % (what, name, group, variant, attribute, image, image_format))
#         rv = layeredimage.format_function(what, name, group, variant, attribute, image, image_format)
#         renpy.log("image_format = %r" % rv)
#         return rv
    
    
    # randomly update e's default idle pose 
    def eileen_idler_set_pose():
        tag = "eileen"
        global g_idle_pose_current
        # on average we will only update eileen's idle pose once every few calls
        if renpy.random.random() > 0.5:
            return
        pose = weighted_choice(g_idle_poses[tag])
#         print("setting %s idle pose to %s" % (tag, pose))
        g_idle_pose_current[tag] = pose
    
    


# by default, 'image' statements are processed at priority 500, and (apparently)
# 'layeredimages' at priority 0. Let's make sure all the normal images are instantiated
# before we process the layeredimages
init 512:
    image eileen turningback speaking = "eileen turningback"


    layeredimage eileen idle:
#         format_function idle_format_eileen
        attribute_function idle_selector_eileen
    
        # base images *must* be named like 'eileen_pose_outfit_variant_style' for 
        # autoselection to work; e.g. 'eileen_idle_outfit_idle1_moodiv.webp'
        group outfit if_not ["idle2", "idle3"] auto variant "idle1":
            attribute moodiv default
        group outfit if_any "idle2" auto variant "idle2":
            attribute moodiv default
        group outfit if_any "idle3" auto variant "idle3":
            attribute moodiv default

        # face images *must* be named like 'eileen_idle_face_idle1_emotion' or 
        # 'eileen_idle_face_idle1_speaking_emotion';
        # e.g. 'eileen_idle_face_idle1_happy.png' and 'eileen_idle_face_idle1_speaking_happy.png'
        group face if_all ["idle1","speaking"] auto variant "idle1_speaking":
            attribute neutral default
        group face if_all "idle1" if_not "speaking" auto variant "idle1":
            attribute neutral null default # face_neutral is missing, but we don't need it
        group face if_all ["idle2","speaking"] auto variant "idle2_speaking":
            attribute neutral default
        group face if_all "idle2" if_not "speaking" auto variant "idle2":
            attribute neutral null default 
        group face if_all ["idle3","speaking"] auto variant "idle3_speaking":
            attribute neutral default
        group face if_all "idle3" if_not "speaking" auto variant "idle3":
            attribute neutral null default 
        
        attribute speaking null     # keep renpy from complaining about 'unknown attribute'
    
        # blink images are defined explicitly above, using the 'blinker' transform.
        # (ensure they are initialized before this statement is)
        group blink if_not ["idle2", "idle3"] auto variant "idle1":
            attribute neutral default
        group blink if_any "idle2" auto variant "idle2":
            attribute neutral default
        group blink if_any "idle3" auto variant "idle3":
            attribute neutral default
        
        # if these aren't included, renpy drops them before passing the initial set of 
        # 'relevant' attributes to the attribute_function
        attribute idle1 null 
        attribute idle2 null 
        attribute idle3 null 
    
    
# possible addional mood/expressions: wink, eyebrow quirk, tongue-stick-out, silly, 
# ironic
    
# experimenting with flipped poses. These *do* flip, but don't handle face attributes 
# image eileen_idle4 = At("eileen_idle2", xflip)
# image eileen_idle5 = At("eileen_idle3", xflip)


init 600 python:

    # we require pose to be the first attribute in any image name (see rules above).
    # process this at later priority than any image is instantiated, so that we pick up
    # all the poses
    g_poses['eileen'] = set([a[1] for a in [img.split() for img in renpy.list_images()] if len(a)>1 and a[0]=='eileen'])
    g_idle_poses['eileen'] = [(3,"idle1"), (1,"idle2"), (1,"idle3"), ]
    
    
    # --- Pose Selection ---
    
    # Requirements:
    # 1. recognize when no pose attribute is specified in a show request, and if so, choose 
    # an idle pose.
    # 2. preserve all other attributes on the image request, including temporary 'speaking'
    # and '-speaking'
    # 3. Recognize when a pose, including an idle pose, has been explicitly requested and 
    # do *not* override it, including if the image is reshown with additional tags like 
    # 'happy' or 'speaking'
    # 4. Make all this as close to transparent to the script writer as possible. That is, 
    # the least possible amount of extra cruft that has to be specified when showing 
    # characters, moving them around, and writing dialog. As much as possible, all the 
    # machinery for updating poses should be encapsulated in a characters.rpy file, away 
    # from the story.

    # one problem: when a temporary attribute (like 'speaking') is added, renpy makes a copy of 
    # the current attributes, and tries to restore all of them when the temporary attribute
    # is removed. Therefore any attributes added by adjust_attributes also get backed out:
    # '-speaking','-idle2','-randomized'
    # nominally, this is fine: the extra added attributes can be regenerated any time, 
    # provided we ensure that the callback function actually *does*. but there seem to be 
    # a lot of unexpected corner cases where the adjust_attributes fn does not receive the
    # expected set of attributes, and with the amount of noise caused by prediction calls,
    # debugging adjust_attributes is difficult. So we'll keep this function simple as and 
    # do variant-pose selection in the layeredimage attribute_function.
    
    
init python:
    # register a callback to select on the fly which pose gets shown for a registered tag.
    # return value: a new name to use instead of the supplied name
    def select_pose(name):
        # name: a tuple of (image_tag, attr, attr, ...)
        tag = name[0]
        attrs = set(name[1:])
        cur_attrs = set(renpy.get_attributes(tag) or [])
#         renpy.log("select_pose: received name=%r with cur_attrs=%r" % (name, cur_attrs))
        
        # if no pose has been requested or is already showing, default to 'idle'.
        # (test by intersections with the set of valid poses for this character. by 
        # requirement, a character sprite should always be shown with one and only one pose.)
        if not (attrs & g_poses[tag] or cur_attrs & g_poses[tag]):
            attrs.add("idle")
            
        rv = (tag,) + tuple(attrs)
#         renpy.log("select_pose: returning %r" % (rv,))
        return rv            


# called every time the image is reshown, including for updating the 'speaking' attribute
define config.adjust_attributes["eileen"] = select_pose


screen sprite_idler():
    timer 3.5 repeat True action Function(eileen_idler_set_pose)
All of that is in service to getting it out of the way, so that my actual game script doesn't have to deal with any of it and "just works" the way you'd expect it to.

Code: Select all

label start:
    show screen sprite_idler    # show this once, leave it running

    scene room_bg
    show eileen     # automatically picks an idle pose, and updates it periodically
    ei "Hi there!"
    
    show eileen turningback  # normal alternate poses can be used without fuss
    ei "Lily should be along soon."
    
    ei @idle2   "Require hand-on-hip for this statement, because it feels right."
    
    show eileen # return to randomly-selected idle pose
    "..."
colin r
➔ if you're an artist and need a bit of help coding your game, feel free to send me a PM

Post Reply

Who is online

Users browsing this forum: No registered users