I am a student in high school doing a summative project in tetris. Lately I've been having this problem where for certain positions, when my block collides with the RIGHT side of the screen, it sort of 'flickers'. I can't seem to find the reason why it is doing this, and I don't know if the shape is going out of the grid and coming in and being drawn again, I'm not sure and very confused.
So far I've been able to make random blocks spawn once the first one hits the ground, I've been able to do collision with the left side correctly for all my blocks, some stacking and rotation works as well. I've included the main ideas of how I did my code as a lot of it is repeated, so I included 2 examples of each. (2 different rotations, 2 ways shown of how I drew the block, etc.)
I'm really stuck so if someone could help that would be amazing, Thank you.
import pygame
import colors
import random
class Shape:
def __init__ (self, x, y, shape1, shape2, shape3, shape4, shapeType):
self.shape1 = shape1
self.shape2 = shape2
self.shape3 = shape3
self.shape4 = shape4
self.x = x
self.y = y
self.lasty = 0
self.direction = 0
self.collided = False
self.fc = 0
self.shapeType = shapeType
def draw (self, screen):
self.lasty = self.y
self.fc += 1
if pygame.key.get_pressed()[pygame.K_DOWN] and self.fc >= 5:
self.fc = 0
self.y += 39
elif self.fc >= 90:
self.fc = 0
self.y += 39
if pygame.key.get_pressed()[pygame.K_RIGHT] and self.fc >= 5:
self.fc = 0
self.x += 39
elif self.fc >= 90:
self.fc = 0
self.x += 39
## When the block collides with the bottom of the grid
if self.y >= 778:
self.y = 0
## I USE 'DIRECTION' TO REPRESENT THE DIFFERENT ROTATED POSITIONS -- ONLY 1 IS SHOWN BELOW BUT I HAVE (0-3 DIRECTIONS) = 4 DIFFERENT ROTATIONS
if self.direction == 0: # if the shape is in the first position
for y in range(len(self.shape1)): # RUNS A LOOP THAT GETS THE LENGTH OF THE SHAPE IN THE FIRST POSITION
for x in range(len(self.shape1[y])):
if self.shape1[y][x] == 1 and not self.collided:
if (self.y + 39*y) + 39 > 780: # CHECKS THAT THE SHAPE IS NOT GOING PASSED THE RIGHT SIDE OF THE GRID
self.collided = True
if (self.x + (39*x)) >= 739:
self.x = 740-(39*x)-39
for blok in blocks: ## stacking the blocks and checking if the block collides with the other blocks
if self.x + (39*x) == blok.x and self.y + (39*y) == blok.y:
self.collided = True
pygame.draw.rect(screen, colors.lightBlue,(self.x + (39*x), self.y + (39*y), 39,39))
elif self.direction == 1: # WHEN THE SHAPE IS IN THE SECOND ROTATION
for y in range(len(self.shape2)):
for x in range(len(self.shape2[y])):
if self.shape2[y][x] == 1 and not self.collided:
if(self.y + 39*y) + 39 > 780:
self.collided = True
if (self.x + (39*x)) >= 739:
self.x = 740-(39*x)-39
for blok in blocks:
if self.x + (39*x) == blok.x and self.y + (39*y) == blok.y:
self.collided = True
pygame.draw.rect(screen, colors.lightBlue,(self.x + (39*x), self.y + (39*y), 39,39))
if self.collided == True:
self.y = self.lasty
if self.direction == 0:
for y in range(len(self.shape1)):
for x in range(len(self.shape1[y])):
if self.shape1[y][x] == 1:
blocks.append(Block(self.x + (39 * x), self.y + (39 * y)))
elif self.direction == 1:
for y in range(len(self.shape2)):
for x in range(len(self.shape2[y])):
if self.shape2[y][x] == 1:
blocks.append(Block(self.x + (39 * x), self.y + (39 * y)))
class Block:
def __init__(self, x, y):
self.x = x
self.y = y
def draw(self, screen):
pygame.draw.rect(screen, colors.lightBlue, (self.x, self.y, 39, 39))
blocks = []
## EXAMPLE OF HOW EACH SHAPE IS DRAWN
## L SHAPE
b = Shape(350,0, [[1,1],[0,1],[0,1]], [[0,0,1],[1,1,1]], [[1,0,],[1,0], [1,1]], [[1,1,1],[1,0,0]],"L SHAPE")
## Z SHAPE
##b = Shape(300,300, [[0,1],[1,1],[1,0]], [[1,1],[0,1,1]], [[0,1],[1,1],[1,0]], [[1,1],[0,1,1]])
# FUNCTION FOR THE GRID
def drawGrid(_x, _y, screen,width,height, columns,rows ):
for y in range(0, rows+1):
for x in range(0, columns+1):
screen.fill(colors.grey, [(x*(width/columns))+_x, _y, 1, height])
screen.fill(colors.grey, [_x, (y*(height/rows))+_y, width, 1])
def drawGame(screen, scene): # UPDATING DRAW FUNCTION
global b
for evn in pygame.event.get():
if evn.type == pygame.QUIT:
quit()
return scene, True
elif evn.type == pygame.KEYDOWN:
if evn.key == pygame.K_UP: # IF THE UP ARROW KEY IS PRESSED, CHANGE THE ROTATION OF THE SHAPE
b.direction += 1
if b.direction == 4: # IF THE DIRECTION EQUALS 4 RESET TO THE FIRST POSITION
b.direction = 0
screen.fill(colors.black)
## ACCELERATES THE BLOCK DOWNWARDS
drawGrid(350, 0, screen, 390, 780, 10, 20)
if not b.collided:
b.draw(screen)
else: # CHECKS WHICH VALUE OUT OF (0-6) IS RECEIVED TO CHOOSE THE RANDOM SHAPE
i = random.randint(0,6)
if i == 0:
## L shape
b = Shape(350,0, [[1,1],[0,1],[0,1]], [[0,0,1],[1,1,1]], [[1,0,],[1,0], [1,1]], [[1,1,1],[1,0,0]],"L SHAPE")
elif i == 5:
## Z Shape
b = Shape(350, 0, [[0, 1], [1, 1], [1, 0]], [[1, 1], [0, 1, 1]], [[0, 1], [1, 1], [1, 0]],
[[1, 1], [0, 1, 1]],"Z SHAPE")
for i in blocks: # RUNS THE LOOP TO DRAW THE SHAPE
i.draw(screen) # DRAWS THE VALUE OF 'i' ONTO THE SCREEN
pygame.display.flip()
return scene, False
As it turns out, I was drawing some of the rectangles in the draw loop before some of them were checked for collision with the wall.
Related
I am working on this code challenge:
Given a 2D bot/robot which can only move in four directions, move forward which is UP(U), move backward which is DOWN(D), LEFT(L), RIGHT(R) in a 10x10 grid. The robot can't go beyond the 10x10 area.
Given a string consisting of instructions to move.
Output the coordinates of a robot after executing the instructions. Initial position of robot is at origin(0, 0).
Example:
Input : move = “UDDLRL”
Output : (-1, -1)
Explanation:
Move U : (0, 0)–(0, 1)
Move D : (0, 1)–(0, 0)
Move D : (0, 0)–(0, -1)
Move L : (0, -1)–(-1, -1)
Move R : (-1, -1)–(0, -1)
Move L : (0, -1)–(-1, -1)
Therefore final position after the complete
movement is: (-1, -1)
I got the code working without using the 10x10 grid information. How could I incorporate the 10x10 grid information into my solution in an OOP fashion? My solution doesn't follow the OOP principles.
# function to find final position of
# robot after the complete movement
def finalPosition(move):
l = len(move)
countUp, countDown = 0, 0
countLeft, countRight = 0, 0
# traverse the instruction string 'move'
for i in range(l):
# for each movement increment its respective counter
if (move[i] == 'U'):
countUp += 1
elif(move[i] == 'D'):
countDown += 1
elif(move[i] == 'L'):
countLeft += 1
elif(move[i] == 'R'):
countRight += 1
# required final position of robot
print("Final Position: (", (countRight - countLeft),
", ", (countUp - countDown), ")")
# Driver code
if __name__ == '__main__':
move = "UDDLLRUUUDUURUDDUULLDRRRR"
finalPosition(move)
This fixes it:
class Robot:
class Mover:
def __init__(self, x, y):
self.x, self.y = x, y
def new_pos(self, x, y):
new_x = x + self.x
new_y = y + self.y
if (new_x > 9 or new_y > 9):
raise ValueError("Box dimensions are greater than 10 X 10")
return new_x, new_y
WALKS = dict(U=Mover(0, 1), D=Mover(0, -1),
L=Mover(-1, 0), R=Mover(1, 0))
def move(self, moves):
x = y = 0
for id in moves:
x, y = self.WALKS[id].new_pos(x, y)
return (x,y)
if __name__ == '__main__':
moves2 = "UDDLLRUUUDUURUDDUULLDRRRR"
robot = Robot()
print(robot.move(moves2))
Output :
(2,3)
The way you use your counters makes it less trivial to detect that you would hit the border of the 10x10 grid. Without changing too much, you could replace the countUp and countDown variables by one countVertical variable, and add -1 to it when going up and 1 when going down. Then ignore a move if it would make that counter negative or greater than 9. And obviously you would do the same for horizontal movements.
[Edit: After the edit to your question, it turns out that you want the Y-coordinate to be opposite to what I assumed above. So I have changed the sign of the Y-coordinate updates (+1, -1).]
That's really it.
Now to make this more OOP, you could define a Robot class, which would maintain its x and y coordinate. Anyhow it would be good to remove the print call out of your function, so the function only deals with the movements, not with the reporting (separation of concern).
Here is how it could work:
class Robot:
def __init__(self, x=0, y=0):
self.position(x, y)
def position(self, x, y):
self.x = min(9, max(0, x))
self.y = min(9, max(0, y))
def move(self, moves):
for move in moves:
if move == 'U':
self.position(self.x, self.y + 1)
elif move == 'D':
self.position(self.x, self.y - 1)
elif move == 'L':
self.position(self.x - 1, self.y)
elif move == 'R':
self.position(self.x + 1, self.y)
else:
raise ValueError(f"Invalid direction '{move}'")
if __name__ == '__main__':
moves = "UDDLLRUUUDUURUDDUULLDRRRR"
robot = Robot(0, 0)
robot.move(moves)
print(f"Final position: {robot.x}, {robot.y}")
This question already has answers here:
How do I detect collision in pygame?
(5 answers)
How to detect collisions between two rectangular objects or images in pygame
(1 answer)
Closed 2 years ago.
I'm making a game environment for NEAT. The first two obstacles seem to collide with the players just fine, but none of the other obstacles do anything. The visuals won't work, but based on the size of the player list, yeah no it's not working. The collide class also wouldn't work before I started implementing NEAT, so there's that.
Anyway here's some (probably) irrelevant code:
import pygame
from pygame.locals import *
import random
import os
import neat
debugfun = 0
W = 300
H = 300
win = pygame.display.set_mode((W, H))
pygame.display.set_caption("bruv moment")
coords = [0, 60, 120, 180, 240]
class Player(object):
def __init__(self, x):
self.x = x
self.y = 600 - 60
self.width = 60
self.height = 60
self.pos = 0
self.left = False
self.right = False
def move(self):
if self.left:
self.pos -= 1
if self.right:
self.pos += 1
if self.pos < 0:
self.pos = 4
if self.pos > 4:
self.pos = 0
self.x = coords[self.pos]
class Block(object):
def __init__(self, pos, vel):
self.pos = pos
self.x = coords[self.pos]
self.y = -60
self.width = 60
self.height = 60
self.vel = vel
def move(self):
self.y += self.vel
def redraw_window():
pygame.draw.rect(win, (0, 0, 0), (0, 0, W, H))
for ob in obs:
pygame.draw.rect(win, (0, 255, 0), (ob.x, ob.y, ob.width, ob.height))
for ind in homies:
pygame.draw.rect(win, (255, 0, 0), (ind.x, ind.y, ind.width, ind.height))
pygame.display.update()
obs = []
homies = []
player_collision = False
ge = []
nets = []
And here's some relevant code:
def collide():
for ob in obs:
for x, ind in enumerate(homies):
if ind.y < ob.y + ob.height and ind.y + ind.height > ob.y and ind.x + ind.width > ob.x and ind.x < ob.x + ob.width:
ge[x].fitness -= 1
homies.pop(x)
nets.pop(x)
ge.pop(x)
def main(genomes, config):
speed = 30
pygame.time.set_timer(USEREVENT + 1, 550)
pygame.time.set_timer(USEREVENT + 2, 1000)
for _, g in genomes:
net = neat.nn.FeedForwardNetwork.create(g, config)
nets.append(net)
homies.append(Player(0))
g.fitness = 0
ge.append(g)
clock = pygame.time.Clock()
ran = True
while ran and len(homies) > 0:
clock.tick(27)
collide()
for x, ind in enumerate(homies):
ind.move()
ge[x].fitness += 0.1
try:
output = nets[x].activate((ind.x, abs(obs[0].x - ind.x)))
except IndexError:
output = 50
try:
if output in range(5, 25):
ind.left = True
else:
ind.left = False
if output > 25:
ind.right = True
else:
ind.right = False
except TypeError:
if output[0] in range(5, 25):
ind.left = True
else:
ind.left = False
if output[1] > 25:
ind.right = True
else:
ind.right = False
for ob in obs:
ob.move()
if ob.x > H:
obs.pop(obs.index(ob))
for event in pygame.event.get():
if event.type == pygame.QUIT:
ran = False
pygame.quit()
if event.type == USEREVENT+1:
if speed <= 200:
speed += 3
if event.type == USEREVENT+2:
for g in ge:
g.fitness += 0.5
if len(obs) == 0:
obs.append(Block(round(random.randint(0, 4)), speed))
print(len(homies))
print(len(obs))
redraw_window()
def run(config_file):
config = neat.config.Config(neat.DefaultGenome, neat.DefaultReproduction, neat.DefaultSpeciesSet,
neat.DefaultStagnation, config_path)
p = neat.Population(config)
p.add_reporter(neat.StdOutReporter(True))
stats = neat.StatisticsReporter()
p.add_reporter(stats)
winner = p.run(main, 50)
if __name__ == "__main__":
local_dir = os.path.dirname(__file__)
config_path = os.path.join(local_dir, "avoidpedofiles.txt")
run(config_path)
I am learning the basics of pygame in my free time at work. I wanted to move the bottom boundary of my program up, but when I changed the boundary collision condition, the ball moves in a straight line instead of at an angle.
When I delete the 525 part of the condition in the bounceCircle definition, it moves as expected. When I place it back, it moves in a horizontal line.
import pygame
import sys
import random
import math
# Initalize the game engine
pygame.init()
# Define common colors:
WHITE = (255, 255, 255)
BLUE = (0, 0, 255)
# Set window size, title, and background color
(width, height) = (900, 600)
screen = pygame.display.set_mode((width, height))
pygame.display.set_caption("Ball Playground")
screen.fill(WHITE)
# Used to manage how fast the screen updates
clock = pygame.time.Clock()
# Ball class
class Particles():
def __init__(self, position, radius):
self.x = position[0]
self.y = position[1]
self.radius = radius
self.color = (BLUE)
# thickness = 0 means filled
# thickness > 0 thicker border
# thickness < 0 nothing
self.thickness = 1
self.speed = 0
self.angle = 0
# Definition for drawing circle
def drawCircle(self):
pygame.draw.circle(screen, self.color, (int(self.x), int(self.y)), self.radius, self.thickness)
# Definition for moving the circle
def moveCircle(self):
self.x += math.sin(self.angle) * self.speed
self.y -= math.cos(self.angle) * self.speed
# Definition for bouncing off of surfaces
def bounceCircle(self):
if (self.x > width - self.radius) or (self.x < self.radius):
self.angle = - self.angle
elif (self.y > height - self.radius) or (self.y < 525 - self.radius):
self.angle = math.pi - self.angle
ball = Particles((450, 300), 40)
ball.speed = 2
ball.angle = random.uniform(0, math.pi*2)
# --------- Main Program Loop ----------
while True:
# --- Main Event Loop
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
pygame.display.update()
screen.fill(WHITE)
#--- Game Logic
ball.moveCircle()
ball.bounceCircle()
ball.drawCircle()
#--- Drawings
pygame.draw.line(screen, BLACK, [0, 525], [900, 525], 2)
# Prints tiny diaginal lines to mark surface
x1 = 0
x2 = 5
for i in range(0, width):
pygame.draw.line(screen, BLACK, [x1, 530], [x2, 525], 2)
x1 += 5
x2 += 5
pygame.display.flip()
clock.tick(60)
You need to edit the bounceCircle function to:
def bounceCircle(self):
if (self.x + self.radius > width ) or (self.x - self.radius < 0):
self.angle = - self.angle
elif (self.y + self.radius > (525)) or (self.y - self.radius < 0):
self.angle = math.pi - self.angle
Whole Code (fixed a few bugs):
import pygame
import sys
import random
import math
# Initalize the game engine
pygame.init()
# Define common colors:
WHITE = (255, 255, 255)
BLUE = (0, 0, 255)
# Set window size, title, and background color
(width, height) = (900, 600)
screen = pygame.display.set_mode((width, height))
pygame.display.set_caption("Ball Playground")
screen.fill(WHITE)
# Used to manage how fast the screen updates
clock = pygame.time.Clock()
# Ball class
class Particles():
def __init__(self, position, radius):
self.x = position[0]
self.y = position[1]
self.radius = radius
self.color = (BLUE)
# thickness = 0 means filled
# thickness > 0 thicker border
# thickness < 0 nothing
self.thickness = 1
self.speed = 0
self.angle = 0
# Definition for drawing circle
def drawCircle(self):
pygame.draw.circle(screen, self.color, (int(self.x), int(self.y)), self.radius, self.thickness)
# Definition for moving the circle
def moveCircle(self):
self.x += math.sin(self.angle) * self.speed
self.y -= math.cos(self.angle) * self.speed
print(self.angle, self.x, self.y)
# Definition for bouncing off of surfaces
def bounceCircle(self):
if (self.x > width - self.radius) or (self.x < self.radius):
self.angle = - self.angle
elif (self.y + self.radius > (height-100)) or (self.y - self.radius < 0):
self.angle = math.pi - self.angle
ball = Particles((450, 300), 40)
ball.speed = 20
ball.angle = random.uniform(0, math.pi*2)
print(ball.angle)
# --------- Main Program Loop ----------
while True:
# --- Main Event Loop
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
pygame.display.update()
screen.fill(WHITE)
#--- Game Logic
ball.moveCircle()
ball.bounceCircle()
ball.drawCircle()
#--- Drawings
pygame.draw.line(screen, BLUE, [0, 525], [900, 525], 2)
# Prints tiny diaginal lines to mark surface
x1 = 0
x2 = 5
for i in range(0, width):
pygame.draw.line(screen, BLUE, [x1, 530], [x2, 525], 2)
x1 += 5
x2 += 5
pygame.display.flip()
clock.tick(60)
After some work I finished my first algorithm for a maze generator and now I am trying to make it visual in Pygame. I first let the algorithm generate a maze and then I make a visual representation of it.
Here I get into multiple problems, but I think they are all linked to the same thing which is that the first cell of the maze gets overwritten in some way. Because of this, what I get is totally not a maze at all but just some lines everywhere.
I tried putting the removal of walls in a seperate method, but that does not seem to work also. I looked if the remove walls method gets called at the first cell and it says it is true, but in some way the values of that cell gets overwritten.
Code:
import pygame
import random
WIDTH = 300
HEIGHT = 300
CellSize = 30
Rows = int(WIDTH/30)
Columns = int(HEIGHT/30)
current = None
grid = []
visited = []
DISPLAY = pygame.display.set_mode((WIDTH, HEIGHT))
class Cell:
def __init__(self, r, c):
self.r = r
self.c = c
self.x = CellSize * c
self.y = CellSize * r
self.sides = [True, True, True, True] # Top, Bottom, Left, Right
self.visited = False
self.neighbours = []
self.NTop = None
self.NBottom = None
self.NRight = None
self.NLeft = None
self.NTopIndex = None
self.NBottomIndex = None
self.NRightIndex = None
self.NLeftIndex = None
self.nr = None
self.nc = None
self.random = None
self.neighbour = None
def index(self, nr, nc):
self.nr = nr
self.nc = nc
if self.nr < 0 or self.nc < 0 or self.nr > Rows-1 or self.nc > Columns-1:
return -1
return self.nr + self.nc * Columns
def neighbour_check(self):
# Get neighbour positions in Grid
self.NTopIndex = self.index(self.r, self.c - 1)
self.NBottomIndex = self.index(self.r, self.c + 1)
self.NRightIndex = self.index(self.r + 1, self.c)
self.NLeftIndex = self.index(self.r - 1, self.c)
# Look if they are truly neighbours and then append to neighbour list
if self.NTopIndex >= 0:
self.NTop = grid[self.NTopIndex]
if not self.NTop.visited:
self.neighbours.append(self.NTop)
if self.NBottomIndex >= 0:
self.NBottom = grid[self.NBottomIndex]
if not self.NBottom.visited:
self.neighbours.append(self.NBottom)
if self.NRightIndex >= 0:
self.NRight = grid[self.NRightIndex]
if not self.NRight.visited:
self.neighbours.append(self.NRight)
if self.NLeftIndex >= 0:
self.NLeft = grid[self.NLeftIndex]
if not self.NLeft.visited:
self.neighbours.append(self.NLeft)
# Choose random neighbour
if len(self.neighbours) > 0:
self.random = random.randint(0, len(self.neighbours) - 1)
self.neighbour = self.neighbours[self.random]
# Remove the wall between self and neighbour
if self.neighbour == self.NTop:
if self == grid[0]:
print('TOP')
self.sides[0] = False
self.NTop.sides[1] = False
elif self.neighbour == self.NBottom:
if self == grid[0]:
print('BOTTOM')
self.sides[1] = False
self.NBottom.sides[0] = False
elif self.neighbour == self.NLeft:
if self == grid[0]:
print('LEFT')
self.sides[2] = False
self.NLeft.sides[3] = False
elif self.neighbour == self.NRight:
if self == grid[0]:
print('RIGHT')
self.sides[3] = False
self.NRight.sides[2] = False
else:
print('SIDES ERROR')
return self.neighbours[self.random]
else:
return -1
def draw(self):
global DISPLAY, CellSize
# Top
if self.sides[0]:
pygame.draw.line(DISPLAY, (0, 0, 0), (self.x, self.y), (self.x + CellSize, self.y))
# Bottom
if self.sides[1]:
pygame.draw.line(DISPLAY, (0, 0, 0), (self.x, self.y + CellSize), (self.x + CellSize, self.y + CellSize))
# Left
if self.sides[2]:
pygame.draw.line(DISPLAY, (0, 0, 0), (self.x, self.y), (self.x, self.y + CellSize))
# Right
if self.sides[3]:
pygame.draw.line(DISPLAY, (0, 0, 0), (self.x + CellSize, self.y), (self.x + CellSize, self.y + CellSize))
class Maze:
def __init__(self):
global current
self.next = None
self.running = True
self.DISPLAY = None
self.display_running = True
def init_cells(self):
# Make grid and make cell 0 the begin of the algorithm
global current
for i in range(0, Columns):
for j in range(0, Rows):
cell = Cell(j, i)
grid.append(cell)
current = grid[0]
def init_maze(self):
global current, visited
print(grid[0].sides)
# Start Algorithm
while self.running:
# Check if the current cell is visited, if not make it visited and choose new neighbour
if not current.visited:
current.visited = True
visited.append(current)
self.next = current.neighbour_check()
if not self.next == -1:
# If it finds a neighbour then make it the new current cell
# self.next.visited = True
current = self.next
elif self.next == -1 and len(visited) > 0:
# If it doesn't then look trough the path and backtrack trough it to find a possible neighbour
if len(visited) > 1:
del visited[-1]
current = visited[-1]
# If the last cell of the visited list is Cell 0 then remove it
elif len(visited) <= 1:
del visited[-1]
elif len(visited) <= 0:
# Stop the Algorithm
self.running = False
print('Done')
def draw(self):
DISPLAY.fill((255, 255, 255))
# Get the maze made by the algorithm and draw it on the screen
for i in range(0, len(grid)):
grid[i].draw()
pygame.display.update()
while self.display_running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.display_running = False
maze = Maze()
maze.init_cells()
maze.init_maze()
maze.draw()
I put some print methods in it for debugging purposes.
And am still a beginner in programming, I know it could probably be way cleaner or that some naming of methods could be better.
What I want to happen is that in def init_maze the maze blueprints gets written out and that in def draw the blueprint gets drawn on the screen.
I am currently attempting to make a simple string simulation. The purpose is to just look like string but mine looks a bit weird and I don't know what the solution is. Does anyone have any ideas, here is the code
from pygame.locals import *
from math import sqrt
import pygame
pygame.init()
screen = pygame.display.set_mode([500, 500])
clock = pygame.time.Clock()
class Node():
def __init__(self, position, mass, initalVelocity):
self.position = position[:]
self.mass = mass
self.velocity = initalVelocity
def update(self):
self.position[0] += self.velocity[0]
self.position[1] += self.velocity[1]
class String():
def __init__(self, nodes, gravity = .981 , springConstant = 10, iterations = 1):
self.nodes = nodes[:]
self.gravity = gravity
self.springC = springConstant
self.iterations = iterations
self.setDistance = 1
def calculateForces(self):
for x in range(self.iterations):
for i in range(1, len(self.nodes), 1):
distance = sqrt((self.nodes[i].position[0] - self.nodes[i - 1].position[0]) ** 2 + (self.nodes[i].position[1] - self.nodes[i - 1].position[1]) ** 2)
force = -self.springC * (distance - self.setDistance)
force = force / self.nodes[i].mass
nDistanceVector = [(self.nodes[i].position[0] - self.nodes[i - 1].position[0]) / distance, (self.nodes[i].position[1] - self.nodes[i - 1].position[1]) / distance]
nDistanceVector = [nDistanceVector[0] * force, nDistanceVector[1] * force]
self.nodes[i].velocity[0] = 0.71 * self.nodes[i].velocity[0] + nDistanceVector[0]
self.nodes[i].velocity[1] = 0.71 * self.nodes[i].velocity[1] + (nDistanceVector[1] + self.gravity)
self.nodes[i].update()
pygame.draw.aalines(screen, [0, 0, 0], False, a)
a = [Node([250 + i * 10, 100], 100, [0, 1]) for i in range(25)]
s = String(a)
while 1:
screen.fill([255, 255, 255])
s.calculateForces()
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
pygame.display.update()
clock.tick(60)`
I'm using Hooke's law and each node acts as a spring as soon as it goes beyond a certain distance from the previous node, but it just looks clunky and unrealistic. What am i missing?
Use pygame.math.Vector2 to simplify the code. Divide the calculateForces into 2 loops. First calculate the new velocity, then move the positions:
import pygame
pygame.init()
window = pygame.display.set_mode([500, 500])
clock = pygame.time.Clock()
class Node():
def __init__(self, position, mass, initial_velocity):
self.position = pygame.math.Vector2(position)
self.mass = mass
self.velocity = pygame.math.Vector2(initial_velocity)
class String():
def __init__(self, nodes, gravity = .981, spring_constant = 10):
self.nodes = nodes[:]
self.gravity = gravity
self.spring_constant = spring_constant
self.set_distance = 1
def calculateForces(self):
for i in range(1, len(self.nodes), 1):
n1, n0 = self.nodes[i], self.nodes[i - 1]
distance = n1.position.distance_to(n0.position)
force = -self.spring_constant * (distance - self.set_distance) / n1.mass
n_dist_v = (n1.position - n0.position) / distance * force
n1.velocity = 0.71 * n1.velocity + n_dist_v + pygame.math.Vector2(0, self.gravity)
for i in range(1, len(self.nodes), 1):
self.nodes[i].position += self.nodes[i].velocity
def draw(self, surf):
pygame.draw.aalines(surf, [0, 0, 0], False, [node.position for node in self.nodes])
def createString():
return String([Node([150 + i * 10, 100], 120, [0, 1]) for i in range(25)])
s = createString()
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
s = createString()
s.calculateForces()
window.fill([255, 255, 255])
s.draw(window)
pygame.display.update()
clock.tick(60)
pygame.quit()
exit()