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)
def random_start(tft, sprites, bitmaps): for a, b in zip(sprites[::], sprites[1::]))
'''return new location along the top or right of the screen that does not overlap any sprites'''
while True: def random_start(tft, sprites, bitmaps, num):
if random.getrandbits(2) > 1: '''
row = 1 Return a random location along the top or right of the screen, if that location would overlaps
col = random.randint(bitmaps.WIDTH, tft.width()-bitmaps.WIDTH) with another sprite return (0,0). This allows the other sprites to keep moving giving the next
else: random_start a better chance to avoid a collision.
col = tft.width() - bitmaps.WIDTH
row = random.randint(bitmaps.HEIGHT, tft.height()-bitmaps.HEIGHT) '''
# 50/50 chance to try along the top/right half or along the right/top half of the screen
new_location = rect(col, row, bitmaps.WIDTH, bitmaps.HEIGHT) if random.getrandbits(1):
if not any(collide(new_location, sprite.location) for sprite in sprites): row = 1
return new_location col = random.randint(bitmaps.WIDTH//2, tft.width()-bitmaps.WIDTH)
else:
col = tft.width() - bitmaps.WIDTH
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
return (col, row)
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 down for 16 frames # if new location collides with another sprite, change direction for 32 frames
if any(collide(new_location, sprite.location) for sprite in sprites if self.id != sprite.id): if any(
self.iceberg = 16 collide(new_col, new_row, self.width, self.height, sprite.col, sprite.row, sprite.width, sprite.height)
self.direction = rect(-1, 2, 0, 0) for sprite in sprites if self.num != sprite.num):
new_location = self.location + self.direction
self.location = new_location self.iceberg = 32
self.dir_col = -1
self.dir_row = 3
new_col = self.col + self.dir_col
new_row = self.row + self.dir_row
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)
def random_start(tft, sprites, bitmaps): for a, b in zip(sprites[::], sprites[1::]))
'''return new location along the top or right of the screen that does not overlap any sprites'''
while True: def random_start(tft, sprites, bitmaps, num):
if random.getrandbits(2) > 1: '''
row = 1 Return a random location along the top or right of the screen, if that location would overlaps
col = random.randint(bitmaps.WIDTH, tft.width()-bitmaps.WIDTH) with another sprite return (0,0). This allows the other sprites to keep moving giving the next
else: random_start a better chance to avoid a collision.
col = tft.width() - bitmaps.WIDTH
row = random.randint(bitmaps.HEIGHT, tft.height()-bitmaps.HEIGHT) '''
# 50/50 chance to try along the top/right half or along the right/top half of the screen
new_location = rect(col, row, bitmaps.WIDTH, bitmaps.HEIGHT) if random.getrandbits(1):
if not any(collide(new_location, sprite.location) for sprite in sprites): row = 1
return new_location col = random.randint(bitmaps.WIDTH//2, tft.width()-bitmaps.WIDTH)
else:
col = tft.width() - bitmaps.WIDTH
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
return (col, row)
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 down for 16 frames # if new location collides with another sprite, change direction for 32 frames
if any(collide(new_location, sprite.location) for sprite in sprites if self.id != sprite.id): if any(
self.iceberg = 16 collide(new_col, new_row, self.width, self.height, sprite.col, sprite.row, sprite.width, sprite.height)
self.direction = rect(-1, 2, 0, 0) for sprite in sprites if self.num != sprite.num):
new_location = self.location + self.direction
self.location = new_location self.iceberg = 32
self.dir_col = -1
self.dir_row = 3
new_col = self.col + self.dir_col
new_row = self.row + self.dir_row
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