[SOLVED] Porting Pygame to a CDD (Collision Detection)

Discuss how to use the Ren'Py engine to create visual novels and story-based games. New releases are announced in this section.
Forum rules
This is the right place for Ren'Py help. Please ask one question per thread, use a descriptive subject like 'NotFound error in option.rpy' , and include all the relevant information - especially any relevant code and traceback messages. Use the code tag to format scripts.
Post Reply
Message
Author
User avatar
GemstoneSystem
Newbie
Posts: 11
Joined: Sun Jun 19, 2022 5:04 pm
Projects: Shards
itch: gemmysystem
Contact:

[SOLVED] Porting Pygame to a CDD (Collision Detection)

#1 Post by GemstoneSystem » Thu Jul 28, 2022 12:39 pm

Hi! So I'm trying to port a pygame I made in one of my courses over to Ren'Py. In my game, you move a dragon up and down with either the up and down arrow keys or W and S (though for Ren'Py, focus_up and focus_down would probably be better). Coins come flying at you one at a time from the right of the screen and you have to collect them. As you do, your score goes up and the coins come faster. When you miss, you lose a life. When you've lost all five lives, you lose.

I'm not fully understanding what I need to do to turn my game into a Creator-Defined Displayable. I'm especially confused about what to do with collision detection in Ren'Py. When it comes to collision detection in pygame, every image gets a rect defined around it and you just use "player_rect.colliderect(coin_rect)" but I imagine I'm going to have to do something differently for Ren'Py. Any ideas on this and porting the game in general?

Here's my pygame code:

Code: Select all

import random
import pygame

# Initialize pygame
pygame.init()

# Set display surface
WINDOW_WIDTH = 1000
WINDOW_HEIGHT = 400
display_surface = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
pygame.display.set_caption("Feed the Dragon")

# Set FPS and clock
FPS = 60
clock = pygame.time.Clock()

# Set game values
PLAYER_STARTING_LIVES = 5
PLAYER_VELOCITY = 10
COIN_STARTING_VELOCITY = 10
COIN_ACCELERATION = 0.5
BUFFER_DISTANCE = 100

score = 0
player_lives = PLAYER_STARTING_LIVES
coin_velocity = COIN_STARTING_VELOCITY

# Set colors
GREEN = (0, 255, 0)
DARKGREEN = (10, 50, 10)
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)

# Set fonts
font = pygame.font.Font("AttackGraffiti.ttf", 32)

# Set text
score_text = font.render("Score: " + str(score), True, GREEN, DARKGREEN)
score_rect = score_text.get_rect()
score_rect.topleft = (10, 10)

title_text = font.render("Feed the Dragon", True, GREEN, DARKGREEN)
title_rect = title_text.get_rect()
title_rect.centerx = WINDOW_WIDTH//2
title_rect.y = 10

lives_text = font.render("Lives: " + str(player_lives), True, GREEN, DARKGREEN)
lives_rect = lives_text.get_rect()
lives_rect.topright = (WINDOW_WIDTH - 10, 10)

game_over_text = font.render("GAME OVER", True, GREEN, DARKGREEN)
game_over_rect = game_over_text.get_rect()
game_over_rect.center = (WINDOW_WIDTH//2, WINDOW_HEIGHT//2)

continue_text = font.render("Press Enter to play again", True, GREEN, DARKGREEN)
continue_rect = continue_text.get_rect()
continue_rect.center = (WINDOW_WIDTH//2, WINDOW_HEIGHT//2 + 32)

# Set sounds and music
coin_sound = pygame.mixer.Sound("coin_sound.wav")
miss_sound = pygame.mixer.Sound("miss_sound.wav")
miss_sound.set_volume(0.1)
pygame.mixer.music.load("ftd_background_music.wav")

# Set images
player_image = pygame.image.load("dragon_right.png")
player_rect = player_image.get_rect()
player_rect.left = 32
player_rect.centery = WINDOW_HEIGHT//2

coin_image = pygame.image.load("coin.png")
coin_rect = coin_image.get_rect()
coin_rect.x = WINDOW_WIDTH + BUFFER_DISTANCE
coin_rect.y = random.randint(64, WINDOW_HEIGHT - 32)

# The main game loop
pygame.mixer.music.play(-1, 0.0)
running = True
while running:
    # Check to see if user wants to quit
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # Check to see if the user wants to move
    keys = pygame.key.get_pressed()
    if (keys[pygame.K_UP] or keys[pygame.K_w]) and player_rect.top > 64:
        player_rect.y -= PLAYER_VELOCITY
    if (keys[pygame.K_DOWN] or keys[pygame.K_s]) and player_rect.bottom < WINDOW_HEIGHT:
        player_rect.y += PLAYER_VELOCITY

    # Move the coin
    if coin_rect.x < 0:
        # Player missed the coin
        player_lives -= 1
        miss_sound.play()
        coin_rect.x = WINDOW_WIDTH + BUFFER_DISTANCE
        coin_rect.y = random.randint(64, WINDOW_HEIGHT - 32)
    else:
        # Move the coin
        coin_rect.x -= coin_velocity

    # Check for collisions
    if player_rect.colliderect(coin_rect):
        score += 1
        coin_sound.play()
        coin_velocity += COIN_ACCELERATION
        coin_rect.x = WINDOW_WIDTH + BUFFER_DISTANCE
        coin_rect.y = random.randint(64, WINDOW_HEIGHT - 32)

    # Update HUD
    score_text = font.render("Score: " + str(score), True, GREEN, DARKGREEN)
    lives_text = font.render("Lives: " + str(player_lives), True, GREEN, DARKGREEN)

    # Check for game over
    if player_lives == 0:
        display_surface.blit(game_over_text, game_over_rect)
        display_surface.blit(continue_text, continue_rect)
        pygame.display.update()

        # Pause the game until player presses a key, then reset the game
        pygame.mixer.music.stop()
        is_paused = True
        while is_paused:
            for event in pygame.event.get():
                # The player wants to play again
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_RETURN:
                        score = 0
                        player_lives = PLAYER_STARTING_LIVES
                        player_rect.y = WINDOW_HEIGHT//2
                        coin_velocity = COIN_STARTING_VELOCITY
                        pygame.mixer.music.play(-1, 0.0)
                        is_paused = False
                # The player wants to quit
                if event.type == pygame.QUIT:
                    is_paused = False
                    running = False

    # Fill the display
    display_surface.fill(BLACK)

    # Blit the HUD to screen
    display_surface.blit(score_text, score_rect)
    display_surface.blit(title_text, title_rect)
    display_surface.blit(lives_text, lives_rect)
    pygame.draw.line(display_surface, WHITE, (0, 64), (WINDOW_WIDTH, 64), 2)

    # Blit assets to screen
    display_surface.blit(player_image, player_rect)
    display_surface.blit(coin_image, coin_rect)

    # Update display and tick the clock
    pygame.display.update()
    clock.tick(FPS)

# End the game
pygame.quit()
Last edited by GemstoneSystem on Fri Jul 29, 2022 10:40 pm, edited 1 time in total.

User avatar
GemstoneSystem
Newbie
Posts: 11
Joined: Sun Jun 19, 2022 5:04 pm
Projects: Shards
itch: gemmysystem
Contact:

Re: Porting Pygame to a CDD (Collision Detection)

#2 Post by GemstoneSystem » Fri Jul 29, 2022 1:11 pm

Okay, so I've figured out my game for the most part, including the collision detection. My last major issue is that when the keyboard keys are held down to move the player, there's a moment before the dragon actually moves. Anyone know how to fix this?

My Ren'Py Feed the Dragon code:

Code: Select all

init python:

    import random

    class FeedtheDragonDisplayable(renpy.Displayable):

        def __init__(self):

            renpy.Displayable.__init__(self)

            # Set game values
            self.PLAYER_WIDTH = 250
            self.PLAYER_HEIGHT = 200
            self.COIN_WIDTH = 128
            self.COIN_HEIGHT = 128
            self.PLAYER_STARTING_LIVES = 5
            self.PLAYER_VELOCITY = 50
            self.COIN_STARTING_VELOCITY = 10
            self.COIN_ACCELERATION = 1
            self.BUFFER_DISTANCE = 100

            self.score = 0
            self.player_lives = self.PLAYER_STARTING_LIVES
            self.coin_velocity = self.COIN_STARTING_VELOCITY

            # Some displayables we use.
            self.player = Image("images/minigames/dragon_right.png")
            self.coin = Image("images/minigames/coin.png")

            # The positions of the two displayables.
            self.px = 20
            self.py = 500
            self.pymin = 100
            self.pymax = 1080 - 256
            self.cx = 1920 + 128
            self.cy = random.randint(128, 1080 - 128)
            self.cymin = 100
            self.cymax = 1080 - 128

            # The time of the past render-frame.
            self.oldst = None

            self.lose = False

            return

        # Draws the screen
        def render(self, width, height, st, at):

            # The Render object we'll be drawing into.
            r = renpy.Render(width, height)

            # Figure out the time elapsed since the previous frame.
            if self.oldst is None:
                self.oldst = st

            dtime = st - self.oldst
            self.oldst = st

            # This draws the player.
            def player(px, py, pymin, pymax):

                # Render the player image.
                player = renpy.render(self.player, width, height, st, at)

                # renpy.render returns a Render object, which we can
                # blit to the Render we're making.
                r.blit(player, (int(self.px), int(self.py)))
            
            # This draws the coin.
            def coin(cx, cy, cymin, cymax):

                # Render the coin image.
                coin = renpy.render(self.coin, width, height, st, at)

                # renpy.render returns a Render object, which we can
                # blit to the Render we're making.
                r.blit(coin, (int(self.cx), int(self.cy)))

            if self.cx < -128:
                # Player missed the coin
                self.player_lives -= 1
                renpy.sound.play("audio/minigames/miss_sound.wav")
                self.cx = width + 128
                self.cy = random.randint(128, height - 128)
            else:
                # Move the coin
                self.cx -= self.coin_velocity

            if self.py < self.pymin:
                self.py = self.pymin               
            if self.py > self.pymax:
                self.py = self.pymax

            if self.cy < self.cymin:
                self.cy = self.cymin               
            if self.cy > self.cymax:
                self.cy = self.cymax

            player(self.px, self.py, self.pymin, self.pymax)
            coin(self.cx, self.cy, self.cymin, self.cymax)

            # Check for collisions
            def is_colliding(player, coin):
                return (
                    self.px <= self.cx + self.COIN_WIDTH and
                    self.px + self.PLAYER_WIDTH >= self.cx and
                    self.py <= self.cy + self.COIN_HEIGHT and
                    self.py + self.PLAYER_HEIGHT >= self.cy
                )
            
            if is_colliding(player, coin):
                self.score += 1
                renpy.sound.play("audio/minigames/coin_sound.wav")
                self.coin_velocity += self.COIN_ACCELERATION
                self.cx = width + 128
                self.cy = random.randint(128, height - 128)

            # Check for a loss.
            if self.player_lives == 0:
                self.lose = True

                renpy.timeout(0)

            # Ask that we be re-rendered ASAP, so we can show the next
            # frame.
            renpy.redraw(self, 0)

            # Return the Render object.
            return r

        # Handles events.
        def event(self, ev, x, y, st):

            import pygame

            if ev.type == pygame.KEYDOWN and ev.key == pygame.K_UP:
                self.py -= self.PLAYER_VELOCITY
                renpy.redraw(self, 0)
                raise renpy.IgnoreEvent()
                
            if ev.type == pygame.KEYDOWN and ev.key == pygame.K_DOWN:
                self.py += self.PLAYER_VELOCITY
                renpy.redraw(self, 0)
                raise renpy.IgnoreEvent()

                # Ensure the screen updates.
                renpy.restart_interaction()

            # If the player loses, return it.
            if self.lose:
                return self.lose
            else:
                raise renpy.IgnoreEvent()

    def display_score(st, at):
        return Text(_("Score: ") + "%d" % feed_the_dragon.score, size=40, color="#00cc00", outlines=[ (4, "#006600", 0, 0) ], font="gui/font/PressStart2P.ttf"), .1

    def display_player_lives(st, at):
        return Text(_("Lives: ") + "%d" % feed_the_dragon.player_lives, size=40, color="#00cc00", outlines=[ (4, "#006600", 0, 0) ], font="gui/font/PressStart2P.ttf"), .1

default feed_the_dragon = FeedtheDragonDisplayable()

screen feed_the_dragon():

    add Solid("#000000")

    add feed_the_dragon

    add DynamicDisplayable(display_score) xpos 240 xanchor 0.5 ypos 25

    add DynamicDisplayable(display_player_lives) xpos (1920 - 240) xanchor 0.5 ypos 25

    text _("Feed the Dragon"):
        xalign 0.5
        ypos 25
        size 40
        color "#00cc00"
        outlines [ (4, "#006600", 0, 0) ]
        font "gui/font/PressStart2P.ttf"

label play_feed_the_dragon:

    window hide  # Hide the window and quick menu while in Feed the Dragon
    $ quick_menu = False

    play music feed_the_dragon

    $ feed_the_dragon.lose = False
    $ feed_the_dragon.score = 0
    $ feed_the_dragon.player_lives = 5
    $ feed_the_dragon.coin_velocity = feed_the_dragon.COIN_STARTING_VELOCITY
    $ feed_the_dragon.py = 500
    $ feed_the_dragon.cy = random.randint(128, 1080 - 128)

    call screen feed_the_dragon

    play music amusement

    $ quick_menu = True
    window auto

label feed_the_dragon_done:

    if persistent.feed_the_dragon_high_score >= feed_the_dragon.score:
        pass
    else:
        $ persistent.feed_the_dragon_high_score = feed_the_dragon.score

    "Score: [feed_the_dragon.score]\n\nHigh Score: [persistent.feed_the_dragon_high_score]"

    menu:
        "Would you like to play again?"

        "Yes.":
            jump play_feed_the_dragon

        "No.":
            jump arcade_game_selection

User avatar
laure44
Regular
Posts: 60
Joined: Mon Mar 08, 2021 10:55 pm
Projects: Arkan'sTower, Gemshine Lorelei!
Location: France
Contact:

Re: Porting Pygame to a CDD (Collision Detection)

#3 Post by laure44 » Fri Jul 29, 2022 9:24 pm

I believe this is a "normal" behavior, but there are some workarounds for that.

For instance, you could do this:

Code: Select all

    class FeedtheDragonDisplayable(renpy.Displayable):
        def __init__(self):
            renpy.Displayable.__init__(self)
            self.key_pressed = None # This will store which key is held down
	    # ...

Code: Select all

    def event(self, ev, x, y, st):

        import pygame

        # The following allows to store which key was held down.
        if ev.type == pygame.KEYDOWN and ev.key == pygame.K_UP and self.key_pressed != "up":
            self.key_pressed = "up"
        elif ev.type == pygame.KEYDOWN and ev.key == pygame.K_DOWN and self.key_pressed != "down":
            self.key_pressed = "down"
        elif ev.type == pygame.KEYUP:
            self.key_pressed = None

        # Ensure the screen updates.
        renpy.restart_interaction()

        # If the player loses, return it.
        if self.lose:
            return self.lose
        else:
            raise renpy.IgnoreEvent()
And finally, in the render...

Code: Select all

    def render(self, width, height, st, at):

        # ...

        if self.key_pressed == "up":
            self.py -= self.PLAYER_VELOCITY
        elif self.key_pressed == "down":
            self.py += self.PLAYER_VELOCITY

        # ...

This seems to work quite well on my end, the only 'downside' is that I had to lower player's velocity, otherwise the dragon just moves too fast.

Edit: Also, player's movement becomes way smoother. If that bothers you, then hopefully someone can find another solution to solve your issue.

User avatar
GemstoneSystem
Newbie
Posts: 11
Joined: Sun Jun 19, 2022 5:04 pm
Projects: Shards
itch: gemmysystem
Contact:

Re: Porting Pygame to a CDD (Collision Detection)

#4 Post by GemstoneSystem » Fri Jul 29, 2022 10:27 pm

laure44 wrote:
Fri Jul 29, 2022 9:24 pm

Code: Select all

    def event(self, ev, x, y, st):

        import pygame

        # The following allows to store which key was held down.
        if ev.type == pygame.KEYDOWN and ev.key == pygame.K_UP and self.key_pressed != "up":
            self.key_pressed = "up"
        elif ev.type == pygame.KEYDOWN and ev.key == pygame.K_DOWN and self.key_pressed != "down":
            self.key_pressed = "down"
        elif ev.type == pygame.KEYUP:
            self.key_pressed = None

        # Ensure the screen updates.
        renpy.restart_interaction()

        # If the player loses, return it.
        if self.lose:
            return self.lose
        else:
            raise renpy.IgnoreEvent()
The dragon does move rather fast, huh? Even when the velocity is miniscule. Still, this is much better than the previous situation.

I was wondering, though, do you happen to know how I could switch to a focus_up and focus_down format? I'd prefer if my minigames were playable on controller, too, not just keyboard.

EDIT: Nvm! I figured it out. Also, the dragon moves at an acceptable pace at 20 but I was just soft-resetting and not realizing that it wouldn't change with just that. Ha ha.

Here's the code for those that are curious:

Code: Select all

            if renpy.map_event(ev, "focus_up") and self.key_pressed != "up":
                self.key_pressed = "up"
            elif renpy.map_event(ev, "focus_down") and self.key_pressed != "down":
                self.key_pressed = "down"
            elif renpy.map_event(ev, "pad_lefty_zero") or renpy.map_event(ev, "pad_righty_zero") or renpy.map_event("pad_dpup_release") or renpy.map_event("pad_dpdown_release") or ev.type == pygame.KEYUP:
                self.key_pressed = None

Post Reply

Who is online

Users browsing this forum: No registered users