Pygame - Rotate and move a spaceship (Polygon) - python

Ok, I've been working all day on this and I haven't found the logic.
I wanna make a classic style asteroids game, and I'm starting with the spaceship.
What I did was draw some lines with the shape of a spaceship:
import pygame
import colors
from helpers import *
class Ship :
def __init__(self, display, x, y) :
self.x = x
self.y = y
self.width = 24
self.height = 32
self.color = colors.green
self.rotation = 0
self.points = [
#A TOP POINT
(self.x, self.y - (self.height / 2)),
#B BOTTOM LEFT POINT
(self.x - (self.width / 2), self.y + (self.height /2)),
#C CENTER POINT
(self.x, self.y + (self.height / 4)),
#D BOTTOM RIGHT POINT
(self.x + (self.width / 2), self.y + (self.height / 2)),
#A TOP AGAIN
(self.x, self.y - (self.height / 2)),
#C A NICE LINE IN THE MIDDLE
(self.x, self.y + (self.height / 4)),
]
def move(self, strdir) :
dir = 0
if strdir == 'left' :
dir = -3
elif strdir == 'right' :
dir = 3
self.points = rotate_polygon((self.x, self.y), self.points, dir)
def draw(self, display) :
stroke = 2
pygame.draw.lines(display, self.color, False, self.points, stroke)
The ship looks like this:
Now important things to know:
The tuple (self.x, self.y) is the middle of the spaceship.
Using this function I managed to rotate (spin) it on command using the keys A and D
def rotate_polygon(origin, points, angle) :
angle = math.radians(angle)
rotated_polygon = []
for point in points :
temp_point = point[0] - origin[0] , point[1] - origin[1]
temp_point = (temp_point[0] * math.cos(angle) - temp_point[1] * math.sin(angle),
temp_point[0] * math.sin(angle) + temp_point[1] * math.cos(angle))
temp_point = temp_point[0] + origin[0], temp_point[1] + origin[1]
rotated_polygon.append(temp_point)
return rotated_polygon
The problem is: How can I make it move forward or backwards in the direction the spaceship is pointing?
OR
How can I update the self.x and self.y values and update them inside the self.points list and preserve rotation?

The easiest, most general way to deal with movement and rotation would to use some vector math (can apply to 3D graphics as well). You could keep a 2D vector representing the forward direction of your ship. For example, if your ship starts facing upwards and your (0,0) coordinate is the top left. You could do.
self.forward = Vector2D(0, -1) # Vector2D(x, y)
When you rotate you must rotate this vector. You can rotate using the following.
self.forward.x = self.forward.x * cos(angle) - self.forward.y * sin(angle)
self.forward.y = self.forward.x * sin(angle) + self.forward.y * cos(angle)
Then when you want to move the ship you can transform the ship points relative to this vector. For example.
self.x += forward.x * velocity.x
self.y += forward.y * velocity.y
I would highly recommend you write a little Vector2D class which can do some of the basic operations, e.g. dot, cross, mult, add, sub, normalize, etc.
If you are familiar with matrices then these operations can become easier if you implement them using matrices instead of a system of linear equations.

It seems to me that you should be able to simply do the following.
def updatePosition(self, dx, dy):
self.x += dx
self.y += dy
newPoints = []
for (x,y) in self.points:
newPoints.append((x+dx, y+dy))
self.points = newPoints

Related

Why is my enemy movement script not working in pygame [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 1 year ago.
Improve this question
I've been working on movement for the enemy in my game for a day or so and can't seem to get it working
the problem is it will chase the character for a little bit until it hits x or y zero and will stick there forever, and sometimes it will just stick in place when it hits the player
any help will be greatly appreciated
import pygame
import math
ALPHA = (0, 255, 0)
path = "data/chr/squid.png"
def findDist(coords1, coords2):
try:
return math.sqrt(pow(coords1[0] - coords2[0], 2) + pow(coords1[1] - coords2[1], 2))
except:
print("invalid dist input, ignoring")
class Squid(pygame.sprite.Sprite):
#player object
def __init__(self, X, Y):
pygame.sprite.Sprite.__init__(self)
self.speed = .01
self.images = []
for i in range(1, 5):
img = pygame.image.load(path).convert()
img.convert_alpha() # optimise alpha
img.set_colorkey(ALPHA) # set alpha
self.images.append(img)
self.image = self.images[0]
self.rect = self.image.get_rect()
self.rect.x = X
self.rect.y = Y
def chasePlayer(self, player):
dx, dy = player.rect.x - self.rect.x, player.rect.y - self.rect.y
dist = math.hypot(dx, dy)
dx, dy = dx / dist, dy / dist
self.rect.x += dx * self.speed
self.rect.y += dy * self.speed
Since pygame.Rect is supposed to represent an area on the screen, a pygame.Rect object can only store integral data.
The coordinates for Rect objects are all integers. [...]
The fraction part of the coordinates gets lost when the new position of the object is assigned to the Rect object. If this is done every frame, the position error will accumulate over time.
If you want to store object positions with floating point accuracy, you have to store the location of the object in separate variables respectively attributes and to synchronize the pygame.Rect object. round the coordinates and assign it to the location (e.g. .topleft) of the rectangle.
x, y = # floating point coordinates
rect.topleft = round(x), round(y)
Read about Classes. If you want to spawn multiple enemies, you must create multiple Instance Objects of the Squid class. e.g.:
enemies = []
for _ in range(5):
x = random.randrange(screen_width)
y = random.randrange(screen_height)
enemies.append(Squid(x, y))
Squid class:
class Squid(pygame.sprite.Sprite):
def __init__(self, x, y):
pygame.sprite.Sprite.__init__(self)
self.speed = .01
self.image = pygame.image.load(path).convert()
self.image.convert_alpha() # optimise alpha
self.image.set_colorkey(ALPHA) # set alpha
self.x = x
self.y = y
self.rect = self.image.get_rect(topleft = (round(x), round(y)))
def chasePlayer(self, player):
dx, dy = player.rect.x - self.rect.x, player.rect.y - self.rect.y
dist = math.hypot(dx, dy)
dx, dy = dx / dist, dy / dist
self.x += dx * self.speed
self.y += dy * self.speed
self.rect.x = round(self.x)
self.rect.y = round(self.y)

Coordinates for Moving Ball Bounce Off Canvas

I'm looking for this circle to rebound off the edges of the screen and bounce back into the canvas. I know you need to input coordinates just need a formula so I can understand what I'm looking at.
import tkinter # Used to draw on canvas (2D plot)
class MyCircle:
def __init__(self, x, y, dx, dy, cnum): # "self" always 1st parameter in python methods
self.x = x
self.y = y
self.dx = dx # "delta-x" or "change in x" or "x velocity"
self.dy = dy
self.cnum = cnum # Add circle number as parameter for log
# delete the log files to retry
# https://docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files
def logData(self):
# Filename uses circle number.
with open('logfile_' + str(self.cnum) + '.txt', 'a') as f:
f.write("x=" + str(self.x) +
" y=" + str(self.y) +
" dx=" + str(self.dx) +
" dy=" + str(self.dy) + '\n')
def MoveIt(self, coordinates=tkinter.Canvas):
self.x = self.x + self.dx
self.y = self.y + self.dy
def DrawIt(self, w):
w.create_arc(self.x, self.y, self.x + 20, self.y + 20, start=0, extent=359,
fill='red')
TL;DR
Add a method called bounce and call it every frame (You can call it in the MoveIt method):
def bounce(self):
if not 0 <= self.x <= WIDTH:
self.dx *= -1
if not 0 <= self.y <= HEIGHT:
self.dy *= -1
We need to assume somethings here:
The edges (walls) has infinite mass. Means at the collisions the energy would not be passed from balls to the walls.
Collisions are elastic. Means the energy would not transferred to something else.
With these assumptions the collisions happens to be change in direction of speed vector of the ball.
If collision happens on left or right wall, you must change the sign (*-1) of the dx and if it happens on top or bottom walls you must change the sign of dy.

How can I draw text at any angle above and parallel to a line?

I want to place a text centrally above the line (controlled with variable distance). There is a method drawLineDescription() for this. This method takes the start and endpoints of the lines and then calculates the center point x, y. I have already worked with angle to ensure that the text is placed correctly. Unfortunately, I can't figure out how to place the text vertically over the line at every angle, i.e. depending on the rotation the variables x, y have to be able to move. How can I supplement this?
def drawLineDescription(canvas, startX, startY, endX, endY, distance):
lengthX = endX - startX
lengthY = endY - startY
x = int(startX+((lengthX)/2))
y = int(startY+((lengthY)/2))
angle = math.degrees(math.atan2(lengthY, lengthX)*-1)
angle = round(angle)
if angle < -90 or angle > 90:
angle += 180
canvas.create_text(x, y, angle=angle, font=("Arial", 12), text="exampleText")
In the end it should look like this (example of multiple lines with text - the lines never cross their text):
Example result
lengthX = endX - startX
lengthY = endY - startY
fullLength = math.sqrt(lengthX**2 + lengthY**2)
#unit direction vector
ux = lengthX / fullLength
uy = lengthY / fullLength
#unit normal
if ux < 0:
nx, ny = -uy, ux
else:
nx, ny = uy, -ux
#text center point (D at the picture)
cx = x + nx * distance
cy = y + ny * distance
#if you need start of text (S at the picture)
sx = x + nx * distance - ux * halfwidth
sy = y + ny * distance - uy * halfwidth
You can draw rotated text on tkinter if you are using tcl > 8.6 with the following instructions: canvas_item = tk.create_text, and canvas.itemconfig(canvas_item, angle=rotation_angle)
In order to achieve what you want, you need a little bit of geometry, notably the coordinates of the line segment, its mid point, an offset vector perpendicular to the segment, and the angle of the segment.
I encapsulated the arithmetic necessary to calculate the proper geometric elements in a class point, and in c class Vector. These classes are not bullet proof, but they give you a starting point for basic geometry.
I added an example of a line defined by two points, with the text placed as you need and rotated to match the direction of the line segment.
import math
import tkinter as tk
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other: 'Vector'):
return Vector(other.x + self.x, other.y + self.y)
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
def __iter__(self):
yield self.x
yield self.y
def __str__(self):
return f'{self.__class__.__name__}({self.x}, {self.y})'
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Point(other.x + self.x, other.y + self.y)
def __sub__(self, other):
return Vector(other.x - self.x, other.y - self.y)
def scale(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def normal(self):
norm = self.norm()
return Vector(self.x / norm, self.y / norm)
def norm(self):
return math.hypot(self.x, self.y)
def perp(self):
x, y = self.normal()
return Vector(y, -x)
def angle(self):
return math.atan2(-self.y, self.x) * (180 / math.pi)
def __iter__(self):
yield self.x
yield self.y
def __str__(self):
return f'{self.__class__.__name__}({self.x}, {self.y})'
if __name__ == '__main__':
root = tk.Tk()
canvas = tk.Canvas(root, width=500, height=500)
p0, p1 = Point(100, 40), Point(200, 300)
segment = p1 - p0
mid_point = segment.scale(0.5) + p0
# canvas.create_oval(*(mid_point - Vector(2, 2)), *(Vector(2, 2) + mid_point))
line = canvas.create_line(*p0, *p1)
offset = segment.perp().scale(20)
# canvas.create_line(*mid_point, *(mid_point+offset))
txt = canvas.create_text(*(offset + mid_point), text='example')
canvas.itemconfig(txt, angle=segment.angle())
canvas.pack()
root.mainloop()

Python Turtle/Tkinter Timers Accelerating

I thought the canonical way to do animation with Python Turtle Graphics was to do something like
def animate():
# move stuff
ontimer(animate, delay)
Looking into the source code for turtle this implements tkinter after() in the background.
Can someone explain why in the program below the animation accelerates and decelerates dramatically when it is left running?
My theory is that since a new .after() id is created each time ontimer() is called, there are somehow multiple timers in existence which interfere with each other? Or maybe it's just a result of the randomness in the program? Or maybe the short interval between callbacks causes problems?
from random import *
from turtle import *
import math
class Vector(object):
def __init__(self, x = 0.0, y = 0.0):
self.x = x
self.y = y
def move(self, other):
""" Move vector by other (in-place)."""
self.__iadd__(other)
def __iadd__(self, other):
if isinstance(other, Vector):
self.x += other.x
self.y += other.y
else:
self.x += other
self.y += other
def rotate(self, angle):
"""Rotate vector counter-clockwise by angle (in-place)."""
radians = angle * math.pi / 180.0
cosine = math.cos(radians)
sine = math.sin(radians)
x = self.x
y = self.y
self.x = x * cosine - y * sine
self.y = y * cosine + x * sine
ant = Vector(0, 0)
aim = Vector(2, 0)
def wrap(value):
"Wrap value around -200 and 200."
if value > 200:
value = -200
elif value < -200:
value = 200
return value
def draw():
"Move ant and draw screen."
ant.move(aim)
ant.x = wrap(ant.x)
ant.y = wrap(ant.y)
aim.move(random() - 0.5)
aim.rotate(random() * 10 - 5)
clear()
goto(ant.x, ant.y)
dot(10)
if running:
ontimer(draw, 50)
setup(420, 420, 370, 0)
hideturtle()
tracer(False)
up()
running = True
draw()
done()
My belief is that your animation accelerates and decelerates because you use tracer() but fail to do an explicit update(). The tracer() function turns off animation but some turtle operations do an implicit update() as a side effect. Since you didn't do an explicit update() you're only getting random updates caused by those side effects.
Below I've added an explicit update() and simplified the code to make the turtle itself the moving object, rather than stamping and clearing. (BTW, if you save the result of stamp() you can ask it to clear itself.)
I've also switched from a circle to a turtle cursor image and added in logic to set the heading to the direction of motion:
from random import random
from turtle import Screen, Turtle, Vec2D
from math import pi, cos, sin
class Vector(object):
def __init__(self, x=0.0, y=0.0):
self.x = x
self.y = y
def move(self, other):
""" Move vector by other (in-place)."""
self.__iadd__(other)
self.wrap()
def __iadd__(self, other):
if isinstance(other, Vector):
self.x += other.x
self.y += other.y
else:
self.x += other
self.y += other
def rotate(self, degrees):
""" Rotate vector counter-clockwise by angle (in-place). """
radians = degrees * pi / 180.0
cosine = cos(radians)
sine = sin(radians)
x = self.x
y = self.y
self.x = x * cosine - y * sine
self.y = y * cosine + x * sine
def position(self):
return Vec2D(self.x, self.y)
def wrap(self):
""" Wrap value around -200 and 200. """
x = self.x
y = self.y
if x > 200:
self.x = -200
elif x < -200:
self.x = 200
if y > 200:
self.y = -200
elif y < -200:
self.y = 200
def draw():
""" Move ant and draw screen. """
ant.move(aim)
position = ant.position()
turtle.setheading(turtle.towards(position))
turtle.setposition(position)
screen.update()
aim.move(random() - 0.5)
aim.rotate(random() * 10 - 5)
screen.ontimer(draw, 75)
screen = Screen()
screen.setup(420, 420)
screen.tracer(False)
turtle = Turtle()
turtle.hideturtle()
turtle.shape('turtle')
turtle.shapesize(0.5)
turtle.penup()
turtle.showturtle()
ant = Vector(0, 0)
aim = Vector(2, 0)
draw()
screen.mainloop()

Circle objects register a collision, but they are not touching

I just checked here to make sure this question was allowed, and it seems that it is so here I go:
I am currently making a 2D physics engine as a small project. I have a class called circle which has properties such as radius, rotation, position, and velocity:
class circle():
def __init__(self, radius = 10, r = 0.0, x = 0, y = 0, Vr = 0, Vx = 0, Vy = 0):
self.radius = radius
self.r = r
self.x = x
self.y = y
self.Vr = Vr
self.Vx = Vx
self.Vy = Vy
The class has a method called CheckCollisions(), which checks if the distance between its centre and another circle's centre is less than the sum of their radii:
def CheckCollisions(self):
for c in circles:
distance = math.sqrt((c.x - self.x)*(c.x - self.x) + (c.y - self.y)*(c.y - self.y))
if distance < self.radius + c.radius:
print('!')
else:
print('')
The idea is that on detecting the collision, forces can be applied as vectors to each object as a response to the impact.
When my code runs, I see constant exclamation marks appearing in the shell, despite the circles not colliding. What is causing this? Perhaps something in my calculation of distance is incorrect?
Full code:
import pygame, random, math
from pygame.locals import*
# set up pygame window
(width, height) = (1000, 800)
screen = pygame.display.set_mode((width,height))
pygame.display.set_caption('Impulse Physics v0.1 BETA')
pen = pygame.image.load('Pen.png').convert()
background = (0, 0, 0)
class circle():
def __init__(self, radius = 10, r = 0.0, x = 0, y = 0, Vr = 0, Vx = 0, Vy = 0):
self.radius = radius
# position and rotation
self.r = r
self.x = x
self.y = y
# velocity
self.Vr = Vr
self.Vx = Vx
self.Vy = Vy
def CheckCollisions(self):
for c in circles:
# use pythagoras to find direct distance between centres
distance = math.sqrt((c.x - self.x)*(c.x - self.x) + (c.y - self.y)*(c.y - self.y))
if distance < self.radius + c.radius:
print('!')
else:
print('')
def Move(self):
# apply slight "air resistance"
self.Vx = self.Vx * 0.9999
# gravity. REMEMBER y axis is inverted in pygame!
self.Vy = self.Vy + 0.15
# move object
self.x = self.x + self.Vx
self.y = self.y + self.Vy
self.r = self.r + self.Vr
self.CheckCollisions()
# check if colliding with the sides of the window
if self.y + self.radius > height:
self.Vy = self.Vy * -0.98
self.y = self.y + self.Vy
if (self.x + self.radius > width) or (self.x - self.radius < 0):
self.Vx = self.Vx * -0.98
self.x = self.x + self.Vx
def Render(self):
penX = self.x
penY = self.y
penR = self.r
screen.blit(pen, (penX, penY))
# draw the radius of the circle
for counter in range(self.radius):
penX = self.x + (math.sin(penR) * counter)
penY = self.y - (math.cos(penR) * counter)
screen.blit(pen, (penX, penY))
# draw the circumference of the circle
for counter in range(self.radius * 20):
penR = counter * (360 / self.radius * 20)
penX = self.x + (math.sin(penR) * self.radius)
penY = self.y + (math.cos(penR) * self.radius)
screen.blit(pen, (penX, penY))
circles = []
#create objects here
c1 = circle(100, 0, 400, 400, 0.1, 4)
circles.append(c1)
c2 = circle(50, 0, 50, 50, 0.08, 10)
circles.append(c2)
c3 = circle(10, 0, 300, 200, 0.02, -3)
circles.append(c3)
running = True
while running:
screen.fill(background)
for obj in circles:
obj.Move()
obj.Render()
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.display.flip()
pygame.quit()
In short: a circle collides with itself. The reason is simply that the circles list contains [c1,c2,c3] and thus checks are done against the circles themselves.
Now for c1 you check whether there is a collision so it iterates over the circles and the first thing it checks is whether it collides with itself (since c1 is the first element in the list). And obviously it does (your test looks if the distance is less than the sum of the circles radiuses, but the distance is zero). If none of the circles collide, there will thus be three exclamation marks (one for each circle).
You can resolve this error by performing a reference equality check first:
def CheckCollisions(self):
for c in circles:
if c is not self:
distance = math.sqrt((c.x - self.x)*(c.x - self.x) + (c.y - self.y)*(c.y - self.y))
#...

Categories

Resources