Skip to content

天气系统

动态天气影响视觉表现和世界模拟。与季节系统联动。

概述

天气是独立于季节的系统,但受季节影响概率。每种天气有:

  • 视觉效果:粒子、色调、光照
  • 模拟影响:NPC 行为、事件触发

天气类型

天气描述视觉效果
☀️ 晴天阳光明媚正常亮度,暖色调
⛅ 多云有云层亮度降低10%,微灰
☁️ 阴天厚云层亮度降低20%,灰色调
🌧️ 小雨细雨绵绵雨滴粒子,地面湿润反光
⛈️ 暴雨大雨+雷声密集雨滴,闪电效果,能见度低
🌨️ 小雪雪花飘落雪花粒子,地面薄雪
🌨️ 暴风雪大雪+大风密集雪花,白色遮罩,能见度极低
🌫️ 雾能见度低半透明白色遮罩
🌩️ 雷阵雨夏季特有雨滴+闪电,短暂明亮

天气状态机

ts
type Weather = 'clear' | 'cloudy' | 'overcast' | 'rain' | 'storm' | 'snow' | 'blizzard' | 'fog' | 'thunder';

interface WeatherState {
  current: Weather;
  intensity: number;       // 0-1,影响粒子密度
  duration: number;        // 剩余持续时间(游戏小时)
  transitionProgress: number; // 0-1,天气切换过渡
}

interface WeatherConfig {
  minDuration: number;  // 最小持续游戏小时
  maxDuration: number;  // 最大持续游戏小时
  particleDensity: number;  // 粒子数量/秒
  tint: number;         // 场景色调
  brightness: number;   // 亮度倍率 0-1
}

季节 × 天气概率矩阵

ts
const WEATHER_PROBABILITY: Record<Season, Record<Weather, number>> = {
  spring: {
    clear: 0.30,
    cloudy: 0.25,
    overcast: 0.15,
    rain: 0.20,
    storm: 0.05,
    fog: 0.05,
    snow: 0,
    blizzard: 0,
    thunder: 0,
  },
  summer: {
    clear: 0.40,
    cloudy: 0.15,
    overcast: 0.10,
    rain: 0.10,
    storm: 0.05,
    thunder: 0.15,
    fog: 0.05,
    snow: 0,
    blizzard: 0,
  },
  autumn: {
    clear: 0.20,
    cloudy: 0.25,
    overcast: 0.20,
    rain: 0.20,
    storm: 0.05,
    fog: 0.10,
    snow: 0,
    blizzard: 0,
    thunder: 0,
  },
  winter: {
    clear: 0.15,
    cloudy: 0.15,
    overcast: 0.15,
    rain: 0.05,
    storm: 0,
    fog: 0.10,
    snow: 0.25,
    blizzard: 0.15,
    thunder: 0,
  },
};

天气切换逻辑

ts
class WeatherSystem {
  private state: WeatherState;
  private seasonState: SeasonState;

  // 每个游戏小时检查是否切换天气
  tick(gameHour: number) {
    this.state.duration--;

    if (this.state.duration <= 0) {
      this.transition(this.pickNextWeather());
    }
  }

  private pickNextWeather(): Weather {
    const probs = WEATHER_PROBABILITY[this.seasonState.season];
    const roll = Math.random();
    let cumulative = 0;

    for (const [weather, prob] of Object.entries(probs)) {
      cumulative += prob;
      if (roll <= cumulative) return weather as Weather;
    }
    return 'clear';
  }

  private transition(next: Weather) {
    // 平滑过渡:保持当前天气的粒子逐渐消散,新天气粒子逐渐出现
    this.state.current = next;
    this.state.duration = this.randomDuration(next);
    this.state.intensity = this.getBaseIntensity(next);
    this.state.transitionProgress = 0;
  }

  private randomDuration(weather: Weather): number {
    const configs: Record<Weather, [number, number]> = {
      clear: [6, 24],
      cloudy: [4, 12],
      overcast: [3, 8],
      rain: [2, 6],
      storm: [1, 3],
      snow: [3, 10],
      blizzard: [1, 4],
      fog: [2, 6],
      thunder: [1, 2],
    };
    const [min, max] = configs[weather];
    return min + Math.floor(Math.random() * (max - min));
  }
}

粒子系统

雨滴粒子

ts
const RAIN_PARTICLE = {
  texture: 'particle_rain',  // 1×4 px 半透明白色
  speed: { min: 400, max: 600 },  // px/s
  angle: { min: 85, max: 95 },    // 接近垂直
  alpha: { min: 0.3, max: 0.6 },
  frequency: 0.5,  // 每秒发射数(×intensity)
  lifetime: 2,     // 秒
  tint: 0xAACCFF,
};

// 暴雨:frequency ×5,speed ×1.5
// 地面反光:在建筑下方绘制半透明蓝色矩形

雪花粒子

ts
const SNOW_PARTICLE = {
  texture: 'particle_snow',  // 2×2 px 白色圆点
  speed: { min: 30, max: 80 },
  angle: { min: 70, max: 110 },  // 有飘动
  alpha: { min: 0.6, max: 1.0 },
  frequency: 0.3,
  lifetime: 8,
  tint: 0xFFFFFF,
  wobble: true,  // 左右摇摆
};

// 暴风雪:frequency ×8,speed ×3,加白色遮罩

樱花粒子(春)

ts
const SAKURA_PARTICLE = {
  texture: 'particle_petal',  // 3×3 px 粉色
  speed: { min: 20, max: 50 },
  angle: { min: 60, max: 120 },  // 飘落角度大
  alpha: { min: 0.5, max: 0.8 },
  frequency: 0.1,
  lifetime: 12,
  tint: 0xFFB7C5,
  wobble: true,
  rotate: true,
};

萤火虫粒子(夏夜)

ts
const FIREFLY_PARTICLE = {
  texture: 'particle_dot',  // 2×2 px
  speed: { min: 5, max: 15 },    // 慢速漂浮
  angle: { min: 0, max: 360 },   // 全方向
  alpha: { min: 0, max: 1 },     // 呼吸闪烁
  alphaSpeed: 0.5,               // 闪烁频率
  frequency: 0.05,
  lifetime: 20,
  tint: 0xFFFF44,
  blendMode: 'ADD',              // 叠加发光
  onlyNight: true,               // 只在夜间出现
};

落叶粒子(秋)

ts
const LEAF_PARTICLE = {
  texture: 'particle_leaf',  // 4×4 px
  speed: { min: 15, max: 40 },
  angle: { min: 50, max: 130 },
  alpha: { min: 0.6, max: 0.9 },
  frequency: 0.08,
  lifetime: 15,
  tint: [0xFF6B35, 0xC9A66B, 0xFF4500, 0x8B4513],  // 随机颜色
  wobble: true,
  rotate: true,
};

天气遮罩

在场景最上层绘制半透明遮罩,改变整体氛围。

ts
function renderWeatherOverlay(ctx: Graphics, weather: Weather, intensity: number) {
  const overlays: Record<Weather, { color: number; alpha: number }> = {
    clear:     { color: 0x000000, alpha: 0 },
    cloudy:    { color: 0x333344, alpha: 0.05 },
    overcast:  { color: 0x333344, alpha: 0.12 },
    rain:      { color: 0x223344, alpha: 0.15 },
    storm:     { color: 0x112233, alpha: 0.25 },
    snow:      { color: 0xCCDDEE, alpha: 0.08 },
    blizzard:  { color: 0xEEEEFF, alpha: 0.30 },
    fog:       { color: 0xDDDDEE, alpha: 0.35 },
    thunder:   { color: 0x223344, alpha: 0.20 },
  };

  const { color, alpha } = overlays[weather];
  ctx.beginFill(color, alpha * intensity);
  ctx.drawRect(0, 0, SCREEN_W, SCREEN_H);
  ctx.endFill();
}

闪电效果(雷阵雨/暴雨)

ts
class LightningSystem {
  private flashTimer = 0;
  private flashAlpha = 0;

  tick(dt: number) {
    if (this.flashTimer > 0) {
      this.flashTimer -= dt;
      this.flashAlpha = this.flashTimer > 0.1 ? 0.8 : 0.3;
    } else if (Math.random() < 0.001) {  // 随机触发
      this.flashTimer = 0.2;
    }
  }

  render(ctx: Graphics) {
    if (this.flashAlpha > 0) {
      ctx.beginFill(0xFFFFFF, this.flashAlpha);
      ctx.drawRect(0, 0, SCREEN_W, SCREEN_H);
      ctx.endFill();
    }
  }
}

NPC 对天气的反应

天气NPC 行为变化
晴天户外活动正常
小雨倾向进室内,带伞动画
暴雨全部进室内,取消户外事件
小雪穿冬装,户外活动减少
暴风雪全部进室内,触发"暴风雪"特殊事件
移动速度降低

地面积水/积雪

ts
// 地面叠加层
function renderGroundOverlay(ctx: Graphics, weather: Weather, intensity: number) {
  if (weather === 'rain' || weather === 'storm') {
    // 积水:在低洼处绘制半透明蓝色区域
    ctx.beginFill(0x5b9bd5, 0.15 * intensity);
    ctx.drawRect(0, GROUND_Y, SCREEN_W, 20);
    ctx.endFill();
  }

  if (weather === 'snow' || weather === 'blizzard') {
    // 积雪:在地面绘制白色半透明层
    ctx.beginFill(0xFFFFFF, 0.2 * intensity);
    ctx.drawRect(0, GROUND_Y, SCREEN_W, 10);
    ctx.endFill();
  }
}

渲染层级

...(建筑/角色层之后)
11. 地面积水/积雪叠加
12. 天气粒子层(雨/雪/花瓣/萤火虫/落叶)
13. 天气遮罩层
14. 闪电层
15. UI 层

性能预算

项目最大数量
雨滴粒子200
雪花粒子150
樱花/落叶粒子30
萤火虫粒子20
遮罩层1
闪电1

📐 季节系统见 季节系统 🎨 美术规范见 Art Bible

Released under the MIT License.