wrote a surface type system for Godot 4 — footstep sounds, particles, and decals from one resource

317 views 0 replies

This is the thing I've rebuilt from scratch in every project and finally decided to do properly. A surface type system: tag your floor/terrain nodes with a resource, and anything that touches those surfaces (character feet, projectiles, whatever) can ask "what's here?" and get back the right footstep sound, impact particle, and decal.

The resource itself is just:

class_name SurfaceType
extends Resource

@export var surface_name: String = "default"
@export var footstep_sounds: Array[AudioStream] = []
@export var impact_particles: PackedScene = null
@export var decal_scene: PackedScene = null
@export var footstep_volume_db: float = 0.0
@export var footstep_pitch_variance: float = 0.1

Then a small autoload does a short downward raycast and reads surface_type meta from whatever it hits:

extends Node

const DEFAULT_SURFACE := preload("res://surfaces/default.tres")

func get_surface_at(world_pos: Vector3, mask: int = 1) -> SurfaceType:
    var space := get_viewport().find_world_3d().direct_space_state
    var query := PhysicsRayQueryParameters3D.create(
        world_pos + Vector3.UP * 0.15,
        world_pos + Vector3.DOWN * 0.4,
        mask
    )
    var hit := space.intersect_ray(query)
    if hit.is_empty():
        return DEFAULT_SURFACE
    var body = hit.collider
    if body.has_meta(&"surface_type"):
        return body.get_meta(&"surface_type")
    return DEFAULT_SURFACE

Tag your floor nodes in the editor or at runtime: $GrassFloor.set_meta(&"surface_type", preload("res://surfaces/grass.tres")). Characters call SurfaceDetector.get_surface_at(foot_pos) on foot plant frames. Projectile impacts just cast from the hit point. Surprisingly clean once it's wired up.

The part I haven't solved: terrain with multiple surface materials. If you're using Terrain3D or a splatmap setup, meta on the collision body doesn't tell you which texture is under the foot. I'm thinking about sampling the splatmap via Image.get_pixel at the projected UV, but it feels janky and I don't love reading from the CPU side every footstep. Has anyone tackled blended surface detection on terrain? Curious if there's a cleaner approach without a bunch of extra raycasts or texture readback.

Moonjump
Forum Search Shader Sandbox
Sign In Register