I made an object with three points of view. one for forward, one for left, and one for right.
these POVs are some path like a slice of a circle
I want to detect intersections or collisions between these POVs with rectangles to set the color of each POV
POVs rotate with the object in any direction but rectangles are always oriented
here is my code
from random import randint
from sys import argv
from PyQt6.QtCore import QRectF, Qt, QTimer, QPoint
from PyQt6.QtGui import QColor, QKeyEvent, QMouseEvent, QPainter, QPen, QPaintEvent, QPainterPath, QBrush
from PyQt6.QtWidgets import QApplication, QVBoxLayout, QWidget
class Window(QWidget):
def __init__(self, parent=None) -> None:
super().__init__(parent)
screenWidth = 1920
screenHeight = 1080
self.gX = []
self.gY = []
self.framesShowPerSecond = 30
self.age = 0
self.maxAge = 500
self.windowWidth = 1920
self.windowHeight = 1080
self.isRunning = True
self.angle = -90
self.clockCounterVariable = 0
self.milliseconds = 0
self.seconds = 0
self.minutes = 0
self.hours = 0
self.setWindowTitle("test")
self.setGeometry((screenWidth - self.windowWidth) // 2, (screenHeight - self.windowHeight) // 2, self.windowWidth, self.windowHeight)
self.setLayout(QVBoxLayout())
self.showFullScreen()
self.setStyleSheet("background-color:rgb(20, 20, 20);font-size:20px;")
self.xCounter = 0
self.clock = QTimer(self)
self.graphicTimer = QTimer(self)
self.clock.timeout.connect(self.clockCounter)
self.graphicTimer.timeout.connect(self.update)
self.graphicTimer.start(round((1/self.framesShowPerSecond)*1000))
self.clock.start(10)
self.show()
def clockCounter(self) -> None:
if self.clockCounterVariable % 10 == 0:
x = self.xCounter
y = randint(0, 100)
self.xCounter += 1
self.gX.append(x - 0.5)
self.gX.append(x + 0.5)
self.gY.append(y)
self.gY.append(y)
self.clockCounterVariable += 1
def keyPressEvent(self, event: QKeyEvent) -> super:
key = QKeyEvent.key(event)
if key == 112 or key == 80: # P/p
if self.isRunning:
print("pause process")
self.isRunning = False
self.clock.stop()
self.graphicTimer.stop()
else:
print("continue process")
self.isRunning = True
self.clock.start(1000)
self.graphicTimer.start(round((1/self.framesShowPerSecond)*1000))
elif (key == 115) or (key == 83): # S/s
self.closeWindow()
self.update()
return super().keyPressEvent(event)
def mousePressEvent(self, event: QMouseEvent) -> super:
if event.buttons() == Qt.MouseButton.LeftButton:
if self.isRunning:
print("pause process")
self.isRunning = False
self.clock.stop()
self.graphicTimer.stop()
else:
print("continue process")
self.isRunning = True
self.clock.start(1000)
self.graphicTimer.start(round((1/self.framesShowPerSecond)*1000))
return super().mousePressEvent(event)
def paintEvent(self, event: QPaintEvent) -> super:
self.milliseconds = self.clockCounterVariable
self.seconds, self.milliseconds = divmod(self.milliseconds, 100)
self.minutes, self.seconds = divmod(self.seconds, 60)
self.hours, self.minutes = divmod(self.minutes, 60)
painter = QPainter()
painter.begin(self)
painter.setPen(QPen(QColor(255, 128, 20), 1, Qt.PenStyle.SolidLine))
painter.drawText(QRectF(35, 30, 400, 30), Qt.AlignmentFlag.AlignLeft, "{:02d} : {:02d} : {:02d} : {:02d}".format(self.hours, self.minutes, self.seconds, self.milliseconds))
painter.setPen(QPen(QColor(20, 20, 20), -1, Qt.PenStyle.SolidLine))
painter.setBrush(QBrush(QColor(20, 20, 160), Qt.BrushStyle.SolidPattern))
barrier = QRectF(1920//2-25, 1080//2-25-40, 50, 20)
painter.drawRect(barrier)
painter.translate(QPoint(1920//2, 1080//2))
painter.rotate(self.angle)
painter.setBrush(QBrush(QColor(200, 200, 200, 50), Qt.BrushStyle.SolidPattern))
r = 200
a = 40
b = a * 2
rect = QRectF(-r/2, -r/2, r, r)
path = QPainterPath()
path.arcTo(rect, -a, b)
path.closeSubpath()
if path.contains(barrier):
painter.setBrush(QBrush(QColor(200, 20, 20, 50), Qt.BrushStyle.SolidPattern))
else:
painter.setBrush(QBrush(QColor(20, 200, 20, 50), Qt.BrushStyle.SolidPattern))
painter.drawPath(path)
path = QPainterPath()
path.arcTo(rect, -a+90, b)
path.closeSubpath()
if path.contains(barrier):
painter.setBrush(QBrush(QColor(200, 20, 20, 50), Qt.BrushStyle.SolidPattern))
else:
painter.setBrush(QBrush(QColor(20, 200, 20, 50), Qt.BrushStyle.SolidPattern))
painter.drawPath(path)
path = QPainterPath()
path.arcTo(rect, -a-90, b)
path.closeSubpath()
if path.contains(barrier):
painter.setBrush(QBrush(QColor(200, 20, 20, 50), Qt.BrushStyle.SolidPattern))
else:
painter.setBrush(QBrush(QColor(20, 200, 20, 50), Qt.BrushStyle.SolidPattern))
painter.drawPath(path)
painter.setBrush(QBrush(QColor(160, 20, 20), Qt.BrushStyle.SolidPattern))
path = QPainterPath()
path.moveTo(30, 0)
path.lineTo(-30, -15)
path.lineTo(-10, 0)
path.lineTo(-30, 15)
path.closeSubpath()
painter.drawPath(path)
painter.end()
self.angle += 1
if self.angle == 360:
self.angle = 0
return super().paintEvent(event)
def closeWindow(self) -> None:
print("closing window ...")
self.close()
if __name__ == "__main__":
App = QApplication(argv)
window = Window()
exit(App.exec())
how should I do this purpose?
I want to detect collisions between some slices of a circle and rectangles.
Your code has many issues, but your main problem is caused by two factors:
you are using contains(), which "Returns true if the given rectangle is inside the path"; you should use intersects() instead;
the "barrier" is created using the final, absolute coordinates, while all the other paths are in relative coordinates, since you are translating the painter;
the painter is rotated, not the objects;
The result is that even using contains(), it will never work, since the objects are too far apart:
barrier = QRectF(1920//2-25, 1080//2-25-40, 50, 20) which is 935, 475, 50, 20;
rect = QRectF(-r/2, -r/2, r, r) which is -100, -100, 200, 200;
As you can see, they are not even close.
Not only: it wouldn't work anyway because the paths never consider the rotation, as you applied it to the painter.
The proper solution requires to:
always use the same coordinate reference;
apply the transformations (translation and rotation) to the objects and then detect the possible collision;
In order to do that, we can use Qtransform along with its map() function, which returns a new transformed path.
Note that QTransform is always aligned to the 0, 0 coordinate, so in order to properly rotate around a different reference point, you must:
translate the transform to the reference point;
apply the rotation;
translate back using the negative of the reference point;
Here is an improved version of your code:
def paintEvent(self, event: QPaintEvent) -> super:
secs, ms = divmod(self.clockCounterVariable, 100)
mins, secs = divmod(secs, 60)
hours, mins = divmod(mins, 60)
painter = QPainter(self)
painter.setPen(QPen(QColor(255, 128, 20), 1, Qt.SolidLine))
painter.drawText(QRectF(35, 30, 400, 30), Qt.AlignLeft,
"{:02d} : {:02d} : {:02d} : {:02d}".format(hours, mins, secs, ms))
painter.setPen(QPen(QColor(20, 20, 20), -1, Qt.SolidLine))
painter.setBrush(QBrush(QColor(20, 20, 160), Qt.SolidPattern))
reference = QPointF(1920 / 2, 1080 / 2)
barrier = QRectF(reference.x() - 25, reference.y() - 65, 50, 20)
painter.drawRect(barrier)
r = 200
a = 40
b = a * 2
rect = QRectF(-r/2, -r/2, r, r).translated(reference)
collideBrush = QBrush(QColor(200, 20, 20, 50))
normalBrush = QBrush(QColor(20, 200, 20, 50))
reference = rect.center()
transform = QTransform()
transform.translate(reference.x(), reference.y())
transform.rotate(self.angle)
transform.translate(-reference.x(), -reference.y())
for deltaAngle in (0, 90, -90):
path = QPainterPath(reference)
path.arcTo(rect, -a + deltaAngle, b)
path.closeSubpath()
path = transform.map(path)
if path.intersects(barrier):
painter.setBrush(collideBrush)
else:
painter.setBrush(normalBrush)
painter.drawPath(path)
painter.setBrush(QBrush(QColor(160, 20, 20)))
path = QPainterPath()
path.moveTo(30, 0)
path.lineTo(-30, -15)
path.lineTo(-10, 0)
path.lineTo(-30, 15)
path.closeSubpath()
path.translate(rect.center())
painter.drawPath(transform.map(path))
self.angle = (self.angle + 1) % 360
Note that there are other issues with your code, for instance:
use event.key(), not QKeyEvent.key(event);
I don't know where you got those 112 and 115 values for the keys, but they are wrong: event.key() returns an enum that is always the same for letter keys, no matter the modifiers (which you must check with event.modifier()); just check the key against Qt.Key.Key_P or Qt.Key.Key_S and it will work for both lower and upper cases;
event handlers don't return anything, those return when calling the base implementations are useless;
you are creating instance attributes that are only used within the scope of a function: setting self.milliseconds, self.seconds etc. is quite pointless for this case; if you do need those values outside of that function, they should certainly not be computed in the paintEvent(), because if the window is hidden it will not be called: instead, compute those values in clockCounter and call self.update();
Finally, when dealing with complex graphics, implementing everything with the basic QPainter is not really effective, and normally results in making things more complex (and prone to errors and bugs) than necessary. Consider using the Graphics View Framework instead.
For the collision between a circle and an oriented rectangle
find the closest point on the rectangle to the circle -> easy because the rectangle is oriented on the grid.
check the distance of this point to the circle center.
general example
circle_x, circle_y = 1,1 #circle centre
circle_r = 3 #circle radius
rect_xmin = 2 #quadrilateral left x limit
rect_xmax = 4 #quadrilateral right x limit
rect_ymin = 3 #quadrilateral bottom y limit
rect_ymax = 3 #quadrilateral top y limit
if circle_x < rect_xmin:
point_x = rect_xmin
elif circle_x > rect_xmax:
point_x = rect_xmax
else:
point_x = circle_x
if circle_y < rect_ymin:
point_y = rect_ymin
elif circle_y > rect_ymax:
point_y = rect_ymax
else:
point_y = circle_y
print("collision", (circle_x-point_x)**2 + (circle_y-point_y)**2 < circle_r**2, "at", (point_x, point_y))
maybe you could use (point_x, point_y) to check the angle of the location of the collision to find the quadrant?
Related
I want to draw a triangle from a class, so I call the function
pygame.draw.polygon()
Now, the problem is that I need to pass the points in a manner that will allow me to calculate the centre of the triangle.
I was trying to pass the tuples one by one in this way
self.first_point = (int, int)
self.second_point = (int, int)
self.third_point = (int, int)
so that I can then access the single tuple values.
Then pass the three points like this
self.position = [self.first_point, self.second_point, self.third_point]
But for some reason, it doesn't work.
This is the error I get
File "C:/Users/oricc/PycharmProjects/designAChessboardChallange/display.py", line 178, in <module>
white_archer_3 = Archer(white, [(100, 100), (200, 200), (300, 300)])
[(100, 100), (200, 200), (300, 300)]
File "C:/Users/oricc/PycharmProjects/designAChessboardChallange/display.py", line 132, in __init__
self.triangle = pygame.draw.polygon(game_window, colour, self.position)
TypeError: points must be number pairs
By number of pairs, the Pygame documentation gives as an example
e.g. [(x1, y1), (x2, y2), (x3, y3)]
In fact, when I print the position I pass I get, as you can see from the error above
[(100, 100), (200, 200), (300, 300)]
Anyone can help with this?
Is there another manner to calculate the centre without accessing the xs and ys like that?
Full code here
import pygame
import sys
from coordinator import coordinator
# set up the display
pygame.init()
window_size = (800, 800)
game_window = pygame.display.set_mode(size=window_size)
pygame.display.set_caption('My Game')
# defines classes and related methods
class WhiteSquare:
def __init__(self):
self.height = int(window_size[0] / 8)
self.width = int(window_size[1] / 8)
self.white_square = pygame.Surface((self.height, self.width))
self.white_square.fill((255, 255, 255))
class BlackSquare:
def __init__(self):
self.height = int(window_size[0] / 8)
self.width = int(window_size[1] / 8)
self.black_square = pygame.Surface((self.height, self.width))
self.black_square.fill((0, 0, 0))
class ChessBoard:
def __init__(self):
self.ws = ws
self.bs = bs
self.white_columns = white_columns
self.black_columns = black_columns
def draw(self):
for w_columns in self.white_columns:
game_window.blit(self.ws.white_square, w_columns)
for b_columns in self.black_columns:
game_window.blit(self.bs.black_square, b_columns)
# declare letters and numbers
letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
numbers = ['1', '2', '3', '4', '5', '6', '7', '8']
# create coordinates
coordinates = []
for item_letter in letters:
letter = item_letter
for item_number in numbers:
number = item_number
coordinates.append(letter + number)
# create coordinates values components
x_values = []
for number in range(0, 800, 100):
x = number
x_values.append(x)
y_values = []
for number in range(0, 800, 100):
y = number
y_values.append(y)
# create coordinate values
coordinate_values = []
for x in x_values:
for y in y_values:
coordinate_values.append((x, y))
# assign values to coordinates
squares_coordinates = dict(zip(coordinates, coordinate_values))
# Background for units
class CircleSurface:
def __init__(self):
self.circle_surface = pygame.Surface((100, 100), flags=pygame.SRCALPHA)
pygame.draw.circle(self.circle_surface, (255, 0, 0), (50, 50), 45)
# define colours
black = (0, 0, 0)
white = (255, 255, 255)
gold = (153, 153, 0)
class Unit:
def __init__(self, colour, position):
# define Unit colour
self.colour = colour
# define Unit position
self.position = position
class Knight(Unit):
def __init__(self, colour, position):
# draw circle, inline, and outline
super().__init__(colour, position)
self.center_x = position[0]
self.center_y = position[1]
self.colour = colour
self.position = position
circle_radius = 40
self.circle = pygame.draw.circle(game_window, colour, self.position, circle_radius)
self.circle_outline = pygame.draw.circle(game_window, gold, self.position, circle_radius, 5)
self.circle_inline = pygame.draw.circle(game_window, gold, self.position, (circle_radius - 10), 5)
# draw letter
pygame.font.init()
my_font_size = 50
my_font = pygame.font.SysFont('Time New Roman', my_font_size)
text_surface = my_font.render('K', 1, gold)
center_text = text_surface.get_rect(center=(self.center_x, self.center_y))
game_window.blit(text_surface, center_text)
class Archer(Unit):
def __init__(self, colour, position):
super().__init__(colour, position)
self.first_point = (int, int)
self.second_point = (int, int)
self.third_point = (int, int)
self.position = [self.first_point, self.second_point, self.third_point]
print(position)
self.triangle = pygame.draw.polygon(game_window, colour, self.position)
self.triangle_outline = pygame.draw.polygon(game_window, gold, self.position, 5)
self.triangle_inline = pygame.draw.polygon(game_window, gold, self.position, 5)
# draw letter
# pygame.font.init()
# my_font_size = 50
# my_font = pygame.font.SysFont('Time New Roman', my_font_size)
# text_surface = my_font.render('A', 1, gold)
# center_text = text_surface.get_rect(center=(self.center_x, self.center_y))
# game_window.blit(text_surface, center_text)
# Sets and gets the coordinates for black and white squares
coordinator = coordinator()
black_columns = coordinator[2] + coordinator[3]
white_columns = coordinator[0] + coordinator[1]
# Creates needed objects
ws = WhiteSquare()
bs = BlackSquare()
cb = ChessBoard()
cs = CircleSurface()
# Event loop (outer)
while 1:
# Event loop (inner)
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
# Draws the chessboard
cb.draw()
# Draws white pieces in their initial position
white_knight_1 = Knight(white, (150, 650))
white_knight_2 = Knight(white, (650, 650))
white_archer_3 = Archer(white, [(100, 100), (200, 200), (300, 300)])
pygame.display.update()
Thank you
OK, I managed to make it work.
You guys are both right, I should know by now that I can't pass placeholders, so I figured out the solution to my problem as follow:
class Archer(Unit):
def __init__(self, colour, first_point, second_point, third_point):
self.first_point = first_point
self.second_point = second_point
self.third_point = third_point
position = [self.first_point, self.second_point, self.third_point]
super().__init__(colour, position)
print(self.position)
self.triangle = pygame.draw.polygon(game_window, colour, self.position)
self.triangle_outline = pygame.draw.polygon(game_window, gold, self.position, 5)
self.triangle_inline = pygame.draw.polygon(game_window, gold, self.position, 5)
Basically I have to declare the three points self variables, as well as position before the super function so that I can then pass them as 'position' to the parent class initializer. This has been really useful!!
Thanks both
you could also do this: self.first_point = (int(0), int(0)) as int is not a placeholder but to declare that a variable is an integer str('0') will print '0' you could also input this
zero = 0
str(zero) #'0'
int(zero) #0
and you dont need to put rgb tupels because you can store them in a variable like this
black = (0, 0, 0)
Display = pygame.display.set_mode((800, 600))
Display.fill(black)
I'm working on a project at the moment and I have been trying to change the fonts of the labels in the pyglet library to fonts that I found online but I couldn't get it to work. I tried searching online for an hour now and nothing seems to be working. Added some code for reference:
font.add_file('ZukaDoodle.ttf')
ZukaDoodle = font.load('ZukaDoodle.ttf', 16)
PlayLabel = pyglet.text.Label('Go', font_name='ZukaDoodle', font_size=100, x=window.width // 2,
y=window.height - 450, anchor_x='center', anchor_y='center',
batch=buttons_batch,
color=(0, 0, 0, 1000), width=250, height=130)
So the error is pretty simple. The loaded font-name is not ZukaDoodle, it's Zuka Doodle with a space. Here's a working executable sample:
from pyglet import *
from pyglet.gl import *
font.add_file('ZukaDoodle.ttf')
ZukaDoodle = font.load('ZukaDoodle.ttf', 16)
key = pyglet.window.key
class main(pyglet.window.Window):
def __init__ (self, width=800, height=600, fps=False, *args, **kwargs):
super(main, self).__init__(width, height, *args, **kwargs)
self.x, self.y = 0, 0
self.keys = {}
self.mouse_x = 0
self.mouse_y = 0
self.PlayLabel = pyglet.text.Label('Go', font_name='Zuka Doodle', font_size=100,
x=self.width // 2,
y=self.height - 450,
anchor_x='center', anchor_y='center',
color=(255, 0, 0, 255),
width=250, height=130)
self.alive = 1
def on_draw(self):
self.render()
def on_close(self):
self.alive = 0
def on_mouse_motion(self, x, y, dx, dy):
self.mouse_x = x
def on_key_release(self, symbol, modifiers):
try:
del self.keys[symbol]
except:
pass
def on_key_press(self, symbol, modifiers):
if symbol == key.ESCAPE: # [ESC]
self.alive = 0
self.keys[symbol] = True
def render(self):
self.clear()
self.PlayLabel.draw()
## Add stuff you want to render here.
## Preferably in the form of a batch.
self.flip()
def run(self):
while self.alive == 1:
self.render()
# -----------> This is key <----------
# This is what replaces pyglet.app.run()
# but is required for the GUI to not freeze
#
event = self.dispatch_events()
if __name__ == '__main__':
x = main()
x.run()
The key difference here is font_name='Zuka Doodle'.
Also, the alpha channel usually doesn't need to be higher than 255 since that's the max value of a colored byte, so unless you're using 16-bit color representation per channel, 255 will be the maximum value.
I found a nice image of space that I'd like sitting in the background of this tiny game I'm working on and can't figure out what and where to write it. It needs to be placed behind all classes to make sure that it doesn't block the screen. I thought it might be in class Window, but I'm not sure. I am brand new to python so any help is much appreciated! This is the entire project so far.
import sys, logging, os, random, math, open_color, arcade
#check to make sure we are running the right version of Python
version = (3,7)
assert sys.version_info >= version, "This script requires at least Python {0}.{1}".format(version[0],version[1])
#turn on logging, in case we have to leave ourselves debugging messages
logging.basicConfig(format='[%(filename)s:%(lineno)d] %(message)s', level=logging.DEBUG)
logger = logging.getLogger(__name__)
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
MARGIN = 30
SCREEN_TITLE = "Intergalactic slam"
NUM_ENEMIES = 5
STARTING_LOCATION = (400,100)
BULLET_DAMAGE = 10
ENEMY_HP = 10
HIT_SCORE = 10
KILL_SCORE = 100
PLAYER_HP = 100
class Bullet(arcade.Sprite):
def __init__(self, position, velocity, damage):
'''
initializes the bullet
Parameters: position: (x,y) tuple
velocity: (dx, dy) tuple
damage: int (or float)
'''
super().__init__("PNG/laserPink3.png", 0.5)
(self.center_x, self.center_y) = position
(self.dx, self.dy) = velocity
self.damage = damage
def update(self):
'''
Moves the bullet
'''
self.center_x += self.dx
self.center_y += self.dy
class Enemy_Bullet(arcade.Sprite):
def __init__(self, position, velocity, damage):
super().__init__("PNG/laserGreen1.png", 0.5)
(self.center_x, self.center_y) = position
(self.dx, self.dy) = velocity
self.damage = damage
def update(self):
self.center_x += self.dx
self.center_y += self.dy
class Player(arcade.Sprite):
def __init__(self):
super().__init__("PNG/shipYellow_manned.png", 0.5)
(self.center_x, self.center_y) = STARTING_LOCATION
self.hp = PLAYER_HP
class Enemy(arcade.Sprite):
def __init__(self, position):
'''
initializes an alien enemy
Parameter: position: (x,y) tuple
'''
super().__init__("PNG/shipGreen_manned.png", 0.5)
self.hp = ENEMY_HP
(self.center_x, self.center_y) = position
class Window(arcade.Window):
def __init__(self, width, height, title):
super().__init__(width, height, title)
file_path = os.path.dirname(os.path.abspath(__file__))
os.chdir(file_path)
self.set_mouse_visible(True)
arcade.set_background_color(open_color.black)
self.bullet_list = arcade.SpriteList()
self.enemy_list = arcade.SpriteList()
self.enemy_bullet_list = arcade.SpriteList()
self.player = Player()
self.score = 0
self.win = False
self.lose = False
def setup(self):
'''
Set up enemies
'''
for i in range(NUM_ENEMIES):
x = 120 * (i+1) + 40
y = 500
enemy = Enemy((x,y))
self.enemy_list.append(enemy)
def update(self, delta_time):
self.bullet_list.update()
self.enemy_bullet_list.update()
if (not (self.win or self.lose)):
for e in self.enemy_list:
for b in self.bullet_list:
if (abs(b.center_x - e.center_x) <= e.width / 2 and abs(b.center_y - e.center_y) <= e.height / 2):
self.score += HIT_SCORE
e.hp -= b.damage
b.kill()
if (e.hp <= 0):
e.kill()
self.score += KILL_SCORE
if (len(self.enemy_list) == 0):
self.win = True
if (random.randint(1, 75) == 1):
self.enemy_bullet_list.append(Enemy_Bullet((e.center_x, e.center_y - 15), (0, -10), BULLET_DAMAGE))
for b in self.enemy_bullet_list:
if (abs(b.center_x - self.player.center_x) <= self.player.width / 2 and abs(b.center_y - self.player.center_y) <= self.player.height / 2):
self.player.hp -= b.damage
b.kill()
if (self.player.hp <= 0):
self.lose = True
def on_draw(self):
arcade.start_render()
arcade.draw_text(str(self.score), 20, SCREEN_HEIGHT - 40, open_color.white, 16)
arcade.draw_text("HP: {}".format(self.player.hp), 20, 40, open_color.white, 16)
if (self.player.hp > 0):
self.player.draw()
self.bullet_list.draw()
self.enemy_bullet_list.draw()
self.enemy_list.draw()
if (self.lose):
self.draw_game_loss()
elif (self.win):
self.draw_game_won()
def draw_game_loss(self):
arcade.draw_text(str("LOSER!"), SCREEN_WIDTH / 2 - 90, SCREEN_HEIGHT / 2 - 10, open_color.white, 30)
def draw_game_won(self):
arcade.draw_text(str("WINNER!"), SCREEN_WIDTH / 2 - 90, SCREEN_HEIGHT / 2 - 10, open_color.white, 30)
def on_mouse_motion(self, x, y, dx, dy):
'''
The player moves left and right with the mouse
'''
self.player.center_x = x
def on_mouse_press(self, x, y, button, modifiers):
if button == arcade.MOUSE_BUTTON_LEFT:
x = self.player.center_x
y = self.player.center_y + 15
bullet = Bullet((x,y),(0,10),BULLET_DAMAGE)
self.bullet_list.append(bullet)
def main():
window = Window(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
window.setup()
arcade.run()
if __name__ == "__main__":
main()
One way to do it would be to load the .jpg or .png as a texture, and draw that texture each frame, as big as the screen is (or bigger!).
I haven't tested this, but as an example, loading the texture could be done in Window.__init__, like so (reference):
self.background = arcade.load_texture('PNG/background.png')
And then in on_draw, just after you call start_render, you would draw it (reference), passing the required center coordinates, as well as width and height:
self.background.draw(SCREEN_WIDTH/2, SCREEN_HEIGHT/2, SCREEN_WIDTH, SCREEN_HEIGHT)
The reason it needs to be the first thing is because everything is drawn back-to-front, like you would do in a painting.
If the image is not the exact same size as your screen/window, your background will probably be stretched/squished. If that's not what you want, the easiest fix would be to change the image so that it's the right size.
Yes, you should be able to add it to class window...
You could do something like this to add it:
def __init__(self, width, height, title):
super().__init__(width, height, title)
file_path = os.path.dirname(os.path.abspath(__file__))
os.chdir(file_path)
self.set_mouse_visible(True)
arcade.set_background_color(open_color.black)
self.bullet_list = arcade.SpriteList()
self.enemy_list = arcade.SpriteList()
self.enemy_bullet_list = arcade.SpriteList()
self.player = Player()
self.score = 0
self.win = False
self.lose = False
self.background = None
def setup(self):
'''
Set up enemies
'''
self.background = arcade.load_texture("images/background.jpg")
for i in range(NUM_ENEMIES):
x = 120 * (i+1) + 40
y = 500
enemy = Enemy((x,y))
self.enemy_list.append(enemy)
I've come to a dead end, and after excessive (and unsuccessful) Googling, I need help.
I'm building a simple PyQt4 Widget where it lies out a grid of 60x80 squares, each initialized to None. If the user clicks on that box it changes color based on how many times left-clicked, defined by this list:
self.COLORS=[
(0, 0, 255), #WATER
(255, 210, 128), #SAND
(0, 128, 0), #GREEN
(255, 255, 0), #YELLOW
(255, 165, 0), #ORANGE
(255, 0, 0) #RED
]
If the user right clicks, it flood fills an area, using the common recursive flood fill algo. This works perfectly for small spaces, however if the space is large enough the program fails with the error Fatal Python error: Cannot recover from stack overflow. I have no idea how to fix this, perhaps a flood fill that isn't recursive?
All squares and subsequent color codes are stored in self.cells so by setting self.cells[(y,x)]=1 would set cell (y,x) to the Sand color.
Here is the program in whole.
import sys
from PyQt4 import QtGui, QtCore
class Example(QtGui.QWidget):
def __init__(self, cell_size=10, swidth=800, sheight=600):
QtGui.QWidget.__init__(self)
self.resize(swidth,sheight)
self.cell_size = cell_size
self.height = sheight
self.width = swidth
self.columns = self.width // self.cell_size
self.rows = self.height // self.cell_size
self.COLORS=[
(0, 0, 255), #WATER
(255, 210, 128), #SAND
(0, 128, 0), #GREEN
(255, 255, 0), #YELLOW
(255, 165, 0), #ORANGE
(255, 0, 0) #RED
]
self.cells = {(x,y):None for x in range(1,self.columns+1) for y in range(1,self.rows+1)}
def translate(self,pixel_x, pixel_y):
"Translate pixel coordinates (pixel_x,pixel_y), into grid coordinates"
x = pixel_x * self.columns // self.width + 1
y = pixel_y * self.rows // self.height + 1
return x,y
def check_cell(self,x,y):
if self.cells[(x,y)] <= 0:
self.cells[(x,y)]=0
elif self.cells[(x,y)] >= len(self.COLORS)-1:
self.cells[(x,y)]=len(self.COLORS)-1
else:
pass
def draw_cell(self, qp, col, row):
x1,y1 = (col-1) * self.cell_size, (row-1) * self.cell_size
x2,y2 = (col-1) * self.cell_size + self.cell_size, (row-1) * self.cell_size + self.cell_size
qp.drawRect(x1, y1, x2-x1, y2-y1)
def color_cell(self, qp, col, row):
qp.setBrush(QtGui.QColor(*self.COLORS[self.cells[(col,row)]]))
self.draw_cell(qp, col, row)
def draw_grid(self, qp):
qp.setPen(QtGui.QColor(128,128,128)) # gray
# Horizontal lines
for i in range(self.rows):
qp.drawLine(0, i * self.cell_size, self.width, i * self.cell_size)
# Vertical lines
for j in range(self.columns):
qp.drawLine(j * self.cell_size, 0, j * self.cell_size, self.height)
def set_all(self, type):
self.cells = {(x,y):type for x in range(1,self.columns+1) for y in range(1,self.rows+1)}
self.repaint()
def fill(self, x, y, type):
print(x,y)
if x < 1 or x >= self.columns+1 or y < 1 or y >= self.rows+1:
return
if self.cells[(x,y)] != None:
return
self.cells[(x,y)] = type
self.repaint()
self.fill(x+1, y, type)
self.fill(x-1, y, type)
self.fill(x, y+1, type)
self.fill(x, y-1, type)
def paintEvent(self, e):
qp = QtGui.QPainter()
qp.begin(self)
self.draw_grid(qp)
for row in range(1, self.rows+1):
for col in range(1, self.columns+1):
if self.cells[(col,row)] != None:
self.color_cell(qp, col, row)
qp.end()
def drawPoints(self, qp):
size = self.size()
for i in range(1000):
x = random.randint(1, size.width()-1)
y = random.randint(1, size.height()-1)
qp.drawPoint(x, y)
def mousePressEvent(self, e):
x,y = self.translate(e.pos().x(),e.pos().y())
if e.button() == QtCore.Qt.LeftButton:
if self.cells[(x,y)] == None:
self.cells[(x,y)]=0
else:
self.cells[(x,y)]+=1
self.check_cell(x,y)
elif e.button() == QtCore.Qt.RightButton:
self.fill(x,y,0)
'''
if self.cells[(x,y)] == None:
self.cells[(x,y)]=0
else:
self.cells[(x,y)]-=1
self.check_cell(x,y)
'''
else: pass
self.repaint()
def save(self):
return self.cells
def open(self, new_cells):
self.cells=new_cells
self.repaint()
def main():
app = QtGui.QApplication(sys.argv)
ex = Example()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Can anyone help diagnose the problem or perhaps point in a direction to fix it?
You are using a stack-based forest fire algorithm, known to eat a lot of stack, so it's better to avoid it.
My proposal to avoid recursion: alternate forest fire algorithm
I even implemented it using your class objects. Tested it with some ASCII-art and also with your actual code and it worked fine even on big zones:
def fill(self, x, y, t):
if self.cells[(x,y)] == None: # cannot use not: there are 0 values
to_fill = [(x,y)]
while to_fill:
# pick a point from the queue
x,y = to_fill.pop()
# change color if possible
self.cells[(x,y)] = t
# now the neighbours x,y +- 1
for delta_x in range(-1,2):
xdx = x+delta_x
if xdx > 0 and xdx < self.columns+1:
for delta_y in range(-1,2):
ydy = y+delta_y
# avoid diagonals
if (delta_x == 0) ^ (delta_y == 0):
if ydy > 0 and ydy < self.rows+1:
# valid x+delta_x,y+delta_y
# push in queue if no color
if self.cells[(xdx,ydy)] == None:
to_fill.append((xdx,ydy))
self.repaint()
When you pass a point, it checks if must be filled.
If must be filled, it inserts it in the queue and runs a loop.
The loop just pops an item from the queue, changes its color, and attempts to do the same for its neighbors: if still in the picture (x,y boundary checking) and not the diagonals, and no color defined on the neighbour, just insert the coord in the queue.
The loops stops when all items have been processed: after a while, either you reach the edges or you encounter only filled points, so no extra points are queued.
This approach only relies on available memory, not stack.
Proof that it works: successfully filled a huge blue zone without stack overflow.
I have some simple circles (boids) that move in a way that simulates birds; they should avoid getting too close to each other while maintaining the same general heading et cetera.
I'm using pygame but the circles don't move unless I press one of the buttons in the GUI, which seems kind of strange but I can't figure out where I messed up.
The most relevant part of the code is probably the gui function and the draw function inside the Boid class.
import pygame
import numpy as np
import sys
import math
class BoidWorld:
# Boid movement parameters
w_separation = 10
w_alignment = 1
w_cohesion = 1
w_avoidance = 0
w_flee = 50
dim = 0 # dim*dim = Size of world
neighbour_radius = 100
max_velocity = 100
# Objects in world
boids = []
predators = []
obstacles = []
def __init__(self, dim):
self.dim = dim
def update_boid_velocity(self, boid):
# Flee from predators, if any
predator = self.get_predator(boid)
flee_x, flee_y = self.calc_flee_force(boid, predator)
# Avoid obstacles, if any
obstacle = self.get_obstacle(boid)
avoid_x, avoid_y = self.calc_avoidance_force(boid, obstacle)
# Get neighbours within radius r
neighbours = self.get_neighbours(boid)
sep_x, sep_y = self.calc_separation_force(boid, neighbours)
align_x, align_y = self.calc_alignment_force(neighbours)
coh_x, coh_y = self.calc_cohesion_force(neighbours)
boid.velocity_x += self.w_separation * sep_x + self.w_alignment * align_x + self.w_cohesion * coh_x + \
self.w_avoidance * avoid_x + self.w_flee * flee_x
boid.velocity_y += self.w_separation * sep_y + self.w_alignment * align_y + self.w_cohesion * coh_y + \
self.w_avoidance * avoid_y + self.w_flee * flee_y
# Limit velocity by creating unit vectors and multiplying by max velocity
v = math.sqrt(boid.velocity_x**2 + boid.velocity_y**2)
if v > self.max_velocity:
boid.velocity_x = boid.velocity_x*self.max_velocity/v
boid.velocity_y = boid.velocity_y*self.max_velocity/v
boid.position_x += boid.velocity_x
boid.position_y += boid.velocity_y
print(boid.velocity_x, boid.velocity_y)
# Wrap around
if boid.position_x > self.dim or boid.position_x < 0:
boid.position_x %= self.dim
if boid.position_y > self.dim or boid.position_y < 0:
boid.position_y %= self.dim
def update_predator_velocity(self, predator):
pass
def calc_separation_force(self, boid, neighbours):
sep_x = 0.
sep_y = 0.
for b in neighbours:
sep_x = sep_x - (b.position_x - boid.position_x)
sep_y = sep_y - (b.position_y - boid.position_y)
return sep_x, sep_y
def calc_alignment_force(self, neighbours):
if not neighbours: return 0, 0
avg_heading_x = 0.
avg_heading_y = 0.
for b in neighbours:
avg_heading_x += b.velocity_x
avg_heading_y += b.velocity_y
return avg_heading_x/len(neighbours), avg_heading_y/len(neighbours)
def calc_cohesion_force(self, neighbours):
if not neighbours: return 0, 0
avg_pos_x = 0.
avg_pos_y = 0.
for b in neighbours:
avg_pos_x += b.position_x
avg_pos_y += b.position_y
return avg_pos_x/len(neighbours), avg_pos_y/len(neighbours)
# Flee straight away from predators
def calc_flee_force(self, boid, predator):
if not predator: return 0
return boid.position - predator.position
# Avoid obstacles
def calc_avoidance_force(self, boid, obstacle):
if not obstacle: return 0
return 0
# Predators chasing boids
def calc_chasing_force(self, predator, boids):
return 0
def get_predator(self, boid):
for predator in self.predators:
if self.is_neighbour(predator, boid):
return predator
return None
def get_obstacle(self, boid):
for obstacle in self.obstacles:
if self.is_neighbour(obstacle, boid):
return obstacle
return None
def is_neighbour(self, boid1, boid2):
if np.power(boid2.position_x - boid1.position_x, 2) + \
np.power(boid2.position_y - boid1.position_y, 2) \
< np.power(self.neighbour_radius, 2):
return True
return False
def get_neighbours(self, boid):
neighbours = []
for b in self.boids:
if b != boid and self.is_neighbour(b, boid):
neighbours.append(b)
return neighbours
def add_boid(self):
self.boids.append(Boid(
self.rand_position(), self.rand_position(),
self.rand_velocity(), self.rand_velocity()
))
def add_obstacle(self):
self.obstacles.append(Obstacle(
self.rand_position(), self.rand_position()))
def add_predator(self):
self.predators.append(Predator(
self.rand_position(), self.rand_position(),
self.rand_velocity(), self.rand_velocity()
))
def remove_boids(self):
self.boids = []
def remove_obstacles(self):
self.obstacles = []
def remove_predators(self):
self.predators = []
def rand_position(self):
return float(np.random.randint(0, self.dim))
def rand_velocity(self):
return float(np.random.randint(0, self.max_velocity))
class Boid(object):
color_circle = (100, 0, 0)
color_line = (100, 0, 100)
radius = 10
position_x = 0.
position_y = 0.
velocity_x = 0.
velocity_y = 0.
def __init__(self, position_x, position_y, velocity_x, velocity_y):
self.position_x = position_x
self.position_y = position_y
self.velocity_x = velocity_x
self.velocity_y = velocity_y
def draw(self, screen):
pygame.draw.circle(screen, self.color_circle, (int(round(self.position_x)), int(round(self.position_y))),
self.radius, 0)
# Velocity vector
pygame.draw.lines(screen, self.color_line, False, [
(int(round(self.position_x)), int(round(self.position_y))),
(int(round(self.position_x+self.velocity_x)), int(round(self.position_y+self.velocity_y)))
], 2)
class Predator(Boid):
color_circle = (100, 55, 0)
color_line = (100, 0, 100)
radius = 20
class Obstacle:
color = (0, 33, 50)
position_x = 0.
position_y = 0.
radius = 15
def __init__(self, position_x, position_y):
self.position_x = position_x
self.position_y = position_y
def draw(self, screen):
pygame.draw.circle(screen, self.color, (int(round(self.position_x)), int(round(self.position_y))),
self.radius, 0)
def main():
pygame.init()
boid_world = BoidWorld(800)
boid_world.add_boid()
gui(boid_world)
def gui(boid_world):
weight_inc = 0.1
btn_boid_add = Button('Add boid')
btn_boid_rem = Button('Remove boids')
btn_obst_add = Button('Add obstacle')
btn_obst_rem = Button('Remove obstacles')
btn_pred_add = Button('Add predator')
btn_pred_rem = Button('Remove predators')
btn_sep_p = Button('+')
btn_sep_m = Button('-')
btn_ali_p = Button('+')
btn_ali_m = Button('-')
btn_coh_p = Button('+')
btn_coh_m = Button('-')
pygame.font.init()
font = pygame.font.Font(None, 20)
font_color = (255, 255, 255)
screen = pygame.display.set_mode((1200, 800))
screen_half = screen.subsurface((400, 0, 800, 800))
pygame.display.set_caption('Boids')
clock = pygame.time.Clock()
run = True
while run:
screen.fill((0, 0, 0))
mouse = pygame.mouse.get_pos()
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
elif event.type == pygame.MOUSEBUTTONDOWN:
if btn_boid_add.obj.collidepoint(mouse):
boid_world.add_boid()
elif btn_boid_rem.obj.collidepoint(mouse):
boid_world.remove_boids()
elif btn_obst_add.obj.collidepoint(mouse):
boid_world.add_obstacle()
elif btn_obst_rem.obj.collidepoint(mouse):
boid_world.remove_obstacles()
elif btn_pred_add.obj.collidepoint(mouse):
boid_world.add_predator()
elif btn_pred_rem.obj.collidepoint(mouse):
boid_world.remove_predators()
elif btn_sep_m.obj.collidepoint(mouse):
boid_world.w_separation -= weight_inc
elif btn_sep_p.obj.collidepoint(mouse):
boid_world.w_separation += weight_inc
elif btn_ali_p.obj.collidepoint(mouse):
boid_world.w_alignment -= weight_inc
elif btn_ali_m.obj.collidepoint(mouse):
boid_world.w_alignment += weight_inc
elif btn_coh_m.obj.collidepoint(mouse):
boid_world.w_cohesion -= weight_inc
elif btn_coh_p.obj.collidepoint(mouse):
boid_world.w_cohesion += weight_inc
btn_boid_add.draw(screen, mouse, (10, 10, 100, 20), (15, 15))
btn_boid_rem.draw(screen, mouse, (120, 10, 130, 20), (125, 15))
btn_obst_add.draw(screen, mouse, (10, 40, 100, 20), (15, 45))
btn_obst_rem.draw(screen, mouse, (120, 40, 130, 20), (125, 45))
btn_pred_add.draw(screen, mouse, (10, 70, 100, 20), (15, 75))
btn_pred_rem.draw(screen, mouse, (120, 70, 130, 20), (125, 75))
btn_sep_m.draw(screen, mouse, (120, 100, 20, 20), (125, 105))
btn_sep_p.draw(screen, mouse, (150, 100, 20, 20), (155, 105))
btn_ali_m.draw(screen, mouse, (120, 130, 20, 20), (125, 135))
btn_ali_p.draw(screen, mouse, (150, 130, 20, 20), (155, 135))
btn_coh_m.draw(screen, mouse, (120, 160, 20, 20), (125, 165))
btn_coh_p.draw(screen, mouse, (150, 160, 20, 20), (155, 165))
screen.blit(font.render('Separation', 1, font_color), (15, 105))
screen.blit(font.render('Alignment', 1, font_color), (15, 135))
screen.blit(font.render('Cohesion', 1, font_color), (15, 165))
for boid in boid_world.boids:
boid_world.update_boid_velocity(boid)
boid.draw(screen_half)
for obstacle in boid_world.obstacles:
obstacle.draw(screen_half)
for predator in boid_world.predators:
boid_world.update_predator_velocity(predator)
predator.draw(screen_half)
pygame.display.update()
clock.tick(60)
pygame.quit()
quit()
class Button:
def __init__(self, text):
self.text = text
self.is_hover = False
self.default_color = (100, 100, 100)
self.hover_color = (255, 255, 255)
self.font_color = (100, 0, 0)
self.obj = None
def label(self):
font = pygame.font.Font(None, 20)
return font.render(self.text, 1, self.font_color)
def color(self):
if self.is_hover:
return self.hover_color
else:
return self.default_color
def draw(self, screen, mouse, rectcoord, labelcoord):
# create rect obj, draw, and change color based on input
self.obj = pygame.draw.rect(screen, self.color(), rectcoord)
screen.blit(self.label(), labelcoord)
# change color if mouse over button
self.check_hover(mouse)
def check_hover(self, mouse):
# adjust is_hover value based on mouse over button - to change hover color
if self.obj.collidepoint(mouse):
self.is_hover = True
else:
self.is_hover = False
if __name__ == "__main__":
main()
It happens because you do all calculation inside
elif event.type == pygame.MOUSEBUTTONDOWN:
MOUSEBUTTONDOWN means that button changes state from UP to DOWN (and it takes very short time). It doesn't means button is holding pressed all the time
If you need to check weather button is holding pressed then use pygame.mouse.get_pressed() but use it outside/after for event loop.
It is similar to key events: