HUD and Height Changes

This commit is contained in:
2026-06-07 22:29:25 -07:00
parent 4ebaba9933
commit 1ed2aa46c5
41 changed files with 24787 additions and 766 deletions
@@ -0,0 +1,30 @@
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Drop one instance into the Gameplay subscene. Bakes <see cref="WorldCollisionConfig"/> capturing the
/// "Environment" physics layer's BelongsTo mask (the layer the boundary-ring + landmark colliders live on).
/// The baker runs on the main thread, so the managed <see cref="LayerMask.NameToLayer"/> lookup is fine here —
/// this exists precisely so the Bursted server EnemyAISystem can read the mask as a plain uint at runtime.
/// </summary>
[DisallowMultipleComponent]
public class WorldCollisionAuthoring : MonoBehaviour
{
[Tooltip("Name of the Unity layer carrying the static world colliders (boundary ring + landmarks).")]
public string EnvironmentLayerName = "Environment";
private class WorldCollisionBaker : Baker<WorldCollisionAuthoring>
{
public override void Bake(WorldCollisionAuthoring authoring)
{
int layer = LayerMask.NameToLayer(authoring.EnvironmentLayerName);
uint mask = layer >= 0 ? 1u << layer : 0u;
var entity = GetEntity(TransformUsageFlags.None);
AddComponent(entity, new WorldCollisionConfig { EnvironmentMask = mask });
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ab1c895817ca4b64db74215f13abb5e3
@@ -4,6 +4,7 @@ using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
using Unity.Physics;
using Unity.Transforms;
namespace ProjectM.Server
@@ -61,6 +62,11 @@ namespace ProjectM.Server
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
uint now = serverTick.TickIndexForValidTick;
var ecb = new EntityCommandBuffer(Allocator.Temp);
bool havePhysics = SystemAPI.TryGetSingleton<PhysicsWorldSingleton>(out var physics);
uint envMask = SystemAPI.TryGetSingleton<WorldCollisionConfig>(out var worldCol) ? worldCol.EnvironmentMask : 0u;
var envFilter = new CollisionFilter { BelongsTo = ~0u, CollidesWith = envMask, GroupIndex = 0 };
bool sweep = havePhysics && envMask != 0u;
const float SweepRadius = 0.5f; // collide-and-slide sphere radius for Husk movement
foreach (var (xform, stats, cooldown, knockback, windup) in
SystemAPI.Query<RefRW<LocalTransform>, RefRO<EnemyStats>, RefRW<EnemyAttackCooldown>,
@@ -78,6 +84,8 @@ namespace ProjectM.Server
{
float3 kpos = pos + new float3(kb.Dir.x, 0f, kb.Dir.y) * (kb.Speed * dt);
kpos.y = pos.y;
if (sweep)
kpos = SweptMove(in physics, pos, kpos, SweepRadius, envFilter);
xform.ValueRW.Position = kpos;
windup.ValueRW.WindUpUntilTick = 0; // a recoiling Husk does not wind up
continue; // recoiling: skip seek + strike this tick
@@ -106,6 +114,8 @@ namespace ProjectM.Server
float3 vel = EnemyAIMath.SeekVelocity(pos, targetPos, stats.ValueRO.MoveSpeed, stopDistance);
float3 newPos = pos + vel * dt;
newPos.y = pos.y; // hold the movement plane
if (sweep)
newPos = SweptMove(in physics, pos, newPos, SweepRadius, envFilter);
xform.ValueRW.Position = newPos;
// Face the target (planar) for presentation.
@@ -166,5 +176,38 @@ namespace ProjectM.Server
playerEntities.Dispose();
playerPositions.Dispose();
}
// Swept collide-and-slide for server-authoritative Husk movement: sphere-cast the intended step against
// the static environment (boundary ring + landmarks) and stop at / glance along the first wall hit. Closest-
// hit SphereCast is non-generic -> Burst-safe (CLAUDE.md generic-collector hazard avoided). Y is held flat.
static float3 SweptMove(in PhysicsWorldSingleton physics, float3 from, float3 to, float radius, CollisionFilter filter)
{
float3 delta = to - from;
delta.y = 0f;
float dist = math.length(delta);
if (dist < 1e-5f)
return to;
float3 dir = delta / dist;
const float skin = 0.05f;
var cw = physics.CollisionWorld;
if (!cw.SphereCast(from, radius, dir, dist, out var hit, filter))
return to;
float allowed = math.max(0f, hit.Fraction * dist - skin);
float3 stop = from + dir * allowed;
stop.y = from.y;
// Slide the unused motion along the wall, then sweep the slide so we don't tunnel a second wall.
float3 slide = EnemyAIMath.SlideVelocity(to - stop, hit.SurfaceNormal);
float slideDist = math.length(slide);
if (slideDist < 1e-5f)
return stop;
float3 sdir = slide / slideDist;
float3 result = cw.SphereCast(stop, radius, sdir, slideDist, out var hit2, filter)
? stop + sdir * math.max(0f, hit2.Fraction * slideDist - skin)
: stop + slide;
result.y = from.y;
return result;
}
}
}
@@ -9,7 +9,8 @@
"Unity.Mathematics",
"Unity.Burst",
"Unity.NetCode",
"Unity.Networking.Transport"
"Unity.Networking.Transport",
"Unity.Physics"
],
"includePlatforms": [],
"excludePlatforms": [],
@@ -35,6 +35,24 @@ namespace ProjectM.Simulation
d.y = 0f;
return math.lengthsq(d) <= range * range;
}
/// <summary>
/// Projects a planar movement <paramref name="vel"/> onto a wall plane defined by <paramref name="surfaceNormal"/>
/// (collide-and-slide): removes the component of <paramref name="vel"/> that pushes into the surface so the
/// mover glances along the wall instead of stopping dead. Both inputs are flattened to the XZ plane (top-down).
/// Returns <paramref name="vel"/> unchanged when the normal is degenerate.
/// </summary>
public static float3 SlideVelocity(float3 vel, float3 surfaceNormal)
{
surfaceNormal.y = 0f;
float len = math.length(surfaceNormal);
if (len < 1e-6f)
return vel;
float3 n = surfaceNormal / len;
float3 slid = vel - math.dot(vel, n) * n;
slid.y = 0f;
return slid;
}
/// <summary>
/// Deterministic planar ring position around <paramref name="center"/> for spawn
@@ -0,0 +1,17 @@
using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// Singleton holding the physics-filter mask of the static world-collision layer ("Environment"): the baked
/// boundary ring + landmark colliders the player CC sweeps. Baked from the GameObject layer at edit-time (see
/// WorldCollisionAuthoring) so server systems can build a <c>CollisionFilter</c> in Burst WITHOUT a managed
/// <c>LayerMask.NameToLayer</c> call or a hardcoded layer index. Read by <see cref="ProjectM.Server"/>'s
/// EnemyAISystem to sweep-test Husk movement against the environment only (never the player / other Husks).
/// </summary>
public struct WorldCollisionConfig : IComponentData
{
/// <summary>BelongsTo bitmask of the Environment physics layer (<c>1u &lt;&lt; layerIndex</c>).</summary>
public uint EnvironmentMask;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 70c75b96478d39f43aca221c926c9561