作为一名游戏开发者,我经常需要在项目中实现各种天气效果。最近使用LayaAir引擎开发了一个2D下雨粒子系统,经过多次迭代优化后效果相当不错。这个系统不仅支持基础的雨滴下落效果,还实现了地面溅射、风力影响等细节,最关键的是性能表现非常出色,在移动设备上也能流畅运行。
整个系统采用模块化设计,主要分为三个核心组件:
这种分层设计使得每个模块职责单一,便于维护和扩展。比如要修改雨滴效果时,只需要改动RainSystem,不会影响到其他部分。
code复制RainEnvironmentScript (主脚本)
│
├── RainSystem (雨滴系统)
│ └── RainDrop[] (雨滴粒子数组)
│
└── SplashSystem (溅射系统)
└── SplashParticle[] (溅射粒子数组)
每个雨滴粒子都是一个独立的RainDrop对象,包含以下关键属性:
typescript复制class RainDrop {
sprite: Laya.Sprite; // 精灵对象
texture: Laya.Texture; // 纹理(可选)
vx: number; // X方向速度(风力)
vy: number; // Y方向速度(下落)
length: number; // 雨滴长度
scale: number; // 缩放比例
inUse: boolean; // 使用状态
}
系统支持两种雨滴渲染方式,开发者可以根据项目需求选择:
纹理模式:使用PNG图片作为雨滴纹理
绘制模式:直接绘制线条作为雨滴
实现代码:
typescript复制// 有纹理时:使用 drawTexture 绘制 PNG 图片
if (this.texture) {
this.sprite.graphics.drawTexture(
this.texture,
0, 0,
this.texture.width,
this.texture.height
);
this.sprite.pivotX = this.texture.width / 2;
this.sprite.pivotY = this.texture.height / 2;
}
// 无纹理时:使用 drawLine 绘制默认线条
else {
this.sprite.graphics.drawLine(0, 0, 0, this.length, "#aaddff", 2);
}
RainSystem负责管理所有雨滴粒子的生命周期。每帧会根据当前雨量参数(intensity)生成新雨滴:
typescript复制update() {
const texture = this.texture;
// 产生新雨滴
for (let i = 0; i < this.intensity; i++) {
const x = Math.random() * (this.stageWidth + 100) - 50;
const y = -20;
const drop = RainDrop.acquire(x, y, this.config, texture);
this.drops.push(drop);
this.container.addChild(drop.sprite);
}
// 更新现有雨滴...
}
这里有几个关键点:
每个雨滴在下落过程中会持续更新位置,当超出屏幕底部时会被回收:
typescript复制update(stageHeight: number): boolean {
this.sprite.x += this.vx;
this.sprite.y += this.vy;
// 检查是否超出屏幕底部
if (this.sprite.y > stageHeight + this.length) {
this.recycle();
return false;
}
return true;
}
使用反向循环安全删除已回收的雨滴:
typescript复制for (let i = this.drops.length - 1; i >= 0; i--) {
const alive = this.drops[i].update(this.stageHeight);
if (!alive) {
this.drops.splice(i, 1);
}
}
当雨滴撞击地面时,SplashSystem会生成溅射粒子模拟水花效果:
typescript复制class SplashParticle {
init(x: number, y: number): void {
// 绘制小水珠
this.sprite.graphics.drawCircle(0, 0, 2, "#cceeff");
this.sprite.pos(x, y);
this.sprite.alpha = 1;
// 向上溅射
const angle = -Math.PI / 2 + (Math.random() - 0.5);
const speed = 1 + Math.random() * 2;
this.vx = Math.cos(angle) * speed;
this.vy = Math.sin(angle) * speed;
this.life = 10 + Math.random() * 10;
}
}
溅射粒子会模拟简单的物理效果:
typescript复制update(): boolean {
this.life--;
if (this.life <= 0) {
this.recycle();
return false;
}
this.sprite.x += this.vx;
this.sprite.y += this.vy;
this.vy += 0.2; // 重力
this.sprite.alpha = this.life / 20; // 淡出
return true;
}
系统大量使用LayaAir内置的对象池来复用粒子对象:
typescript复制// 获取雨滴
static acquire(x: number, y: number, config: RainDropConfig, texture: Laya.Texture | null): RainDrop {
let drop = Laya.Pool.getItemByClass("RainDrop", RainDrop);
if (!drop) {
drop = new RainDrop();
}
drop.init(x, y, config, texture);
return drop;
}
// 回收雨滴
recycle(): void {
this.inUse = false;
this.sprite.visible = false;
this.sprite.graphics.clear();
this.texture = null;
if (this.sprite.parent) {
this.sprite.parent.removeChild(this.sprite);
}
Laya.Pool.recover("RainDrop", this);
}
使用对象池可以带来以下好处:
在更新大量粒子时,使用反向循环遍历可以安全删除元素:
typescript复制for (let i = this.drops.length - 1; i >= 0; i--) {
const alive = this.drops[i].update(this.stageHeight);
if (!alive) {
this.drops.splice(i, 1);
}
}
系统提供了丰富的参数供开发者调整下雨效果:
| 参数 | 类型 | 说明 | 默认值 |
|---|---|---|---|
rainTexture |
Texture | 雨滴纹理图片 | null |
rainIntensity |
Number | 雨量大小 (0-20) | 4 |
minSpeed |
Number | 最小下落速度 | 8 |
maxSpeed |
Number | 最大下落速度 | 14 |
wind |
Number | 风力(负数向左,正数向右) | -1 |
minLength |
Number | 最小雨滴长度 | 10 |
maxLength |
Number | 最大雨滴长度 | 25 |
enableSplash |
Boolean | 是否启用地面溅射 | true |
splashRate |
Number | 溅射频率 (0-1) | 0.3 |
还提供了常见雨型的预设参数参考:
| 效果 | 雨量 | 速度范围 |
|---|---|---|
| 毛毛雨 | 1-3 | 3-6 |
| 小雨 | 3-6 | 6-10 |
| 中雨 | 6-10 | 8-14 |
| 大雨 | 10-15 | 12-20 |
| 暴雨 | 15-20 | 18-30 |
RainEnvironmentScript组件typescript复制import { RainEnvironmentScript } from "./environment/RainSystem";
// 添加下雨脚本
const rainScript = sprite.addComponent(RainEnvironmentScript);
// 动态调整参数
rainScript.setRainIntensity(10); // 设置雨量
rainScript.setRainSpeed(15, 25); // 设置速度
rainScript.setWind(-3); // 设置风力
typescript复制// 在游戏场景中添加下雨效果
const rainScript = scene2D.addComponent(RainEnvironmentScript);
rainScript.setRainIntensity(12);
rainScript.setRainSpeed(12, 20);
rainScript.setWind(-2);
typescript复制class WeatherSystem extends Laya.Script {
private rain!: RainEnvironmentScript;
async onAwake() {
this.rain = this.owner.addComponent(RainEnvironmentScript);
// 模拟天气变化:从小雨到大雨
Laya.timer.loop(5000, this, this.changeWeather);
}
changeWeather() {
const intensity = Math.random() * 15;
this.rain.setRainIntensity(intensity);
this.rain.setWind((Math.random() - 0.5) * 4);
}
}
typescript复制private flash(): void {
const originalColor = Laya.stage.bgColor;
Laya.stage.bgColor = "#555577";
Laya.timer.once(50, this, () => {
Laya.stage.bgColor = originalColor;
});
}
// 在 onUpdate 中随机触发(大雨时)
if (Math.random() < 0.005 && this.rainIntensity > 10) {
this.flash();
}
typescript复制private updateWind(): void {
// 缓慢变化风向
const time = Laya.timer.currTimer * 0.0001;
const wind = Math.sin(time) * 2;
this.rainSystem.setWind(wind);
}
typescript复制async onAwake() {
// 深蓝色夜空
Laya.stage.bgColor = "#0a1520";
// 添加水坑反射效果
const puddle = new Laya.Sprite();
puddle.graphics.drawCircle(0, 0, 30, "rgba(100, 150, 200, 0.3)");
puddle.pos(Laya.stage.width / 2, Laya.stage.height - 50);
this.owner.addChild(puddle);
}
在实际开发过程中,我总结了以下几点经验:
性能优先:移动设备上粒子效果很容易成为性能瓶颈,一定要使用对象池和合理的粒子数量控制。
参数可调:提供丰富的参数配置,让美术同学可以自由调整效果,减少程序反复修改的工作量。
模块化设计:将雨滴、溅射等不同效果分离到不同系统,便于单独优化和扩展。
渐进增强:先实现基础功能,再逐步添加高级特性,避免一开始就陷入细节。
多设备测试:在低端安卓设备上测试效果,确保性能达标。
这个下雨系统目前已经在多个项目中实际应用,表现稳定。特别是在一些需要动态天气变化的RPG游戏中,通过简单的参数调整就能实现从毛毛雨到暴雨的各种效果,大大提升了开发效率。