Porting an unportable Ren'Py game to Android
Ren'Py projets rarely start with a clear roadmap in mind about features or even distribution platforms.
More often, they slowly build as you learn more about coding and the engine until you wake up one morning and suddenly decide you should have thought about this or that from day one. But you're already hundred of carefree lines deep in the projet.
That's how we felt when we decide our game Love Tryouts - totally computer-oriented and barely accessible - needed :
- to be ported to Android ;
- more accessibility options to better account for dyslexia, dyschromatopia, dyscalculia and visual impairment.
Problem: the game is now 400,000 words and 110 screens long.

If you're in the same situation, then this post aims to share with you a few (humble) solutions I set up in the last weeks to allow for such a mid-development shift... and spare you some trial and error.
First, I'll focus on Android.
1. Setting up Ren'Py
Most of what you need to know is covered by this section of Ren'Py's documentation. You need to install the packages that will allow you to test and build your game for Android.
Beware of the emulator though. It's great to test and change your code on the fly, but you have to run your game on a real Android device :
- first, to test in situ usability and legibility, which may vastly differ from a small window/mouse combination ;
- second, because some bugs only happen on a real Android device. For instance, my device can't handle image paths starting with ".../" while they work just fine on the provided emulator.
Note that the emulator handles the console and autoreload - not Android.
Once you're all set, you can serenely tackle the reshaping of your game.
2. Things Ren'Py does on its own
If your game sticks to Ren'Py's normal flow (dialogue, characters, images, choice menus) and quite standard GUI then you really don't have much to worry about.
Ren'Py wonderfully handles small screens, notably through its variants and GUI systems.
To summarize, Ren'Py selects variants based on your device: "touch" if you use a touchscreen, "small" if you play on a small screen, etc. You can easily check the active variants by testing renpy.variant(name).
Several variant can be used at once. For instance, if a player uses a small touchscreen then renpy.variant("small") and renpy.variant("touch") both return True.
Furthermore, Ren'Py automatically changes some style properties through its GUI system.
For instance, in gui.rpy we have the following code:
define gui.text_size = 22
init python:
# (...)
@gui.variant
def small():
gui.text_size = 30
If the "small" variant is set at game start, then the standard size of text is set to 30.
Ren'Py handles most things on its own, but you might have to tweak some of the lines in gui.rpy in the small function to adapt them to your game.
Notably, on most devices the virtual keyboard will cover the input screen :

(the emulator shows us where the keyboard will be)
If you want to avoid that, we chose to add two lines to the input() screen in screens.rpy to align the dialogue top :
screen input(prompt):
style_prefix "input"
window:
if renpy.variant("android"):
yalign 0.0
Which gives us :

I don't have a physical home button on my device. You have to swipe from the bottom to access the home and return buttons PyTom refers to in the documentation. The Back button from the quick_menu can be used for rollback but I chose to bring back the Quit button in the navigation menu in screens.rpy to allow for an easier exit :
if renpy.variant("pc") or renpy.variant("android"):
## The quit button is banned on iOS and unnecessary on Android and
## Web.
textbutton _("Quitter") action Quit(confirm=not main_menu)
(pardon my french, but the game is originally written and coded in french)
(the _() are here to mark the string for translation btw)
Note that Ren'Py autmatically resizes your displayables, transitions, etc. to preserve their relative position on the screen.
Other than that and a little GUI resizing (check your navigation menu!), you're done as long as you don't stray from the standard Ren'Py interface and don't use any custom screen.
Well, of course if you do... your journey only began.
3. Usual screen issues
For the record, LT's game page casually mentions 27 minigames. I guess I counted them at some point.
Add to that a few multi-screen minigames, screens that don't count as minigames (point and click intermissions), a gallery (not using Ren'Py built-in system... it was added right after we were done I guess), an agenda, several office software simulators, a website displayed on a phone (which will sonn become a phone in a phone) etc. etc. etc. Overall, a motley collection of 113 screens with varying gameplay, layout and engin proficency from the developpers during the years of development.

Here they are again. Well, not all of them.
If you have the same kind of things to deal with, well there's no shortcut : you have to test each and every screen on emulator, then (once you've improved them) on Android. Just use a test script and call all your screens one after another.
What you have to check foremost is :
- handling the resizing of the screen (by increasing the size of text and buttons), not only for legibility but also to account for touchscreen imprecision. Note that some (and by that I don't mean most) drag and drops might be quite impractical on a touch screen.
- any keyboard-controlled screen (arrows, tab, etc.) to offer an alternative ;
- any text input : first because the keyboard will cover two thirds of your screen, second because most times input can't be validated
- any hover mechanism, especially on imagebuttons. If you put an imagebutton, it's probably because you expect something for the hover whether it'd be user-friendliness, to hint at gameplay or ot provide any other kind of information. Forget it, because there's no hover on android. Imagebuttons only show their hover side when you click on them. You can click on them and move your finger before their action triggers, but nobody will actually do that.
There are solutions though.
I will now give you some examples of how I (probably imperfectly) handled these issues on our screens.
4. Upsizing
Needless to say, your screen always has to be resized up on small screens since Ren'Py will automatically shrink it. Moreover, your buttons need to be sized up because touchscreen in inherently inaccurate. Repeated failure to hit a minigame's buttons can easily render it unpleasant at best and unplayable at worst.
You can use the stardard styles defined by Ren'Py as much as possible, since they are already resized automatically. I personnally didn't use them that much since many of our screens already strayed from these styles, except when we mimicked built-in screens like say or choice. You're probably in the same situation.
You can easily replace a custom style by using a variant propery, for instance I used :
style pizz_app_text:
color "#000000"
size 15
text_align 0.5
xalign 0.5
style pizz_app_text:
variant "small"
size 22
![]() | ![]() |
... to easily resize text in a simple screen, in which elements were positionned by a vbox with enough space for the resized text.
I actually didn't use this much since I had to also account for a custom persistent.forcesmall variable that can't be processed through the variant property (more on that in my accessibility post).
I instead used a longer form (that doesn't retain properties from the first style, though you could do it through inheritance):
style contrat_text:
color "#000000"
font "tahoma.ttf"
size 15
style contratsmall_text:
color "#000000"
font "tahoma.ttf"
size 28
# In your screen you'll then have to use
if renpy.variant("small"):
style_prefix "contratsmall"
else:
style_prefix "contrat"
Obviously, even for a screen as simple as the one above, you'll have to change the screen a little more than just upsizing the text.
In the case of my pizza screen, the point was to resize the window at the root of my whole screen AND its vbox child :

There actually is another way to resize a root element and all its children.
This other screen had empty space around a central gameplay zone:
![]() | ![]() |
(you can right click the images to open them in a new tab for a larger display)
The code for the screen has a window, a grid inside, 25 fixed inside each with a button inside, themselves with an image inside. That's all.
By applyind a transform with a zoom to the window, I can resize it and all its children as if I just stretched it, improving its legibility and its touch-compatibility (since the buttons are also resized) with little to no effort:
![]() | ![]() |
This trick isn't meant for all screens though. Also, pay attention to elements that are not children to the transformed element, since they aren't stretched. You can merrily combine elements 'inside' and 'outside' of the transform, to keep buttons on the side or corners of the screen, for instance.
This other screen had three shelves to stock (on the right) and a large space between cardboard boxes and the shelves.
![]() | ![]() |
In order to make the most of this empty space and improve cardboard boxes (drags) handling, I first tried a horizontal stretch, which proved insufficient for both goals.
Thus I opted to remove a shelf in the minigame itself, which allowed me a vertical resize (using yzoom). These changes pushed the cardboard boxes outside the game screen, since the window was now larger than display.
I had to change the drag group's position accordingly which had repercussions on various effects in my drop callables which forced me to redefine coordinates based on variant-dependant variables:
![]() etc. | Part of the drop callable, before:![]() |
Part of the drop callable, after:![]() | End result:![]() |
If you don't want to slice up your screens like I did, note that you can create a variant from scratch:

Another trick with this screen that receives a list of strings (depicting the pages of a yet unstranslated book):

Since resizing caused issues with the layout, I added custom {#pg_br} tags to the strings and used the following function to automatically slice them before sending them to the screen (deux tags maximum per string):

These little shortcurts are really circumstancial though. Most times, you'll have to individually resize most of the elements in your screens, using renpy.variant conditions.
If you happen to need to crop resized text so that it doesn't overflow or wrap, the best way way to do so is to place it in an unscrollable/undraggable viewport :

5. Keyboard alternatives
Solutions to this problem are heavily dependent on gameplay. After serching for all instances of "key" in our screens, here are a few examples of solutions I picked:
- This minigame is a FFVI mockup using a tweaked version of Ren'Py standard choice menu so I chose to replace up/down arrows controls with a virtual SNES pad, placed on opposite sides of the screen for thumb usage that preserves screen visibility:
![]() | ![]() |

I also took the opportunity to refresh my code, written before the implementation of data actions (you can see it at the bottom), to replace it by a more efficient version that loops between the options.
The hotspots actually cover the top and bottom half of the pad and go about 50 px beyond to make up for touch inaccuracy.
- Another minigame had the player mash the spacebar to increase a variable. I replaced that with a dismiss. Be careful to place it BEHIND (thus, higher in your code) any other touchable element that needs to go over the dismiss:

The same minigame had the player press an arrow key on timed moments, which is replaced by buttons located on the bottom, left, top and right side of the screen. Remember to increase timer durations on these! Bear in mind it takes far longer to hit a button than to press a key with your fingers restin around them. It took me a few tests and builds before finding an equivalent reaction time by increasing the timer from 0.4 to 0.9 seconds:

Of course I had to spread the change to other timers in the screen:

- Some movements can be easily handled by moving buttons, positioned near the objet to move:
![]() | ![]() |
Nearrect can be a useful element to position button next to an object. When reactivity matters, fixed buttons are always preferable to moving ones.
- Remember to add alternatives options to your keyboard-controlled viewports:

6. Input management
This is one of the main problems on Android.
- For instance, this flagship screen of the game had the player cycling through input fields in order to update stocks:
![]() | ![]() |
* Disclaimer: this one was of the very first Ren'Py screens I wrote. The code is really messy.
This didn't work on Android at all.
First there was no tab key, then the virtual keyboard covered two thirds of the minigame preventing the use of the other mode of navigating through the stocks (using the textbuttons). I couldn't validate a field either, the return key of the virtual keyboard would have no effect, probably because of how the screen handled focus.
I was completely stuck with no way to move forward or rollback and had to force quit the game.
Of course my screen was quite ill-written but sometimes the news hits close to home THIS CAN HAPPEN TO YOUR PLAYERS TOO!
The upside of a screen that uses numeral values is that the workaround was quite straightforward:

Notice the incredibly oversized buttons meant to make it less strenuous. Well, it is a work minigame so it has to be strenuous somehow.
- Another minigame had the player guess words based on drawing. I chose to use choice buttons for this:
![]() | ![]() |
As before, there was no way to send an anwser because of screen architecture.

In the touch variant, every time the player sends an answer, the list of words is updated using the following function (triggered by the textbutton's second action):

This function:
- takes a list of standard work-related words
- adds the wrong answers submitted by opponents, depending on the drawing
- adds the right answer
- randomly shuffles the answers to randomize availability
- randomly adds, with an increased chance as the minigame goes on, the right answer on first position
- picks the first nine answers
- shuffles these nine answers to avoid having the right answer always first
- returns a list of available answers, which the screen variable is set to.
- In another minigame, the player has to fill five forms with data scattered among messy sticky notes - or as Gilou would put it:

Though the minigame is quite different from the stocks one, we still cycled through inputs with mouse or keyboard; a drag group was there to allow the player to move sticky notes around and reorganize the data. There was no drop function since the drags didn't actually do anything.
![]() | ![]() |
Legibility issues and keyboard overlap had me replacing this system with a more complex one:

- the forms are replaced by drop zones
- the sticky notes are no longer just visual elements, now dropping them on a zone removes them and copies the info they contain:
![]() | ![]() |
- to preserve the original gameplay, the code checks if the same kind of information is added twice (then the drag is snapped) but doesn't check the coherence of the data (meaning if it pertains to the same delivery).
![]() | ![]() |
- I also added two textbuttons to allow the player to botch the game or start over, which could be achieved in the original minigame by signing empty forms. The sticky notes drags are added to a list with every drop, which allows us to snap them all at once when the player starts over (way nicer than just resetting the screen). Here's the code for that:
# In our drop function, we add the drags to a screen list called drags_placed:
def mj_postits_collectinfo(drop, drags):
if drop is not None:
# (...)
l_dr_pl = renpy.get_screen_variable("drags_placed")
# (...)
l_dr_pl.append(drags[0])
# To hide the drags, I send them behind the drop zones and make them undraggable
drags[0].bottom()
drags[0].draggable=False
# This is mandatory to update the screen
renpy.restart_interaction()
return
# This is the function used to start over and snapp all the hidden drags
# Other parameters are screens varaibles used to process data etc.
# What matters is the last parameter, so I will only show what happens with this
def mj_postits_reshuffle(l_s_pi,l_h_pi,l_dr_pl):
# (...)
for p in l_dr_pl:
p.draggable = True
p.top()
p.snap(renpy.random.randint(620,1080),renpy.random.randint(120,600),1.0)
l_dr_pl.clear()
return
- Note that you can keep certain inputs, as long as you have enough space for the virtual keyboard and the input validation causes no issues:
![]() | ![]() |
(the phone shifts a bit upwards when we got to the password, just in case the keyboard is higher than previewed)
(I removed a few unnecessary text elements to gain as much space as possible)
(notice the player can easily 'escape' the input by using the brownish navigation buttons above)
Also:
![]() | ![]() |
(the player doesn't need to access the objets on the bottom or the textbuttons during this pase of the minigame)
(interestingly enough, the last line of code DOES WORK when you press return in the input field)
7. Replacing hovering
In some situations, hover can be used as a hint to the player, meant to draw their attention when they move their mouse across the screen.
To replace that on touchscreens, especially in an investigation phase, I added a twinkling effect that simulates hovering when the screen is shown:
Note that this will only happen thrice when the screen is shown, unless you remove the 3 on the repeat.
screen scr_indices_val(lbl, over):
# Never mind this condition, it's game-specific
if lbl.split(".")[1] not in pastref:
imagebutton:
auto "images/decor/acte3b/indices/"+over+"_%s.png"
focus_mask True
# This screen calls a label when the player clicks on the button. It is meant to be shown not called.
# Change that and provide a Return() action if that's not what you intend to do
action Call(lbl)
if renpy.variant("touch"):
# Please note this image is actually shown over the idle one. It doesn't replace it.
add "images/decor/acte3b/indices/"+over+"_hover.png":
at transform:
alpha 0.0
linear 0.2 alpha 1.0
linear 0.2 alpha 0.0
repeat 3
This concludes this little overview of the kind of solutions I used for our screens. I hope you can find anything useful to you.
Feel free to comment if you notice mistakes, have other suggestions or questions.
Best of luck with your game!
Get Love Tryouts
Love Tryouts
An adventure in corporate hell
| Status | In development |
| Authors | LEAR, Bosh'tet |
| Genre | Visual Novel, Adventure, Interactive Fiction |
| Tags | absurd, Comedy, Creepy, office, Romance, satire, Story Rich, Working Simulator |
| Languages | English, French |
| Accessibility | Color-blind friendly, Subtitles, High-contrast |
More posts
- Version 2.2 - accessibility, android and bugfixes7 days ago
- Version 2.2 - accessibilité, android et bugs9 days ago
- Portage Android d'un projet Ren'Py importable29 days ago
- Amazing results in translation this quarter!Jun 28, 2025
- Version 2.1.8.4 (en)May 29, 2025
- Version 2.1.8.4May 29, 2025
- Version 2.1 (minor patch)Feb 23, 2025































Leave a comment
Log in with itch.io to leave a comment.