I'm a newb when it comes to programming, but I'm learned a lot so far and I'm trying to make a very basic RPG.
I want to move my image object oPC with a mouse click. I've been able to accomplish this with the code I'm sharing below, however, no matter where I click on the screen the image takes the same amount of steps/time to get there. For instance, if I click a few inches away from the object it will gradually shift across the screen towards the target location just as fast as if I'd click right off the image.
I've been stuck trying to figure out a way to solve this issue for the last few days. Is there someway to use time for movement as opposed to the steps I've used?
Thanks!
import pygame, sys
import oPC
pygame.init()
WINDOWSIZE = (1000, 800)
BLACK = (0, 0, 0)
screen = pygame.display.set_mode((WINDOWSIZE))
pygame.display.set_caption("Game")
screen.fill(BLACK)
terrain = pygame.image.load("terrain.jpg").convert()
terrainRect = terrain.get_rect()
terrain = pygame.transform.scale(terrain, ((WINDOWSIZE)))
screen.blit(terrain, terrainRect)
oPC = oPC.Player()
oPC.draw(screen)
pygame.display.flip()
running = True
n_steps = 80
while running == True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN:
mlocX, mlocY = pygame.mouse.get_pos()
while mlocX != oPC.rect.x and mlocY != oPC.rect.y:
clock.tick(60)
oPC.update(mlocX, mlocY, n_steps)
if n_steps > 1:
screen.fill(BLACK)
screen.blit(terrain, terrainRect)
n_steps -= 1
oPC.draw(screen)
pygame.display.flip()
n_steps = 80
pygame.quit()
#sys.exit()
import pygame, sys
class Player(object):
def __init__(self):
self.image = pygame.image.load("tipping over s0000.bmp").convert()
self.rect = self.image.get_rect()
self.name = " "
self.stats = [0, 0, 0, 0, 0] #str, int, dex, health, mana
self.admin = False # False = Member / True = Administrator
def draw(self, screen):
self.image = pygame.transform.scale(self.image, (75, 75))
screen.blit(self.image, (self.rect.x, self.rect.y))
def update(self, mlocX, mlocY, n_steps):
self.rect.x += (mlocX - self.rect.x) / n_steps
self.rect.y += (mlocY - self.rect.y) / n_steps
Your design has a few flaws.
You are calling clock.tick() only on the MOUSEBUTTONDOWN event. It should be called on every frame.
Your bliting and display.update should also be done in the loop, not in the event queue.
The player update should also be called in the loop.
You scale your image each time you call draw. I think you may want to do that in the init method only. Since draw should be called repeatedly in the loop.
As for the step counter, I suggest to have a player state, and a step counter there. Something like this:
# for easier state access
def enum(*sequential, **named):
enums = dict(zip(sequential, range(len(sequential))), **named)
return type('Enum', (), enums)
player_states = enum('STATIONARY', 'MOVING')
STEPS = 30
def __init__(self):
self.state = player_states.STATIONARY
self.steps = 0
self.dest = None
def move(self,mlocX,mlocY):
if self.state != player_states.MOVING:
self.state = player_state.MOVING
self.steps = STEPS
self.dest = (mlocX,mlocY)
def update(self):
if self.steps != 0:
self.rect.x += (self.dest[0] - self.rect.x) / STEPS
self.rect.y += (self.dest[1] - self.rect.y) / STEPS
self.steps -= 1
else:
self.state = player_states.STATIONARY
As for you question with steps, you can use physics and the famous distance = velocity * time equation.
The clock.tick() method returns the number of ms passed since the last call to tick().
If you pass this to the update method, you can than change the moving equations to:
def update(self,delta):
self.rect.x += PLAYER_SPEED * direction * delta
Then pick PLAYER_SPEED to something that will suit you.
Related
I had a problem with creating camera in pygame, I assumed code below should work but our player is moving faster than camera and is going out of the window. Somebody know what's the issue?
import pygame, sys
class Player(pygame.sprite.Sprite):
def __init__(self, pos, group):
super().__init__(group)
self.image = pygame.image.load('./chatacters/players/player_one.png').convert_alpha()
self.rect = self.image.get_rect(center=(640, 360))
self.direction = pygame.math.Vector2()
self.speed = 5
# key inputs
def input(self):
keys = pygame.key.get_pressed()
if keys[pygame.K_UP]:
self.direction.y = -1
elif keys[pygame.K_DOWN]:
self.direction.y = 1
else:
self.direction.y = 0
if keys[pygame.K_LEFT]:
self.direction.x = -1
elif keys[pygame.K_RIGHT]:
self.direction.x = 1
else:
self.direction.x = 0
# Moving using inputs
def move(self, speed):
if self.direction.magnitude() != 0:
self.direction = self.direction.normalize()
self.rect.center += self.direction * speed
# updating drawing
def update(self):
self.input()
self.move(self.speed)
class Camera(pygame.sprite.Group):
def __init__(self):
super().__init__()
self.display_surface = pygame.display.get_surface()
self.offset = pygame.math.Vector2()
self.half_w = self.display_surface.get_size()[0] // 2
self.half_h = self.display_surface.get_size()[1] // 2
self.map = pygame.image.load('./map/level_data/level.png').convert_alpha()
self.rect = self.map.get_rect(topleft=(0, 0))
def custom_draw(self, player):
self.offset.x = player.rect.centerx - self.half_w
self.offset.y = player.rect.centery - self.half_h
ground_offset = self.rect.topleft - self.offset
self.display_surface.blit(self.map, ground_offset)
class Game():
def __init__(self):
# Settings
self.WIDTH = 1280
self.HEIGHT = 720
self.FPS = 60
pygame.init()
pygame.display.set_caption('BetterFortinite')
self.screen = pygame.display.set_mode((self.WIDTH, self.HEIGHT))
self.clock = pygame.time.Clock()
self.camera_group = Camera()
self.player = Player((100, 200), self.camera_group)
def game(self):
while True:
self.clock.tick(self.FPS)
self.screen.fill('black')
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
# self.screen.fill("WHITE")
self.camera_group.custom_draw(self.player)
self.player.move(5)
self.player.update()
self.camera_group.draw(self.screen)
# self.camera_group.update()
pygame.display.update()
if __name__ in '__main__':
game = Game()
game.game()
I'm taking the center position of player rect minus half of the width size. Same with height and setting with it my offset. Then I'm setting my ground_offset as cords of topleft screen rect minus offset. What is wrong with this formula?
The problem is not with your formula, but with the code itsself. In the main game loop, you have:
self.player.move(5)
self.player.update()
While Player.update contains:
def update(self):
self.input()
self.move(self.speed)
As you can see, player.move is called twice. This means that the player is moved twice as much as intended and thus twice as fast as the camera, causing both to move at a different speed.
The solution to this problem would be to remove one of the calls of Player.move. I would remove the one in the main game loop as it uses a hardcoded value rather than the Player.speed constant, but it doesn't really matter which one you remove.
I'm trying to create a simple game where u have to dodge enemies(asteroids), with pygame, but now I'm having trouble spawning them and I don't know if i should use lists or other things, or the enemy class(asteroidClass) is enough. The interval between spawning them is pretty simple I just don't how to deal with the spawn part (dealing with this for 3 days).
import pygame
import random
pygame.init()
#images
background = pygame.image.load('#path')
asteroid = pygame.image.load('#path')
display = pygame.display.set_mode((300,500))
FPS = 50
display.blit(background,(0,0))
#player everything is fine
#asteroid
class asteroidClass:
def __init__(self,asteroidX,asteroidY,asteroidVel):
self.x = asteroidX
self.y = asteroidY
self.vel = asteroidVel
def asteroid_advancing(self):
self.y += self.vel
display.blit(asteroid, (self.x, self.y))
def update():
pygame.display.update()
pygame.time.Clock().tick(FPS)
#variables
asteroidX = random.randint(0,250)
asteroidY, asteroidVel = 0, 2
asteroidOutClass = asteroidClass(asteroidX,asteroidY,asteroidVel)
#main loop
run = True
while run:
#should spawn multiple I don't know with what logic
#spawning in the same x
#saw in web they're using lists, maybe i should too?
#when i print i it just do 0123456 like it should, then restart to 0123456, is it wrong? kinda 100%
for i in range(7):
asteroidOutClass.asteroid_advancing() #everytime it's called should spawn and asteroid in random x?
update()
display.blit(background, (0, 0))
A few things:
Create the asteroids before the main loop
Use the init method to randomly set asteroid position and speed
Be sure to call an event method in the main loop to prevent freezing
Here's the updated code. I removed the background calls and used a circle for the asteroid.
import pygame
import random
pygame.init()
#images pygame.load() not in here
display = pygame.display.set_mode((300,500))
FPS = 50
#display.blit(background,(0,0))
#player everything is fine
#asteroid
class asteroidClass:
def __init__(self):
self.x = random.randrange(0, 300)
self.y = 0 # always top
self.velx = random.randrange(1,15)/3
self.vely = random.randrange(1,15)/3
def asteroid_advancing(self):
self.y += self.vely
self.x += self.velx
# wrap around screen
if self.x < 0: self.x=300
if self.x > 300: self.x=0
if self.y < 0: self.y=500
if self.y > 500: self.y=0
pygame.draw.circle(display, (200, 0, 0), (int(self.x), int(self.y)), 20) # draw asteroid
def update():
pygame.display.update()
pygame.time.Clock().tick(FPS)
#variables
# create list of 5 asteroids
roidlist = [asteroidClass() for x in range(7)]
#main loop
run = True
while run:
for event in pygame.event.get(): # required for OS events
if event.type == pygame.QUIT:
pygame.quit()
display.fill((0,0,0))
#should spawn multiple I don't know with what logic
#spawning in the same x
#saw in web they're using lists, maybe i should too?
#when i print i it just do 0123456 like it should, then restart to 0123456, is it wrong? kinda 100%
for r in roidlist:
r.asteroid_advancing() #everytime it's called should spawn and asteroid in random x?
update()
# display.blit(background, (0, 0))
Unable to move player in pygame
So, I was trying to create a simple physics system in pygame. My main goal was to add a Y gravity function that would move the player when activated. I have tried several times to make it work, but I guess I am missing something.
Here is the code:
class Physic:
def __init__(self, color, master, type, sizeX, sizeY, pos=[0,0]):
self.master = master
self.color = color
self.type = type
self.sizeX = sizeX
self.sizeY = sizeY
self.x = pos[0]
self.y = pos[1]
self.moveX = 0
self.moveY = 0
self.create_Object()
def create_Object(self):
if self.type == 'rect':
self.rect()
def rect(self):
return pygame.draw.rect(self.master, self.color, (self.x, self.y, self.sizeX, self.sizeY))
def drag(self):
for event in pygame.event.get():
if event.type == pygame.mouse.get_pressed()[0]:
self.x = pygame.mouse.get_pos()
def add_gravity(self):
self.moveY += 3.2
self.update_pos()
def update_pos(self):
self.y += self.moveY
self.x += self.moveX
In the main script I put this:
def game():
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
screen.fill(WHITE)
player = object.Physic(BLUE, screen, 'rect', 50, 50, [POS[0], POS[1]])
player.add_gravity()
# platform = object.rect(screen, RED, [30, 30], 100, 30)
# physics.add_collider(player, platform, POS[0])
pygame.display.update()
game()
Do you know what I am missing?
Your big problem is that you are recreating the player every pass inside the main loop and so it looks like it is frozen in place.
You also need to have a limit on the frame rate so that you control the speed of the game and therefor can properly set the acceleration per frame.
There are a some other minor things that needed to be fixed to run this. I tried to change the minimum possible to run it, since the point was to fix the error rather than rewrite it on you. Obviously I had to add some wrapper code around it
Try this slightly adjusted version:
#!/usr/bin/env python
import traceback
import pygame
import sys
FRAME_RATE = 60
GRAVITY = 32
SCREEN_SIZE = (600, 800)
WHITE = pygame.Color("white")
BLUE = pygame.Color("blue")
POS = (100, 600)
class Physic:
def __init__(self, color, master, type, sizeX, sizeY, pos=(0,0)):
self.master = master
self.color = color
self.type = type
self.sizeX = sizeX
self.sizeY = sizeY
self.x = pos[0]
self.y = pos[1]
self.moveX = 0
self.moveY = 0
self.create_Object()
def create_Object(self):
if self.type == 'rect':
self.draw()
def draw(self):
return pygame.draw.rect(self.master, self.color, (self.x, self.y, self.sizeX, self.sizeY))
def drag(self):
for event in pygame.event.get():
#if event.type == pygame.mouse.get_pressed()[0]: <--- remove this
if event.type == pygame.MOUSEBUTTONDOWN:
self.x = pygame.mouse.get_pos()
def add_gravity(self):
self.moveY += GRAVITY / FRAME_RATE
self.update_pos()
def update_pos(self):
self.y += self.moveY
self.x += self.moveX
def game():
pygame.init()
screen = pygame.display.set_mode(SCREEN_SIZE)
clock = pygame.time.Clock()
player = Physic(BLUE, screen, 'rect', 50, 50, POS)
# I added this to illustrate the gravity better ... going up and down
player.moveY = -25
player.moveX = 2
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
return
screen.fill(WHITE)
player.add_gravity()
player.draw()
# platform = object.rect(screen, RED, [30, 30], 100, 30)
# physics.add_collider(player, platform, POS[0])
pygame.display.update()
clock.tick(FRAME_RATE)
def main():
try:
game()
except Exception as ex:
print(traceback.format_exc())
raise
finally:
# game exit cleanup stuff
pygame.quit()
if __name__ == '__main__':
main()
An issue that I want to point out though it is not affecting this code here. You should not use an immutable object (like a list) as an default when defining an optional/named argument in a method. I.e. in the Physic __init__() I changed pos=[0,0] to pos=(0,0). Not a big deal here, but can cause really odd bugs if you had assigned it to a var, then tried to change it. It will have effects on other instances of the object because they actually share the default initialization object and if it gets modified by one of them it happens in all of them!
In simple words my spawning system doesnt work
I am making a dodge the blocks game and I want 1 out of 5 spawnpoints to be empty so that the player can dodge the others. Of course that spawnpoint is random. I tried changing the numbers and looking at some of my older and similar games but nothing works
EDIT: I posted the other 2 files so that you can run the game. I dont think that they are part of the problem since they don't contain anything related to the spawning process
Here my main file:
import pygame as pg
import random
from sprites import *
from game_options import *
class Game:
def __init__(self):
pg.init()
pg.mixer.init()
self.screen = pg.display.set_mode((WIDTH, HEIGHT))
pg.display.set_caption("BRUH")
self.clock = pg.time.Clock()
self.running = True
def new(self):
self.SPAWNENEMIES = 1
self.my_event = pg.event.Event(self.SPAWNENEMIES)
pg.event.post(self.my_event)
pg.time.set_timer(self.SPAWNENEMIES, 3000)
self.spawnpoint1 = 20, -80
self.spawnpoint2 = 140, -80
self.spawnpoint3 = 260, -80
self.spawnpoint4 = 380, -80
self.spawnpoint5 = 500, -80
self.spawnpoints = (self.spawnpoint1,self.spawnpoint2,self.spawnpoint3,self.spawnpoint4,self.spawnpoint5)
self.all_sprites = pg.sprite.Group()
self.blocks = pg.sprite.Group()
self.player = Player()
self.all_sprites.add(self.player)
self.all_sprites.add(self.blocks)
g.run()
def run(self):
self.running = True
while self.running:
self.clock.tick(FPS)
self.events()
self.update()
self.draw()
def update(self):
self.all_sprites.update()
def events(self):
for event in pg.event.get():
if event.type == pg.QUIT:
self.running = False
if event.type == self.SPAWNENEMIES:
num = random.randint(0,len(self.spawnpoints))
#print(num)
for i in range(5):
if num != i:
print(i)
self.block = Block(self.spawnpoints[i])
self.blocks.add(self.block)
self.all_sprites.add(self.blocks)
dead_blocks = pg.sprite.spritecollide(self.player, self.blocks, True)
# if dead_blocks:
# self.running = False
def draw(self):
self.screen.fill(MY_RED)
self.all_sprites.draw(self.screen)
pg.display.flip()
g = Game()
while g.running:
g.new()
g.quit()
Here is game_options.py:
WIDTH = 580
HEIGHT = 800
FPS = 30
# Simple colors
WHITE = (255,255,255)
BLACK = (0,0,0)
GREEN = (0,255,0)
BLUE = (0,0,255)
RED = (255,0,0)
MY_RED = (255, 67, 67)
GREY = (108,106,106)
and the sprites.py
import pygame as pg
from game_options import *
class Player(pg.sprite.Sprite):
def __init__(self):
pg.sprite.Sprite.__init__(self)
self.image = pg.Surface((70,70))
self.image.fill(WHITE)
self.rect = self.image.get_rect()
self.rect.center = (WIDTH/2, HEIGHT - 100)
self.speedx = 0
def update(self):
keystate = pg.key.get_pressed()
if keystate[pg.K_LEFT]:
self.speedx += -30
if keystate[pg.K_RIGHT]:
self.speedx += 30
self.rect.x = self.speedx
class Block(pg.sprite.Sprite):
def __init__(self,position):
pg.sprite.Sprite.__init__(self)
self.image = pg.Surface((100,70))
self.image.fill(GREY)
self.rect = self.image.get_rect()
self.rect.center = position
self.speedy = 20
def update(self):
self.rect.y += self.speedy
if self.rect.x > HEIGHT:
self.kill()
I expected to have 1 out of 5 spawnpoints empty but for some reason when I run the game, the first "wave" of sprites always has no empty space. Meanwhile the game continues with the expected results. No error messages
Any help would be appreciated
the first "wave" of sprites always has no empty space
This is due to:
self.SPAWNENEMIES = 1
Change it to:
self.SPAWNENEMIES = pg.USEREVENT
The id of the event is an integer. The pygame event system use a set of integers to identify ordinary actions (mouse motion or button clicks, key press and so on). As the docs says:
User defined events should have a value in the inclusive range of USEREVENT to NUMEVENTS - 1.
You should not define self.SPAWNENEMIES equal to 1, because 1 is reserved another type of event (not sure which one actually), this creates confusion and unexpected behaviour.
Seems that in your case yourv event in fact is posted multiple times, especially at the beginning, so you have two waves superimposed. Unless by chance both waves have the missing block at the same position, you'll see 5 blocks.
Another thing you should fix is:
num = random.randint(0,len(self.spawnpoints))
it should be:
num = random.randint(0,len(self.spawnpoints)-1)
or, alternatively:
num = random.randrange(0,len(self.spawnpoints))
The function random.randint:
Return a random integer N such that a <= N <= b. Alias for randrange(a, b+1).
The endpoint is inclusive.
You have 5 blocks, whose index goes from 0 to 4. When random.randint returns 5, no one of them is removed from the following spawning loop.
I'm not familiar with pygame, but i don't see where you reset the blocks once they pass through the dodging block. I'd guess that everytime you are adding blocks over the existing blocks and they are redrawn on top of the old ones, therefore it's not easily clear that there're bazillion of blocks raining down on each run.
And adding a print(self.blocks) puts out this, proving my guess :D
...
<Group(15 sprites)>
<Group(19 sprites)>
<Group(23 sprites)>
...
I have two sprites: a robot sprite and an obstacle sprite. I am using mask.overlap to determine if there is an overlap to prevent the robot from moving into the area of the obstacle (it functions as a blocking obstacle). Below is a portion of the movement evaluation code. It tests to see if the movement will cause a collision:
if pressed_keys[pygame.K_s]:
temp_position = [robot.rect.x, robot.rect.y]
temp_position[1] += speed
offset_x = temp_position[0] - obstacle.rect.x
offset_y = temp_position[1] - obstacle.rect.y
overlap = obstacle.mask.overlap(robot.mask, (offset_x, offset_y))
if overlap is None:
robot.rect.y += speed
else:
# adjust the speed to make the objects perfectly collide
This code works. If the movement would cause a collision, then it prevents the robot from moving.
ISSUE
For high speeds, the code prevents movement like it should, but it leaves a visual gap between the robot and the obstacle.
For example: if the speed is 30 and the two obstacles are 20 pixels away, the code will prevent the movement because a collision would be caused. But leaves a 20 pixel gap.
GOAL
If there were to be a collision, adjust the speed to the remaining pixel distance (20px like in the example) so that the robot and the obstacle perfectly collide. The robot can't move 30, but he can move 20. How can I calculate that remaining distance?
Here's what I described in the comment. Check if the sprites are colliding (I use spritecollide and the pygame.sprite.collide_mask functions here), and then use the normalized negative velocity vector to move the player backwards until it doesn't collide with the obstacle anymore.
import pygame as pg
from pygame.math import Vector2
pg.init()
screen = pg.display.set_mode((800, 600))
GRAY = pg.Color('gray12')
CIRCLE_BLUE = pg.Surface((70, 70), pg.SRCALPHA)
pg.draw.circle(CIRCLE_BLUE, (0, 0, 230), (35, 35), 35)
CIRCLE_RED = pg.Surface((170, 170), pg.SRCALPHA)
pg.draw.circle(CIRCLE_RED, (230, 0, 0), (85, 85), 85)
class Player(pg.sprite.Sprite):
def __init__(self, pos, key_left, key_right, key_up, key_down):
super().__init__()
self.image = CIRCLE_BLUE
self.mask = pg.mask.from_surface(self.image)
self.rect = self.image.get_rect(topleft=pos)
self.vel = Vector2(0, 0)
self.pos = Vector2(self.rect.topleft)
self.dt = 0.03
self.key_left = key_left
self.key_right = key_right
self.key_up = key_up
self.key_down = key_down
def handle_event(self, event):
if event.type == pg.KEYDOWN:
if event.key == self.key_left:
self.vel.x = -230
elif event.key == self.key_right:
self.vel.x = 230
elif event.key == self.key_up:
self.vel.y = -230
elif event.key == self.key_down:
self.vel.y = 230
elif event.type == pg.KEYUP:
if event.key == self.key_left and self.vel.x < 0:
self.vel.x = 0
elif event.key == self.key_right and self.vel.x > 0:
self.vel.x = 0
elif event.key == self.key_down and self.vel.y > 0:
self.vel.y = 0
elif event.key == self.key_up and self.vel.y < 0:
self.vel.y = 0
def update(self, dt):
self.pos += self.vel * dt
self.rect.center = self.pos
class Obstacle(pg.sprite.Sprite):
def __init__(self, pos):
super().__init__()
self.image = CIRCLE_RED
self.mask = pg.mask.from_surface(self.image)
self.rect = self.image.get_rect(topleft=pos)
class Game:
def __init__(self):
self.done = False
self.clock = pg.time.Clock()
self.screen = screen
self.player = Player((100, 50), pg.K_a, pg.K_d, pg.K_w, pg.K_s)
obstacle = Obstacle((300, 240))
self.all_sprites = pg.sprite.Group(self.player, obstacle)
self.obstacles = pg.sprite.Group(obstacle)
def run(self):
while not self.done:
self.dt = self.clock.tick(60) / 1000
self.handle_events()
self.run_logic()
self.draw()
pg.quit()
def handle_events(self):
for event in pg.event.get():
if event.type == pg.QUIT:
self.done = True
elif event.type == pg.MOUSEBUTTONDOWN:
if event.button == 2:
print(BACKGROUND.get_at(event.pos))
self.player.handle_event(event)
def run_logic(self):
self.all_sprites.update(self.dt)
collided_sprites = pg.sprite.spritecollide(
self.player, self.obstacles, False, pg.sprite.collide_mask)
for obstacle in collided_sprites:
# The length of the velocity vector tells us how many steps we need.
for _ in range(int(self.player.vel.length())+1):
# Move back. Use the normalized velocity vector.
self.player.pos -= self.player.vel.normalize()
self.player.rect.center = self.player.pos
# Break out of the loop when the masks aren't touching anymore.
if not pg.sprite.collide_mask(self.player, obstacle):
break
def draw(self):
self.screen.fill(GRAY)
self.all_sprites.draw(self.screen)
pg.display.flip()
if __name__ == '__main__':
Game().run()
You can pretty easily get a precise (if not exact) solution via a bisection search: once the collision is detected at the end of the full step, try half a step, and then either one or three quarters, and so on. This is treating the collision test as a boolean-valued function of the movement distance and looking for a “zero” (really the transition from miss to hit).
Note that this does nothing to resolve the issue of clipping through a thin wall or corner (where the initial collision test fails to detect the obstacle) and with complicated obstacles will find an arbitrary edge (not necessarily the first) to stop at.
I decided to go with the approach suggested by skrx in his comment: to basically back up by 1px until there is no longer a collision.
if pressed_keys[pygame.K_s]:
temp_position = [robot.rect.x, robot.rect.y]
temp_position[1] += speed
offset_x = temp_position[0] - obstacle.rect.x
offset_y = temp_position[1] - obstacle.rect.y
overlap = obstacle.mask.overlap(robot.mask, (offset_x, offset_y))
if overlap is None:
robot.rect.y += speed
else:
for step_speed in range(1, speed - 1):
collision[1] -= 1
offset_x = collision[0] - obstacle.rect.x
offset_y = collision[1] - obstacle.rect.y
overlap_adj = obstacle.mask.overlap(robot.mask, (offset_x, offset_y))
if overlap_adj is None:
robot.rect.y += (speed - step_speed)
break
This is a bit of a clumsy approach, but it will satisfy what i need for now and keep vector math at bay. For those who are looking for the proper way to approach this using normalized vectors and such, i would recommend using the answer skrx provided. I will likely come back to this and update it in the future. But for now, this will give users a couple options on how to proceed with perfect collision.