Beispiel #1
0
def test_signal():

    s = Signal()
    hello = s.connect(lambda: print("hello ", end=""), weak=False)
    s.connect(lambda: print("world"), weak=False)
    assert len(s) == 2
    s()  # 'hello world'

    assert s.disconnect(hello)
    s()  # 'world'
    assert len(s) == 1

    s.clear()
    assert len(s) == 0
Beispiel #2
0
def test_signal_weak():

    s = Signal()
    w = s.connect(lambda: print("test"))
    del w
    assert len(s) == 0
    s()
    assert len(s) == 0

    s = Signal()
    w = s.connect(lambda: print("test"))
    del s  # slot outlives signal?
    assert w.sig() is None  # it works
    del w
Beispiel #3
0
def test_scene():

    scene = Signal()
    ent = Entity(None, scene)

    slot = scene.connect(ent)
    ent.slot = weakref.ref(slot)
    assert len(scene) == 1

    ent.remove()
    assert len(scene) == 0
Beispiel #4
0
def test_scene_blocked():

    scene = Signal()
    ent = Entity(None, scene)

    scene.blocked += 1
    slot = scene.connect(ent)
    scene.blocked -= 1
    ent.slot = weakref.ref(slot)

    assert len(scene) == 0
    scene.refresh()
    assert len(scene) == 1

    scene.blocked += 1
    ent.remove()
    scene.blocked -= 1

    assert len(scene) == 1
    scene.refresh()
    assert len(scene) == 0
Beispiel #5
0
def test_signal_queue():

    # queued connection
    s = Signal()
    s.blocked += 1
    a = s.connect(lambda: print("queued"), weak=False)
    assert len(s.queued) == 1
    s()  # nothing
    s.blocked -= 1
    for slot in s.queued:
        slot()
    s.queued = []
    s()  # "queued"

    # queued disconnection
    s.blocked += 1
    a.disconnect()
    assert len(s) == 1  # still attached
    assert len(s.queued) == 1
    s.blocked -= 1
    for q in s.queued:
        q()
    s.queued = []
    assert len(s) == 0
Beispiel #6
0
class App:

    STATES = {
        "intro": Intro,
        "game": Game,
        "menu": Menu,
        "intermission": Intermission,
        "credits": Credits,
    }

    # MAX_KEYS = 512

    def __init__(self, initial_state):
        """
        The main beginning of our application.
        Initializes pygame and the initial state.
        """

        # pygame.mixer.pre_init(44100, 16, 2, 4096)
        pygame.init()

        self.size = ivec2(1920, 1080) / 2
        """Display size"""
        self.cache = {}
        """Resources with filenames as keys"""
        pygame.display.set_caption("Butterfly Destroyers")
        self.screen = pygame.display.set_mode(self.size)
        self.on_event = Signal()
        self.quit = False
        self.clock = pygame.time.Clock()
        self.inputs = Inputs()
        self.time = 0
        self.dirty = True
        self.data = {}  # data persisting between modes
        # self.keys = [False] * self.MAX_KEYS

        self._state = None
        self.last_state = None
        self.next_state = initial_state
        self.process_state_change()

    def load(self, filename, resource_func):
        """
        Attempt to load a resource from the cache, otherwise, loads it
        :param resource_func: a function that loads the resource if its
            not already available in the cache
        """
        if filename not in self.cache:
            r = self.cache[filename] = resource_func()
            return r
        return self.cache[filename]

    def load_img(self, filename, scale=1, flipped=False):
        """
        Load the image at the given path in a pygame surface.
        The file name is the name of the file without the full path.
        Files are looked for in the SPRITES_DIR
        Results are cached.
        Scale is an optional integer to scale the image by a given factor.
        """
        def load_fn():
            img = pygame.image.load(os.path.join(SPRITES_DIR, filename))
            if scale != 1:
                w, h = img.get_size()
                img = pygame.transform.scale(img, ivec2(vec2(w, h) * scale))
                if flipped:
                    img = pygame.transform.flip(img, True, False)
            return img

        return self.load((filename, scale, flipped), load_fn)

    # def pend(self):

    #     self.dirty = True

    def run(self):
        """
        Main game loop.

        Runs until the `quit` flag is set
        Runs update(dt) and render() of the current game state (default: Game)
        """

        last_t = time.time_ns()
        accum = 0
        self.fps = 0
        frames = 0
        dt = 0

        self.inputs.event([])

        while (not self.quit) and self.state:

            cur_t = time.time_ns()
            dt += (cur_t - last_t) / (1000 * 1000 * 1000)

            # if dt < 0.001:
            #     time.sleep(1 / 300)
            #     continue  # accumulate dt for skipped frames

            last_t = cur_t
            accum += dt
            frames += 1
            if accum > 1:
                self.fps = frames
                frames = 0
                accum -= 1

            events = pygame.event.get()
            for event in events:
                if event.type == pygame.QUIT:
                    return 0
                self.on_event(event)

            self.inputs.event(events)

            if self.state is None:
                break

            if DEBUG:
                print("FRAME, dt =", dt, "FPS,", self.fps)

            self.inputs.update(dt)
            if self.update(dt) is False:
                break

            if self.render() is False:
                break

            dt = 0  # reset to accumulate

    def add_event_listener(self, obj):
        slot = self.on_event.connect(obj.event)
        obj.slots.append(slot)
        return slot

    def update(self, dt):
        """
        Called every frame to update our game logic
        :param dt: time since last frame in seconds
        :return: returns False to quit gameloop
        """

        if not self.state:
            return False

        if self.next_state:
            self.process_state_change()

        self.state.update(dt)

    def render(self):
        """
        Called every frame to render our game state and update pygame display
        :return: returns False to quit gameloop
        """

        # if not self.dirty:
        #     return
        # self.dirty = False

        if self.state is None:
            return False

        self.state.render()

        pygame.display.update()

    @property
    def state(self):
        return self._state

    @state.setter
    def state(self, s):
        """
        Schedule state change on next frame
        """
        self.next_state = s

    def process_state_change(self):
        """
        Process pending state changes
        """
        lvl = None

        try:
            lvl = int(self.next_state)
            pass
        except ValueError:
            pass

        if lvl:
            stats = self.data["stats"] = self.data.get("stats", Stats())
            stats.level = lvl
            self.next_state = "game"

        if self.next_state:
            self._state = self.STATES[self.next_state.lower()](self)

        self.next_state = None
Beispiel #7
0
class Axis:
    def __init__(self, negative, positive, *axis, smooth=0.1):
        """
        An input axis taking values between -1 and 1.

        Callbacks are disconnected with -=
        :param negative: keycode or list of keycodes
        :param positive: keycode or list of keycodes
        :param axis: any number of JoyAxis
        :param smooth: Duration (s) to smooth values
        """

        if isinstance(negative, int):
            negative = [negative]
        if isinstance(positive, int):
            positive = [positive]

        self._negative = {KeyPress(n): False for n in negative}
        self._positive = {KeyPress(p): False for p in positive}
        self._axis = set(axis)
        self._callbacks = Signal()
        self._smooth = smooth

        self.non_zero_time = 0
        self.zero_time = 0

        # Hold the number of keys pressed
        self._int_value = 0
        # Hold the smoothed number of keys pressed
        self._value = 0
        # Hold the total value of axis,
        # separately because of different tracking methods
        self._axis_value = 0

    def __str__(self):
        return f"Axis({self.value})"

    @property
    def value(self):
        return clamp(self._value + self._axis_value, -1, 1)

    def always_call(self, callback):
        return self._callbacks.connect(callback)

    def __isub__(self, callback):
        return self._callbacks.disconnect(callback)

    def update(self, dt):
        """Trigger all callbacks and updates times"""
        if self._int_value != 0:
            # Nonzero check is okay as JoyAxis already count the threshold
            self.non_zero_time += dt
            self.zero_time = 0
        else:
            self.non_zero_time = 0
            self.zero_time += dt

        if self._smooth <= 0:
            self._value = self._int_value
        else:
            dv = dt / self._smooth
            if self._int_value > 0:
                self._value += dv
            elif self._int_value < 0:
                self._value -= dv
            else:
                if self._value > 0:
                    self._value -= dv
                else:
                    self._value += dv

                if abs(self._value) <= dv:
                    # To have hard zeros
                    self._value = 0
        self._value = clamp(self._value, -1, 1)

        self._callbacks(self)

    def event(self, events):
        axis_value = 0
        any_axis = False
        for event in events:
            for pos in self._positive:
                if pos.match(event):
                    self._positive[pos] = pos.pressed(event)
            for neg in self._negative:
                if neg.match(event):
                    self._negative[neg] = neg.pressed(event)

            for axis in self._axis:
                if axis.match(event):
                    # We take the most extreme value
                    val = axis.value(event)
                    if abs(val) > abs(axis_value):
                        axis_value = val
                    any_axis = True

        self._int_value = sum(self._positive.values()) - sum(self._negative.values())
        if any_axis:
            self._axis_value = axis_value
Beispiel #8
0
class Button:
    def __init__(self, *keys):
        """
        A boolean input.

        :param keys: any number of keycodes or ButtonInputs
        """

        self._keys: Set[ButtonInput] = {
            KeyPress(key) if isinstance(key, int) else key for key in keys
        }
        self._pressed = {}
        self.just_released = False
        self.just_pressed = False
        self.just_double_pressed = False

        self._always = Signal()
        self._on_press = Signal()
        self._on_release = Signal()
        self._on_double_press = Signal()
        self._repeat = Signal()  # _repeat[callback] = [delay, trigger_count]

        self.last_press = float("-inf")
        """Time since last release of the button"""
        self.press_time = 0
        self.dt = 0  # time since last frame
        """
        Time the button has been pressed.
        If it isn't pressed, it is the duration of the last press.
        """

    def update(self, dt):
        """Trigger all callbacks and updates times"""

        self.last_press += dt
        if self.pressed:
            self.press_time += dt

        self.dt = dt

        self._always(self)

        if self.just_pressed:
            self._on_press(self)

        if self.just_double_pressed:
            self._on_double_press(self)

        if self.just_released:
            self._on_release(self)

        if self.pressed:
            self._repeat.blocked += 1
            for wref in self._repeat.slots:
                c = wref()
                if not c:
                    continue
                if c.delay * c.repetitions <= self.press_time:
                    # It isn;t possible to set it directly, I don't know why
                    c.repetitions += 1
                    c(self)
            self._repeat.blocked -= 1
            self._repeat.refresh()

    def event(self, events):
        self.just_pressed = False
        self.just_double_pressed = False
        self.just_released = False

        old_pressed = self.pressed
        for event in events:
            for key in self._keys:
                if key.match(event):
                    self._pressed[key] = key.pressed(event)

        if not old_pressed:
            if self.pressed:
                self.press_time = 0
                self.just_pressed = True
            if self.double_pressed:
                self.just_double_pressed = True
        else:
            if not self.pressed:
                # All keys were just released
                self.last_press = 0
                self.just_released = True
                for wref in self._repeat.slots:
                    c = wref()
                    if not c:
                        continue
                    c.repetitions = 0

    @property
    def pressed(self):
        """Whether the button is actually pressed."""
        return sum(self._pressed.values(), 0) > 0

    @property
    def double_pressed(self):
        """Whether the button was just double pressed"""
        return self.pressed and self.last_press < 0.1

    def always_call(self, callback):
        return self._always.connect(callback)

    def on_press(self, callback):
        return self._on_press.connect(callback)

    def on_release(self, callback):
        return self._on_release.connect(callback)

    def on_double_press(self, callback):
        return self._on_double_press.connect(callback)

    def on_press_repeated(self, callback, delay):
        """
        Call `callback` when the button is pressed and
        every `delay` seconds while it is pressed.
        Note: the same function cannot be a repeat callback
        for two different things.
        """

        slot = self._repeat.connect(callback)
        slot.delay = delay
        slot.repetitions = 0
        return slot

    def disconnect(self, callback):
        """Remove a callback from all types if present."""
        if callback in self._always:
            self._always.disconnect(callback)
        if callback in self._on_press:
            self._on_press.disconnect(callback)
        if callback in self._on_release:
            self._on_release.disconnect(callback)
        if callback in self._on_double_press:
            self._on_double_press.disconnect(callback)
        if callback in self._repeat:
            self._on_double_press.disconnect(callback)
Beispiel #9
0
class Scene(Signal):
    def __init__(self, app, state, script=None, script_args=None):
        super().__init__()
        self.max_particles = 32
        self.app = app
        self.state = state
        self.when = When()
        self.slotlist = SlotList()
        self._sky_color = None
        self.ground = None
        self._ground_color = None
        self._script = None
        self.scripts = Signal(lambda fn: Script(self.app, self, fn))
        self.lightning_slot = None
        self.lightning_density = 0

        self.player = None

        self.rock_slot = None
        self.rain_slot = None
        self.has_clouds = False
        self.lowest_fps = 1000

        self.on_render = Signal()

        # self.script_paused = False
        # self.script_slots = []
        # self.stars_visible = 0

        # star_density = 80
        # self.star_pos = [
        #     (randint(0, 200), randint(0, 200)) for i in range(star_density)
        # ]

        # color change delays when using opt funcs
        self.delay_t = 0
        self.delay = 0.5
        self.time = 0

        self.sky_color = None
        # self.ground_color = GREEN
        self.dt = 0
        self.sounds = {}

        # self.script_fn = script
        # self.event_slot = self.app.on_event.connect(self.even)

        # self.script_resume_condition = None

        # The below wrapper is just to keep the interface the same with signal
        # on_collision.connect -> on_collision_connect
        # class CollisionSignal:
        #     pass

        # self.on_collision = CollisionSignal()
        # self.on_collision.connect = self.on_collision_connect
        # self.on_collision.once = self.on_collision_once
        # self.on_collision.enter = self.on_collision_enter
        # self.on_collision.leave = self.on_collision_leave

        self._music = None

        if script:
            self.script = script  # trigger setter

        self.when.every(1, self.stabilize, weak=False)

    def iter_entities(self, *types):
        for slot in self.slots:
            ent = slot.get()
            if ent and isinstance(ent, types):
                yield ent

    def cloudy(self):
        if self.has_clouds:
            return

        pv = self.player.velocity if self.player else vec3(0)
        if hasattr(self.app.state, "player"):
            velz = pv.z
        else:
            velz = 0
        for i in range(30):
            x = randint(-3000, 3000)
            y = randint(0, 300)
            z = randint(-4000, -1300)
            pos = vec3(x, y, z)
            self.add(Cloud(self.app, self, pos, velz))

        self.has_clouds = True

    def lightning_strike(self):
        oldsky = vec4(self.sky_color) if self.sky_color else None
        self.sky_color = "white"
        self.when.once(0.1,
                       lambda oldsky=oldsky: self.set_sky_color(oldsky),
                       weak=False)
        self.play_sound("lightning.wav")

    def lightning_script(self, script):
        yield
        while True:
            if self.lowest_fps <= 25:
                break
            yield script.sleep(1)
            yield script.sleep((1 / self.lightning_density) * random.random())
            self.lightning_strike()

    def lightning(self, density=0.5):
        if density < EPSILON:
            self.lightning_slot = None
            return

        if self.lowest_fps <= 25:
            return

        self.lightning_density = density
        self.lightning_slot = self.scripts.connect(self.lightning_script)

    def add_rock(self):

        if self.lowest_fps <= 25:
            return

        velz = self.player.velocity.z if self.player else vec3(0)
        x = randint(-500, 500)
        y = GROUND_HEIGHT - 15
        z = -4000
        ppos = self.player.position if self.player else vec3(0)
        pos = vec3(ppos.x, 0, ppos.z) + vec3(x, y, z)
        self.add(Rock(self.app, self, pos, velz))

    def add_rain_drop(self):
        velz = self.player.velocity.z if self.player else 0
        x = randint(-400, 400)
        y = randint(0, 300)
        z = randint(-3000, -2000)
        ppos = self.player.position if self.player else vec3(0)
        pos = vec3(ppos.x, 0, ppos.z) + vec3(x, y, z)
        self.add(Rain(self.app, self, pos, velz, particle=True))

    def rain(self, density=25):
        if density:
            self.rain_slot = self.when.every(1 / density, self.add_rain_drop)
        else:
            self.rain_slot = None

    def rocks(self, density=10):
        if density:
            self.rock_slot = self.when.every(1 / density, self.add_rock)
        else:
            self.rock_slot = None

    def stars(self):
        if hasattr(self.app.state, "player"):
            velz = self.player.velocity.z
        else:
            velz = 0

        for i in range(50):
            x = randint(-500, 500)
            y = -200 + (random.random()**0.5 * 800)
            z = -3000
            pos = vec3(x, y, z)
            self.add(Star(self.app, self, pos, velz))

    def draw_sky(self):
        width, height = self.app.size / 8

        self.sky = pygame.Surface((width, height))
        sky_color = self.sky_color or ncolor(pygame.Color("blue"))
        sky_color = [255 * s for s in sky_color]

        # for y in range(height):
        #     interp = (1 - y / height) * 2
        #     for x in range(width):
        #         rand = rand_RGB()
        #         color = rgb_mix(sky_color, rand, 0.02)
        #           c = (sk * 0.98 + rand * 0.02) / i**1.1

        #         if interp == 0:
        #             color = (255, 255, 255)
        #         else:
        #             color = [min(int(c / interp ** 1.1), 255) for c in color]
        #         self.sky.set_at((x, y), color)

        # Draw gradient
        for y in range(height):
            interp = (1 - y / height) * 2
            base = [min(int(c / interp**1.1), 255) for c in sky_color]
            pygame.draw.line(self.sky, base, (0, y), (width, y))

        noise = noise_surf_dense_bottom(self.sky.get_size(),
                                        random.randrange(5))
        self.sky.blit(
            noise,
            (0, 0),
            None,
        )
        self.sky = pygame.transform.scale(self.sky, self.app.size)

        # if self.stars_visible:
        #     self.draw_stars(self.sky, self.star_pos)

        self.sky = pygame.transform.scale(self.sky, self.app.size)

    # def draw_stars(self, surface, star_positions):
    #     size = 1
    #     for pos in star_positions:
    #         star = pygame.Surface((size, size))
    #         star.fill((255, 255, 255))
    #         star.set_alpha(175)
    #         surface.blit(star, pos)

    def remove_sound(self, filename):
        if filename in self.sounds:
            self.sounds[filename][0].stop()
            del self.sounds[filename]
            return True
        return False

    def ensure_sound(self, filename, callback=None, *args):
        """
        Ensure a sound is playing.  If it isn't, play it.
        """
        if filename in self.sounds:
            return None, None, None
        return self.play_sound(filename, callback, *args)

    def play_sound(self, filename, callback=None, *args):
        """
        Plays the sound with the given filename (relative to SOUNDS_DIR).
        Returns sound, channel, and callback slot.
        """

        if filename in self.sounds:
            self.sounds[filename][0].stop()
            del self.sounds[filename]

        filename = path.join(SOUNDS_DIR, filename)
        sound = self.app.load(filename, lambda: pygame.mixer.Sound(filename))
        if not sound:
            return None, None, None
        channel = pygame.mixer.find_channel()
        if not channel:
            return None, None, None
        channel.set_volume(SOUND_VOLUME)
        if callback:
            self.when.once(self.sounds[0].get_length(), callback, weak=False)
        else:
            slot = None
        self.sounds[filename] = (sound, channel, slot)
        channel.play(sound, *args)
        self.when.once(sound.get_length(),
                       lambda: self.remove_sound(sound),
                       weak=False)
        return sound, channel, slot

    @property
    def script(self):
        return self._script

    @script.setter
    def script(self, fn):
        self._script = (Script(
            self.app, self, fn, use_input=True, script_args=(self.app, self))
                        if fn else None)

    @property
    def music(self):
        return self._music

    @property
    def ground_color(self):
        return self._ground_color

    @ground_color.setter
    def ground_color(self, color):
        if color is None:
            return

        self._ground_color = ncolor(color)
        if not self.ground:
            self.ground = self.add(Ground(self.app, self, GROUND_HEIGHT))

        self.ground.color = color
        if "ROCK" in self.app.cache:
            del self.app.cache["ROCK"]  # rocks need to reload their color

    @music.setter
    def music(self, filename):
        self._music = filename
        if self._music:
            pygame.mixer.music.load(path.join(MUSIC_DIR, filename))
            pygame.mixer.music.play(-1)

    def on_collision_connect(self, A, B, func, once=True):
        """
        during collision (touching)
        """
        pass

    def on_collision_once(self, A, B, func, once=True):
        """
        trigger only once
        """
        pass

    def on_collision_enter(self, A, B, func, once=True):
        """
        trigger upon enter collision
        """

        pass

    def on_collision_leave(self, A, B, func, once=True):
        """
        trigger upon leave collision
        """
        pass

    # @property
    # def script(self):
    #     return self.script_fn

    # @script.setter
    # def script(self, script):
    #     print("Script:",script)
    #     if isinstance(script, str):
    #         local = {}
    #         exec(open(path.join(SCRIPTS_DIR, script + ".py")).read(), globals(), local)
    #         self._script = local["script"](self.app, self)
    #     elif isinstance(script, type):
    #         # So we can pass a Level class
    #         self._script = iter(script(self.app, self))
    #     elif script is None:
    #         self._script = None
    #     else:
    #         raise TypeError

    # def sleep(self, t):
    #     return self.when.once(t, self.resume)

    def add(self, entity):
        slot = self.connect(entity, weak=False)
        entity.slot = weakref.ref(slot)
        # self.slotlist += slot
        return entity

    @property
    def sky_color(self):
        return self._sky_color

    @sky_color.setter
    def sky_color(self, c):
        self._sky_color = ncolor(c) if c else None
        self.draw_sky()
        # reset ground gradient (depend on sky color)
        self.ground_color = self._ground_color

    # for scripts to call when.fade(1, set_sky_color)
    def set_sky_color(self, c):
        self.sky_color = ncolor(c) if c else None

    def set_sky_color_opt(self, c):
        """
        Optimized for fades.
        """
        if self.delay_t > EPSILON:
            return False
        # print("delay")

        self.delay_t = self.delay

        self._sky_color = ncolor(c) if c else None
        if self._sky_color:
            self.draw_sky()
            # reset ground gradient (depend on sky color)
            self.set_ground_color_opt(self.ground_color)
        return True

    def set_ground_color(self, c):
        self.ground_color = ncolor(c) if c else None

    def set_ground_color_opt(self, c):
        """
        Optimized for fades.
        """
        if not self.ground:
            self.ground = self.add(Ground(self.app, self, GROUND_HEIGHT))
        if c:
            self.ground.fade_opt(ncolor(c))
        else:
            self.ground = None

    def remove(self, entity):
        # self.slotlist -= entity
        super().disconnect(entity)

    # def resume(self):
    #     self.script_paused = False

    def invalid_size(self, size):
        """Checks component for 0 or NaNs"""
        return any(c != c or abs(c) < EPSILON for c in size)

    def update_collisions(self, dt):

        # cause all scene operations to be queueed

        self.blocked += 1

        for slot in self.slots:
            a = slot.get()
            # only check if a is solid
            if not a or not a.solid:
                continue

            if self.invalid_size(a.collision_size):
                continue

            # for each slot, loop through each slot
            for slot2 in self.slots:
                b = slot2.get()
                # only check if b is solid
                if not b or not b.solid:
                    continue
                if slot is not slot2:
                    if not a.has_collision and not b.has_collision:
                        continue

                    if self.invalid_size(b.collision_size):
                        continue

                    a_min = a.position - a.collision_size / 2
                    a_max = a.position + a.collision_size / 2
                    b_min = b.position - b.collision_size / 2
                    b_max = b.position + b.collision_size / 2
                    col = not (b_min.x > a_max.x or b_max.x < a_min.x
                               or b_min.y > a_max.y or b_max.y < a_min.y
                               or b_min.z > a_max.z or b_max.z < a_min.z)
                    if col:
                        if a.has_collision:
                            a.collision(b, dt)
                        if b.has_collision:
                            b.collision(a, dt)
        self.blocked -= 1

        # run pending slot queue
        if self.blocked == 0:
            self.refresh()

    def stabilize(self):
        """"
        Stablize FPS by setting max partilces
        Called every second (see when.once(1, self.stabilize in init)
        """

        if self.app.fps > 1:
            self.lowest_fps = min(self.app.fps, self.lowest_fps)

        if self.app.fps < 45:
            self.max_particles = max(self.max_particles / 2, 4)

        if self.lowest_fps >= 60:
            if self.app.fps > 120:
                self.max_particles = min(self.max_particles * 2, 64)

    def filter_script(self, slot):
        if isinstance(slot, weakref.ref):
            wref = slot
            slot = wref()
            if not slot:
                return False
        if slot.get().done():
            return False
        return True

    def update(self, dt):

        self.time += dt
        # print(self.time)
        self.delay_t = max(0, self.delay_t - dt)

        # do time-based events
        self.when.update(dt)

        self.update_collisions(dt)
        self.refresh()

        # main level script
        if self._script:
            self._script.update(dt)

        # extra scripts
        if self.scripts:
            self.scripts.each(lambda x, dt: x.update(dt), dt)
            self.scripts.slots = list(
                filter(self.filter_script, self.scripts.slots))

        # self.sort(lambda a, b: a.z < b.z)
        # self.slots = sorted(self.slots, key=z_compare)
        self.slots.sort(key=z_compare)
        self.slots = list(filter(lambda x: not x.get().removed, self.slots))

        # call update(dt) on each entity
        self.each(lambda x, dt: x.update(dt), dt)

        # update particles (remove if too many)
        self.blocked += 1
        particle_count = 0
        for i, slot in enumerate(reversed(self.slots)):
            e = slot.get()
            if e.particle:
                particle_count += 1
                if particle_count >= self.max_particles:
                    slot.disconnect()
        self.blocked -= 1
        self.clean()

    def render(self, camera):
        # call render(camera) on all scene entities

        if self.sky_color is not None:
            self.app.screen.blit(self.sky, (0, 0))

        # call render on each entity
        self.each(lambda x: x.render(camera))

        self.on_render(camera)