This document is a work in progress!

Prerequisites

Walkthrough

1. Project Preparation

  1. Create a new project directory under your Ren’Py workspace directory titled "CharacterCreatorTutorial" (or whatever you would like to call it).

  2. Download the latest release of the Sprite Customizer (specifically the 'base-project' zip file). and unzip it into your new project directory.

  3. In the new project directory, delete the game/images/ccp directory and its contents.

  4. Create a new directory under game/images called "sugar".

  5. Download the tutorial asset pack and unzip it into the game/images/sugar directory.

    1. Your game/images/sugar directory should now contain multiple subdirectories named "accessories", "brows", "clothes", etc…​

    2. Explore the directories to familiarize yourself with the assets we will be using in this tutorial.

2. The First Steps

2.1 Prep

  1. Open up your tutorial project directory in your code editor.

  2. Open the game/sprite_customizer/config.rpy file.

  3. Delete all the file’s contents, leaving it empty.

  4. Add a new define statement for our CustomizedSprite, it’s best to go ahead and make the declaration multi-line as we are going to be adding a lot to it.

    define player_customizer = CustomizedSprite(
    )
    1. Give it a name that we can use in our show statements. We are going to name it player_base for reasons we will explore later.

      define player_customizer = CustomizedSprite(
        "player_base",
      )

2.2 Adding Our First Layer!

Now we are going to add our first layer to the CustomizedSprite. Layers are declared by creating new SCLayer instances and passing in a set of values that tell it what options it has and how to render them.

Since layers are stacked on top of each other, the first layer is generally going to be the base or skin layer. In our case we have multiple customization options for the skin layer, so this will be one of the more complex examples.

The full snippet is as follows, but we will break down what it means below.

init python:
    def skin_lcb(skin_base, skin_color, **kwargs): (1)
        return Transform(f"images/sugar/skin/{skin_base}.png", matrixcolor=TintMatrix(skin_color))

define player_customizer = CustomizedSprite(
    "player_base",
    SCLayer( (2)
        "skin", (3)
        skin_lcb, (4)
        skin_base=("Skin Base", "Body", [ "dark", "medium", "light" ]), (5)
        skin_color=("Skin Color", "Body", [ (6)
            "#613021",
            "#874c2c",
            "#803716",
            "#b66837",
            "#a96238",
            "#f9bf91",
            "#ecc19f",
            "#9e9a73",
        ])
    )
)
1 The skin_lcb Callback is used to generate the Displayable that will be used as the base layer for our customized sprite.
2 SCLayer constructor call.
3 Name of the layer.
4 Reference to our skin_lcb callback, passing the callback to the new layer.
5 Skin base option, directly relates to the skin base image files included in the tutorial sprite pack ("dark.png", "medium.png", "light.png").
6 Skin color option. A collection of hex codes that will be used to make a TintMatrix to apply the skin color to our grayscale base images.
The skin_lcb Callback
def skin_lcb(skin_base, skin_color, **kwargs):
    return Transform(f"images/sugar/skin/{skin_base}.png", matrixcolor=TintMatrix(skin_color))

The skin_lcb function is a Layer Callback that is used to build the Displayable that will be used as the base layer for our CustomizedSprite instance.

This function takes our two option values ("skin_base" and "skin_color") and uses them to provide a path to the target file and apply a TintMatrix to that target file.

The names of a callback’s arguments must be the same as the names of the options on the relevant layer. Since, in this case, our layer has two options named skin_base and skin_color, those two options will be passed to the callback under arguments with those names. The ordering of the arguments does not matter.

We pass this function to the created SCLayer instance to tell the layer how to render that part of the customized sprite.

Our First SCLayer

Our first SCLayer instance is constructed with the following arguments:

  1. The layer name

  2. A callback that will be used to generate our layer image

  3. An option group named skin_base

  4. An option group named skin_color

The layer name should be unique across all the layers in a single CustomizedSprite instance.

The callback is described above in The skin_lcb Callback.

The first option group, skin_base, is a list of the distinct portion of the paths to the target image files. In our case, the paths are all the same except for the file name (minus the extension) which is one of "dark", "medium", or "light".

The second option group, skin_color, is a list of hex color codes that will be used to apply a TintMatrix to the selected grayscale base image.

3. Showing Our Image

Now we have a base sprite (even if it’s just a skin layer), lets show it so we can watch our progress in-game via script reloads.

  1. Open game/script.rpy and delete all its contents.

  2. Create a new [sc-state] object to hold our custom sprite option selections.

    default customizer_state = SCState()
  3. Add an after_load label and a python line to load our state into the sprite customizer on game load time (without this the option selections will not show when loading a game).

    label after_load:
        $ player_customizer.set_state(customizer_state)
  4. Add our start label and another python line to load our state into the sprite customizer. This is done so that the player’s choices will be stored as part of the game save files.

    label start:
        $ player_customizer.set_state(customizer_state)
  5. Show our customized sprite.

    label start:
        $ player_customizer.set_state(customizer_state)
        show player_base (1)
    1 "player_base" is the name we gave our CustomizedSprite instance in config.rpy
  6. Add a pause so the game doesn’t end immediately.

    label start:
        $ player_customizer.set_state(customizer_state)
        show player_base
        pause 1000000

Now we can reload our game as we add layers and see our changes live!

tutorial 01

4. Adding More Layers

Now that we can see what we are doing, let’s go back to config.rpy and add more layers.

4.1 Clothes

Next we will add the clothes layer, this layer is a simple image swap layer so the definition of it will be more simple than the skin layer.

  1. In the CustomizedSprite declaration, add a new argument that is another SCLayer constructor:

    define player_customizer = CustomizedSprite(
        "player_base",
        SCLayer(
            ...
        ),
        SCLayer(
            "clothes",
            "images/sugar/clothes/{clothes}.png", (1)
            clothes=("Clothes", "Body", [ "tshirt", "tie", "bow" ])
        ),
    )
    1 For this layer, since we will just be swapping images out without any transforms, we can pass a template path instead of a callback.
  2. Go ahead and reload the game now and you should see the tshirt layer rendering on top of the base image.

    tutorial 02

4.2 Hair

Now we will stack the hair layer onto our sprite. This layer is another one with multiple images as well as color options, so we will be declaring another callback.

Under our existing skin_lcb callback in the init python block we will add a new function named hair_lcb which does the same thing the skin_lcb function does but with a different image path.

init python:
    def skin_lcb(skin_base, skin_color, **kwargs):
        return Transform(f"images/sugar/skin/{skin_base}.png", matrixcolor=TintMatrix(skin_color))

    def hair_lcb(hair_style, hair_color, **kwargs):
        return Transform(f"images/sugar/hair/hair{hair_style}.png", matrixcolor=TintMatrix(hair_color))

Lets look at this callback quickly. As it takes the arguments hair_style and hair_color which means our new layer must have those options attached to it.

Now that that’s out of the way, lets add our hair layer.

define player_customizer = CustomizedSprite(
    "player_base",
    ...
    SCLayer(
        "hair",
        hair_lcb,
        hair_style=("Style", "Hair", [ 1, 2, 3 ]), (1)
        hair_color=("Color", "Hair", [ (2)
            "#3D2314",
            "#100C07",
            "#DA680F",
            "#FFCC47",
            "#9A9E9F",
            "#FAFAFA",
            "#801818",
            "#680271",
            "#1692CA",
            "#11694E",
            "#FF87C6"
        ])
    ),
)
1 The first option just has the values 1, 2, and 3 as that is the only part of the image name that differs between hair style images.
2 The second option is a selection of hex codes for hair color options we will allow.
tutorial 03

4.3 Eyes

The eyes layer is another simple layer without a transform, so we can construct our SCLayer instance an image path rather than a callback.

define player_customizer = CustomizedSprite(
    "player_base",
    ...
    SCLayer(
        "eyes",
        "images/sugar/eyes/{eye_color}.png",
        eye_color=("Eyes", "Face", [ "blue", "green", "grey", "yellow" ])
    ),
)
tutorial 04

4.4 Glasses

Our sprite pack comes with one glasses sprite that we will want to toggle on and off. For this use case, it doesn’t make much sense to provide a list of options since there are only two, "glasses.png" and "none.png". To implement this yes/no choice we can use a new option type SCBooleanOption which is purpose built for this kind of situation.

This is also a good point to talk about removing options. Right now, there is now way to do this via the Sprite Customizer other than by using a blank PNG file. This is what we will be doing for the glasses and other accessories.

define player_customizer = CustomizedSprite(
    "player_base",
    ...
    SCLayer(
        "eyes",
        "images/sugar/accessories/{face_accessory}.png",
        face_accessory=SCBooleanOption(
            "face_accessory", (1)
            "Glasses", (2)
            "Face", (3)
            False, (4)
            ("glasses", "none") (5)
        )
    )
1 For SCOption types like SCBooleanOption, the keyword for the option must be passed in the constructor.
2 The display name for the option.
3 The group name.
4 The default state for the option, False here equates to "none" in the option tuple.
5 The option value tuple, the first value is what will be used when the boolean value is True, the second value will be used when the boolean value is False.

4.6 Bow

In addition to glasses, our sprite pack comes with another accessory; a bow. We will want to be able to both toggle the bow on and off, but also for this example, we want to apply colors to the bow.

Here we will be constructing another layer callback which will take a value from a standard option as well as an SCBooleanOption.

init python:
    ...
    def hair_accessory_lcb(hair_accessory, hair_accessory_color, **kwargs):
        return Transform(f"images/sugar/accessories/{hair_accessory}.png", matrixcolor=TintMatrix(hair_accessory_color))

define player_customizer = CustomizedSprite(
    "player_base",
    ...
    SCLayer(
        "hair_accessory",
        hair_accessory_lcb,
        hair_accessory=SCBooleanOption(
            "hair_accessory",
            "Bow",
            "Hair",
            False,
            ("bow", "none")
        ),
        hair_accessory_color=("Bow Color", "Hair", [
            "#849FF7",
            "#A683F7",
            "#FFEB79",
            "#FFD679"
        ])
    ),
)

6. The Character Customization Screen

Now we have added all our base options (the remaining options will be used in a layeredimage for mood), lets show the included character customization screen and play with what we’ve made so far.

To do this, go back to the script.rpy file and add call the customization screen.

script.rpy
default customizer_state = SCState()

label start:
    $ player_customizer.set_state(customizer_state)
    show player_base
    pause 1000000

    $ quick_menu = False (1)
    call screen sprite_creator("player_base", player_customizer) (2)
    $ quick_menu = True (3)

    pause 1000000
1 Hide the quick menu as it looks out of place with the sprite_creator screen open.
2 Call the sprite_creator screen passing in the name of our player image as well as a reference to the CustomizedSprite instance itself.
3 Reshow the quick menu when we leave the character customization screen.

Play around with what you’ve made!

tutorial 06

6. Character Expressions

Now that we have a base for character, we can make the Layered Images necesesary to have character expressions. We won’t go into too much detail here as this is a standard Ren’Py feature.

  1. Open up the game/images.rpy file and delete all of its contents.

  2. Create a new layeredimage named "player".

    layeredimage player:
  3. Add an always attribute to show our customized sprite, player_base.

    layeredimage player:
        always:
            "player_base"
  4. Add a new group named "eyebrows" for our eyebrow expression sprites, "angry" "neutral", "questioning", "raised", and "sad" and add those sprite paths.

    layeredimage player:
        always:
            "player_base"
    
        group eyebrows:
            attribute angry_brows:
                "images/sugar/brows/angry.png"
            attribute neutral_brows default:
                "images/sugar/brows/neutral.png"
            attribute questioning_brows:
                "images/sugar/brows/questioning.png"
            attribute raised_brows:
                "images/sugar/brows/raised.png"
            attribute sad_brows:
                "images/sugar/brows/sad.png"
  5. Add another group named "mouth" for our mouth expression sprites, "frown", "happy", "smile", and "surprised".

    layeredimage player:
        ...
        group mouth:
            attribute frown:
                "images/sugar/mouth/frown.png"
            attribute happy:
                "images/sugar/mouth/happy.png"
            attribute smile default:
                "images/sugar/mouth/smile.png"
            attribute surprised:
                "images/sugar/mouth/surprised.png"

Now we have constructed a layered image with expressions that we can use as our character sprite. To do this we need to go back to script.rpy and edit the references to player_base to say "player" instead. This will use our new layered image rather than the raw custom sprite it’s based on.

default customizer_state = SCState()

label start:
    $ player_customizer.set_state(customizer_state)
    show player
    pause 1000000

    $ quick_menu = False
    call screen sprite_creator("player", player_customizer)
    $ quick_menu = True

    pause 1000000

Now we can change our player character sprites expression by using our `layeredimage’s attributes like so:

    show player raised_brows surprised
    # or
    show player angry_brows frown
    # etc...

Completed Source

game/sprite_customizer/config.rpy
init python:
    def skin_lcb(skin_base, skin_color, **kwargs):
        return Transform(f"images/sugar/skin/{skin_base}.png", matrixcolor=TintMatrix(skin_color))

    def hair_lcb(hair_style, hair_color, **kwargs):
        return Transform(f"images/sugar/hair/hair{hair_style}.png", matrixcolor=TintMatrix(hair_color))

    def hair_accessory_lcb(hair_accessory, hair_accessory_color, **kwargs):
        return Transform(f"images/sugar/accessories/{hair_accessory}.png", matrixcolor=TintMatrix(hair_accessory_color))

define player_customizer = CustomizedSprite(
    "player_base",
    SCLayer(
        "skin",
        skin_lcb,
        skin_base=("Skin Base", "Body", [ "dark", "medium", "light" ]),
        skin_color=("Skin Color", "Body", [
            "#613021",
            "#874c2c",
            "#803716",
            "#b66837",
            "#a96238",
            "#f9bf91",
            "#ecc19f",
            "#9e9a73",
        ])
    ),
    SCLayer(
        "clothes",
        "images/sugar/clothes/{clothes}.png",
        clothes=("Clothes", "Body", [ "tshirt", "tie", "bow" ])
    ),
    SCLayer(
        "hair",
        hair_lcb,
        hair_style=("Style", "Hair", [ 1, 2, 3 ]),
        hair_color=("Color", "Hair", [
            "#3D2314",
            "#100C07",
            "#DA680F",
            "#FFCC47",
            "#9A9E9F",
            "#FAFAFA",
            "#801818",
            "#680271",
            "#1692CA",
            "#11694E",
            "#FF87C6"
        ])
    ),
    SCLayer(
        "eyes",
        "images/sugar/eyes/{eye_color}.png",
        eye_color=("Eyes", "Face", [ "blue", "green", "grey", "yellow" ])
    ),
    SCLayer(
        "eyes",
        "images/sugar/accessories/{face_accessory}.png",
        face_accessory=("Glasses", "Face", [ "glasses", "none" ])
    ),
    SCLayer(
        "hair_accessory",
        hair_accessory_lcb,
        hair_accessory=("Bow", "Hair", [ "bow", "none" ]),
        hair_accessory_color=("Bow Color", "Hair", [
            "#849FF7",
            "#A683F7",
            "#FFEB79",
            "#FFD679"
        ])
    ),
)
game/images.rpy
layeredimage player:
    always:
        "player_base"

    group eyebrows:
        attribute angry_brows:
            "images/sugar/brows/angry.png"
        attribute neutral_brows default:
            "images/sugar/brows/neutral.png"
        attribute questioning_brows:
            "images/sugar/brows/questioning.png"
        attribute raised_brows:
            "images/sugar/brows/raised.png"
        attribute sad_brows:
            "images/sugar/brows/sad.png"

    group mouth:
        attribute frown:
            "images/sugar/mouth/frown.png"
        attribute happy:
            "images/sugar/mouth/happy.png"
        attribute smile default:
            "images/sugar/mouth/smile.png"
        attribute surprised:
            "images/sugar/mouth/surprised.png"
game/script.rpy
default customizer_state = SCState()

label start:
    $ player_customizer.set_state(customizer_state)

    show player
    pause 1000000

    $ quick_menu = False
    call screen sprite_creator("player", player_customizer)
    $ quick_menu = True

    pause 1000000

Credits