Skip to content

Character System

Comprehensive documentation for the Rent Earth character system using the GanzSe Low Poly Modular Character Pack. This document covers the Unity prefab structure, appearance customization, equipment slots, and data flow.


┌─────────────────────────────────────────────────────────────────┐
│ Unity Client │
│ - Adventurer prefab (GanzSe modular character) │
│ - Armor overlays (6 visual slots) │
│ - Face customization (6 face slots) │
│ - Kevin Iglesias animations (Humanoid rig) │
└─────────────────────────────────────────────────────────────────┘
↕ WebSocket / Protobuf
┌─────────────────────────────────────────────────────────────────┐
│ Axum Game Server │
│ - Authoritative equipment state │
│ - Appearance validation │
│ - Stat calculations from equipped items │
└─────────────────────────────────────────────────────────────────┘
↕ PostgREST RPC
┌─────────────────────────────────────────────────────────────────┐
│ Supabase │
│ - character_appearance table │
│ - character_equipment table │
│ - items table (armor definitions) │
└─────────────────────────────────────────────────────────────────┘

The GanzSe system uses an OVERLAY approach:

  • Base Character Mesh = Single unified body mesh (not separate body parts)
  • Base Character Root = Skeleton with 50+ bones
  • Armor pieces = Overlay meshes that render ON TOP of the body
  • Face details = Additional meshes attached to head bones
Assets/Resources/Prefabs/
├── Models/Character/
│ ├── Base/
│ │ ├── GanzSe Free Modular Character 1_1.fbx (v1.1 base model)
│ │ ├── StylizedLit.mat (Quilbi URP material)
│ │ └── Base Palette Texture URP.png (color palette)
│ ├── Armor/ (108 prefabs)
│ │ ├── Head Armor Type * Color * Part.prefab
│ │ ├── Chest Armor Type * Color * Part.prefab
│ │ ├── Arm Armor Type * Color * Part.prefab
│ │ ├── Belt Armor Type * Color * Part.prefab
│ │ ├── Legs Armor Type * Color * Part.prefab
│ │ └── Feet Armor Type * Color * Part.prefab
│ └── Face/ (107 prefabs)
│ ├── Eyes Type * Color * Part.prefab
│ ├── Eyebrow Type * Color * Part.prefab
│ ├── Hair Type * Color * Part.prefab
│ ├── Face Hair Type * Color * Part.prefab
│ ├── Nose Type * Part.prefab
│ └── Ears Type * Part.prefab
└── Characters/Adventurer/
├── Adventurer.prefab (main character prefab)
└── AdventurerAnimatorController.controller

Adventurer
├── Animator (AdventurerAnimatorController + Humanoid Avatar)
├── Base Character Mesh (SkinnedMeshRenderer - StylizedLit material)
├── Base Character Root (Armature/Skeleton)
│ └── [bone hierarchy for animation]
├── ARMOR_SLOTS/
│ ├── HEAD_SLOT → helmet, hat, hood
│ ├── CHEST_SLOT → chest armor, robes
│ ├── ARMS_SLOT → arm guards, gauntlets
│ ├── BELT_SLOT → belt accessories
│ ├── LEGS_SLOT → leg armor, greaves
│ └── FEET_SLOT → boots, shoes
└── FACE_SLOTS/
├── EYES_SLOT → eye mesh variants
├── EYEBROWS_SLOT → eyebrow mesh variants
├── HAIR_SLOT → hairstyle meshes
├── FACIAL_HAIR_SLOT → beard/mustache meshes
├── NOSE_SLOT → nose mesh variants
└── EARS_SLOT → ear mesh variants

These slots render visible armor on the character model:

Slot IDSlot NameProto EnumGanzSe CategoryPrefab Pattern
0HeadSLOT_HEADHEADSHead Armor Type * Color *
1ChestSLOT_CHESTCHESTSChest Armor Type * Color *
2ArmsSLOT_ARMSARMSArm Armor Type * Color *
3LegsSLOT_LEGSLEGSLegs Armor Type * Color *
4FeetSLOT_FEETFEETFeet Armor Type * Color *
5BeltSLOT_BELTBELTSBelt Armor Type * Color *

Stats-only equipment (no mesh representation on GanzSe model):

Slot IDSlot NameProto EnumDescription
6WeaponSLOT_WEAPONMain-hand weapon
7Off-HandSLOT_OFFHANDShield, tome, off-hand
8Ring 1SLOT_RING_1Accessory ring
9Ring 2SLOT_RING_2Accessory ring
10NecklaceSLOT_NECKLACEAmulet, pendant
11BackSLOT_BACKCape, cloak, backpack
Slot IDKeybindDescription
121Quick consumable
132Quick consumable
143Quick consumable
154Quick consumable
CategoryTypesColorsTotal
Head Armor6 (5 + v1.1)318
Chest Armor6 (5 + v1.1)318
Arm Armor6 (5 + v1.1)318
Belt Armor6 (5 + v1.1)318
Legs Armor6 (5 + v1.1)318
Feet Armor6 (5 + v1.1)318
Total108

Database FieldGanzSe CategoryVariants
eye_shape + eye_colorEYES5 types × 5 colors = 25
eyebrow_style + eyebrow_colorEYEBROWS5 types × 5 colors = 25
hair_style + hair_colorHAIRS5 types × 5 colors = 25
facial_hair_style + facial_hair_colorFACE HAIRS5 types × 5 colors = 25
nose_styleNOSES5 types
ear_styleEARS2 types

When a helmet (HEAD_SLOT) is equipped:

  • Disable HAIR_SLOT mesh to prevent clipping
  • Optionally disable EARS_SLOT depending on helmet type
public void OnHelmetEquipped(bool hasHelmet)
{
hairSlot.SetActive(!hasHelmet);
// earsSlot visibility depends on helmet model
}

CREATE TABLE rentearth.character (
id uuid PRIMARY KEY,
user_id uuid REFERENCES auth.users(id),
slot smallint CHECK (slot BETWEEN 0 AND 9),
name text NOT NULL,
archetype text NOT NULL,
level integer DEFAULT 1,
experience bigint DEFAULT 0,
created_at timestamptz,
updated_at timestamptz
);
CREATE TABLE IF NOT EXISTS rentearth.character_appearance (
character_id uuid PRIMARY KEY REFERENCES rentearth.character(id),
-- Body (base mesh - not swappable in GanzSe free version)
skin_color smallint NOT NULL DEFAULT 0, -- Future: texture tint
body_type smallint NOT NULL DEFAULT 0, -- Future: mesh variants
-- Face slots (mapped to GanzSe prefabs)
eye_style smallint NOT NULL DEFAULT 0, -- 0-4 (Type 1-5)
eye_color smallint NOT NULL DEFAULT 0, -- 0-4 (Color 1-5)
eyebrow_style smallint NOT NULL DEFAULT 0, -- 0-4
eyebrow_color smallint NOT NULL DEFAULT 0, -- 0-4
nose_style smallint NOT NULL DEFAULT 0, -- 0-4
hair_style smallint NOT NULL DEFAULT 0, -- 0-4
hair_color smallint NOT NULL DEFAULT 0, -- 0-4
facial_hair_style smallint NOT NULL DEFAULT 0, -- 0-4 (0=none uses Type 1)
facial_hair_color smallint NOT NULL DEFAULT 0,
ear_style smallint NOT NULL DEFAULT 0, -- 0-1 (Type 1-2)
-- Voice
voice_type smallint NOT NULL DEFAULT 0,
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);
CREATE TABLE IF NOT EXISTS rentearth.character_equipment (
character_id uuid PRIMARY KEY REFERENCES rentearth.character(id),
-- Visual armor slots (GanzSe overlays)
slot_head uuid REFERENCES rentearth.items(id),
slot_chest uuid REFERENCES rentearth.items(id),
slot_arms uuid REFERENCES rentearth.items(id),
slot_belt uuid REFERENCES rentearth.items(id),
slot_legs uuid REFERENCES rentearth.items(id),
slot_feet uuid REFERENCES rentearth.items(id),
-- Non-visual slots
slot_weapon uuid REFERENCES rentearth.items(id),
slot_offhand uuid REFERENCES rentearth.items(id),
slot_ring_1 uuid REFERENCES rentearth.items(id),
slot_ring_2 uuid REFERENCES rentearth.items(id),
slot_necklace uuid REFERENCES rentearth.items(id),
slot_back uuid REFERENCES rentearth.items(id), -- cape, cloak, backpack
-- Hotbar (consumable references)
hotbar_1 uuid REFERENCES rentearth.items(id),
hotbar_2 uuid REFERENCES rentearth.items(id),
hotbar_3 uuid REFERENCES rentearth.items(id),
hotbar_4 uuid REFERENCES rentearth.items(id),
-- Cached stats
gear_score integer NOT NULL DEFAULT 0,
updated_at timestamptz DEFAULT now()
);
CREATE TABLE rentearth.character_stats (
character_id uuid PRIMARY KEY REFERENCES rentearth.character(id),
-- Position
world_x real NOT NULL DEFAULT 0,
world_y real NOT NULL DEFAULT 0,
world_z real NOT NULL DEFAULT 0,
rotation_yaw real NOT NULL DEFAULT 0,
-- Vitals
health_current integer NOT NULL DEFAULT 100,
health_max integer NOT NULL DEFAULT 100,
mana_current integer NOT NULL DEFAULT 100,
mana_max integer NOT NULL DEFAULT 100,
stamina_current integer NOT NULL DEFAULT 100,
stamina_max integer NOT NULL DEFAULT 100,
-- Combat
attack_power integer NOT NULL DEFAULT 10,
defense integer NOT NULL DEFAULT 10,
speed real NOT NULL DEFAULT 5.0,
-- Zone
current_zone text NOT NULL DEFAULT 'starting_area',
last_safe_x real NOT NULL DEFAULT 0,
last_safe_y real NOT NULL DEFAULT 0,
last_safe_z real NOT NULL DEFAULT 0,
-- Time
last_login_at timestamptz,
total_playtime interval DEFAULT '0 seconds',
updated_at timestamptz
);

enum EquipmentSlot {
// Visual armor slots (GanzSe overlays)
SLOT_HEAD = 0;
SLOT_CHEST = 1;
SLOT_ARMS = 2;
SLOT_LEGS = 3;
SLOT_FEET = 4;
SLOT_BELT = 5;
// Non-visual slots
SLOT_WEAPON = 6;
SLOT_OFFHAND = 7;
SLOT_RING_1 = 8;
SLOT_RING_2 = 9;
SLOT_NECKLACE = 10;
SLOT_BACK = 11; // Cape, cloak, backpack
// Hotbar
SLOT_QUICK_1 = 12;
SLOT_QUICK_2 = 13;
SLOT_QUICK_3 = 14;
SLOT_QUICK_4 = 15;
}
message CharacterAppearance {
// Face customization (indexes into GanzSe prefabs)
int32 eye_style = 1; // 0-4
int32 eye_color = 2; // 0-4
int32 eyebrow_style = 3; // 0-4
int32 eyebrow_color = 4; // 0-4
int32 nose_style = 5; // 0-4
int32 hair_style = 6; // 0-4
int32 hair_color = 7; // 0-4
int32 facial_hair_style = 8; // 0-4
int32 facial_hair_color = 9; // 0-4
int32 ear_style = 10; // 0-1
// Future body customization
int32 skin_color = 11;
int32 body_type = 12;
int32 voice_type = 13;
}
message VisualEquipment {
// GanzSe armor type + color encoded as: (type * 10) + color
// e.g., Type 3 Color 2 = 32
int32 head_visual = 1;
int32 chest_visual = 2;
int32 arms_visual = 3;
int32 belt_visual = 4;
int32 legs_visual = 5;
int32 feet_visual = 6;
}

public class CharacterAppearanceController : MonoBehaviour
{
[Header("Slot Containers")]
[SerializeField] private Transform armorSlotsRoot;
[SerializeField] private Transform faceSlotsRoot;
[Header("Slot References")]
[SerializeField] private Transform headSlot;
[SerializeField] private Transform chestSlot;
[SerializeField] private Transform armsSlot;
[SerializeField] private Transform beltSlot;
[SerializeField] private Transform legsSlot;
[SerializeField] private Transform feetSlot;
[SerializeField] private Transform eyesSlot;
[SerializeField] private Transform eyebrowsSlot;
[SerializeField] private Transform hairSlot;
[SerializeField] private Transform facialHairSlot;
[SerializeField] private Transform noseSlot;
[SerializeField] private Transform earsSlot;
private const string ARMOR_PATH = "Prefabs/Models/Character/Armor/";
private const string FACE_PATH = "Prefabs/Models/Character/Face/";
public void ApplyAppearance(CharacterAppearance appearance)
{
// Clear existing face meshes
ClearSlot(eyesSlot);
ClearSlot(eyebrowsSlot);
ClearSlot(hairSlot);
ClearSlot(facialHairSlot);
ClearSlot(noseSlot);
ClearSlot(earsSlot);
// Load face prefabs based on appearance data
LoadFacePart(eyesSlot, "Eyes", appearance.EyeStyle + 1, appearance.EyeColor + 1);
LoadFacePart(eyebrowsSlot, "Eyebrow", appearance.EyebrowStyle + 1, appearance.EyebrowColor + 1);
LoadFacePart(hairSlot, "Hair", appearance.HairStyle + 1, appearance.HairColor + 1);
LoadFacePart(facialHairSlot, "Face Hair", appearance.FacialHairStyle + 1, appearance.FacialHairColor + 1);
LoadFacePart(noseSlot, "Nose", appearance.NoseStyle + 1, 0);
LoadFacePart(earsSlot, "Ears", appearance.EarStyle + 1, 0);
}
public void EquipArmor(EquipmentSlot slot, int armorType, int armorColor)
{
Transform targetSlot = GetArmorSlot(slot);
ClearSlot(targetSlot);
string category = GetArmorCategory(slot);
string prefabName = $"{category} Armor Type {armorType} Color {armorColor} Part";
GameObject prefab = Resources.Load<GameObject>(ARMOR_PATH + prefabName);
if (prefab != null)
{
Instantiate(prefab, targetSlot);
}
}
private void LoadFacePart(Transform slot, string category, int type, int color)
{
string prefabName = color > 0
? $"{category} Type {type} Color {color} Part"
: $"{category} Type {type} Part";
GameObject prefab = Resources.Load<GameObject>(FACE_PATH + prefabName);
if (prefab != null)
{
Instantiate(prefab, slot);
}
}
private void ClearSlot(Transform slot)
{
foreach (Transform child in slot)
Destroy(child.gameObject);
}
private Transform GetArmorSlot(EquipmentSlot slot) => slot switch
{
EquipmentSlot.SlotHead => headSlot,
EquipmentSlot.SlotChest => chestSlot,
EquipmentSlot.SlotArms => armsSlot,
EquipmentSlot.SlotBelt => beltSlot,
EquipmentSlot.SlotLegs => legsSlot,
EquipmentSlot.SlotFeet => feetSlot,
_ => null
};
private string GetArmorCategory(EquipmentSlot slot) => slot switch
{
EquipmentSlot.SlotHead => "Head",
EquipmentSlot.SlotChest => "Chest",
EquipmentSlot.SlotArms => "Arm",
EquipmentSlot.SlotBelt => "Belt",
EquipmentSlot.SlotLegs => "Legs",
EquipmentSlot.SlotFeet => "Feet",
_ => ""
};
}

The Adventurer uses Kevin Iglesias’ Human Animations pack:

ParameterTypeDescription
Speedfloat0=Idle, 0.5=Walk, 1.0=Run
IsGroundedboolFor jump/fall states
JumptriggerTrigger jump animation
Assets/Kevin Iglesias/Human Animations/
├── HumanM@Idles
├── HumanM@Walk01_Forward
├── HumanM@Run01_Forward
├── HumanM@Jump01 [RM]
└── HumanM@Fall01

For upper/lower body blending:

Assets/Kevin Iglesias/Human Animations/Scripts/SpineProxy.cs

1. Unity → Axum: EnterGameRequest { character_id }
2. Axum → Supabase: load_character_full(character_id)
3. Supabase returns: character + appearance + equipment
4. Axum → Unity: CharacterData { appearance, equipment, stats }
5. Unity:
- Spawn Adventurer prefab
- ApplyAppearance(appearance)
- EquipArmor for each equipped slot
- Set position and start animations
1. Unity → Axum: EquipItemRequest { item_id, slot }
2. Axum: Validate (ownership, level, class)
3. Axum → Supabase: Update character_equipment
4. Axum → Unity: EquipmentUpdate { slot, visual_id }
5. Unity: EquipArmor(slot, type, color)
1. Unity → Axum: ChangeAppearanceRequest { appearance }
2. Axum: Validate (changeable fields only)
3. Axum → Supabase: Update character_appearance
4. Axum → Unity: AppearanceUpdate { appearance }
5. Unity: ApplyAppearance(appearance)

RarityColorStat MultiplierMax Sockets
CommonWhite1.0x0
UncommonGreen1.1x0
RareBlue1.25x1
EpicPurple1.5x2
LegendaryOrange2.0x3
enum GemType {
GEM_ATTACK = 0;
GEM_DEFENSE = 1;
GEM_HEALTH = 2;
GEM_MANA = 3;
GEM_SPEED = 4;
GEM_CRIT = 5;
GEM_CRIT_DMG = 6;
GEM_LIFESTEAL = 7;
GEM_RESIST_FIRE = 8;
GEM_RESIST_ICE = 9;
GEM_RESIST_LIGHTNING = 10;
}
gear_score = sum(item_level * rarity_weight * enhancement_bonus)
where:
rarity_weight = 1, 1.2, 1.5, 2.0, 3.0 (common -> legendary)
enhancement_bonus = 1 + (enhancement_level * 0.1)

ActionDurability Cost
Melee attackWeapon: 1
Ranged attackWeapon: 1
Take damageRandom armor: 1-3
Block with shieldOff-hand: 2
DieAll equipped: 10% of max
ThresholdDisplay
50%Yellow durability bar
25%Orange bar + chat warning
10%Red bar + prominent warning
0%Item breaks, unusable until repaired

IndexNameHex
0Pale#FFE4C4
1Fair#FFDAB9
2Light#F5DEB3
3Medium Light#DEB887
4Medium#D2B48C
5Olive#BDB76B
6Tan#C4A484
7Brown#A0785A
8Dark Brown#8B5A2B
9Deep Brown#6B4423
10Ebony#3D2B1F
11Ashen#C0C0C0
12Blue (fantasy)#87CEEB
13Green (fantasy)#98FB98
14Purple (fantasy)#DDA0DD
15Red (fantasy)#FFB6C1
IndexNameHex
0Black#1C1C1C
1Dark Brown#3B2F2F
2Brown#6B4423
3Light Brown#A0522D
4Auburn#A52A2A
5Red#B22222
6Ginger#E97451
7Strawberry#E4717A
8Blonde#F0E68C
9Platinum#E5E5E5
10White#FFFFFF
11Gray#808080
12Blue#4169E1
13Green#228B22
14Purple#9370DB
15Pink#FF69B4

Players can change some appearance options at a Barber NPC.

  • Hair style & color
  • Facial hair style & color
  • Eyebrow style & color
  • Skin color
  • Body type
  • Face shape
  • Eye color/shape
  • Nose style
  • Ear style
Change TypeCost
Hair style100 gold
Hair color50 gold
Facial hair75 gold
Eyebrows25 gold

Returns complete character data for game entry:

{
"id": "uuid",
"name": "Adventurer",
"archetype": "warrior",
"level": 15,
"stats": { "world_x": 125.5, "health_current": 450, ... },
"appearance": { "eye_style": 2, "hair_style": 3, ... },
"equipment": { "slot_head": "item-uuid", ... }
}

Periodic state persistence from Axum:

rentearth.save_character_state(
p_character_id uuid,
p_world_x real,
p_world_y real,
p_world_z real,
p_health_current integer,
p_mana_current integer,
p_level integer,
p_experience bigint
)

  1. Position Validation: Axum validates all movement to prevent teleport hacks
  2. Service Role Only: All RPC functions require service_role
  3. Rate Limiting: Position updates are batched and rate-limited
  4. Equip Validation: Level, class, and ownership checks on server

ComponentStatusNotes
Adventurer Prefab✅ CompleteGanzSe v1.1 base model
Armor Slot System✅ Complete6 visual overlay slots
Face Slot System✅ Complete6 face customization slots
Animation Controller✅ CompleteKevin Iglesias Humanoid rig
Database Schema🔲 PlannedSupabase migrations
Server Validation🔲 PlannedAxum equip/appearance handlers
Barber NPC🔲 PlannedAppearance change UI