Scrolling objects don't move smoothly or as expected /pygame

Scrolling objects don't move smoothly or as expected /pygame

Postby flashdamage » Mon Apr 07, 2014 5:08 pm

I'm working on a prototype for an arcade game. In the full version of the code I scroll a series of png images as a parallax background. The images scroll smoothly and as expected. This video of the scrolling pngs is a bit jerky, but the actual app isn't: https://www.youtube.com/watch?v=EihQ2iclzuM&list=UUOFx42xI8VYGtSvr18R5hgg

When I started to play with the player movement physics, I went back to the original prototype that grew into the game idea. I re-wrote my code to better optimize it but in both the original prototype and the re-written one I have movement issues. This video of the prototype is fairly accurate with very little video capture lag. Pay attention to the gaps between the buildings, as they get within the right most 3rd of the display area, the gap will suddenly shrink as if the building to the left of the gap is suddenly shifting right a few pixels. The smaller the gap, the more noticeable it is. Here's a very short video that clearly shows this phenomenon: https://www.youtube.com/watch?v=0cdhrez ... e=youtu.be

At about 1 second in you'll notice a very narrow gap between buildings in the rear layer. At :04 seconds in the gap is even with the blue player object, the left rectangle shifts and the gap vanishes. There's a second, larger gap to the right of that one that does the same thing but since the gap is larger it doesn't completely vanish. I've looked over the code numerous times but can't see what could be causing this anomaly. I'm hoping someone can tell me if it's something I did or a limitation I'm encountering.

here's the code I created for this:
Code: Select all
import pygame
import sys
import random
from pygame.locals import *

# set up pygame
pygame.init()
mainClock = pygame.time.Clock()

# set up window
WINDOWWIDTH = 400
WINDOWHEIGHT = 400
windowSurface = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT), 0, 32)
pygame.display.set_caption('DEMO #1: City Skyline')

# set up the colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
DARK = (38, 16, 30)
MEDIUM = (74, 34, 58)
BRIGHT = (112, 52, 88)
SKY = (70, 10, 88)
BLUE = (0, 0, 255)


class Scroller():
    def __init__(self, speed, color, heightMax):
        # Speed of the layer scroll, the color of the layer and the maximum height for buildings
        # set up the building parameters
        self.buildingHeightMax = heightMax
        self.buildingHeightMin = 100
        self.buildingWidthMax = 125
        self.buildingWidthMin = 75
        self.buildings = []
        self.layerspeed = speed
        self.buildTime = True
        self.buildCountdown = 10
        self.color = color

    def update(self):
        # Check if it's time to build. If not, decrement counter
        if self.buildTime == False:
            self.buildCountdown -= 1
            # If time is 0, time to build, reset counter to a new random time
            if self.buildCountdown <= 0:
                self.buildTime = True
                self.buildCountdown = random.randint(3, self.layerspeed)

        # create front layer building if it's time
        if self.buildTime:
            # generate random width and height of building
            buildingHeight = random.randint(self.buildingHeightMin, self.buildingHeightMax)
            buildingWidth = random.randint(self.buildingWidthMin, self.buildingWidthMax)
            buildingTop = WINDOWHEIGHT - buildingHeight
            # This generates the building object from the above parameters
            building = pygame.Rect(WINDOWWIDTH, buildingTop, buildingWidth, WINDOWHEIGHT)
            self.buildTime = False
            self.buildCountdown = random.randint(3, self.layerspeed * 5)
            # add building to buildings list
            self.buildings.append(building)

        # move all buildings on layer at set speed
        for building in self.buildings:
            # if the building is off the screen, trash it. If not, move it to the
            # right at the objects speed.
            if building.right < 0:
                self.buildings.remove(building)
            else:
                building.left -= self.layerspeed


        # draw the Front buildings
        for i in range(len(self.buildings)):
            pygame.draw.rect(windowSurface, self.color, self.buildings[i])



def main():

    # set up the player
    player = pygame.Rect(50, 50, 20, 10)
    moveUp = False
    moveDown = False
    moveRight = False
    moveLeft = False
    player_vert_speed = 4
    player_forward_speed = 3
    player_reverse_speed = 7

    # Set up scrolling background layers
    # Works best when front row is darker than middle
    # and back row is lightest
    city_back_row = Scroller(3, BRIGHT, 350)
    city_middle_row = Scroller(5, MEDIUM, 300)
    city_front_row = Scroller(7, DARK, 250)

    # run the main loop
    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()
            if event.type == KEYDOWN:
                if event.key == K_UP or event.key == ord('w'):
                    moveUp = True
                    moveDown = False
                if event.key == K_DOWN or event.key == ord('s'):
                    moveUp = False
                    moveDown = True
                if event.key == K_RIGHT or event.key == ord('d'):
                    moveRight = True
                    moveLeft = False
                if event.key == K_LEFT or event.key == ord('a'):
                    moveLeft = True
                    moveRight = False

            if event.type == KEYUP:
                if event.key == K_ESCAPE:
                    pygame.quit()
                    sys.exit()
                if event.key == K_UP or event.key == ord('w'):
                    moveUp = False
                if event.key == K_DOWN or event.key == ord('s'):
                    moveDown = False
                if event.key == K_RIGHT or event.key == ord ('d'):
                    moveRight = False
                if event.key == K_LEFT or event.key == ord ('a'):
                    moveLeft = False


        # Move the player
        if moveDown and player.bottom < (WINDOWHEIGHT - 10):
            player.bottom += player_vert_speed
        if moveUp and player.top > 10:
            player.top -= player_vert_speed
        if moveRight and player.right < (WINDOWWIDTH - 10):
            player.right += player_forward_speed
        if moveLeft and player.left > 10:
            player.left -= player_reverse_speed


        # draw the sky color background onto the surface
        windowSurface.fill(SKY)

        # Create 3 parallax scrollers for background
        city_back_row.update()
        city_middle_row.update()
        city_front_row.update()

        # draw the player
        pygame.draw.rect(windowSurface, BLUE, player)

        # draw the scene
        pygame.display.update()
        mainClock.tick(30)


main()
User avatar
flashdamage
 
Posts: 9
Joined: Sun Apr 06, 2014 9:59 pm
Location: Colorado Springs

Re: Scrolling objects don't move smoothly or as expected /py

Postby flashdamage » Mon Apr 07, 2014 8:57 pm

I finally got a response on StackOverflow that contained a solution. It comes down to manipulating a list that is being currently iterated. The solution was as simple as making the line of code that moves the buildings to a separate loop from the one that does garbage collection. Just in case this is useful for anyone else I'll post the details. This simple example code demonstrates the problem. Try it and you'll see that not only 4 won't be printed, but also 1.5 will be skipped:
Code: Select all
a = [2, 3, 4, 1.5, 6, 8, 3.2]

for element in a:
    if element == 4:
        a.remove(element)
    else:
        print element

When that same logic is applied to my scrolling buildings, it results in a building having it's movement skipped just like 1.5 is skipped in the above example. So every few iterations through the loop, whenever a building is removed, the building immediately to the right of it get's stalled and doesn't move. Here's my code before applying the solution:
Code: Select all
for building in self.buildings:
    if building.right < 0:
    self.buildings.remove(building)
else:
    building.left -= self.layerspeed

And here's the same code after the solution. The app runs very smooth now:
Code: Select all
for building in self.buildings:
    if building.right < 0:
        self.buildings.remove(building)

for building in self.buildings:
    building.left -= self.layerspeed
User avatar
flashdamage
 
Posts: 9
Joined: Sun Apr 06, 2014 9:59 pm
Location: Colorado Springs

Re: Scrolling objects don't move smoothly or as expected /py

Postby Mekire » Tue Apr 08, 2014 1:33 am

Glad you solved your problem. Wanted to note, this isn't related to garbage collection. This is a result of changing the size of a sequence while you iterate over it (never a good idea).

Since you solved your problem I will instead provide you with this quick recode. Specifically take note of how movement is handled. There is absolutely no need for those movement flags (moveUp, moveDown, moveRight, moveLeft).

Let me know if you have any questions on how anything is implemented:
Code: Select all
import sys
import random
import pygame as pg


SCREEN_SIZE = (SCREEN_WIDTH, SCREEN_HEIGHT) = (400, 400)

DIRECT_DICT = {"LEFT"  : (-1, 0),
               "RIGHT" : ( 1, 0),
               "UP"    : ( 0,-1),
               "DOWN"  : ( 0, 1)}

CONTROLS = {(pg.K_LEFT, pg.K_a)  : "LEFT",
            (pg.K_RIGHT, pg.K_d) : "RIGHT",
            (pg.K_UP, pg.K_w)    : "UP",
            (pg.K_DOWN, pg.K_s)  : "DOWN"}

#Set up the colors.
DARK = (38, 16, 30)
MEDIUM = (74, 34, 58)
BRIGHT = (112, 52, 88)
SKY = (70, 10, 88)
PLAYER_COLOR = pg.Color("blue")


BUILDING_MIN_WIDTH = 75
BUIDLING_MAX_WIDTH = 125
BUILDING_MIN_HEIGHT = 100


class Player(pg.sprite.Sprite):
    def __init__(self, pos, size, *groups):
        pg.sprite.Sprite.__init__(self, *groups)
        self.image = pg.Surface(size).convert()
        self.image.fill(PLAYER_COLOR)
        self.rect = self.image.get_rect(topleft=pos)
        self.speed = {"LEFT" : 7, "RIGHT" : 3, "UP" : 4, "DOWN" : 4}

    def update(self, keys, screen_rect):
        for control in CONTROLS:
            if any(keys[key] for key in control):
                direct = CONTROLS[control]
                self.rect.x += DIRECT_DICT[direct][0]*self.speed[direct]
                self.rect.y += DIRECT_DICT[direct][1]*self.speed[direct]
        self.rect.clamp_ip(screen_rect)

    def draw(self, surface):
        surface.blit(self.image, self.rect)


class Building(pg.sprite.Sprite):
    def __init__(self, color, speed, max_height, *groups):
        pg.sprite.Sprite.__init__(self, *groups)
        self.speed = speed
        self.image = self.make_image(color, max_height)
        self.rect = self.image.get_rect(x=SCREEN_WIDTH, bottom=SCREEN_HEIGHT)

    def make_image(self, color, max_height):
        width = random.randint(BUILDING_MIN_WIDTH, BUIDLING_MAX_WIDTH)
        height = random.randint(BUILDING_MIN_HEIGHT, max_height)
        image = pg.Surface((width,height)).convert()
        image.fill(color)
        return image

    def update(self, bound_rect):
        self.rect.x -= self.speed
        if not self.rect.colliderect(bound_rect):
            self.kill()


class Scroller(object):
    def __init__(self, speed, color, max_height):
        self.speed = speed
        self.max_height = max_height
        self.buildings = pg.sprite.Group()
        self.build_time = True
        self.build_countdown = 10
        self.color = color

    def update(self, screen_rect):
        if not self.build_time:
            self.build_countdown -= 1
            if not self.build_countdown:
                self.build_time = True
                self.build_countdown = random.randint(3, self.speed)
        if self.build_time:
            Building(self.color, self.speed, self.max_height, self.buildings)
            self.build_time = False
            self.build_countdown = random.randint(3, self.speed*5)
        self.buildings.update(screen_rect)

    def draw(self, surface):
        self.buildings.draw(surface)


class Control(object):
    def __init__(self):
        pg.init()
        pg.display.set_caption('DEMO #1: City Skyline')
        self.screen = pg.display.set_mode(SCREEN_SIZE)
        self.screen_rect = self.screen.get_rect()
        self.clock = pg.time.Clock()
        self.fps = 30
        self.done = False
        self.player = Player((50,50), (20,10))
        self.scrollers = [Scroller(3, BRIGHT, 350),
                          Scroller(5, MEDIUM, 300),
                          Scroller(7, DARK, 250)]

    def event_loop(self):
        for event in pg.event.get():
            if event.type == pg.QUIT:
                self.done = True

    def update(self):
        keys = pg.key.get_pressed()
        for scroller in self.scrollers:
            scroller.update(self.screen_rect)
        self.player.update(keys, self.screen_rect)

    def draw(self):
        self.screen.fill(SKY)
        for scroller in self.scrollers:
            scroller.draw(self.screen)
        self.player.draw(self.screen)

    def main_loop(self):
        while not self.done:
            self.event_loop()
            self.update()
            self.draw()
            pg.display.update()
            self.clock.tick(self.fps)


def main():
    app = Control()
    app.main_loop()
    pg.quit()
    sys.exit()


if __name__ == "__main__":
    main()

-Mek
Last edited by Mekire on Fri Apr 11, 2014 5:35 am, edited 2 times in total.
Reason: Minor code edit.
User avatar
Mekire
 
Posts: 1149
Joined: Thu Feb 07, 2013 11:33 pm
Location: Asakusa, Japan

Re: Scrolling objects don't move smoothly or as expected /py

Postby flashdamage » Tue Apr 08, 2014 1:53 am

You're beating me to the punchline. Retooling the player as a class is the next part of the project after I finished my Coursera class this evening. I've used the same movement structure for the last two projects, it's the basic one used in most of Al Sweigart's examples. I'm going to study your rewrite tonight. I'm sure I'll have some follow up questions or comments. Thanks for clarifying exactly why modifying a loop as it's being iterated was a bad idea. I knew it wasn't with garbage collection, just that the act of removing items was the cause of the issue. The exact reasoning wasn't clear.
User avatar
flashdamage
 
Posts: 9
Joined: Sun Apr 06, 2014 9:59 pm
Location: Colorado Springs

Re: Scrolling objects don't move smoothly or as expected /py

Postby Mekire » Tue Apr 08, 2014 1:57 am

Honestly, no particular offense to him, but I disagree with a ton of stuff Al does.
The way he avoids the subject of classes completely in his Invent books really drives me crazy (among other things).

-Mek
User avatar
Mekire
 
Posts: 1149
Joined: Thu Feb 07, 2013 11:33 pm
Location: Asakusa, Japan

Re: Scrolling objects don't move smoothly or as expected /py

Postby flashdamage » Tue Apr 08, 2014 2:09 am

I can't argue with that. It does the best job I've seen at teaching pygame but violates fundamental pythonic structure while doing so. As a result, when I first wrote this scroller it was as a liner program with no functions or classes. After I was done I decided it would be a good learning experience to rewrite it properly... as you have done for me. You're example is going to help me bridge that gap I believe.
User avatar
flashdamage
 
Posts: 9
Joined: Sun Apr 06, 2014 9:59 pm
Location: Colorado Springs

Re: Scrolling objects don't move smoothly or as expected /py

Postby Mekire » Tue Apr 08, 2014 2:17 am

I have a repo of sample code with that exact goal in mind if you are interested.
All other tutorials/examples I know of focus on taking someone from complete beginner to intermediate (with a bit of game thrown in).
I instead am trying to aim at those who are already comfortable with the language who are beginners to game programming; not programming in general.

https://github.com/Mekire/meks-pygame-samples

-Mek
User avatar
Mekire
 
Posts: 1149
Joined: Thu Feb 07, 2013 11:33 pm
Location: Asakusa, Japan

Re: Scrolling objects don't move smoothly or as expected /py

Postby flashdamage » Tue Apr 08, 2014 2:28 am

Fantastic! This is the kind of thing I need most right now.
User avatar
flashdamage
 
Posts: 9
Joined: Sun Apr 06, 2014 9:59 pm
Location: Colorado Springs

Re: Scrolling objects don't move smoothly or as expected /py

Postby flashdamage » Wed Apr 09, 2014 2:54 am

Wow Mekire, I've spent the last few hours reading over and over the code rewrite you did. First of all, thanks! I would have never come up with nearly as elegant a schema and your approach taught me plenty. I am going to keep a print out of it near by as a reference. I had thought about making the buildings their own objects but wasn't sure how. Admittedly, I've only just begun to work with sprites and have been fumbling with them. Your code makes some things more clear to me. I had to read through the classes many times to figure out the what and how of the *args input for the Building() class. I now see they belong to the self.buildings group of the Scroller() class. I had to read the docs on Sprite and sprite.Group, paired with your code I think I now understand sprites much better. I do believe I understand everything you've done with my original code except for player movement, which is also the aspect I am working on currently. I'll go through what I do and don't understand.

  1. The player is initialized in the __init__ of Control() with a pos and size
    1. The player object is a sprite and when it's initialized it accepts a pos and a size
    2. A surface object is created and filled with color, it's rect is assigned the rect of self.image with a flag designating topleft as pos (because the default is 0,0. Correct ?)
    3. Player.speed is set to a dict defining direction keys linked to speed variables.
  2. The player is then called for the first time in Control.update and passed any keys that got pressed as well as the rect area of the display surface.
  3. Player update .... This is where I can intuitively understand what's happening but would like to really understand the code, because I don't yet.
    1. The CONTROLS dict is iterated
    2. "if any(keys[key] for key in control"... So keys = pg.key.get_pressed(). If anything in the () is True, execute the indented code. I can't put into words what the items in () are exactly doing, can you elaborate?
    3. If the above is true, assign the control (a direction string) to direct.
    4. Increment player.rect.x and player.rect.y by the values assigned to the appropriately named item in the DIRECT_DICT dict multiplied by the speed appropriate for the direction. I find this to be brilliant btw :)
    5. The player sprite rect is moved within the screen_rect. This is the first time I've seen this method. Does this prevent checking the boundaries of the display area, making sure the movement is always within that boundary?
  4. The Player object is called one last time in Control.draw: Player.draw(self.screen). Player.draw blits the player onto the display surface.

Typing that out really helped me clarify a few things. I guess all I am really left with is 3b, I can't coherently parse that, and 3e which I think I get the reason for but want to be sure.
Last edited by Mekire on Wed Apr 09, 2014 4:38 am, edited 1 time in total.
Reason: Edited the list a bit for format.
User avatar
flashdamage
 
Posts: 9
Joined: Sun Apr 06, 2014 9:59 pm
Location: Colorado Springs

Re: Scrolling objects don't move smoothly or as expected /py

Postby Mekire » Wed Apr 09, 2014 4:18 am

Pretty accurate for the most part.

For 3b, first let's look at the simpler situation where there is a one to one relationship between controls and results (one key per direction). This makes our CONTROL dictionary reduce to this:
Code: Select all
CONTROLS = {pg.K_LEFT : "LEFT",
            pg.K_RIGHT : "RIGHT",
            pg.K_UP : "UP",
            pg.K_DOWN : "DOWN"}
And by extension simplifies our player update function to this:
Code: Select all
def update(self, keys, screen_rect):
    for key in CONTROLS:
        if keys[key]:
            direct = CONTROLS[key]
            self.rect.x += DIRECT_DICT[direct][0]*self.speed[direct]
            self.rect.y += DIRECT_DICT[direct][1]*self.speed[direct]
    self.rect.clamp_ip(screen_rect)
That should be plain enough. We look at all the CONTROL keys and if they are pressed, we adjust the player's rect appropriately.

It gets a little trickier when we have multiple keys per result. We need to change the rect if any of the keys are held that correspond to a single result. We need to make sure we don't double dip too (i.e. don't let the player get double speed by holding 'left' and 'a' at the same time). We do this by using any.

Let's take a closer look:
Code: Select all
def update(self, keys, screen_rect):
    for control in CONTROLS:
        if any(keys[key] for key in control):
            direct = CONTROLS[control]
            self.rect.x += DIRECT_DICT[direct][0]*self.speed[direct]
            self.rect.y += DIRECT_DICT[direct][1]*self.speed[direct]
    self.rect.clamp_ip(screen_rect)
The first line:
Code: Select all
for control in CONTROLS
assigns the tuple of keys associated with a single direction to control each loop. So on the first iteration control could be assigned:
Code: Select all
(pg.K_LEFT, pg.K_a)
Then you can see our next line expands to:
Code: Select all
if any(pg.key.get_pressed()[key] for key in (pg.K_LEFT, pg.K_a)):
Written more verbosely as:
Code: Select all
for key in (pg.K_LEFT, pg.K_a):
    pressed_keys = pg.key.get_pressed()
    if pressed_keys[key]:
        #adjust player's position
        break
So now, if either of these (or both) are pressed, we update the player's rect. Pressing both does not result in double dipping.

Hopefully that cleared that up (or perhaps made it worse).

3e is much easier to understand:
pygame.Rect.clamp and its inplace counterpart pygame.Rect.clamp_ip take the rect they are called with and return/alter it so that it is inside the bounding rect argument. If for whatever reason the calling rect is too large to fit inside the bounding rect, the returned\altered rect will be centered on the bounding rect. It is a simple way to keep something from moving off screen, though it does have its limitations (if you for instance need to know which edge of the screen the actor hit).

I'll leave it at that for now; I'll add more later if I think of anything (currently on terrible work computer).
-Mek
User avatar
Mekire
 
Posts: 1149
Joined: Thu Feb 07, 2013 11:33 pm
Location: Asakusa, Japan

Re: Scrolling objects don't move smoothly or as expected /py

Postby flashdamage » Wed Apr 09, 2014 5:42 pm

Fantastic! I understand that. Thanks for explaining it. I'm going to continue working from your rewrite and start adding new features. I'm also going to use this as a basis for rewriting the actual game that's being built from this prototype. Incidentally, I also downloaded those examples from your GitHub and will be poring over them for a while.

The next step, as I previously mentioned, was to work on player movement. I'm trying to come up with something more dynamic than what is currently in the code. I may make a new post asking for ideas. I want the player to feel more fluid in it's movement than it currently is.
User avatar
flashdamage
 
Posts: 9
Joined: Sun Apr 06, 2014 9:59 pm
Location: Colorado Springs


Return to Game Development

Who is online

Users browsing this forum: W3C [Linkcheck] and 1 guest