2
\$\begingroup\$

Hello im trying to learn more about smoother movement in games. I have created a little program to hopefully help me understand this. As far as i can tell i am suppose to track time, and update positions accordingly. However, for some reason, this concept is very hard for me to understand. I am using python and pygame at the moment for my games, and in none of them i have been able to come up with the smoothness i wanted. Ive also tried several games off of the pygame website and so far none of them have had the smoothness i prefer.

like i said, i understand you have to track time, and update positions accordingly instead of just +1'ing location each frame/iteration. By doing it the 'wrong way' as far as i understand then it doesnt neccesarily sync up with the monitor thus creating the sense of jittery motion, even at high FPS.

i have read a few articles on the subject of time steps, however i have been unable to fully understand the concept and incorporate it in my game. so i would like to ask, if someone can explain how to implement a timestep and create smooth motion. i am not going to incorporate physics in my first games, so the accuracy of the timer does not matter too much, what is most important right now is a game that can run as smooth as a proffesional game. i am sure a revisit if not simplified explanation of timestepping, and even how to do it in a simple way with python will be of benefit to any programmer hoping to use pygame/python for his games.

Anyway this is the program i was able to write, that doesent have smooth motion. Perhaps someone can modify it, so we can get a working example with time stepping and see how it improves the smoothness. PS. you need python 2.7 and a python 2.7 pygame compatible version to run the program/game

http://bpaste.net/show/qmE362O29sq6GPGBK3fg/

\$\endgroup\$
1
  • 1
    \$\begingroup\$ I don't see anything in that code that would make the movement less than perfectly smooth. But try taking out the clock.tick() call - it may be aliasing badly with your monitor's refresh rate. \$\endgroup\$
    – Kylotan
    Jan 28, 2013 at 1:21

4 Answers 4

8
\$\begingroup\$

First we need to clarify what "smooth movement" is.

Let's first talk about smooth movement in a pixelated space.

Not taking into account motion blur and sub-pixel movement, the smoothest amount of movement you can have on a pixel screen is one pixel. So your code

pygame.draw.circle(SCREEN, (255, 255, 255), (newx, SCREENRECT.centery), 30)
pygame.display.update()
newx+= 1    

actually procudes the smoothest movement in space possible.

Your code produces a smooth movement of roughly 60 pixels per second because of the clock.tick(60) call. This is smooth, but not very fast.

To get fast movement, you now could increase the amount of pixels your image jumps per step by using newx+= 2 or newx+= 5. But this, in line with the above definition, will no longer produce smooth movement. Why? Because you move more than 1px in one step, which we percieve as stuttering.

So, what can be done? If we want fast movement, but can only get it smooth by moving one pixel at a time, we need to move 1px at a time more often per second. And that means: increasing the renderings per second, or, more common, frames per second.

(Note that we have not yet talked about timesteps here at all.)

So as a first step, decrease the waiting time between frames (Clock.tick() essentially is a pause function with dynamic duration) by increasing the argument. Try clock.tick(120) or clock.tick(360).

At some point you will notice that the perceived speed of the motion does no longer increase. This is because you reach the limit of how long your rendering actually takes to compute. At this point, optimization has to happen if the movement is still to slow.

Your drawing routine is inefficient for various reasons:

  • It clears the whole screen to black every frame with SCREEN.fill((0, 0, 0)), although technically you would just neet to clear the area where the circle is.

  • You compute and draw a circle in every frame using pygame.draw.circle(). It would be more efficient to pre-render the circle to a surface an blit that surface to the background.

These would be starting points to get your simple animation smooth and fast. You can optimize further by abandoning Pygame Surfaces and switching to OpenGL. But it is important to note that there is always a limit to the speed of smooth, i.e. 1px movement. Most of the time the practical limit is the monitor refresh rate. If your monitor updates the application's display from the frame buffer memory, say, at 90 times per second (90 Hz), then 90 px / s is the most of smooth movement in space that you can get.

Now you were asking about timesteps, so let's talk about smooth movement in time.

We perceive an object moving smoothly if it moves by equal amounts of space in equal amounts of time. If you have tuned up the FPS in the above example to 360, you might have noticed that the circle stutters and moves irregularily. This is because at some frames Pygame manages to draw everything in less than 1/360 s, and in some frames not. This means that some times the circle does move 360px in one second, and some times the circle takes longer than one second wall clock time to move 360 px. This is because rendering time is not guaranteed. And this is where timesteps come in.

The principle of timestepping is to keep the movement in sync with the wall clock time. That means, instead of blindly moving a fixed amount of pixels per frame, you measure the time your frames take, and adjust the amount of movement (and possibly the time to pause) accordingly. The answer of Yos233 shows one way to do this using the return value of clock.tick().

Timestepping requires that there is a target speed of your animation. Say an object shall move 50 px per second. The basic procedure is

  1. Draw the object.

  2. Wait until 1/50 s has passed.

  3. Update position by 1 px.

  4. Repeat.

The critical point is when in 2. you see that actually more than 1/50 s has already passed. Here, to keep the movement smooth in time, you have to quantify how much time over 1/50 s has passed, and add to the amount of space to move accordingly.

Note that in this case you inevitably will have to move more than 1px to stay in sync; this means that your movement will be smooth in time, but no longer smooth in space.

To sum up:

  1. 1px movement is the smoothest you can get, and it is limited by the frame rate your whole setup can achieve.

  2. Timestepping will smooth out your movement over time, but will actually increase jumps and stutter if the target speed can not be met.

For further reading on timesteps, check out the excellent article Fix Your Timestep!.

\$\endgroup\$
2
  • \$\begingroup\$ very informative and very well put. +1 \$\endgroup\$ Feb 6, 2013 at 9:08
  • \$\begingroup\$ Great, great answer. \$\endgroup\$
    – miracle2k
    Apr 21, 2018 at 12:38
4
\$\begingroup\$

clock.tick() returns a float in milliseconds since the last call. This is often reffered to as the timedelta.

This means you can do things like this:

while running:
    timedelta = clock.tick(60)
    timedelta /= 1000 # Convert milliseconds to seconds
    pygame.draw.circle(SCREEN, (255, 255, 255), (int(newx), SCREENRECT.centery), 30)
    newx += 100*timedelta

newx will now increase by 100 per second.

\$\endgroup\$
3
  • \$\begingroup\$ hi. its me again. i tried the code and the circle stayed stationary. first i tried dividing by 1000.0 to get a float, then converted it into a int using int(). BUT that rounds it down to zero, which means the circle will not move at all \$\endgroup\$ Jan 29, 2013 at 19:46
  • \$\begingroup\$ That was an interesting problem. I thought pygame would take floats (it does for blit, but not draw). What I did was put the int() around the newx inside the draw.circle method, so that while draw.circle received an int, newx was still a float. Have edited into my answer above. \$\endgroup\$
    – Yos233
    Jan 30, 2013 at 2:35
  • \$\begingroup\$ thanks for your help. i still had to put .0 behind the /1000 in order to get a float. putting an int() aorund newx in the draw function made the circle move. however it was still jittering \$\endgroup\$ Feb 3, 2013 at 13:16
0
\$\begingroup\$

I'm not sure exactly how python works, but i work with physics games that deal with this stuff.

Position: this is the position on the screen. EX: the unit is meters.

Velocity: this is the speed, how much it moves over time. EX: The unit is meters / sec.

To get the next position, you can use velocity, which will give you the distance traveled over a time period passed.

Position = Velocity * (Change in time)

Now to get a smoother speed you need to bring time into the equation. This can be achived if python has a function such as get Time() or something.

Position = Velocity * ( CurrentTime - LastTimeSinceRender)

Basically, don't do this:

newX++;

that isn't good for any physics simulation. Always bring the timestep, which relates position to velocity.

\$\endgroup\$
0
\$\begingroup\$

The problem is in your pygame usage: you're re-creating the circle on every frame. Pygame works best if you create every image only once, on its own Surface (think of it as a virtual canvas), and then in each frame you blit that surface's image onto the screen (which is also a surface). Think of blit as an ultra-fast copy from surface to surface.

Here's an alternate version of your code (I've also changed the ordering of a few steps: to me, it seems more to logical to check for input first, then update the objects position, draw, and finally tick the clock):

import pygame, sys, random

pygame.init()

## Functions and classes

## Variables

clock = pygame.time.Clock()
SCREEN = pygame.display.set_mode(pygame.display.list_modes()[-1])

radius = 50

# create the surface that will contain the circle image.
# circle will be drawn on its center, so surface width and height is 2*radius
circle = pygame.Surface((2*radius, 2*radius))

# Let's also paint the surface GREEN so you see its corners behind the circle
circle.fill((0, 255, 0))

# draw the circle on the surface instead of screen
pygame.draw.circle(circle, (255, 255, 255), (radius, radius), radius)

# Last but not the least, create a companion rect, based on surface's size
# A rect is just a tool to define width, height and position at the same time
rect = circle.get_rect()  

# rect now has same width and height as the surface,
# It's positioned at (0, 0) so let's move it to desired initial position

rect.x = 0  # instead of SCRENRECT.centerx, so it stays longer on screen
rect.y = SCREEN.get_rect().centery

# The surface (and our friend the rect) is set up and ready to be used on screen


## Main loop

running = True

while running:
    #Quit event etc
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
            running= False

        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                pygame.quit()
                sys.exit()
                running= False

    rect.x += 1

    SCREEN.fill((0, 0, 0))

    # and now we blit the pre-drawn circle onto the screen,
    # using rect as coordinates
    SCREEN.blit(circle, rect)

    pygame.display.update()
    clock.tick(60)
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .