Blog
Character customization
For this video game, I attached quite a lot of importance to character customization, i.e. the little brat you control… as well as the bots, of course. This is also what you can find in the “Litte Brats!” avatar generator.
Here's a look at how it works technically with the Godot game engine.
The creation of a typical little brat
The game is seen in faux-3D isometric with a 2D cartoon style. I chose to represent the characters in eight directions: top, right, bottom, left, as well as the four intermediate directions top-right, bottom-right, bottom-left, top-left.
For each of these eight directions, there are also two possible animations: idle or in motion. This gives us 16 different animations in all. By taking advantage of symmetry, we can reduce this number to 2 times 5 (the top-left, left and bottom-left positions are obtained by simply mirroring the top-right, right and bottom-right positions horizontally).
In terms of drawing, we need 12 sprites for each character: 2 feet, 2 legs, 1 pelvis, 1 torso, 2 arms, a face, hair, mouth and eyes. In some positions, not all sprites are visible, and some sprites are reused from one position to the next (for example, the head, torso and pelvis sprites are the same in resting and moving positions). Other variations include different moods for eyes and mouths, and movements such as slapping, etc.
In practice, we also have other animations when the character uses an object (skipping rope, yo-yo, swing, etc.), but they reuse a lot of existing sprites.
That's all very well, but now how do you customize this character?
Colors
As you've probably noticed, my character is either an albino with a taste for white clothes… or he's simply lacking in color.
Obviously, I've colored all the sprites white on purpose, because it
allows me to use a great Godot feature: the self_modulate
property.
This is a property of the
CanvasItem
object (from which Sprite2D inherits) that allows you to… well,
modulate the object's color. It's similar to modulate
, except that
modulate
applies to this node and to all child nodes, whereas
self_modulate
only applies to the node in question: the arms, for
example, are children of the torso node, but we clearly don't want the
t-shirt color to be applied to the arms.
By setting this property, we can give any hue we like to each of our character's sprites:
Obviously, in practice, colors are restricted to a pre-determined subset, to avoid ending up with characters with alien skin, for example…
In the character creation menu, sliders allow you to select the color of each element: skin, hair, T-shirt, pants and shoes. Each slider is connected to a color ramp with, once again, predefined colors. To save the color of each character, simply save the position of each of these sliders!
Sprite variants
Changing colors is cool, but we'd also like to be able to change hairstyle, face shape, glasses, T-shirt and so on.
To do this, we start by organizing our sprite sheet: each element is on a separate line. Then, when we want to add a new variant, we simply create a new line, and by shifting the position of the chosen rectangle on the display, we can simply switch to a new sprite line.
It's even possible to use several sprite sheets to avoid ending up with a single huge image. For example, here are the four sheets used in v1 of the game:
In the script, we create a function that applies the line change to all the sprites selected, giving us:
func set_y_for_all(sprites: Array, y: int) -> void:
for sprite: Sprite2D in sprites:
sprite.region_rect.position.y = y
As I want to be able to add variants later, sometimes from other sprite sheets, I have a config file in which I can indicate which lines to use for which part of the body:
const BRAT_BASE: Texture2D = preload("res://assets/characters/brat_base.png")
const BRAT_VARIANT: Texture2D = preload("res://assets/characters/brat_variant.png")
const BRAT_VARIANT2: Texture2D = preload("res://assets/characters/brat_variant2.png")
const BRAT_VARIANT3: Texture2D = preload("res://assets/characters/brat_variant3.png")
const POSSIBLE_HAIR_SKINS: Array = [
[BRAT_BASE, 2, PRNG.Proba.COMMON], # Straight
[BRAT_VARIANT, 2, PRNG.Proba.COMMON], # Curly
[BRAT_VARIANT2, 4, PRNG.Proba.COMMON], # Ponytail
[BRAT_VARIANT3, 8, PRNG.Proba.COMMON], # Curly ponytail
[BRAT_VARIANT, 10, PRNG.Proba.COMMON], # Long
[BRAT_VARIANT2, 10, PRNG.Proba.COMMON], # Thick
[BRAT_VARIANT3, 10, PRNG.Proba.COMMON], # Wavy
[BRAT_VARIANT, 8, PRNG.Proba.COMMON], # Nerdy
[BRAT_VARIANT2, 0, PRNG.Proba.COMMON], # Short
[BRAT_VARIANT2, 2, PRNG.Proba.COMMON], # Messy
[BRAT_VARIANT3, 12, PRNG.Proba.COMMON], # Bushy
[BRAT_VARIANT, 4, PRNG.Proba.RARE], # Pigtails
[BRAT_VARIANT2, 12, PRNG.Proba.RARE], # Dreadlocks
[BRAT_VARIANT, 6, PRNG.Proba.RARE], # Punk
[BRAT_VARIANT2, 6, PRNG.Proba.RARE], # Palmtree
[BRAT_VARIANT2, 8, PRNG.Proba.RARE], # Mohawk
[BRAT_BASE, 0, PRNG.Proba.VERY_RARE], # Bald
]
const POSSIBLE_HEAD_SKINS: Array = [
[BRAT_BASE, 4, PRNG.Proba.COMMON],
[BRAT_VARIANT, 0, PRNG.Proba.COMMON],
[BRAT_VARIANT3, 0, PRNG.Proba.RARE],
[BRAT_VARIANT3, 2, PRNG.Proba.RARE],
[BRAT_VARIANT3, 4, PRNG.Proba.RARE],
[BRAT_VARIANT3, 6, PRNG.Proba.RARE],
]
const POSSIBLE_EYES_SKINS: Array = [
[BRAT_BASE, 7, PRNG.Proba.COMMON], # No glasses
[BRAT_VARIANT, 12, PRNG.Proba.RARE], # Round glasses
[BRAT_VARIANT, 13, PRNG.Proba.RARE], # Square glasses
[BRAT_VARIANT2, 14, PRNG.Proba.VERY_RARE], # Sun glasses
[BRAT_VARIANT2, 15, PRNG.Proba.VERY_RARE], # Sun glasses
[BRAT_VARIANT2, 16, PRNG.Proba.VERY_RARE], # Sun glasses
]
const POSSIBLE_TSHIRT_SKINS: Array = [
[BRAT_BASE, 11],
[BRAT_VARIANT, 14],
[BRAT_VARIANT, 15],
[BRAT_VARIANT, 16],
[BRAT_VARIANT, 17],
[BRAT_VARIANT, 18],
[BRAT_VARIANT3, 14],
[BRAT_VARIANT3, 15],
[BRAT_VARIANT3, 16],
]
const POSSIBLE_PANTS_SKINS: Array = [
[BRAT_BASE, 12],
[BRAT_VARIANT2, 17],
[BRAT_VARIANT2, 18],
]
On each line, we have the file in which to find the sprite line, and below it the line index. As you can guess from the code, there are also notions of rarity for certain skins, to avoid ending up with 10 kids with punk or bald haircuts, which would be rather unrealistic…
With this method, I can easily add skins later on, arrange them in different spritesheets and so on.
In v1 of the game, there are 17 hairstyles, 6 face shapes, 6 types of glasses, 9 T-shirts and 3 pairs of pants — a total of 16524 possible combinations! And that's without counting the color variations. In short, it's a great way to bring diversity and ensure there are no “clones” in the playground!
Conclusion
By using two simple Godot properties (color modulation and sprite rectangle position), you can customize the character, generate bots with various random skins... while preserving the animations and interactions developed for all characters. Skin saving is a simple list of integers corresponding to the index of each sprite and, for colors, to the position on the color ramp.
Of course, other skins will be added to the existing list in future game updates, so stay tuned!