Sprite
NPC sprites. Every NPC has a unique visual identity, generated from a small set of components.
NPC Sprite System
NPC sprites are composed, not pre-drawn. Each NPC is generated from:
- Body — base body sprite
- Skin tone — overlay
- Hair — back layer + front layer
- Outfit — profession-specific
- Accessories — glasses, hat, etc.
- Expression — face overlay (smile, frown, neutral, etc.)
This lets us produce hundreds of distinct NPCs from a small set of component sprites.
Sprite Layout
Each NPC sprite is a 32×48 px stacked composition:
┌──────────────────┐
│ Hair (back) │ y: 0–12
├──────────────────┤
│ Face + Body │ y: 8–40
├──────────────────┤
│ Legs/Feet │ y: 36–48
└──────────────────┘The sprite is centered horizontally, with feet at the bottom.
Component Atlas
/assets/npcs/
├── components.png
├── components.json
└── generator.tsComponents
| Component | Variants | Notes |
|---|---|---|
| Body base | 3 | slim, average, stocky |
| Skin tone | 5 | Light, light-tan, tan, brown, dark-brown |
| Hair back | 8 styles | |
| Hair front | 12 styles | matches back style |
| Outfit | 20 | per profession + casual variants |
| Accessory | 6 | Glasses, hat variants |
| Expression | 5 | Happy, neutral, sad, angry, surprised |
Total unique combinations: ~72,000 possible NPCs
NPC Data Model
ts
interface NPCSpriteData {
npcId: string;
body: 'slim' | 'average' | 'stocky';
skin: SkinTone;
hairBack: HairStyle;
hairFront: HairStyle;
hairColor: string;
outfit: OutfitId;
accessories: AccessoryId[];
defaultExpression: Expression;
}Generation
NPCs are assigned sprite data once at world creation, based on:
- Profession → default outfit
- Personality → influences default expression
- Random seed → body, hair, color, accessories
Same NPC ID + same seed = same sprite. Reproducible.
Rendering Pipeline
NPC sprites are rendered as layered sprites, not pre-baked:
ts
class NPCSprite extends Container {
private layers: Record<string, Sprite>;
constructor(data: NPCSpriteData) {
super();
this.layers = {
hairBack: new Sprite(Assets.get(`hair-back-${data.hairBack}`)),
body: new Sprite(Assets.get(`body-${data.body}-${data.skin}`)),
outfit: new Sprite(Assets.get(`outfit-${data.outfit}`)),
hairFront: new Sprite(Assets.get(`hair-front-${data.hairFront}`)),
accessories: new Sprite(Assets.get(`acc-${data.accessories[0]}`)),
expression: new Sprite(Assets.get(`face-${data.defaultExpression}`)),
};
for (const layer of Object.values(this.layers)) {
this.addChild(layer);
}
}
setExpression(expr: Expression) {
this.layers.expression.texture = Assets.get(`face-${expr}`);
}
setOutfit(outfit: OutfitId) {
this.layers.outfit.texture = Assets.get(`outfit-${outfit}`);
}
}Z-order
- Hair back
- Body
- Outfit (overlays body)
- Hair front (over face)
- Accessories
- Expression (top of face)
Expressions
| NPC Emotion | Expression Sprite |
|---|---|
| happy | face-happy |
| content | face-content |
| neutral | face-neutral |
| sad | face-sad |
| angry | face-angry |
| anxious | face-anxious |
| bored | face-bored |
Updated on each tick via SSE.
Performance
Each NPC is ~6 layered sprites. With 30 NPCs:
- Total sprite draws: 180 (well under 200-budget)
- Memory: ~30 NPCs × 6 layers × 32×48×4 bytes = ~110KB
Easily within budget.
📐 详细角色规范见 Design — Character Spec