Welcome to the sponsor-exclusive content for the Ren'Py Patreon. Sponsors like you ensure this page exists. Thank you.

_images/three-creator-defined-statements.jpg

Three Creator Defined Statements link

While the biggest new feature coming in Ren’Py 7.3 is the new support for the HTML5 web platform, one of the other new features is a long-requested revamp of the support for creator-defined statements. In this month’s article, I’ll be showing you how to use this new support to add three new statements to Ren’Py.

Creator-defined statements are probably the ultimate expression of the “Py” part of Ren’Py - the idea that Ren’Py is a visual novel engine that is implemented in Python, and can be extended with Python as needed. (Of course, this goal has to be balanced with others, like keeping a well-defined API so development doesn’t stagnate.) Many common statements, like the play, queue, and stop statements are implemented as creator-defined statements.

In Ren’Py 7.3, creator-defined statements have been extended in several ways, with the most important being that it is now possible to have blocks of Ren’Py statements inside of a creator-defined statement. In this article, we’ll introduce three statements - a random statement that randomly picks one of its children to run; a cycle statement that cycles through its children in order, and a block statement that groups multiple statements together.

Where to Place Creator-Defined Statements link

Unlike most other constructs in the Ren’Py language, there are a number of constraints as to where you can put a creator-defined statement. These statements are defined in python early blocks, inside a file that is loaded before the statement itself is used.

Since filenames beginning with 00 are reserved for Ren’Py, I suggest defining your custom statements in files beginning with 01 - either a single file named 01statements.rpy, or multiple files with names like 01random.rpy, 01cycle.rpy, and 01block.rpy.

It’s also important to know that a creator-defined statement can’t be used in the file in which it’s defined. Ren’Py needs to load the file and process the creator-defined statements, and only in the next file that it loads is it ready to parse these statements.

Random link

The random statement picks one of multiple choices at random, potentially with a weight associated with each one. Here’s an example of a random statement in use:

random:
    e "This occurs one quarter of the time."
    e "This also happens one quarter of the time."
    weight 2 e "This happens two quarters of the time - or half the time!"

Each statement inside the random block can be selected, and only one of these statements can be run. If a weight clause is given, it can be used to make one statement more likely than others. If not explicitly given, a statement’s weight is 1. The chance of a statement running is its weight divided by the weight of all statements in the random block.

Random isn’t just for dialogue - it’s possible to use other statements, inside, like scene:

e "Let's check the weather."

random:
    scene bg hurricane
    scene bg hailstorm
    scene bg frograin

e "Yeah, looks like a good day to stay inside and add statements to Ren'Py."

To add the random statement to your game, add:

python early:

    def parse_random(l):

        # Looks for a colon at the end of the line.
        l.require(":")
        l.expect_eol()

        # This is a list of (weight, block) tuples.
        blocks = [ ]

        # ll is a lexer (an object that can match words, numbers, and other parts of text) that accesses the block under the current statement.
        ll = l.subblock_lexer()

        # For each line in the file, check for errors...
        while ll.advance():
            with ll.catch_error():

                # ...determine the weight...
                weight = 1.0

                if ll.keyword('weight'):
                    weight = float(ll.require(ll.float))

                # ...and then store the weight and the statement.
                blocks.append((weight, ll.renpy_statement()))

        return { "blocks" : blocks }

    def next_random(p):

        blocks = p["blocks"]

        # Pick a number between 0 and the total weight.
        total_weight = sum(i[0] for i in blocks)
        n = renpy.random.random() * total_weight

        # Then determine which block that number belongs to.
        for weight, block in blocks:
            if n <= weight:
                break
            else:
                n -= weight

        return block

    renpy.register_statement("random", parse=parse_random, next=next_random, predict_all=True, block=True)

Cycle link

The cycle statement cycles through the statements underneath it in the order in which they occur. The first time the cycle statement runs, the first statement underneath it is executed. The second time it runs, the second statement is executed, and so on. After the last child statement is executed, it starts over with the first one. This can provided more controlled, if predictable, diversity in responses - unlike random, which can produce the same choice several times in a row.

The cycle statement takes a name, and then one statement for each step in the cycle.

cycle win:
    e "Good job!"
    e "Way to go!"
    e "I knew you could do it!"
    e "Attaboy!"
    e "Way to not mess it up!"

The implementation is very similar, with some changes to store the name and to get rid of the unneeded weight clause:

default cycles = { }

python early:

    def parse_cycle(l):

        # After the 'cycle' keyword, we need a name, colon, and end of line.
        name = l.require(l.name)
        l.require(":")
        l.expect_eol()

        # Parse each of the statements in the subblock, and store it.
        blocks = [ ]
        ll = l.subblock_lexer()

        while ll.advance():
            with ll.catch_error():
                blocks.append(ll.renpy_statement())

        return { "name" : name, "blocks" : blocks }

    def next_cycle(p, advance=True):

        name = p["name"]
        blocks = p["blocks"]

        # Figure out how many times this statement has been reached.
        current = cycles.get(name, -1) + 1
        if advance:
            cycles[name] = current

        # And use that to pick the correct next statement.
        return blocks[current % len(blocks)]

    def predict_next_cycle(p):
        return [ next_cycle(p, advance=False) ]

    renpy.register_statement("cycle", parse=parse_cycle, next=next_cycle, predict_next=predict_next_cycle, block=True)

Block link

One problem with the above two statements is that each will only pick a single statement to run, which might be a problem if you’d like to have a choice between multiple blocks of dialogue. While it is technically possible to put a Ren’Py label statement inside, that introduces an unnecessary name, and seems visually unnatural, at least to my eyes.

With our new ability to make statements, we can introduce a statement to solve this problem for us. The block statement simply takes a block of Ren’Py statements, and runs them in order. This means we can write:

random:
    block:
        $ win = True
        e "Looks like you won this time."
        e "But I'll get back at you next time."

    block:
        $ win = False
        e "Looks like it's not your day."
        e "These coinflips are rough."

    weight 0.01 block:
        e "On the edge? What's the chance of that?"
        jump and_then_the_sun_explodes

The block statement is the simplest of the three we discuss today, as it just parses the block, then runs it:

python early:

    def parse_block(l):

        # Looks for a colon and the end of line.
        l.require(':')
        l.expect_eol()

        # Parses the block below this statement.
        block = l.subblock_lexer().renpy_block()

        return { "block" : block }

    # The next function returns the statement to execute next - in this case,
    # the first statement in the block.
    def next_block(p):
        return p["block"]

    renpy.register_statement("block", parse=parse_block, next=next_block, predict_all=True, block=True)

Closing Thoughts link

I’ve focused this article on showing you some of the benefits of creator-defined statements. If you’d like to write your own, and see what the functions and methods in the example do, check out the creator-defined statement documentation.

There are several other benefits to a CDS that I haven’t touched on here, one of the biggest being the compile-time and lint-time error checking that can be done in a CDS but not a Python function. At the same time, realize that by adding a CDS, you might make your game script harder to read. In addition to learning Ren’Py, being a developer means you need to learn your own syntax. (And remember how to use it years later.) As a result, it makes sense to use creator-defined statements where they can benefit you the most.

One place where I think creator-defined statements could prove really useful is in defining events for something like the Dating Sim Engine (DSE) framework. Custom syntax could make the events easier to read and write. If you’re interested in something like this, please let me know, and I’ll write another article in a few months. Until then, thank you for supporting Ren’Py!