Skip to content

Minigame with a render loop

Black Ram edited this page Jun 26, 2023 · 26 revisions

IMPORTANT: Currently the only way to take advantage of pygame.draw functions without having major flashing is: pygame.RenpyGameByTimerForDraw

INFO: it is recommended to use webp images (more info)


To create a minigame with a rendering loop, you need to create a class that inherits from pygame.RenpyGameByTimer or pygame.RenpyGameByLoop and implement the methods (lambda) first_step, update_process and etc.

pygame.RenpyGameByTimer and pygame.RenpyGameByLoop are similar, but pygame.RenpyGameByTimer use a timer for update a game, and pygame.RenpyGameByLoop use a renpy.redraw (renpy.redraw is affected, in part, by the time the functions take (so it can be considered as if it were a loop)) for update a game.

pygame.RenpyGameByTimer

pygame.RenpyGameByTimer update the render based on a event timer. And has an extra parameter called is_full_redraw_lamda

minigame = pygame.RenpyGameByTimer(
    first_step=my_game_first_step,
    update_process=my_game_logic,
    event_lambda=game_event,
    delay=0.04,
    # is_full_redraw_lamda=is_full_redraw_lamda
)

is_full_redraw_lamda

Also if is_full_redraw_lamda return False performs a update of render, and if it returns True performs a full redraw. (the update of render is more performant)

by default is_full_redraw_lamda is:

def _is_full_redraw(self, current_frame_number: int) -> bool:
    if self.is_full_redraw_lamda:
        return self.is_full_redraw_lamda(current_frame_number)
    return current_frame_number % 2 == 0

if you want to change it, you can pass a function lambda to is_full_redraw_lamda parameter.

pygame.RenpyGameByLoop

pygame.RenpyGameByLoop update the render based on a renpy.redraw (renpy.redraw is affected, in part, by the time the functions take (so it can be considered as if it were a loop)).

(if you pass is_full_redraw_lamda to pygame.RenpyGameByLoop, it will be ignored)

minigame = pygame.RenpyGameByLoop(
    first_step=my_game_first_step,
    update_process=my_game_logic,
    event_lambda=game_event,
    delay=0.04,
)

pygame.RenpyGameByTimer vs pygame.RenpyGameByLoop

pygame.RenpyGameByTimer Pro:

  • more performant
  • respect the delay
  • you can use the draw function of pygame

pygame.RenpyGameByTimer Cons:

  • could lose a frame
  • if use a small delay -> in the firsts renders, the game 'flickers'

pygame.RenpyGameByLoop Pro:

  • don't lose a frame

pygame.RenpyGameByLoop Cons:

  • less performant
  • could don't respect the delay
  • you can't use the draw function of pygame

RenpyGameByTimer & RenpyGameByLoop

first_step() and update_process()

first_step() and update_process() is a function lambda that return a child_render, it is a current frame of the game.

first_step() is a function lambda that return a child_render, it is a first frame of the game.

If you show a game and not start a render loop, you can use a first_step for show a menu. (more info in start a game between a sterted menu)

example:

def my_game_first_step(width: int, height: int, st: float, at: float) -> pygame.Surface:
    bestdepth = pygame.display.mode_ok((0, 0), winstyle, 32)
    screen = pygame.display.set_mode((0, 0), winstyle, bestdepth)

    # set up the vales
    # ...

    # decorate the game window
    pygame.display.set_icon(pygame.image.load("icon.webp").pygame_image)
    pygame.display.set_caption("My Game")


    # create the background
    bgdtile = pygame.image.load("background.webp")
    bgdtile = bgdtile.convert(st, at)
    sh.background = pygame.Surface(SCREENRECT.size)
    for x in range(0, SCREENRECT.width, bgdtile.get_width()):
        sh.background.blit(bgdtile, (x, 0))
    screen.blit(sh.background, (0, 0))
    pygame.display.flip()

    # play music
    # ...

    return screen

update_process() is a function that edit a child_render.

Return a delay or None, if is None, the game will end. Not return a child_render for set a child_render, because it is a Object, so it is not a copy, but a reference.

For pass a game end, return None.

example:

def my_game_logic(
    cur_screen: pygame.Surface,
    st: float,
    at: float,
    next_frame_time: Optional[float],
    current_frame_number: int,
) -> Optional[float]:
    if sh.player.alive():
        # clear/erase the last drawn sprites
        sh.all.clear(cur_screen, sh.background)

        # update all the sprites
        sh.all.update()
        
        # ...

        return next_frame_time
    else:
        # game end
        return None

Event handling

For handle a event, you need to create a function lambda pass it to event_lambda.

example:

def my_game_logic(
    cur_screen: pygame.Surface,
    st: float,
    at: float,
    next_frame_time: float,
    current_frame_number: int,
) -> Optional[float]:
    # ...

    # handle player input
    direction = sh.player_move
    sh.player.move(direction)

    # ...

def game_event(ev: EventType, x: int, y: int, st: float):
    if ev.type == pygame.KEYDOWN and ev.key == pygame.K_LEFT:
        sh.player_move = -1
    elif ev.type == pygame.KEYUP and ev.key == pygame.K_LEFT:
        if sh.player_move == -1:
            sh.player_move = 0
    elif ev.type == pygame.KEYDOWN and ev.key == pygame.K_RIGHT:
        sh.player_move = 1
    elif ev.type == pygame.KEYUP and ev.key == pygame.K_RIGHT:
        if sh.player_move == 1:
            sh.player_move = 0
    return

Delay

Delay is a time between frames, if is None, the game will end.

Start a game

To start a game, you need to call show(show_and_start = True) method of pygame.RenpyGameByTimer object.

Or you can use a show(show_and_start = False) and call start() method of pygame.RenpyGameByTimer object, but you must call start() in the event().

def show(self, show_and_start: bool = True): Show a game, if show_and_start is True, start a render loop. If show_and_start is False, you must call start() method. Show a game and not start a render loop, is useful for show a game in a menu, and start a render loop into a event().

def start(self): Start a render loop. Attention, this method work only if a game is not started. If you want to show a game, use show() method.

Shared data between a lambda function

You can use a global variable, but it is not a good idea, because it is a global variable, so you can use a class for shared data.

example:

class SharedData:
    def __init__(self):
        self.all = None
        self.player = None
        self.score = 0

sh = SharedData()

def main():
    # # Initialize a shared data
    global sh

    if not sh:
        sh = SharedData()

    # Initialize a game ...
    # Start a game ...

    # clean up the shared data
    score = sh.score
    sh = None

    return score

Start a game between a sterted menu

If you want to start a game between a sterted menu, you need to use a first_step() for show a menu and show(show_and_start = False).

And in the event() you need to call start() method of pygame.RenpyGameByTimer object.

example:

def main():
    minigame = pygame.RenpyGameByTimer(
        first_step=my_game_first_step,
        update_process=my_game_logic,
        event_lambda=game_event,
        delay=0.04,
    )

    minigame.show(show_and_start = False)

def my_game_first_step(width: int, height: int, st: float, at: float) -> pygame.Surface:
    # ...

    # show a menu
    # ...

    return screen

def game_event(ev: EventType, x: int, y: int, st: float):
    # ...

    if click_on_start_game_button:
        minigame.start()
    return

Game end

In renpy CDD, for end a game, you need to return a value from event().

In renpygame, for end a game, you need set a delay to None, or for force a game end, you can use a quit(). Also return a value from event(), but is not recommended.

For a game end menu you can use a end_game_frame() and use a quit() for force a game end. Attention, if you use a quit(), the game not call a end_game_frame().

Restart a game

(in development)

Pause a game

(in development)