Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 21 additions & 21 deletions public/config/collision.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,40 +90,40 @@
},

"buildingAvoidance": {
"_description": "Building avoidance - prevents units from walking into structures",
"_description": "Building avoidance - ONLY used for flying units or when crowd unavailable. Ground units using crowd trust the navmesh.",

"strength": 6.0,
"_strength_description": "Base avoidance force. Lower = less fighting with target direction, reduces orbiting.",
"strength": 3.0,
"_strength_description": "Base avoidance force. Lower than before since navmesh handles most routing now.",

"hardMargin": 0.1,
"_hardMargin_description": "Hard collision margin around buildings (world units).",
"hardMargin": 0.3,
"_hardMargin_description": "Hard collision margin around buildings (world units). Wider to prevent clipping.",

"softMargin": 0.5,
"_softMargin_description": "Soft avoidance zone for early steering (world units).",
"softMargin": 1.0,
"_softMargin_description": "Soft avoidance zone for early steering (world units). Wider for smoother turning.",

"predictionLookahead": 0.2,
"_predictionLookahead_description": "How far ahead to predict collisions (seconds).",
"predictionLookahead": 0.3,
"_predictionLookahead_description": "How far ahead to predict collisions (seconds). Longer for earlier reaction.",

"predictiveStrengthMultiplier": 0.3,
"predictiveStrengthMultiplier": 0.2,
"_predictiveStrengthMultiplier_description": "Predictive avoidance strength as fraction of base. Lower = less orbiting."
},

"stuck": {
"_description": "Stuck detection and resolution - helps units escape when blocked",
"_description": "Stuck detection and resolution - helps units escape when blocked. Less aggressive with navmesh routing.",

"detectionFrames": 15,
"_detectionFrames_description": "Frames of no movement before unit is considered stuck.",
"detectionFrames": 30,
"_detectionFrames_description": "Frames of no movement before unit is considered stuck. Longer to avoid false positives.",

"velocityThreshold": 0.08,
"_velocityThreshold_description": "Speed below which unit might be stuck.",
"velocityThreshold": 0.05,
"_velocityThreshold_description": "Speed below which unit might be stuck. Lower threshold for more accurate detection.",

"nudgeStrength": 2.5,
"_nudgeStrength_description": "Force applied to nudge stuck units. Should be strong enough to overcome avoidance.",
"nudgeStrength": 1.5,
"_nudgeStrength_description": "Force applied to nudge stuck units. Gentler since navmesh handles most routing.",

"minDistanceToTarget": 1.0,
"_minDistanceToTarget_description": "Don't apply stuck detection if closer than this to target.",
"minDistanceToTarget": 1.5,
"_minDistanceToTarget_description": "Don't apply stuck detection if closer than this to target. Wider to prevent false nudges at destination.",

"tangentialBias": 0.7,
"_tangentialBias_description": "How much to nudge tangentially vs toward target. 1.0 = fully tangential escape."
"tangentialBias": 0.5,
"_tangentialBias_description": "How much to nudge tangentially vs toward target. Lower = more toward target, avoids random walks."
}
}
20 changes: 10 additions & 10 deletions src/data/collisionConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,18 +115,18 @@ const DEFAULT_CONFIG: CollisionConfig = {
flyingUnitRadius: 0.4,
},
buildingAvoidance: {
strength: 6.0,
hardMargin: 0.1,
softMargin: 0.5,
predictionLookahead: 0.2,
predictiveStrengthMultiplier: 0.3,
strength: 3.0,
hardMargin: 0.3,
softMargin: 1.0,
predictionLookahead: 0.3,
predictiveStrengthMultiplier: 0.2,
},
stuck: {
detectionFrames: 15,
velocityThreshold: 0.08,
nudgeStrength: 2.5,
minDistanceToTarget: 1.0,
tangentialBias: 0.7,
detectionFrames: 30,
velocityThreshold: 0.05,
nudgeStrength: 1.5,
minDistanceToTarget: 1.5,
tangentialBias: 0.5,
},
};

Expand Down
6 changes: 5 additions & 1 deletion src/data/pathfinding.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,12 @@ export const WALKABLE_HEIGHT = 2.0;
* Agent collision radius for pathfinding.
* Paths maintain this clearance from obstacles.
* Units of: world units
*
* SC2-style: Set wide enough that units following navmesh paths won't need
* additional steering forces to avoid clipping buildings. This prevents
* the oscillation issue where steering and pathfinding give conflicting directions.
*/
export const WALKABLE_RADIUS = 0.6;
export const WALKABLE_RADIUS = 0.8;

/**
* Cell size for navmesh generation.
Expand Down
31 changes: 30 additions & 1 deletion src/engine/systems/movement/FlockingBehavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,10 @@ export class FlockingBehavior {

/**
* Calculate physics push force from nearby units.
* Units push each other instead of avoiding - creates natural flow.
* SC2-style: units push each other with priority based on movement state.
* - Moving units have priority over idle units
* - Units don't push through others who have higher priority
* This creates natural streaming flow through choke points.
* PERF: Results are cached and only recalculated every PHYSICS_PUSH_THROTTLE_TICKS ticks
*/
public calculatePhysicsPush(
Expand Down Expand Up @@ -490,6 +493,11 @@ export class FlockingBehavior {
let forceX = 0;
let forceY = 0;

// SC2-style priority: determine if self is "heavy" (moving with purpose) or "light" (idle)
const selfIsMoving = selfUnit.state === 'moving' || selfUnit.state === 'attackmoving' ||
selfUnit.state === 'patrolling' || selfUnit.state === 'gathering' ||
selfUnit.state === 'building';

for (let i = 0; i < nearbyData.length; i++) {
const other = nearbyData[i];
if (other.id === selfId) continue;
Expand All @@ -513,6 +521,24 @@ export class FlockingBehavior {
const nx = dx / dist;
const ny = dy / dist;

// SC2-style priority: moving units push idle units more than vice versa
// Idle units yield to moving units by receiving stronger push
const otherIsMoving = other.state === SpatialUnitState.Moving ||
other.state === SpatialUnitState.AttackMoving ||
other.state === SpatialUnitState.Gathering;

// Priority multiplier: if I'm idle and they're moving, I get pushed more (yield)
// If I'm moving and they're idle, I push them but they don't push me much
let priorityMultiplier = 1.0;
if (!selfIsMoving && otherIsMoving) {
// I'm idle, they're moving - I yield (get pushed more)
priorityMultiplier = 1.5;
} else if (selfIsMoving && !otherIsMoving) {
// I'm moving, they're idle - push through them (they should yield, not me)
priorityMultiplier = 0.3;
}
// If both moving or both idle: equal push (priorityMultiplier = 1.0)

// Calculate push strength based on distance
let pushStrength: number;
if (dist < minDist) {
Expand All @@ -524,6 +550,9 @@ export class FlockingBehavior {
pushStrength = collisionConfig.physicsPushStrength * Math.pow(1 - t, collisionConfig.physicsPushFalloff);
}

// Apply priority multiplier
pushStrength *= priorityMultiplier;

forceX += nx * pushStrength;
forceY += ny * pushStrength;
}
Expand Down
13 changes: 9 additions & 4 deletions src/engine/systems/movement/MovementOrchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1093,10 +1093,15 @@ export class MovementOrchestrator {
}
}

// Building avoidance
this.pathfinding.calculateBuildingAvoidanceForce(entityId, transform, unit, tempBuildingAvoid, finalVx, finalVy);
finalVx += tempBuildingAvoid.x;
finalVy += tempBuildingAvoid.y;
// Building avoidance - ONLY for non-crowd paths (flying units or crowd unavailable)
// When using crowd navigation, the navmesh already routes around buildings.
// Adding steering forces on top causes oscillation/vibration at building corners.
// SC2-style: trust the navmesh for buildings, use boids only for unit-to-unit.
if (!USE_RECAST_CROWD || unit.isFlying || !this.pathfinding.isAgentRegistered(entityId)) {
this.pathfinding.calculateBuildingAvoidanceForce(entityId, transform, unit, tempBuildingAvoid, finalVx, finalVy);
finalVx += tempBuildingAvoid.x;
finalVy += tempBuildingAvoid.y;
}

// Physics pushing
if (!unit.isFlying) {
Expand Down
9 changes: 5 additions & 4 deletions src/engine/systems/movement/PathfindingMovement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -587,12 +587,13 @@ export class PathfindingMovement {
const escapeY =
Math.abs(escapeUp) < Math.abs(escapeDown) ? escapeUp : escapeDown;

// Push out with extra buffer to prevent oscillation
const pushBuffer = 0.3;
// Push exactly to edge - no buffer to prevent oscillation
// The navmesh path should keep units away from buildings; this is just a safety net
// for edge cases like physics push or teleport placing unit inside building
if (Math.abs(escapeX) < Math.abs(escapeY)) {
transform.x += escapeX + (escapeX > 0 ? pushBuffer : -pushBuffer);
transform.x += escapeX;
} else {
transform.y += escapeY + (escapeY > 0 ? pushBuffer : -pushBuffer);
transform.y += escapeY;
}
}
}
Expand Down