feat: Phase 19-C/D - sprite loading, transparency, and animation
- Add runtime sprite loading from Characters.png and Environment.png - Implement ProcessTransparency for chroma-key white background removal - Add AgentAnimator for procedural idle/movement animations - Add Billboard component support for 2.5D perspective - Normalize sprite scales based on world units - Fix SetMovement parameter mismatch 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -115,45 +115,77 @@ namespace TheIsland.Visual
|
|||||||
{
|
{
|
||||||
if (!IsAlive) return;
|
if (!IsAlive) return;
|
||||||
|
|
||||||
|
// Phase 19-D: Apply soft-repulsion to prevent crowding
|
||||||
|
Vector3 repulsion = CalculateRepulsion();
|
||||||
|
|
||||||
// Handle Movement
|
// Handle Movement
|
||||||
if (_isMoving)
|
if (_isMoving)
|
||||||
{
|
{
|
||||||
transform.position = Vector3.MoveTowards(transform.position, _targetPosition, _moveSpeed * Time.deltaTime);
|
// Simple steering toward target
|
||||||
|
Vector3 moveDir = (_targetPosition - transform.position).normalized;
|
||||||
|
Vector3 finalVelocity = (moveDir * _moveSpeed) + repulsion;
|
||||||
|
|
||||||
|
transform.position += finalVelocity * Time.deltaTime;
|
||||||
|
|
||||||
// Flip sprite based on direction
|
// Flip sprite based on direction
|
||||||
if (_spriteRenderer != null)
|
if (_spriteRenderer != null && Mathf.Abs(moveDir.x) > 0.01f)
|
||||||
{
|
{
|
||||||
float dx = _targetPosition.x - transform.position.x;
|
_spriteRenderer.flipX = moveDir.x < 0;
|
||||||
if (Mathf.Abs(dx) > 0.1f)
|
|
||||||
{
|
|
||||||
// FlipX = true means face Left (assuming sprite faces Right by default)
|
|
||||||
// If sprite faces Front, we might need a different approach, but FlipX is standard for 2D.
|
|
||||||
_spriteRenderer.flipX = dx < 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Vector3.Distance(transform.position, _targetPosition) < 0.05f)
|
if (Vector3.Distance(transform.position, _targetPosition) < 0.1f)
|
||||||
{
|
{
|
||||||
_isMoving = false;
|
_isMoving = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (repulsion.sqrMagnitude > 0.001f)
|
||||||
if (_isMoving)
|
|
||||||
{
|
{
|
||||||
MoveTowardsTarget();
|
// Push away even when idle
|
||||||
|
transform.position += repulsion * Time.deltaTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 19-B: Use AgentAnimator for procedural movement/idle
|
// Phase 19-D: Dynamic Z-Sorting
|
||||||
|
if (_spriteRenderer != null)
|
||||||
|
{
|
||||||
|
// In world space, higher Z (further) should have lower sorting order
|
||||||
|
// Z typically ranges from -10 to 10 on the island
|
||||||
|
_spriteRenderer.sortingOrder = Mathf.RoundToInt(-transform.position.z * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 19-B/D: Use AgentAnimator
|
||||||
if (_animator != null)
|
if (_animator != null)
|
||||||
{
|
{
|
||||||
float velocity = _isMoving ? _moveSpeed : 0;
|
float currentSpeed = _isMoving ? _moveSpeed : 0;
|
||||||
_animator.SetMovement(velocity, _moveSpeed);
|
_animator.SetMovement(currentSpeed, _moveSpeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 19: Smooth UI Bar Transitions
|
// Phase 19: Smooth UI Bar Transitions
|
||||||
UpdateSmoothBars();
|
UpdateSmoothBars();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Vector3 CalculateRepulsion()
|
||||||
|
{
|
||||||
|
Vector3 force = Vector3.zero;
|
||||||
|
float radius = 1.2f; // Social distancing radius
|
||||||
|
float strength = 1.5f;
|
||||||
|
|
||||||
|
var allAgents = FindObjectsByType<AgentVisual>(FindObjectsSortMode.None);
|
||||||
|
foreach (var other in allAgents)
|
||||||
|
{
|
||||||
|
if (other == this || !other.IsAlive) continue;
|
||||||
|
|
||||||
|
Vector3 diff = transform.position - other.transform.position;
|
||||||
|
float dist = diff.magnitude;
|
||||||
|
|
||||||
|
if (dist < radius && dist > 0.01f)
|
||||||
|
{
|
||||||
|
// Linear falloff repulsion
|
||||||
|
force += diff.normalized * (1.0f - (dist / radius)) * strength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return force;
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateSmoothBars()
|
private void UpdateSmoothBars()
|
||||||
{
|
{
|
||||||
float lerpSpeed = 5f * Time.deltaTime;
|
float lerpSpeed = 5f * Time.deltaTime;
|
||||||
@@ -288,11 +320,11 @@ namespace TheIsland.Visual
|
|||||||
if (!System.IO.File.Exists(path)) yield break;
|
if (!System.IO.File.Exists(path)) yield break;
|
||||||
|
|
||||||
byte[] fileData = System.IO.File.ReadAllBytes(path);
|
byte[] fileData = System.IO.File.ReadAllBytes(path);
|
||||||
Texture2D tex = new Texture2D(2, 2);
|
Texture2D sourceTex = new Texture2D(2, 2);
|
||||||
tex.LoadImage(fileData);
|
sourceTex.LoadImage(fileData);
|
||||||
|
|
||||||
// Phase 19-B: Fix white background transparency
|
// Phase 19-C: Fix black/white background with robust transcoding
|
||||||
ProcessTransparency(tex);
|
Texture2D tex = ProcessTransparency(sourceTex);
|
||||||
|
|
||||||
// Slice the 1x3 collection (3 characters in a row)
|
// Slice the 1x3 collection (3 characters in a row)
|
||||||
int charIndex = id % 3;
|
int charIndex = id % 3;
|
||||||
@@ -304,23 +336,44 @@ namespace TheIsland.Visual
|
|||||||
{
|
{
|
||||||
_spriteRenderer.sprite = characterSprite;
|
_spriteRenderer.sprite = characterSprite;
|
||||||
_spriteRenderer.color = Color.white;
|
_spriteRenderer.color = Color.white;
|
||||||
|
|
||||||
|
// Phase 19-C: Normalize scale. Target height approx 2.0 units.
|
||||||
|
float spriteHeightUnits = characterSprite.rect.height / characterSprite.pixelsPerUnit;
|
||||||
|
float normScale = 2.0f / spriteHeightUnits; // Desired height is 2.0 units
|
||||||
|
_spriteRenderer.transform.localScale = new Vector3(normScale, normScale, 1);
|
||||||
|
|
||||||
|
// Update original scale for animator
|
||||||
|
_originalSpriteScale = _spriteRenderer.transform.localScale;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessTransparency(Texture2D tex)
|
private Texture2D ProcessTransparency(Texture2D source)
|
||||||
{
|
{
|
||||||
if (tex == null) return;
|
if (source == null) return null;
|
||||||
Color[] pixels = tex.GetPixels();
|
|
||||||
|
// Create a new texture with Alpha channel
|
||||||
|
Texture2D tex = new Texture2D(source.width, source.height, TextureFormat.RGBA32, false);
|
||||||
|
Color[] pixels = source.GetPixels();
|
||||||
|
|
||||||
for (int i = 0; i < pixels.Length; i++)
|
for (int i = 0; i < pixels.Length; i++)
|
||||||
{
|
{
|
||||||
// If the pixel is very close to white, make it transparent
|
Color p = pixels[i];
|
||||||
if (pixels[i].r > 0.95f && pixels[i].g > 0.95f && pixels[i].b > 0.95f)
|
// Chroma-key: If pixel is very close to white, make it transparent
|
||||||
|
// Using 0.9f as threshold to catch almost-white artifacts
|
||||||
|
if (p.r > 0.9f && p.g > 0.9f && p.b > 0.9f)
|
||||||
{
|
{
|
||||||
pixels[i] = Color.clear;
|
pixels[i] = new Color(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Ensure full opacity for others
|
||||||
|
pixels[i] = new Color(p.r, p.g, p.b, 1.0f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tex.SetPixels(pixels);
|
tex.SetPixels(pixels);
|
||||||
tex.Apply();
|
tex.Apply();
|
||||||
|
return tex;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyAgentColor(int agentId)
|
private void ApplyAgentColor(int agentId)
|
||||||
|
|||||||
123
unity-client/Assets/Scripts/Visual/AgentAnimator.cs
Normal file
123
unity-client/Assets/Scripts/Visual/AgentAnimator.cs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
namespace TheIsland
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Procedural 2D animator for agents.
|
||||||
|
/// Handles idle breathing, movement bopping, and action-based squash/stretch.
|
||||||
|
/// </summary>
|
||||||
|
public class AgentAnimator : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("Animation Settings")]
|
||||||
|
public float idleSpeed = 2f;
|
||||||
|
public float idleAmount = 0.05f;
|
||||||
|
public float moveBopSpeed = 12f;
|
||||||
|
public float moveBopAmount = 0.12f;
|
||||||
|
public float moveTiltAmount = 8f;
|
||||||
|
|
||||||
|
private SpriteRenderer _spriteRenderer;
|
||||||
|
private Vector3 _originalScale;
|
||||||
|
private Vector3 _targetLocalPos;
|
||||||
|
private Quaternion _targetLocalRot;
|
||||||
|
private Vector3 _targetScale;
|
||||||
|
|
||||||
|
private float _velocityPercentage; // 0 to 1
|
||||||
|
private bool _isMoving;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
// Find in children if not on this object
|
||||||
|
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
if (_spriteRenderer != null)
|
||||||
|
{
|
||||||
|
_originalScale = _spriteRenderer.transform.localScale;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_originalScale = Vector3.one;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetMovement(float currentVelocity, float maxVelocity)
|
||||||
|
{
|
||||||
|
_velocityPercentage = Mathf.Clamp01(currentVelocity / Mathf.Max(0.1f, maxVelocity));
|
||||||
|
_isMoving = _velocityPercentage > 0.05f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TriggerActionEffect()
|
||||||
|
{
|
||||||
|
StopAllCoroutines();
|
||||||
|
StartCoroutine(ActionPulseRoutine());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (_spriteRenderer == null) return;
|
||||||
|
|
||||||
|
if (_isMoving)
|
||||||
|
{
|
||||||
|
AnimateMove();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AnimateIdle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AnimateIdle()
|
||||||
|
{
|
||||||
|
// Idle "Breathing"
|
||||||
|
float breathe = Mathf.Sin(Time.time * idleSpeed) * idleAmount;
|
||||||
|
|
||||||
|
_targetScale = new Vector3(_originalScale.x, _originalScale.y * (1f + breathe), _originalScale.z);
|
||||||
|
_targetLocalPos = Vector3.zero;
|
||||||
|
_targetLocalRot = Quaternion.identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AnimateMove()
|
||||||
|
{
|
||||||
|
// Movement "Bopping" - Sin wave for vertical bounce
|
||||||
|
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;
|
||||||
|
|
||||||
|
_targetLocalPos = new Vector3(0, bop, 0);
|
||||||
|
_targetLocalRot = Quaternion.Euler(0, 0, tilt);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator ActionPulseRoutine()
|
||||||
|
{
|
||||||
|
float elapsed = 0;
|
||||||
|
float duration = 0.25f;
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Override target scale briefly
|
||||||
|
_spriteRenderer.transform.localScale = new Vector3(_originalScale.x * scale, _originalScale.y * (2f - scale), _originalScale.z);
|
||||||
|
yield return null;
|
||||||
|
}
|
||||||
|
_targetScale = _originalScale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
unity-client/Assets/Scripts/Visual/AgentAnimator.cs.meta
Normal file
2
unity-client/Assets/Scripts/Visual/AgentAnimator.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4b9e1cbf8a16c41ccb4b5f197e3ade72
|
||||||
@@ -437,8 +437,17 @@ namespace TheIsland.Visual
|
|||||||
var trunkRenderer = trunkSprite.AddComponent<SpriteRenderer>();
|
var trunkRenderer = trunkSprite.AddComponent<SpriteRenderer>();
|
||||||
trunkRenderer.sprite = CreateTreeSprite();
|
trunkRenderer.sprite = CreateTreeSprite();
|
||||||
trunkRenderer.sortingOrder = -20;
|
trunkRenderer.sortingOrder = -20;
|
||||||
// Phase 19-B: Uniform scale to avoid distortion
|
|
||||||
trunkSprite.transform.localScale = new Vector3(scale, scale, 1);
|
// Phase 19-C: Add Billboard for 2.5D perspective
|
||||||
|
trunkSprite.AddComponent<Billboard>();
|
||||||
|
|
||||||
|
// Phase 19-C: Normalize scale based on world units.
|
||||||
|
// If the sprite is large, we want it to fit the intended 'scale' height.
|
||||||
|
// A typical tree sprite at 100 PPU might be 10 units high.
|
||||||
|
// We want it to be 'scale' units high (e.g. 3 units).
|
||||||
|
float spriteHeightUnits = trunkRenderer.sprite.rect.height / trunkRenderer.sprite.pixelsPerUnit;
|
||||||
|
float normScale = scale / spriteHeightUnits;
|
||||||
|
trunkSprite.transform.localScale = new Vector3(normScale, normScale, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Texture2D _envTexture;
|
private Texture2D _envTexture;
|
||||||
@@ -449,33 +458,39 @@ namespace TheIsland.Visual
|
|||||||
if (System.IO.File.Exists(path))
|
if (System.IO.File.Exists(path))
|
||||||
{
|
{
|
||||||
byte[] data = System.IO.File.ReadAllBytes(path);
|
byte[] data = System.IO.File.ReadAllBytes(path);
|
||||||
_envTexture = new Texture2D(2, 2);
|
Texture2D sourceTex = new Texture2D(2, 2);
|
||||||
_envTexture.LoadImage(data);
|
sourceTex.LoadImage(data);
|
||||||
|
|
||||||
// Phase 19-B: Fix white background transparency
|
// Phase 19-C: Robust transparency transcoding
|
||||||
ProcessTransparency(_envTexture);
|
_envTexture = ProcessTransparency(sourceTex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessTransparency(Texture2D tex)
|
private Texture2D ProcessTransparency(Texture2D source)
|
||||||
{
|
{
|
||||||
if (tex == null) return;
|
if (source == null) return null;
|
||||||
Color[] pixels = tex.GetPixels();
|
|
||||||
bool changed = false;
|
// Create a new texture with Alpha channel
|
||||||
|
Texture2D tex = new Texture2D(source.width, source.height, TextureFormat.RGBA32, false);
|
||||||
|
Color[] pixels = source.GetPixels();
|
||||||
|
|
||||||
for (int i = 0; i < pixels.Length; i++)
|
for (int i = 0; i < pixels.Length; i++)
|
||||||
{
|
{
|
||||||
// If the pixel is very close to white, make it transparent
|
Color p = pixels[i];
|
||||||
if (pixels[i].r > 0.92f && pixels[i].g > 0.92f && pixels[i].b > 0.92f)
|
// Chroma-key: If pixel is very close to white, make it transparent
|
||||||
|
if (p.r > 0.9f && p.g > 0.9f && p.b > 0.9f)
|
||||||
{
|
{
|
||||||
pixels[i] = Color.clear;
|
pixels[i] = new Color(0, 0, 0, 0);
|
||||||
changed = true;
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pixels[i] = new Color(p.r, p.g, p.b, 1.0f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (changed)
|
|
||||||
{
|
|
||||||
tex.SetPixels(pixels);
|
tex.SetPixels(pixels);
|
||||||
tex.Apply();
|
tex.Apply();
|
||||||
}
|
return tex;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Sprite CreateTreeSprite()
|
private Sprite CreateTreeSprite()
|
||||||
@@ -606,7 +621,14 @@ namespace TheIsland.Visual
|
|||||||
var rockRenderer = rockObj.AddComponent<SpriteRenderer>();
|
var rockRenderer = rockObj.AddComponent<SpriteRenderer>();
|
||||||
rockRenderer.sprite = CreateRockSprite();
|
rockRenderer.sprite = CreateRockSprite();
|
||||||
rockRenderer.sortingOrder = -15;
|
rockRenderer.sortingOrder = -15;
|
||||||
rockObj.transform.localScale = Vector3.one * scale;
|
|
||||||
|
// Phase 19-C: Add Billboard
|
||||||
|
rockObj.AddComponent<Billboard>();
|
||||||
|
|
||||||
|
// Phase 19-C: Normalize scale
|
||||||
|
float spriteWidthUnits = rockRenderer.sprite.rect.width / rockRenderer.sprite.pixelsPerUnit;
|
||||||
|
float normScale = scale / spriteWidthUnits;
|
||||||
|
rockObj.transform.localScale = Vector3.one * normScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Sprite CreateRockSprite()
|
private Sprite CreateRockSprite()
|
||||||
|
|||||||
Reference in New Issue
Block a user