Skip to content

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:

  1. Body — base body sprite
  2. Skin tone — overlay
  3. Hair — back layer + front layer
  4. Outfit — profession-specific
  5. Accessories — glasses, hat, etc.
  6. 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.ts

Components

ComponentVariantsNotes
Body base3slim, average, stocky
Skin tone5Light, light-tan, tan, brown, dark-brown
Hair back8 styles
Hair front12 stylesmatches back style
Outfit20per profession + casual variants
Accessory6Glasses, hat variants
Expression5Happy, 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:

  1. Profession → default outfit
  2. Personality → influences default expression
  3. 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

  1. Hair back
  2. Body
  3. Outfit (overlays body)
  4. Hair front (over face)
  5. Accessories
  6. Expression (top of face)

Expressions

NPC EmotionExpression Sprite
happyface-happy
contentface-content
neutralface-neutral
sadface-sad
angryface-angry
anxiousface-anxious
boredface-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

Last updated:

Released under the MIT License.