Author an enemy presentation prefab
verifiedAgainst e50826a · verifiedOn 2026-09-10.
What you will build
Section titled “What you will build”Shambler and Ghoul are the primary nighttime hostile visual archetypes in Voxamine. They represent humanoid threats that roam the dark wilderness outside daylight safety. Rather than hand-editing third-party vendor assets or placing test instances inside the active Gameplay.unity scene, you will author a reproducible, project-owned presentation pipeline using EnemyLab (EnemyLabModule.cs).
This pipeline isolates vendor models (Assets/ThirdParty/Kenney_*) and animation takes (Assets/ThirdParty/Universal_Animation_Library_Standard/), enforces Humanoid rig retargeting, generates clean project-owned HDRP Lit materials, builds a non-root-motion AnimatorController featuring a 1D Speed locomotion blend tree with combat trigger transitions, configures a standard CapsuleCollider, and outputs production-ready prefabs to Assets/_Game/Resources/Prefabs/Enemies/.
// The project-owned prefab output destination:// Assets/_Game/Resources/Prefabs/Enemies/Ghoul.prefabWhere this sits
Section titled “Where this sits”Enemy presentation authoring enforces a strict boundary between raw vendor art, editor generation tooling, project-owned serialized assets, and runtime entity simulation:
- ThirdParty (Vendor) contains pristine, unmodified source FBX models, textures, and animation clips.
- Voxel Workshop (
EnemyLab) reads the vendor sources, verifies their import settings, and constructs project-owned assets. - Resources (
Prefabs/Enemies) holds the generated standalone prefabs, materials, and animator controllers. - World Entities (
VoxelSandbox.World) deterministically plans spawn locations and tracks pure combat state (HostileMob,HostileSpawnPlanner) completely independent of scene markup.
flowchart TD
subgraph VENDOR["Assets/ThirdParty/ (Quarantined)"]
KENNEY_S["Kenney Survivors FBX\n(characterMedium.fbx + zombieA.png)"]
KENNEY_R["Kenney Retro FBX\n(characterMedium.fbx + zombieFemaleA.png)"]
UAL_ANIM["Universal Animation Library\n(UAL1_Standard.fbx)"]
end
subgraph BUILDER["Editor / Voxel Workshop"]
LAB_MOD["EnemyLabModule\n(Workshop UI & validation)"]
PRES_BLD["EnemyPresentationBuilder\n(Rig setup, material composition, blend tree authoring)"]
end
subgraph PROJECT_ASSETS["Assets/_Game/ (Project Owned)"]
MATS["Art/Materials/Enemies/\n(Shambler.mat, Ghoul.mat)"]
CTRLS["Art/Animation/Enemies/\n(1D Speed BlendTree, Attack, Hit, Death)"]
PREFABS["Resources/Prefabs/Enemies/\n(Shambler.prefab, Ghoul.prefab)"]
end
subgraph SIMULATION["VoxelSandbox.World.Entities"]
SPAWN_RULES["HostileSpawnRules\n(Daylight & weather dimming)"]
PLANNER["HostileSpawnPlanner\n(Deterministic seed hashing & terrain gating)"]
HOSTILE_MOB["HostileMob\n(WalkSpeed, AttackDamage, AttackReach, Cooldown)"]
end
KENNEY_S --> PRES_BLD
KENNEY_R --> PRES_BLD
UAL_ANIM --> PRES_BLD
LAB_MOD --> PRES_BLD
PRES_BLD --> MATS
PRES_BLD --> CTRLS
PRES_BLD --> PREFABS
PLANNER --> PREFABS
SPAWN_RULES --> PLANNER
HOSTILE_MOB -.-> PREFABS
Gameplay.unity is never dirtied by mob composition. The spawner loads prefabs dynamically from Resources at runtime when HostileSpawnPlanner yields a valid candidate.
Before you start
Section titled “Before you start”Read Docs/MASTERPLAN.md §31 (“The Lab is the Monster”) for hostile encounter philosophies and review:
EnemyLabModule, the Voxel Workshop UI integration.EnemyPresentationBuilder, the core procedural prefab builder.HostileMob, defining kinematic movement and combat attributes.HostileSpawnRules, defining daylight and weather spawn suppression thresholds.HostileSpawnPlanner, deterministic coordinate candidate selection.
Decide these design constraints before assembling enemy visuals:
| Decision | Specification | Architectural Rationale |
|---|---|---|
| Rig import type | ModelImporterAnimationType.Human |
Humanoid avatar retargeting decouples vendor skeletal bone naming differences from shared clips. |
| Root motion | applyRootMotion = false |
Movement speed and collision displacement must be governed by simulation and nav/kinematics, never visual animation displacement. |
| Animator culling | CullUpdateTransforms |
Disables bone transform updates when out of camera frustum while preserving state machine progression. |
| Material shader | HDRP/Lit (with standard fallback) |
Matches project render pipeline; sets low smoothness ($0.12$) for cloth/flesh specular response. |
| Locomotion blending | 1D Blend Tree on Speed ($[0, 1]$) |
Smooth interpolation between Idle_Loop ($0.0$), Walk_Loop ($0.5$), and Jog_Fwd_Loop ($1.0$). |
| Collider | CapsuleCollider ($r=0.28\text{ m}, h=1.8\text{ m}, y_c=0.9\text{ m}$) |
Standard humanoid physical envelope conforming to $1\times 2$ block doorways. |
The build, step by step
Section titled “The build, step by step”1. Configure Humanoid rig imports for vendor models
Section titled “1. Configure Humanoid rig imports for vendor models”Both the character mesh FBX files and the animation library FBX must be imported as Humanoid rigs so Unity generates an internal Avatar for clip retargeting. EnemyPresentationBuilder.ConfigureHumanoidImport automates this:
private static void ConfigureHumanoidImport(string path){ if (AssetImporter.GetAtPath(path) is not ModelImporter importer) { throw new InvalidOperationException($"Expected a model importer at {path}."); }
if (importer.animationType == ModelImporterAnimationType.Human) { return; }
importer.animationType = ModelImporterAnimationType.Human; importer.avatarSetup = ModelImporterAvatarSetup.CreateFromThisModel; importer.SaveAndReimport();}This ensures that animations authored on standard skeletons can seamlessly drive the Kenney low-poly character meshes.
2. Isolate and resolve required animation takes
Section titled “2. Isolate and resolve required animation takes”The vendor animation library (UAL1_Standard.fbx) bundles dozens of motion clips. Some import versions prefix clips with Armature|. The builder safely strips this prefix and extracts the exact six clips required for basic combat locomotion:
private static readonly string[] RequiredClipNames ={ "Idle_Loop", "Walk_Loop", "Jog_Fwd_Loop", "Punch_Jab", "Hit_Chest", "Death01"};
private static Dictionary<string, AnimationClip> FindRequiredAnimationClips(){ var clips = new Dictionary<string, AnimationClip>(StringComparer.Ordinal); foreach (UnityEngine.Object asset in AssetDatabase.LoadAllAssetsAtPath(AnimationSourcePath)) { if (asset is not AnimationClip clip) continue;
foreach (string requiredName in RequiredClipNames) { if (clip.name == requiredName || clip.name.EndsWith($"|{requiredName}", StringComparison.Ordinal)) { clips[requiredName] = clip; break; } } } return clips;}3. Create project-owned materials
Section titled “3. Create project-owned materials”Never apply textures directly to vendor materials. Create dedicated HDRP materials in Assets/_Game/Art/Materials/Enemies/ with tailored albedo skins and species tinting:
private static Material LoadOrCreateMaterial(string displayName, string skinPath, Color tint){ string path = $"{MaterialDirectory}/{displayName}.mat"; Material material = AssetDatabase.LoadAssetAtPath<Material>(path); if (material == null) { Shader shader = Shader.Find("HDRP/Lit") ?? Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard"); material = new Material(shader); AssetDatabase.CreateAsset(material, path); }
Texture2D skin = AssetDatabase.LoadAssetAtPath<Texture2D>(skinPath); material.name = displayName; SetTextureIfPresent(material, "_BaseColorMap", skin); SetTextureIfPresent(material, "_MainTex", skin); SetColorIfPresent(material, "_BaseColor", tint); SetColorIfPresent(material, "_Color", tint); if (material.HasProperty("_Smoothness")) { material.SetFloat("_Smoothness", 0.12f); }
EditorUtility.SetDirty(material); return material;}The Shambler receives a desiccated pale green tint (#ABD194), while the Ghoul receives an ethereal bluish-green tint (#BDDCA8).
4. Build the Animator Controller state machine
Section titled “4. Build the Animator Controller state machine”Author the AnimatorController with a single base layer containing:
- Float parameter
Speed - Trigger parameter
Attack - Trigger parameter
Hit - Bool parameter
IsDead
private static AnimatorController CreateController(string displayName, IReadOnlyDictionary<string, AnimationClip> clips){ string path = $"{ControllerDirectory}/{displayName}.controller"; if (AssetDatabase.LoadAssetAtPath<AnimatorController>(path) != null) { AssetDatabase.DeleteAsset(path); }
AnimatorController controller = AnimatorController.CreateAnimatorControllerAtPath(path); controller.AddParameter("Speed", AnimatorControllerParameterType.Float); controller.AddParameter("Attack", AnimatorControllerParameterType.Trigger); controller.AddParameter("Hit", AnimatorControllerParameterType.Trigger); controller.AddParameter("IsDead", AnimatorControllerParameterType.Bool);
AnimatorStateMachine machine = controller.layers[0].stateMachine;
// Locomotion 1D Blend Tree AnimatorState locomotion = machine.AddState("Locomotion"); locomotion.motion = CreateLocomotionBlendTree(controller, clips); machine.defaultState = locomotion;
// Action states AnimatorState attack = machine.AddState("Attack"); attack.motion = clips["Punch_Jab"]; AnimatorState hit = machine.AddState("Hit"); hit.motion = clips["Hit_Chest"]; AnimatorState death = machine.AddState("Death"); death.motion = clips["Death01"];
// Transitions from AnyState AddTriggerTransition(machine, attack, "Attack"); AddTriggerTransition(machine, hit, "Hit");
AnimatorStateTransition deathTransition = machine.AddAnyStateTransition(death); deathTransition.hasExitTime = false; deathTransition.duration = 0.08f; deathTransition.canTransitionToSelf = false; deathTransition.AddCondition(AnimatorConditionMode.If, 0f, "IsDead");
AddReturnTransition(attack, locomotion); AddReturnTransition(hit, locomotion);
EditorUtility.SetDirty(controller); return controller;}The locomotion blend tree maps normalized speed thresholds:
private static BlendTree CreateLocomotionBlendTree(AnimatorController controller, IReadOnlyDictionary<string, AnimationClip> clips){ var tree = new BlendTree { name = "Locomotion Blend Tree", blendType = BlendTreeType.Simple1D, blendParameter = "Speed", useAutomaticThresholds = false }; tree.AddChild(clips["Idle_Loop"], 0f); tree.AddChild(clips["Walk_Loop"], 0.5f); tree.AddChild(clips["Jog_Fwd_Loop"], 1f); AssetDatabase.AddObjectToAsset(tree, controller); return tree;}5. Assemble and save the presentation prefab
Section titled “5. Assemble and save the presentation prefab”Instantiate the base Kenney mesh in memory, reassign its renderers to the project material, attach and configure the Animator component, set up the CapsuleCollider, and save to Assets/_Game/Resources/Prefabs/Enemies/:
private static void BuildEnemy( string displayName, string modelPath, string skinPath, Color tint, IReadOnlyDictionary<string, AnimationClip> clips){ Material material = LoadOrCreateMaterial(displayName, skinPath, tint); AnimatorController controller = CreateController(displayName, clips);
GameObject sourceModel = AssetDatabase.LoadAssetAtPath<GameObject>(modelPath); GameObject instance = (GameObject)PrefabUtility.InstantiatePrefab(sourceModel); try { instance.name = displayName; foreach (Renderer renderer in instance.GetComponentsInChildren<Renderer>(true)) { renderer.sharedMaterial = material; }
Animator animator = instance.GetComponent<Animator>() ?? instance.AddComponent<Animator>(); animator.runtimeAnimatorController = controller; animator.applyRootMotion = false; animator.cullingMode = AnimatorCullingMode.CullUpdateTransforms;
CapsuleCollider collider = instance.GetComponent<CapsuleCollider>() ?? instance.AddComponent<CapsuleCollider>(); collider.center = new Vector3(0f, 0.9f, 0f); collider.height = 1.8f; collider.radius = 0.28f;
PrefabUtility.SaveAsPrefabAsset(instance, $"{PrefabDirectory}/{displayName}.prefab"); } finally { UnityEngine.Object.DestroyImmediate(instance); }}6. Verify deterministic surface spawn proposals
Section titled “6. Verify deterministic surface spawn proposals”At runtime, enemies are not baked into the scene; they are planned by HostileSpawnPlanner.TryPlanSurfaceSpawn. The planner ensures:
- Daylight conditions allow spawning via
HostileSpawnRules.CanAttemptSurfaceSpawn. - Candidate coordinates fall between $18\text{ m}$ and $32\text{ m}$ from the player column.
- Candidate terrain is dry land above sea level and not carved by an active river.
- Hash parity determines whether a
ShamblerorGhoulis requested:
EntityKind kind = (hash & 0x80000000u) == 0u ? EntityKind.Shambler : EntityKind.Ghoul;candidate = new HostileSpawnCandidate(kind, worldX, surfaceHeight + 1, worldZ);Verify
Section titled “Verify”1. Execute EnemyLab in the Editor
Section titled “1. Execute EnemyLab in the Editor”Open Tools → Voxel Sandbox → Voxel Workshop → Enemy Lab in Unity.
- Confirm source assets show green status (“Sources are ready”).
- Click Build / Replace Enemy Prefabs.
- Inspect
Assets/_Game/Resources/Prefabs/Enemies/:Shambler.prefabexists with assigned material, animator, and capsule collider.Ghoul.prefabexists with matching structure.
- Select
Shambler.prefaband open the Animator window: confirm theLocomotion Blend TreelinksIdle_Loop,Walk_Loop, andJog_Fwd_Loop.
2. Headless Test Execution
Section titled “2. Headless Test Execution”Run the EditMode test fixture headlessly via bash to verify spawn planning logic:
/opt/unity/Editor/Unity -batchmode -nographics \ -projectPath /home/soulwax/workspace/engines/unity/minecraft/Minecraft-HD \ -runTests -testPlatform EditMode \ -testFilter VoxelSandbox.Tests.HostileSpawnPlannerTestsConfirm all tests pass:
TryPlanSurfaceSpawn_RejectsFullDaylightTryPlanSurfaceSpawn_RejectsMissingGenerationSettingsTryPlanSurfaceSpawn_IsDeterministicAndUsesDryLandTryPlanSurfaceSpawn_DoesNotChooseThePlayerColumn
Now do your own
Section titled “Now do your own”To introduce an additional hostile visual archetype (e.g. Wraith or Dredge):
- Place source art in vendor quarantine: Add the character mesh FBX and skin PNG into
Assets/ThirdParty/<Vendor>/. Never modify these files directly. - Declare constants: Add the source paths and output prefab name to
EnemyPresentationBuilder. - Register clip requirements: If the archetype requires special animation takes (e.g.
Crawl_LooporCast_Spell), ensure they exist inRequiredClipNames. - Call
BuildEnemy: Provide the archetype name, model path, skin path, and distinctive tint color. - Register entity kind: Add the archetype to
EntityKindinVoxelSandbox.World.Entitiesand assign spawn weights inHostileSpawnPlanner. - Rebuild via EnemyLab: Open the EnemyLab module, click Build, and verify the resulting prefab in the Inspector.
Pitfalls
Section titled “Pitfalls”| Symptom | Cause | Fix |
|---|---|---|
| Mob slides or drifts rapidly during walk cycles. | Root motion is enabled on the Animator component. |
Ensure animator.applyRootMotion = false. Physical locomotion must be driven by simulation velocities, not animation displacement curves. |
| Enemy prefab appears pink in Play Mode. | Material shader defaulted to Built-in Standard instead of HDRP Lit. | Verify the shader lookup finds HDRP/Lit (Shader.Find("HDRP/Lit")) and sets the _BaseColorMap texture property. |
| Animation clips fail to play or T-pose occurs. | FBX model importer was left on Generic or Legacy animation type. |
Force ModelImporterAnimationType.Human with ModelImporterAvatarSetup.CreateFromThisModel on all models and animation sources. |
Git shows unexpected modifications in Gameplay.unity. |
Someone dragged an enemy prefab into the scene hierarchy while testing. | Revert Gameplay.unity. Hostile entities must be spawned programmatically at runtime from Resources/Prefabs/Enemies/. |
| Mobs spawn underwater or inside river gorges. | Spawn planner neglected terrain height or river carve checks. | Gate spawn coordinates against TerrainGenerator.GetSurfaceHeight > SeaLevel and !RiverField.TryGetRiverCarve. |
| Missing animation clip runtime exception. | The FBX animation take was renamed or prepended with Armature|. |
Use FindRequiredAnimationClips() suffix matching (`clip.name.EndsWith($” |
User-contributed notes
Corrections, clarifications, and practical tips for this page. Anonymous is fine — a name is optional. Basic Markdown works:
**bold**,*italic*,`code`, and links.Notes policy
Notes are lightly filtered for spam and may be edited or removed. Keep them about this page — no support requests, no personal data, nothing you would not publish. Links are limited and marked
nofollow.