🌦️ Sistema de Clima
Guía completa del sistema meteorológico de Hytale: clima dinámico, nubes, niebla, partículas de lluvia/nieve, colores de cielo según hora del día, y pronósticos por bioma.
🎯 Características del Sistema
| Característica | Descripción |
|---|---|
| 🌍 Por Environment/Bioma | Cada bioma tiene su propio pronóstico de clima |
| ⏰ Cambio por Hora | El clima se selecciona automáticamente cada hora del día (24 horas) |
| 🎲 Probabilístico | WeatherForecast con pesos determina la probabilidad de cada clima |
| 🎨 Colores Dinámicos | Cielo, sol, luna, nubes cambian de color según la hora |
| ☁️ Nubes Animadas | Texturas de nubes con velocidad y colores variables |
| 🌧️ Partículas | Lluvia, nieve y otros efectos de partículas |
| 🌫️ Niebla | Densidad, color y falloff configurables por clima |
| 🔄 Transiciones Suaves | 10 segundos de transición entre climas (0.5s al unirse) |
🏗️ Arquitectura del Sistema
// Flujo del sistema de clima
Environment (Bioma)
└── WeatherForecast[] (por hora 0-23)
└── { WeatherId, Weight } // Probabilidades
WeatherPlugin
├── WeatherResource (estado actual)
│ ├── forcedWeatherIndex
│ └── environmentWeather (clima por bioma)
│
├── WeatherTracker (componente en jugadores)
│ └── Tracking del clima actual del jugador
│
└── WeatherSystem
├── WorldAddedSystem (init)
├── PlayerAddedSystem (añade tracker)
├── TickingSystem (actualiza cada hora)
└── InvalidateWeatherAfterTeleport
☀️ Configuración de Weather
El asset Weather.java (867 líneas) define todos los aspectos visuales del clima:
Propiedades del Cielo
class Weather {
String id; // "Clear", "Rain", "Storm"...
// Colores del cielo (cambian según hora)
TimeColorAlpha[] skyTopColors; // Color superior del cielo
TimeColorAlpha[] skyBottomColors; // Color inferior del cielo
TimeColorAlpha[] skySunsetColors; // Colores del atardecer
// Sol y Luna
TimeColor[] sunColors; // Color del sol
TimeColorAlpha[] sunGlowColors; // Resplandor del sol
TimeFloat[] sunScales; // Tamaño del sol
TimeColorAlpha[] moonColors; // Color de la luna
TimeColorAlpha[] moonGlowColors; // Resplandor de la luna
TimeFloat[] moonScales; // Tamaño de la luna
DayTexture[] moons; // Texturas de luna por día
// Iluminación
TimeColor[] sunlightColors; // Color de la luz solar
TimeFloat[] sunlightDampingMultiplier; // Atenuación
}
Nubes
class Cloud {
String texture; // Textura de las nubes
TimeColorAlpha[] colors; // Colores por hora del día
TimeFloat[] speeds; // Velocidad de movimiento por hora
}
// Múltiples capas de nubes posibles
Cloud[] clouds;
Niebla
// Configuración de niebla
TimeColor[] fogColors; // Colores por hora
TimeFloat[] fogDensities; // Densidad por hora
TimeFloat[] fogHeightFalloffs; // Caída de altura
float[] fogDistance; // [near, far] distancia
FogOptions fogOptions; // Opciones adicionales
Efectos Adicionales
// Partículas de clima (lluvia, nieve)
WeatherParticle particle;
// Efecto de pantalla (overlay)
String screenEffect;
TimeColorAlpha[] screenEffectColors;
// Filtros de color
TimeColor[] colorFilters;
// Tinte del agua
TimeColor[] waterTints;
// Estrellas
String stars;
⏰ Sistema de Colores por Hora (TimeColor)
El clima utiliza arrays de TimeColor y TimeFloat para definir valores que
cambian según la hora del día:
class TimeColor {
float hour; // Hora del día (0-24)
Color color; // Color RGB
}
class TimeFloat {
float hour; // Hora del día
float value; // Valor interpolado
}
// Ejemplo: cielo que cambia durante el día
"SkyTopColors": [
{ "Hour": 0, "Color": "#1a1a2e" }, // Medianoche - oscuro
{ "Hour": 6, "Color": "#ff7b54" }, // Amanecer - naranja
{ "Hour": 12, "Color": "#87ceeb" }, // Mediodía - azul cielo
{ "Hour": 18, "Color": "#ff6b6b" }, // Atardecer - rojo
{ "Hour": 24, "Color": "#1a1a2e" } // Vuelta a medianoche
]
El sistema interpola automáticamente los valores entre las horas definidas, creando transiciones suaves durante el ciclo día/noche.
🎲 Sistema de Pronóstico (WeatherForecast)
Cada Environment (bioma) tiene un pronóstico de clima con probabilidades:
class WeatherForecast implements IWeightedElement {
String weatherId; // "Clear", "Rain", "Storm"
double weight; // Peso/probabilidad
}
// En Environment.java
Int2ObjectMap<IWeightedMap<WeatherForecast>> weatherForecasts;
IWeightedMap<WeatherForecast> getWeatherForecast(int hour) {
// Devuelve el pronóstico para esa hora
return weatherForecasts.get(hour);
}
Ejemplo de Probabilidades
// Bosque en hora 12 (mediodía)
"WeatherForecast": {
"12": [
{ "WeatherId": "Clear", "Weight": 60.0 }, // 60%
{ "WeatherId": "Cloudy", "Weight": 25.0 }, // 25%
{ "WeatherId": "Rain", "Weight": 10.0 }, // 10%
{ "WeatherId": "Storm", "Weight": 5.0 } // 5%
]
}
🔄 WeatherSystem (Actualización)
class WeatherSystem {
// Constantes de transición
static final float JOIN_TRANSITION_SECONDS = 0.5f;
static final float WEATHERCHANGE_TRANSITION_SECONDS = 10.0f;
// TickingSystem - Se ejecuta cada tick
void tick(float dt, Store store) {
WeatherResource weatherRes = store.getResource(WEATHER_RESOURCE_TYPE);
// Si hay un clima forzado, usarlo
if (weatherRes.consumeForcedWeatherChange()) {
// Forzar actualización inmediata
return;
}
// Verificar si cambió la hora
int currentHour = worldTimeResource.getCurrentHour();
if (weatherRes.compareAndSwapHour(currentHour)) {
// Nueva hora = seleccionar nuevo clima para cada bioma
for (Environment env : Environment.getAssetMap()) {
IWeightedMap forecast = env.getWeatherForecast(currentHour);
int selectedWeather = forecast.get(random).getWeatherIndex();
environmentWeather.put(envIndex, selectedWeather);
}
}
// Actualizar jugadores cada segundo
playerUpdateDelay -= dt;
if (playerUpdateDelay <= 0) {
playerUpdateDelay = 1.0f;
updateAllPlayers();
}
}
}
💻 Comandos de Clima
| Comando | Descripción |
|---|---|
/weather |
Comando base de clima |
/weather get |
Muestra el clima actual |
/weather set <id> |
Fuerza un clima específico |
/weather reset |
Quita el clima forzado (vuelve a automático) |
// WeatherResource.setForcedWeather()
public void setForcedWeather(@Nullable String forcedWeather) {
this.previousForcedWeatherIndex = this.forcedWeatherIndex;
this.forcedWeatherIndex = forcedWeather == null
? 0 // Automático
: Weather.getAssetMap().getIndex(forcedWeather);
}
🤖 Sensores de Clima para NPCs
Los NPCs pueden reaccionar al clima usando SensorWeather:
class SensorWeather extends Sensor {
String[] weathers; // Globs a matchear: "Rain*", "Storm"
boolean evaluate(Role role, Store store) {
int weatherIndex = role.getWorldSupport().getCurrentWeatherIndex(store);
String weatherId = Weather.getAssetMap().getAsset(weatherIndex).getId();
return matchesWeather(weatherId);
}
boolean matchesWeather(String weather) {
for (String pattern : weathers) {
if (StringUtil.isGlobMatching(pattern, weather)) {
return true;
}
}
return false;
}
}
// Ejemplo de uso en NPC config:
"Sensors": {
"IsRaining": {
"Type": "Weather",
"Weathers": ["Rain*", "Storm"]
}
}
• NPCs que buscan refugio cuando llueve
• Mobs que solo aparecen durante tormentas
• Animales que cambian comportamiento según el clima
🐾 Spawning basado en Clima
El sistema de spawn puede usar el WeatherTracker para condicionar spawns:
// LocalSpawnControllerSystem usa WeatherTracker
WeatherTracker weatherTracker = store.getComponent(ref, WeatherTracker.getComponentType());
weatherTracker.updateEnvironment(transform, store);
int environmentIndex = weatherTracker.getEnvironmentId();
// Luego verifica condiciones de spawn por clima
🎯 WeatherTriggerCondition (Adventures)
El sistema de aventuras puede usar el clima como condición de trigger:
// WeatherTriggerCondition.java
// Permite crear objetivos que dependen del clima
// Ejemplo en Adventure config:
"Objectives": {
"WaitForRain": {
"TriggerCondition": {
"Type": "Weather",
"Weather": "Rain"
}
}
}
⚠️ Estabilidad y Problemas Comunes
Es posible experimentar inconsistencias en el clima debido a la arquitectura desacoplada de audio y video:
🔊 Sonido sin Lluvia (Ghost Rain)
Los sonidos ambientales se gestionan mediante AmbienceFX, no en el asset de
Weather. Si un AmbienceFX tiene la condición
WeatherIds: ["Rain"] pero el asset visual de Clima no tiene definidas
particle, el jugador escuchará la lluvia pero no la verá.
| Problema | Causa Técnica | Solución / Nota |
|---|---|---|
| 🫨 Clima Inestable | El jugador cambia de bioma rápidamente. | Cada cambio de bioma dispara una transición de 10s
(WEATHERCHANGE_TRANSITION_SECONDS). |
| 🔄 Transición Lenta | Hardcoded en WeatherTracker |
Las transiciones normales duran 10s para evitar saltos bruscos de color en el cielo. |
| 🤖 NPCs confundidos | Uso de Globs en SensorWeather |
Un NPC puede detectar "Rain*" y reaccionar incluso si visualmente la transición apenas comienza. |
Para evitar el efecto de "sonido fantasma", asegúrate de que tus assets de
Weather
y AmbienceFX estén perfectamente sincronizados en sus IDs y condiciones.
🛠️ Extensión: WeatherFixer Plugin
Para solucionar de forma automática los problemas de desincronía, hemos desarrollado el plugin WeatherFixer basado en la arquitectura de Hytale.
Funcionalidades Clave
- Auditoría ECS: Escanea todos los assets de clima al iniciar el servidor para encontrar climas con audio (AmbienceFX) pero sin partículas visuales.
- Comando
/wsync:/wsync audit: Lanza un escaneo manual de inconsistencias./wsync transition <seg>: Ajusta la velocidad de cambio climático para reducir la inestabilidad.
// Fragmento de la Auditoría en WeatherAuditSystem.java
if (suggestsRain && !hasParticles) {
LOGGER.warning("[!] Inconsistencia Detectada: Clima '" + id + "' parece ser lluvioso pero NO TIENE partículas.");
}
📁 Archivos Clave
- Weather.java 867 líneas - Asset de configuración visual
- WeatherPlugin.java 57 líneas - Plugin principal
- WeatherSystem.java 190 líneas - Sistemas de actualización
- WeatherResource.java 75 líneas - Estado del clima por mundo
- WeatherForecast.java 57 líneas - Pronóstico con pesos
- Cloud.java 69 líneas - Configuración de nubes
- SensorWeather.java Sensor de clima para NPCs
- WeatherCommand.java Comandos /weather