Zooming (+ moving) renpy.Render displayables

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
thexerox123
Regular
Posts: 131
Joined: Fri Jan 20, 2023 3:21 pm
itch: thexerox123
Contact:

Zooming (+ moving) renpy.Render displayables

#1 Post by thexerox123 »

I've started muddling my way through putting together another minigame, and am a bit stuck.

The idea is just to make a basic tobogganing game, dodging obstacles and getting pickups. I've got my basic loop working so that my toboggan is displayed, and I can move it back and forth.

So here's what I have at the moment:

Image

Vs what I'm envisioning:

Image

I want the trees, when generated, to start off small, and near the horizon line. The ones generated on the left would increase in size as they move forwards, and eventually go offscreen left. Likewise for the ones generated on the right and offscreen right.

I ran a test just to see what my outside ranges would be, going from zoom 0.05 to zoom 1.0:

Image

Code: Select all

screen treetest():
    vbox:
        add "images/pine.png"
        xpos 600 ypos 170
        at zoomquantum

    vbox:
        add "images/pine.png"
        xpos 1220 ypos 170 
        at zoomquantum
    vbox:
        add "images/pine.png"
        xpos -10 ypos 170
        at zoomquantum

    vbox:
        add "images/pine.png"
        xpos 1900 ypos 170 
        at zoomquantum
    vbox:
        add "images/pine.png"
        xpos -500 ypos -600

    vbox:
        add "images/pine.png"
        xpos 1810 ypos -400
Given that as the zoom increases, the ypos eventually ends up in the negatives, do I need to get, like, quadratic interpolation working or something? Because I assume it would be parabolic; the ypos would increase at first, then when overtaken by the zoom, it would start to go downwards and eventually into the negatives.

So, I have no idea how to go about this. I spent a good amount of time trying different things, but kept getting errors with my render method. When I didn't get errors, the trees would generate at full-size right away.

Is there some other type of displayable I should be using for this? Some way to change the anchor for the trees to the bottom of the trunk that would be easier than the situation I currently have going?

Here's my current mess of code, just working with the one pine.png image for now:

Code: Select all

init python:
    def quad_interp(a, b, c, t):
        return a * (1 - t) ** 2 + 2 * b * (1 - t) * t + c * t ** 2

    class TreeGenerator(renpy.Displayable):
        def __init__(self, xpos_range, zoom):
            renpy.Displayable.__init__(self)
            self.image_path = Image("images/pine.png")
            self.width = 32
            self.height = 64
            self.ypos = 170
            self.zoom = 0.05
            self.xpos = random.uniform(xpos_range[0], xpos_range[1])
            self.inside_count = 10
            self.zoom_speed = 15.0
            self.start_time = 0
            self.progress = 0    

        def update(self, dt, st):
            if self.start_time == 0:
                self.start_time = st    

            time_elapsed = st - self.start_time    

            start_xpos = random.uniform(-10, 600) if random.choice([True, False]) else random.uniform(1220, 1900)
            self.side = "left" if start_xpos <= 600 else "right"    

            target_xpos = 1810 if self.side == "right" else -500
            target_ypos = -400 if self.side == "right" else -600    

            divisor = self.ypos - target_ypos
            self.progress = min(time_elapsed / self.zoom_speed, 1.0) if divisor != 0 else 0.0    

            # Calculate the ypos and zoom using quadratic interpolation
            self.ypos = quad_interp(170, -600, 0, self.progress)
            self.zoom = quad_interp(0.05, 1.0, 1.0, self.progress)    

            # Calculate the current xpos based on the initial xpos and progress
            self.xpos = lerp(self.xpos, target_xpos, self.progress)    

        def is_offscreen(self):
            return (self.xpos > 1910 or self.xpos < -10)    

        def render(self, width, height, st, at):
            # Calculate the scaled width and height based on the zoom factor
            scaled_width = self.width * self.zoom
            scaled_height = self.height * self.zoom    

            # Render the tree with the scaled dimensions
            tree_image = renpy.render(self.image_path, scaled_width, scaled_height, st, at)
            return tree_image


    class Toboggan(renpy.Displayable):
        def __init__(self, x, y):
            renpy.Displayable.__init__(self)
            self.px = x
            self.py = y
            self.width = 283
            self.height = 221
            self.pxmin = 0
            self.pxmax = 1920 - 283
            self.velocity = 200  # Adjust the velocity as needed
            self.key_left = False
            self.key_right = False
            self.tob_image = Image("tob.png")    


        def move(self, dt):
            print(f"Key Left: {self.key_left}, Key Right: {self.key_right}")
            print(f"Moving toboggan: {self.px}")
            if self.key_left:
                self.px -= 800 * dt
            if self.key_right:
                self.px += 800 * dt    

        def render(self, width, height, st, at):
            print(f"Drawing toboggan at position: ({self.px}, {self.py})")
            tob_image = renpy.render(self.tob_image, width, height, st, at)
            return tob_image

        def event(self, ev, x, y, st):
            if ev.type == pygame.KEYDOWN and ev.key == pygame.K_LEFT:
                print("Left key pressed")
                self.key_left = True
            if ev.type == pygame.KEYDOWN and ev.key == pygame.K_RIGHT:
                print("Right key pressed")
                self.key_right = True
            if ev.type == pygame.KEYUP and ev.key == pygame.K_LEFT:
                print("Left key released")
                self.key_left = False
            if ev.type == pygame.KEYUP and ev.key == pygame.K_RIGHT:
                print("Right key released")
                self.key_right = False

            # if self.key_left:
            #     self.px -= 50
            # if self.key_right:
            #     self.px += 50

            # if self.px < self.pxmin:
            #     self.px = self.pxmin
            # if self.px > self.pxmax:
            #     self.px = self.pxmax

    class HillLoop(renpy.Displayable):
        def __init__(self, toboggan, st):
            renpy.Displayable.__init__(self)
            self.toboggan = toboggan
            self.st = st
            self.oldst = None
            self.end = False
            self.tree_generators = [
                TreeGenerator(xpos_range=(-10, 600), zoom=0.05),
                TreeGenerator(xpos_range=(1220, 1900), zoom=0.05),
            ]
            self.rendered_trees = []  # Separate list for rendered trees    

        def calculate_dt(self, st):
            if self.oldst is None:
                self.oldst = st    

            dt = st - self.oldst
            self.oldst = st
            return dt    

        def update(self, dt):
            # Update toboggan position
            self.toboggan.move(dt)    

            # Update tree generators
            for tree in self.rendered_trees[:]:
                tree.update(dt, self.st)
                if tree.is_offscreen():
                    self.rendered_trees.remove(tree)    

            # Check game conditions (elapsed time, etc.)
            elapsed_time = self.st
            if elapsed_time >= 80:
                # End the loop
                self.end = True    

            if len(self.tree_generators) < 30:
                xpos_range = (-10, 1900)
                if not (600 < self.toboggan.px < 1220):
                    new_tree = TreeGenerator(xpos_range, 0.05)
                    self.rendered_trees.append(new_tree)
                    self.rendered_trees.append(new_tree)    

            renpy.redraw(self.toboggan, 0)    

        def render(self, width, height, st, at):
            # The Render object we'll be drawing into.
            r = renpy.Render(width, height)    

            # Figure out the time elapsed since the previous frame.
            dt = st - self.st
            self.st = st    

            # Draw trees
            for tree in self.rendered_trees:
                # Render the tree
                tree_render = tree.render(width, height, st, at)
                # Blit the tree onto the main Render object
                r.blit(tree_render, (tree.xpos, 170), main=False)    

            # Draw toboggan after trees
            tob_r = self.toboggan.render(width, height, st, at)
            r.blit(tob_r, (int(self.toboggan.px), int(self.toboggan.py)))    

            renpy.redraw(self, 0)    

            # Update game state after drawing all elements
            self.update(dt)    

            return r    

        def event(self, ev, x, y, st):
            renpy.restart_interaction()
            # Call toboggan's event method to handle key events
            self.toboggan.event(ev, x, y, st)


screen go_tob():
    add hill_loop

Looking at the documentation for renpy.Render, I see the zoom attribute, but am a bit confused by it.
zoom(xzoom, yzoom) link

Sets the zoom level of the children of this displayable in the horizontal and vertical axes. Only the children of the displayable are zoomed – the width, height, and blit coordinates are not zoomed.
If the width, height, and blit coordinates are not zoomed, will it not be able to achieve what I'm going for?

Any help or insight would be greatly appreciated! :)

jeffster
Veteran
Posts: 409
Joined: Wed Feb 03, 2021 9:55 pm
Contact:

Re: Zooming (+ moving) renpy.Render displayables

#2 Post by jeffster »

I would start with something like

Code: Select all

default new_trees_per_frame = 0.1
define forest_side_width = 1920 / 4       # 1 quarter left, 2 quarters road, 1 quarter right
define y_length = 600

class Forest(renpy.Displayable):
    def __init__():
        #...
        self.trees = []
        self.tree_h = 64
        self.tree_w = 32

    def render(...)
        r = renpy.Render(1920, y_length)
        if renpy.random.random() < new_trees_per_frame:
            self.trees.append({"distance": y_length, "x": int((forest_side_width*(renpy.random.random() - 0.5) + 1920) % 1920)})

        for t in trees[:]:
            scaled_width = zooming_function(self.tree_w, t["distance"])
            scaled_height = zooming_function(self.tree_h, t["distance"])
            tree_render = renpy.render(self.image_path, scaled_width, scaled_height, st, at)
            r.blit(tree_render, trajectory(t))

            t["distance"] -= 1   # or something
            if t["distance"] <= 0:
                self.trees.remove(t)

        renpy.redraw(self, 0)
        return r
Meaning Forest is one object rendered with many blits, zooming_function() and trajectory() could be something simple (linear) for testing purposes. When the displayable works, they could be fine-tuned.

PS. If there could be multiple trees at the same "distance" (and they would probably be adjacent in the list), the same render could be reused for them.

thexerox123
Regular
Posts: 131
Joined: Fri Jan 20, 2023 3:21 pm
itch: thexerox123
Contact:

Re: Zooming (+ moving) renpy.Render displayables

#3 Post by thexerox123 »

jeffster wrote: Tue Mar 12, 2024 7:44 pm I would start with something like
...
Meaning Forest is one object rendered with many blits, zooming_function() and trajectory() could be something simple (linear) for testing purposes. When the displayable works, they could be fine-tuned.

PS. If there could be multiple trees at the same "distance" (and they would probably be adjacent in the list), the same render could be reused for them.
Thank you for this! I'll try working from that example! :D

Also, I should have noted, the pine.png image has a native size of 640 x 1280; so the 32 x 64 that I had as my width and height was an attempt at manually inputting a smaller size. Not sure if that impacts how I should be applying things. (I'm also not sure if the large image size might make them hard to process? I could always shrink them or convert them to svg, if Renpy can handle vectors? fwiw my original code didn't seem to be having trouble loading a bunch of the trees in at full size, that's just not at all what I wanted it to do :lol: )

jeffster
Veteran
Posts: 409
Joined: Wed Feb 03, 2021 9:55 pm
Contact:

Re: Zooming (+ moving) renpy.Render displayables

#4 Post by jeffster »

thexerox123 wrote: Thu Mar 14, 2024 12:52 am Also, I should have noted, the pine.png image has a native size of 640 x 1280; so the 32 x 64 that I had as my width and height was an attempt at manually inputting a smaller size. Not sure if that impacts how I should be applying things. (I'm also not sure if the large image size might make them hard to process? I could always shrink them or convert them to svg, if Renpy can handle vectors? fwiw my original code didn't seem to be having trouble loading a bunch of the trees in at full size, that's just not at all what I wanted it to do :lol: )
Modern Ren'Py probably works with gl2 textures and can use GPU for resize operations. If an image is shown always the same size, then of course it's best to use the original of the same size. But here as we resize, perhaps it could be optimal to have double size in the original (64x128), for better resizing to various sizes (but that's just my guess). 640x1280 seems to be overkill, costing extra memory and computing resources.

Post Reply

Who is online

Users browsing this forum: Ahrefs [Bot]