Skip to content
Advertisement

pygame sprite move faster if window is smaller

my character sprite moves faster if my game is in window mode. to set the velocity i used ROOTwidth, in theory the velocity should be scaled… this is my code (simplified)

#MAIN CODE
#ROOT dimension don't change (window can't be resized while playing,
#only in main menu function where ROOTwidth, ROOTheight are obtained) 
ROOTwidth, ROOTheight = pygame.display.get_surface().get_size()

velocity = ROOTheight/450
playertopx = ROOTwidth/2.2
playertopy = ROOTwidth/2

playermovement = PlayerMovement(playertopx, playertopy)

while True:
   key = pygame.key.get_pressed()
   if key[pygame.K_w]:
      playermovement.human_moveup(velocity)

#PLAYER MOVEMENT CLASS
import pygame

class PlayerMovement:
    #init
    def __init__(self, playertopx, playertopy):
        self.x = playertopx
        self.y = playertopy
    
    #movement
    def human_moveup(self, velocity):
        self.y -= velocity
#MAIN CODE
   ROOT.blit(playermovement.spritesheet_human, (playermovement.x, playermovement.y), (0, 50, 25, 18))

I don’t know what to do… for every element in my game, using ROOT dimensions works fine, only in velocity I have problems

Advertisement

Answer

I guess it might be a case of your main event loop’s looping speed being dependent on the time it takes to draw on the window and that main event’s code not accounting for that.

Execution speed

Assuming you have this code as your main event loop:

while NotExited:
  doGameLogic() # Your velocity computations and other stuff
  drawOnWindow() # Filling entire window with background, drawing sprites over, refreshing it, etc

Now, imagine doGameLogic() always takes 1ms (0.001 seconds) of time, and drawOnWindow() always takes 50ms. While this loop is running, therefore, the loop will take up 51 milliseconds total, and hence doGameLogic() will be called once every 51ms.

Then you perform your velocity computation in there. Let’s, for simplicity, say you do playermovement.x += 5 in there every time.

As a result, your player’s X coordinate is increased by 5 units every 51 milliseconds. That amounts to an increase of about 98 units in one second.


Variance in execution speed

Now imagine that drawOnWindow() starts taking 20ms of time instead. Then the loop takes 21ms of time total to run, which causes doGameLogic() to run every 21ms aswell. In that case the X coordinate increases by 5 units every 21 milliseconds instead, amounting to increasing by 238 units every second.

That is way faster than the previous 98 units every second. Because drawing takes less time now, your character ends up moving way faster.

This is what I assume is happening in your case. As you make the window smaller, drawing calls (like drawing a background/filling it with a color) take less time as there’s less pixels to draw on, and therefore change how long drawOnWindow() takes time, and therefore the frequency at which doGameLogic() is run changes.


Fixing

There are many different ways to fix this. Here are some:

Enforcing loop speed

One of them is to ensure that your loop always takes the exact same amount of time to run regardless of how much time the calls take:

import time

while NotExited:
  startTime = time.time() # Record when the loop was started

  doGameLogic()
  drawOnWindow()

  # Calculate how long did it take the loop to run.
  HowLong = time.time() - startTime 

  # Sleep until this loop takes exactly 0.05 seconds.
  # The "max" call is to ensure we don't try to sleep
  # for a negative value if the loop took longer than that.
  time.sleep(max(0, 0.05-HowLong))

Or alternatively, the library you are using for rendering may allow you to set an upper limit to FPS (frames per second), which can also work to make sure the time it takes to draw is constant.

This method has a disadvantage in that it becomes ineffective if the loop takes longer than the designated time, and restricts how fast your game runs in the opposite case, but it is very easy to implement.

Scaling with speed

Instead of making sure playermovement.x += 5 and the rest of the logic is ran exactly once every 50 milliseconds, you can make sure that it is run with values scaled proportionally to how often it is run, producing the same results.

In other words, running playermovement.x += 5 once every 50ms is fully equivalent to running playermovement.x += 1 once every 10ms: as a result of either, every 50ms the value is increased by 5 units.

We can calculate how long it took to render the last frame, and then adjust the values in the calculations proportionally to that:

import time
# This will store when was the last frame started.
# Initialize with a reasonable value for now.
previousTime = time.time()

while NotExited:
  # Get how long it took to run the loop the last time.
  difference = time.time() - previousTime
  # Get a scale value to adjust for the delay.
  # The faster the game runs, the smaller this value is.
  # If difference is 50ms, this returns 1.
  # If difference is 100ms, this returns 2.
  timeScale = difference / 0.05

  doGameLogic(timeScale)
  drawOnWindow()

  previousTime = time.time()


# ... in the game logic:
def doGameLogic(timeScale): 
    # ...
    # Perform game logic proportionally to the loop speed.
    playermovement.x += 5 * timeScale

This method is more adaptable depending on the speed, but requires to be taken in account whereever time dependent actions like this one are done.

It can also be a source of unique problems: for example, if your game runs very very slowly even for one frame, the time scale value might get disproportionally large, causing playermovement.x to be incremented by 5*100000, teleporting your player character very far away. It can also produce jerky results if the loop speed is unstable, and provide more problems since it is performed with floating point math.

Decoupling logic and rendering

Another more reliable than the other ones but harder to implement way is to decouple doGameLogic() from drawOnWindow(), allowing one to be run independently from the other. This is most often implemented with use of multithreading.

You could make two loops running concurrently: one that runs doGameLogic() on a fixed interval, like 10ms, with the aforementioned “Enforcing loop speed” method, and another one that runs drawOnWindow() as fast as it can to render on the window at any arbitrary speed.

This method also involves questions of interpolation (if drawOnWindow() runs twice as fast as doGameLogic(), you probably don’t want every second time to draw an identical image, but an intermediate one that appears smoother), and threading management (make sure you don’t draw on the window while doGameLogic() is still running, as you might draw an incomplete game state in the middle of processing).

Unfortunately I am not knowledgeable enough to provide an example of code for that, nor I am even sure if that is doable in Python or PyGame.

User contributions licensed under: CC BY-SA
2 People found this is helpful
Advertisement