[SOLVED] Minigame Help - Screens, Inputs, Gameloops - tysm _ticlock_!

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.
Message
Author
thexerox123
Regular
Posts: 134
Joined: Fri Jan 20, 2023 3:21 pm
itch: thexerox123
Contact:

Re: [SOLVED] Minigame Help - Screens, Inputs, Gameloops - tysm _ticlock_!

#31 Post by thexerox123 »

That did the trick perfectly, it works so well! Huge thank you once again! :)

Just to put a bow on things, so to speak, I uploaded a standalone version of the minigame to itch.io:

https://thexerox123.itch.io/christmas-cranes

And I'll put the final code here for posterity!

crane_game_displayable.rpy:

Code: Select all

image blue_crane = Image("BlueCrane.png", xysize=(200,200))
image red_crane = Image("RedCrane.png", xysize=(200,200))
image green_crane = Image("GreenCrane.png", xysize=(200,200))
image gold_crane = Image("GoldCrane.png", xysize=(200,200))

image a_right = Image("Right_Idle.png", xysize=(100,100))
image a_left = Image("Left_Idle.png", xysize=(100,100))
image a_up = Image("Up_Idle.png", xysize=(100,100))

image bear_one = Image("Bear1.png", xysize=(100,100))
image bear_two = Image("Bear2.png", xysize=(100,100))
image bear_three = Image("Bear3.png", xysize=(100,100))
image bear_four = Image("Bear4.png", xysize=(100,100))
image bear_five = Image("Bear5.png", xysize=(100,100))



init python:
    class TimerObj(renpy.Displayable):
        def __init__(self, obj, name, **kwargs):
            super(TimerObj, self).__init__(**kwargs)
            self.obj = obj
            self.name = name
            setattr(self.obj, self.name, 0)
        def render(self, width, height, st, at):
            if getattr(self.obj, self.name) < st:
                setattr(self.obj, self.name, st)

            r = renpy.Render(width, height)
            cr = renpy.render(Text("{:.2f}".format(getattr(self.obj, self.name))), width, height, st, at)
            r.blit(cr, (0,0))
            renpy.redraw(self, 0)
            return renpy.Render(0, 0)

    class Crane():
        def __init__(self, crane_type, toy_image, box_image, start_st, duration,
                    number_of_arrows = 3, dt_start = 5.0, dt_end = 3.0):
            self.d = crane_type
            self.toy_image = toy_image
            self.box_image = box_image
            self.start_st = start_st
            self.duration = duration
            self.dt_start = dt_start
            self.dt_end = dt_end

            self.arrows = []
            for i in range(0,number_of_arrows):
                self.arrows.append(renpy.random.choice(
                                    ["a_left", "a_up", "a_right"]))
            self.progress = "start"


        def input_handler(self, key, st, game):
            try:
                if self.arrows[self.progress] == key and self.progress == game.progress:
                    self.progress += 1
                    game.reset_progress = False
                    if self.progress == len(self.arrows):
                        self.progress = "done"
                        self.end_st = st
                        game.reset_progress = True
                        return True
                else:
                    self.progress = 0
            except TypeError:
                return

        def time_handler(self, st):
            if self.progress == "start":
                if self.timer(st) <= 0:
                    self.progress = 0
            else:
                if self.timer(st) <= 0:
                    if self.progress == "done" or self.progress == "miss":
                        return True
                    else:
                        self.end_st = st
                        renpy.play(Wrong)
                        self.progress = "miss"

        def timer(self, st):
            if self.progress == "start":
                return self.start_st + self.dt_start - st
            elif self.progress == "done" or self.progress == "miss":
                return self.end_st + self.dt_end - st
            else:
                return self.start_st + self.dt_start + self.duration - st



    class CraneGame():
        def __init__(self, difficulty):
            self.game_finished = False
            self.score = 0
            self.difficulty = difficulty
            self.time = 0
            if difficulty == "easy":
                self.cranes_left = [30,15,0,0]
            elif difficulty == "medium":
                self.cranes_left = [20,24,5,1]
            elif difficulty == "hard":
                self.cranes_left = [20,20,10,5]
            else:
                self.cranes_left = [20,20,20,10]

            self.columns = [[],[],[],[],[]]
            self.progress = 0


        def generate_crane(self):
            if self.cranes_left == [0,0,0,0]:
                return

            # generate crane in empty columns
            weights = [(1 if i == [] else 0) for i in self.columns]
            if weights == [0]*len(self.columns):
                weights = [1]*len(self.columns)
            column = renpy.random.choices(range(0,5), weights=weights, k=1)[0]
            crane_types = ["blue_crane", "red_crane", "green_crane", "gold_crane"]
            toy_image = renpy.random.choices(["Bear1.png", "Bear2.png", "Bear3.png", "Bear4.png", "Bear5.png", "Doll1.png", "Doll2.png", "Doll3.png", "Doll4.png", "Doll5.png", "Robot1.png", "Robot2.png", "Robot3.png", "Robot4.png", "Robot5.png", "Car1.png", "Car2.png", "Car3.png", "Car4.png", "Car5.png"])
            box_image = renpy.random.choices(["Box1.png", "Box2.png", "Box3.png", "Box4.png", "Box5.png", "Box6.png", "Box7.png", "Box8.png", "Box9.png", "Box10.png", "Box11.png", "Box12.png", "Box13.png", "Box14.png", "Box15.png", "Box16.png", "Box17.png", "Box18.png", "Box19.png", "Box20.png"])
            crane_id = renpy.random.choices(range(0,4), weights=self.cranes_left, k=1)[0]
            self.cranes_left[crane_id] -= 1
            crane_type = crane_types[crane_id]
            duration = 14.0 - 2.0*crane_id
            dt_start = 0.2 + 0.2*column
            dt_end = 1.0 - 0.2*column
            crane = Crane(crane_type, toy_image, box_image, self.time, duration, dt_start = dt_start, dt_end = dt_end)



            self.columns[column].append(crane)

        def number_of_shown_cranes(self):
            n = 0
            for column in self.columns:
                for crane in column:
                    n += 1
            return n

        def set_screen_timer(self):
            t = 1000
            for column in self.columns:
                for crane in column:
                    new_t = crane.timer(self.time)
                    if new_t < t:
                        t = new_t
            if t != 1000:
                self.timer = t
            else:
                self.game_finished = True

        def handler(self, key = None):
            if key:
                self.reset_progress = True
                for column in self.columns:
                    for crane in column:
                        if crane.input_handler(key, self.time, self):
                            self.score += 1
                if self.reset_progress:
                    self.progress = 0
                else:
                    self.progress += 1

            self.reset_progress = True
            for column in self.columns:
                for crane in column:
                    remove_crane = crane.time_handler(self.time)
                    if remove_crane:
                        column.pop()
                    elif crane.progress == self.progress:
                        self.reset_progress = False
            if self.reset_progress:
                self.progress = 0

            # Check if necessary to generate new cranes
            n = self.number_of_shown_cranes()
            if n < 5:
                for i in range(0, 5-n):
                    self.generate_crane()

            self.set_screen_timer()


transform crane_appear(col, left_st):
    xalign (0.1 + 0.2*col - left_st) yalign -0.1
    linear left_st  xalign (0.1 + 0.2*col)

transform toy_appear(col, left_st):
    xalign (0.155 + 0.175*col - left_st) yalign 0.34
    linear (left_st*0.9378)  xalign (0.155 + 0.175*col)
    
transform crane_remove(col, left_st):
    xalign (1.1 - left_st) yalign -0.1
    linear left_st  xalign 1.1
    
transform toy_remove(col, left_st):
    xalign (1.1 - left_st) yalign 0.34
    linear left_st  xalign 1.1

transform box_remove(col, left_st):
    xalign (1.1 - left_st) yalign 0.35
    linear left_st xalign 1.1

transform zoommuch:
    zoom 0.08

screen s_crane_game():
    for col, column in enumerate(crane_game.columns):
        for crane in column:
            $ left_time = crane.timer(crane_game.time)
            if crane.progress == "start":
                # move_in animation
                add crane.d at crane_appear(col, left_time)
                add crane.toy_image at toy_appear(col, left_time)
                pass
            elif crane.progress == "done":
                # success animation
                # add crane.d:
                #     xalign (0.1 + 0.2*col) yalign -0.1
                # add crane.box_image:
                #     xalign (0.14 + 0.182*col) yalign 0.35 zoom 0.6
                add crane.d at crane_remove(col, left_time)
                add crane.box_image at box_remove(col, left_time)
                pass
            elif crane.progress == "miss":
                add crane.d at crane_remove(col, left_time)
                add crane.toy_image at toy_remove(col, left_time)
            else:
                add crane.d:
                    xalign (0.1 + 0.2*col) yalign -0.1
                add crane.toy_image:
                    xalign (0.155 + 0.175*col) yalign 0.34

                hbox:
                    xalign (0.15 + 0.175*col) yalign 0.025
                    for j, arrow in enumerate(crane.arrows):
                        frame:
                            if crane.progress > j:
                                background "#8f8"
                            else:
                                background "#737373"
                            add arrow at zoommuch
                bar:
                    xsize 200 ysize 50
                    xalign (0.15 + 0.175*col) yalign 0.5
                    value AnimatedValue(value=0.0,
                        range=crane.duration,
                        delay=left_time,
                        old_value=left_time)

    hbox:
        xalign 0.5 yalign 0.9 spacing 100
        for btn_img in ["a_left", "a_up", "a_right"]:
            imagebutton:
                idle btn_img action Return(btn_img)
                hover Transform(btn_img, matrixcolor=TintMatrix("#10e91a"))

    text "Score = {}".format(crane_game.score) xalign 0.97 yalign 0.98
    text "Cranes left = {}".format(sum(crane_game.cranes_left)) xalign 0.5 yalign 0.98
    timer crane_game.timer action Return(False)
    key "input_left" action Return("a_left")
    key "input_right" action Return("a_right")
    key "input_up" action Return("a_up")
script.rpy:

Code: Select all

label start:
    pass

label l_crane_game:
    scene bg wild1
    menu:
        "Easy":
            $ crane_difficulty = "easy"
        "Medium":
            $ crane_difficulty = "medium"
        "Hard":
            $ crane_difficulty = "hard"
        "Ultra Hard":
            $ crane_difficulty = "ultra"

label gamestart:
    scene bg cranedesk
    if crane_difficulty == "easy":
        "{color=228C22}Your task is to wrap 45 gifts using the arrow keys!{/color}"
    if crane_difficulty == "medium":
        "{color=228C22}Your task is to wrap 50 gifts using the arrow keys!{/color} "
    if crane_difficulty == "hard":
        "{color=228C22}Your task is to wrap 55 gifts using the arrow keys!{/color}"
    if crane_difficulty == "ultra":
        "{color=228C22}Your task is to wrap 70 gifts using the arrow keys! Good luck!{/color}"
    scene bg getready
    play sound Bell        
    pause 3
    scene bg wrap 
    play sound Bell
    pause 1

label minigame_begin:
    scene bg cranedesk
    play music MountainKing
    $ _rollback = False
    $ crane_game = CraneGame(crane_difficulty)
    show expression TimerObj(crane_game, "time") as crane_timer
    $ key = None

label .loop:
    $ crane_game.handler(key)
    if crane_game.game_finished:
        jump .results
    call screen s_crane_game
    if _return:
        $ key = _return
    else:
        $ key = None
    jump .loop

label .results:
    hide crane_timer
    play music ChristmasChill
    if crane_difficulty == "easy":
        "Your score is [crane_game.score] out of 45."
    elif crane_difficulty == "medium":
        "Your score is [crane_game.score] out of 50."
    elif crane_difficulty == "hard":
        "Your score is [crane_game.score] out of 55."
    elif crane_difficulty == "ultra":
        "Your score is [crane_game.score] out of 70."
    menu:
        "Do you want to play again?"  
        "Yes, same difficulty":
            jump gamestart
        "Yes, change difficulty":
            jump l_crane_game
        "No, return to main menu":      
            return

thexerox123
Regular
Posts: 134
Joined: Fri Jan 20, 2023 3:21 pm
itch: thexerox123
Contact:

Re: [SOLVED] Minigame Help - Screens, Inputs, Gameloops - tysm _ticlock_!

#32 Post by thexerox123 »

Hey _ticlock_, sorry to bother you, but if you see this reply...

I've been trying to reverse-engineer the code that you provided to map onscreen controls to keybindings for someone else (as well as myself) in this thread:

viewtopic.php?f=51&t=47820&p=562435#top

But we've both been struggling to get it to work. Thought it was worth reaching out to you to see if you could help clarify, if you get the chance! :)

(I think I largely managed to deconstruct what was going on, but I'm still missing something about how conditionals work with key, or how to pass it properly to the classes, or integrating it with the update methods or something.)

User avatar
_ticlock_
Miko-Class Veteran
Posts: 910
Joined: Mon Oct 26, 2020 5:41 pm
Contact:

Re: [SOLVED] Minigame Help - Screens, Inputs, Gameloops - tysm _ticlock_!

#33 Post by _ticlock_ »

thexerox123 wrote: Thu Aug 24, 2023 4:11 pm viewtopic.php?f=51&t=47820&p=562435#top

But we've both been struggling to get it to work. Thought it was worth reaching out to you to see if you could help clarify, if you get the chance! :)
In the crane minigame we use the following approach:

Minigame loop:
-> minigame screen (call screen statement)
minigame screen ends the interaction and returns the result of any action such as key pressed, time event, etc.
-> process the returned result

thexerox123 wrote: Thu Aug 24, 2023 4:11 pm (I think I largely managed to deconstruct what was going on, but I'm still missing something about how conditionals work with key, or how to pass it properly to the classes, or integrating it with the update methods or something.)
Using the loop approach, you can process pressed key or other events using python blocks. In the crane minigame we pass the key to the class method directly:

Code: Select all

label .loop:
    $ crane_game.handler(key)	# Processing the pressed key using class method.
    if crane_game.game_finished:
        jump .results
    call screen s_crane_game	# call screen statement to display minigame interface, and effects.
    if _return:
        $ key = _return
    else:
        $ key = None
    jump .loop
* The player can create a save file in the middle of the minigame.

Another approach is to make a minigame within a single call screen statement or "show displayable" statement.

Minigame:
-> minigame screen/displayable (call screen statement)
all user interaction are processed inside a the screen/displayable.



I took a quick look at the code from the post viewtopic.php?f=51&t=47820&p=562435#p562428.

Code: Select all

label play_feed_the_tiger:
    ...
    call screen feed_the_tiger
    ...
label feed_the_tiger_done:
This game uses CDD and a single call screen statements. In this case, we need to process the interaction within the screen interaction. For example:
The pressed key can be processed in the event method of the CDD:

Code: Select all

def event(self, ev, x, y, st):
    if ev.type == pygame.KEYDOWN:
        if ev.key == pygame.K_LEFT:
            # left arrow.
            self.handle_key("a_left")
        # similar for other keys
        ...

def handle_key(key):
    # Method that process pressed keys
    # Likely trigger redraw method
For the buttons, you can directly call the handle_key method:

Code: Select all

imagebutton:
    ...
    action Function(feed_the_tiger.handle_key, "a_left")

thexerox123
Regular
Posts: 134
Joined: Fri Jan 20, 2023 3:21 pm
itch: thexerox123
Contact:

Re: [SOLVED] Minigame Help - Screens, Inputs, Gameloops - tysm _ticlock_!

#34 Post by thexerox123 »

_ticlock_ wrote: Sat Aug 26, 2023 3:01 pm In the crane minigame we use the following approach:

Minigame loop:
-> minigame screen (call screen statement)
minigame screen ends the interaction and returns the result of any action such as key pressed, time event, etc.
-> process the returned result


Using the loop approach, you can process pressed key or other events using python blocks. In the crane minigame we pass the key to the class method directly:

* The player can create a save file in the middle of the minigame.

Another approach is to make a minigame within a single call screen statement or "show displayable" statement.

I took a quick look at the code from the post viewtopic.php?f=51&t=47820&p=562435#p562428.

This game uses CDD and a single call screen statements. In this case, we need to process the interaction within the screen interaction. For example:
The pressed key can be processed in the event method of the CDD:

For the buttons, you can directly call the handle_key method:
Thank you so much for responding to my Bat Signal! :lol: I really appreciate it!

Ahhh, that all makes sense! So that's why the Timer is the only class that uses renpy.Displayable. I was chalking that up as my not having a full grasp on how attributes are passed rather than recognizing it as a different implementation method.

The minigame I'm trying to implement it for also uses the dragon one as a base, if I recall, but is now built out a bit more:

viewtopic.php?f=8&t=67022#p562349

But I think it's still using the latter of those two approaches?

User avatar
_ticlock_
Miko-Class Veteran
Posts: 910
Joined: Mon Oct 26, 2020 5:41 pm
Contact:

Re: [SOLVED] Minigame Help - Screens, Inputs, Gameloops - tysm _ticlock_!

#35 Post by _ticlock_ »

thexerox123 wrote: Mon Aug 28, 2023 6:28 pm The minigame I'm trying to implement it for also uses the dragon one as a base, if I recall, but is now built out a bit more:

viewtopic.php?f=8&t=67022#p562349

But I think it's still using the latter of those two approaches?
Likely, since your CDD processes lots of interaction, but it is a matter of preference and convenience for a particular minigame. You can even use a combined approach: (some interactions are processed within the screen call, others - in the minigame loop).

Some things to consider
* Making a "pure single call screen" minigame without using CDD displayables is probably not a good idea. With "loop approach," you can make a minigame without using CDD displayables (use transforms and standard screen displayables like buttons, timers, viewports, etc.).

Using CDD is more powerful but it is typically more time-consuming. Using standard screen displayable gives less freedom, but it is probably a faster approach for simple minigames or partial functionality of more complex minigames.

When creating a CDD I recommend to start with some basic functionality that you could reuse in other games. You can create a new class and inherit from base CDD to customize to particular minigame.

thexerox123
Regular
Posts: 134
Joined: Fri Jan 20, 2023 3:21 pm
itch: thexerox123
Contact:

Re: [SOLVED] Minigame Help - Screens, Inputs, Gameloops - tysm _ticlock_!

#36 Post by thexerox123 »

_ticlock_ wrote: Tue Aug 29, 2023 10:31 am
thexerox123 wrote: Mon Aug 28, 2023 6:28 pm The minigame I'm trying to implement it for also uses the dragon one as a base, if I recall, but is now built out a bit more:

viewtopic.php?f=8&t=67022#p562349

But I think it's still using the latter of those two approaches?
Likely, since your CDD processes lots of interaction, but it is a matter of preference and convenience for a particular minigame. You can even use a combined approach: (some interactions are processed within the screen call, others - in the minigame loop).

Some things to consider
* Making a "pure single call screen" minigame without using CDD displayables is probably not a good idea. With "loop approach," you can make a minigame without using CDD displayables (use transforms and standard screen displayables like buttons, timers, viewports, etc.).

Using CDD is more powerful but it is typically more time-consuming. Using standard screen displayable gives less freedom, but it is probably a faster approach for simple minigames or partial functionality of more complex minigames.

When creating a CDD I recommend to start with some basic functionality that you could reuse in other games. You can create a new class and inherit from base CDD to customize to particular minigame.
Yeah, I've got pretty lofty end goals for this game... I started with that fairly simple ring-collection version just as a way to learn how to implement classes and collisions, but have since implemented a bunch of different projectiles, and am working on an Enemy class and a Pickups class. Working on it has helped a lot to get my head around how CDDs work, but I still have a lot to learn and don't know the optimal decisions to make yet. So I appreciate the explanations/recommendations!

I've been using dicts for the weapons and enemies that are then processed by their respective classes and implemented through the main SleighShooter gameloop. I've also added a second screen for displaying the score at the end of the game. So I think that's an apt use case for CDDs? but I may just be overcomplicating things for myself. :lol: (Going to also go back and give each weapon/projectile type its own collision animation/sfx.)

Image

Code: Select all

    weapon_variables = {
        "basic": {
            "weapon_type": "basic",
            "damage": 10,
            "fire_rate": 0.5,
            "projectile_frames": 2,
            "projectile_speed": 800,
            "animation_frames": [
                f"images/Projectiles/Basic{i:03}.png"
                for i in range(1, 3)
            ],
            "projectile_types": ["projectile1"],
            "frame_delay": 0.01,
            "spos": (300, 95), # Relative to the sleigh
            "fire_sound": ["audio/Blaster.mp3"], 
            "initial_y_velocity": 0,  # Set initial y velocity
            "gravity": 0  # Set gravity 
        },        

        "basic2": {
            "weapon_type": "basic2",
            "damage": 20,
            "fire_rate": 1.0,
            "projectile_frames": 60,
            "projectile_speed": 1000,
            "animation_frames": [
                f"images/Projectiles/tile{i:03}.png"
                for i in range(0, 60)
            ],
            "projectile_types": ["projectile1"],
            "frame_delay": 0.001,
            "spos": (252, 70),  # Relative to the sleigh
            "fire_sound": ["audio/Blaster2.mp3"],
            "initial_y_velocity": 0,  # Set initial y velocity
            "gravity": 0  # Set gravity           
        },

        "basic3": {
            "weapon_type": "basic3",
            "damage": 80,
            "fire_rate": 2,
            "projectile_frames": 4,
            "projectile_speed": 750,
            "animation_frames": [
                f"images/Projectiles/FB{i:03}.png"
                for i in range(1, 4)
            ],
            "projectile_types": ["projectile1"],
            "frame_delay": 0.1,
            "spos": (291, 91),  # Relative to the sleigh
            "fire_sound": ["audio/Blaster3.mp3"],
            "initial_y_velocity": 0,  # Set initial y velocity
            "gravity": 0  # Set gravity
        },

        "tree": {
            "weapon_type": "tree",
            "damage": 20,
            "fire_rate": 1.5,
            "projectile_frames": 0,
            "projectile_speed": 800,
            "animation_frames": [
                f"images/Projectiles/Tree{i:03}.png"
                for i in range(1, 3)
            ],
            "projectile_types": ["projectile1", "projectile2", "projectile3", "projectile4", "projectile5", "projectile6"],
            "frame_delay": 0.1,
            "spos": (230, 50), # Relative to the sleigh
            "fire_sound": ["audio/Tree.mp3"], 
            "initial_y_velocity": 0,  # Set initial y velocity
            "gravity": 0  # Set gravity 
        },        

        "tree2": {
            "weapon_type": "tree2",
            "damage": 20,
            "fire_rate": 0.5,
            "projectile_frames": 2,
            "projectile_speed": 600,
            "animation_frames": [
                f"images/Projectiles/Tree2{i:03}.png"
                for i in range(1, 3)
            ],
            "projectile_types": ["projectile1"],
            "frame_delay": 0.01,
            "spos": (230, 50), # Relative to the sleigh
            "fire_sound": ["audio/Tree.mp3"], 
            "initial_y_velocity": 0,  # Set initial y velocity
            "gravity": 0  # Set gravity 
        },        

        "gbread": {
            "weapon_type": "gbread",
            "damage": 40,
            "fire_rate": 2,
            "projectile_frames": 2,
            "projectile_speed": 1000,
            "animation_frames": [
                f"images/Projectiles/gs{i:03}.png"
                for i in range(1, 3)
            ],
            "projectile_types": ["projectile1"],
            "frame_delay": 0.1,
            "spos": (291, 91),  # Relative to the sleigh
            "fire_sound": ["audio/Cannon.mp3"],
            "initial_y_velocity": 0,  # Set initial y velocity
            "gravity": 0  # Set gravity
            
        },

        "rdeer": {
            "weapon_type": "rdeer",
            "damage": 50,
            "fire_rate": 1.75,
            "projectile_frames": 4,
            "projectile_speed": 700,
            "animation_frames": {
                f"projectile{i}": [f"images/Projectiles/bs{i}-{j}.png" for j in range(1, 5)]
                for i in range(1, 5)
            },
            "projectile_types": ["projectile1", "projectile2", "projectile3", "projectile4"],
            "frame_delay": 0.1,
            "spos": (291, 50),  # Relative to the sleigh
            "fire_sound": [
                "audio/Clink1.mp3",
                "audio/Clink2.mp3",
                "audio/Clink3.mp3",
                "audio/Clink4.mp3",
                "audio/Clink5.mp3"
            ],
            "initial_y_velocity": -300,  # Adjust as needed for the desired arc
            "gravity": 500  # Adjust to achieve the desired arc
            
        },
        # Define more weapon presets
    }

Post Reply

Who is online

Users browsing this forum: Amazon [Bot], Nightpxel, Silac