[RESOLVED] Attempting to achieve smooth, lagless movement of screens

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
DraymondDarksteel
Newbie
Posts: 13
Joined: Mon Sep 05, 2016 1:51 pm
Contact:

[RESOLVED] Attempting to achieve smooth, lagless movement of screens

#1 Post by DraymondDarksteel »

Howdy!

I have a game that I'm working on, and I'm attempting to implement a dungeon-crawling minigame. I want the minigame to appear on a screen--though this isn't entirely mandatory--and I want the screen to move smoothly, and I don't want to render too much at once, so that the screen loads laglessly, and gameplay is nice and smooth.

The Pink Engine demonstrates this behavior quite well. Unfortunately, I cannot use it, because my game is already extremely established in my own engine. I was hoping that by looking at its code, I might figure out how to do it, but I'm afraid I can't quite understand it without significantly more experience.

This is the screen I display maps on:

Code: Select all

screen DungeonMap(dungeon):
    python:
        width = dungeon.GetActiveFloor().GetFloorWidth()
        height = dungeon.GetActiveFloor().GetFloorHeight()
        ts = dungeon.TileSize

    grid width + 22 height + 22:
        at transform:
            subpixel True
            anchor (math.floor((dungeon.LastCameraX + 11) * ts), math.floor((dungeon.LastCameraY + 11) * ts))
            linear 0.2 anchor (math.floor((dungeon.CameraX + 11) * ts), math.floor((dungeon.CameraY + 11) * ts))
        transpose True
        pos (0.5, 0.5)
        for x in range(-11, width + 11):
            for y in range(-11, height + 11):
                python:
                    dx = (dungeon.CameraX - 0.5) - x
                    dy = (dungeon.CameraY - 0.5) - y
                    distance = math.sqrt(dx**2 + dy**2)
                if (abs(dx) < 11 and abs(dy) < 7 and distance < 9):
                    add dungeon.GetTile((x, y))
                else:
                    null height ts width ts

    use DungeonButtons(dungeon)
The "+11"s and "+22"s are to artificially extend the border of the map so that players cannot see any "missing" tiles when they go to the edge. I can reduce the lag by decreasing the size of the border, but there are enough problems here I suspect that would be akin to trying to put out an oil fire with water.

The map itself is pretty simple, with a lot of the tile-fetching work being offset to the Dungeon's GetTile() function, seen here. (And truncated for brevity.)

Code: Select all

def GetTile(self, coords):
            ts = self.Dungeon.TileSize
            cameraCoords = (self.Dungeon.CameraX, self.Dungeon.CameraY)
            finaltile = Null()

            blockedwalls = []
            if (self.IsWall((coords[0] - 1, coords[1] - 1))):
                blockedwalls.append("TL")
            if (self.IsWall((coords[0], coords[1] - 1))):
                blockedwalls.append("T")
            ||TRUNCATION||
            if (self.IsWall((coords[0] + 1, coords[1] + 1))):
                blockedwalls.append("BR")                    

            if (blockedwalls == ["TL", "T", "TR", "L", "R", "BL", "B", "BR"] or self.IsInRoom(coords) and blockedwalls == []):
                sheetcoords = RandomChoice([(1, 1), (1, 1), (1, 1), (4, 1), (7, 1)], True, coords)                    
            elif (blockedwalls == ["T", "L", "R", "B"]):
                sheetcoords = (1, 7)
            elif (blockedwalls == ["TL", "T", "TR", "L", "R", "B"]):
                sheetcoords = (1, 12)
            ||TRUNCATION||
            elif ("L" in blockedwalls):
                sheetcoords = (2, 7)
            elif ("T" in blockedwalls):
                sheetcoords = (1, 8)
            else:
                sheetcoords = (1, 4)

            visibility = self.GetVisibility(coords)
                
            if (self.IsWall(coords)):
                finaltile = Transform("images/sprites/{}.webp".format(self.GetPalette()), crop=(84 + sheetcoords[0] * 25, 163 + sheetcoords[1] * 25, 24, 24), zoom=4)
            elif (self.IsInRoom(coords)):
                finaltile = Transform("images/sprites/{}.webp".format(self.GetPalette()), crop=(84 + (sheetcoords[0] + 9) * 25, 163 + sheetcoords[1] * 25, 24, 24), zoom=4)
                if (coords in self.SpecialTiles.keys()):#visibility == 0 and 
                    finaltile = Composite((ts, ts),
                    (0, 0), finaltile, 
                    (0, 0), self.SpecialTiles[coords].GetTile())

            if (visibility == 0):
                finaltile = Composite((ts, ts), 
                (0, 0), finaltile, 
                (0, 0), self.GetMob(coords))
                
            else:
                finaltile = Composite((ts, ts), 
                (0, 0), finaltile, 
                (0, 0), Transform("images/sprites/fog{}.png".format(visibility), size=(ts, ts)))

            if (coords == cameraCoords and self.Dungeon.DungeonMode == "FE"):
                finaltile = Composite((ts, ts), 
                (0, 0), finaltile, 
                (0, 0), "cursor")

            return finaltile
I can also reduce the lag somewhat by removing the "fog of war", but, again, I suspect that poor practice is causing this, moreso than layering an image.

It's worth mentioning that everything looks correct, graphically, as long as the screen is stationary, and I do not attempt to change the position of anything. However, right now, when I adjust the camera's offset, the map still jerks ahead in spasms--the transform applied to the screen's grid is just a band-aid. Further, as I mentioned, this map becomes quite laggy when I implement all the features I want, even if the map is relatively small.

Here is a video demonstrating the behavior. My recording software makes it look worse than it is, but it's still pretty bad.

I would prefer not to toss away all this work, but, at this point, I'm more than willing to, in order to get this done properly.

I do not know what's causing my errors, but there are a couple possible ideas that occur:

1. Each cropped tile is loading the entire spritesheet, filling memory up far more than I think it should. My understanding is that the spritesheet is only loaded once, but perhaps I'm wrong. Would the solution in this case be to separate the spritesheet up into separate sprites? That sounds miserable.
2. Ren'Py's renderer simply cannot handle the 100-odd sprites I want to put onscreen at once. I'll simply have to change how much the player can see at once.
3. When compositing images, as I do multiple times to determine the "final tile" to display, each composited level is its own image, so each "foggy" tile represents two sprites in memory. I'm not sure how I would fix this one.

I have even less of an idea as to how I could seem to make the movement of the sprite characters smooth, such that I can't even offer something I'd been considering. I can somewhat fake it through the transform applied to the parental grid, but as you've seen in the video above, it doesn't particularly work.

I would absolutely appreciate any advice that anyone might be able to offer.
Last edited by DraymondDarksteel on Wed Nov 08, 2023 11:58 am, edited 1 time in total.

User avatar
m_from_space
Eileen-Class Veteran
Posts: 1011
Joined: Sun Feb 21, 2021 3:36 am
Contact:

Re: Attempting to achieve smooth, lagless movement of screens

#2 Post by m_from_space »

So I don't know what "width" and "height" in your dungeon map refer to, but I assume in your video it's about the (tile) width and height of the whole current map? Because that would be nuts. Assuming we are only talking about 20 tiles x 12 tiles that the player can see on the screen (like I can see in your GIF), at the moment you are drawing a grid of let's say 20 + 22 x 12 + 22 = 1428 cells (!?) instead of just 240 cells. That's the first thing I think I would change: Only draw what the player can see.

You don't have to fill empty cells anymore by the way, Renpy will do it automatically.

DraymondDarksteel
Newbie
Posts: 13
Joined: Mon Sep 05, 2016 1:51 pm
Contact:

Re: Attempting to achieve smooth, lagless movement of screens

#3 Post by DraymondDarksteel »

Thank you! I believe I'm doing that, though I may be wrong-- in the lines

Code: Select all

if (abs(dx) < 11 and abs(dy) < 7 and distance < 9):
    add dungeon.GetTile((x, y))
 else:
    null height ts width ts
I only fetch the tiles that the player would actually be able to see, and put a "null" in its place instead if its outside the player's range of vision. Unless I'm misinterpreting something? Do "nulls" also have to be "drawn?" If so, what should I put in place of the null?

Also, could you elaborate on what you mean by not having to fill empty cells anymore? If I don't put a null as an alternative to drawing the tile, I get a "grid not full" error. Is there some other way I could be handling this?

Many thanks.

User avatar
Imperf3kt
Lemma-Class Veteran
Posts: 3807
Joined: Mon Dec 14, 2015 5:05 am
itch: Imperf3kt
Location: Your monitor
Contact:

Re: Attempting to achieve smooth, lagless movement of screens

#4 Post by Imperf3kt »

You need to enable that feature using configs

Code: Select all

define config.allow_underfull_grids = True
https://www.renpy.org/doc/html/changelo ... nd-vpgrids
Warning: May contain trace amounts of gratuitous plot.
pro·gram·mer (noun) An organism capable of converting caffeine into code.

Current project: GGD Mentor

Twitter

DraymondDarksteel
Newbie
Posts: 13
Joined: Mon Sep 05, 2016 1:51 pm
Contact:

Re: Attempting to achieve smooth, lagless movement of screens

#5 Post by DraymondDarksteel »

Thank you! And this is more efficient than just using nulls to fill the empty space? I've replaced the nulls in my code, and I don't notice a marked improvement, though perhaps it would be more noticeable on larger maps.

EDIT:

I made some changes to the map display code. Seems drawing a grid, even if I filled it full of Nulls, still took some rendering power.

Code: Select all

screen DungeonMap(dungeon):
    python:
        width = dungeon.GetActiveFloor().GetFloorWidth()
        height = dungeon.GetActiveFloor().GetFloorHeight()
    
    grid 18 12:
        transpose True
        align (0.5, 0.5)
        for x in range(dungeon.CameraX - 9, dungeon.CameraX + 9):
            for y in range(dungeon.CameraY - 6, dungeon.CameraY + 6):
                python:
                    dx = x - dungeon.CameraX + 0.5
                    dy = y - dungeon.CameraY + 0.5
                    distance = math.sqrt(dx**2 + dy**2)
                if (distance < 9):
                    add dungeon.GetTile((x, y))
                else:
                    null height ts width ts

    use DungeonButtons(dungeon)
This new DungeonMap code is much more powerful, and can handle dungeons of up to 100*100 as easily as it handles 20*20. However the primary issue of smooth movement is still something I'm struggling with. The last map, at the least, had that smooth transition when the map moved, but this one doesn't even have that. It's far less laggy, though, so if I had to choose between the two, I would take this one.

User avatar
m_from_space
Eileen-Class Veteran
Posts: 1011
Joined: Sun Feb 21, 2021 3:36 am
Contact:

Re: Attempting to achieve smooth, lagless movement of screens

#6 Post by m_from_space »

That's a way better DungeonMap, yes. But you're basically creating a fixed grid on the screen now, so "moving" means the map shifts within this grid, so it's not smooth to look at. It's basically a matrix with changing values inside of it as you move.

Instead of using Renpy's grid system, you could place the relevant (visible) tiles for the player as images on fixed positions on the screen (with an offset for each tile). This way you're not bound to any grid. Your original idea was to create a really large grid, so the "matrix" effect would not occur, but that's just not ideal in Renpy.

Code: Select all

...
    add dungeon.GetTile((x, y)):
        xpos ... ypos ...
You could also think about what parts of the map don't have to be recalculated or redrawn when a player moves and only change those that have to (e.g. maybe fog of war could be an overlay of images over ground images that never will change).

User avatar
PyTom
Ren'Py Creator
Posts: 16096
Joined: Mon Feb 02, 2004 10:58 am
Completed: Moonlight Walks
Projects: Ren'Py
IRC Nick: renpytom
Github: renpytom
itch: renpytom
Location: Kings Park, NY
Contact:

Re: Attempting to achieve smooth, lagless movement of screens

#7 Post by PyTom »

One big thing to do would be to precompute as much as possible. dungeon.GetTile looks expensive, and it seems like it has to be called a lot.

What you probably want is a vpgrid that iterates over precomputed data, as much as possible. Assuming all the tiles are the same size, a vpgrid will ensure only tiles that can be seen are rendered.
Supporting creators since 2004
(When was the last time you backed up your game?)
"Do good work." - Virgil Ivan "Gus" Grissom
Software > Drama • https://www.patreon.com/renpytom

DraymondDarksteel
Newbie
Posts: 13
Joined: Mon Sep 05, 2016 1:51 pm
Contact:

Re: Attempting to achieve smooth, lagless movement of screens

#8 Post by DraymondDarksteel »

Thank you, you two, that's really wonderful. By precomputing everything I physically can, and drawing only what absolutely _must_ be on-screen, I've really managed to make the performance leaps and bounds better than it was before. Here's the final version of the code, now.

Code: Select all

screen DungeonMap(dungeon):
    $ ts = dungeon.TileSize  

    for x in range(20):
        for y in range(12):
            python:
                offsetx = dungeon.CameraX + x - 10
                offsety = dungeon.CameraY + y - 6
                coords = (offsetx, offsety)
                tile = dungeon.GetTile(coords)#Precomputed
                mob =  dungeon.GetMob(coords)#determined at runtime
                visibility = dungeon.GetVisibility(coords)#determined at runtime
            add tile pos (x * ts, y * ts - 30)
            if (visibility != 0):
                add Transform("images/sprites/fog{}.png".format(visibility), size=(ts, ts)) pos (x * ts, y * ts - 30)
            elif (mob != None):
                add mob pos (x * ts, y * ts - 30)

    use DungeonButtons(dungeon)
The last thing I'm trying to figure out is how to smoothly translate the map tiles, and mob positions. I keep track of the camera's previous position, as well as the previous positions of each mob, so it's easy enough to get the delta of their change--however, I'm not sure how to scale the change over time in the screen space. I've used various transitions, to no avail. I also attempted a very gross and hacky solution previously where I iterated a variable as fast as I could using a recurring action on a timer on the screen, but the timer couldn't seem to go faster than, say, a fifth of a second, even if I set the timer to 0.00005. Probably for the best, as even attempting that solution made me feel gross.

If anyone can provide any advice, I'll be extremely grateful!

User avatar
m_from_space
Eileen-Class Veteran
Posts: 1011
Joined: Sun Feb 21, 2021 3:36 am
Contact:

Re: Attempting to achieve smooth, lagless movement of screens

#9 Post by m_from_space »

Here is a *very* basic idea I just tried out some days ago (because of your problem), but I didn't have time to investigate further. I just want to show you how I would try to animate the map. It works flawlessly at this scale. And since you asked for *any* advice, here is at least some idea. I might have a look at it later and come up with a more serious solution. The important part is how I animate the map movement, so have a look at the DungeonMap screen. I just toggle a variable and force a transform, then switch back to a static version of the same map. Maybe this will spark some ideas in you.

Code: Select all

define map_start = [
    "#####################################",
    "#....**.---dssssssssssss------------#",
    "#...................................#",
    "#.---...............................#",
    "#...................................#",
    "#.---...............................#",
    "#...................................#",
    "#.---...............................#",
    "#...................................#",
    "#...................................#",
    "#.---...............................#",
    "#####################################"
]

init python:
    def getMap(map):
        # at the moment we get the whole map
        args = []
        for i, row in enumerate(map):
            for j, char in enumerate(row):
                args.append(Text(char, xpos=j*20, ypos=i*20))
        return Fixed(*args)
    def moveMap(x, y):
        global last_x, last_y, new_x, new_y
        last_x, last_y = map_x, map_y
        new_x, new_y = x, y
        store.map_animate_move = True
        return
    def stopMap():
        store.map_x = store.new_x
        store.map_y = store.new_y
        store.map_animate_move = False
        return
    def movePlayer(x, y):
        global player_x, player_y
        if x == player_x and y == player_y:
            return
        player_x = x
        player_y = y
        moveMap(-20 * player_x + player_pos_x, -20 * player_y + player_pos_y)
        return

# pixel position of camera
default camera_x = 0
default camera_y = 0

# player position inside the map matrix
default player_x = 1
default player_y = 1

# pixel position of player marker on the screen
default player_pos_x = 640
default player_pos_y = 360

# pixel position of the map in relation to player
default map_x = -20 * player_x + player_pos_x
default map_y = -20 * player_y + player_pos_y

default last_x = 0
default last_y = 0

default new_x = 0
default new_y = 0

default current_map = getMap(map_start)

default map_animate_move = False

transform animate_map():
    subpixel True
    xpos last_x
    ypos last_y
    linear 0.4 xpos new_x ypos new_y

screen DungeonMap():
    modal True
    frame:
        background None
        xpos camera_x ypos camera_y
        if map_animate_move:
            add current_map at animate_map
            timer 0.5 action Function(stopMap)
        else:
            add current_map:
                xpos map_x ypos map_y
        add Text("P", color="#0f0"):
            xpos player_pos_x ypos player_pos_y
    use DungeonControls

style sty_dungeon is text:
    font "fonts/UbuntuMono-R.ttf"
    size 16
    color "#00f"

screen DungeonControls():
    vbox:
        xpos 100 ypos 100
        vbox:
            text "Move Player x:[player_x] y:[player_y]"
            grid 3 3:
                textbutton "7" action Function(movePlayer, player_x-1, player_y-1) text_style "sty_dungeon"
                textbutton "8" action Function(movePlayer, player_x, player_y-1) text_style "sty_dungeon"
                textbutton "9" action Function(movePlayer, player_x+1, player_y-1) text_style "sty_dungeon"
                textbutton "4" action Function(movePlayer, player_x-1, player_y) text_style "sty_dungeon"
                null
                textbutton "6" action Function(movePlayer, player_x+1, player_y) text_style "sty_dungeon"
                textbutton "1" action Function(movePlayer, player_x-1, player_y+1) text_style "sty_dungeon"
                textbutton "2" action Function(movePlayer, player_x, player_y+1) text_style "sty_dungeon"
                textbutton "3" action Function(movePlayer, player_x+1, player_y+1) text_style "sty_dungeon"
        vbox:
            text "Move Camera (no animation here)"
            grid 3 3:
                null
                textbutton "u" action SetVariable("camera_y", camera_y-20) text_style "sty_dungeon"
                null
                textbutton "<<" action SetVariable("camera_x", camera_x-20) text_style "sty_dungeon"
                null
                textbutton ">>" action SetVariable("camera_x", camera_x+20) text_style "sty_dungeon"
                null
                textbutton "d" action SetVariable("camera_y", camera_y+20) text_style "sty_dungeon"
                null

label start:

    show screen DungeonMap()
    "Fin."

    return

Post Reply

Who is online

Users browsing this forum: Adabelitoo, Ahrefs [Bot], Google [Bot]