@tool extends Node3D class_name CrowdMarker ## A designer-placed spot for one billboard spectator. Drop these into a level in the ## editor; crowd_billboards.gd harvests every marker (via the &"crowd_marker" group) into ## its MultiMesh at runtime, using each marker's global position and chosen facing. ## ## The marker draws a lightweight preview (a slab standing in for the sprite + a forward ## arrow) so its position and orientation are visible in the viewport. That preview is ## editor-only and never serialised into the scene, so it costs nothing at runtime. ## The six selectable facings, one every 60° about the Y axis. DEG_0 faces the quad's ## front (+Z) along world +Z; increasing values rotate clockwise looking down. enum Orientation { DEG_0, DEG_60, DEG_120, DEG_180, DEG_240, DEG_300, } const ORIENTATION_STEPS := 6 ## Metres — matches crowd_billboards.figure_height so the editor preview reads true to scale. const PREVIEW_HEIGHT := 2.0 @export var orientation := Orientation.DEG_0: set(value): orientation = value _apply_orientation() var _preview: Node3D func _ready() -> void: add_to_group(&"crowd_marker") if Engine.is_editor_hint(): _build_preview() _apply_orientation() # Facing about Y for the current orientation: 0, 60, 120 … degrees as radians. func facing_yaw() -> float: return float(orientation) * (TAU / float(ORIENTATION_STEPS)) # Unit forward the spectator's front (+Z) points along, on the horizontal plane. func facing_dir() -> Vector3: var a := facing_yaw() return Vector3(sin(a), 0.0, cos(a)) func _apply_orientation() -> void: if _preview != null: _preview.rotation = Vector3(0.0, facing_yaw(), 0.0) # Editor-only preview. Children are added without an owner so they are excluded from the # saved .tscn (Godot only serialises nodes owned by the scene root). func _build_preview() -> void: _preview = Node3D.new() add_child(_preview) var aspect := 0.5 var slab := MeshInstance3D.new() var quad := QuadMesh.new() quad.size = Vector2(PREVIEW_HEIGHT * aspect, PREVIEW_HEIGHT) quad.center_offset = Vector3(0.0, PREVIEW_HEIGHT * 0.5, 0.0) # stand on the marker slab.mesh = quad slab.material_override = _preview_material(Color(0.3, 0.7, 1.0, 0.35)) _preview.add_child(slab) var arrow := MeshInstance3D.new() var shaft := BoxMesh.new() shaft.size = Vector3(0.08, 0.08, PREVIEW_HEIGHT * 0.5) arrow.mesh = shaft # Push the shaft out along +Z (the facing) and up to chest height. arrow.position = Vector3(0.0, PREVIEW_HEIGHT * 0.5, PREVIEW_HEIGHT * 0.25) arrow.material_override = _preview_material(Color(1.0, 0.85, 0.2, 0.9)) _preview.add_child(arrow) func _preview_material(col: Color) -> StandardMaterial3D: var mat := StandardMaterial3D.new() mat.albedo_color = col mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA mat.cull_mode = BaseMaterial3D.CULL_DISABLED return mat