├── game ├── images │ └── speech_bubble.png └── speech_bubbles.rpy ├── explain_images ├── speech_bubbles.gif ├── speech_bubble_margins.png ├── speech_bubble_frame_values.png └── speech_bubble_padding_values.png ├── .github └── FUNDING.yml ├── LICENSE ├── README.md ├── explain_usage.md ├── explain_screens.md └── explain_frames.md /game/images/speech_bubble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RenpyRemix/speech-bubbles/HEAD/game/images/speech_bubble.png -------------------------------------------------------------------------------- /explain_images/speech_bubbles.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RenpyRemix/speech-bubbles/HEAD/explain_images/speech_bubbles.gif -------------------------------------------------------------------------------- /explain_images/speech_bubble_margins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RenpyRemix/speech-bubbles/HEAD/explain_images/speech_bubble_margins.png -------------------------------------------------------------------------------- /explain_images/speech_bubble_frame_values.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RenpyRemix/speech-bubbles/HEAD/explain_images/speech_bubble_frame_values.png -------------------------------------------------------------------------------- /explain_images/speech_bubble_padding_values.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RenpyRemix/speech-bubbles/HEAD/explain_images/speech_bubble_padding_values.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: renpyremix 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Remix 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Speech Bubbles 2 | 3 | #### Note: All you really need are the two files in [The game folder](game). Just one image and a small script file. They can be dropped into a new or existing project and a label called to view an example (details in the .rpy file). 4 | #### Alternatively, just clone the lot as a zip from [The Repository Main Page](https://github.com/RenpyRemix/speech-bubbles) 5 | 6 | So, you want a system where each line of dialogue can appear at a set position in a frame that looks like a comic book speech bubble? 7 | 8 | You want all the normal Ren'Py dialogue things to still work... translation, styles, text tags, extend and everything else related to the line of speech. 9 | 10 | You want the ability to show multiple bubbles at once, styled differently and you want a system that makes it easy to specify the settings for each line. 11 | 12 | ![Image of Speech Bubbles](explain_images/speech_bubbles.gif?raw=true "Thanks to: 13 | Pixabay for the background 14 | Kid Blue for the sprite images 15 | Links at the end of this overview.") 16 | 17 | [![Support me on Patreon](https://c5.patreon.com/external/logo/become_a_patron_button.png)](https://www.patreon.com/bePatron?u=19978585) 18 | 19 | # Speech Bubbles in Ren'Py 20 | 21 | In order to achieve this, we need to handle several things: 22 | - Create a new screen say that can use subscreens to show each line of dialogue 23 | - Creating a new screen lets us leave the normal one there if needed. 24 | - This new screen cycles through all the dialogue lines (bubbles) it should show and passes the parameters of each to a used subscreen 25 | - Create the subscreen and allow it to take parameters so each can display differently 26 | - Using say arguments like `e "Hello World" (450, 280, "topleft")` the used subscreen can be positioned at (450, 280) and use a style with tail at "topleft" 27 | - Additional keywords can be passed to adjust other settings 28 | - Include a system wherein the subscreen parameters can be retained, allowing it to be reshown during the following dialogue 29 | - We use the `on "hide"` event of our new screen say to pass the current dialogue and settings to a function 30 | - That function determines what if anything is retained to be reshown with the following line and stores them in a global 31 | - Adjust our Characters so they use the new screen say 32 | - This includes changing their `what_style` so they do not use default settings intended for large dialogue windows 33 | 34 | 35 | ### Important Reading: 36 | 37 | The overview of the system is more fully explained in [Speech Bubbles Overview](explain_screens.md) 38 | 39 | The styling and settings for frame backgrounds are explained in [Frames & Styles](explain_frames.md) 40 | 41 | The parameters and usage guidelines are explained in [Using the Speech Bubbles system](explain_usage.md) 42 | 43 | ### Please note: 44 | 45 | The way this approach works might not be suitable for complete beginners. It is a basic platform on which to build a system that might grow to include many other styles (such as thought clouds, jagged exclaimation bubbles, showing character names etc). As such it will likely require some knowledge of Ren'Py in order to extend it to your particular needs. 46 | 47 | Though I have tried to explain it as simply as possible, I will not be available to help extend it unless under a paid contract. 48 | Basically, if you want it to do more, you are expected to know enough Ren'Py to handle that yourself (or consider paying someone) 49 | 50 | 51 | ### Credits 52 | 53 | Background: https://pixabay.com/illustrations/landscape-nature-summer-forest-4026168/ 54 | 55 | Sprites: 56 | - Kaori: https://www.deviantart.com/kid-blue/art/female-teacher-1-274513797 57 | - Amber: https://www.deviantart.com/kid-blue/art/girl-in-uniform-1-274040211 58 | -------------------------------------------------------------------------------- /explain_usage.md: -------------------------------------------------------------------------------- 1 | # Using the Speech Bubbles system 2 | 3 | So, you've read through and mostly understand how the system works, perhaps even tweaked the styles or extended them to add in your own thought bubbles or other designs. 4 | 5 | Now you want to get your characters using the system. 6 | 7 | First we need to tell each Character that it should be using the amended `what_style` and the new `screen`. 8 | ```py 9 | define speech_bubble_k = Character( 10 | "Kaori", 11 | screen="bubble_say", 12 | what_style="bubble_speech_text") 13 | ``` 14 | Those are the name of the parent screen and the style for the text. You can also add in default settings for other what_ values as well as values passed to the screen (prefixed by `show_`). More details on that after covering the available settings. 15 | 16 | ## Args and Keyword Args 17 | 18 | Each line of dialogue can pass arguments into the system to control how it is shown on the screen. There are default values for any not supplied, just so we do not have to include every setting on every dialogue line. These can be found (and adjusted) in the `say_arguments_callback` function. 19 | 20 | The most commonly used settings can be supplied as positional arguments. 21 | ```py 22 | speech_bubble_k "Hello World" (650, 300, "righttop") 23 | ``` 24 | No need to tell it what each one is, it just knows that three optional arguments can be passed and, if they are passed, they relate to `(xpos, ypos, tail)`. 25 | 26 | We could adjust the position and use default tail ("baseright") by just passing the first two. 27 | 28 | ```py 29 | speech_bubble_k "Hello World" (650, 300) 30 | ``` 31 | 32 | These values can also be passed by full keyword, along with the others here: 33 | ### Position 34 | ```py 35 | # change the xpos of the fixed 36 | e "..." (show_xpos = 140) 37 | 38 | # change the ypos of the fixed 39 | e "..." (show_ypos = 100) 40 | 41 | # change the combined pos of the fixed 42 | e "..." (show_pos = (140, 100)) 43 | ``` 44 | ### Size 45 | ```py 46 | # change the maximum width of the frame (allow it to expand to this width) 47 | e "..." (show_xmax = 440) 48 | 49 | # change the minimum width 50 | e "..." (show_xmin = 440) 51 | # note: The frame itself has an xminimum (so the background pic does not shrink and look odd) 52 | # This setting is more useful when it matches show_xmax in order to use things like extend 53 | # without the bubble changing size as the contained dialogue changes 54 | ``` 55 | ### Style 56 | ```py 57 | # change the style prefix 58 | e "..." (show_type="bubble_thought") 59 | # this will use style bubble_thought_{show_tail}_frame 60 | 61 | # change the tail style (can be passed as the third argument) 62 | e "..." (show_tail="baseleft") 63 | # this will use style {show_type}_baseleft_frame 64 | ``` 65 | ### Retain 66 | 67 | This setting just uses an integer to state how many times this line is reshown for. 68 | ```py 69 | # tell it to retain for one extra line 70 | e "..." (show_retain = 1) 71 | l "..." # still showing the e line as well 72 | l "..." # now the e line is hidden 73 | ``` 74 | You can of course pass multiple settings to the same line, can combine arguments and keywords. 75 | ```py 76 | e "..." (480, 320, show_type="bubble_thought", show_xmax=510, retain=4) 77 | ``` 78 | You can also set defaults for the keywords in your Characters. (inline values will still override these) 79 | ```py 80 | define k_think = Character( 81 | "Kaori", 82 | screen="bubble_say", 83 | what_style="bubble_speech_text", 84 | show_type="bubble_thought", 85 | show_pos=(300,300)) 86 | ``` 87 | Hopefully the system is straight forward enough to extend to meet your needs. 88 | 89 | 90 | 91 | ### Important Reading: 92 | 93 | Back to the main page [Home](README.md) 94 | 95 | The overview of the system is more fully explained in [Speech Bubbles Overview](explain_screens.md) 96 | 97 | The styling and settings for frame backgrounds are explained in [Frames & Styles](explain_frames.md) 98 | -------------------------------------------------------------------------------- /explain_screens.md: -------------------------------------------------------------------------------- 1 | # Speech Bubbles Overview 2 | 3 | So, let's start with the amended `screen say` as this is the first step in the process when a line of dialogue is shown. 4 | 5 | ```py 6 | screen bubble_say(who, what, **kwargs): 7 | 8 | # This is the main container screen 9 | # It uses child screens to display each bubble, one for 10 | # each dialogue that is set to be shown at this time 11 | 12 | # A tuple of information about the current dialogue line 13 | $ current_dialogue = ( 14 | renpy.get_filename_line(), 15 | kwargs.pop('retain', 0), 16 | who, 17 | what, 18 | kwargs ) 19 | 20 | # First the older (retained) dialogues 21 | for old_dialogue in retained_dialogues: 22 | 23 | $ old_who, old_what, old_kwargs = old_dialogue[2:] 24 | 25 | use bubble_subscreen(old_who, **old_kwargs): 26 | 27 | # Just standard Text here, no id needed 28 | # Styled to match how the 'retained' line was shown 29 | add Text(old_what, **old_kwargs['what_style']) 30 | 31 | # The current line (including the "what" id) 32 | use bubble_subscreen(who, **kwargs): 33 | 34 | text what id "what" 35 | 36 | # When this screen is hidden (at the next interaction from the player) 37 | # we pass the tuple of current dialogue info to the function 38 | on "hide": 39 | 40 | action Function(hide_dialogue, current_dialogue) 41 | ``` 42 | First you will notice that we build a sequence of information about the currently shown line. This includes: 43 | ```py 44 | renpy.get_filename_line(), 45 | ``` 46 | So we can check the line is real and not just a pre-game prediction 47 | ```py 48 | kwargs.pop('retain', 0), 49 | ``` 50 | Easier to pull this one out of the kwargs and store it separately as we reference it quite often. It specifies how many additional dialogues this particular line is meant to remain showing for. 51 | ```py 52 | who, 53 | what, 54 | kwargs 55 | ``` 56 | Usual stuff so we have much of the information to redisplay this line should we want to. 57 | 58 | Next the screen iterates through any previous dialogues that were set to retain and displays them by transcluding a simple text widget to the subscreen. That widget is styled from retained styles that we stored when the line was first displayed. It is just a simple text as we cannot have more than one widget with the id "what" in the screen. 59 | 60 | Next we display the current line by transcluding the normal text widget (with the id "what") into another copy of the subscreen. 61 | 62 | Once the line has been shown, we hook into the `on "hide"` event of the screen in order to pass all the information we have about the current dialogue to a named Python function. This means, when the screen is hidden, we know that function is going to run once with all the details we collected about the current dialogue passed to it. 63 | 64 | ## The subscreen 65 | 66 | Just a basic screen that takes the sayer name (just in case you want to display it) and the keyword arguments that control the display. The simple version here just contains a fixed (for positioning) which contains a frame (for background styling) which contains the transcluded content. 67 | 68 | There are also a few lines which add a toggle-able white cross to help highlight the passed position. 69 | 70 | ## The Python function 71 | 72 | ```py 73 | def hide_dialogue(current_dialogue=None, screen="bubble_say"): 74 | ``` 75 | This is our Python function that we call after the dialogue has been seen. We pass it the contents of the info tuple that we created in the screen along with the name of the screen 76 | ```py 77 | global retained_dialogues 78 | 79 | next_retained_dialogues = [] 80 | 81 | for k in retained_dialogues: 82 | 83 | k[1] -= 1 84 | 85 | if k[1] > 0: 86 | 87 | next_retained_dialogues.append(k) 88 | 89 | retained_dialogues = next_retained_dialogues 90 | ``` 91 | First we look at the existing retained dialogues, decrease their `retain` value by one, check if they should be shown again and rebuild the global list with only those that should be reshown. 92 | ```py 93 | if current_dialogue and not current_dialogue[0][0].endswith('.rpym'): 94 | 95 | if current_dialogue[1] > 0: 96 | 97 | # retain this one, so add it to the global 98 | 99 | # first mirror the style applied to the text and add it 100 | # to the kwargs 101 | 102 | widget = renpy.get_widget(screen, 'what') 103 | if widget: 104 | widget_style = { 105 | k : getattr(widget.style, k) 106 | for k in dir(widget.style) 107 | if k in retained_styles 108 | } 109 | current_dialogue[-1]['what_style'] = widget_style 110 | 111 | retained_dialogues.append(list(current_dialogue)) 112 | ``` 113 | Then we check our current dialogue, firstly making sure it is not just being predicted. 114 | 115 | If the current dialogue is set to `retain` we duplicate the styles it uses and add it to the global retained_dialogues list. 116 | To retain the styles we simply find the widget (we passed in the screen name and know it uses the id "what") and then read through the `.style` object to pull out the names and values we want (text properties and positional properties). 117 | 118 | So, basically just check what is going to be shown again and build a list containing all the information we need for each line. 119 | 120 | ## The Say Arguments Callback 121 | 122 | We *could* dispense with this part and solely pass all variable values using keywords. We could add `(show_xpos=100, show_ypos=200)` on a line to tell it to use those values. It's easier just putting the values though `(100, 200)`, especially for those values that change most often. 123 | 124 | We use `config.say_arguments_callback` to do this, re-interpreting the positional arguments in an output keyword dictionary. 125 | 126 | #### You should note that default values for all the settings are created here. 127 | 128 | 129 | 130 | ### Navigation: 131 | 132 | Back to the main page [Home](README.md) 133 | 134 | The styling and settings for frame backgrounds are explained in [Frames & Styles](explain_frames.md) 135 | 136 | The parameters and usage guidelines are explained in [Using the Speech Bubbles system](explain_usage.md) 137 | -------------------------------------------------------------------------------- /explain_frames.md: -------------------------------------------------------------------------------- 1 | # Frames & Styles 2 | 3 | The bulk of the system relies on subscreens that include a frame which contains the dialogue text. For longer text strings we want the frame background to get larger. For shorter text strings we want that background to shrink. Other than specifying a minimum size, we let the shown background expand to fit the text, forcing it to use linebreaks at the default maximum width setting or at a different size by passing a value. 4 | 5 | First though we need to make sure the text itself uses some specific style settings. We need to make sure our bubble text does not use the default settings applied through various gui values. 6 | 7 | ## The Text Style 8 | 9 | The standard Ren'Py dialogue styles are a bit more convoluted than basic styles as the system uses named widgets in the screen and then uses keywords that include those names to alter any default styles applied to those named widgets. 10 | 11 | The widget in the say screen that we are interested in is a text widget with the id "what". After inheriting the usual text styles, that widget then takes styles passed using that keyword plus an underscore. 12 | 13 | A Character can have parameters beginning with what_ and dialogue can have keywords beginning with what_ 14 | ```py 15 | define e = Character("Eileen", what_color="#D89") 16 | # or 17 | label start: 18 | e "Hello World" (what_color="#D45") 19 | ``` 20 | 21 | There are two main properties that we want to alter. We want the `xsize` and `align` values to be `None` and `(0, 0)` respectively. 22 | 23 | Though there are a number of ways we could achieve that, the simplest is just to write a base style and tell all our Characters who use the bubbles to use that style. We can then tweak other `what_` settings as desired. 24 | ```py 25 | style bubble_speech_text: 26 | xsize None # needed - otherwise it uses a gui setting 27 | align (0,0) # also likely needed 28 | 29 | # You could include your standard font specific stuff 30 | color "#F00" 31 | # font "" 32 | kerning -1.0 33 | size 26 34 | bold True 35 | # etc etc 36 | 37 | ## Define the Character, set the screen they use, tell them which style to use for the "what" widget 38 | define speech_bubble_k = Character( 39 | "Kaori", 40 | screen="bubble_say", 41 | what_style="bubble_speech_text", 42 | what_color="#FDD") 43 | ``` 44 | These now make sure that the dialogue text does not try to take the screen width or sit at an offset. 45 | 46 | ## Styling Frames 47 | 48 | Now we have made sure our text is appearing in a suitable rectangle we can handle how our frames respond to that (variable size) area. 49 | 50 | At a basic level, every subscreen is just: 51 | ```py 52 | screen bubble_subscreen(who, **kwargs): 53 | 54 | style_prefix "bubble_speech_baseright" # This is actually passed through keywords 55 | 56 | fixed: 57 | 58 | pos (450, 320) # Our x, y position, also through keywords 59 | 60 | frame: 61 | 62 | text "The Text" # Actually transcluded from the parent screen 63 | ``` 64 | In order to get the `frame:` there to have a nice background (that expands to fit the contained text) we use a style beginning with the prefix and ending with `_frame` 65 | 66 | We can have lots of these styles, one for each style of bubble we use - set up specially to accomodate the stretching image used as the background. 67 | ```py 68 | style bubble_speech_baseright_frame: 69 | 70 | # our background picture 71 | background Frame( 72 | "images/speech_bubble.png", 73 | left = Borders(32, 33, 88, 80) 74 | ) 75 | 76 | # These are the distance between the text area and frame outer edge 77 | left_padding 24 78 | top_padding 22 79 | right_padding 23 80 | bottom_padding 73 81 | # We *could* do all that in one line with 82 | # padding (24, 22, 23, 73) # (left, top, right, base) 83 | 84 | minimum (121, 114) 85 | 86 | # Now the anchor (the pixel of this widget to place at the stated pos) 87 | # This should generally reflect where the end of the tail lies 88 | anchor (1.0, 1.0) 89 | # You could add a slight offset if wanted (so show_pos is on the tail) 90 | offset (12, 7) 91 | ``` 92 | So, the `frame:` that the `_frame` style applies to has a background which is a `Frame`... hmmm... 93 | 94 | The `frame:` is a screen language container which naturally shrinks to fit its contents. 95 | 96 | The `Frame` is a special type of displayable which can stretch or tile parts of itself to fit the size allocated to it. 97 | 98 | Let's take it one part at a time 99 | 100 | ## Our Background Image 101 | 102 | ![Speech Bubble Background Image](game/images/speech_bubble.png) 103 | 104 | This image will need to stretch or tile in certain places to match the size it needs. We can picture the areas that stretch, the fixed areas and the area allocated to the contained text in the following image. 105 | 106 | ![Speech Bubble Areas](explain_images/speech_bubble_margins.png) 107 | 108 | The red shaded area is the parts that stretch. See that we have not let the curved corners stretch and have not let any part of the tail stretch. 109 | 110 | The blue shaded area represents the part where text can be placed. Note that it does not have to match the red stretchable parts, it can extend into the unstretched areas if wanted. As that part expands it causes the full Frame to expand and thus adjust the sizes of the stretched parts. 111 | 112 | ## The background Frame 113 | ```py 114 | background Frame( 115 | "images/speech_bubble.png", 116 | left = Borders(32, 33, 88, 80) 117 | ) 118 | ``` 119 | The first parameter tells it the image we use. This one is our actual file without any transformations. 120 | 121 | The `left = ` part specifies a Borders object which is just a way of passing the left, top, right and base values. If you prefer to just set the values to the Frame, feel free. 122 | 123 | You can see the values reflected in this image and can see that they relate to the distance from each edge inwards until they reach the red stretchable part. 124 | 125 | ![Speech Bubble Frame Values](explain_images/speech_bubble_frame_values.png) 126 | 127 | ## The frame: padding 128 | ```py 129 | # These are the distance between the text area and frame outer edge 130 | left_padding 24 131 | top_padding 22 132 | right_padding 23 133 | bottom_padding 73 134 | # We *could* do all that in one line with 135 | # padding (24, 22, 23, 73) # (left, top, right, base) 136 | ``` 137 | As the comments imply, these are paddings applied to the screen language `frame:` container and are used to tell the frame how much larger than its content it actually is. 138 | 139 | You can see the values reflected in this image and can see that they relate to the distance from each edge inwards until they reach the blue part designated for the text content. 140 | 141 | ![Speech Bubble frame: padding](explain_images/speech_bubble_padding_values.png) 142 | 143 | ## Other style values 144 | ```py 145 | minimum (121, 114) 146 | ``` 147 | This setting just makes sure our Speech Bubble does not shrink too small and look strange. 148 | 149 | ```py 150 | # Now the anchor (the pixel of this widget to place at the stated pos) 151 | # This should generally reflect where the end of the tail lies 152 | anchor (1.0, 1.0) 153 | # You could add a slight offset if wanted (so show_pos is on the tail) 154 | offset (12, 7) 155 | ``` 156 | These allow us to specify one corner of the `frame:` as our anchor and then fine tune that by a number of pixels so the point of the tail sits directly on the pos we passed for that line of dialogue. 157 | 158 | 159 | That style is basically repeated seven times to cover the tail being in eight different places. 160 | 161 | 162 | ### Navigation: 163 | 164 | Back to the main page [Home](README.md) 165 | 166 | The overview of the system is more fully explained in [Speech Bubbles Overview](explain_screens.md) 167 | 168 | The parameters and usage guidelines are explained in [Using the Speech Bubbles system](explain_usage.md) 169 | -------------------------------------------------------------------------------- /game/speech_bubbles.rpy: -------------------------------------------------------------------------------- 1 | ## To view example in your game add 2 | ## 3 | ## call speech_bubble_example 4 | ## 5 | ## somewhere in your running label script 6 | 7 | 8 | 9 | ########################################### 10 | # # 11 | # To use in your game # 12 | # # 13 | # Create your characters and set them # 14 | # to use the speech bubble screen. # 15 | # The example at the end has a couple # 16 | # of characters to see how. # 17 | # Maybe delete the example bits (at # 18 | # the end of this file) once happy # 19 | # # 20 | ########################################### 21 | 22 | 23 | 24 | 25 | ################### 26 | # # 27 | # Styles # 28 | # # 29 | ################### 30 | 31 | # 32 | # https://github.com/RenpyRemix/speech-bubbles/blob/master/explain_frames.md 33 | # 34 | 35 | # Characters who use the speech bubbles will need their what_style pointed 36 | # at a style similar to this 37 | # 38 | # The first two settings are quite important. They let the bubbles 39 | # shrink to the text bounds and position the first letter 40 | 41 | style bubble_speech_text: 42 | xsize None # needed - otherwise it uses a gui setting 43 | align (0,0) # also likely needed 44 | 45 | # just standard font specific stuff 46 | color "#F00" 47 | # font "" 48 | kerning -1.0 49 | size 26 50 | bold True 51 | 52 | 53 | # A rotated version of the bubble image 54 | image speech_bubble_90: 55 | # using contains so it rotates before crop 56 | contains: 57 | "images/speech_bubble.png" 58 | rotate_pad False 59 | transform_anchor True 60 | anchor (0.0, 1.0) 61 | rotate 90.0 62 | # crop (as rotation increased the size) 63 | crop (0, 0, 135, 130) 64 | 65 | 66 | # Styles for the different frames 67 | # Each direction the tail points uses different values so has its own style 68 | 69 | style bubble_speech_baseright_frame: 70 | 71 | # our background picture 72 | background Frame( 73 | "images/speech_bubble.png", 74 | left = Borders(32, 33, 88, 80) 75 | ) 76 | 77 | # These are the distance between the text area and frame outer edge 78 | left_padding 24 79 | top_padding 22 80 | right_padding 23 81 | bottom_padding 73 82 | # We *could* do all that in one line with 83 | # padding (24, 22, 23, 73) # (left, top, right, base) 84 | 85 | minimum (121, 114) 86 | 87 | # Now the anchor (the pixel of this widget to place at the stated pos) 88 | # This should generally reflect where the end of the tail lies 89 | anchor (1.0, 1.0) 90 | # You could add a slight offset if wanted (so show_pos is on the tail) 91 | offset (12, 7) 92 | 93 | 94 | style bubble_speech_baseleft_frame: 95 | 96 | # our background picture 97 | background Frame( 98 | Transform("images/speech_bubble.png", xzoom=-1), 99 | left = Borders(88, 33, 32, 80) 100 | ) 101 | 102 | # The distance between content and outer edge (left, top, right, base) 103 | padding (23, 22, 24, 73) 104 | 105 | minimum (121, 114) 106 | anchor (0.0, 1.0) 107 | offset (-12, 7) 108 | 109 | 110 | style bubble_speech_topright_frame: 111 | 112 | # our background picture 113 | background Frame( 114 | Transform("images/speech_bubble.png", yzoom=-1), 115 | left = Borders(32, 80, 88, 33) 116 | ) 117 | 118 | # The distance between content and outer edge (left, top, right, base) 119 | padding (24, 73, 23, 22) 120 | 121 | minimum (121, 114) 122 | anchor (1.0, 0.0) 123 | offset (12, -7) 124 | 125 | 126 | style bubble_speech_topleft_frame: 127 | 128 | # our background picture 129 | background Frame( 130 | Transform("images/speech_bubble.png", zoom=-1), 131 | left = Borders(88, 80, 32, 33) 132 | ) 133 | 134 | # The distance between content and outer edge (left, top, right, base) 135 | padding (23, 73, 24, 22) 136 | 137 | minimum (121, 114) 138 | anchor (0.0, 0.0) 139 | offset (-12, -7) 140 | 141 | 142 | 143 | style bubble_speech_leftbase_frame: 144 | 145 | # our background picture 146 | background Frame( 147 | "speech_bubble_90", 148 | left = Borders(80, 32, 33, 88) 149 | ) 150 | 151 | # The distance between content and outer edge (left, top, right, base) 152 | padding (73, 24, 22, 23) 153 | 154 | minimum (114, 121) 155 | anchor (0.0, 1.0) 156 | offset (-7, 12) 157 | 158 | 159 | style bubble_speech_lefttop_frame: 160 | 161 | # our background picture 162 | background Frame( 163 | Transform("speech_bubble_90", yzoom=-1), 164 | left = Borders(80, 88, 33, 32) 165 | ) 166 | 167 | # The distance between content and outer edge (left, top, right, base) 168 | padding (73, 23, 22, 24) 169 | 170 | minimum (114, 121) 171 | anchor (0.0, 0.0) 172 | offset (-7, -12) 173 | 174 | 175 | style bubble_speech_righttop_frame: 176 | 177 | # our background picture 178 | background Frame( 179 | Transform("speech_bubble_90", zoom=-1), 180 | left = Borders(33, 88, 80, 32) 181 | ) 182 | 183 | # The distance between content and outer edge (left, top, right, base) 184 | padding (22, 23, 73, 24) 185 | 186 | minimum (114, 121) 187 | anchor (1.0, 0.0) 188 | offset (7, -12) 189 | 190 | 191 | style bubble_speech_rightbase_frame: 192 | 193 | # our background picture 194 | background Frame( 195 | Transform("speech_bubble_90", xzoom=-1), 196 | left = Borders(33, 32, 80, 88) 197 | ) 198 | 199 | # The distance between content and outer edge (left, top, right, base) 200 | padding (22, 24, 73, 23) 201 | 202 | minimum (114, 121) 203 | anchor (1.0, 1.0) 204 | offset (7, 12) 205 | 206 | # Just one to test we can pass (show_type="bubble_thought") and have it 207 | # pick up a different style 208 | style bubble_thought_baseright_frame is bubble_speech_baseright_frame: 209 | alt "Thinking" 210 | 211 | 212 | 213 | #################### 214 | # # 215 | # Screens # 216 | # & # 217 | # Python # 218 | # # 219 | #################### 220 | # 221 | # You probably will not need to edit this part 222 | # 223 | # Except maybe the callback if you want the default 224 | # values to be different 225 | 226 | # 227 | # https://github.com/RenpyRemix/speech-bubbles/blob/master/explain_screens.md 228 | # 229 | 230 | default retained_dialogues = [] 231 | 232 | init python: 233 | 234 | # This is just a list of style names that we want to remember for when 235 | # multiple dialogues (bubbles) appear at once 236 | retained_styles = ( 237 | renpy.screenlang.text_property_names 238 | + renpy.screenlang.position_property_names) 239 | 240 | 241 | #This function gets called after each dialogue is shown 242 | # It is responsible for calculating what will be shown with the 243 | # next dialogue 244 | def hide_dialogue(current_dialogue=None, screen="bubble_say"): 245 | """ 246 | Decrease all `retain` values by one and rebuild global 247 | shown_dialogues only from those set to still be shown 248 | 249 | Then add the current dialogue if it has been set to retain 250 | """ 251 | global retained_dialogues 252 | 253 | next_retained_dialogues = [] 254 | 255 | for k in retained_dialogues: 256 | 257 | k[1] -= 1 258 | 259 | if k[1] > 0: 260 | 261 | next_retained_dialogues.append(k) 262 | 263 | retained_dialogues = next_retained_dialogues 264 | 265 | if current_dialogue and not current_dialogue[0][0].endswith('.rpym'): 266 | 267 | # This is where you could add a feature so one dialogue could 268 | # alter setting for a retained dialogue (such as move it) 269 | 270 | if current_dialogue[1] > 0: 271 | 272 | # retain this one, so add it to the global 273 | 274 | # first mirror the style applied to the text and add it 275 | # to the kwargs 276 | 277 | widget = renpy.get_widget(screen, 'what') 278 | if widget: 279 | widget_style = { 280 | k : getattr(widget.style, k) 281 | for k in dir(widget.style) 282 | if k in retained_styles 283 | } 284 | current_dialogue[-1]['what_style'] = widget_style 285 | 286 | retained_dialogues.append(list(current_dialogue)) 287 | 288 | 289 | # We use this callback so we can use simple arguments rather than 290 | # keywords each time 291 | def say_arguments_callback(who, *args, **kwargs): 292 | 293 | if hasattr(who, "screen") and who.screen == "bubble_say": 294 | 295 | # First we convert any passed args into kwargs 296 | # (if the named kwarg has not been set) 297 | # 298 | # Args are a utility to pass common values quickly 299 | # (xpos, ypos, tail) 300 | 301 | if not "show_pos" in kwargs: 302 | # Default POS as a list Not tuple 303 | kwargs['show_pos'] = list(kwargs.get('show_pos', (800, 400))) 304 | 305 | if args and args[0] is not None: 306 | kwargs['show_pos'][0] = args[0] 307 | if len(args) > 1 and args[1] is not None: 308 | kwargs['show_pos'][1] = args[1] 309 | 310 | if not "show_tail" in kwargs: 311 | # Default tail style 312 | kwargs['show_tail'] = 'baseright' 313 | 314 | if len(args) > 2 and args[2] is not None: 315 | kwargs['show_tail'] = args[2] 316 | 317 | # You could amend to pass others as args too 318 | 319 | kwargs['show_type'] = kwargs.get('show_type', "bubble_speech") 320 | kwargs['show_xmax'] = kwargs.get('show_xmax', 320) 321 | #kwargs['show_xmin'] = kwargs.get('show_xmin', 0) 322 | 323 | kwargs['interact'] = kwargs.get('interact', True) 324 | 325 | return (), kwargs 326 | 327 | config.say_arguments_callback = say_arguments_callback 328 | 329 | 330 | screen bubble_say(who, what, **kwargs): 331 | 332 | # This is the main container screen 333 | # It uses child screens to display each bubble, one for 334 | # each dialogue that is set to be shown at this time 335 | 336 | # A tuple of information about the current dialogue line 337 | $ current_dialogue = ( 338 | renpy.get_filename_line(), 339 | kwargs.pop('retain', 0), 340 | who, 341 | what, 342 | kwargs ) 343 | 344 | # First the older (retained) dialogues 345 | for old_dialogue in retained_dialogues: 346 | 347 | $ old_who, old_what, old_kwargs = old_dialogue[2:] 348 | 349 | use bubble_subscreen(old_who, **old_kwargs): 350 | 351 | # Just standard Text here, no id needed 352 | # Styled to match how the 'retained' line was shown 353 | add Text(old_what, **old_kwargs['what_style']) 354 | 355 | # The current line (including the "what" id) 356 | use bubble_subscreen(who, **kwargs): 357 | 358 | text what id "what" 359 | 360 | # When this screen is hidden (at the next interaction from the player) 361 | # we pass the tuple of current dialogue info to the function 362 | on "hide": 363 | 364 | action Function(hide_dialogue, current_dialogue) 365 | 366 | 367 | 368 | screen bubble_subscreen(who, **kwargs): 369 | 370 | # Each line of dialogue is shown in one of these subscreens 371 | # To use the correct Frame style we can pass keywords 372 | # show_type = the prefix part of the style 373 | # show_tail = the suffix part of the style 374 | # 375 | # The prefix_suffix then can use _frame as a style name 376 | 377 | style_prefix "{}_{}".format(kwargs['type'], kwargs['tail']) 378 | 379 | fixed: 380 | 381 | pos kwargs['pos'] 382 | 383 | frame: 384 | 385 | xmaximum kwargs['xmax'] 386 | 387 | # set the min width as show_xmin if passed else use default 388 | # (do not set smaller than the frame settings though) 389 | if "xmin" in kwargs: 390 | xminimum kwargs['xmin'] 391 | 392 | # this single word tells this screen where to use 393 | # the indented widgets/attributes (the text part) 394 | transclude 395 | 396 | if check_bubble_positions: 397 | add Solid('#FFF', xysize=(2, 40), anchor=(1, 20)) 398 | add Solid('#FFF', xysize=(40, 2), anchor=(20, 1)) 399 | 400 | # Utility settings to draw a white cross at the specified 'show_pos' 401 | # Maybe useful to setup Frame styles or fine tune positions 402 | default check_bubble_positions = False 403 | 404 | # On/Off toggle of the white cross using [Alt] + [c] keys together 405 | init python: 406 | 407 | if config.developer: 408 | 409 | config.underlay.append( 410 | renpy.Keymap( 411 | alt_K_c = lambda: renpy.run( 412 | ToggleVariable("check_bubble_positions")))) 413 | 414 | 415 | 416 | 417 | 418 | ########################################### 419 | # # 420 | # You can delete the example characters # 421 | # and label below once happy # 422 | # # 423 | ########################################### 424 | 425 | 426 | 427 | ##################### 428 | # # 429 | # Characters # 430 | # & # 431 | # Sample # 432 | # # 433 | ##################### 434 | 435 | 436 | # 437 | # https://github.com/RenpyRemix/speech-bubbles/blob/master/explain_usage.md 438 | # 439 | 440 | 441 | 442 | 443 | define speech_bubble_a = Character( 444 | "Amber", 445 | screen="bubble_say", 446 | what_color="#282", 447 | what_style="bubble_speech_text", 448 | show_tail="leftbase") 449 | 450 | define speech_bubble_k = Character( 451 | "Kaori", 452 | screen="bubble_say", 453 | who_color="#FDD", 454 | what_style="bubble_speech_text") 455 | 456 | 457 | 458 | label speech_bubble_example: 459 | 460 | $ quick_menu = False 461 | 462 | window hide 463 | 464 | scene expression "#777" 465 | 466 | speech_bubble_k """A few lines showing the 467 | speech bubble system in action...""" (950, 300, "righttop") 468 | 469 | speech_bubble_a "Style default (baseright) 470 | \nYou can press Alt+C to show the used pos" (640, 320, show_xmax=500, what_color="#888") 471 | 472 | speech_bubble_k "Style baseleft" (640, 320, "baseleft") 473 | 474 | speech_bubble_a "Style leftbase" (640, 320, "leftbase") 475 | 476 | speech_bubble_k "Style lefttop" (640, 320, "lefttop") 477 | 478 | speech_bubble_a "Style topleft" (640, 320, "topleft") 479 | 480 | speech_bubble_k "Style topright" (640, 320, "topright") 481 | 482 | speech_bubble_a "Style righttop" (640, 320, "righttop") 483 | 484 | speech_bubble_k "Style rightbase" (640, 320, "rightbase") 485 | 486 | speech_bubble_a "... and back to the game" ( 487 | 640, 320, show_type="bubble_thought") 488 | 489 | return 490 | --------------------------------------------------------------------------------