Generated banner for Build Rigged Game Assets
Generated logo for Build Rigged Game Assets

Build Rigged Game Assets

Meng ToCommunityCreated by Meng To

  • Skill
  • Workflow
  • Software development
  • MIT
  • Low risk
  • Has scripts

From the author

Create, integrate, or audit production-ready rigged 3D characters and monsters with a main model, skeleton, animation library, sockets, collision contracts, separate character equipment, optimized runtime files, catalog media, and an interactive moveset inspector. Use when generating or importing FBX/GLB game actors; defining character-versus-monster asset requirements; separating swords, shields, helmets, armor, or stowed gear; adding idle, walk, run, attack, hit, dodge, block, cast, or death clips; wiring an asset into gameplay; or adding action and equipment controls plus deterministic tests to a game asset catalog.
— the description by Meng To

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a rigorous, standardized process for developing 3D game actors. It covers the entire lifecycle from source reference and rigging to runtime integration and catalog presentation. The workflow emphasizes maintaining a clear separation between immutable source assets and optimized runtime files, ensuring that characters and monsters meet specific technical budgets and gameplay requirements. It includes strict validation steps, manifest-based contracts, and requirements for interactive catalog inspectors to ensure assets are production-ready.

What it does

  • Defines a structured manifest-based contract for character and monster assets.
  • Provides guidelines for normalizing models, rigs, and animation libraries for game engines.
  • Establishes requirements for equipment systems, socket placement, and collision contracts.
  • Outlines the creation of catalog cards and interactive runtime inspectors for asset review.
  • Includes a suite of deterministic tests to verify asset integrity and performance.
  • Ensures consistent handling of character gear and monster combat states.

When to use it

  • When importing or generating new 3D rigged actors for a game project.
  • When defining technical requirements for character versus monster assets.
  • When setting up or auditing an asset catalog for a game engine.
  • When troubleshooting issues with character equipment toggles or animation clips.
  • When preparing assets for integration into a gameplay environment.

Advantages

  • Ensures high-fidelity source assets are preserved while optimizing runtime performance.
  • Promotes consistency across different character and monster assets.
  • Provides clear, testable criteria for asset validation and rejection.
  • Facilitates easier debugging through standardized runtime telemetry and inspectors.
  • Reduces integration errors by enforcing strict manifest-based contracts.

Usage examples

“How do I add a new character to the game asset catalog?”
The agent will guide you to copy the character-asset-manifest.template.json, define the actor's requirements, build the model and rig according to the specified constraints, and run the validation scripts.
“Verify if my monster asset is ready for production.”
The agent will check your asset against the monster profile requirements, including action roles, collision volumes, and catalog inspector functionality, and run the required validation tests.
“How should I handle equipment for a new character?”
The agent will explain how to define canonical slots, bind equipment to named sockets, and ensure that toggling equipment leaves no artifacts, following the character profile rules.
“What are the requirements for the catalog inspector?”
The agent will detail the necessary controls, such as action selection, drag-to-rotate, and equipment toggles, and confirm that the inspector uses the actual runtime model.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/mengto/build-rigged-game-assets/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip build-rigged-game-assets.zip -d .agents/skills

Documentation by Meng To

Build Rigged Game Assets

Build one truthful actor package from source reference through gameplay and catalog proof.

Establish the contract first

Inspect repository truth before generating or editing files:

  • Locate the source image/model, runtime loader, skeleton conventions, action API, equipment system, collision system, catalog schema, review routes, budgets, and tests.

  • Classify the actor as character or monster.

  • Classify delivery as catalog-only, review-only, or runtime. Do not call an asset implemented until gameplay loads it.

  • Preserve immutable source artifacts separately from optimized runtime files.

  • Decide target formats before using a credit-bearing generator.

Copy the matching template from assets/, fill it in, and keep it beside the implementation or in the project’s asset metadata location:

  • character-asset-manifest.template.json

  • monster-asset-manifest.template.json

Run scripts/validate_asset_manifest.py <manifest> before building. Re-run with --root <repo> for shipped-file checks and --require-verified only after every acceptance check has actually passed.

Read references/requirements.md before creating the model, rig, actions, equipment, sockets, or collision data. Read references/catalog-and-testing.md when adding the catalog card, inspector, moveset route, telemetry, or release tests.

Build the common actor package

  1. Preserve the source.

    • Record reference images, source tasks, imported files, licenses, generator/model, and material provenance.

    • Never overwrite the high-detail source with the runtime optimization.

  2. Produce the main model.

    • Normalize scale, Y-up, forward axis, pivot, ground contact, materials, textures, and triangle budget.

    • Ground from feet or authored contact sockets, never a weapon tip, cloth tail, shadow, or VFX.

    • Remove isolated triangles, non-manifold fragments, duplicate surfaces, hidden generator debris, and unused nodes.

  3. Build one stable rig.

    • Name root, hips/center mass, head, limbs, feet, and attachment sockets explicitly.

    • Keep at most four normalized skin influences per vertex unless the engine contract says otherwise.

    • Preserve a deterministic bind pose, finite transforms, positive usable scale, and compatible skeleton identity across every clip.

  4. Build the action library.

    • Give every clip a stable id, semantic role, loop policy, root-motion policy, duration, contact events, and source.

    • Prefer animation-only GLBs for separate clips. They must not smuggle duplicate meshes, materials, or textures.

    • Make non-looping actions restartable and return to the intended locomotion/idle state.

  5. Build runtime integration.

    • Use one asset-specific loader/factory with disposal for geometry, materials, textures, mixers, events, observers, renderers, and animation frames.

    • Publish source, model, rig, socket, action, equipment, collider, triangle, material, and status metadata.

    • Keep a truthful rollback path when replacing a shipped runtime asset.

Apply the character profile

Treat the main model as the persistent body/base layer. Do not fuse swappable equipment into it.

  • Declare every canonical slot: headgear, vestment, gloves, leggings, boots, main hand, offhand, and back/ranged.

  • Mark unsupported slots explicitly instead of silently omitting them.

  • Store supported swords, shields, bows, helmets, armor, and accessories as separate assets or independently addressable roots.

  • Bind equipment to named sockets. Declare active and stowed sockets when an item changes state.

  • Toggling a slot off must leave no floating trim, grip, one-triangle shards, duplicate baked weapon, or incidental gameplay prop.

  • When the source contains no body beneath occluded equipment, create a fitted rig-following underlayer and record that limitation. Do not invent invisible anatomy claims.

  • Require the character action roles idle, walk, run, primary-attack, hit, dodge, and death. Add block/ward, charged attack, cast, bow aim/shoot, jump, interaction, or class actions when gameplay exposes them.

  • Keep equipment visibility independent from action selection unless the action deliberately changes active/stowed state.

Apply the monster profile

Treat a monster as one authored combat actor, not a player wardrobe.

  • Do not require wearable slots. Separate only weapons, breakable parts, dropped parts, projectiles, or props that must animate, hide, detach, or collide independently.

  • Require the monster action roles idle, locomotion, primary-attack, hit, and death.

  • Add secondary attack, ranged attack, pounce, cast, summon, block, stagger, roar, spawn, or phase-transition roles only when the combat design uses them.

  • Define navigation and hurt colliders plus per-attack telegraph, active, and recovery windows.

  • Bind attack origins, projectiles, VFX, audio, head/torso targeting, and feet to named sockets.

  • Keep visual animation separate from authoritative attack volumes, damage timing, AI state, and navigation.

  • Test every attack at its authored range, facing, contact window, interruption, death, and cleanup boundary.

Build the catalog lab

Create both surfaces:

  • Catalog card: transparent PNG, truthful label, zero WebGL canvases.

  • Selected inspector/review route: exactly one live model canvas with source, status, budgets, rig, sockets, actions, and known boundaries.

Provide:

  • drag-to-rotate while preserving page scroll;

  • action selection for every shipped clip, including visible Walk, Hit, and Death controls;

  • restart and pause/play;

  • equipment toggles for characters, including clean all-off and active/stowed states;

  • combat-state or attack controls for monsters instead of character gear controls;

  • a fallback image until the real model is ready;

  • deterministic runtime telemetry defined in the catalog/testing reference.

The inspector must load the same runtime model, rig, clips, equipment rules, and action API claimed by gameplay. A screenshot or separate look-dev mesh is not runtime proof.

Verify and commit

Run the project’s focused asset tests, manifest validator, build, lint, full tests, and git diff --check.

In the repository-approved browser:

  • verify the card has one transparent image and zero canvases;

  • verify the inspector has one model host/canvas;

  • click every action and confirm the selected/playing state;

  • toggle every character slot individually and all equipment off;

  • inspect front, back, and both sides for fragments, holes, grounding, and socket errors;

  • drag the model, scroll the page, and test the supported narrow/mobile layout;

  • verify fallback and model-load failure behavior;

  • verify no broken media or console warning/error.

Commit only the narrow asset lane. Report the manifest, main/runtime models, gear files, rig and sockets, actions, catalog route, status, verification results, known boundaries, and exact commit SHA. Report full-suite baseline failures separately.

Files

scripts/validate_asset_manifest.py

#!/usr/bin/env python3
"""Validate a rigged character or monster asset manifest."""

from __future__ import annotations

import argparse
import json
from pathlib import Path
from typing import Any


CHARACTER_ACTION_ROLES = {
    "idle",
    "walk",
    "run",
    "primary-attack",
    "hit",
    "dodge",
    "death",
}
MONSTER_ACTION_ROLES = {
    "idle",
    "locomotion",
    "primary-attack",
    "hit",
    "death",
}
CHARACTER_SOCKET_ROLES = {
    "root",
    "hips",
    "head",
    "left-hand",
    "right-hand",
    "back",
    "left-foot",
    "right-foot",
}
MONSTER_SOCKET_ROLES = {"root", "center-mass", "head", "vfx-origin"}
CHARACTER_SLOTS = {
    "headgear",
    "vestment",
    "gloves",
    "leggings",
    "boots",
    "main-hand",
    "offhand",
    "back-ranged",
}
COMMON_CONTROLS = {"action-select", "restart", "play-pause", "drag-rotate"}
VERIFIED_COMMON = {
    "focusedTests",
    "build",
    "lint",
    "fullTestsRun",
    "fullTestsPassedOrBaselineDocumented",
    "browserDesktop",
    "browserNarrow",
    "consoleClean",
    "scroll",
    "dragRotation",
    "allActions",
}


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Validate a build-rigged-game-assets JSON manifest.",
    )
    parser.add_argument("manifest", type=Path)
    parser.add_argument(
        "--root",
        type=Path,
        help="Repository root used to verify declared shipped files.",
    )
    parser.add_argument(
        "--require-verified",
        action="store_true",
        help="Require all implementation and browser verification gates.",
    )
    return parser.parse_args()


def is_nonempty_string(value: Any) -> bool:
    return isinstance(value, str) and bool(value.strip())


def expect_string(
    container: dict[str, Any],
    key: str,
    path: str,
    errors: list[str],
) -> str | None:
    value = container.get(key)
    if not is_nonempty_string(value):
        errors.append(f"{path}.{key} must be a non-empty string")
        return None
    return value


def expect_list(
    container: dict[str, Any],
    key: str,
    path: str,
    errors: list[str],
) -> list[Any]:
    value = container.get(key)
    if not isinstance(value, list) or not value:
        errors.append(f"{path}.{key} must be a non-empty array")
        return []
    return value


def unique_values(
    entries: list[Any],
    key: str,
    path: str,
    errors: list[str],
) -> set[str]:
    values: list[str] = []
    for index, entry in enumerate(entries):
        if not isinstance(entry, dict):
            errors.append(f"{path}[{index}] must be an object")
            continue
        value = entry.get(key)
        if not is_nonempty_string(value):
            errors.append(f"{path}[{index}].{key} must be a non-empty string")
            continue
        values.append(value)
    duplicates = sorted({value for value in values if values.count(value) > 1})
    if duplicates:
        errors.append(f"{path} contains duplicate {key} values: {duplicates}")
    return set(values)


def resolve_declared_file(root: Path, declared: str) -> Path:
    path = Path(declared)
    if path.is_absolute():
        public_candidate = root / "public" / declared.lstrip("/")
        return public_candidate if public_candidate.exists() else path
    return root / path


def validate_files(
    manifest: dict[str, Any],
    root: Path | None,
    errors: list[str],
) -> None:
    files = manifest.get("files")
    if not isinstance(files, dict):
        errors.append("files must be an object")
        return
    required = ["sourceModel", "mainModel", "catalogPng"]
    if manifest.get("status") == "runtime":
        required.append("runtimeModel")
    declared: list[tuple[str, str]] = []
    for key in required:
        value = expect_string(files, key, "files", errors)
        if value:
            declared.append((f"files.{key}", value))

    catalog = manifest.get("catalog")
    if isinstance(catalog, dict):
        card_image = expect_string(catalog, "cardImage", "catalog", errors)
        if card_image:
            declared.append(("catalog.cardImage", card_image))

    actions = manifest.get("actions")
    if isinstance(actions, list):
        for index, action in enumerate(actions):
            if not isinstance(action, dict):
                continue
            action_file = expect_string(
                action,
                "file",
                f"actions[{index}]",
                errors,
            )
            if action_file:
                declared.append((f"actions[{index}].file", action_file))

    equipment = manifest.get("equipment")
    if isinstance(equipment, dict):
        slots = equipment.get("slots")
        if isinstance(slots, list):
            for index, slot in enumerate(slots):
                if not isinstance(slot, dict) or slot.get("supported") is not True:
                    continue
                asset = expect_string(
                    slot,
                    "asset",
                    f"equipment.slots[{index}]",
                    errors,
                )
                if asset:
                    declared.append((f"equipment.slots[{index}].asset", asset))

    if root:
        for field, declared_path in declared:
            if not resolve_declared_file(root, declared_path).is_file():
                errors.append(f"{field} does not resolve to a shipped file: {declared_path}")


def validate_rig(
    manifest: dict[str, Any],
    kind: str,
    errors: list[str],
) -> None:
    rig = manifest.get("rig")
    if not isinstance(rig, dict):
        errors.append("rig must be an object")
        return
    expect_string(rig, "skeletonId", "rig", errors)
    expect_string(rig, "rootBone", "rig", errors)
    if rig.get("maxSkinInfluences") != 4:
        errors.append("rig.maxSkinInfluences must be 4")
    height = rig.get("actorHeightMeters")
    if not isinstance(height, (int, float)) or height <= 0:
        errors.append("rig.actorHeightMeters must be a positive number")
    sockets = expect_list(rig, "sockets", "rig", errors)
    roles = unique_values(sockets, "role", "rig.sockets", errors)
    required = (
        CHARACTER_SOCKET_ROLES
        if kind == "character"
        else MONSTER_SOCKET_ROLES
    )
    missing = sorted(required - roles)
    if missing:
        errors.append(f"rig.sockets is missing required roles: {missing}")
    for index, socket in enumerate(sockets):
        if isinstance(socket, dict):
            expect_string(socket, "name", f"rig.sockets[{index}]", errors)


def validate_actions(
    manifest: dict[str, Any],
    kind: str,
    errors: list[str],
) -> set[str]:
    actions = expect_list(manifest, "actions", "manifest", errors)
    unique_values(actions, "id", "actions", errors)
    roles = unique_values(actions, "role", "actions", errors)
    required = (
        CHARACTER_ACTION_ROLES
        if kind == "character"
        else MONSTER_ACTION_ROLES
    )
    missing = sorted(required - roles)
    if missing:
        errors.append(f"actions is missing required roles: {missing}")
    valid_root_motion = {"in-place", "authored", "extracted"}
    for index, action in enumerate(actions):
        if not isinstance(action, dict):
            continue
        if not isinstance(action.get("loop"), bool):
            errors.append(f"actions[{index}].loop must be a boolean")
        if action.get("rootMotion") not in valid_root_motion:
            errors.append(
                f"actions[{index}].rootMotion must be one of {sorted(valid_root_motion)}",
            )
        duration = action.get("durationSeconds")
        if not isinstance(duration, (int, float)) or duration <= 0:
            errors.append(f"actions[{index}].durationSeconds must be positive")
        if not isinstance(action.get("contactEvents"), list):
            errors.append(f"actions[{index}].contactEvents must be an array")
    return {
        action.get("id")
        for action in actions
        if isinstance(action, dict) and is_nonempty_string(action.get("id"))
    }


def validate_character(manifest: dict[str, Any], errors: list[str]) -> None:
    equipment = manifest.get("equipment")
    if not isinstance(equipment, dict):
        errors.append("character equipment must be an object")
        return
    if equipment.get("separateFromMainModel") is not True:
        errors.append("character equipment.separateFromMainModel must be true")
    slots = expect_list(equipment, "slots", "equipment", errors)
    slot_ids = unique_values(slots, "id", "equipment.slots", errors)
    missing = sorted(CHARACTER_SLOTS - slot_ids)
    if missing:
        errors.append(f"equipment.slots is missing canonical declarations: {missing}")
    for index, slot in enumerate(slots):
        if not isinstance(slot, dict):
            continue
        if not isinstance(slot.get("supported"), bool):
            errors.append(f"equipment.slots[{index}].supported must be a boolean")
        if slot.get("supported") is True:
            expect_string(slot, "asset", f"equipment.slots[{index}]", errors)
            expect_string(slot, "socket", f"equipment.slots[{index}]", errors)
    collision = manifest.get("collision")
    if not isinstance(collision, dict):
        errors.append("character collision must be an object")
    elif not isinstance(collision.get("navigation"), dict):
        errors.append("character collision.navigation must be an object")
    elif not isinstance(collision.get("hurtboxes"), list) or not collision["hurtboxes"]:
        errors.append("character collision.hurtboxes must be a non-empty array")


def validate_monster(
    manifest: dict[str, Any],
    action_ids: set[str],
    errors: list[str],
) -> None:
    equipment = manifest.get("equipment")
    if isinstance(equipment, dict) and equipment.get("separateFromMainModel") is not True:
        errors.append("monster equipment must be separate when declared")
    combat = manifest.get("combat")
    if not isinstance(combat, dict):
        errors.append("monster combat must be an object")
        return
    if not isinstance(combat.get("navigationCollider"), dict):
        errors.append("monster combat.navigationCollider must be an object")
    hurtboxes = combat.get("hurtboxes")
    if not isinstance(hurtboxes, list) or not hurtboxes:
        errors.append("monster combat.hurtboxes must be a non-empty array")
    attacks = combat.get("attacks")
    if not isinstance(attacks, list) or not attacks:
        errors.append("monster combat.attacks must be a non-empty array")
        return
    unique_values(attacks, "id", "combat.attacks", errors)
    for index, attack in enumerate(attacks):
        if not isinstance(attack, dict):
            continue
        action_id = expect_string(
            attack,
            "actionId",
            f"combat.attacks[{index}]",
            errors,
        )
        if action_id and action_id not in action_ids:
            errors.append(
                f"combat.attacks[{index}].actionId is not declared in actions: {action_id}",
            )
        expect_string(
            attack,
            "originSocket",
            f"combat.attacks[{index}]",
            errors,
        )
        for field in ("telegraphSeconds", "activeSeconds", "recoverySeconds"):
            value = attack.get(field)
            if not isinstance(value, (int, float)) or value < 0:
                errors.append(
                    f"combat.attacks[{index}].{field} must be a non-negative number",
                )


def validate_catalog(
    manifest: dict[str, Any],
    kind: str,
    errors: list[str],
) -> None:
    catalog = manifest.get("catalog")
    if not isinstance(catalog, dict):
        errors.append("catalog must be an object")
        return
    expect_string(catalog, "inspectorRoute", "catalog", errors)
    if catalog.get("cardCanvasCount") != 0:
        errors.append("catalog.cardCanvasCount must be 0")
    if catalog.get("inspectorCanvasCount") != 1:
        errors.append("catalog.inspectorCanvasCount must be 1")
    controls = catalog.get("controls")
    if not isinstance(controls, list):
        errors.append("catalog.controls must be an array")
        return
    control_set = {control for control in controls if isinstance(control, str)}
    required = set(COMMON_CONTROLS)
    required.add("equipment-toggle" if kind == "character" else "combat-state")
    missing = sorted(required - control_set)
    if missing:
        errors.append(f"catalog.controls is missing required controls: {missing}")


def validate_verification(
    manifest: dict[str, Any],
    kind: str,
    require_verified: bool,
    errors: list[str],
) -> None:
    verification = manifest.get("verification")
    if not isinstance(verification, dict):
        errors.append("verification must be an object")
        return
    required = set(VERIFIED_COMMON)
    required.add(
        "equipmentOffArtifactFree"
        if kind == "character"
        else "monsterCombatContract",
    )
    missing = sorted(required - set(verification))
    if missing:
        errors.append(f"verification is missing gates: {missing}")
    for key in sorted(required & set(verification)):
        if not isinstance(verification[key], bool):
            errors.append(f"verification.{key} must be a boolean")
        elif require_verified and verification[key] is not True:
            errors.append(f"verification.{key} must be true for release")


def validate_manifest(
    manifest: dict[str, Any],
    root: Path | None,
    require_verified: bool,
) -> list[str]:
    errors: list[str] = []
    if manifest.get("schemaVersion") != 1:
        errors.append("schemaVersion must be 1")
    expect_string(manifest, "id", "manifest", errors)
    kind = manifest.get("kind")
    if kind not in {"character", "monster"}:
        errors.append("kind must be character or monster")
        kind = "character"
    if manifest.get("status") not in {"catalog-only", "review-only", "runtime"}:
        errors.append("status must be catalog-only, review-only, or runtime")
    provenance = manifest.get("provenance")
    if not isinstance(provenance, dict):
        errors.append("provenance must be an object")
    else:
        expect_string(provenance, "pipeline", "provenance", errors)
        expect_string(provenance, "source", "provenance", errors)
    budget = manifest.get("budget")
    if not isinstance(budget, dict):
        errors.append("budget must be an object")
    else:
        for field in (
            "runtimeTriangles",
            "runtimeBytes",
            "materials",
            "textures",
            "skinnedMeshes",
            "bones",
        ):
            value = budget.get(field)
            if not isinstance(value, int) or value < 0:
                errors.append(f"budget.{field} must be a non-negative integer")

    validate_files(manifest, root, errors)
    validate_rig(manifest, kind, errors)
    action_ids = validate_actions(manifest, kind, errors)
    if kind == "character":
        validate_character(manifest, errors)
    else:
        validate_monster(manifest, action_ids, errors)
    validate_catalog(manifest, kind, errors)
    validate_verification(manifest, kind, require_verified, errors)
    return errors


def main() -> int:
    args = parse_args()
    try:
        manifest = json.loads(args.manifest.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as error:
        print(f"[FAIL] Could not read manifest: {error}")
        return 1
    if not isinstance(manifest, dict):
        print("[FAIL] Manifest root must be an object")
        return 1
    root = args.root.resolve() if args.root else None
    errors = validate_manifest(manifest, root, args.require_verified)
    if errors:
        print(f"[FAIL] {args.manifest}: {len(errors)} error(s)")
        for error in errors:
            print(f"  - {error}")
        return 1
    print(
        f"[OK] {args.manifest}: {manifest['kind']} {manifest['id']} "
        f"({manifest['status']})",
    )
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs Python

Security

What our scanner noticed· Low risk

  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
Meng To
Publisher
Meng To (Community)
Source
MengTo/Skills · agent-skills/game-development/build-rigged-game-assets · commit 5ea1661
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Meng To or Meng To.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download build-rigged-game-assets.zip