Related
I'm trying to do a Solar System simulator in python, with the Ursina engine, with physics. It works correctly until the earth (the only planet existing for the moment) gets in the same position on one or two axis than the sun. Then it just starts to shake and clipping and no-clipping of reality for no reason, following an straight line, usually the z axis.
Ursina's discord answer wasn't too helpful, since they lend me a code with that didn't had physics or elliptical orbits, which are the base of what I'm trying to do.
Here's the code:
from ursina import *
from ursina.prefabs.first_person_controller import FirstPersonController
from ursina.texture_importer import load_texture
import math
app = Ursina()
time_multiplicator = 1
G = 0.004
class Sun(Entity):
def __init__(self, position, color, scale, mass):
super().__init__(
parent = scene,
position = position,
origin = (0,0),
model = "sphere",
color = color,
collider = "mesh",
scale = scale
)
self.mass = mass
self.attraction_active = True
sun = Sun(position = (25,5,5), color = color.yellow, scale = 10, mass = 1500)
class Planet(Entity):
def __init__(self, position, color, scale, mass):
super().__init__(
parent = scene,
position = position,
origin = (0,0),
model = "sphere",
color = color,
scale = scale
)
self.mass = mass
self.attraction_active = True
self.initial_velocity = 0.4
def attraction(self):
self.gravitational_attraction = 1 + G * (self.mass * sun.mass)/(distance(sun,self)**2)
self.force_angle = 57.2958 * (math.atan((sun.y - self.y)/(sun.x - self.x))) + 1
self.y_component = self.gravitational_attraction * math.sin(self.force_angle) + 1
self.x_component = self.gravitational_attraction * math.cos(self.force_angle) + 1
print(f"gravitational_attraction ::: {self.gravitational_attraction}")
print(f"force_angle ::: {self.force_angle}")
print(f"y_component ::: {self.y_component}")
print(f"x_component ::: {self.x_component}")
def update(self):
self.attraction()
self.z -= self.initial_velocity * time.dt
self.y += self.y_component * time.dt
self.x += self.x_component * time.dt
blue = Planet(position = (0,0,0), color = color.blue, scale = 1, mass = 100)
print(distance(sun, blue))
EditorCamera()
def input(key):
if key == "q":
camera.look_at(blue)
app.run()
caveat: I just started tinkering with ursina ... pretty neat.
Your orig code had the earth getting sucked into the sun and the jitter was because it was near co-located. I printed the x,y coords and watched a bit.
You have a couple issues with the math and the physics.
You need to keep track of velocities, not just positions, unless I'm missing something with ursina. Recall that we can integrate to get velocities from force and position from velocity...
delta_v = F * dt
delta_pos = velocity * dt
Also, you need to use math.atan2 because it keeps track of both the x and y coordinate signage so that when things cross axes, you still get the correct sign of the angle.
It wasn't clear why you were adding "+1" to everything, so I removed it.
So after that, it is just a (non trivial) matter of putting an initial velocity on the earth so that the orbit is stable and doesn't get (a) sucked in, or (b) go flinging off into space due to lack of orbital capture. I tinkered with the velocities a bit and the below has a rotational orbit, but a weird one. I think you can tinker with the below and get to a working model.
from ursina import *
import sys
from ursina.prefabs.first_person_controller import FirstPersonController
from ursina.texture_importer import load_texture
import math
app = Ursina()
time_multiplicator = .1
G = 0.004
class Sun(Entity):
def __init__(self, position, color, scale, mass):
super().__init__(
parent = scene,
position = position,
origin = (0,0,0),
model = "sphere",
color = color,
collider = "mesh",
scale = scale
)
self.mass = mass
self.attraction_active = True
sun = Sun(position = (25,15,0), color = color.yellow, scale = 10, mass = 1500)
class Planet(Entity):
def __init__(self, position, color, scale, mass):
super().__init__(
parent = scene,
position = position,
origin = (0,0,0),
model = "sphere",
color = color,
scale = scale
)
self.mass = mass
self.attraction_active = True
self.velocity = [1, -3, 0] # vector vx, vy, vz
def attraction(self):
self.gravitational_attraction = G * (self.mass * sun.mass)/(distance(sun,self)**2)
self.force_angle = math.atan2( (-self.y + sun.y),(-self.x + sun.x))
self.y_component = self.gravitational_attraction * math.sin(self.force_angle)
self.x_component = self.gravitational_attraction * math.cos(self.force_angle)
print(f"gravitational_attraction ::: {self.gravitational_attraction}")
print(f"force_angle ::: {self.force_angle}")
print(f"y_component ::: {self.y_component}")
print(f"x_component ::: {self.x_component}")
print(f"x, y ::: {self.x}, {self.y}")
#if self.force_angle < 0: sys.exit(-1)
def update(self):
self.attraction()
# update the velocities, with update of F * dt
self.velocity[2] += 0 # no z velocity
self.velocity[1] += self.y_component * time.dt
self.velocity[0] += self.x_component * time.dt
# now update the positions with update = vel * dt
self.x += self.velocity[0]* time.dt
self.y += self.velocity[1]* time.dt
self.z += self.velocity[2]* time.dt
blue = Planet(position = (0,0,0), color = color.blue, scale = 1, mass = 100)
print(distance(sun, blue))
#EditorCamera()
# def input(key):
# if key == "q":
camera.position=(0,0,-200)
I am an aerospace student working on a school project for our python programming course. The assignment is create a program only using Pygame and numpy. I decided to create a wind tunnel simulation that simulates the airflow over a two dimensional wing. I was wondering if there is a more efficient way of doing the computation from a programming perspective. I will explain the program:
I have attached an image here:
The (steady) flow field is modeled using the vortex panel method. Basically, I am using a grid of Nx times Ny points where at each point a velocity (u,v) vector is given. Then using Pygame I map these grid points as circles, so that they resemble an area of influence. The grid points are the grey circles in the following image:
I create N particles and determine their velocities by iterating as follows:
create a list of particles.
create a grid list.
for each gridpoint in grid list:
for each particle in list of particles:
if particle A is within the area of influence of grid point n (xn,yn): particle A its velocity = velocity at grid point n.
Visualize everything in Pygame.
this basic way was the only way I could think of visualizing the flow in Pygame. The simulation works pretty well, but If I increase the number of grid points(increase the accuracy of the flow field), the performance decreases. My question is if there is a more efficient way to do this just using pygame and numpy?
I have attached the code here:
import pygame,random,sys,numpy
from Flow import Compute
from pygame.locals import *
import random, math, sys
#from PIL import Image
pygame.init()
Surface = pygame.display.set_mode((1000,600))
#read the airfoil geometry from a dat file
with open ('./resources/naca0012.dat') as file_name:
x, y = numpy.loadtxt(file_name, dtype=float, delimiter='\t', unpack=True)
#parameters used to describe the flow
Nx=30# 30 column grid
Ny=10#10 row grid
N=20#number of panels
alpha=0#angle of attack
u_inf=1#freestream velocity
#compute the flow field
u,v,X,Y= Compute(x,y,N,alpha,u_inf,Nx,Ny)
#The lists used for iteration
Circles = []
Particles= []
Velocities=[]
#Scaling factors used to properly map the potential flow datapoints into Pygame
magnitude=400
vmag=30
umag=30
panel_x= numpy.multiply(x,magnitude)+315
panel_y= numpy.multiply(-y,magnitude)+308
#build the grid suited for Pygame
grid_x= numpy.multiply(X,magnitude)+300
grid_y= numpy.multiply(Y,-1*magnitude)+300
grid_u =numpy.multiply(u,umag)
grid_v =numpy.multiply(v,-vmag)
panelcoordinates= zip(panel_x, panel_y)
# a grid area
class Circle:
def __init__(self,xpos,ypos,vx,vy):
self.radius=16
self.x = xpos
self.y = ypos
self.speedx = 0
self.speedy = 0
#create the grid list
for i in range(Ny):
for s in range(Nx):
Circles.append(Circle(int(grid_x[i][s]),int(grid_y[i][s]),grid_u[i][s],grid_v[i][s]))
Velocities.append((grid_u[i][s],grid_v[i][s]))
#a particle
class Particle:
def __init__(self,xpos,ypos,vx,vy):
self.image = pygame.Surface([10, 10])
self.image.fill((150,0,0))
self.rect = self.image.get_rect()
self.width=4
self.height=4
self.radius =2
self.x = xpos
self.y = ypos
self.speedx = 30
self.speedy = 0
#change particle velocity if collision with grid point
def CircleCollide(Circle,Particle):
Particle.speedx = int(Velocities[Circles.index((Circle))][0])
Particle.speedy = int(Velocities[Circles.index((Circle))][1])
#movement of particles
def Move():
for Particle in Particles:
Particle.x += Particle.speedx
Particle.y += Particle.speedy
#create particle streak
def Spawn(number_of_particles):
for i in range(number_of_particles):
i=i*(300/number_of_particles)
Particles.append(Particle(0, 160+i,1,0))
#create particles again if particles are out of wake
def Respawn(number_of_particles):
for Particle in Particles:
if Particle.x >1100:
Particles.remove(Particle)
if Particles==[]:
Spawn(number_of_particles)
#Collsion detection using pythagoras and distance formula
def CollisionDetect():
for Circle in Circles:
for Particle in Particles:
if Particle.y >430 or Particle.y<160:
Particles.remove(Particle)
if math.sqrt( ((Circle.x-Particle.x)**2) + ((Circle.y-Particle.y)**2) ) <= (Circle.radius+Particle.radius):
CircleCollide(Circle,Particle)
#draw everything
def Draw():
Surface.fill((255,255,255))
#Surface.blit(bg,(-300,-83))
for Circle in Circles:
pygame.draw.circle(Surface,(245,245,245),(Circle.x,Circle.y),Circle.radius)
for Particle in Particles:
pygame.draw.rect(Surface,(150,0,0),(Particle.x,Particle.y,Particle.width,Particle.height),0)
#pygame.draw.rect(Surface,(245,245,245),(Circle.x,Circle.y,1,16),0)
for i in range(len(panelcoordinates)-1):
pygame.draw.line(Surface,(0,0,0),panelcoordinates[i],panelcoordinates[i+1],3)
pygame.display.flip()
def GetInput():
keystate = pygame.key.get_pressed()
for event in pygame.event.get():
if event.type == QUIT or keystate[K_ESCAPE]:
pygame.quit();sys.exit()
def main():
#bg = pygame.image.load("pressure.png")
#bg = pygame.transform.scale(bg,(1600,800))
#thesize= bg.get_rect()
#bg= bg.convert()
number_of_particles=10
Spawn(number_of_particles)
clock = pygame.time.Clock()
while True:
ticks = clock.tick(60)
GetInput()
CollisionDetect()
Move()
Respawn(number_of_particles)
Draw()
if __name__ == '__main__': main()
The code requires another script that computes the flow field itself. It also reads datapoints from a textfile to get the geometry of the wing.
I have not provided these two files, but I can add them if necessary. Thank you in advance.
One bottleneck in your code is likely collision detection. CollisionDetect() computes the distance between each particle and each circle. Then, if a collision is detected, CircleCollide() finds the index of the circle in Circles (a linear search), so that the velocities can be retrieved from the same index in Velocities. Clearly this is ripe for improvement.
First, the Circle class already has the velocities in the speedx/speedy attributes, so Velocities can be eliminated .
Second, because the circles are at fixed locations, you can calculate which circle is closest to any given particle from the position of the particle.
# You may already have these values from creating grid_x etc.
# if not, you only need to calculated them once, because the
# circles don't move
circle_spacing_x = Circles[1].x - Circles[0].x
circle_spacing_y = Circles[Nx].y - Circles[0].y
circle_first_x = Circles[0].x - circle_spacing_x / 2
circle_first_y = Circles[0].y - circle_spacing_y / 2
Then CollisionDetect() becomes:
def CollisionDetect():
for particle in Particles:
if particle.y >430 or particle.y<160:
Particles.remove(particle)
continue
c = (particle.x - circle_first_x) // circle_spacing_x
r = (particle.y - circle_first_y) // circle_spacing_y
circle = Circles[r*Nx + c]
if ((circle.x - particle.x)**2 + (circle.y - particle.y)**2
<= (circle.radius+particle.radius)**2):
particle.speedx = int(circle.speedx)
particle.speedy = int(circle.speedy)
I've tidied up your code and made a few changes, namely adding scope to your classes and introducing a couple more. Without further knowledge of Flow I cannot test this fully, but if you could get back to me I can do some more. I'm assuming here that the 'flow field' can be simulated by the numpy.meshgrid function.
import pygame,numpy,sys
import pygame.locals
import math
class Particle:
def __init__(self,xpos,ypos,vx,vy):
self.size = numpy.array([4,4])
self.radius =2
self.pos = numpy.array([xpos,ypos])
self.speed = numpy.array([30,0])
self.rectangle = numpy.hstack((self.pos,self.size))
def move(self):
self.pos += self.speed
self.rectangle = numpy.hstack((self.pos,self.size))
def distance(self,circle1):
return math.sqrt(numpy.sum((circle1.pos - self.pos)**2))
def collision(self,circle1):
result = False
if self.pos[1] >430 or self.pos[1]<160:
result = True
if self.distance(circle1) <= (circle1.radius+self.radius):
self.speed = circle1.speed
return result
class Particles:
def __init__(self,num_particles):
self.num = num_particles
self.particles =[]
self.spawn()
def spawn(self):
for i in range(self.num):
i=i*(300/self.num)
self.particles.append(Particle(0, 160+i,1,0))
def move(self):
for particle in self.particles:
particle.move()
if particle.pos[0] >1100:
self.particles.remove(particle)
if not self.particles: self.spawn()
def draw(self):
for particle in self.particles:
pygame.draw.rect(Surface,(150,0,0),particle.rectangle,0)
def collisiondetect(self,circle1):
for particle in self.particles:
if particle.collision(circle1):
self.particles.remove(particle)
def GetInput():
keystate = pygame.key.get_pressed()
for event in pygame.event.get():
if event.type == pygame.locals.QUIT or keystate[pygame.locals.K_ESCAPE]:
pygame.quit()
sys.exit()
#draw everything
def Draw(sw,cir):
Surface.fill((255,255,255))
cir.draw()
for i in range(panelcoordinates.shape[1]):
pygame.draw.line(Surface,(0,0,0),panelcoordinates[0,i-1],panelcoordinates[0,i],3)
sw.draw()
pygame.display.flip()
# a grid area
class Circle:
def __init__(self,xpos,ypos,vx,vy):
self.radius=16
self.pos = numpy.array([xpos,ypos])
self.speed = numpy.array([vx,vy])
class Circles:
def __init__(self,columns,rows):
self.list = []
grid_x,grid_y = numpy.meshgrid(numpy.linspace(0,1000,columns),numpy.linspace(200,400,rows))
grid_u,grid_v = numpy.meshgrid(numpy.linspace(20,20,columns),numpy.linspace(-1,1,rows))
for y in range(rows):
for x in range(columns):
c1= Circle(int(grid_x[y,x]),int(grid_y[y,x]),grid_u[y,x],grid_v[y,x])
self.list.append(c1)
def draw(self):
for circle in self.list:
pygame.draw.circle(Surface,(245,245,245),circle.pos,circle.radius)
def detectcollision(self,parts):
for circle in self.list:
parts.collisiondetect(circle)
if __name__ == '__main__':
#initialise variables
number_of_particles=10
Nx=30
Ny=10
#circles and particles
circles1 = Circles(Nx,Ny)
particles1 = Particles(number_of_particles)
#read the airfoil geometry
panel_x = numpy.array([400,425,450,500,600,500,450,425,400])
panel_y = numpy.array([300,325,330,320,300,280,270,275,300])
panelcoordinates= numpy.dstack((panel_x,panel_y))
#initialise PyGame
pygame.init()
clock = pygame.time.Clock()
Surface = pygame.display.set_mode((1000,600))
while True:
ticks = clock.tick(6)
GetInput()
circles1.detectcollision(particles1)
particles1.move()
Draw(particles1,circles1)
I've also made some a crude stab at drawing a aerofoil, as again I don't have the knowledge of the data file with the coordinates.
So I've been attempting to make some dots not only come towards a circle but also to make them orbit it. To do this I am using cosine and sine, however I'm running into issues with getting the dots to move forward as well as setting their distance. With the code below the dots are able to form a circle around the bigger dot, as well as follow it, but they don't approach the dot nor do they, when having the coordinates scaled by their distance from t1, come to that location, but instead do funky stuff. This is referring specifically to the line
t2.goto(2 * (t1.xcor() + math.degrees(math.cos(math.radians(t1.towards(t2)))) // 1), 2 * (t1.ycor() + math.degrees(math.sin(math.radians(t1.towards(t2)))) // 1))
which I had replaced with:
t2.goto(dist * (t1.xcor() + math.degrees(math.cos(math.radians(t1.towards(t2)))) // 1), dist * (t1.ycor() + math.degrees(math.sin(math.radians(t1.towards(t2)))) // 1))
and that gave me the sporadic view of the dots attempting to follow the bigger dot.
This line is found in the follow() function. Create() makes the smaller dots, move() moves the bigger dot and grow() grows the bigger dot on collision with the smaller dots. Produce() and redraw() are supposed to be a stage 2 of the program, but those functions are irrelevant to the question. Finally, quit() just exits the Screen() and quits the program.
Thanks to cdlane for help with organizing data and updating the screen more efficiently.
Code as of now:
from turtle import Turtle, Screen
import sys
import math
CURSOR_SIZE = 20
def move(x, y):
""" has it follow cursor """
t1.ondrag(None)
t1.goto(x, y)
screen.update()
t1.ondrag(move)
def grow():
""" grows t1 shape """
global t1_size, g
t1_size += 0.1
t1.shapesize(t1_size / CURSOR_SIZE)
g -= .1
t1.color((r/255, g/255, b/255))
screen.update()
def follow():
""" has create()'d dots follow t1 """
global circles, dist
new_circles = []
for (x, y), stamp in circles:
t2.clearstamp(stamp)
t2.goto(x, y)
dist = t2.distance(t1) / 57.29577951308232 // 1
t2.goto(2 * (t1.xcor() + math.degrees(math.cos(math.radians(t1.towards(t2)))) // 1), 2 * (t1.ycor() + math.degrees(math.sin(math.radians(t1.towards(t2)))) // 1))
t2.setheading(t2.towards(t1))
if t2.distance(t1) < t1_size // 1:
if t2.distance(t1) > t1_size * 1.2:
t2.forward(500/t2.distance(t1)//1)
else:
t2.forward(3)
if t2.distance(t1) > t1_size // 2:
new_circles.append((t2.position(), t2.stamp()))
else:
grow() # we ate one, make t1 fatter
screen.update()
circles = new_circles
if circles:
screen.ontimer(follow, 10)
else:
phase = 1
produce()
def create():
""" create()'s dots with t2 """
count = 0
nux, nuy = -400, 300
while nuy > -400:
t2.goto(nux, nuy)
if t2.distance(t1) > t1_size // 2:
circles.append((t2.position(), t2.stamp()))
nux += 20
count += 1
if count == 40:
nuy -= 50
nux = -400
count = 0
screen.update()
def quit():
screen.bye()
sys.exit(0)
def redraw():
t2.color("black")
t2.shapesize((t2_size + 4) / CURSOR_SIZE)
t2.stamp()
t2.shapesize((t2_size + 2) / CURSOR_SIZE)
t2.color("white")
t2.stamp()
def produce():
#create boundary of star
global t2_size, ironmax
t1.ondrag(None)
t1.ht()
t2.goto(t1.xcor(), t1.ycor())
t2.color("black")
t2.shapesize((t1_size + 4) / CURSOR_SIZE)
t2.stamp()
t2.shapesize((t1_size + 2) / CURSOR_SIZE)
t2.color("white")
t2.stamp()
#start producing helium
while t2_size < t1_size:
t2.color("#ffff00")
t2.shapesize(t2_size / 20)
t2.stamp()
t2_size += .1
redraw()
screen.update()
ironmax = t2_size
t2_size = 4
while t2_size < ironmax:
t2.shapesize(t2_size / 20)
t2.color("grey")
t2.stamp()
t2_size += .1
screen.update()
# variables
t1_size = 6
circles = []
phase = 0
screen = Screen()
screen.screensize(900, 900)
#screen.mode("standard")
t2 = Turtle('circle', visible=False)
t2.shapesize(4 / CURSOR_SIZE)
t2.speed('fastest')
t2.color('purple')
t2.penup()
t2_size = 4
t1 = Turtle('circle')
t1.shapesize(t1_size / CURSOR_SIZE)
t1.speed('fastest')
r = 190
g = 100
b = 190
t1.color((r/255, g/255, b/255))
t1.penup()
t1.ondrag(move)
screen.tracer(False)
screen.listen()
screen.onkeypress(quit, "Escape")
create()
follow()
#print(phase)
screen.mainloop()
I took another crack at this, just looking at the problem of meteors swarming around a planet. Or in this case, moon as I chose Deimos as my model. I attempted to work at scale making the coordinate system 1 pixel = 1 kilometer. At the start, Deimos sits in a field of meteors each of which has a random heading but they all have the same size and velocity:
from turtle import Turtle, Screen
from random import random
METEOR_VELOCITY = 0.011 # kilometers per second
METEOR_RADIUS = 0.5 # kilometers
SECONDS_PER_FRAME = 1000 # each updates represents this many seconds passed
UPDATES_PER_SECOND = 100
DEIMOS_RADIUS = 6.2 # kilometers
G = 0.000003 # Deimos gravitational constant in kilometers per second squared
CURSOR_SIZE = 20
def follow():
global meteors
new_meteors = []
t = SECONDS_PER_FRAME
for (x, y), velocity, heading, stamp in meteors:
meteor.clearstamp(stamp)
meteor.goto(x, y)
meteor.setheading(heading)
meteor.forward(velocity * t)
meteor.setheading(meteor.towards(deimos))
meteor.forward(G * t * t)
meteor.setheading(180 + meteor.towards(x, y))
if meteor.distance(deimos) > DEIMOS_RADIUS * 2:
new_meteors.append((meteor.position(), velocity, meteor.heading(), meteor.stamp()))
screen.update()
meteors = new_meteors
if meteors:
screen.ontimer(follow, 1000 // UPDATES_PER_SECOND)
def create():
""" create()'s dots with meteor """
count = 0
nux, nuy = -400, 300
while nuy > -400:
meteor.goto(nux, nuy)
if meteor.distance(deimos) > DEIMOS_RADIUS * 2:
heading = random() * 360
meteor.setheading(heading) # all meteors have random heading but fixed velocity
meteors.append((meteor.position(), METEOR_VELOCITY, meteor.heading(), meteor.stamp()))
nux += 20
count += 1
if count % 40 == 0:
nuy -= 50
nux = -400
screen.update()
meteors = []
screen = Screen()
screen.screensize(1000, 1000)
screen.setworldcoordinates(-500, -500, 499, 499) # 1 pixel = 1 kilometer
meteor = Turtle('circle', visible=False)
meteor.shapesize(2 * METEOR_RADIUS / CURSOR_SIZE)
meteor.speed('fastest')
meteor.color('purple')
meteor.penup()
deimos = Turtle('circle')
deimos.shapesize(2 * DEIMOS_RADIUS / CURSOR_SIZE)
deimos.color("orange")
deimos.penup()
screen.tracer(False)
create()
follow()
screen.mainloop()
The first variable to investigate is METEOR_VELOCITY. At the setting provided, most meteors will crash into the moon but a few obtain orbital velocity. If you halve its value, all meteors will crash into the moon. If you double its value, a few meteors obtain escape velocity, leaving the window; a few may crash into the moon; most will form an orbiting cloud that gets smaller and tighter.
I tossed the trigonometric stuff and reverted back to degrees instead of radians. I use vector addition logic to work out the motion.
In the end, it's just a crude model.
By changing 180 to some other offsets, for example 195, in the def follow() in cdlane's code,
meteor.setheading(195 + meteor.towards(x, y))
then the metors would not go straight (180 degree) towards the Deimos, but instead would show some spiral movement towards the center.
Great example provided!
Using a PS4 controller in pygame, I already figured out how to capture axis rotation which can vary to a -1 or 1, but I don't know how to convert those numbers to a color ring like scale in order to turn it into a hex triplet number.
The values mimicking that of a color ring is more important than anything as I don't want the joystick capturing a color while it's not in motion. Picture
( Since that was a bit confusing, essentially I want to be able to move my joystick around and capture an accurate hex triplet number based on where it has moved )
This is my code so far:
import pygame
# Define some colors
BLACK = ( 0, 0, 0)
WHITE = ( 62, 210, 255)
# This is a simple class that will help us print to the screen
# It has nothing to do with the joysticks, just outputting the
# information.
class TextPrint:
def __init__(self):
self.reset()
self.font = pygame.font.Font(None, 25)
def print(self, screen, textString):
textBitmap = self.font.render(textString, True, BLACK)
screen.blit(textBitmap, [self.x, self.y])
self.y += self.line_height
def reset(self):
self.x = 25
self.y = 25
self.line_height = 30
def indent(self):
self.x += 10
def unindent(self):
self.x -= 10
pygame.init()
# Set the width and height of the screen [width,height]
size = [900, 1080]
screen = pygame.display.set_mode(size)
pygame.display.set_caption("PS4Testing")
#Loop until the user clicks the close button.
done = False
# Used to manage how fast the screen updates
clock = pygame.time.Clock()
# Initialize the joysticks
pygame.joystick.init()
# Get ready to print
textPrint = TextPrint()
# -------- Main Program Loop -----------
while done==False:
# EVENT PROCESSING STEP
for event in pygame.event.get(): # User did something
if event.type == pygame.QUIT: # If user clicked close
done=True # Flag that we are done so we exit this loop
screen.fill(WHITE)
textPrint.reset()
# Get count of joysticks
joystick_count = pygame.joystick.get_count()
# For each joystick:
for i in range(joystick_count):
joystick = pygame.joystick.Joystick(i)
joystick.init()
# Usually axis run in pairs, up/down for one, and left/right for
# the other.
axes = joystick.get_numaxes()
for i in range( axes ):
axis = joystick.get_axis( i )
textPrint.print(screen, "Axis {} value: {:>6.3f}".format(i, axis) )
textPrint.unindent()
# ALL CODE TO DRAW SHOULD GO ABOVE THIS COMMENT
# Go ahead and update the screen with what we've drawn.
pygame.display.flip()
# Limit to 20 frames per second
clock.tick(20)
# Close the window and quit.
# If you forget this line, the program will 'hang'
# on exit if running from IDLE.
pygame.quit ()
Modified code from official pygame documentation
Any help would be greatly appreciated
My plan:
Find the angle of stick on joystick
Get RGB value using HSV and stick's angle
Convert to HEX
Finding the angle of joystick's stick
First, we need to find the joystick's angle. We can do this using the law of cosines and axis statements as lengths of the sides of a triangle (because they are from one point/center).
Store axis statements in this block:
for i in range( axes ):
axis = joystick.get_axis( i )
#Storing axis statement
if i == 0:
Xaxis = axis
elif i == 1:
Yaxis = axis
textPrint.print(screen, "Axis {} value: {:>6.3f}".format(i, axis) )
We store them because in for loop we can take only one statement at a time.
Then define a function that will return the angle of the stick. We need to use math module for pi and acos.
def get_angle(Xaxis,Yaxis):
#To avoid ZeroDivisionError
#P.S. - you can improve it a bit.
if Xaxis == 0:
Xaxis = 0.001
if Yaxis == 0:
Yaxis = 0.001
#defining the third side of a triangle using the Pythagorean theorem
b = ((Xaxis)**2 + (Yaxis)**2)**0.5
c = Xaxis
a = Yaxis
#Using law of cosines we'll find angle using arccos of cos
#math.acos returns angles in radians, so we need to multiply it by 180/math.pi
angle = math.acos((b**2 + c**2 - a**2) / (2*b*c)) * 180/math.pi
#It'll fix angles to be in range of 0 to 360
if Yaxis > 0:
angle = 360 - angle
return angle
Now get_angle(Xaxis,Yaxis) will return stick's angle. East is 0°, north is 90°, west is 180°, south is 270°.
Getting HSV and converting to RGB
We have stick's angle and we can use it to make an HSV color and then convert it to RGB, because Color Wheel is based on hue of the colors.
Hue has the same range as the stick's angle, so we can use stick's angle as hue.
Using the formulas from Wikipedia, define a function that converts HSV to RGB.
def hsv_to_rgb(H,S,V):
#0 <= H <= 360
#0 <= S <= 1
#0 <= V <= 1
C = V * S
h = H/60
X = C * (1 - abs(h % 2 -1))
#Yes, Python can compare like "0 <= 2 > 1"
if 0 <= h <= 1:
r = C; g = X; b = 0
elif 1 <= h <= 2:
r = X; g = C; b = 0
elif 2 <= h <= 3:
r = 0; g = C; b = X
elif 3 <= h <= 4:
r = 0; g = X; b = C
elif 4 <= h <= 5:
r = X; g = 0; b = C
elif 5 <= h < 6:
r = C; g = 0; b = X
m = V - C
#Final computing and converting from 0 - 1 to 0 - 255
R = int((r+m)*255)
G = int((g+m)*255)
B = int((b+m)*255)
return [R,G,B]
Now hsv_to_rgb(get_angle(Xaxis,Yaxis),1,1) will return list of red, green and blue in range of 0 - 255 (for an easier conversion in hex). Saturation and Value are not necessary in this case.
Conversion to HEX
The easiest part. We need to get the list of RGB and convert values to hex.
colors = hsv_to_rgb(get_angle(Xaxis,Yaxis),1,1)
#Converting to hex
lst = list(map(hex,colors))
#Cutting the "0x" part
for i in range(len(lst)):
lst[i] = lst[i][2:]
#If one of the colors has only one digit, extra 0 will be added for a better look
if len(lst[i]) == 1:
lst[i] = "0"+str(lst[i])
print(get_angle(Xaxis,Yaxis))
Something like #ff0000, #00ff00, and #0000ff will be printed.
You also can make your program to show color change in real time, simply add WHITE = colors in this block. DO NOT use it if you have photosensitive epilepsy.
Merging everything
Place both functions from first and second points somewhere at the beginning.
Add the storing from the first point to the block
for i in range( axes ):
axis = joystick.get_axis( i )
textPrint.print(screen, "Axis {} value: {:>6.3f}".format(i, axis) )
Add the conversion from the third point after the block. I recommend to make a death zone feature.
death_zone = 0.1
if abs(Xaxis) > death_zone or abs(Yaxis) > death_zone:
#If you prefer HSV color wheel, use hsv_to_rgb(get_angle(Xaxis,Yaxis),1,1)
#Else if you prefer RGB color wheel, use hsv_to_rgb(360-get_angle(Xaxis,Yaxis),1,1)
colors = hsv_to_rgb(get_angle(Xaxis,Yaxis),1,1)
#Converting to hex
lst = list(map(hex,colors))
#Cutting the "0x" part
for i in range(len(lst)):
lst[i] = lst[i][2:]
#If one of the colors has only one digit, extra 0 will be added for a better look
if len(lst[i]) == 1:
lst[i] = "0"+str(lst[i])
print("#"+"".join(lst))
That's how your code may look after all:
P.S. - you will probably have to change some code, because I thing my joystick's axis weren't properly captured.
import pygame
import math
# Define some colors
BLACK = ( 0, 0, 0)
WHITE = ( 62, 210, 255)
def hsv_to_rgb(H,S,V):
#Accirding to https://en.wikipedia.org/wiki/HSL_and_HSV#From_HSV
#0 <= H <= 360
#0 <= S <= 1
#0 <= V <= 1
C = V * S
h = H/60
X = C * (1 - abs(h % 2 -1))
#Yes, Python can compare like "0 <= 2 > 1"
if 0 <= h <= 1:
r = C; g = X; b = 0
elif 1 <= h <= 2:
r = X; g = C; b = 0
elif 2 <= h <= 3:
r = 0; g = C; b = X
elif 3 <= h <= 4:
r = 0; g = X; b = C
elif 4 <= h <= 5:
r = X; g = 0; b = C
elif 5 <= h < 6:
r = C; g = 0; b = X
m = V - C
#Final computing and converting from 0 - 1 to 0 - 255
R = int((r+m)*255)
G = int((g+m)*255)
B = int((b+m)*255)
return [R,G,B]
def get_angle(Xaxis,Yaxis):
#To avoid ZeroDivisionError
#P.S. - you can improve it a bit.
if Xaxis == 0:
Xaxis = 0.001
if Yaxis == 0:
Yaxis = 0.001
#defining the third side of a triangle using the Pythagorean theorem
b = ((Xaxis)**2 + (Yaxis)**2)**0.5
c = Xaxis
a = Yaxis
#Using law of cosines we'll fing angle using arccos of cos
#math.acos returns angles in radians, so we need to multiply it by 180/math.pi
angle = math.acos((b**2 + c**2 - a**2) / (2*b*c)) * 180/math.pi
#It'll fix angles to be in range of 0 to 360
if Yaxis > 0:
angle = 360 - angle
return angle
# This is a simple class that will help us print to the screen
# It has nothing to do with the joysticks, just outputting the
# information.
class TextPrint:
def __init__(self):
self.reset()
self.font = pygame.font.Font(None, 25)
def print(self, screen, textString):
textBitmap = self.font.render(textString, True, BLACK)
screen.blit(textBitmap, [self.x, self.y])
self.y += self.line_height
def reset(self):
self.x = 25
self.y = 25
self.line_height = 30
def indent(self):
self.x += 10
def unindent(self):
self.x -= 10
pygame.init()
# Set the width and height of the screen [width,height]
size = [900, 1080]
screen = pygame.display.set_mode(size)
pygame.display.set_caption("PS4Testing")
#Loop until the user clicks the close button.
done = False
# Used to manage how fast the screen updates
clock = pygame.time.Clock()
# Initialize the joysticks
pygame.joystick.init()
# Get ready to print
textPrint = TextPrint()
# -------- Main Program Loop -----------
while done==False:
# EVENT PROCESSING STEP
for event in pygame.event.get(): # User did something
if event.type == pygame.QUIT: # If user clicked close
done=True # Flag that we are done so we exit this loop
screen.fill(WHITE)
textPrint.reset()
# Get count of joysticks
joystick_count = pygame.joystick.get_count()
# For each joystick:
for i in range(joystick_count):
joystick = pygame.joystick.Joystick(i)
joystick.init()
# Usually axis run in pairs, up/down for one, and left/right for
# the other.
axes = joystick.get_numaxes()
for i in range( axes ):
axis = joystick.get_axis( i )
#Storing axis statement
if i == 0:
Xaxis = axis
elif i == 1:
Yaxis = axis
textPrint.print(screen, "Axis {} value: {:>6.3f}".format(i, axis) )
textPrint.unindent()
#If joystick is not in the center
#Death zone is used to not capture joystick if it's very close to the center
death_zone = 0.1
if abs(Xaxis) > death_zone or abs(Yaxis) > death_zone:
#If you prefer HSV color wheel, use hsv_to_rgb(get_angle(Xaxis,Yaxis),1,1)
#Else if you prefer RGB color wheel, use hsv_to_rgb(360-get_angle(Xaxis,Yaxis),1,1)
colors = hsv_to_rgb(get_angle(Xaxis,Yaxis),1,1)
#Converting to hex
lst = list(map(hex,colors))
#Cutting the "0x" part
for i in range(len(lst)):
lst[i] = lst[i][2:]
#If one of the colors has only one digit, extra 0 will be added for a better look
if len(lst[i]) == 1:
lst[i] = "0"+str(lst[i])
print("#"+"".join(lst))
#You can use it to see color change in real time.
#But I don't recomend to use it if you have photosensitive epilepsy.
#WHITE = colors
# ALL CODE TO DRAW SHOULD GO ABOVE THIS COMMENT
# Go ahead and update the screen with what we've drawn.
pygame.display.flip()
# Limit to 20 frames per second
clock.tick(20)
# Close the window and quit.
# If you forget this line, the program will 'hang'
# on exit if running from IDLE.
pygame.quit ()
Extend the projectile and tracker to implement the following graphics game:
a. There is square box of a given dimension (choose one reasonable size box) in which
there is a projectile which randomly bounces of the four walls.
b. When a projectile hits a wall, it is launched again by randomly selecting an angle in
the allowed range and a random velocity in a given range. For example, if the
projectile hits the bottom boundary, it selects a random angle in the range 0 to 180
degrees; if it hits the right vertical wall then, it selects random angle between 90 and
270 degrees; and so on.
c. Instead of drawing the circle, move the circle and also fill it with some color.
d. Extend to two projectiles.
Don't ask user the parameters. Use a set of parameters that you think the best.
So I am having trouble trying to find how to make two cballs (the projectiles) bounce from wall to wall. I figured how to bounce them off the bottom wall of the window but it doesn't stop at the right wall and bounce from there. Not only that I have no idea on how to make the balls follow instead of continuously printing circles to Track the movement of the balls. I use ".undraw()" but its very jumpy and not smooth at all.
I would appreciate the help very much! The assignment is due tomorrow and this is what i have:
# projectilebounce.py
# Projectile hits a wall and again is launced
from math import sin, cos, radians
from graphics import *
class Projectile:
def __init__(self, angle, velocity, height):
self.xpos = 0.0
self.ypos = height
theta = radians(angle)
self.xvel = velocity * cos(theta)
self.yvel = velocity * sin(theta)
def update(self, time):
self.xpos = self.xpos + time * self.xvel
yvel1 = self.yvel - 9.8 * time
self.ypos = self.ypos + time * (self.yvel + yvel1) / 2.0
self.yvel = yvel1
def getY(self):
return self.ypos
def getX(self):
return self.xpos
def reset(self, angle, velocity, height):
self.xpos = self.getX()
self.ypos = height
theta = radians(angle)
self.xvel = velocity * cos(theta)
self.yvel = velocity * sin(theta)
class Tracker:
def __init__(self, window, objToTrack):
self.win = window
self.obj = objToTrack
def update(self):
a = Circle( Point(self.obj.getX(), self.obj.getY()), 5)
a.draw(self.win)
a.setFill("Black")
a.undraw()
def getInputs():
a1 = 60
a2 = 30
v1 = 60
v2 = 60
h1 = 10
h2 = 10
t = 0.1
return a1, a2, v1, v2, h1, h2, t
def main():
print("This program graphically depicts the flight of a cannonball.\n")
win = GraphWin("TRACKER", 600, 500)
win.setCoords(0.0, 0.0, 600, 500)
angle1, angle2, vel1, vel2, h01, h02, time = getInputs()
cball1 = Projectile(angle1, vel1, h01)
cball2 = Projectile(angle2, vel2, h02)
tracker_cball1 = Tracker(win, cball1)
tracker_cball2 = Tracker(win, cball2)
while True:
if cball1.getY() <= 0: #<---ball 1
cball1.reset(60, 60, 10)
else:
cball1.update(time)
tracker_cball1.update()
if cball2.getY() <= 0: #<---ball 2
cball2.reset(30, 60, 10)
else:
cball2.update(time)
tracker_cball2.update()
if __name__ == '__main__' : main()
Thank you!, I am a big noob at coding at the moment. Much appreciated!