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:
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>();
|
||||
trunkRenderer.sprite = CreateTreeSprite();
|
||||
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;
|
||||
@@ -449,33 +458,39 @@ namespace TheIsland.Visual
|
||||
if (System.IO.File.Exists(path))
|
||||
{
|
||||
byte[] data = System.IO.File.ReadAllBytes(path);
|
||||
_envTexture = new Texture2D(2, 2);
|
||||
_envTexture.LoadImage(data);
|
||||
Texture2D sourceTex = new Texture2D(2, 2);
|
||||
sourceTex.LoadImage(data);
|
||||
|
||||
// Phase 19-B: Fix white background transparency
|
||||
ProcessTransparency(_envTexture);
|
||||
// Phase 19-C: Robust transparency transcoding
|
||||
_envTexture = ProcessTransparency(sourceTex);
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessTransparency(Texture2D tex)
|
||||
private Texture2D ProcessTransparency(Texture2D source)
|
||||
{
|
||||
if (tex == null) return;
|
||||
Color[] pixels = tex.GetPixels();
|
||||
bool changed = false;
|
||||
if (source == null) return null;
|
||||
|
||||
// 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++)
|
||||
{
|
||||
// If the pixel is very close to white, make it transparent
|
||||
if (pixels[i].r > 0.92f && pixels[i].g > 0.92f && pixels[i].b > 0.92f)
|
||||
Color p = pixels[i];
|
||||
// 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;
|
||||
changed = true;
|
||||
pixels[i] = new Color(0, 0, 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
pixels[i] = new Color(p.r, p.g, p.b, 1.0f);
|
||||
}
|
||||
}
|
||||
if (changed)
|
||||
{
|
||||
tex.SetPixels(pixels);
|
||||
tex.Apply();
|
||||
}
|
||||
|
||||
tex.SetPixels(pixels);
|
||||
tex.Apply();
|
||||
return tex;
|
||||
}
|
||||
|
||||
private Sprite CreateTreeSprite()
|
||||
@@ -606,7 +621,14 @@ namespace TheIsland.Visual
|
||||
var rockRenderer = rockObj.AddComponent<SpriteRenderer>();
|
||||
rockRenderer.sprite = CreateRockSprite();
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user