Advanced Surface System Topics

Use these workflows when a basic Surface Identifier is not enough: one renderer contains several materials, a terrain blends textures, a runtime object changes material identity, or frequent impacts need stricter control over decals, pooling, and detection cost.

Before you begin

Build and verify one simple response first:

  1. Add the scene managers with Tools > Opsive > Ultimate Character Controller > Setup Manager, Scene > Manager Setup > Add Managers.
  2. Confirm the scene’s Game object has Surface Manager, Decal Manager, and the shared Object Pool.
  3. Create one Surface Impact, Surface Type, and Surface Effect, then map the impact to the effect in the type’s Impact Effects list.
  4. Add Surface Identifier to a test collider, assign the type, and trigger the impact in Play Mode.
  5. Continue only after that direct route resolves the expected sound, object, decal, or State.

The advanced resolvers still end at the same Surface Impact + Surface Type mapping. They change how the Surface Type is detected, not what the Surface Effect means.

Choose the simplest reliable resolver

Situation Recommended route
One collider has one logical material Add Surface Identifier. It is the first lookup and avoids texture, UV, terrain, and submesh analysis.
Many simple objects share one texture Add one Object Surfaces entry on Surface Manager, assign the Surface Type, select Add Texture, and keep UV at (0, 0, 1, 1).
One mesh uses several renderer materials Use a readable mesh, a MeshCollider that produces a triangle index, and matching renderer/collider triangle and submesh data. Map every material texture once.
One texture atlas represents several surfaces Prefer separate colliders with Surface Identifiers in released Version 3.2.0. Repeating one texture for several UV regions has an initialization defect described below.
Terrain blends several layers Map each Terrain Layer diffuse texture to its Surface Type. The manager chooses the dominant layer at the hit position.
A runtime object changes from dry to wet, clean to dirty, or another identity Put Surface Identifier on the collider before its first hit and change its SurfaceType property. Do not rebuild texture or hierarchy mappings at runtime.
The project needs a different query or response Perform the project-owned detection, use the public diagnostic APIs where helpful, or derive a Surface Effect/Character Foot Effects response. There is no pluggable resolver interface in this release.

Follow the detection and fallback order

For each RaycastHit, Surface Manager checks:

  1. a cached Surface Identifier on the hit Collider GameObject, then one found in its children, then one found in its parents;
  2. a cached simple main texture mapping;
  3. multi-material, UV-region, or supported secondary-texture resolution;
  4. the dominant mapped Terrain Layer texture, optionally including terrain trees; and
  5. Surface Manager Fallbacks for missing inputs or a missing pair on a type with a nonempty Impact Effects list. A known type with an empty list exits without a fallback response.

Keep one obvious Surface Identifier near each collider when a hierarchy contains several renderers or identifiers. The first child or parent discovered by the component searches is cached; rearranging that hierarchy later does not force a new search.

The fallback chain is pair-based:

  • a missing Surface Impact is replaced by Fallback Surface Impact;
  • a missing Surface Type is replaced by Fallback Surface Type;
  • if the initially resolved type has no Impact Effects entries, resolution stops with no effect;
  • otherwise, when the resolved type has no mapping for the current impact, Version 3 next tries the current impact on Fallback Surface Type;
  • if that still has no effect, it tries Fallback Surface Impact on that fallback type; and
  • if no valid pair exists, nothing spawns.

Do not use fallbacks to hide incomplete core mappings. Add an explicit Impact Effect entry to every known Surface Type, then reserve fallbacks for genuinely unknown inputs.

Released Version 3.2.0 applies Fallback Allow Decals when the impact or type was initially missing. It does not mark the later “pair missing, switch to fallback type” path as a fallback for that decal check. When a fallback effect must never create decals, omit decals from that effect or provide every expected explicit pair.

Map textures and multiple materials

One texture per surface

  1. Select Surface Manager on the scene’s Game object.
  2. Add an Object Surfaces row and select it.
  3. Assign its Surface Type, select Add Texture, and assign the exact texture used by the material.
  4. Leave UV at (0, 0, 1, 1) for the complete texture.
  5. Set Main Texture Property Name to the shader property that holds the texture. The released default is _BaseMap; a shader using _MainTex or a custom name must be configured explicitly before Play Mode.
  6. Restart Play Mode after changing the list or shader-property name so the manager rebuilds its lookup dictionaries and property ID in Awake.

A material with no usable texture cannot participate in texture mapping. Add Surface Identifier to its collider instead.

Multiple renderer materials

The released resolver can identify the hit submesh, but only when all of these conditions are true:

  • the physics hit supplies triangleIndex, which requires a MeshCollider;
  • Surface Manager can find an enabled, non-skinned Renderer on the collider, a child, or a parent;
  • it can find a readable MeshFilter mesh or readable MeshCollider shared mesh with triangle data;
  • the hit triangle indexes correspond to the Renderer mesh submeshes; and
  • the Renderer has a non-null shared material at the resolved submesh index.

This corrects the legacy claim that a MeshCollider itself prevents multi-material detection. In Version 3.2.0 the MeshCollider hit is required for the triangle/submesh path. Detection still fails when collider and renderer meshes do not share the same logical triangle layout, when Read/Write is disabled, or when static batching changes the mesh relationship. Use separate single-material colliders with Surface Identifiers when that relationship cannot be guaranteed.

UV regions and secondary textures

The Inspector exposes repeated UV rectangles, and the runtime can compare an adjusted RaycastHit.textureCoord against a region. However, released Version 3.2.0 also inserts every configured texture into a single texture-to-surface dictionary with Add. Registering the same atlas texture more than once can throw a duplicate-key exception during Surface Manager Awake before those UV mappings are usable.

For this release, do not build production detection around repeated atlas entries. Split the geometry or colliders and assign Surface Identifiers, use distinct textures, or maintain a bounded project resolver. A UV region also requires a MeshCollider; a Unity primitive Collider does not provide the needed triangle/texture-coordinate route.

The built-in secondary-map branch requires a verified initialization fix before use with an explicit scene manager: the scene component marks the singleton initialized before the static shader-property IDs are assigned. Prefer Surface Identifier for this setup; see Surface Manager. With those IDs initialized, the branch recognizes material properties named _Mask and _MainTex2. It reads the mask texture pixel at the hit UV and selects the secondary texture when alpha exceeds 0.5. The mask must be a readable Texture2D; otherwise use a Surface Identifier or project-specific material resolver.

Detect terrain and trees

  1. Add each Terrain Layer’s Diffuse Texture to the appropriate Surface Manager Object Surfaces mapping.
  2. Trigger impacts on clearly dominant areas first. The manager samples the terrain alphamap cell at the hit position and chooses the layer with the highest weight; it does not blend two Surface Effects.
  3. Test painted boundaries repeatedly and decide whether a dominant-layer switch is acceptable for footsteps and impacts.
  4. Leave Detect Terrain Tree Textures disabled unless trees genuinely need texture resolution. It starts disabled and its tooltip correctly warns that the operation is CPU intensive.

Tree detection iterates terrain tree instances, obtains a prototype object through the pool, positions it, synchronizes physics, and raycasts its Collider until a texture is found. Use direct identifiers on independently instantiated tree prefabs or another project-owned lookup for frequent impacts in a large forest.

Terrain lookup also uses the same texture-to-surface dictionary initialized from Object Surfaces. One exact texture should map to one Surface Type.

Plan runtime surface changes around the caches

Surface Manager caches identifiers, decal permission, renderers, meshes, main textures, simple detected types, terrain components, and impact-to-effect dictionaries. Its Object Surfaces maps and main texture property ID are initialized in Awake.

Runtime change Released-Version-3 behavior
Change SurfaceIdentifier.SurfaceType on an identifier that existed before detection Supported for later type queries because the cached identifier reference is read again.
Change SurfaceIdentifier.AllowDecals after the first lookup Not reflected; decal permission is cached as a Boolean per Collider.
Add or move a Surface Identifier after the Collider was queried Not rediscovered; the earlier component or null result remains cached.
Swap a Renderer, Mesh, material, main texture, or collider hierarchy after detection Existing cache entries remain; later hits can use the original relationship.
Edit Surface Type Impact Effects after that type resolved once The first mapping dictionary remains cached. Duplicate Surface Impact entries also use only the first and log a warning.
Edit Object Surfaces or Main Texture Property Name after Awake The lookup dictionaries or shader property ID are not rebuilt. The public name setter alone does not refresh the ID.
Spawn a new, fully configured Collider and identifier Its first lookup can be cached normally. Keep its hierarchy and identifier stable afterward.

For a supported wet/dry switch, author both Surface Types, put one Surface Identifier on the collider from the start, and swap only its SurfaceType. Keep Allow Decals constant. When the renderer itself must change, let Surface Identifier remain the authoritative material identity instead of asking the texture resolver to notice the swap.

There is no public cache-clear or rebuild API. Stop and restart Play Mode for authored changes, reload the scene for a deliberate runtime rebuild, or own the dynamic detection in project code.

Coordinate impacts, effects, footsteps, and decals

  • Give each cause its own Surface Impact, such as Footstep, Bullet Hit, or Melee Hit. Do not create a different impact for wood and metal; Surface Type owns that distinction.
  • In every Surface Type, keep one Impact Effects entry per Surface Impact. The first duplicate wins and later duplicates are ignored with a warning.
  • A Surface Effect can spawn pooled objects, play an Audio Config or clips, activate a State on the hit GameObject, and request one decal. Keep high-frequency footstep effects lighter than rare explosions or heavy impacts.
  • Allow Decals on Surface Identifier is cached per collider. Allow Footprints on Surface Type selects SpawnFootprint when true; when false, the manager calls the regular Spawn response instead, so audio, objects, State, and an ordinary decal can still run.
  • Character Foot Effects raycasts below the chosen foot and calls the footprint overload. Its FootStep method returns success when the raycast hits even if no Surface Effect pair resolves, so diagnose the type/effect mapping separately from footstep timing.

Decal Manager obtains decals through the shared object pool, needs a MeshFilter, mesh, Renderer, and material on the prefab, and can reject a decal by raycasting its first four mesh vertices near an exposed edge. Use a simple quad-style decal mesh.

Current code avoids non-uniform-parent warping by parenting a decal to the hit transform only when that transform has uniform local scale. On a non-uniform target, it parents the decal under Decal Manager instead. The decal therefore remains unwarped but does not follow a moving or rotating receiver; use uniform receiver scale or a project-owned moving-decal solution.

Control pooling and cost

  1. Keep one explicit Surface Manager, Decal Manager, and Object Pool in the scene rather than relying on runtime fallback objects.
  2. Use Surface Identifier for frequently hit geometry. It avoids renderer, readable-mesh, submesh, UV, and terrain work after the identifier lookup.
  3. Keep Detect Terrain Tree Textures disabled for ordinary terrain footsteps and projectiles.
  4. Limit complex multi-material hits on high-triangle meshes. Renderer and mesh references are cached, but the resolver still scans submesh triangle arrays to identify the hit material.
  5. Tune Decal Manager Decal Limit, Weathered Decal Limit, and Remove Fadeout Speed; released defaults are 100, 20, and 10.
  6. Use Surface Effect Min Audio Clip Frame Interval to suppress duplicate high-frequency audio, and use Spawned Object Probability deliberately.
  7. Ensure spawned-effect and decal prefabs are pool-safe and reset their visible state when reactivated. Durable gameplay objects should have their own lifecycle rather than being treated as disposable feedback.

Editor checkpoint

Before Play Mode, confirm that:

  • one direct Surface Identifier test already resolves the intended Surface Impact + Surface Type pair;
  • each known Surface Type has one explicit entry for every impact it should handle;
  • Main Texture Property Name matches the shader before Surface Manager Awake;
  • each texture is registered once, and no production atlas depends on duplicate UV entries in released Version 3.2.0;
  • multi-material objects use a readable, triangle-compatible MeshCollider and Renderer mesh, or direct identifiers instead;
  • every Terrain Layer diffuse texture that matters has one mapping, while tree detection is enabled only with a measured need;
  • runtime-changing objects start with a stable Surface Identifier and constant decal policy;
  • decal prefabs contain the required quad-style mesh and renderer, and moving decal receivers use uniform scale;
  • Decal Manager limits, audio frame interval, and spawned-object probabilities match the expected impact rate; and
  • fallback assets are deliberate safety nets rather than substitutes for missing primary mappings.

Verify advanced cases in Play Mode

  1. Hit two Surface Identifier colliders with the same Surface Impact and confirm their distinct Surface Types choose different effects.
  2. Hit a simple texture-mapped collider without an identifier. Confirm the exact configured texture resolves, then change Main Texture Property Name to an incorrect value in Edit Mode and confirm the test fails after a fresh Play Mode start.
  3. Test a two-material readable mesh with a MeshCollider. Hit triangles in each submesh and confirm the resolved effect follows the Renderer material. Repeat with mismatched collider geometry and confirm the diagnostic fails rather than assuming a material.
  4. On terrain, paint two large dominant regions and cross the boundary with footsteps. Confirm one dominant type is chosen per hit and tree detection remains off.
  5. Change a pre-existing Surface Identifier’s SurfaceType through project code and confirm the next hit uses the new type. Change Allow Decals only after the first hit and confirm why that cached permission is not a supported switch.
  6. Remove one primary Impact Effect mapping while leaving another entry in the type, and test the fallback chain. Also test a type with an empty list: it produces no response. Confirm the resolved type/effect and decal policy, then restore the explicit mapping.
  7. Spawn a decal on a uniform stationary receiver, a uniform moving receiver, and a non-uniform moving receiver. Confirm parenting/following behavior and edge rejection are acceptable.
  8. Generate enough repeated effects to reach the configured decal and audio limits. Confirm old decals weather/fade and pooled feedback does not accumulate unbounded active objects.

Troubleshooting and released-Version-3 limitations

Symptom Check Fix
Surface Manager throws a duplicate-key error during Awake The same texture appears more than once in Object Surfaces, commonly for atlas UV regions. Register each texture once. Use Surface Identifiers, separate textures, or a bounded custom resolver for multiple regions.
A multi-material hit always uses fallback The hit is not from a MeshCollider, triangleIndex is unavailable, the mesh is not readable, or collider triangles do not match Renderer submeshes. Use a readable matching MeshCollider/Renderer mesh or split the colliders and assign Surface Identifiers.
A Unity primitive or non-mesh Collider cannot resolve an atlas region UV-region resolution requires a MeshCollider hit and texture coordinates. Use Surface Identifier or author a suitable MeshCollider workflow.
A material swap still uses the old type Renderer, main texture, or simple type was cached for that Collider. Keep a pre-existing Surface Identifier authoritative and change only its SurfaceType, or rebuild through a scene reload.
Adding Surface Identifier at runtime has no effect The Collider’s earlier identifier lookup cached null or another component. Add the identifier before the first hit or use project-owned resolution for that dynamic object.
Changing Allow Decals at runtime has no effect Decal permission was cached during the first identifier lookup. Treat it as authored configuration or implement the dynamic decision in a custom response.
A detected Surface Type ignores a changed Impact Effect Its impact/effect dictionary was cached on first use. Author asset mappings before Play Mode and restart the test after edits.
Fallback response creates an unexpected decal The primary pair was missing and the later fallback-type path does not apply Fallback Allow Decals in released Version 3.2.0. Add the explicit pair or use a fallback effect with no decal.
Terrain always chooses one material near a blend Resolution selects the single highest alphamap weight at the hit cell. Paint a clearer boundary or accept/project a custom blend policy.
Tree impacts cause spikes Detect Terrain Tree Textures performs per-tree pooled instantiation, physics synchronization, and raycasts. Disable it and use direct identifiers or a project-owned tree lookup.
A footstep ray hits but no feedback plays Character Foot Effects found ground, but the Surface Impact + resolved Surface Type pair has no Surface Effect. Inspect detection and the type’s explicit Impact Effects mapping separately from foot timing.
A decal does not appear The identifier/fallback disallows it, the Surface Effect has no valid decal, its prefab lacks required components, or the edge test rejects it. Verify the resolved route, use a quad-style prefab, and test away from an exposed edge.
A decal does not follow a non-uniform moving object Decal Manager parents it under itself to prevent inherited scale distortion. Use uniform receiver scale or a moving-decal implementation designed for that object.

Developer reference

The released surface types are in Opsive.UltimateCharacterController.SurfaceSystem. SurfaceManager.SpawnEffect has regular and footprint overloads, each with an optional explicit Collider. It returns true when it resolves and invokes a Surface Effect. With a component reference, GetSurfaceType(RaycastHit, Collider) returns the detected type, while GetSurfaceEffect(RaycastHit, Collider, SurfaceImpact, ref SurfaceType, ref bool) exposes the final resolved type, effect, and decal decision.

Use those query methods for a diagnostic before replacing the resolver:

using Opsive.UltimateCharacterController.SurfaceSystem;
using UnityEngine;

public sealed class SurfaceDiagnostic : MonoBehaviour
{
    [SerializeField] private SurfaceManager m_SurfaceManager;
    [SerializeField] private SurfaceImpact m_SurfaceImpact;

    public void LogHit(RaycastHit hit)
    {
        var detectedType = m_SurfaceManager.GetSurfaceType(hit, hit.collider);
        SurfaceType resolvedType = null;
        var allowDecals = false;
        var effect = m_SurfaceManager.GetSurfaceEffect(
            hit, hit.collider, m_SurfaceImpact, ref resolvedType, ref allowDecals);

        Debug.Log($"Detected: {detectedType}; Resolved: {resolvedType}; " +
                  $"Effect: {effect}; Decals: {allowDecals}");
    }
}

SurfaceIdentifier.SurfaceType and AllowDecals are writable, with the cache boundaries described above. SurfaceEffect.Spawn and SpawnFootprint are virtual extension points for a custom response. CharacterFootEffects.FootStep is virtual for custom foot detection. DecalManager.Spawn and SpawnFootprint are lower-level static decal entry points.

Surface Manager’s type-resolution methods are public but not virtual, and the package has no resolver interface or cache invalidation API. A project that needs dynamic renderer/material resolution should keep that decision outside the built-in cache, then call a chosen Surface Effect or another project response deliberately.