I started work on my own version of Super Mario Kart in python, just as a small project for myself. So far I've managed to transform an image to look like the player is moving over a pseudo-3D track. However, there is no perspective at all, which somewhat ruins the illusion of 3D. I know that I could pretty much fix this issue if I had a way of skewing the image so that the bottom of the surface is wider than the top, as this would help to simulate perspective.
I've been looking for quite a while online to see if there is a way to achieve this, as I really have no idea where to start. Although my searches have been inconclusive, and I have not found anything useful.
So how would I go about doing this to skew the image of the track?
Here is my code so far:
import os, sys, pygame
from pygame.locals import *
from math import *
pygame.init()
path, file_name = os.path.split(__file__)
path = os.path.join(path, 'data')
WIDTH = 800
HEIGHT = 600
SCREEN = pygame.display.set_mode((WIDTH,HEIGHT))
CLOCK = pygame.time.Clock()
FPS = 30
pygame.mouse.set_visible(False)
pygame.display.set_caption('Mario Kart')
class Driver(object):
def __init__(self, image, start_pos):
self.surface = pygame.image.load(os.path.join(path, image)).convert()
self.surface.set_colorkey(MAGENTA)
self.surface = pygame.transform.scale(self.surface, (64, 64))
self.pos = list(start_pos)
self.angle = 0
self.velocity = 0
def move(self):
keys = pygame.key.get_pressed()
if keys[K_a] or keys[K_LEFT]:
self.angle -= 14 / (1 + e ** (-0.3 * self.velocity)) - 7
#Sigmoid function to contain the angular velocity between 7 and -7
if keys[K_d] or keys[K_RIGHT]:
self.angle += 14 / (1 + e ** (-0.3 * self.velocity)) - 7
if keys[K_w] or keys[K_UP]:
self.velocity += 3
elif keys[K_s] or keys[K_DOWN]:
self.velocity -= 1
self.velocity *= 0.85
self.pos[0] += self.velocity * sin(radians(self.angle))
self.pos[1] -= self.velocity * cos(radians(self.angle))
def render(self):
SCREEN.blit(self.surface, (WIDTH / 2 - 32, HEIGHT / 2 - 32))
class Track(object):
def __init__(self, image, tilt = 1, zoom = 1):
self.surface = pygame.image.load(os.path.join(path, image)).convert_alpha()
self.angle = 0
self.tilt = tilt
self.zoom = zoom
self.rect = self.surface.get_rect()
self.rect.center = (WIDTH / 2, HEIGHT / 2)
self.image = self.surface
self.image_rect = self.image.get_rect()
def render(self, angle, center = (0, 0)):
self.angle = angle
self.image = self.surface
self.image = pygame.transform.rotate(self.image, self.angle)
self.image_rect = self.image.get_rect()
self.image = pygame.transform.scale(self.image, (int(self.image_rect.width * self.zoom), int((self.image_rect.height * self.zoom) / self.tilt)))
self.image_rect = self.image.get_rect(center = (self.rect.centerx - ((center[0] * self.zoom) * cos(radians(-self.angle)) - (center[1] * self.zoom) * sin(radians(-self.angle))),
self.rect.centery - ((center[0] * self.zoom) * sin(radians(-self.angle)) + (center[1] * self.zoom) * cos(radians(-self.angle))) / self.tilt))
SCREEN.blit(self.image, self.image_rect)
def main():
track = Track('MushroomCup1.png', tilt = 4, zoom = 7)
toad = Driver('Toad Sprite.png', (408, 90))
while True:
SCREEN.fill((255, 255, 255))
toad.move()
track.render(toad.angle, toad.pos)
toad.render()
events = pygame.event.get()
for event in events:
if event.type == QUIT or event.type == KEYDOWN and event.key == K_ESCAPE:
pygame.quit()
sys.exit()
pygame.display.update()
CLOCK.tick(FPS)
if __name__ == '__main__':
main()
MushroomCup1.png
Toad Sprite.png
You'll have to try something like PyOpenGL or Pyglet if you want to get real 3D. PyGame is really only for doing things on a conventional 2D surface/perspective.
That said, I actually did write a Mario Kart style 3D driving game that ran on pure PyGame (here's a JS port). I set coordinates for the road, decomposed the road into individual triangles, and did lots of matrix math. This was mostly a humorous exercise to see if I could, not something that I should have done. And that was with only geometric primitives. For an image it gets more complicated.
If you really wanted to, you could probably do something like preprocess that image resource to simplify and decompose it into a bunch of large-ish square blocks, and do a similar method of decomposing those into triangles with lots of matrix math. It wouldn't be fast, but it would technically work and only require PyGame.
Basically what I'm saying is, there is no Pygame solution in spirit, and if you want 3D proper, you'll have to move to a framework that supports it (like Pyglet or PyOpenGL).
Related
This question already has answers here:
How to make smooth movement in pygame
(2 answers)
How can i make a block follow another block in pygame [duplicate]
(2 answers)
Closed 2 years ago.
So I followed the answers in another question asked on StackOverflow but it seems that I have missed something. I went ahead after reading the answer and copied the code and adjusted it to my variables and class names.
The following is the error code that Idle gives me:
Traceback (most recent call last):
File "D:\Programme (x86)\Python\Games\Zombie Game\Zombie Game_Test1.py", line 133, in <module>
Zombie.move_towards_Char(Char)
TypeError: move_towards_Char() missing 1 required positional argument: 'Char'
This is where I looked:
How to make an enemy follow the player in pygame?
import pygame
import turtle
import time
import math
import random
import sys
import os
pygame.init()
WHITE = (255,255,255)
GREEN = (0,255,0)
RED = (255,0,0)
BLUE = (0,0,255)
BLACK = (0,0,0)
BGColor = (96,128,56)
ZColor = (225,0,0)
PColor = (0,0,255)
MOVE = 2.5
size = (1920, 1080)
screen = pygame.display.set_mode(size)
pygame.display.set_caption("Zombie Game")
class Char(pygame.sprite.Sprite):
def __init__(self, color, pos, radius, width):
super().__init__()
self.image = pygame.Surface([radius*2, radius*2])
self.image.fill(WHITE)
self.image.set_colorkey(WHITE)
pygame.draw.circle(self.image, color, [radius, radius], radius, width)
self.rect = self.image.get_rect()
def moveRightP(self, pixels):
self.rect.x += pixels
pass
def moveLeftP(self, pixels):
self.rect.x -= pixels
pass
def moveUpP(self, pixels):
self.rect.y -= pixels
pass
def moveDownP(self, pixels):
self.rect.y += pixels
pass
class Zombie(pygame.sprite.Sprite):
def __init__(self2, color, pos, radius, width):
super().__init__()
self2.image = pygame.Surface([radius*2, radius*2])
self2.image.fill(WHITE)
self2.image.set_colorkey(WHITE)
pygame.draw.circle(self2.image, color, [radius, radius], radius, width)
self2.rect = self2.image.get_rect()
self2.rect.center = pos
def move_towards_Char(self2, Char):
dx, dy = self2.rect.x - Char.rect.x, self2.rect.y - Char.rect.y
dist = math.hypot(dx, dy)
dx, dy = dx / dist, dy / dist
self2.rect.x += dx * self2.speed
self2.rect.y += dy * self2.speed
def moveRightZ(self2, pixels):
self2.rect.x += pixels
pass
def moveLeftZ(self2, pixels):
self2.rect.x -= pixels
pass
def moveUpZ(self2, pixels):
self2.rect.y -= pixels
pass
def moveDownZ(self2, pixels):
self2.rect.y += pixels
pass
all_sprites_list = pygame.sprite.Group()
playerChar = Char(PColor, [0, 0], 15, 0)
playerChar.rect.x = 960
playerChar.rect.y = 505
all_sprites_list.add(playerChar)
carryOn = True
clock = pygame.time.Clock()
zombie_list = []
zombie_rad = 15
zombie_dist = (200, 900)
next_zombie_time = pygame.time.get_ticks() + 10000
zombie_list = []
zombie_rad = 15
zombie_dist = (200, 900)
next_zombie_time = 10000
while carryOn:
for event in pygame.event.get():
if event.type==pygame.QUIT:
carryOn=False
elif event.type==pygame.KEYDOWN:
if event.key==pygame.K_x:
carryOn=False
keys = pygame.key.get_pressed()
if keys[pygame.K_a]:
playerChar.moveLeftP(MOVE)
if keys[pygame.K_d]:
playerChar.moveRightP(MOVE)
if keys[pygame.K_w]:
playerChar.moveUpP(MOVE)
if keys[pygame.K_s]:
playerChar.moveDownP(MOVE)
current_time = pygame.time.get_ticks()
if current_time > next_zombie_time:
next_zombie_time = current_time + 2000
on_screen_rect = pygame.Rect(zombie_rad, zombie_rad, size[0]-2*zombie_rad, size[1]-2*zombie_rad)
zombie_pos = (-1, -1)
while not on_screen_rect.collidepoint(zombie_pos):
dist = random.randint(*zombie_dist)
angle = random.random() * math.pi * 2
p_pos = (playerChar.rect.centerx, playerChar.rect.centery)
zombie_pos = (p_pos[0] + dist * math.sin(angle), p_pos[1] + dist * math.cos(angle))
new_pos = (random.randrange(0, size[0]), random.randrange(0, size[1]))
new_zombie = Zombie(RED, zombie_pos, zombie_rad, 0)
zombie_list.append(new_zombie)
screen.fill(BGColor)
screen.blit(playerChar.image,playerChar.rect)
for zombie in zombie_list:
screen.blit(zombie.image,zombie.rect)
pygame.display.flip()
clock.tick(60)
pygame.quit()
The major issue is, that you do the zombie movement calculations with integral data types. If the movement of a zombie is 1 pixel and the movement is diagonal, then the x and y component of the movement is < 1. Using an integral data type, this may results in 0 movement, because of truncating to int. Note the members of pygame.Rect are integral values.
You've to switch to floating point values to solve the issue. Use pygame.math.Vector2 to do the calculations.
Add a member pos of type Vector2 to the class Zombie which stores the floating point position of the zombie:
class Zombie(pygame.sprite.Sprite):
def __init__(self2, color, pos, radius, width):
super().__init__()
self2.image = pygame.Surface([radius*2, radius*2])
self2.image.fill(WHITE)
self2.image.set_colorkey(WHITE)
pygame.draw.circle(self2.image, color, [radius, radius], radius, width)
self2.rect = self2.image.get_rect()
self2.speed = 1
self2.pos = pygame.Vector2(pos[0], pos[1])
# [...]
Add a new method draw to the class Zombie, which draws (blit) a zombie at the position pos:
class Zombie(pygame.sprite.Sprite):
# [...]
def draw(self2):
self2.rect.center = (int(round(self2.pos.x)), int(round(self2.pos.y)))
screen.blit(self2.image, self2.rect)
Do the calculation of the movement of the zombie based on Vector2. Ensure that the distance between the player and the zombie is greater than 0 and that the zombie does not step over of the position of the player (min(len, self2.speed)):
class Zombie(pygame.sprite.Sprite):
# [...]
def move_towards_Char(self2, Char):
deltaVec = pygame.Vector2(Char.rect.center) - self2.pos
len = deltaVec.length()
if len > 0:
self2.pos += deltaVec/len * min(len, self2.speed)
Call the methods move_towards_Char and draw for each zombie, in the main loop of the application:
while carryOn:
for event in pygame.event.get():
if event.type==pygame.QUIT:
carryOn=False
elif event.type==pygame.KEYDOWN:
if event.key==pygame.K_x:
carryOn=False
keys = pygame.key.get_pressed()
if keys[pygame.K_a]:
playerChar.moveLeftP(MOVE)
if keys[pygame.K_d]:
playerChar.moveRightP(MOVE)
if keys[pygame.K_w]:
playerChar.moveUpP(MOVE)
if keys[pygame.K_s]:
playerChar.moveDownP(MOVE)
current_time = pygame.time.get_ticks()
if current_time > next_zombie_time:
next_zombie_time = current_time + 2000
on_screen_rect = pygame.Rect(zombie_rad, zombie_rad, size[0]-2*zombie_rad, size[1]-2*zombie_rad)
zombie_pos = (-1, -1)
while not on_screen_rect.collidepoint(zombie_pos):
dist = random.randint(*zombie_dist)
angle = random.random() * math.pi * 2
p_pos = (playerChar.rect.centerx, playerChar.rect.centery)
zombie_pos = (p_pos[0] + dist * math.sin(angle), p_pos[1] + dist * math.cos(angle))
new_pos = (random.randrange(0, size[0]), random.randrange(0, size[1]))
new_zombie = Zombie(RED, zombie_pos, zombie_rad, 0)
zombie_list.append(new_zombie)
# update all the positions of the zombies
for zombie in zombie_list:
zombie.move_towards_Char(playerChar)
screen.fill(BGColor)
screen.blit(playerChar.image,playerChar.rect)
# draw all the zombies
for zombie in zombie_list:
zombie.draw()
pygame.display.flip()
clock.tick(60)
L{Zombie.move_towards_Char} is a self method. You need to create object of Zombie class passing the required args mentioned in L{Zombie.init}.
Something like below:
zm = Zombie(color=<color>, pos=<pos>, radius=<radius>, width=<width>)
zm.move_towards_Char(Char)
my first ever Python program has hit a block I don't think I have the knowledge to solve myself.
It's a controllable spaceship on a 2d surface, I want to add momentum / inertia
I have it so the ship keeps travelling on the vector it previously was, when the engine is stopped.
However I can only get it to 'snap' to the new vector it rotates to face instantly.
What I want to happen is that inertia vector slowly aligns with the new pointing vector as it accelerates- like rotational acceleration? ( I'm not too hot on the math ) - I can rotate the inertia vector , but I would need to compare it somehow with the new pointing vector , and modify it based upon their difference?
if anyone could advise as to how I might start to approach this, that would be great - I suspect I coming at this from completely the wrong way.
Heres some of the code ( be gentle please!)
the sprite used is this : - ship.png
import pygame
import sys
from math import sin, cos, pi, atan2
from pygame.locals import *
import random
from random import randint
from pygame.math import Vector2
import operator
"""solar system generator"""
"""set screen size and center and some global namespace colors for ease of use"""
globalalpha = 255
screenx = int(1200)
screeny = int(700)
centerx = int(screenx / 2)
centery = int(screeny / 2)
center = (centerx, centery)
black = ( 0, 0, 0)
white = (255, 255, 255)
red = (209, 2, 22)
TRANSPARENT = (255,0,255)
numstars = 150
DISPLAYSURF = pygame.display.set_mode((screenx, screeny), 0, 32)
clock = pygame.time.Clock()
globaltimefactor = 1
shipimage = pygame.image.load('ship.png').convert()
DISPLAYSURF.fill(black)
screen_rect = DISPLAYSURF.get_rect()
class Playership(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.imageorig = pygame.image.load('ship.png').convert_alpha()
self.startpos = (screen_rect.center)
self.image = self.imageorig.copy()
self.rect = self.imageorig.get_rect(center=self.startpos)
self.angle = 0
self.currentposx = 600
self.currentposy = 350
self.tuplepos = (self.currentposx, self.currentposy)
self.speed = 1
self.rotatespeed = 1.5
self.initialvec = (600, 0)
self.destination = 0
self.anglechange = 0
self.currentspeed = 0
self.maxspeed = 5
self.engineon = False
self.newvec = (600, 0)
self.newdestination = 0
self.acceleration = 0.015
self.inertiaspeed = 0
self.transitionalvec = self.initialvec
def get_angleafterstopping(self):
newvec = self.initialvec
self.newvec = newvec
def get_destinationafterstopping(self):
x_dist = self.newvec[0] - self.tuplepos[0]
y_dist = self.newvec[1] - self.tuplepos[1]
self.newdestination = atan2(-y_dist, x_dist) % (2 * pi)
def get_destination(self):
x_dist = self.initialvec[0] - self.tuplepos[0]
y_dist = self.initialvec[1] - self.tuplepos[1]
self.destination = atan2(-y_dist, x_dist) % (2 * pi)
def moveship(self):
if self.engineon is True:
self.currentspeed = self.currentspeed + self.acceleration
if self.currentspeed > self.maxspeed:
self.currentspeed = self.maxspeed
elif self.currentspeed < 0:
self.currentspeed = 0
self.inertiaspeed = self.currentspeed
elif self.engineon is False:
self.currentposx = self.currentposx + (cos(self.newdestination) * self.inertiaspeed * globaltimefactor)
self.currentposy = self.currentposy - (sin(self.newdestination) * self.inertiaspeed * globaltimefactor)
self.tuplepos = (self.currentposx, self.currentposy)
self.rect.center = self.tuplepos
return
self.get_destination()
self.currentposx = self.currentposx + (cos(self.destination) * self.currentspeed * globaltimefactor)
self.currentposy = self.currentposy - (sin(self.destination) * self.currentspeed * globaltimefactor)
self.tuplepos = (self.currentposx, self.currentposy)
self.rect.center = self.tuplepos
def rotateship(self, rotation):
self.anglechange = self.anglechange - (rotation * self.rotatespeed * globaltimefactor)
self.angle += (rotation * self.rotatespeed * globaltimefactor)
self.image = pygame.transform.rotate(self.imageorig, self.angle)
self.rect = self.image.get_rect(center=self.rect.center)
initialvec = self.tuplepos + Vector2(0, -600).rotate(self.anglechange * globaltimefactor)
initialvec = int(initialvec.x), int(initialvec.y)
self.initialvec = initialvec
myship = Playership()
all_sprites_list = pygame.sprite.Group()
all_sprites_list.add(myship)
firsttimedone = False
def main():
done = False
while not done:
keys_pressed = pygame.key.get_pressed()
if keys_pressed[pygame.K_LEFT]:
myship.rotateship(1)
if keys_pressed[pygame.K_RIGHT]:
myship.rotateship(-1)
if keys_pressed[pygame.K_UP]:
myship.engineon = True
myship.moveship()
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit(); sys.exit();
if event.type == pygame.KEYUP:
if event.key == pygame.K_UP:
myship.engineon = False
myship.currentspeed = 0
myship.get_angleafterstopping()
myship.get_destinationafterstopping()
DISPLAYSURF.fill(black)
all_sprites_list.update()
all_sprites_list.draw(DISPLAYSURF)
pygame.draw.line(DISPLAYSURF, white, (myship.tuplepos), (myship.initialvec))
pygame.draw.line(DISPLAYSURF, red, (myship.tuplepos), (myship.newvec))
pygame.display.flip()
if myship.engineon is False:
myship.moveship()
clock.tick(50)
pygame.display.set_caption("fps: " + str(clock.get_fps()))
if __name__ == '__main__':
pygame.init()
main()
pygame.quit(); sys.exit();
EDIT :
I fixed it : just required a better understanding of vectors
ship starts off with acceleration and velocity both stated as vectors.
self.position = vec(screenx / 2, screeny / 2)
self.vel = vec(0, 0)
self.acceleration = vec(0, -0.2) # The acceleration vec points upwards from the starting ship position
rotating the ship rotates that vector in place
self.acceleration.rotate_ip(self.angle_speed)
self.angle += self.angle_speed
self.image = pygame.transform.rotate(self.imageorig, -self.angle)
self.rect = self.image.get_rect(center=self.rect.center)
accelerating is this :
self.vel += self.acceleration * self.enginepower * globaltimefactor
updating position :
self.position += self.vel
self.rect.center = self.position
I was making it harder than it needed to be, velocity needed to be constant until acted upon by the rotated acceleration vector. I didn't know how to add vectors together etc.
I fixed it : just required a better understanding of vectors
ship starts off with acceleration and velocity both stated as vectors.
self.position = vec(screenx / 2, screeny / 2)
self.vel = vec(0, 0)
self.acceleration = vec(0, -0.2) # The acceleration vec points upwards from the starting ship position
rotating the ship rotates that vector in place
self.acceleration.rotate_ip(self.angle_speed)
self.angle += self.angle_speed
self.image = pygame.transform.rotate(self.imageorig, -self.angle)
self.rect = self.image.get_rect(center=self.rect.center)
accelerating is this :
self.vel += self.acceleration * self.enginepower * globaltimefactor
updating position :
self.position += self.vel
self.rect.center = self.position
I was making it harder than it needed to be, velocity needed to be constant until acted upon by the rotated acceleration vector. I didn't know how to add vectors together etc.
So I'm trying to make a game where there are 2 objects and am trying to make it so that my program knows when the 2 objects collide.
if players[0].mask.overlap(players[1].mask, offset):
print("collided")`
My objects both rotate clockwise and counterclockwise and have masks both for the head and the body. (I have 2 images which are both the same size but one contains only the head and another contains only the body, in order to use mask.from_surface().)
def turn(self, event):
self.surface = self.ojSurface
if event == 0:
self.deg -= self.turnSpd
elif event == 1:
self.deg += self.turnSpd
self.surface = pygame.transform.rotate(self.surface, self.deg)
self.newPos = (self.pos[0] - self.surface.get_width() / 2, self.pos[1] - self.surface.get_height() / 2)
self.mask = pygame.mask.from_surface(self.surface)
However, what I find is when I try to check for collision when they are rotated the mask isn't in the same place as the image. It should update the mask every frame through the move function which is called every loop
def move(self):
self.pos[0] = pygame.mouse.get_pos()[0]
self.pos[1] = pygame.mouse.get_pos()[1]
self.newPos = (self.pos[0] - self.surface.get_width() / 2, self.pos[1] - self.surface.get_height() / 2)
self.mask = pygame.mask.from_surface(self.surface)
Faulty Hit Detection
If you want to peruse what I have so far here it is:
import pygame
class Player:
def __init__(self, pos,surface,screen):
self.pos = [pos[0],pos[1]]
self.newPos = (pos[0] - surface.get_width()/2, pos[1] - surface.get_height()/2)
self.deg = 0
self.surface = surface
self.ojSurface = surface
self.screen = screen
self.mask = pygame.mask.from_surface(self.surface)
def turn(self, event):
self.surface = self.ojSurface
#clockwise
if event == 0:
self.deg -= 0.1
#counter clockwise
elif event == 1:
self.deg += 0.1
self.surface = pygame.transform.rotate(self.surface, self.deg)
#resetting pos and mask
self.newPos = (self.pos[0] - self.surface.get_width() / 2, self.pos[1] - self.surface.get_height() / 2)
self.mask = pygame.mask.from_surface(self.surface)
def move(self):
self.pos[0] = pygame.mouse.get_pos()[0]
self.pos[1] = pygame.mouse.get_pos()[1]
self.newPos = (self.pos[0] - self.surface.get_width() / 2, self.pos[1] - self.surface.get_height() / 2)
self.mask = pygame.mask.from_surface(self.surface)
def draw(self):
self.screen.blit(self.surface, self.newPos)
screenRes = (640,480)
screen = pygame.display.set_mode(screenRes)
closed = False
players = [Player((320,240),pygame.image.load("body.png"), screen),Player((480,240),pygame.image.load("body.png"), screen)]
controls = [[pygame.K_a,pygame.K_s],[pygame.K_k,pygame.K_l]]
while not closed:
screen.fill((0, 0, 0))
keys = pygame.key.get_pressed()
offset = (int(players[0].newPos[0] - players[1].newPos[0]), int(players[0].newPos[1] - players[1].newPos[1]))
#collision
if players[0].mask.overlap(players[1].mask, offset):
print("collided")
#controls
for i in range(len(players)):
if keys[controls[i][0]]:
players[i].turn(0)
if keys[controls[i][1]]:
players[i].turn(1)
players[i].draw()
players[0].move()
pygame.display.update()
for event in pygame.event.get():
# standard quit
if event.type == pygame.QUIT:
pygame.display.quit()
closed = True
pygame.quit()
You're calculating the offset in the wrong order. Subtract players[0]'s position from players[1]'s position:
offset = (
int(players[1].newPos[0] - players[0].newPos[0]),
int(players[1].newPos[1] - players[0].newPos[1]),
)
So I've reviewed your GitHub code, and while I'm not completely sure (I skimmed your code, looking for certain methods being called), I believe that you're not updating your masks every frame. Somewhere in your code, you'll have to update the masks every frame/loop, allowing for the collision to work properly. Try to update all the masks, the head, body and the one in the Player class. Hope this helps!
I don't understand why the sprite collision detection is not taking the image rotation into account.
I tried different functions but they didn't work out for me.
CarSprites.py:
import pygame, math, random
class CarSprite(pygame.sprite.Sprite):
MIN_FORWARD_SPEED = 5
ACCELERATION = 2
TURN_SPEED = 5
IS_DUMMY = False
def __init__(self, image, position, direction):
pygame.sprite.Sprite.__init__(self)
self.src_image = pygame.transform.scale(pygame.image.load(image), (51, 113))
self.position = position
self.rect = self.src_image.get_rect()
self.rect.center = self.position
self.speed = 0
self.direction = direction
self.k_left = self.k_right = self.k_down = self.k_up = 0
def update(self, deltat):
# SIMULATION
#speed
self.speed += (self.k_up + self.k_down)
if self.speed < self.MIN_FORWARD_SPEED:
self.speed = self.MIN_FORWARD_SPEED
self.speed += (self.k_up + self.k_down)
if self.speed > self.MIN_FORWARD_SPEED * 2:
self.speed = self.MIN_FORWARD_SPEED * 2
#direction
self.direction += (self.k_right + self.k_left)
x, y = self.position
rad = self.direction * math.pi / 180
x += -self.speed*math.sin(rad)
y += -self.speed*math.cos(rad)
self.position = (x, y)
self.image = pygame.transform.rotate(self.src_image, self.direction)
self.rect = self.image.get_rect()
self.rect.center = self.position
#Emulate friction with road and wind
if self.speed > self.MIN_FORWARD_SPEED :
self.speed += -0.1
class DummyCarSprite(pygame.sprite.Sprite):
#MIN_FORWARD_SPEED = 5
#MIN_REVERSE_SPEED = 10.1
#MAX_FORWARD_SPEED_ABOVE_MIN = 5
#ACCELERATION = 2
#TURN_SPEED = 5
def __init__(self, image, position, direction):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.transform.scale(pygame.image.load(image), (51, 113))
self.position = position
self.rect = self.image.get_rect()
self.rect.center = self.position
self.speed = 0
self.direction = direction
self.k_left = self.k_right = self.k_down = self.k_up = 0
if random.randint(0,1) == 1 :
self.direction = self.direction + 180
game.py
def GetDummyCars() :
allDummyCars = [
#Row1
#DummyCarSprite(getCarImage(), (211.9915431212928, 209.36603413022453), 180),
#DummyCarSprite(getCarImage(), (268.9915431212928, 209.36603413022453), 180),
DummyCarSprite(getCarImage(), (325.9915431212928, 209.36603413022453), 180),
DummyCarSprite(getCarImage(), (382.9915431212928, 209.36603413022453), 180)
#etc. etc.
]
dummyCars = []
for dummyCar in allDummyCars :
if random.randint(0,1) == 1 :
dummyCars.append(dummyCar)
return pygame.sprite.RenderPlain(*dummyCars)
playerCar = CarSprite(playerCarImagePath, (1550, 100), 90)
playerCar_group = pygame.sprite.RenderPlain(playerCar)
dummyCar_group = GetDummyCars()
#collisions with dummy cars
dummyCarCollisions = pygame.sprite.groupcollide(playerCar_group, dummyCar_group)
if dummyCarCollisions != {}:
lose_condition = True
playerCar.src_image = pygame.image.load('images/collision.png')
seconds = 0
playerCar.speed = 0
playerCar.MIN_FORWARD_SPEED = 0
playerCar.MAX_FORWARD_SPEED_ABOVE_MIN = 0
playerCar.k_right = 0
playerCar.k_left = 0
I would like to find a way to detect collision between the sprites in the 2 car groups, or collision between the player sprite and the dummycar_group (each would work out for me) that takes the rotation of the image into account.
What happens now is when I steer the car, the car image rotates but it looks like the collision detection doesn't see that.
Is there a better function i can use that could handle this?
My full source code: dropbox
I found this question very interesting and fun to work on and fix! Thanks for posting!
I have found the problem and it is rather unfortunate. Your code runs perfectly from what I have seen. The problem is that pygame uses rectangles for collision detection which are not precise enough.
You are applying the rotation to the image but that just makes it bigger and less accurate. I have highlighted the problem with the addition of rendiering debug lines in the GameLoop function.
# draw some debug lines
pygame.draw.rect(screen, (255, 0, 0), playerCar.rect, 1)
for dummyCar in dummyCar_group.sprites():
pygame.draw.rect(screen, (0, 0, 255), dummyCar.rect, 1)
Add these lines in and you shall see for yourself.
The only solution that I can think of is to add in the functionality to use polygons for collision detection yourself.
The way I would implement this is to:
Stop using the rect attribute of all Sprites for collision detection and stop using any methods for collision detection that use the underlying Rects, e.g pygame.sprite.spritecollide().
add a pointlist field to all sprites that need it which will store all the points of the polygon
Implement your own function that takes in two lists of points and returns if they overlap
I hope that this answer helped you and if you have any further questions please feel free to post a comment below!
I'm working on a game for class and this is what we've learned to do so far. I'm trying to get the sprites to stay onscreen with clamp_ip, but it keeps giving me an error message that "Butterfly instance has no attribute clamp_ip." Is there a different way I should be keeping the butterflies onscreen?
This first bit is just setting up pygame and the butterfly class (where I included a line to define a rectangle for the butterflies), I've highlighted where I'm guessing the potential errors are below.
This should be ok.
pygame.init()
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
playground = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption('Butterflies')
screenRect = playground.get_rect()
steps = 1
class Butterfly:
def __init__(self):
self.x = random.randint(0, SCREEN_WIDTH)
self.y = random.randint(0, SCREEN_HEIGHT)
self.image_Butterfly = pygame.image.load('butterflya.png')
self.image_ButterflyB = pygame.image.load('butterflyb.png')
self.height = self.image_Butterfly.get_height()
self.width = self.image_Butterfly.get_width()
self.rect = (self.width, self.height)
clock = pygame.time.Clock()
fps = 10
net = []
for i in range (15):
butterflyInstance = Butterfly()
net.append(butterflyInstance)
playground.fill(cyan)
Game_Over = False
while not Game_Over:
for event in pygame.event.get():
if event.type == pygame.QUIT:
Game_Over = True
This is where I'm guessing I messed up. I tried to use the clamp_ip function at the end of this loop
for butterflyInstance in net:
butterflyInstance.x += random.randint(-10, 10)
butterflyInstance.y += random.randint(-10,10)
if steps % 2 == 0:
playground.blit(butterflyInstance.image_Butterfly, (butterflyInstance.x, butterflyInstance.y))
steps += 1
else:
playground.blit(butterflyInstance.image_ButterflyB, (butterflyInstance.x, butterflyInstance.y))
steps +=1
butterflyInstance.clamp_ip(screenRect)
Thank you so much!
See PyGame doc pygame.Rect()
clamp_ip is pygame.Rect method so you have to use butterflyInstance.rect.clamp_ip()
But you have to use self.rect.x, self.rect.y, self.rect.width, self.rect.height instead of self.x, self.y, self.width, self.height to keep position and size of object.
And use butterflyInstance.rect.x, butterflyInstance.rect.y, butterflyInstance.rect.width, butterflyInstance.rect.height
instead of butterflyInstance.x, butterflyInstance.y, butterflyInstance.width, butterflyInstance.height
PyGame use pygame.Rect in many places - it is usefull. For example: you can use rect.right instead of rect.x + rect.width, or rect.center = screenRect.center to center object on screen.