feat: Phase 19-D/E - social behavior and velocity-based animation
- Add soft-repulsion to prevent agent crowding - Implement dynamic Z-sorting based on world position - Add social orientation (agents face nearby agents) - Use velocity-based animation instead of isMoving flag - Track lastPosition for smooth velocity calculation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -82,6 +82,7 @@ namespace TheIsland.Visual
|
||||
private Vector3 _targetPosition;
|
||||
private bool _isMoving;
|
||||
private float _moveSpeed = 3f;
|
||||
private Vector3 _lastPosition;
|
||||
|
||||
// UI Smoothing (Phase 19)
|
||||
private float _currentHpPercent;
|
||||
@@ -109,6 +110,7 @@ namespace TheIsland.Visual
|
||||
if (_animator == null) _animator = gameObject.AddComponent<AgentAnimator>();
|
||||
|
||||
CreateVisuals();
|
||||
_lastPosition = transform.position;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
@@ -152,17 +154,54 @@ namespace TheIsland.Visual
|
||||
_spriteRenderer.sortingOrder = Mathf.RoundToInt(-transform.position.z * 100);
|
||||
}
|
||||
|
||||
// Phase 19-B/D: Use AgentAnimator
|
||||
// Phase 19-B/D/E: Use AgentAnimator
|
||||
if (_animator != null)
|
||||
{
|
||||
float currentSpeed = _isMoving ? _moveSpeed : 0;
|
||||
_animator.SetMovement(currentSpeed, _moveSpeed);
|
||||
// Calculate world velocity based on position change
|
||||
Vector3 currentVelocity = (transform.position - _lastPosition) / (Time.deltaTime > 0 ? Time.deltaTime : 0.001f);
|
||||
_animator.SetMovement(currentVelocity, _moveSpeed);
|
||||
_lastPosition = transform.position;
|
||||
}
|
||||
|
||||
// Phase 19-E: Social Orientation (Interaction Facing)
|
||||
if (!_isMoving)
|
||||
{
|
||||
FaceInteractionTarget();
|
||||
}
|
||||
|
||||
// Phase 19: Smooth UI Bar Transitions
|
||||
UpdateSmoothBars();
|
||||
}
|
||||
|
||||
private void FaceInteractionTarget()
|
||||
{
|
||||
// If the agent is talking or near others, turn to face them
|
||||
float socialRange = 2.5f;
|
||||
AgentVisual nearestAgent = null;
|
||||
float minDist = socialRange;
|
||||
|
||||
var allAgents = FindObjectsByType<AgentVisual>(FindObjectsSortMode.None);
|
||||
foreach (var other in allAgents)
|
||||
{
|
||||
if (other == this || !other.IsAlive) continue;
|
||||
float d = Vector3.Distance(transform.position, other.transform.position);
|
||||
if (d < minDist)
|
||||
{
|
||||
minDist = d;
|
||||
nearestAgent = other;
|
||||
}
|
||||
}
|
||||
|
||||
if (nearestAgent != null && _spriteRenderer != null)
|
||||
{
|
||||
float dx = nearestAgent.transform.position.x - transform.position.x;
|
||||
if (Mathf.Abs(dx) > 0.1f)
|
||||
{
|
||||
_spriteRenderer.flipX = dx < 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Vector3 CalculateRepulsion()
|
||||
{
|
||||
Vector3 force = Vector3.zero;
|
||||
|
||||
@@ -6,15 +6,17 @@ namespace TheIsland
|
||||
/// <summary>
|
||||
/// Procedural 2D animator for agents.
|
||||
/// Handles idle breathing, movement bopping, and action-based squash/stretch.
|
||||
/// Phase 19-E: Added banking turns and anticipation/overshoot.
|
||||
/// </summary>
|
||||
public class AgentAnimator : MonoBehaviour
|
||||
{
|
||||
[Header("Animation Settings")]
|
||||
public float idleSpeed = 2f;
|
||||
public float idleAmount = 0.05f;
|
||||
public float idleAmount = 0.04f;
|
||||
public float moveBopSpeed = 12f;
|
||||
public float moveBopAmount = 0.12f;
|
||||
public float moveTiltAmount = 8f;
|
||||
public float moveBopAmount = 0.1f;
|
||||
public float moveTiltAmount = 10f;
|
||||
public float bankingAmount = 15f;
|
||||
|
||||
private SpriteRenderer _spriteRenderer;
|
||||
private Vector3 _originalScale;
|
||||
@@ -22,12 +24,13 @@ namespace TheIsland
|
||||
private Quaternion _targetLocalRot;
|
||||
private Vector3 _targetScale;
|
||||
|
||||
private Vector3 _currentVelocity;
|
||||
private float _velocityPercentage; // 0 to 1
|
||||
private bool _isMoving;
|
||||
private float _transitionTimer;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Find in children if not on this object
|
||||
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
||||
}
|
||||
|
||||
@@ -41,18 +44,49 @@ namespace TheIsland
|
||||
{
|
||||
_originalScale = Vector3.one;
|
||||
}
|
||||
_targetScale = _originalScale;
|
||||
}
|
||||
|
||||
public void SetMovement(Vector3 velocity, float maxVelocity = 3f)
|
||||
{
|
||||
_currentVelocity = velocity;
|
||||
_velocityPercentage = Mathf.Clamp01(velocity.magnitude / Mathf.Max(0.1f, maxVelocity));
|
||||
|
||||
bool nowMoving = _velocityPercentage > 0.05f;
|
||||
if (nowMoving && !_isMoving)
|
||||
{
|
||||
// Anticipation: Squash when starting to move
|
||||
TriggerAnticipation();
|
||||
}
|
||||
else if (!nowMoving && _isMoving)
|
||||
{
|
||||
// Overshoot: Rebound when stopping
|
||||
TriggerOvershoot();
|
||||
}
|
||||
|
||||
_isMoving = nowMoving;
|
||||
}
|
||||
|
||||
// Compatibility for older code
|
||||
public void SetMovement(float currentVelocity, float maxVelocity)
|
||||
{
|
||||
_velocityPercentage = Mathf.Clamp01(currentVelocity / Mathf.Max(0.1f, maxVelocity));
|
||||
_isMoving = _velocityPercentage > 0.05f;
|
||||
SetMovement(new Vector3(currentVelocity, 0, 0), maxVelocity);
|
||||
}
|
||||
|
||||
public void TriggerActionEffect()
|
||||
{
|
||||
StopAllCoroutines();
|
||||
StartCoroutine(ActionPulseRoutine());
|
||||
StartCoroutine(ActionPulseRoutine(0.4f, 1.3f));
|
||||
}
|
||||
|
||||
private void TriggerAnticipation()
|
||||
{
|
||||
StartCoroutine(ActionPulseRoutine(0.15f, 0.8f)); // Squash
|
||||
}
|
||||
|
||||
private void TriggerOvershoot()
|
||||
{
|
||||
StartCoroutine(ActionPulseRoutine(0.2f, 1.15f)); // Slight stretch
|
||||
}
|
||||
|
||||
private void Update()
|
||||
@@ -69,9 +103,11 @@ namespace TheIsland
|
||||
}
|
||||
|
||||
// Smoothly apply transforms
|
||||
_spriteRenderer.transform.localPosition = Vector3.Lerp(_spriteRenderer.transform.localPosition, _targetLocalPos, Time.deltaTime * 10f);
|
||||
_spriteRenderer.transform.localRotation = Quaternion.Slerp(_spriteRenderer.transform.localRotation, _targetLocalRot, Time.deltaTime * 10f);
|
||||
_spriteRenderer.transform.localScale = Vector3.Lerp(_spriteRenderer.transform.localScale, _targetScale, Time.deltaTime * 10f);
|
||||
float lerpSpeed = 12f;
|
||||
var t = _spriteRenderer.transform;
|
||||
t.localPosition = Vector3.Lerp(t.localPosition, _targetLocalPos, Time.deltaTime * lerpSpeed);
|
||||
t.localRotation = Quaternion.Slerp(t.localRotation, _targetLocalRot, Time.deltaTime * lerpSpeed);
|
||||
t.localScale = Vector3.Lerp(t.localScale, _targetScale, Time.deltaTime * lerpSpeed);
|
||||
}
|
||||
|
||||
private void AnimateIdle()
|
||||
@@ -90,34 +126,37 @@ namespace TheIsland
|
||||
float cycle = Time.time * moveBopSpeed;
|
||||
float bop = Mathf.Abs(Mathf.Sin(cycle)) * moveBopAmount * _velocityPercentage;
|
||||
|
||||
// Tilt based on the cycle to give a "walking" feel
|
||||
float tilt = Mathf.Sin(cycle) * moveTiltAmount * _velocityPercentage;
|
||||
// Traditional Bop Tilt
|
||||
float bopTilt = Mathf.Sin(cycle) * moveTiltAmount * _velocityPercentage;
|
||||
|
||||
// Phase 19-E: Banking Turn Tilt
|
||||
// Lean into the direction of X velocity
|
||||
float bankingTilt = -(_currentVelocity.x / 3f) * bankingAmount;
|
||||
|
||||
_targetLocalPos = new Vector3(0, bop, 0);
|
||||
_targetLocalRot = Quaternion.Euler(0, 0, tilt);
|
||||
_targetLocalRot = Quaternion.Euler(0, 0, bopTilt + bankingTilt);
|
||||
|
||||
// Squash and stretch during the bop
|
||||
float stretch = 1f + bop;
|
||||
float squash = 1f / stretch;
|
||||
_targetScale = new Vector3(_originalScale.x * stretch, _originalScale.y * squash, _originalScale.z);
|
||||
_targetScale = new Vector3(_originalScale.x * squash, _originalScale.y * stretch, _originalScale.z);
|
||||
}
|
||||
|
||||
private IEnumerator ActionPulseRoutine()
|
||||
private IEnumerator ActionPulseRoutine(float duration, float targetScaleY)
|
||||
{
|
||||
float elapsed = 0;
|
||||
float duration = 0.25f;
|
||||
Vector3 peakScale = new Vector3(_originalScale.x * (2f - targetScaleY), _originalScale.y * targetScaleY, _originalScale.z);
|
||||
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
float t = elapsed / duration;
|
||||
// Double pulse or overshoot
|
||||
float scale = 1.0f + Mathf.Sin(t * Mathf.PI) * 0.4f;
|
||||
float progress = elapsed / duration;
|
||||
float sin = Mathf.Sin(progress * Mathf.PI);
|
||||
|
||||
// Override target scale briefly
|
||||
_spriteRenderer.transform.localScale = new Vector3(_originalScale.x * scale, _originalScale.y * (2f - scale), _originalScale.z);
|
||||
// Temp override of targetScale for the pulse
|
||||
_spriteRenderer.transform.localScale = Vector3.Lerp(_originalScale, peakScale, sin);
|
||||
yield return null;
|
||||
}
|
||||
_targetScale = _originalScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user