Add blood decals, gore, mobile HUD, web start gate + touch/perf tests
Remove tools/fstest.html scratch page used to probe browser fullscreen/orientation APIs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EznnY8rH2dXhtono1kwsXg
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
extends SceneTree
|
||||
## Micro-benchmark for the on-hit ragdoll spike.
|
||||
##
|
||||
## On mobile the profiler reads SCRIPT-bound (main-thread GDScript, one thread) exactly when the
|
||||
## bull's attack connects and a matador is hit. This times the pieces of that synchronous path so
|
||||
## we can see which op eats the milliseconds instead of guessing:
|
||||
## • spawn — _matador.instantiate() + add_child (_ready builds the 21-bone ragdoll rig)
|
||||
## • sim start — PhysicalBoneSimulator3D.physical_bones_start_simulation() (Jolt makes bodies)
|
||||
## • blood burst — the CPUParticles3D one-shot fired on death
|
||||
## • full hit — apply_ability_hit() end to end (what the player actually triggers)
|
||||
## • MASS hit — hitting N matadors in ONE frame (what a slam / roll does — the real spike)
|
||||
##
|
||||
## Run: godot --headless --script res://tests/ragdoll_perf_test.gd
|
||||
## It PRINTS per-op avg/worst ms and names the dominant cost; it also fails (exit 1) if a single
|
||||
## hit or a mass-hit frame blows a generous budget, so it doubles as a regression guard.
|
||||
|
||||
# Loaded at runtime (not preload): a preload here would compile matador.gd at this entry
|
||||
# script's parse time, before the autoloads (DP / Controls) register as global identifiers,
|
||||
# so matador.gd's `Controls.rigid_skin_enabled()` would fail to resolve. load() in _run runs
|
||||
# after the tree — and its autoloads — are up.
|
||||
var _matador: PackedScene = null
|
||||
|
||||
const N := 8 # matadors sampled per op
|
||||
const MASS := 6 # matadors hit in one frame (a slam catching a cluster)
|
||||
const SINGLE_HIT_BUDGET_MS := 12.0
|
||||
const MASS_HIT_BUDGET_MS := 33.0 # two 60fps frames — a slam may cost a hitch, not a freeze
|
||||
|
||||
var _fail := 0
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_run.call_deferred()
|
||||
|
||||
|
||||
func _us() -> int:
|
||||
return Time.get_ticks_usec()
|
||||
|
||||
|
||||
func _stats(us: Array) -> Dictionary:
|
||||
var total := 0
|
||||
var worst := 0
|
||||
for v: int in us:
|
||||
total += v
|
||||
worst = maxi(worst, v)
|
||||
var avg := (total / us.size()) if us.size() > 0 else 0
|
||||
return {"avg_ms": avg / 1000.0, "max_ms": worst / 1000.0}
|
||||
|
||||
|
||||
func _sample(frames: int) -> Dictionary:
|
||||
var proc_sum := 0.0
|
||||
var phys_sum := 0.0
|
||||
for f: int in frames:
|
||||
await process_frame
|
||||
proc_sum += Performance.get_monitor(Performance.TIME_PROCESS) * 1000.0
|
||||
phys_sum += Performance.get_monitor(Performance.TIME_PHYSICS_PROCESS) * 1000.0
|
||||
return {"proc_ms": proc_sum / frames, "phys_ms": phys_sum / frames}
|
||||
|
||||
|
||||
func _spawn_one() -> Node3D:
|
||||
var m: Node3D = _matador.instantiate()
|
||||
m.position = Vector3(randf_range(-20.0, 20.0), 0.0, randf_range(-20.0, 20.0))
|
||||
root.add_child(m)
|
||||
return m
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
# Warm up: first instance pays one-time import/JIT/shader costs that would skew sample 1.
|
||||
_matador = load("res://Matador.tscn") as PackedScene
|
||||
var warm := _spawn_one()
|
||||
await process_frame
|
||||
warm.call(&"apply_ability_hit", Vector3(1.0, 0.0, 0.0), 12.0)
|
||||
await process_frame
|
||||
warm.free()
|
||||
await process_frame
|
||||
print("[stage] warmup done")
|
||||
|
||||
# ── spawn: instantiate() vs add_child(_ready = ragdoll build) ───────────────
|
||||
var inst_us: Array = []
|
||||
var ready_us: Array = []
|
||||
for i: int in N:
|
||||
var t0 := _us()
|
||||
var m: Node3D = _matador.instantiate()
|
||||
var t1 := _us()
|
||||
root.add_child(m) # _ready runs synchronously → MatadorRagdoll.build (21 bodies + shapes)
|
||||
var t2 := _us()
|
||||
inst_us.append(t1 - t0)
|
||||
ready_us.append(t2 - t1)
|
||||
m.free()
|
||||
await process_frame
|
||||
print("[stage] spawn done")
|
||||
|
||||
# ── component: sim start (Jolt body creation) and blood burst, in isolation ──
|
||||
var simstart_us: Array = []
|
||||
var burst_us: Array = []
|
||||
for i: int in N:
|
||||
var m := _spawn_one()
|
||||
await process_frame
|
||||
var sim: Node = m.get(&"_sim")
|
||||
if sim != null:
|
||||
sim.set("active", true)
|
||||
var t0 := _us()
|
||||
sim.call(&"physical_bones_start_simulation")
|
||||
simstart_us.append(_us() - t0)
|
||||
var blood: Node = m.get(&"_blood_burst")
|
||||
if blood != null:
|
||||
var t2 := _us()
|
||||
blood.call(&"burst", m.global_position + Vector3(0, 0.9, 0), Vector3(1, 0, 0))
|
||||
burst_us.append(_us() - t2)
|
||||
m.free()
|
||||
await process_frame
|
||||
print("[stage] components done")
|
||||
|
||||
# ── full hit: apply_ability_hit end to end (state WANDER → RAGDOLL) ──────────
|
||||
var hit_us: Array = []
|
||||
for i: int in N:
|
||||
var m := _spawn_one()
|
||||
await process_frame
|
||||
var t0 := _us()
|
||||
m.call(&"apply_ability_hit", Vector3(1.0, 0.0, 0.0), 12.0)
|
||||
hit_us.append(_us() - t0)
|
||||
await process_frame
|
||||
m.free()
|
||||
await process_frame
|
||||
print("[stage] full-hit done")
|
||||
|
||||
# ── MASS hit: MASS matadors ragdolled in ONE frame (a slam catching a cluster) ─
|
||||
var cluster: Array = []
|
||||
for i: int in MASS:
|
||||
cluster.append(_spawn_one())
|
||||
await process_frame
|
||||
await process_frame
|
||||
var mt0 := _us()
|
||||
for m: Node3D in cluster:
|
||||
m.call(&"apply_ability_hit", Vector3(1.0, 0.0, 0.0), 12.0)
|
||||
var mass_ms := (_us() - mt0) / 1000.0
|
||||
for m: Node3D in cluster:
|
||||
m.free()
|
||||
await process_frame
|
||||
print("[stage] mass done")
|
||||
|
||||
# ── STEADY load: per-frame cost of live_n ragdolls ALIVE at once (they live ~4 s each) ─
|
||||
# The hit is instantaneous; the drag is every ragdoll still simulating afterward. Sample
|
||||
# the frame cost with live_n matadors idle, then with all live_n ragdolling, and report the delta —
|
||||
# split process (idle-frame GDScript = the mobile "SCRIPT" bucket) vs physics (the Jolt step).
|
||||
var live_n := 10
|
||||
var steady: Array = []
|
||||
for i: int in live_n:
|
||||
steady.append(_spawn_one())
|
||||
for f: int in 15:
|
||||
await process_frame
|
||||
var idle: Dictionary = await _sample(20)
|
||||
for m: Node3D in steady:
|
||||
m.call(&"apply_ability_hit", Vector3(1.0, 0.0, 0.0), 12.0)
|
||||
for f: int in 3:
|
||||
await process_frame
|
||||
var active: Dictionary = await _sample(20)
|
||||
for m: Node3D in steady:
|
||||
if is_instance_valid(m):
|
||||
m.free()
|
||||
|
||||
# ── report ──────────────────────────────────────────────────────────────────
|
||||
var inst := _stats(inst_us)
|
||||
var rdy := _stats(ready_us)
|
||||
var ss := _stats(simstart_us)
|
||||
var bu := _stats(burst_us)
|
||||
var hit := _stats(hit_us)
|
||||
|
||||
print("\n==== on-hit ragdoll cost (per matador, avg / worst) ====")
|
||||
print(" instantiate() %6.2f / %6.2f ms" % [inst["avg_ms"], inst["max_ms"]])
|
||||
print(" add_child (_ready build)%6.2f / %6.2f ms" % [rdy["avg_ms"], rdy["max_ms"]])
|
||||
print(" sim start (Jolt bodies) %6.2f / %6.2f ms" % [ss["avg_ms"], ss["max_ms"]])
|
||||
print(" blood burst %6.2f / %6.2f ms" % [bu["avg_ms"], bu["max_ms"]])
|
||||
print(" FULL apply_ability_hit %6.2f / %6.2f ms" % [hit["avg_ms"], hit["max_ms"]])
|
||||
print(" MASS hit (%d in 1 frame) %6.2f ms total" % [MASS, mass_ms])
|
||||
print("---- steady per-frame cost, %d matadors idle vs ragdolling ----" % live_n)
|
||||
print(" idle: process %5.2f ms physics %5.2f ms" % [idle["proc_ms"], idle["phys_ms"]])
|
||||
print(" ragdolling: process %5.2f ms physics %5.2f ms" % [active["proc_ms"], active["phys_ms"]])
|
||||
print(" delta/%d ragdolls: process +%5.2f ms physics +%5.2f ms (per ragdoll ~%.2f / %.2f ms)" % [
|
||||
live_n, active["proc_ms"] - idle["proc_ms"], active["phys_ms"] - idle["phys_ms"],
|
||||
(active["proc_ms"] - idle["proc_ms"]) / live_n, (active["phys_ms"] - idle["phys_ms"]) / live_n])
|
||||
|
||||
# Name the dominant component of the full hit so the fix target is obvious.
|
||||
var parts := {"sim start": ss["avg_ms"], "blood burst": bu["avg_ms"]}
|
||||
var worst_name := "sim start"
|
||||
var worst_val := -1.0
|
||||
for k: String in parts:
|
||||
if parts[k] > worst_val:
|
||||
worst_val = parts[k]
|
||||
worst_name = k
|
||||
print(" >> dominant hit cost: %s (%.2f ms of the %.2f ms hit)" % [
|
||||
worst_name, worst_val, hit["avg_ms"]])
|
||||
|
||||
if hit["max_ms"] > SINGLE_HIT_BUDGET_MS:
|
||||
_fail += 1
|
||||
print(" FAIL: worst single hit %.2f ms > %.1f ms budget" % [hit["max_ms"], SINGLE_HIT_BUDGET_MS])
|
||||
if mass_ms > MASS_HIT_BUDGET_MS:
|
||||
_fail += 1
|
||||
print(" FAIL: mass hit %.2f ms > %.1f ms budget" % [mass_ms, MASS_HIT_BUDGET_MS])
|
||||
|
||||
print("Results: %s" % ("FAIL (%d)" % _fail if _fail > 0 else "PASS"))
|
||||
quit(1 if _fail > 0 else 0)
|
||||
Reference in New Issue
Block a user