feat(unity): enhance visual effects and animations

- Add cloud system with procedural sprites and parallax movement
- Add tree swaying animation for palm trees
- Improve agent breathing with squash & stretch animation
- Add jump animation routine for agent reactions
- Add custom CartoonWater shader support
- Add SetupVisuals editor tool and GlobalProfile asset
- Lower speech bubble alpha for glass effect

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
empty
2026-01-01 22:07:12 +08:00
parent d1b02b4dfd
commit 20c82276fa
15 changed files with 387 additions and 22 deletions

View File

@@ -95,29 +95,60 @@ namespace TheIsland.Visual
{
if (!IsAlive) return;
// Idle breathing animation
// Idle breathing animation (Squash and Stretch)
_idleAnimTimer += Time.deltaTime;
_breathScale = 1f + Mathf.Sin(_idleAnimTimer * 2f) * 0.02f;
// Breathing: Scale Y up, Scale X down (preserving volume)
float breath = Mathf.Sin(_idleAnimTimer * 3f) * 0.05f;
_breathScale = 1f + breath;
float antiBreath = 1f - (breath * 0.5f); // Squash X when stretching Y
// Gentle bobbing
_bobOffset = Mathf.Sin(_idleAnimTimer * 1.5f) * 0.05f;
// Bobbing: Move up and down
_bobOffset = Mathf.Sin(_idleAnimTimer * 2f) * 0.08f;
if (_spriteRenderer != null && _originalSpriteScale != Vector3.zero)
{
// Apply breathing scale
// Apply squash & stretch
_spriteRenderer.transform.localScale = new Vector3(
_originalSpriteScale.x * _breathScale,
_originalSpriteScale.x * antiBreath,
_originalSpriteScale.y * _breathScale,
_originalSpriteScale.z
);
// Apply bobbing
// Apply bobbing position
var pos = _spriteRenderer.transform.localPosition;
pos.y = 1f + _bobOffset;
_spriteRenderer.transform.localPosition = pos;
}
}
// Trigger a jump animation (to be called by events)
public void DoJump()
{
StartCoroutine(JumpRoutine());
}
private IEnumerator JumpRoutine()
{
float timer = 0;
float duration = 0.4f;
Vector3 startPos = _spriteRenderer.transform.localPosition;
while (timer < duration)
{
timer += Time.deltaTime;
float t = timer / duration;
// Parabolic jump height
float height = Mathf.Sin(t * Mathf.PI) * 0.5f;
var pos = _spriteRenderer.transform.localPosition;
pos.y = startPos.y + height;
_spriteRenderer.transform.localPosition = pos;
yield return null;
}
}
private void OnMouseDown()
{
if (!IsAlive)
@@ -604,7 +635,7 @@ namespace TheIsland.Visual
var bg = panel.AddComponent<Image>();
bg.sprite = CreateRoundedRectSprite(32, 32, 8);
bg.type = Image.Type.Sliced;
bg.color = new Color(0.1f, 0.12f, 0.18f, 0.85f);
bg.color = new Color(0.1f, 0.12f, 0.18f, 0.6f); // Lower alpha for glass effect
// Add subtle border
var borderObj = new GameObject("Border");

View File

@@ -189,7 +189,7 @@ namespace TheIsland.UI
panelRect.offsetMax = new Vector2(360, -80);
var panelImg = _panel.AddComponent<Image>();
panelImg.color = new Color(0.05f, 0.07f, 0.1f, 0.95f);
panelImg.color = new Color(0f, 0f, 0f, 0.0f); // 完全透明背景
// 标题
var header = new GameObject("Header");
@@ -201,7 +201,7 @@ namespace TheIsland.UI
headerRect.sizeDelta = new Vector2(0, 28);
headerRect.anchoredPosition = Vector2.zero;
header.AddComponent<Image>().color = new Color(0.12f, 0.15f, 0.2f);
header.AddComponent<Image>().color = new Color(0.12f, 0.15f, 0.2f, 0.8f);
var titleObj = new GameObject("Title");
titleObj.transform.SetParent(header.transform, false);
@@ -296,8 +296,8 @@ namespace TheIsland.UI
entry.transform.SetParent(_content, false);
entry.AddComponent<Image>().color = _entries.Count % 2 == 0
? new Color(0.08f, 0.1f, 0.13f, 0.9f)
: new Color(0.06f, 0.08f, 0.11f, 0.9f);
? new Color(0f, 0f, 0f, 0.2f)
: new Color(0f, 0f, 0f, 0.1f);
var le = entry.AddComponent<LayoutElement>();
le.minHeight = 36;

View File

@@ -322,6 +322,7 @@ namespace TheIsland.Core
if (_agentVisuals.TryGetValue(data.agent_id, out AgentVisual agentVisual))
{
agentVisual.ShowSpeech(data.text);
agentVisual.DoJump(); // Add jump effect
}
// Check AgentUI (programmatic UI system)
else if (_agentUIs.TryGetValue(data.agent_id, out AgentUI agentUI))

View File

@@ -164,7 +164,7 @@ namespace TheIsland.UI
topBar.offsetMax = new Vector2(-10, -10);
var topBarImg = topBar.gameObject.AddComponent<Image>();
topBarImg.color = new Color(0, 0, 0, 0.7f);
topBarImg.color = new Color(0, 0, 0, 0.0f); // 透明顶部栏
// Connection Status (Left)
_connectionStatus = CreateText(topBar, "ConnectionStatus", "● Disconnected",
@@ -205,7 +205,7 @@ namespace TheIsland.UI
bottomBar.offsetMax = new Vector2(-10, 70);
var bottomBarImg = bottomBar.gameObject.AddComponent<Image>();
bottomBarImg.color = new Color(0, 0, 0, 0.7f);
bottomBarImg.color = new Color(0, 0, 0, 0.2f); // 低透明度底部栏
// Command Input
var inputObj = new GameObject("CommandInput");

View File

@@ -57,6 +57,7 @@ namespace TheIsland.Visual
[SerializeField] private Color waterDeepColor = new Color(0.1f, 0.4f, 0.6f, 0.9f);
[SerializeField] private float waveSpeed = 0.5f;
[SerializeField] private float waveAmplitude = 0.1f;
[SerializeField] private Material customWaterMaterial; // Custom shader support
#endregion
#region References
@@ -116,8 +117,9 @@ namespace TheIsland.Visual
UpdateSkyMaterial();
}
// Animate water
AnimateWater();
// Animate environment (Water & Trees)
AnimateEnvironment();
AnimateClouds();
}
private void OnDestroy()
@@ -140,6 +142,7 @@ namespace TheIsland.Visual
CreateWater();
CreateLighting();
CreateDecorations();
CreateClouds();
}
private void CreateSky()
@@ -308,9 +311,17 @@ namespace TheIsland.Visual
_waterPlane.transform.localScale = new Vector3(60, 15, 1);
// Create water material
_waterMaterial = new Material(Shader.Find("Unlit/Transparent"));
_waterMaterial.mainTexture = CreateWaterTexture();
_waterPlane.GetComponent<Renderer>().material = _waterMaterial;
if (customWaterMaterial != null)
{
_waterMaterial = customWaterMaterial;
_waterPlane.GetComponent<Renderer>().material = _waterMaterial;
}
else
{
_waterMaterial = new Material(Shader.Find("Unlit/Transparent"));
_waterMaterial.mainTexture = CreateWaterTexture();
_waterPlane.GetComponent<Renderer>().material = _waterMaterial;
}
_waterPlane.GetComponent<Renderer>().sortingOrder = -40;
Destroy(_waterPlane.GetComponent<Collider>());
@@ -439,6 +450,29 @@ namespace TheIsland.Visual
return Sprite.Create(tex, new Rect(0, 0, width, height), new Vector2(0.5f, 0));
}
private void AnimateEnvironment()
{
// Water animation
if (_waterMaterial != null)
{
float offset = Time.time * waveSpeed * 0.1f;
_waterMaterial.mainTextureOffset = new Vector2(offset, offset * 0.5f);
}
// Tree swaying animation
// Find all palm tree objects (simple lookup by name since we created them)
// Ideally we'd cache these, but for this scale it's fine
foreach (Transform child in transform)
{
if (child.name == "PalmTree")
{
// Sway rotation
float sway = Mathf.Sin(Time.time * 1.5f + child.position.x) * 2.0f;
child.rotation = Quaternion.Euler(0, 0, sway);
}
}
}
private void DrawPalmFronds(Color[] pixels, int width, int height, Color leaf, Color leafBright)
{
Vector2 center = new Vector2(width / 2, height * 0.65f);
@@ -612,6 +646,80 @@ namespace TheIsland.Visual
}
#endregion
private void CreateClouds()
{
for (int i = 0; i < 5; i++)
{
var cloud = new GameObject("Cloud");
cloud.transform.SetParent(transform);
// Random position in sky
float startX = Random.Range(-25f, 25f);
float startY = Random.Range(3f, 8f);
float depth = Random.Range(15f, 25f);
cloud.transform.position = new Vector3(startX, startY, depth);
var renderer = cloud.AddComponent<SpriteRenderer>();
renderer.sprite = CreateCloudSprite();
renderer.sortingOrder = -90; // Behind everything but sky
// Random size and opacity
float scale = Random.Range(3f, 6f);
cloud.transform.localScale = new Vector3(scale * 1.5f, scale, 1f);
renderer.color = new Color(1f, 1f, 1f, Random.Range(0.4f, 0.8f));
}
}
private Sprite CreateCloudSprite()
{
int size = 64;
Texture2D tex = new Texture2D(size, size);
Color[] pixels = new Color[size * size];
// Procedural fluffy cloud
Vector2 center = new Vector2(size/2, size/2);
for (int y = 0; y < size; y++)
{
for (int x = 0; x < size; x++)
{
float noise = Mathf.PerlinNoise(x * 0.15f, y * 0.15f); // Noise frequency
float dist = Vector2.Distance(new Vector2(x, y), center) / (size * 0.4f);
// Soft circle with noise
float density = Mathf.Clamp01(1f - dist);
density *= (0.5f + noise * 0.5f);
// Threshold for fluffiness
density = Mathf.SmoothStep(0.2f, 0.8f, density);
pixels[y * size + x] = new Color(1, 1, 1, density * density);
}
}
tex.SetPixels(pixels);
tex.Apply();
return Sprite.Create(tex, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f));
}
private void AnimateClouds()
{
// Move clouds slowly
foreach (Transform child in transform)
{
if (child.name == "Cloud")
{
Vector3 pos = child.transform.position;
// Wind speed depends on cloud distance for parallax
float speed = 0.5f + (25f - pos.z) * 0.05f;
pos.x += Time.deltaTime * speed;
// Wrap around
if (pos.x > 30f) pos.x = -30f;
child.transform.position = pos;
}
}
}
#region Public API
/// <summary>
/// Force update the environment to specific conditions.