- Add EventLog.cs: scrollable event log panel showing game events - Color-coded entries for different event types - Toggle visibility, clear button, timestamps - Auto-scroll to newest entries - Add Chinese font support (Source Han Sans SC) - SourceHanSansSC-Regular.otf font file - ChineseFontSetup.cs editor tool for TMP font asset generation - Configure as fallback font for TextMeshPro - Fix particle system velocity curve warnings in WeatherEffects.cs - Ensure all velocity axes use consistent curve mode - Add comprehensive README.md with project documentation - Architecture overview, features, tech stack - Quick start guide, Unity client structure - Communication protocol, development notes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
481 lines
17 KiB
C#
481 lines
17 KiB
C#
using UnityEngine;
|
|
using TheIsland.Network;
|
|
using TheIsland.Models;
|
|
|
|
namespace TheIsland.Visual
|
|
{
|
|
/// <summary>
|
|
/// Creates and manages weather particle effects.
|
|
/// Responds to weather changes from the server.
|
|
/// </summary>
|
|
public class WeatherEffects : MonoBehaviour
|
|
{
|
|
#region Singleton
|
|
private static WeatherEffects _instance;
|
|
public static WeatherEffects Instance => _instance;
|
|
#endregion
|
|
|
|
#region Configuration
|
|
[Header("Rain Settings")]
|
|
[SerializeField] private int rainParticleCount = 500;
|
|
[SerializeField] private int stormParticleCount = 1000;
|
|
[SerializeField] private Color rainColor = new Color(0.7f, 0.8f, 0.9f, 0.6f);
|
|
|
|
[Header("Sun Settings")]
|
|
[SerializeField] private int sunRayCount = 50;
|
|
[SerializeField] private Color sunRayColor = new Color(1f, 0.95f, 0.8f, 0.3f);
|
|
|
|
[Header("Fog Settings")]
|
|
[SerializeField] private Color fogColor = new Color(0.85f, 0.85f, 0.9f, 0.5f);
|
|
|
|
[Header("Hot Weather Settings")]
|
|
[SerializeField] private int heatWaveCount = 30;
|
|
[SerializeField] private Color heatColor = new Color(1f, 0.9f, 0.7f, 0.2f);
|
|
#endregion
|
|
|
|
#region References
|
|
private ParticleSystem _rainSystem;
|
|
private ParticleSystem _sunRaySystem;
|
|
private ParticleSystem _fogSystem;
|
|
private ParticleSystem _heatSystem;
|
|
private ParticleSystem _cloudSystem;
|
|
|
|
private string _currentWeather = "Sunny";
|
|
#endregion
|
|
|
|
#region Unity Lifecycle
|
|
private void Awake()
|
|
{
|
|
if (_instance != null && _instance != this)
|
|
{
|
|
Destroy(gameObject);
|
|
return;
|
|
}
|
|
_instance = this;
|
|
|
|
CreateAllEffects();
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
var network = NetworkManager.Instance;
|
|
if (network != null)
|
|
{
|
|
network.OnWeatherChange += HandleWeatherChange;
|
|
network.OnTick += HandleTick;
|
|
}
|
|
|
|
// Start with sunny weather
|
|
SetWeather("Sunny");
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
var network = NetworkManager.Instance;
|
|
if (network != null)
|
|
{
|
|
network.OnWeatherChange -= HandleWeatherChange;
|
|
network.OnTick -= HandleTick;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Effect Creation
|
|
private void CreateAllEffects()
|
|
{
|
|
CreateRainEffect();
|
|
CreateSunRayEffect();
|
|
CreateFogEffect();
|
|
CreateHeatEffect();
|
|
CreateCloudEffect();
|
|
}
|
|
|
|
private void CreateRainEffect()
|
|
{
|
|
var rainObj = new GameObject("RainEffect");
|
|
rainObj.transform.SetParent(transform);
|
|
rainObj.transform.position = new Vector3(0, 10, 5);
|
|
|
|
_rainSystem = rainObj.AddComponent<ParticleSystem>();
|
|
var main = _rainSystem.main;
|
|
main.maxParticles = stormParticleCount;
|
|
main.startLifetime = 1.5f;
|
|
main.startSpeed = 15f;
|
|
main.startSize = 0.05f;
|
|
main.startColor = rainColor;
|
|
main.simulationSpace = ParticleSystemSimulationSpace.World;
|
|
main.gravityModifier = 1.5f;
|
|
|
|
var emission = _rainSystem.emission;
|
|
emission.rateOverTime = rainParticleCount;
|
|
|
|
var shape = _rainSystem.shape;
|
|
shape.shapeType = ParticleSystemShapeType.Box;
|
|
shape.scale = new Vector3(25, 0.1f, 15);
|
|
|
|
// Renderer settings
|
|
var renderer = rainObj.GetComponent<ParticleSystemRenderer>();
|
|
renderer.material = CreateParticleMaterial(rainColor);
|
|
renderer.sortingOrder = 50;
|
|
|
|
// Start stopped
|
|
_rainSystem.Stop();
|
|
}
|
|
|
|
private void CreateSunRayEffect()
|
|
{
|
|
var sunObj = new GameObject("SunRayEffect");
|
|
sunObj.transform.SetParent(transform);
|
|
sunObj.transform.position = new Vector3(5, 8, 10);
|
|
sunObj.transform.rotation = Quaternion.Euler(45, -30, 0);
|
|
|
|
_sunRaySystem = sunObj.AddComponent<ParticleSystem>();
|
|
var main = _sunRaySystem.main;
|
|
main.maxParticles = sunRayCount;
|
|
main.startLifetime = 3f;
|
|
main.startSpeed = 0.5f;
|
|
main.startSize = new ParticleSystem.MinMaxCurve(0.5f, 2f);
|
|
main.startColor = sunRayColor;
|
|
main.simulationSpace = ParticleSystemSimulationSpace.World;
|
|
|
|
var emission = _sunRaySystem.emission;
|
|
emission.rateOverTime = 10;
|
|
|
|
var shape = _sunRaySystem.shape;
|
|
shape.shapeType = ParticleSystemShapeType.Cone;
|
|
shape.angle = 15;
|
|
shape.radius = 3;
|
|
|
|
var colorOverLifetime = _sunRaySystem.colorOverLifetime;
|
|
colorOverLifetime.enabled = true;
|
|
Gradient gradient = new Gradient();
|
|
gradient.SetKeys(
|
|
new GradientColorKey[] { new GradientColorKey(Color.white, 0), new GradientColorKey(Color.white, 1) },
|
|
new GradientAlphaKey[] { new GradientAlphaKey(0, 0), new GradientAlphaKey(0.3f, 0.3f), new GradientAlphaKey(0, 1) }
|
|
);
|
|
colorOverLifetime.color = gradient;
|
|
|
|
var sizeOverLifetime = _sunRaySystem.sizeOverLifetime;
|
|
sizeOverLifetime.enabled = true;
|
|
sizeOverLifetime.size = new ParticleSystem.MinMaxCurve(1f, new AnimationCurve(
|
|
new Keyframe(0, 0.5f), new Keyframe(0.5f, 1f), new Keyframe(1, 1.5f)));
|
|
|
|
var renderer = sunObj.GetComponent<ParticleSystemRenderer>();
|
|
renderer.material = CreateSunRayMaterial();
|
|
renderer.sortingOrder = 40;
|
|
|
|
_sunRaySystem.Stop();
|
|
}
|
|
|
|
private void CreateFogEffect()
|
|
{
|
|
var fogObj = new GameObject("FogEffect");
|
|
fogObj.transform.SetParent(transform);
|
|
fogObj.transform.position = new Vector3(0, 1, 5);
|
|
|
|
_fogSystem = fogObj.AddComponent<ParticleSystem>();
|
|
var main = _fogSystem.main;
|
|
main.maxParticles = 100;
|
|
main.startLifetime = 8f;
|
|
main.startSpeed = 0.3f;
|
|
main.startSize = new ParticleSystem.MinMaxCurve(3f, 6f);
|
|
main.startColor = fogColor;
|
|
main.simulationSpace = ParticleSystemSimulationSpace.World;
|
|
|
|
var emission = _fogSystem.emission;
|
|
emission.rateOverTime = 5;
|
|
|
|
var shape = _fogSystem.shape;
|
|
shape.shapeType = ParticleSystemShapeType.Box;
|
|
shape.scale = new Vector3(30, 2, 15);
|
|
|
|
var velocityOverLifetime = _fogSystem.velocityOverLifetime;
|
|
velocityOverLifetime.enabled = true;
|
|
// 所有轴使用相同的曲线模式 (Constant)
|
|
velocityOverLifetime.x = 0f;
|
|
velocityOverLifetime.y = 0.08f;
|
|
velocityOverLifetime.z = 0f;
|
|
|
|
var colorOverLifetime = _fogSystem.colorOverLifetime;
|
|
colorOverLifetime.enabled = true;
|
|
Gradient gradient = new Gradient();
|
|
gradient.SetKeys(
|
|
new GradientColorKey[] { new GradientColorKey(Color.white, 0), new GradientColorKey(Color.white, 1) },
|
|
new GradientAlphaKey[] { new GradientAlphaKey(0, 0), new GradientAlphaKey(0.5f, 0.3f), new GradientAlphaKey(0.5f, 0.7f), new GradientAlphaKey(0, 1) }
|
|
);
|
|
colorOverLifetime.color = gradient;
|
|
|
|
var renderer = fogObj.GetComponent<ParticleSystemRenderer>();
|
|
renderer.material = CreateFogMaterial();
|
|
renderer.sortingOrder = 30;
|
|
|
|
_fogSystem.Stop();
|
|
}
|
|
|
|
private void CreateHeatEffect()
|
|
{
|
|
var heatObj = new GameObject("HeatEffect");
|
|
heatObj.transform.SetParent(transform);
|
|
heatObj.transform.position = new Vector3(0, 0, 5);
|
|
|
|
_heatSystem = heatObj.AddComponent<ParticleSystem>();
|
|
var main = _heatSystem.main;
|
|
main.maxParticles = heatWaveCount;
|
|
main.startLifetime = 4f;
|
|
main.startSpeed = 0.8f;
|
|
main.startSize = new ParticleSystem.MinMaxCurve(1f, 3f);
|
|
main.startColor = heatColor;
|
|
main.simulationSpace = ParticleSystemSimulationSpace.World;
|
|
|
|
var emission = _heatSystem.emission;
|
|
emission.rateOverTime = 8;
|
|
|
|
var shape = _heatSystem.shape;
|
|
shape.shapeType = ParticleSystemShapeType.Box;
|
|
shape.scale = new Vector3(20, 0.1f, 10);
|
|
|
|
var velocityOverLifetime = _heatSystem.velocityOverLifetime;
|
|
velocityOverLifetime.enabled = true;
|
|
// 所有轴使用相同的曲线模式 (Constant)
|
|
velocityOverLifetime.x = 0f;
|
|
velocityOverLifetime.y = 1f;
|
|
velocityOverLifetime.z = 0f;
|
|
|
|
var colorOverLifetime = _heatSystem.colorOverLifetime;
|
|
colorOverLifetime.enabled = true;
|
|
Gradient gradient = new Gradient();
|
|
gradient.SetKeys(
|
|
new GradientColorKey[] { new GradientColorKey(heatColor, 0), new GradientColorKey(heatColor, 1) },
|
|
new GradientAlphaKey[] { new GradientAlphaKey(0, 0), new GradientAlphaKey(0.2f, 0.3f), new GradientAlphaKey(0, 1) }
|
|
);
|
|
colorOverLifetime.color = gradient;
|
|
|
|
var renderer = heatObj.GetComponent<ParticleSystemRenderer>();
|
|
renderer.material = CreateHeatMaterial();
|
|
renderer.sortingOrder = 35;
|
|
|
|
_heatSystem.Stop();
|
|
}
|
|
|
|
private void CreateCloudEffect()
|
|
{
|
|
var cloudObj = new GameObject("CloudEffect");
|
|
cloudObj.transform.SetParent(transform);
|
|
cloudObj.transform.position = new Vector3(0, 8, 15);
|
|
|
|
_cloudSystem = cloudObj.AddComponent<ParticleSystem>();
|
|
var main = _cloudSystem.main;
|
|
main.maxParticles = 30;
|
|
main.startLifetime = 20f;
|
|
main.startSpeed = 0.2f;
|
|
main.startSize = new ParticleSystem.MinMaxCurve(5f, 10f);
|
|
main.startColor = new Color(1, 1, 1, 0.7f);
|
|
main.simulationSpace = ParticleSystemSimulationSpace.World;
|
|
|
|
var emission = _cloudSystem.emission;
|
|
emission.rateOverTime = 1;
|
|
|
|
var shape = _cloudSystem.shape;
|
|
shape.shapeType = ParticleSystemShapeType.Box;
|
|
shape.scale = new Vector3(40, 2, 5);
|
|
|
|
var velocityOverLifetime = _cloudSystem.velocityOverLifetime;
|
|
velocityOverLifetime.enabled = true;
|
|
// 所有轴使用相同的曲线模式 (Constant)
|
|
velocityOverLifetime.x = 0.3f;
|
|
velocityOverLifetime.y = 0f;
|
|
velocityOverLifetime.z = 0f;
|
|
|
|
var renderer = cloudObj.GetComponent<ParticleSystemRenderer>();
|
|
renderer.material = CreateCloudMaterial();
|
|
renderer.sortingOrder = 25;
|
|
|
|
_cloudSystem.Stop();
|
|
}
|
|
#endregion
|
|
|
|
#region Material Creation
|
|
private Material CreateParticleMaterial(Color color)
|
|
{
|
|
Material mat = new Material(Shader.Find("Particles/Standard Unlit"));
|
|
mat.SetColor("_Color", color);
|
|
mat.SetFloat("_Mode", 2); // Fade mode
|
|
|
|
// Create simple white texture
|
|
Texture2D tex = new Texture2D(8, 8);
|
|
for (int i = 0; i < 64; i++) tex.SetPixel(i % 8, i / 8, Color.white);
|
|
tex.Apply();
|
|
mat.mainTexture = tex;
|
|
|
|
return mat;
|
|
}
|
|
|
|
private Material CreateSunRayMaterial()
|
|
{
|
|
Material mat = new Material(Shader.Find("Particles/Standard Unlit"));
|
|
mat.SetColor("_Color", sunRayColor);
|
|
mat.SetFloat("_Mode", 1); // Additive
|
|
|
|
// Create soft gradient texture
|
|
Texture2D tex = new Texture2D(32, 32);
|
|
Vector2 center = new Vector2(16, 16);
|
|
for (int y = 0; y < 32; y++)
|
|
{
|
|
for (int x = 0; x < 32; x++)
|
|
{
|
|
float dist = Vector2.Distance(new Vector2(x, y), center) / 16f;
|
|
float alpha = Mathf.Clamp01(1 - dist);
|
|
tex.SetPixel(x, y, new Color(1, 1, 1, alpha * alpha));
|
|
}
|
|
}
|
|
tex.Apply();
|
|
mat.mainTexture = tex;
|
|
|
|
return mat;
|
|
}
|
|
|
|
private Material CreateFogMaterial()
|
|
{
|
|
Material mat = new Material(Shader.Find("Particles/Standard Unlit"));
|
|
mat.SetColor("_Color", fogColor);
|
|
mat.SetFloat("_Mode", 2); // Fade
|
|
|
|
// Create soft cloud texture
|
|
Texture2D tex = new Texture2D(64, 64);
|
|
for (int y = 0; y < 64; y++)
|
|
{
|
|
for (int x = 0; x < 64; x++)
|
|
{
|
|
float noise = Mathf.PerlinNoise(x * 0.1f, y * 0.1f);
|
|
float dist = Vector2.Distance(new Vector2(x, y), new Vector2(32, 32)) / 32f;
|
|
float alpha = Mathf.Clamp01((1 - dist) * noise);
|
|
tex.SetPixel(x, y, new Color(1, 1, 1, alpha));
|
|
}
|
|
}
|
|
tex.Apply();
|
|
mat.mainTexture = tex;
|
|
|
|
return mat;
|
|
}
|
|
|
|
private Material CreateHeatMaterial()
|
|
{
|
|
Material mat = new Material(Shader.Find("Particles/Standard Unlit"));
|
|
mat.SetColor("_Color", heatColor);
|
|
mat.SetFloat("_Mode", 1); // Additive
|
|
|
|
// Create wavy heat texture
|
|
Texture2D tex = new Texture2D(32, 64);
|
|
for (int y = 0; y < 64; y++)
|
|
{
|
|
for (int x = 0; x < 32; x++)
|
|
{
|
|
float wave = Mathf.Sin((x + y * 0.3f) * 0.3f) * 0.5f + 0.5f;
|
|
float fade = 1 - Mathf.Abs(x - 16) / 16f;
|
|
float alpha = wave * fade * (1 - y / 64f);
|
|
tex.SetPixel(x, y, new Color(1, 1, 1, alpha * 0.3f));
|
|
}
|
|
}
|
|
tex.Apply();
|
|
mat.mainTexture = tex;
|
|
|
|
return mat;
|
|
}
|
|
|
|
private Material CreateCloudMaterial()
|
|
{
|
|
Material mat = new Material(Shader.Find("Particles/Standard Unlit"));
|
|
mat.SetColor("_Color", Color.white);
|
|
mat.SetFloat("_Mode", 2); // Fade
|
|
|
|
// Create fluffy cloud texture
|
|
Texture2D tex = new Texture2D(64, 64);
|
|
for (int y = 0; y < 64; y++)
|
|
{
|
|
for (int x = 0; x < 64; x++)
|
|
{
|
|
float noise1 = Mathf.PerlinNoise(x * 0.08f, y * 0.08f);
|
|
float noise2 = Mathf.PerlinNoise(x * 0.15f + 100, y * 0.15f + 100) * 0.5f;
|
|
float dist = Vector2.Distance(new Vector2(x, y), new Vector2(32, 32)) / 32f;
|
|
float alpha = Mathf.Clamp01((noise1 + noise2) * (1 - dist * dist));
|
|
tex.SetPixel(x, y, new Color(1, 1, 1, alpha * 0.8f));
|
|
}
|
|
}
|
|
tex.Apply();
|
|
tex.filterMode = FilterMode.Bilinear;
|
|
mat.mainTexture = tex;
|
|
|
|
return mat;
|
|
}
|
|
#endregion
|
|
|
|
#region Weather Control
|
|
private void HandleWeatherChange(WeatherChangeData data)
|
|
{
|
|
SetWeather(data.new_weather);
|
|
}
|
|
|
|
private void HandleTick(TickData data)
|
|
{
|
|
if (!string.IsNullOrEmpty(data.weather) && data.weather != _currentWeather)
|
|
{
|
|
SetWeather(data.weather);
|
|
}
|
|
}
|
|
|
|
public void SetWeather(string weather)
|
|
{
|
|
_currentWeather = weather;
|
|
|
|
// Stop all effects first
|
|
_rainSystem?.Stop();
|
|
_sunRaySystem?.Stop();
|
|
_fogSystem?.Stop();
|
|
_heatSystem?.Stop();
|
|
_cloudSystem?.Stop();
|
|
|
|
// Enable appropriate effects
|
|
switch (weather)
|
|
{
|
|
case "Sunny":
|
|
_sunRaySystem?.Play();
|
|
break;
|
|
|
|
case "Cloudy":
|
|
_cloudSystem?.Play();
|
|
break;
|
|
|
|
case "Rainy":
|
|
_rainSystem?.Play();
|
|
var rainMain = _rainSystem.main;
|
|
var rainEmission = _rainSystem.emission;
|
|
rainEmission.rateOverTime = rainParticleCount;
|
|
_cloudSystem?.Play();
|
|
break;
|
|
|
|
case "Stormy":
|
|
_rainSystem?.Play();
|
|
var stormMain = _rainSystem.main;
|
|
var stormEmission = _rainSystem.emission;
|
|
stormEmission.rateOverTime = stormParticleCount;
|
|
stormMain.startSpeed = 20f;
|
|
_cloudSystem?.Play();
|
|
// Could add lightning flashes here
|
|
break;
|
|
|
|
case "Foggy":
|
|
_fogSystem?.Play();
|
|
break;
|
|
|
|
case "Hot":
|
|
_heatSystem?.Play();
|
|
_sunRaySystem?.Play();
|
|
break;
|
|
}
|
|
|
|
Debug.Log($"[WeatherEffects] Weather set to: {weather}");
|
|
}
|
|
#endregion
|
|
}
|
|
}
|