174 lines
5.1 KiB
Python
Executable File
174 lines
5.1 KiB
Python
Executable File
# The game looop
|
|
import time
|
|
import gc
|
|
from math import ceil
|
|
|
|
if not hasattr(time, 'sleep_ms') or not hasattr(time, 'ticks_ms'):
|
|
# Emulate https://docs.pycom.io/firmwareapi/micropython/utime.html
|
|
time.ticks_ms = lambda: int(time.time() * 1000)
|
|
time.sleep_ms = lambda x: time.sleep(x/1000.0)
|
|
|
|
|
|
class RenderLoop:
|
|
def __init__(self, display, config=None):
|
|
self.display = display
|
|
self.debug = False
|
|
self.fps = display.fps
|
|
self.t_next_frame = None
|
|
self.prev_frame = 0
|
|
self.frame = 1
|
|
self.t_init = time.ticks_ms()
|
|
self.scenes = []
|
|
self.scene_index = 0
|
|
self.scene_switch_effect = 0
|
|
self.scene_switch_countdown = self.fps * 40
|
|
self.display.clear()
|
|
if not config:
|
|
return
|
|
if 'debug' in config:
|
|
self.debug = config['debug']
|
|
if 'sceneTimeout' in config:
|
|
self.scene_switch_countdown = self.fps * config['sceneTimeout']
|
|
|
|
def add_scene(self, scene):
|
|
"""
|
|
Add new scene to the render loop.
|
|
Called by main.py.
|
|
"""
|
|
self.scenes.append(scene)
|
|
|
|
def next_frame(self, button_state=0):
|
|
"""
|
|
Display next frame, possibly after a delay to ensure we meet the FPS target
|
|
Called by main.py.
|
|
"""
|
|
|
|
scene = self.scenes[self.scene_index]
|
|
# Process input
|
|
if button_state:
|
|
# Let the scene handle input
|
|
handled_bit = scene.input(button_state)
|
|
button_state &= ~handled_bit
|
|
# Use long-pressed buttons to handle intensity changes
|
|
if button_state & 0x22:
|
|
clear = 0
|
|
for s in self.scenes:
|
|
if hasattr(s, 'set_intensity'):
|
|
i = s.set_intensity()
|
|
if button_state & 0x02:
|
|
i -= 2
|
|
clear = 0x02
|
|
elif button_state & 0x20:
|
|
i += 2
|
|
clear = 0x20
|
|
i = (i + 32) % 32
|
|
s.set_intensity(i)
|
|
button_state &= ~clear
|
|
if self.debug:
|
|
print('RenderLoop: updated intensity to {} on scenes, remaining state: {}'.format(i, button_state))
|
|
|
|
# Calculate how much we need to wait before rendering the next frame
|
|
t_now = time.ticks_ms() - self.t_init
|
|
if not self.t_next_frame:
|
|
self.t_next_frame = t_now
|
|
|
|
delay = self.t_next_frame - t_now
|
|
if delay >= 0:
|
|
# Wait until we can display next frame
|
|
time.sleep_ms(delay)
|
|
else:
|
|
# Resynchronize
|
|
num_dropped_frames = ceil(-delay*self.fps/1000)
|
|
if self.debug:
|
|
print('RenderLoop: FPS {} too high, should\'ve rendered frame {} at {}ms but was {}ms late and dropped {} frames'.format(self.fps, self.frame, self.t_next_frame, -delay, num_dropped_frames))
|
|
self.frame += num_dropped_frames
|
|
self.t_next_frame += ceil(1000*num_dropped_frames/self.fps)
|
|
if self.debug:
|
|
print('RenderLoop: Updated frame counters to frame {} with current next at {}'.format(self.frame, self.t_next_frame))
|
|
|
|
# Let the scene render its frame
|
|
t = time.ticks_ms()
|
|
loop_again = scene.render(self.frame, self.frame - self.prev_frame - 1, self.fps)
|
|
t = time.ticks_ms() - t
|
|
if t > 1000/self.fps and self.debug:
|
|
print('RenderLoop: WARN: Spent {}ms rendering'.format(t))
|
|
|
|
|
|
# Consider switching scenes and update frame counters
|
|
self.scene_switch_countdown -= 1
|
|
scene_increment = 1
|
|
if not loop_again:
|
|
self.scene_switch_countdown = 0
|
|
elif button_state:
|
|
self.scene_switch_countdown = 0
|
|
if button_state & 0x1:
|
|
scene_increment = -1
|
|
|
|
if not self.scene_switch_countdown:
|
|
self.reset_scene_switch_counter()
|
|
# Transition to next scene
|
|
self.next_scene(scene_increment, button_state)
|
|
# Account for time wasted above
|
|
t_new = time.ticks_ms() - self.t_init
|
|
t_diff = t_new - t_now
|
|
frames_wasted = ceil(t_diff*self.fps/1000.0)
|
|
if self.debug:
|
|
print('RenderLoop: setup: scene switch took {}ms, original t {}ms, new t {}ms, spent {} frames'.format(t_diff, t_now,t_new, self.fps*t_diff/1000.0))
|
|
self.frame += int(frames_wasted)
|
|
self.t_next_frame += int(1000.0 * frames_wasted / self.fps)
|
|
|
|
self.prev_frame = self.frame
|
|
self.frame += 1
|
|
self.t_next_frame += int(1000/self.fps)
|
|
|
|
def reset_scene_switch_counter(self):
|
|
"""
|
|
Reset counter used to automatically switch scenes.
|
|
The counter is decreased in .next_frame()
|
|
"""
|
|
self.scene_switch_countdown = 45 * self.fps
|
|
|
|
def next_scene(self, increment=1, button_state=0):
|
|
"""
|
|
Transition to a new scene and re-initialize the scene
|
|
"""
|
|
if len(self.scenes) < 2:
|
|
return
|
|
|
|
print('RenderLoop: next_scene: transitioning scene')
|
|
# Fade out current scene
|
|
t0 = time.ticks_ms()
|
|
if button_state & 0x01:
|
|
self.display.hscroll(-4)
|
|
button_state &= ~0x01
|
|
elif button_state & 0x10:
|
|
self.display.hscroll(4)
|
|
button_state &= ~0x10
|
|
else:
|
|
effect = self.scene_switch_effect
|
|
self.scene_switch_effect = (effect + 1) % 4
|
|
if effect == 0:
|
|
self.display.vscroll()
|
|
elif effect == 1:
|
|
self.display.hscroll()
|
|
elif effect == 2:
|
|
self.display.fade()
|
|
else:
|
|
self.display.dissolve()
|
|
|
|
t2 = time.ticks_ms()
|
|
t1 = t2 - t0
|
|
gc.collect()
|
|
|
|
t3 = time.ticks_ms()
|
|
t2 = t3 - t1
|
|
num_scenes = len(self.scenes)
|
|
i = self.scene_index = (num_scenes + self.scene_index + increment) % num_scenes
|
|
# (Re-)initialize scene
|
|
self.scenes[i].reset()
|
|
t4 = time.ticks_ms()
|
|
t3 = t4 - t3
|
|
if self.debug:
|
|
print('RenderLoop: next_scene: selected {}, effect {}ms, gc {}ms, scene reset {}ms, total {}ms'.format(self.scenes[i].__class__.__name__, t1, t2, t3, t4-t0))
|
|
return button_state
|