天气系统
动态天气影响视觉表现和世界模拟。与季节系统联动。
概述
天气是独立于季节的系统,但受季节影响概率。每种天气有:
- 视觉效果:粒子、色调、光照
- 模拟影响: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 |