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

@@ -0,0 +1,74 @@
using UnityEngine;
using UnityEditor;
using UnityEngine.Rendering.PostProcessing;
public class SetupVisuals
{
[MenuItem("TheIsland/Setup PostProcessing")]
public static void Setup()
{
// 1. Setup Camera
var camera = Camera.main;
if (camera == null) { Debug.LogError("No Main Camera!"); return; }
var layer = camera.gameObject.GetComponent<PostProcessLayer>();
if (layer == null) layer = camera.gameObject.AddComponent<PostProcessLayer>();
// Use Default layer
layer.volumeLayer = LayerMask.GetMask("Default");
// Simple AA
layer.antialiasingMode = PostProcessLayer.Antialiasing.SubpixelMorphologicalAntialiasing;
// 2. Create Global Volume
var volumeGo = GameObject.Find("GlobalVolume");
if (volumeGo == null)
{
volumeGo = new GameObject("GlobalVolume");
volumeGo.layer = LayerMask.NameToLayer("Default");
}
var volume = volumeGo.GetComponent<PostProcessVolume>();
if (volume == null) volume = volumeGo.AddComponent<PostProcessVolume>();
volume.isGlobal = true;
// 3. Create Profile if not exists
var profilePath = "Assets/GlobalProfile.asset";
var profile = AssetDatabase.LoadAssetAtPath<PostProcessProfile>(profilePath);
if (profile == null)
{
profile = ScriptableObject.CreateInstance<PostProcessProfile>();
AssetDatabase.CreateAsset(profile, profilePath);
}
volume.profile = profile;
// 4. Clean existing settings
profile.settings.Clear();
// 5. Add Effects
// Bloom - Glow effect
var bloom = profile.AddSettings<Bloom>();
bloom.enabled.value = true;
bloom.intensity.value = 3.0f;
bloom.threshold.value = 1.0f;
bloom.softKnee.value = 0.5f;
// Color Grading - Better colors
var colorGrading = profile.AddSettings<ColorGrading>();
colorGrading.enabled.value = true;
colorGrading.tonemapper.value = Tonemapper.ACES;
colorGrading.postExposure.value = 0.5f; // Slightly brighter
colorGrading.saturation.value = 20f; // More vibrant
colorGrading.contrast.value = 15f; // More pop
// Vignette - Focus center
var vignette = profile.AddSettings<Vignette>();
vignette.enabled.value = true;
vignette.intensity.value = 0.35f;
vignette.smoothness.value = 0.4f;
EditorUtility.SetDirty(profile);
AssetDatabase.SaveAssets();
Debug.Log("Visuals Setup Complete! Bloom, ColorGrading, and Vignette configured.");
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2f42deb3551fc40dfbe902fe1945f25b

View File

@@ -0,0 +1,18 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 8e6292b2c06870d4495f009f912b9600, type: 3}
m_Name: GlobalProfile
m_EditorClassIdentifier: Unity.Postprocessing.Runtime::UnityEngine.Rendering.PostProcessing.PostProcessProfile
settings:
- {fileID: 0}
- {fileID: 0}
- {fileID: 0}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5a9d5cd213c7f4bebb196509d67c68e3
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

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.

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 45d3ecb46bd4b4019847fcced069b50f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,81 @@
Shader "TheIsland/CartoonWater"
{
Properties
{
_MainColor ("Water Color", Color) = (0.2, 0.5, 0.9, 0.8)
_FoamColor ("Foam Color", Color) = (1, 1, 1, 1)
_WaveSpeed ("Wave Speed", Range(0, 5)) = 1.0
_WaveHeight ("Wave Height", Range(0, 1)) = 0.1
_FoamAmount ("Foam Amount", Range(0, 1)) = 0.1
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent" }
LOD 100
Blend SrcAlpha OneMinusSrcAlpha
ZWrite Off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 worldPos : TEXCOORD1;
};
fixed4 _MainColor;
fixed4 _FoamColor;
float _WaveSpeed;
float _WaveHeight;
float _FoamAmount;
v2f vert (appdata v)
{
v2f o;
// Simple vertex displacement wave
float wave = sin(_Time.y * _WaveSpeed + v.vertex.x * 2.0) * _WaveHeight;
v.vertex.y += wave;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// Moving foam texture simulation using noise
float2 uv1 = i.uv + float2(_Time.x * 0.1, _Time.x * 0.05);
float noise = frac(sin(dot(uv1, float2(12.9898, 78.233))) * 43758.5453);
// 1. Horizon/Wave Foam (Top)
float waveFoam = step(1.0 - _FoamAmount - (noise * 0.05), i.uv.y);
// 2. Shoreline Foam (Bottom)
// Sine wave for "tide" effect
float tide = sin(_Time.y * 1.5) * 0.05;
float shoreThreshold = 0.05 + tide + (noise * 0.02);
float shoreFoam = step(i.uv.y, shoreThreshold);
// Combine foam
float totalFoam = max(waveFoam, shoreFoam);
fixed4 col = lerp(_MainColor, _FoamColor, totalFoam);
return col;
}
ENDCG
}
}
}

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: f82a9056a602b4fcd9b86f52d2c43c9a
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -43,6 +43,7 @@
"com.unity.modules.video": "1.0.0",
"com.unity.modules.vr": "1.0.0",
"com.unity.modules.wind": "1.0.0",
"com.unity.modules.xr": "1.0.0"
"com.unity.modules.xr": "1.0.0",
"com.unity.postprocessing": "3.4.0"
}
}
}

View File

@@ -276,6 +276,15 @@
"dependencies": {},
"url": "https://packages.unity.com"
},
"com.unity.postprocessing": {
"version": "3.4.0",
"depth": 0,
"source": "registry",
"dependencies": {
"com.unity.modules.physics": "1.0.0"
},
"url": "https://packages.unity.com"
},
"com.unity.serialization": {
"version": "3.1.3",
"depth": 1,

View File

@@ -682,7 +682,22 @@ PlayerSettings:
webWasm2023: 0
webEnableSubmoduleStrippingCompatibility: 0
scriptingDefineSymbols:
Standalone: APP_UI_EDITOR_ONLY
Android: UNITY_POST_PROCESSING_STACK_V2
EmbeddedLinux: UNITY_POST_PROCESSING_STACK_V2
GameCoreScarlett: UNITY_POST_PROCESSING_STACK_V2
GameCoreXboxOne: UNITY_POST_PROCESSING_STACK_V2
Kepler: UNITY_POST_PROCESSING_STACK_V2
LinuxHeadlessSimulation: UNITY_POST_PROCESSING_STACK_V2
Nintendo Switch: UNITY_POST_PROCESSING_STACK_V2
Nintendo Switch 2: UNITY_POST_PROCESSING_STACK_V2
PS4: UNITY_POST_PROCESSING_STACK_V2
PS5: UNITY_POST_PROCESSING_STACK_V2
QNX: UNITY_POST_PROCESSING_STACK_V2
Standalone: APP_UI_EDITOR_ONLY;UNITY_POST_PROCESSING_STACK_V2
VisionOS: UNITY_POST_PROCESSING_STACK_V2
WebGL: UNITY_POST_PROCESSING_STACK_V2
XboxOne: UNITY_POST_PROCESSING_STACK_V2
tvOS: UNITY_POST_PROCESSING_STACK_V2
additionalCompilerArguments: {}
platformArchitecture: {}
scriptingBackend: {}