Simulating Momentum in a Game Engine - python

Just for fun, I've been working on a simple platformer engine in Pygame over break. Today I tried to add some momentum code so that the player doesn't instantly stop, but I've been running into a problem. My momentum code does not slow the player down at a constant rate over all possible FPS rates. Here is my code:
def getXDisplacement(self):
pressed = pygame.key.get_pressed()
dt = self.xTimer.elapsedTime()
self.momentum = self.momentum * ( (1-self.momentumDecayRate) ** dt)
self.xDisplacement = ((self.rightKey.ifActive(pressed)-self.leftKey.ifActive(pressed)) + self.momentum)
self.momentum = self.xDisplacement
self.xDisplacement *= dt
self.xDisplacement = numpy.clip(self.xDisplacement, -700*dt, 700*dt)
self.xTimer.reset()
To slow down the FPS, I'll add a print() function to the top of the getXDisplacement() function. I also comment out the numpy.clip() function towards the bottom to see how total speed is affected. With a slower FPS rate, the player accelerates much more slowly and doesn't quite reach the same speed as at higher FPS rates. I can't imagine why an exponential decay function wouldn't work here. Am I using it improperly, or is the problem more complicated than I think? For reference, here is the link to the full program: https://github.com/2Amateurs/SimplePygamePlatformer.

You can use the pygame clock to limit the maximum FPS:
clock = pygame.time.Clock()
# in your game loop
while True:
clock.tick(FPS)
If you're certain your game won't run slower than say 30 FPS, this is already sufficient. If not, you can use the clock to calculate the delta time between the last and this frame.
clock = pygame.time.Clock()
# in your game loop
while True:
delta_time = clock.tick_busy_loop()
getXDisplacement(delta_time)
def getXDisplacement(self, dt):
pressed = pygame.key.get_pressed()
self.momentum = self.momentum * ( (1-self.momentumDecayRate) ** dt)
self.xDisplacement = ((self.rightKey.ifActive(pressed)-self.leftKey.ifActive(pressed)) + self.momentum)
self.momentum = self.xDisplacement
self.xDisplacement *= dt
self.xDisplacement = numpy.clip(self.xDisplacement, -700*dt, 700*dt)
tick_busy_loop is not the only way, also take a look at Clock.get_time.

Related

How do I make an object accelerate a chosen amount per real time second, instead of per frame?

As part of a Pygame physics engine I'm working on, I would like to have it so each particle accelerates by 9.81 pixels per real time second, instead of every single frame, which is the way it currently works:
self.y_acceleration = 9.81
self.y_velocity += self.y_acceleration
self.y += self.y_velocity * delta_time
I already have use this code to create a timer:
current_time = time.time()
delta_time = current_time - previous_time
previous_time = current_time
timer += delta_time
You need to calculate the motion per frame as a function of the frame rate.
pygame.time.Clock.tick returns the number of milliseconds since the last call. If you call it in the application loop, this is the number of milliseconds that have elapsed since the last frame. Multiply the object speed by the elapsed time per frame to get constant motion regardless of FPS.
Define the distance in pixels that the player should move per second (pixels_per_second) when self.y_velocity is 1. Then calculate the distance per frame in the application loop:
clock = pygame.time.Clock()
pixels_per_second = 500 # 500 pixels/second - just for example
run = True
while run:
# [...]
delta_time = clock.tick(100)
pixel_per_frame = pixels_per_second * delta_time / 1000
self.y += self.y_velocity * pixel_per_frame
# [...]

Smooth Jump over different FPS

I've created a game, but I have problems controlling the jump of the player. This code is a simplified version only showing the jump problem. Keep in mind that in the game itself the FPS may vary in mid jump. I've separated the jump code so it's the easier to identify, and also with the UP/DOWN arrows you can change the FPS to make tests.
Problem
The higher the FPS the smaller the jumps are, and the lower the FPS are the higher the jumps.
Expected Result
The jump reach the same height over many different FPS. Example: 30 and 120 FPS
Code
import pygame
Screen = pygame.display.set_mode((250,300))
Clock = pygame.time.Clock()
X = 50; Y = 250
FPS = 60; Current_FPS = FPS
Move = 480 / Current_FPS; Dir = "Up"
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()
if pygame.key.get_pressed()[pygame.K_UP]: FPS += 10 / Current_FPS
elif pygame.key.get_pressed()[pygame.K_DOWN] and FPS > 2: FPS -= 10 / Current_FPS
pygame.display.set_caption("FPS: "+str(int(FPS)))
Screen.fill((255,255,255))
X += 120 / Current_FPS
#---------------- JUMP CODE ---------------------#
if Dir == "Up":
if Move <= 0.0: Dir = "Down"
else:
Move -= 10 / Current_FPS
Y -= Move
else:
#RESET \/
if Y >= 250:
Dir = "Up"
X = 50; Y = 250
Move = 480 / Current_FPS; Dir = "Up"
#RESET /\
else:
Move += 120 / Current_FPS
Y += Move
#--------------------------------------------------#
pygame.draw.circle(Screen,(0,0,0),(int(X),int(Y)),5)
pygame.display.update()
Current_FPS = 1000.0 / Clock.tick_busy_loop(FPS)
You should set your initial jump velocity to be independent of frame rate, i.e:
Move = 480
Note that when you update the velocity (or in your case, it looks, speed) you do need to divide by the frame rate, since you are essentially multiplying by the time interval: v ~ u + a*dt. The same applies when updating the position, so this should be Y += Move / Current_FPS.
There are a couple of other things worth mentioning. Why do you track the direction variable? Why not just have your variable Move be the velocity. That way you would just need: Y += Move / Current_FPS and have positive/negative values indicate the direction. Also, your gravity is currently a lot stronger on the way down than on the way up, but this could be entirely deliberate!

setting the pace of a character

I want to define a function to set the speed to characters in my game, I understand there is a formula for this:
rate * time = distance
1. Establish a rate of movement in whatever units of measure you want (such as pixels per millisecond).
2. Get the time since the last update that has passed (elapsed time).
3. Establish the direction of movement .
I have tryied to define a method which implements this:
def speed(self, speed):
clock = pygame.time.Clock()
milliseconds = clock.tick(60) # milliseconds passed since last frame
seconds = milliseconds / 1000.0
speed = seconds * (self.dx+self.dy)
But when I call this method to change the speed of my character, nothing happens.
Any suggestions?
You need to use the self keyword so set a class attribute in a method. But in your method you use speed as a parameter which I don't believe is how you want to use it.
def speed(self, speed):
clock = pygame.time.Clock()
milliseconds = clock.tick(60) # milliseconds passed since last frame
seconds = milliseconds / 1000.0
speed = seconds * (self.dx+self.dy)
This method takes a speed and then sets it equal to something and then it goes out of of scope with out making a change. To set an objects speed attribute with a method you would:
def set_speed(self):
clock = pygame.time.Clock()
milliseconds = clock.tick(60) # milliseconds passed since last frame
seconds = milliseconds / 1000.0
self.speed = seconds * (self.dx+self.dy)
self is the way to reference an object from inside one of it's methods. self.speed = 10 equates to doing my_object.speed = 10 outside the method. Using this method:
class Character:
def __init__():
self.speed = 0 # your character class needs to have a speed attribute somewhere
hero = Character() # creates an object that represents your character
hero.set_speed() # call the method to set the speed attribute of the character class
I don't know exactly how you have everything set up, but here is a rough example using 2-tuples (x, y) for position and speed (I think you might have self.x, self.y for my self.position and self.dx, self.dy for my self.speed, but the principles are the same):
def Character(object):
def __init__(self, position):
self.position = position # e.g. (200, 100)
self.speed = (0, 0) # start stationary
def move(self, elapsed):
"""Update Character position based on speed and elapsed time."""
# distance = time * rate
# position = old position + distance
self.position = (int(self.position[0] + (elapsed * self.speed[0])),
int(self.position[1] + (elapsed * self.speed[1])))
Note that you don't need to work out how far a Character should travel for a given speed when you set self.speed, just when you try to move the Character. You can directly access character.speed; having a "setter" method (e.g. set_speed(self, speed)) is unpythonic.
Now you could call this:
hero = Character(start_loc)
framerate = 60
clock = pygame.time.Clock()
...
while True: # main gameplay loop
elapsed = clock.tick(60) # update world time and get elapsed
...
hero.speed = (2, 0) # travelling horizontally in the positive x-direction
hero.move(elapsed) # update the hero's position
Note that speed here would be in units of pixels per millisecond.

Simple 2D gravity with pygame; Value Guidelines?

I am trying to simulate gravity in a simple 2D window using pygame. It's very simple stuff (a dot rising and falling again) and I understand the mechanics, i.e. speed as a vector and the y portion being consistently diminished by a value g representing gravity during each run through the mainloop and the subsequent update of the position of the dot.
It's all working fine, but I am having trouble choosing the correct values to be inserted. It is all trial and error at the moment. Is there a good rule of thumb on how to determine which numbers to use, to achieve a semi-realistic looking trajectory?
I inserted a minimal example below, with the following values important to my question:
window = (640, 480) # pixels
initial_speed = 20 # pixels per update along the y axis
gravity = 0.4 # deduction on initial_speed per update
Now, why do these numbers happen to make the illusion work? I tried first to use formulae I learnt in physics class years and years ago, but, with or without unit conversions, the simulation was just not right. Most of the time, I didn't even see the ball, yet the above values, found through trial and error work.
Thanks for all your help in advance. If you need more information, just post a comment and I will try to oblige.
Here is the minimal example. Note the vector2D library was conveniently borrowed from the pygame website (follow this link)
#!/usr/bin/env python
import pygame
from pygame.locals import *
from vector2D import Vec2d
pygame.init()
GRAVITY = 0.4
class Dot(pygame.sprite.Sprite):
def __init__(self, screen, img_file, init_position, init_direction, speed):
pygame.sprite.Sprite.__init__(self)
self.screen = screen
self.speed = Vec2d(speed)
self.base_image = pygame.image.load(img_file).convert_alpha()
self.image = self.base_image
# A vector specifying the Dot's position on the screen
self.pos = Vec2d(init_position)
# The direction is a normalized vector
self.direction = Vec2d(init_direction).normalized()
def blitme(self):
""" Blit the Dot onto the screen that was provided in
the constructor.
"""
self.screen.blit(self.image, self.pos)
def update(self):
self.speed.y -= GRAVITY
displacement = Vec2d(
self.direction.x * self.speed.x,
self.direction.y * self.speed.y
)
self.pos += displacement
def main():
DIMENSION = SCREEN_WIDTH, SCREEN_HEIGHT = 640, 480
BG_COLOUR = 0,0,0
# Creating the screen
window = screen = pygame.display.set_mode(
(SCREEN_WIDTH, SCREEN_HEIGHT), 0, 32)
screen = pygame.display.get_surface()
clock = pygame.time.Clock()
dot = Dot(screen, "my/path/to/dot.jpg", (180, SCREEN_HEIGHT),
(0, -1), (0, 20))
mainloop = True
while mainloop:
# Limit frame speed to 50 FPS
time_passed = clock.tick(50)
for event in pygame.event.get():
if event.type == pygame.QUIT:
mainloop = False
# Redraw the background
screen.fill(BG_COLOUR)
dot.update()
dot.blitme()
pygame.display.flip()
if __name__ == '__main__':
main()
def update(self):
self.speed.y -= GRAVITY
displacement = Vec2d(
self.direction.x * self.speed.x,
self.direction.y * self.speed.y
)
self.pos += displacement
You call Dot.update() 50 times every second. The equation you're using,
delta v_y = - g #* (1 second)
represents the change in velocity every second, but it gets called 50 times every second. This means your velocities will lose 50*GRAVITY m/s ever second and this is why you are forced to make your gravity so weak.
So, I would recommend adding time_passed as an argument for Dot.update, and change the speed changing statement to
def update(self, time_passed):
self.speed.y -= GRAVITY * time_passed/1000. #time converted from millisecond to seconds
This will make the units more intuitive. Switch GRAVITY back to 10 and use realistic velocities.
EDIT:
Additionally, the displacement vector also needs to contain time, or else the displacement will depend on FPS. If you have x frames per second, then each update occurs after t = 1000/x milliseconds. This is the amount of time Dot spends "moving" in each update. Think about how we approximate Dot's motion; it gets an update, calculates its new velocity and travels at that speed for t seconds. However, in your implementation, the displacement vector is independent of time. Change the update method to be something like:
def update(self, time):
time /= 1000.
self.speed.y -= GRAVITY * time
displacement = Vec2d(
self.direction.x * self.speed.x * time,
self.direction.y * self.speed.y * time
)
self.pos += displacement
Now, according to kinematics, we'll get something like
max height = v0**2 / 2g
so initial velocities of 10-20m/s will only produce max heights of 5-20m. If you don't want to work with velocities of 50-200m/s, then you could add a scaling factor in the displacement vector, something like
displacement = Vec2d(
self.direction.x * self.speed.x * time * scaling_factor,
self.direction.y * self.speed.y * time * scaling_factor
)

Pygame clock function returns a strange value

Basically, I've wanted to get back into Python, so I decided to make a small game in pygame, where there is a bouncy ball which you need to keep bouncing in the air. The problem is that when I use the functions clock.tick() and clock.get_time(), get_time should return the time passed in milliseconds, but it passes time in milliseconds*10.
My code:
GRAVITY = 10
def move(self, delta):
self.x+= (self.vx * delta)
self.y+= (self.vy * delta)
def speed(self, delta):
self.vy += (GRAVITY * delta)
clock.tick()
while True:
clock.tick()
delta = (clock.get_time() / 100) #should be /1000
ball.move(delta)
ball.speed(delta)
It works smoothly like in real world when its /100, but works really slow then its /1000.
I think part of the problem might be truncation from dividing by "1000" instead of "1000.0". You can verify clock.tick is working with this:
import pygame
pygame.init()
i = 0
clock = pygame.time.Clock()
while i < 10: # Just run a few cycles to check the output
res = clock.tick(1) # Caps framerate to 1 fps
print res # This should print out 1000
i += 1
Note that clock.tick already returns the delta and is normally used to cap the framerate. If left uncapped, you might have a really high FPS, giving a small delta and, when divided by an int, isn't larger than 1 so you rarely get a number at all.
Also, make sure you have all your units right, that you really want to be converting to seconds

Categories

Resources