Tile based lighting system 2d - python-3.x

I am looking for a tile based lighting system for my tile based game. I have not tried anything because I can't think of an effective way to do this. I have searched stack overflow and I found this but its not what I want. I am making a 2d version of Minecraft with pygame.
here is my tile class
class tile():
def __init__(self, block_category, block_type, x, y, world, win):
self.x, self.y, self.width, self.height = (x*64), (y*64), 64, 64
self.block_type = block_type
self.light_level = 1 # i want light level to range from 0-1
self._image = None
self.world = world
self.win = win
self.posx, self.posy = x, y
try:
self._image = self.world.block_textures[block_category][block_type]
except:
self._image = self.world.block_textures["missing"]["missing_texture"]
self.image = self._image
def draw(self):
#draw code here self.posx, self.win, self.world and self.posy are used here if you are wondering
def change_block(self, block_category, block_type):
try:
self._image = self.world.block_textures[block_category][block_type]
except:
self._image = self.world.block_textures["missing"]["missing_texture"]
self.image = self._image
and my world data looks like this
def generate_world(self):
for x in range(0, self.width):
self.tiles[x] = {}
for y in range(0, self.height):
self.tiles[x][y] = tile("terrain", "air", x, y, self, self.win)
for x in range(0, self.width):
for y in range(0, self.height):
if y == 0:
self.tiles[x][y].change_block("terrain", "bedrock")
elif y == 38:
self.tiles[x][y].change_block("terrain", "grass_block")
elif y < 38 and y > 34:
self.tiles[x][y].change_block("terrain", "dirt")
elif y < 35 and y > 0:
self.tiles[x][y].change_block("terrain", "stone")
if x == 0 or x == self.height - 1:
self.tiles[x][y].change_block("terrain", "bedrock")
return self.tiles
my game looks like this

For 2D games like you're making, how we could apply lighting - more like, shadowing - could go into 2 options:
Change screen color to shadow color & set transparency to objects, as OP suggested
Sandwich entire thing between screen and light layer
Let's start with problem of 1st option:
Problem of setting transparency
Here's demo code based on your idea:
"""
Demonstration of color overlapping
"""
import pygame as pg
class Player(pg.sprite.Sprite):
def __init__(self):
super(Player, self).__init__()
self.image = pg.Surface((50, 50))
self.image.fill((255, 255, 255))
self.rect = self.image.get_rect()
# setting alpha on player
self.image.set_alpha(125)
def update(self, *args, **kwargs):
x, y = pg.mouse.get_pos()
c_x, c_y = self.rect.center
self.rect.move_ip(x - c_x, y - c_y)
def mainloop():
player = Player()
screen = pg.display.set_mode((500, 500))
circle_colors = (255, 0, 0), (0, 255, 0), (0, 0, 255)
circle_coords = (150, 250), (250, 250), (350, 250)
# make surface, set alpha then draw circle
bg_surfaces = []
for (color, center) in zip(circle_colors, circle_coords):
surface = pg.Surface((500, 500), pg.SRCALPHA, 32)
surface.convert_alpha()
surface.set_alpha(125)
pg.draw.circle(surface, color, center, 75)
bg_surfaces.append(surface)
running = True
while running:
screen.fill((0, 0, 0))
# draw background
for surface in bg_surfaces:
screen.blit(surface, surface.get_rect())
for event in pg.event.get():
if event.type == pg.QUIT:
running = False
player.update()
screen.blit(player.image, player.rect)
pg.display.flip()
if __name__ == '__main__':
pg.init()
mainloop()
pg.quit()
As you see, now the player (White square)'s color is Mixed with background circles.
It's basically just like what the drawing program does with layers.
Set layer transparency 50% and stack - everything mixes, producing undesirable effect which is far from lighting effect you wanted.
Unless you want Creeper or Steve to blend with the background and become a ghosty figure, it's better to go for sandwiched layout.
Sandwiched Layout
Following is demo code which uses mouse position as light source position.
Rendering order is Ground > Player > light overlay(shadow)
Demo code:
"""
Code demonstration for https://stackoverflow.com/q/72610504/10909029
Written on Python 3.10 (Using Match on input / event dispatching)
"""
import math
import random
import itertools
from typing import Dict, Tuple, Sequence
import pygame as pg
class Position:
"""Namespace for size and positions"""
tile_x = 20
tile_size = tile_x, tile_x
class SpriteGroup:
"""Namespace for sprite groups, with chain iterator keeping the order"""
ground = pg.sprite.Group()
entities = pg.sprite.Group()
light_overlay = pg.sprite.Group()
#classmethod
def all_sprites(cls):
return itertools.chain(cls.ground, cls.entities, cls.light_overlay)
class Player(pg.sprite.Sprite):
"""Player class, which is merely a rect following pointer in this example."""
def __init__(self):
super(Player, self).__init__()
self.image = pg.Surface((50, 50))
self.image.fill((255, 255, 255))
self.rect = self.image.get_rect()
SpriteGroup.entities.add(self)
self.rect.move_ip(225, 225)
def update(self, *args, **kwargs):
pass
# Intentionally disabling mouse following code
# x, y = pg.mouse.get_pos()
# c_x, c_y = self.rect.center
# self.rect.move_ip(x - c_x, y - c_y)
class TileLightOverlay(pg.sprite.Sprite):
"""
Light overlay tile. Using separate sprites, so we don't have to blit on
every object above ground that requires lighting.
"""
# light lowest boundary
lighting_lo = 255
# light effect radius
light_radius = Position.tile_x * 8
def __init__(self, x, y):
super(TileLightOverlay, self).__init__()
self.image = pg.Surface(Position.tile_size)
self.image.fill((0, 0, 0))
self.rect = self.image.get_rect()
self.rect.move_ip(x * Position.tile_x, y * Position.tile_x)
SpriteGroup.light_overlay.add(self)
def update(self, *args, **kwargs):
self.image.set_alpha(self.brightness)
#property
def brightness(self):
"""Calculate distance between mouse & apply light falloff accordingly"""
distance = math.dist(self.rect.center, pg.mouse.get_pos())
if distance > self.light_radius:
return self.lighting_lo
return (distance / self.light_radius) * self.lighting_lo
class TileGround(pg.sprite.Sprite):
"""Ground tile representation. Not much is going on here."""
def __init__(self, x, y, tile_color: Sequence[float]):
super(TileGround, self).__init__()
self.image = pg.Surface(Position.tile_size)
self.image.fill(tile_color)
self.rect = self.image.get_rect()
self.rect.move_ip(x * Position.tile_x, y * Position.tile_x)
SpriteGroup.ground.add(self)
# create and keep its pair light overlay tile.
self.light_tile = TileLightOverlay(x, y)
class World:
"""World storing ground tile data."""
# tile type storing color etc. for this example only have color.
tile_type: Dict[int, Tuple[float, float, float]] = {
0: (56, 135, 93),
1: (36, 135, 38),
2: (135, 128, 56)
}
def __init__(self):
# coord system : +x → / +y ↓
# generating random tile data
self.tile_data = [
[random.randint(0, 2) for _ in range(25)]
for _ in range(25)
]
# generated tiles
self.tiles = []
def generate(self):
"""Generate world tiles"""
for x, row in enumerate(self.tile_data):
tiles_row = [TileGround(x, y, self.tile_type[col]) for y, col in enumerate(row)]
self.tiles.append(tiles_row)
def process_input(event: pg.event.Event):
"""Process input, in case you need it"""
match event.key:
case pg.K_ESCAPE:
pg.event.post(pg.event.Event(pg.QUIT))
case pg.K_UP:
pass
# etc..
def display_fps_closure(screen: pg.Surface, clock: pg.time.Clock):
"""FPS display"""
font_name = pg.font.get_default_font()
font = pg.font.Font(font_name, 10)
color = (0, 255, 0)
def inner():
text = font.render(f"{int(clock.get_fps())} fps", True, color)
screen.blit(text, text.get_rect())
return inner
def mainloop():
# keeping reference of method/functions to reduce access overhead
fetch_events = pg.event.get
display = pg.display
# local variable setup
screen = display.set_mode((500, 500))
player = Player()
world = World()
world.generate()
clock = pg.time.Clock()
display_fps = display_fps_closure(screen, clock)
running = True
# main loop
while running:
screen.fill((0, 0, 0))
# process event
for event in fetch_events():
# event dispatch
match event.type:
case pg.QUIT:
running = False
case pg.KEYDOWN:
process_input(event)
# draw in ground > entities > light overlay order
for sprite in SpriteGroup.all_sprites():
sprite.update()
screen.blit(sprite.image, sprite.rect)
# draw fps - not related to question, was lazy to remove & looks fancy
clock.tick()
display_fps()
display.flip()
if __name__ == '__main__':
pg.init()
pg.font.init()
mainloop()
pg.quit()
You'll see it's blending properly with shadow without mixing color with ground tiles.
There could be much better approach or ways to implement this - as I never used pygame before, there would be bunch of good/better stuffs I didn't read on document.
But one thing for sure - always approach your goal with mindset that everything is related to your problem until you reach the goal! Comment you thought it wasn't going to be helpful gave me idea for this design.

One option is a black background, then I use set_alpha() to set how light or dark the tile is (how much the black background is seen through the tile) and no overlay is needed. Thanks to #jupiterbjy's original answer for inspiration.

Related

Pygame limiting collision targets [duplicate]

I have made a list of bullets and a list of sprites using the classes below. How do I detect if a bullet collides with a sprite and then delete that sprite and the bullet?
#Define the sprite class
class Sprite:
def __init__(self,x,y, name):
self.x=x
self.y=y
self.image = pygame.image.load(name)
self.rect = self.image.get_rect()
def render(self):
window.blit(self.image, (self.x,self.y))
# Define the bullet class to create bullets
class Bullet:
def __init__(self,x,y):
self.x = x + 23
self.y = y
self.bullet = pygame.image.load("user_bullet.BMP")
self.rect = self.bullet.get_rect()
def render(self):
window.blit(self.bullet, (self.x, self.y))
In PyGame, collision detection is done using pygame.Rect objects. The Rect object offers various methods for detecting collisions between objects. Even the collision between a rectangular and circular object such as a paddle and a ball can be detected by a collision between two rectangular objects, the paddle and the bounding rectangle of the ball.
Some examples:
pygame.Rect.collidepoint:
Test if a point is inside a rectangle
repl.it/#Rabbid76/PyGame-collidepoint
import pygame
pygame.init()
window = pygame.display.set_mode((250, 250))
rect = pygame.Rect(*window.get_rect().center, 0, 0).inflate(100, 100)
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
point = pygame.mouse.get_pos()
collide = rect.collidepoint(point)
color = (255, 0, 0) if collide else (255, 255, 255)
window.fill(0)
pygame.draw.rect(window, color, rect)
pygame.display.flip()
pygame.quit()
exit()
pygame.Rect.colliderect
Test if two rectangles overlap
See also How to detect collisions between two rectangular objects or images in pygame
repl.it/#Rabbid76/PyGame-colliderect
import pygame
pygame.init()
window = pygame.display.set_mode((250, 250))
rect1 = pygame.Rect(*window.get_rect().center, 0, 0).inflate(75, 75)
rect2 = pygame.Rect(0, 0, 75, 75)
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
rect2.center = pygame.mouse.get_pos()
collide = rect1.colliderect(rect2)
color = (255, 0, 0) if collide else (255, 255, 255)
window.fill(0)
pygame.draw.rect(window, color, rect1)
pygame.draw.rect(window, (0, 255, 0), rect2, 6, 1)
pygame.display.flip()
pygame.quit()
exit()
Furthermore, pygame.Rect.collidelist and pygame.Rect.collidelistall can be used for the collision test between a rectangle and a list of rectangles. pygame.Rect.collidedict and pygame.Rect.collidedictall can be used for the collision test between a rectangle and a dictionary of rectangles.
The collision of pygame.sprite.Sprite and pygame.sprite.Group objects, can be detected by pygame.sprite.spritecollide(), pygame.sprite.groupcollide() or pygame.sprite.spritecollideany(). When using these methods, the collision detection algorithm can be specified by the collided argument:
The collided argument is a callback function used to calculate if two sprites are colliding.
Possible collided callables are collide_rect, collide_rect_ratio, collide_circle, collide_circle_ratio, collide_mask
Some examples:
pygame.sprite.spritecollide()
repl.it/#Rabbid76/PyGame-spritecollide
import pygame
pygame.init()
window = pygame.display.set_mode((250, 250))
sprite1 = pygame.sprite.Sprite()
sprite1.image = pygame.Surface((75, 75))
sprite1.image.fill((255, 0, 0))
sprite1.rect = pygame.Rect(*window.get_rect().center, 0, 0).inflate(75, 75)
sprite2 = pygame.sprite.Sprite()
sprite2.image = pygame.Surface((75, 75))
sprite2.image.fill((0, 255, 0))
sprite2.rect = pygame.Rect(*window.get_rect().center, 0, 0).inflate(75, 75)
all_group = pygame.sprite.Group([sprite2, sprite1])
test_group = pygame.sprite.Group(sprite2)
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
sprite1.rect.center = pygame.mouse.get_pos()
collide = pygame.sprite.spritecollide(sprite1, test_group, False)
window.fill(0)
all_group.draw(window)
for s in collide:
pygame.draw.rect(window, (255, 255, 255), s.rect, 5, 1)
pygame.display.flip()
pygame.quit()
exit()
For a collision with masks, see How can I make a collision mask? or Pygame mask collision
See also Collision and Intersection
pygame.sprite.spritecollide() / collide_circle
repl.it/#Rabbid76/PyGame-spritecollidecollidecircle
import pygame
pygame.init()
window = pygame.display.set_mode((250, 250))
sprite1 = pygame.sprite.Sprite()
sprite1.image = pygame.Surface((80, 80), pygame.SRCALPHA)
pygame.draw.circle(sprite1.image, (255, 0, 0), (40, 40), 40)
sprite1.rect = pygame.Rect(*window.get_rect().center, 0, 0).inflate(80, 80)
sprite1.radius = 40
sprite2 = pygame.sprite.Sprite()
sprite2.image = pygame.Surface((80, 89), pygame.SRCALPHA)
pygame.draw.circle(sprite2.image, (0, 255, 0), (40, 40), 40)
sprite2.rect = pygame.Rect(*window.get_rect().center, 0, 0).inflate(80, 80)
sprite2.radius = 40
all_group = pygame.sprite.Group([sprite2, sprite1])
test_group = pygame.sprite.Group(sprite2)
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
sprite1.rect.center = pygame.mouse.get_pos()
collide = pygame.sprite.spritecollide(sprite1, test_group, False, pygame.sprite.collide_circle)
window.fill(0)
all_group.draw(window)
for s in collide:
pygame.draw.circle(window, (255, 255, 255), s.rect.center, s.rect.width // 2, 5)
pygame.display.flip()
pygame.quit()
exit()
What does this all mean for your code?
pygame.Surface.get_rect.get_rect() returns a rectangle with the size of the Surface object, that always starts at (0, 0) since a Surface object has no position. The position of the rectangle can be specified by a keyword argument. For example, the centre of the rectangle can be specified with the keyword argument center. These keyword arguments are applied to the attributes of the pygame.Rect before it is returned (see pygame.Rect for a list of the keyword arguments).
See *Why is my collision test always returning 'true' and why is the position of the rectangle of the image always wrong (0, 0)?
You do not need the x and y attributes of Sprite and Bullet at all. Use the position of the rect attribute instead:
#Define the sprite class
class Sprite:
def __init__(self, x, y, name):
self.image = pygame.image.load(name)
self.rect = self.image.get_rect(topleft = (x, y))
def render(self):
window.blit(self.image, self.rect)
# Define the bullet class to create bullets
class Bullet:
def __init__(self, x, y):
self.bullet = pygame.image.load("user_bullet.BMP")
self.rect = self.bullet.get_rect(topleft = (x + 23, y))
def render(self):
window.blit(self.bullet, self.rect)
Use pygame.Rect.colliderect() to detect collisions between instances of Sprite and Bullet.
See How to detect collisions between two rectangular objects or images in pygame:
my_sprite = Sprite(sx, sy, name)
my_bullet = Bullet(by, by)
while True:
# [...]
if my_sprite.rect.colliderect(my_bullet.rect):
printe("hit")
From what I understand of pygame you just need to check if the two rectangles overlap using the colliderect method. One way to do it is to have a method in your Bullet class that checks for collisions:
def is_collided_with(self, sprite):
return self.rect.colliderect(sprite.rect)
Then you can call it like:
sprite = Sprite(10, 10, 'my_sprite')
bullet = Bullet(20, 10)
if bullet.is_collided_with(sprite):
print('collision!')
bullet.kill()
sprite.kill()
There is a very simple method for what you are trying to do using built in methods.
here is an example.
import pygame
import sys
class Sprite(pygame.sprite.Sprite):
def __init__(self, pos):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface([20, 20])
self.image.fill((255, 0, 0))
self.rect = self.image.get_rect()
self.rect.center = pos
def main():
pygame.init()
clock = pygame.time.Clock()
fps = 50
bg = [255, 255, 255]
size =[200, 200]
screen = pygame.display.set_mode(size)
player = Sprite([40, 50])
player.move = [pygame.K_LEFT, pygame.K_RIGHT, pygame.K_UP, pygame.K_DOWN]
player.vx = 5
player.vy = 5
wall = Sprite([100, 60])
wall_group = pygame.sprite.Group()
wall_group.add(wall)
player_group = pygame.sprite.Group()
player_group.add(player)
# I added loop for a better exit from the game
loop = 1
while loop:
for event in pygame.event.get():
if event.type == pygame.QUIT:
loop = 0
key = pygame.key.get_pressed()
for i in range(2):
if key[player.move[i]]:
player.rect.x += player.vx * [-1, 1][i]
for i in range(2):
if key[player.move[2:4][i]]:
player.rect.y += player.vy * [-1, 1][i]
screen.fill(bg)
# first parameter takes a single sprite
# second parameter takes sprite groups
# third parameter is a do kill command if true
# all group objects colliding with the first parameter object will be
# destroyed. The first parameter could be bullets and the second one
# targets although the bullet is not destroyed but can be done with
# simple trick bellow
hit = pygame.sprite.spritecollide(player, wall_group, True)
if hit:
# if collision is detected call a function in your case destroy
# bullet
player.image.fill((255, 255, 255))
player_group.draw(screen)
wall_group.draw(screen)
pygame.display.update()
clock.tick(fps)
pygame.quit()
# sys.exit
if __name__ == '__main__':
main()
Make a group for the bullets, and then add the bullets to the group.
What I would do is this:
In the class for the player:
def collideWithBullet(self):
if pygame.sprite.spritecollideany(self, 'groupName'):
print("CollideWithBullet!!")
return True
And in the main loop somewhere:
def run(self):
if self.player.collideWithBullet():
print("Game Over")
Hopefully that works for you!!!
Inside the Sprite class, try adding a self.mask attribute with
self.mask = pygame.mask.from_surface(self.image)
and a collide_mask function inside of the Sprite class with this code:
def collide_mask(self, mask):
collided = False
mask_outline = mask.outline()
self.mask_outline = self.mask.outline()
for point in range(len(mask_outline)):
mask_outline[point] = list(mask_outline[point])
mask_outline[point][0] += bullet.x
mask_outline[point][1] += bullet.y
for point in range(len(self.mask_outline)):
self.mask_outline[point] = list(mask_outline[point])
self.mask_outline[point][0] += self.x
self.mask_outline[point][1] += self.y
for point in mask_outline:
for self_mask_point in self.mask_outline:
if point = self_mask_point:
collided = True
return collided

How to do OpenCV Laser shot detection with Python on light backgrounds?

I'm trying to build a laser shot detection game in Python3 using OpenCV. I have a proof of concept working which will highlight detected momentary "shots" of the laser on the background.
The problem I'm having is that the laser signature doesn't get detected on lighter backgrounds, or if there is a very white / bright color item near the shot. I'm sure this is because the way I'm using binary thresholds to detect the laser as the brightest thing in the frame, and with light elements the laser gets washed out.
My question is how can I alter my approach to handle this situation, or perhaps "calibrate" the background / other items so that the laser can be detected? Ultimately I'm trying to detect laser shots on a computer screen, where the background where shots are landing is a video with its own high lights and the screen puts out its own light.
Any guidance is appreciated.
main.py
import cv2
from camera import VideoCamera
from detection import LaserDetector
debug=False
radius_min = float(1)
radius_max = float(10)
shot_size = 5
color_blue = (255, 0, 0)
cam = VideoCamera(640, 480)
shots = []
try:
while(True):
frame = cam.get_frame()
if frame is not False:
shot = LaserDetector(frame, debug).detect()
if shot:
x, y, radius = shot
if radius >= radius_min and radius <= radius_max:
shots.append(shot)
if debug:
print(f"(x:{int(x)}, y:{int(y)}, r:{int(radius)})")
for s in shots:
cv2.circle(frame, (int(s[0]), int(s[1])), shot_size, color_blue, 1)
cv2.imshow('frame', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
except KeyboardInterrupt:
pass
finally:
print("\nClosing video capture...")
cam.release()
cv2.destroyAllWindows()
camera.py
import cv2
class VideoCamera(object):
def __init__(self, cam_width, cam_height):
self.capture = cv2.VideoCapture(0)
self.capture.set(cv2.CAP_PROP_FRAME_WIDTH, cam_width)
self.capture.set(cv2.CAP_PROP_FRAME_HEIGHT, cam_height)
def release(self):
self.capture.release()
def __del__(self):
self.capture.release()
def get_frame(self):
success, frame = self.capture.read()
if success:
return frame
else:
return False
def frame_to_jpeg(self, frame):
ret, jpeg = cv2.imencode('.jpg', frame)
return jpeg.tobytes()
detection.py
import cv2
import numpy as np
import decimal
class LaserDetector(object):
def __init__(self, frame, debug=False):
self.debug = debug
self.frame = frame
def get_contour_points(self, mask):
countours = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
if len(countours) > 0:
# find the largest contour in the mask, then use
# it to compute the minimum enclosing circle and
c = max(countours, key=cv2.contourArea)
((x, y), radius) = cv2.minEnclosingCircle(c)
moments = cv2.moments(c)
if moments["m00"] > 0:
# set the center
(x,y) = int(moments["m10"] / moments["m00"]), \
int(moments["m01"] / moments["m00"])
radius = round(decimal.Decimal(radius), 3)
return (int(x), int(y), radius)
return False
def get_hsv_threshold_mask(self, frame):
hsv_image = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
h, s, v = cv2.split(hsv_image)
ret, h_frame = cv2.threshold(h, 125, 160, cv2.THRESH_BINARY)
ret, v_frame = cv2.threshold(v, 250, 256, cv2.THRESH_BINARY)
output = cv2.bitwise_and(h_frame, v_frame, frame)
if self.debug:
indiv_output = np.concatenate((h_frame, v_frame, output), axis=1)
cv2.imshow("threshold", indiv_output)
return output
def detect(self):
mask = self.get_hsv_threshold_mask(self.frame)
return self.get_contour_points(mask)

How To drag an image object in WxPython GUI?

In below code, I have image shaped transparent window and a image inside of it, I would like to move the image(screw photo)by mouse. I wrote a bind function for that screw image but it does not move? what might be the problem?
As it can be seen I added images and bind functions. Is there a missing logic?
import wx
from wx import *
import wx.lib.statbmp as sb
from io import StringIO
# Create a .png image with something drawn on a white background
# and put the path to it here.
IMAGE_PATH = './wood.png'
class ShapedFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1, "Shaped Window",
style = wx.FRAME_SHAPED | wx.SIMPLE_BORDER | wx.STAY_ON_TOP)
self.hasShape = False
self.delta = wx.Point(0,0)
# Load the image
image = wx.Image(IMAGE_PATH, wx.BITMAP_TYPE_PNG)
image.SetMaskColour(255,255,255)
image.SetMask(True)
self.bmp = wx.Bitmap(image)
self.SetClientSize((self.bmp.GetWidth(), self.bmp.GetHeight()))
dc = wx.ClientDC(self)
dc.DrawBitmap(self.bmp, 0,0, True)
self.SetWindowShape()
self.Bind(wx.EVT_RIGHT_UP, self.OnExit)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_WINDOW_CREATE, self.SetWindowShape)
panel = MyPanel(parent=self)
def SetWindowShape(self, evt=None):
r = wx.Region(self.bmp)
self.hasShape = self.SetShape(r)
def OnDoubleClick(self, evt):
if self.hasShape:
self.SetShape(wx.Region())
self.hasShape = False
else:
self.SetWindowShape()
def OnPaint(self, evt):
dc = wx.PaintDC(self)
dc.DrawBitmap(self.bmp, 0,0, True)
def OnExit(self, evt):
self.Close()
def OnLeftDown(self, evt):
self.CaptureMouse()
pos = self.ClientToScreen(evt.GetPosition())
origin = self.GetPosition()
self.delta = wx.Point(pos.x - origin.x, pos.y - origin.y)
def OnMouseMove(self, evt):
if evt.Dragging() and evt.LeftIsDown():
pos = self.ClientToScreen(evt.GetPosition())
newPos = (pos.x - self.delta.x, pos.y - self.delta.y)
self.Move(newPos)
def OnLeftUp(self, evt):
if self.HasCapture():
self.ReleaseMouse()
class MyPanel(wx.Panel):
# A panel is a window on which controls are placed. (e.g. buttons and text boxes)
# wx.Panel class is usually put inside a wxFrame object. This class is also inherited from wxWindow class.
def __init__(self,parent):
super().__init__(parent=parent)
MyImage(self)
class MyImage(wx.StaticBitmap):
def __init__(self,parent):
super().__init__(parent=parent)
jpg1 = wx.Image('./Images/screwsmall.png', wx.BITMAP_TYPE_ANY).ConvertToBitmap()
# bitmap upper left corner is in the position tuple (x, y) = (5, 5)
self.myImage = wx.StaticBitmap(parent, -1, jpg1, (10 + jpg1.GetWidth(), 5), (jpg1.GetWidth(), jpg1.GetHeight()))
self.myImage.Bind(wx.EVT_MOTION, self.OnMouseMove)
self.myImage.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
def OnMouseMove(self, evt):
if evt.Dragging() and evt.LeftIsDown():
pos = self.ClientToScreen(evt.GetPosition())
newPos = (pos.x - self.delta.x, pos.y - self.delta.y)
self.Move(newPos)
def OnLeftUp(self, evt):
if self.HasCapture():
self.ReleaseMouse()
def OnLeftDown(self, evt):
self.CaptureMouse()
pos = self.ClientToScreen(evt.GetPosition())
origin = self.GetPosition()
self.delta = wx.Point(pos.x - origin.x, pos.y - origin.y)
if __name__ == '__main__':
app = wx.App()
ShapedFrame().Show()
app.MainLoop()
Visual output of my code you can use different shapes in local directory. To install wxpython for 3.x you can check this link https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-16.04/ download your version for ubuntu and use pip install command.
I am going to put my answer. As it can be seen in the #RolfofSaxony comment, there is a drag image demo inside the WXPYTHON tar file. In that DragImage.py file there are two different classes do the dragging job. I modified those functions and wrote my own two class. You can use these classes in your code as a component. My code is working and tested.
class DragShape:
def __init__(self, bmp):
self.bmp = bmp
self.pos = (0,0)
self.shown = True
self.text = None
self.fullscreen = False
def HitTest(self, pt):
rect = self.GetRect()
return rect.Contains(pt)
def GetRect(self):
return wx.Rect(self.pos[0], self.pos[1],
self.bmp.GetWidth(), self.bmp.GetHeight())
def Draw(self, dc, op = wx.COPY):
if self.bmp.IsOk():
memDC = wx.MemoryDC()
memDC.SelectObject(self.bmp)
dc.Blit(self.pos[0], self.pos[1],
self.bmp.GetWidth(), self.bmp.GetHeight(),
memDC, 0, 0, op, True)
return True
else:
return False
#----------------------------------------------------------------------
class DragCanvas(wx.ScrolledWindow):
def __init__(self, parent, ID):
wx.ScrolledWindow.__init__(self, parent, ID)
self.shapes = []
self.dragImage = None
self.dragShape = None
self.hiliteShape = None
self.SetCursor(wx.Cursor(wx.CURSOR_ARROW))
bmp = images.TheKid.GetBitmap()
shape = DragShape(bmp)
shape.pos = (200, 5)
self.shapes.append(shape)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
self.Bind(wx.EVT_MOTION, self.OnMotion)
self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow)
# We're not doing anything here, but you might have reason to.
# for example, if you were dragging something, you might elect to
# 'drop it' when the cursor left the window.
def OnLeaveWindow(self, evt):
pass
# Go through our list of shapes and draw them in whatever place they are.
def DrawShapes(self, dc):
for shape in self.shapes:
if shape.shown:
shape.Draw(dc)
# This is actually a sophisticated 'hit test', but in this
# case we're also determining which shape, if any, was 'hit'.
def FindShape(self, pt):
for shape in self.shapes:
if shape.HitTest(pt):
return shape
return None
# Fired whenever a paint event occurs
def OnPaint(self, evt):
dc = wx.PaintDC(self)
self.PrepareDC(dc)
self.DrawShapes(dc)
# print('OnPaint')
# Left mouse button is down.
def OnLeftDown(self, evt):
# Did the mouse go down on one of our shapes?
shape = self.FindShape(evt.GetPosition())
# If a shape was 'hit', then set that as the shape we're going to
# drag around. Get our start position. Dragging has not yet started.
# That will happen once the mouse moves, OR the mouse is released.
if shape:
self.dragShape = shape
self.dragStartPos = evt.GetPosition()
# Left mouse button up.
def OnLeftUp(self, evt):
if not self.dragImage or not self.dragShape:
self.dragImage = None
self.dragShape = None
return
# Hide the image, end dragging, and nuke out the drag image.
self.dragImage.Hide()
self.dragImage.EndDrag()
self.dragImage = None
if self.hiliteShape:
self.RefreshRect(self.hiliteShape.GetRect())
self.hiliteShape = None
# reposition and draw the shape
# Note by jmg 11/28/03
# Here's the original:
#
# self.dragShape.pos = self.dragShape.pos + evt.GetPosition() - self.dragStartPos
#
# So if there are any problems associated with this, use that as
# a starting place in your investigation. I've tried to simulate the
# wx.Point __add__ method here -- it won't work for tuples as we
# have now from the various methods
#
# There must be a better way to do this :-)
#
self.dragShape.pos = (
self.dragShape.pos[0] + evt.GetPosition()[0] - self.dragStartPos[0],
self.dragShape.pos[1] + evt.GetPosition()[1] - self.dragStartPos[1]
)
self.dragShape.shown = True
self.RefreshRect(self.dragShape.GetRect())
self.dragShape = None
# The mouse is moving
def OnMotion(self, evt):
# Ignore mouse movement if we're not dragging.
if not self.dragShape or not evt.Dragging() or not evt.LeftIsDown():
return
# if we have a shape, but haven't started dragging yet
if self.dragShape and not self.dragImage:
# only start the drag after having moved a couple pixels
tolerance = 2
pt = evt.GetPosition()
dx = abs(pt.x - self.dragStartPos.x)
dy = abs(pt.y - self.dragStartPos.y)
if dx <= tolerance and dy <= tolerance:
return
# refresh the area of the window where the shape was so it
# will get erased.
self.dragShape.shown = False
self.RefreshRect(self.dragShape.GetRect(), True)
self.Update()
item = self.dragShape.text if self.dragShape.text else self.dragShape.bmp
self.dragImage = wx.DragImage(item,
wx.Cursor(wx.CURSOR_HAND))
hotspot = self.dragStartPos - self.dragShape.pos
self.dragImage.BeginDrag(hotspot, self, self.dragShape.fullscreen)
self.dragImage.Move(pt)
self.dragImage.Show()
# if we have shape and image then move it, posibly highlighting another shape.
elif self.dragShape and self.dragImage:
onShape = self.FindShape(evt.GetPosition())
unhiliteOld = False
hiliteNew = False
# figure out what to hilite and what to unhilite
if self.hiliteShape:
if onShape is None or self.hiliteShape is not onShape:
unhiliteOld = True
if onShape and onShape is not self.hiliteShape and onShape.shown:
hiliteNew = True
# if needed, hide the drag image so we can update the window
if unhiliteOld or hiliteNew:
self.dragImage.Hide()
if unhiliteOld:
dc = wx.ClientDC(self)
self.hiliteShape.Draw(dc)
self.hiliteShape = None
if hiliteNew:
dc = wx.ClientDC(self)
self.hiliteShape = onShape
self.hiliteShape.Draw(dc, wx.INVERT)
# now move it and show it again if needed
self.dragImage.Move(evt.GetPosition())
if unhiliteOld or hiliteNew:
self.dragImage.Show()

Return a sprite to its original position?

So I have a very basic PyGame Python program which is a 'car' that the user controls and one which comes down he road towards it. How can I get the NPC car to return to its original position once it leaves the screen? My current code is shown below, thanks for any help you can provide!
import pygame, random
from car import Car
from car import AutoCar
pygame.init()
play = True
clock = pygame.time.Clock()
screen = pygame.display.set_mode((700, 750))
pygame.display.set_caption("Car Game")
# Define a bunch of colours
GREEN = (20, 255, 100)
GREY = (210, 210, 210)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
PURPLE = (255, 0, 255)
BLUE = (0, 0, 255)
all_sprites = pygame.sprite.Group() # Creates a list of all sprites in the game
playerCar = Car(RED, 20, 30) # Defines the car, its colour, and its
size
playerCar.rect.x = 200
playerCar.rect.y = 300
AutoCar1 = AutoCar(BLUE, 20, 30)
AutoCar1.rect.x = 200
AutoCar1.rect.y = 20
all_sprites.add(playerCar) # Adds the playerCar to the list of sprites
all_sprites.add(AutoCar1) # Adds an automated 'enemy' car which you must avoid
while play:
for event in pygame.event.get():
if event.type == pygame.QUIT: # If the detected event is clicking the exit button, stop the program.
play = False
elif event.type == pygame.KEYDOWN: # If a key is pressed and the key is 'x', quit the game.
if event.key == pygame.K_x:
play = False
keys = pygame.key.get_pressed() # Defines a variable keys that consists of whatever event is happening in the game
if keys[pygame.K_LEFT]:
playerCar.move_left(5)
if keys[pygame.K_RIGHT]:
playerCar.move_right(5)
if keys[pygame.K_DOWN]:
playerCar.move_backward(5)
if keys[pygame.K_UP]:
playerCar.move_forward(5)
all_sprites.update() # Draws in all sprites
AutoCar1.auto_move_forward(3) # Makes the 'enemy' move forward automatically.
# Create the background
screen.fill(GREEN)
# Create the road
pygame.draw.rect(screen, GREY, [100, 0, 500, 750])
# Draw the lines on the road
pygame.draw.line(screen, WHITE, [340, 0], [340, 750], 5)
# Draw in all the sprites
all_sprites.draw(screen)
pygame.display.flip()
clock.tick(60) # Cap limit at 60FPS
pygame.quit()
I also have a file defining the class Car and Autocar:
import pygame
WHITE = (255, 255, 255)
class Car(pygame.sprite.Sprite): # Creates a class representing a car, derived from the sprite class
def __init__(self, color, width, height):
# Call the parent class (i.e. Sprite) constructor
super().__init__()
self.image = pygame.Surface([width,height])
self.image.fill(WHITE)
self.image.set_colorkey(WHITE) # Sets white as transparent for this class
# alternatively load a proper car picture:
# self.image = pygame.image.load("car.png").convert_alpha()
pygame.draw.rect(self.image, color, [0, 0, width, height])
self.rect = self.image.get_rect() # Fetches the rectangle with the dimensions of the image
def move_right(self, pixels): # Adds to the x value of the car the desired amount of pixels
self.rect.x += pixels
def move_left(self, pixels): # Same as move_right()
self.rect.x -= pixels
def move_forward(self, pixels):
self.rect.y -= pixels
def move_backward(self, pixels):
self.rect.y += pixels
class AutoCar(pygame.sprite.Sprite):
def __init__(self, color, width, height):
super().__init__()
self.image = pygame.Surface([width, height])
self.image.fill(WHITE)
self.image.set_colorkey(WHITE)
pygame.draw.rect(self.image, color, [0, 0, width, height])
self.rect = self.image.get_rect()
def auto_move_forward(self, pixels):
self.rect.y += pixels
def return_to_top(self):
Just store the start position as an attribute of the AutoCar, check in the update method if the sprite is below the screen and then set the self.rect.topleft (or one of the other rect attributes) to the self.start_position.
class AutoCar(pygame.sprite.Sprite):
def __init__(self, color, width, height):
super().__init__()
self.image = pygame.Surface([width, height])
self.image.fill(WHITE)
self.image.set_colorkey(WHITE)
pygame.draw.rect(self.image, color, [0, 0, width, height])
self.rect = self.image.get_rect()
self.start_position = (200, -50)
self.rect.topleft = self.start_position
def auto_move_forward(self, pixels):
self.rect.y += pixels
def return_to_top(self):
self.rect.topleft = self.start_position
def update(self):
if self.rect.top > 750: # Screen height.
self.rect.topleft = self.start_position

How do I pass a rect into pygame.display.update() to update a specific area of the window?

On the documentation page for pygame.display.update(), it says you can pass a rect into the method to update part of the screen. However, all of the examples I see just pass an existing rect from an image or shape in the program. How can I tell it to update an area on the screen directly? For example, when drawing a rectangle, I could use a rect argument of (100,200,30,40). This would draw a rectangle with a top at 200, a left at 100, a width of 30, and a height of 40. How can I pass a similar argument into pygame.display.update()? I tried pygame.display.update((100,200,30,40)), but this updates the entire window.
Just define a rect and pass it to pygame.display.update() to update only this specific region of the display. You can also pass a list of rects.
import random
import pygame as pg
from pygame.math import Vector2
# A simple sprite, just to have something moving on the screen.
class Ball(pg.sprite.Sprite):
def __init__(self, screen_rect):
super().__init__()
radius = random.randrange(5, 31)
self.image = pg.Surface((radius*2, radius*2), pg.SRCALPHA)
pg.draw.circle(self.image, pg.Color('dodgerblue1'), (radius, radius), radius)
pg.draw.circle(self.image, pg.Color('dodgerblue3'), (radius, radius), radius-2)
self.rect = self.image.get_rect(center=screen_rect.center)
self.vel = Vector2(random.uniform(-2, 2), random.uniform(-2, 2))
self.pos = Vector2(self.rect.center)
self.screen_rect = screen_rect
self.lifetime = 350
def update(self):
self.pos += self.vel
self.rect.center = self.pos
self.lifetime -= 1
if not self.screen_rect.contains(self.rect) or self.lifetime <= 0:
self.kill()
def main():
screen = pg.display.set_mode((800, 600))
screen.fill((20, 40, 70))
pg.display.update()
screen_rect = screen.get_rect()
clock = pg.time.Clock()
all_sprites = pg.sprite.Group()
# Pass this rect to `pg.display.update` to update only this area.
update_rect = pg.Rect(50, 50, 500, 400)
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
all_sprites.add(Ball(screen_rect))
all_sprites.update()
screen.fill((20, 50, 90))
all_sprites.draw(screen)
# Update only the area that we specified with the `update_rect`.
pg.display.update(update_rect)
clock.tick(60)
if __name__ == '__main__':
pg.init()
main()
pg.quit()

Resources