Commit 054aed01 authored by russhughes's avatar russhughes

smoother animation, less gc glitching, better collision handling

parent 5f6bbbd6
''' '''
toasters.py - Flying Toasters(ish) for Raspberry Pi PICO with Waveshare Pico LCD 2 Display Module. toasters.py - Flying Toasters(ish) for Raspberry Pi Pico with Waveshare Pico LCD 2 Display Module.
Uses spritesheet from CircuitPython_Flying_Toasters pendant project Uses spritesheet from CircuitPython_Flying_Toasters pendant project
https://learn.adafruit.com/circuitpython-sprite-animation-pendant-mario-clouds-flying-toasters https://learn.adafruit.com/circuitpython-sprite-animation-pendant-mario-clouds-flying-toasters
...@@ -7,6 +7,8 @@ toasters.py - Flying Toasters(ish) for Raspberry Pi PICO with Waveshare Pico LCD ...@@ -7,6 +7,8 @@ toasters.py - Flying Toasters(ish) for Raspberry Pi PICO with Waveshare Pico LCD
Convert spritesheet bmp to tft.bitmap() method compatible python module using: Convert spritesheet bmp to tft.bitmap() method compatible python module using:
python3 ./sprites2bitmap.py toasters.bmp 64 64 4 > toast_bitmaps.py python3 ./sprites2bitmap.py toasters.bmp 64 64 4 > toast_bitmaps.py
Video: https://youtu.be/9W51gukpQus
''' '''
import gc import gc
...@@ -19,115 +21,124 @@ import toast_bitmaps ...@@ -19,115 +21,124 @@ import toast_bitmaps
TOASTER_FRAMES = [0, 1, 2, 3] TOASTER_FRAMES = [0, 1, 2, 3]
TOAST_FRAMES = [4] TOAST_FRAMES = [4]
class rect(): def collide(a_col, a_row, a_width, a_height, b_col, b_row, b_width, b_height):
'''rectangle class'''
def __init__(self, col, row, width, height):
'''create new rectangle'''
self.col = col
self.row = row
self.width = width
self.height = height
def __add__(self, other):
'''add two rectangles'''
return rect(
self.col + other.col,
self.row + other.row,
self.width + other.width,
self.height + other.height)
def collide(rect_a, rect_b):
'''return true if two rectangles overlap''' '''return true if two rectangles overlap'''
return (rect_a.col + rect_a.width >= rect_b.col return (a_col + a_width >= b_col and a_col <= b_col + b_width
and rect_a.col <= rect_b.col + rect_b.width and a_row + a_height >= b_row and a_row <= b_row + b_height)
and rect_a.row + rect_a.height >= rect_b.row
and rect_a.row <= rect_b.row + rect_b.height)
def collision(sprites): def collision(sprites):
''''return true if any sprites overlap''' ''''return true if any sprites overlap'''
return any(collide(a.location, b.location) for a, b in zip(sprites[::], sprites[1::])) return any(
collide(a.col, a.row, a.width, a.height, b.col, b.row, b.width, b.height)
for a, b in zip(sprites[::], sprites[1::]))
def random_start(tft, sprites, bitmaps, num):
'''
Return a random location along the top or right of the screen, if that location would overlaps
with another sprite return (0,0). This allows the other sprites to keep moving giving the next
random_start a better chance to avoid a collision.
def random_start(tft, sprites, bitmaps): '''
'''return new location along the top or right of the screen that does not overlap any sprites''' # 50/50 chance to try along the top/right half or along the right/top half of the screen
while True: if random.getrandbits(1):
if random.getrandbits(2) > 1:
row = 1 row = 1
col = random.randint(bitmaps.WIDTH, tft.width()-bitmaps.WIDTH) col = random.randint(bitmaps.WIDTH//2, tft.width()-bitmaps.WIDTH)
else: else:
col = tft.width() - bitmaps.WIDTH col = tft.width() - bitmaps.WIDTH
row = random.randint(bitmaps.HEIGHT, tft.height()-bitmaps.HEIGHT) row = random.randint(1, tft.height() // 2)
if any(collide(
col, row, bitmaps.WIDTH, bitmaps.HEIGHT,
sprite.col, sprite.row, sprite.width, sprite.height)
for sprite in sprites if num != sprite.num):
col = 0
row = 0
new_location = rect(col, row, bitmaps.WIDTH, bitmaps.HEIGHT) return (col, row)
if not any(collide(new_location, sprite.location) for sprite in sprites):
return new_location
def main(): def main():
class toast(): class Toast():
''' '''
toast class to keep track of toaster and toast sprites Toast class to keep track of toaster and toast sprites
''' '''
def __init__(self, sprites, bitmaps, frames): def __init__(self, sprites, bitmaps, frames):
'''create new sprite in random location that does not overlap other sprites''' '''create new sprite in random location that does not overlap other sprites'''
self.id = len(sprites) self.num = len(sprites)
self.bitmaps = bitmaps self.bitmaps = bitmaps
self.frames = frames self.frames = frames
self.steps = len(frames) self.steps = len(frames)
self.location = random_start(tft, sprites, bitmaps) self.col, self.row = random_start(tft, sprites, bitmaps, self.num)
self.last = self.location self.width = bitmaps.WIDTH
self.height = bitmaps.HEIGHT
self.last_col = self.col
self.last_row = self.row
self.step = random.randint(0, self.steps) self.step = random.randint(0, self.steps)
self.direction = rect(-random.randint(2, 5), 2, 0, 0) self.dir_col = -random.randint(2, 5)
self.prev_direction = self.direction self.dir_row = 2
self.prev_dir_col = self.dir_col
self.prev_dir_row = self.dir_row
self.iceberg = 0 self.iceberg = 0
def clear(self): def clear(self):
'''clear above and behind sprite''' '''clear above and behind sprite'''
tft.fill_rect( tft.fill_rect(
self.location.col, self.location.row-1, self.location.width, self.direction.row+1, self.col, self.row-1, self.width, self.dir_row+1,
st7789.BLACK) st7789.BLACK)
tft.fill_rect( tft.fill_rect(
self.location.col+self.bitmaps.WIDTH+self.direction.col, self.location.row, self.col+self.width+self.dir_col, self.row,
-self.direction.col, self.location.height, st7789.BLACK) -self.dir_col, self.height, st7789.BLACK)
def erase(self): def erase(self):
'''erase last postion of sprite''' '''erase last postion of sprite'''
tft.fill_rect( tft.fill_rect(
self.last.col, self.last.row, self.last.width, self.last.height, st7789.BLACK) self.last_col, self.last_row, self.width, self.height, st7789.BLACK)
def move(self, sprites): def move(self, sprites):
'''step frame and move sprite''' '''step frame and move sprite'''
if self.steps: if self.steps:
self.step = (self.step + 1) % self.steps self.step = (self.step + 1) % self.steps
self.last = self.location self.last_col = self.col
new_location = self.location + self.direction self.last_row = self.row
new_col = self.col + self.dir_col
new_row = self.row + self.dir_row
# if new location collides with another sprite, change direction for 32 frames
if any(
collide(new_col, new_row, self.width, self.height, sprite.col, sprite.row, sprite.width, sprite.height)
for sprite in sprites if self.num != sprite.num):
# if new location collides with another sprite, change direction down for 16 frames self.iceberg = 32
if any(collide(new_location, sprite.location) for sprite in sprites if self.id != sprite.id): self.dir_col = -1
self.iceberg = 16 self.dir_row = 3
self.direction = rect(-1, 2, 0, 0) new_col = self.col + self.dir_col
new_location = self.location + self.direction new_row = self.row + self.dir_row
self.location = new_location self.col = new_col
self.row = new_row
# if new location touches edge of screen, erase then set new start location # if new location touches edge of screen, erase then set new start location
if new_location.col <= 0 or new_location.row > tft.height() - self.location.height: if self.col <= 0 or self.row > tft.height() - self.height:
self.erase() self.erase()
self.direction = rect(-random.randint(2, 5), 2, 0, 0) self.dir_col = -random.randint(2, 5)
self.location = random_start(tft, sprites, self.bitmaps) self.dir_row = 2
self.col, self.row = random_start(tft, sprites, self.bitmaps, self.num)
# Track post collision direction change # Track post collision direction change
if self.iceberg: if self.iceberg:
self.iceberg -= 1 self.iceberg -= 1
if self.iceberg == 1: if self.iceberg == 1:
self.direction = self.prev_direction self.dir_col = self.prev_dir_col
self.dir_row = self.prev_dir_row
def draw(self): def draw(self):
'''draw current frame of sprite at it's location''' '''if the location is not 0,0 draw current frame of sprite at it's location'''
tft.bitmap(self.bitmaps, self.location.col, self.location.row, self.frames[self.step]) if self.col and self.row:
tft.bitmap(self.bitmaps, self.col, self.row, self.frames[self.step])
# configure spi interface # configure spi interface
# configure spi interface # configure spi interface
...@@ -151,14 +162,16 @@ def main(): ...@@ -151,14 +162,16 @@ def main():
# create toast spites and set animation frames # create toast spites and set animation frames
sprites = [] sprites = []
sprites.append(toast(sprites, toast_bitmaps, TOAST_FRAMES)) sprites.append(Toast(sprites, toast_bitmaps, TOAST_FRAMES))
sprites.append(toast(sprites, toast_bitmaps, TOASTER_FRAMES)) sprites.append(Toast(sprites, toast_bitmaps, TOASTER_FRAMES))
sprites.append(toast(sprites, toast_bitmaps, TOASTER_FRAMES)) sprites.append(Toast(sprites, toast_bitmaps, TOASTER_FRAMES))
# move and draw sprites stop = Pin(15, Pin.IN, Pin.PULL_UP) # Top Left
while True:
for sprite in sprites: # move and draw sprites until stop button is pressed
while stop.value():
for sprite in sprites:
sprite.clear() sprite.clear()
sprite.move(sprites) sprite.move(sprites)
sprite.draw() sprite.draw()
......
''' '''
toasters.py - Flying Toasters(ish) toasters.py - Flying Toasters(ish) an ESP-32 and ST7789 240x320 display.
Uses spritesheet from CircuitPython_Flying_Toasters pendant project Uses spritesheet from CircuitPython_Flying_Toasters pendant project
https://learn.adafruit.com/circuitpython-sprite-animation-pendant-mario-clouds-flying-toasters https://learn.adafruit.com/circuitpython-sprite-animation-pendant-mario-clouds-flying-toasters
...@@ -19,115 +19,124 @@ import toast_bitmaps ...@@ -19,115 +19,124 @@ import toast_bitmaps
TOASTER_FRAMES = [0, 1, 2, 3] TOASTER_FRAMES = [0, 1, 2, 3]
TOAST_FRAMES = [4] TOAST_FRAMES = [4]
class rect(): def collide(a_col, a_row, a_width, a_height, b_col, b_row, b_width, b_height):
'''rectangle class'''
def __init__(self, col, row, width, height):
'''create new rectangle'''
self.col = col
self.row = row
self.width = width
self.height = height
def __add__(self, other):
'''add two rectangles'''
return rect(
self.col + other.col,
self.row + other.row,
self.width + other.width,
self.height + other.height)
def collide(rect_a, rect_b):
'''return true if two rectangles overlap''' '''return true if two rectangles overlap'''
return (rect_a.col + rect_a.width >= rect_b.col return (a_col + a_width >= b_col and a_col <= b_col + b_width
and rect_a.col <= rect_b.col + rect_b.width and a_row + a_height >= b_row and a_row <= b_row + b_height)
and rect_a.row + rect_a.height >= rect_b.row
and rect_a.row <= rect_b.row + rect_b.height)
def collision(sprites): def collision(sprites):
''''return true if any sprites overlap''' ''''return true if any sprites overlap'''
return any(collide(a.location, b.location) for a, b in zip(sprites[::], sprites[1::])) return any(
collide(a.col, a.row, a.width, a.height, b.col, b.row, b.width, b.height)
for a, b in zip(sprites[::], sprites[1::]))
def random_start(tft, sprites, bitmaps): def random_start(tft, sprites, bitmaps, num):
'''return new location along the top or right of the screen that does not overlap any sprites''' '''
while True: Return a random location along the top or right of the screen, if that location would overlaps
if random.getrandbits(2) > 1: with another sprite return (0,0). This allows the other sprites to keep moving giving the next
random_start a better chance to avoid a collision.
'''
# 50/50 chance to try along the top/right half or along the right/top half of the screen
if random.getrandbits(1):
row = 1 row = 1
col = random.randint(bitmaps.WIDTH, tft.width()-bitmaps.WIDTH) col = random.randint(bitmaps.WIDTH//2, tft.width()-bitmaps.WIDTH)
else: else:
col = tft.width() - bitmaps.WIDTH col = tft.width() - bitmaps.WIDTH
row = random.randint(bitmaps.HEIGHT, tft.height()-bitmaps.HEIGHT) row = random.randint(1, tft.height() // 2)
if any(collide(
col, row, bitmaps.WIDTH, bitmaps.HEIGHT,
sprite.col, sprite.row, sprite.width, sprite.height)
for sprite in sprites if num != sprite.num):
col = 0
row = 0
new_location = rect(col, row, bitmaps.WIDTH, bitmaps.HEIGHT) return (col, row)
if not any(collide(new_location, sprite.location) for sprite in sprites):
return new_location
def main(): def main():
class toast(): class Toast():
''' '''
toast class to keep track of toaster and toast sprites Toast class to keep track of toaster and toast sprites
''' '''
def __init__(self, sprites, bitmaps, frames): def __init__(self, sprites, bitmaps, frames):
'''create new sprite in random location that does not overlap other sprites''' '''create new sprite in random location that does not overlap other sprites'''
self.id = len(sprites) self.num = len(sprites)
self.bitmaps = bitmaps self.bitmaps = bitmaps
self.frames = frames self.frames = frames
self.steps = len(frames) self.steps = len(frames)
self.location = random_start(tft, sprites, bitmaps) self.col, self.row = random_start(tft, sprites, bitmaps, self.num)
self.last = self.location self.width = bitmaps.WIDTH
self.height = bitmaps.HEIGHT
self.last_col = self.col
self.last_row = self.row
self.step = random.randint(0, self.steps) self.step = random.randint(0, self.steps)
self.direction = rect(-random.randint(2, 5), 2, 0, 0) self.dir_col = -random.randint(2, 5)
self.prev_direction = self.direction self.dir_row = 2
self.prev_dir_col = self.dir_col
self.prev_dir_row = self.dir_row
self.iceberg = 0 self.iceberg = 0
def clear(self): def clear(self):
'''clear above and behind sprite''' '''clear above and behind sprite'''
tft.fill_rect( tft.fill_rect(
self.location.col, self.location.row-1, self.location.width, self.direction.row+1, self.col, self.row-1, self.width, self.dir_row+1,
st7789.BLACK) st7789.BLACK)
tft.fill_rect( tft.fill_rect(
self.location.col+self.bitmaps.WIDTH+self.direction.col, self.location.row, self.col+self.width+self.dir_col, self.row,
-self.direction.col, self.location.height, st7789.BLACK) -self.dir_col, self.height, st7789.BLACK)
def erase(self): def erase(self):
'''erase last postion of sprite''' '''erase last postion of sprite'''
tft.fill_rect( tft.fill_rect(
self.last.col, self.last.row, self.last.width, self.last.height, st7789.BLACK) self.last_col, self.last_row, self.width, self.height, st7789.BLACK)
def move(self, sprites): def move(self, sprites):
'''step frame and move sprite''' '''step frame and move sprite'''
if self.steps: if self.steps:
self.step = (self.step + 1) % self.steps self.step = (self.step + 1) % self.steps
self.last = self.location self.last_col = self.col
new_location = self.location + self.direction self.last_row = self.row
new_col = self.col + self.dir_col
new_row = self.row + self.dir_row
# if new location collides with another sprite, change direction for 32 frames
if any(
collide(new_col, new_row, self.width, self.height, sprite.col, sprite.row, sprite.width, sprite.height)
for sprite in sprites if self.num != sprite.num):
# if new location collides with another sprite, change direction down for 16 frames self.iceberg = 32
if any(collide(new_location, sprite.location) for sprite in sprites if self.id != sprite.id): self.dir_col = -1
self.iceberg = 16 self.dir_row = 3
self.direction = rect(-1, 2, 0, 0) new_col = self.col + self.dir_col
new_location = self.location + self.direction new_row = self.row + self.dir_row
self.location = new_location self.col = new_col
self.row = new_row
# if new location touches edge of screen, erase then set new start location # if new location touches edge of screen, erase then set new start location
if new_location.col <= 0 or new_location.row > tft.height() - self.location.height: if self.col <= 0 or self.row > tft.height() - self.height:
self.erase() self.erase()
self.direction = rect(-random.randint(2, 5), 2, 0, 0) self.dir_col = -random.randint(2, 5)
self.location = random_start(tft, sprites, self.bitmaps) self.dir_row = 2
self.col, self.row = random_start(tft, sprites, self.bitmaps, self.num)
# Track post collision direction change # Track post collision direction change
if self.iceberg: if self.iceberg:
self.iceberg -= 1 self.iceberg -= 1
if self.iceberg == 1: if self.iceberg == 1:
self.direction = self.prev_direction self.dir_col = self.prev_dir_col
self.dir_row = self.prev_dir_row
def draw(self): def draw(self):
'''draw current frame of sprite at it's location''' '''if the location is not 0,0 draw current frame of sprite at it's location'''
tft.bitmap(self.bitmaps, self.location.col, self.location.row, self.frames[self.step]) if self.col and self.row:
tft.bitmap(self.bitmaps, self.col, self.row, self.frames[self.step])
# configure spi interface # configure spi interface
spi = SPI(1, baudrate=31250000, sck=Pin(18), mosi=Pin(19)) spi = SPI(1, baudrate=31250000, sck=Pin(18), mosi=Pin(19))
...@@ -150,14 +159,14 @@ def main(): ...@@ -150,14 +159,14 @@ def main():
# create toast spites and set animation frames # create toast spites and set animation frames
sprites = [] sprites = []
sprites.append(toast(sprites, toast_bitmaps, TOAST_FRAMES)) sprites.append(Toast(sprites, toast_bitmaps, TOAST_FRAMES))
sprites.append(toast(sprites, toast_bitmaps, TOASTER_FRAMES)) sprites.append(Toast(sprites, toast_bitmaps, TOASTER_FRAMES))
sprites.append(toast(sprites, toast_bitmaps, TOASTER_FRAMES)) sprites.append(Toast(sprites, toast_bitmaps, TOASTER_FRAMES))
# move and draw sprites # move and draw sprites
while True: while True:
for sprite in sprites: for sprite in sprites:
sprite.clear() sprite.clear()
sprite.move(sprites) sprite.move(sprites)
sprite.draw() sprite.draw()
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment