We are writing a text game in Python / Ren'Py part 2: mini-games and pitfalls

    Summary of the previous twenty five thousand series : we are writing a text game about swimming on the sea and getting involved in history. We write it on the Ren'Py engine, which was originally intended for visual short stories, but after a minimal set-up it is able to do everything we need. In a previous article, I talked about how to make a simple interactive reader, and in this we will add a couple more screens with more complex functions and finally work on python.
    image


    The previous article ended on the fact that the player can read the text, watch pictures and influence the development of the plot. If we were going to port Braslavsky's book-games or write CYOA in the spirit of Choice of Games- that would be enough. But in a game about navigation, you need at least a simple interface of navigation itself. To build a full-fledged simulator, like in the Sunless Sea, in Ren'Py is possible, but labor-intensive and generally pointless. In addition, we don’t want to make our own Sunless Sea with Buryats and buuses, so we will restrict ourselves to something like a global map in the “Space Rangers”: a dozen or two active points, from each of which you can go to several neighboring ones. Something like this:

    image

    Displayables, screens and layers



    For the interface itself, we just need to display a bunch of buttons (one for each point) and a background image; one Displayable type of Imagemap is enough for this. Basically, it is not much different from its counterpart in HTML: this is a picture in which active zones are defined, each with its own action. However, working with bare Displayable is inconvenient, and from an architectural point of view it is ugly. Therefore, before undertaking the code, it is worthwhile to understand the hierarchy of elements that Ren'Py displays.

    The lowest level is Displayables, that is, widgets built-in or created by developers of a particular game. Each of them performs some basic function: it shows a picture, displays text, provides input, etc. Displayables are usually grouped into screens.which provide more abstract interactions: for example, text output and in-game menus are performed by the nvl screen, which includes the actual text, background, frame, and menu buttons. At the same time, you can show as many screens as you like: on one, for example, there may be text, on the other buttons that control the volume of music, and on the third some snowflakes flying on top of all this. As necessary, you can show or remove individual screens with mini-games, save menus and everything else that is needed for the game. And finally, there are layers . It is not necessary to work directly with layers, but you need to know about their existence. The wallpaper, for example, is not related to the screen system and is displayed below them in the master layer.

    So, the description of the travel screen is as follows:

    screen map_screen():
        tag map 
        modal True 
        zorder 2 
        imagemap: 
            auto 'images/1129map_%s.png' 
            #  Main cities 
            hotspot monet.hotspot action Travel(monet) 
            hotspot tartari.hotspot action Travel(tartari) 
            #  …
            #  More of the same boilerplate
            #  …
        add 'images/1024ship.png': 
            at shiptransform(old_coords, coords) 
            anchor (0.5, 1.0) 
            id 'ship'
    transform shiptransform(old_coords, coords): 
        pos old_coords 
        linear 0.5 pos coords 
    


    The map_screen () screen is declared first. The tag is optional; it just allows you to group screens for commands like “Remove all screens associated with moving”. Zorder is the “height” of the screen by which it is decided which elements will obscure each other. Since we set zorder = 2, and the nvl screen has default zorder = 1, during the trip the player will not see windows with text. But most importantly, this screen is modal, that is, no elements below it will receive input events. It turns out that the nvl screen is blocked with the appearance of the map, so we do not need to track the game state ourselves and make sure that clicking on the map at the same time does not scroll the text.

    Imagemap itself consists of two main elements: the auto tag and a list of active zones. Auto contains a link to a set of files that will be used as background. It’s a set, not a single picture: separately lies a file in which all the buttons are drawn as inactive, separately - in which they are all pressed and so on. And Ren'Py already selects the desired fragment from each file and compiles a picture on the screen. And finally, the active zones (hotspot). They are described by a tuple of four integers (coordinates and size) and the object of the action. We do not hardcode the tuple, but use the attribute of the object; in the screen description, you can always insert a variable or attribute instead of a value. The action object describes what happens when the button is pressed and controls whether the button should be active at the moment. Ren ' Py provides quite a few built-in actions for routine tasks such as switching scripts or saving a game, but you can also make your own. Finally, the ship’s drawing and transformation are added last, thanks to which it does not just appear on the screen, but crawls from point A to point B. Transformations are described in a separate languageATL (Animation & Transformation Language) .

    One pitfall is associated with the screens: the screen code is not executed immediately before it is shown, but in advance. This happens at any time to the engine and as many times as it pleases, so everything that is accessed in this code should be initialized before the start of the game and should not affect the state of other important variables.

    Play cards



    In a previous article, I promised to show two ways to make interfaces in Ren'Py. The first one we just saw: it is quite simple, but by no means universal. I still did not understand, for example, how to describe the dragging of objects with the mouse in the on-screen language and handle collisions. Or how to layout screens if the number of elements on them is determined during the game. Fortunately, Displayables are complete widgets: they can include other Displayables, catch input events and change if necessary. Therefore, you can describe the entire mini-game in one Displayable in almost the same way as we would for a separate application, and insert it into the project.

    We will have a mini-game card. All potentially useful property and knowledge of the protagonist are presented in the form of a deck of cards with a face value of 1 to 10 in four suits: Strength, Knowledge, Intrigue and Money. Say, a deuce of strength is invaluable knowledge of where a person has his breath to beat, and eight intrigues is a reliable list of agents of the Office among smugglers. A simple game with bribes determines whether the player coped with the problem before him and what suit he won or lost. As a result, a conflict can have a maximum of eight outcomes: victory and defeat with each suit in principle can lead to different consequences.

    A card game means that cards are featured in it. The code for a Displayable displaying a small map looks like this:

    class CardSmallDisplayable(renpy.Displayable): 
            """ 
            Regular card displayable 
            """ 
            suit_bg = {u'Деньги': 'images/MoneySmall{0}Card.jpg', 
               u'Знания': 'images/KnowledgeSmall{0}Card.jpg', 
               u'Интриги': 'images/IntrigueSmall{0}Card.jpg', 
               u'Сила': 'images/ForceSmall{0}Card.jpg'} 
            def __init__(self, card, **kwargs): 
                super(CardSmallDisplayable, self).__init__(xysize=(100, 140), xfill=False, yfill=False, **kwargs) 
                self.bg = Image(self.suit_bg[card.suit].format((card.spendable and 'Spendable' or 'Permanent'))) 
                self.text = Text(u'{0}'.format(card.number), color = '#6A3819', font='Hangyaboly.ttf') 
                self.xpos = 0 
                self.ypos = 0 
                self.xsize = 100 
                self.ysize = 140 
                self.x_offset = 0 
                self.y_offset = 0 
                self.transform = Transform(child=self) 
            def render(self, width, height, st, at): 
                """ 
                Return 100*140 render for a card 
                """ 
                bg_render = renpy.render(self.bg, self.xsize-4, self.ysize-4, st, at) 
                text_render = renpy.render(self.text, width, height, st, at) 
                render = renpy.Render(width, height, st, at) 
                render.blit(bg_render, (2, 2)) 
                render.blit(text_render, (15-int(text_render.width/2), 3)) 
                render.blit(text_render, (88-int(text_render.width/2), 117)) 
                return render 
            def visit(self): 
                return[self.bg, 
                       self.text] 
    


    This is essentially the easiest Displayable possible. It consists of two others: a background image, selected depending on the suit, and text with a face value. Both methods (apart from the constructor) are necessary for Displayable to work: self.render returns a texture, and self.visit returns a
    list of all Displayables included in this. And she does draw a small map; Here are a few of these cards on the deck screen:

    image

    Already not bad, but the card itself can only be on the screen, and then only if someone puts it there. In order to play the mini-game, in fact, you need to add an external Displayable, capable of processing input and computing game logic. Maps and other interface elements will be included in it in the same way that text fields and backgrounds are parts of the map. This Displayable will differ in the presence of the self.event () method, which receives PyGame events as input. LINK TO PYGAME.EVENT Something like this (the full code is available on the github in the Table class):

    def event(self, ev, x, y, st): 
            if ev.type == pygame.MOUSEBUTTONDOWN and ev.button == 1: 
                #  …
                #  Process click at x, y
                #  …
            elif ev.type == pygame.MOUSEMOTION and self.dragged is not None:
                #  …
                #  Process dragging card to x, y
                #  …
            renpy.restart_interaction()
    


    You don’t have to worry about the queue of events: the engine will distribute all events to all elements that are currently active. All that remains for the method is to check whether it is of interest to the incident, respond to the event and complete the interaction. Interaction in Ren'Py is approximately equivalent to the tick of game logic in other engines, but is not limited in time. In general, this is one player’s team and the game’s response to it, although sometimes (for example, when scrolling through the text), interactions can end by themselves.
    This Displayable, like everyone else, we wrap up in the screen:

    screen conflict_table_screen():
        modal True
        zorder 9
        add conflict_table
    


    conflict_table in this case is not a class name, but a global variable in which the corresponding Displayable is stored. It was mentioned above that the screen code can, in principle, be executed at any time, but before the show it will be executed without fail, otherwise the game will not know what it should actually output to it. Therefore, it’s quite safe immediately before the mini-game to do something like conflict_table.set_decks (player_deck, opponent_deck) and rely on the fact that the player will find exactly what is needed. Similarly, at the end of the mini-game, you can access the results that are stored in the same object.

    I must say that the use of global variables is not a limitation of Ren'Py, but our own decision. Screens and Displayables are supported, which are capable of accepting arguments and returning values, but they are somewhat more complicated. Firstly, the behavior of such screens is poorly documented. At the very least, it’s quite difficult to figure out at what exact moment the screen begins its first interaction and when exactly it returns control to the script. And this is a very important question, since without knowing the answer to it, it is difficult to guarantee that the entire text preceding the conflict will be shown before the conflict begins, and the text following the conflict will not be shown. Secondly, with the use of global variables, most of the objects needed for the mini-game are initialized only once, and then we change their attributes each time we run it. Otherwise, you would have to spend time with each conflict loading all the necessary files; the game lags significantly if the HDD is also accessed simultaneously, for example, torrent and antivirus. Finally, not only the conflict screen, but also several other screens refers to the cards, so it is logical to use the same Displayables wherever they are needed.

    Afterword



    On this, the actual software part of the development ends and the filling of the game with content begins. Literature in the game is mainly not done by me, so I will not discuss the structure of the narration and style. On this subject, I can recommend reading, for example, a classic article on the structure of CYOA . Or a good guide to writing compelling independent NPCs from a scriptwriter 80 days .
    More links (as well as reviews of fresh English-language works and related articles) can be found on Emily Short's blog .

    Also popular now: