消消方块阵换皮表情
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

650 lines
21 KiB

1 week ago
/**
* ParticleEmitter2D - Cocos Creator 3.8.8 2D粒子发射器
* ParticleSystem2D
*
* :
* - //
* -
* - ///
* -
* - Sprite Frame
* -
*
* 使:
* 1. Node
* 2. spriteFrame
* 3. play() / stop()
*/
import {
_decorator,
Component,
Node,
Sprite,
SpriteFrame,
UITransform,
Color,
Vec2,
Vec3,
Quat,
Layers,
view,
size,
} from 'cc';
const { ccclass, property, menu, type, executeInEditMode } = _decorator;
// ──────────────────────────────────────────────
// 数据结构
// ──────────────────────────────────────────────
/** 发射器形状 */
enum EmitterMode {
/** 圆形发射(向随机方向) */
GRAVITY = 0,
/** 从圆形边缘向中心 / 从中心向外 */
RADIUS = 1,
}
interface Particle {
node: Node;
sprite: Sprite;
transform: UITransform;
// 位置
pos: Vec2;
startPos: Vec2;
// 速度(重力模式)
dir: Vec2;
radialAccel: number;
tangentialAccel: number;
// 速度(半径模式)
angle: number;
degreesPerSecond: number;
radius: number;
deltaRadius: number;
// 大小
size: number;
deltaSize: number;
// 旋转
rotation: number;
deltaRotation: number;
// 颜色
color: Color;
deltaColor: Color;
// 生命周期
timeToLive: number;
totalTime: number;
}
// ──────────────────────────────────────────────
// 组件定义
// ──────────────────────────────────────────────
@ccclass('ParticleEmitter2D')
@menu('Custom/ParticleEmitter2D')
@executeInEditMode
export class ParticleEmitter2D extends Component {
// ── 基础设置 ────────────────────────────────
@property({ type: SpriteFrame, tooltip: '粒子纹理 SpriteFrame' })
spriteFrame: SpriteFrame = null;
@property({ tooltip: '最大粒子数量', min: 1, max: 10000 })
maxParticles: number = 150;
@property({ tooltip: '自动播放' })
autoPlay: boolean = true;
@property({ tooltip: '循环播放' })
loop: boolean = true;
@property({ tooltip: '是否激活', visible: true })
active: boolean = true;
// ── 发射器设置 ──────────────────────────────
@property({ tooltip: '发射器模式: 0=重力 1=半径' })
emitterMode: EmitterMode = EmitterMode.GRAVITY;
@property({ tooltip: '粒子持续时间(秒, -1=永久)' })
duration: number = -1;
@property({ tooltip: '每秒发射粒子数', min: 0 })
emissionRate: number = 10;
// ── 粒子属性(通用) ────────────────────────
@property({ tooltip: '粒子生命周期(秒)' })
particleLifespan: number = 2;
@property({ tooltip: '粒子生命周期随机浮动' })
particleLifespanVariance: number = 0.5;
@property({ tooltip: '起始大小' })
startSize: number = 32;
@property({ tooltip: '起始大小浮动' })
startSizeVariance: number = 5;
@property({ tooltip: '结束大小(-1=同起始)' })
endSize: number = -1;
@property({ tooltip: '结束大小浮动' })
endSizeVariance: number = 0;
@property({ tooltip: '起始旋转(度)' })
startSpin: number = 0;
@property({ tooltip: '起始旋转浮动' })
startSpinVariance: number = 0;
@property({ tooltip: '结束旋转(度)' })
endSpin: number = 0;
@property({ tooltip: '结束旋转浮动' })
endSpinVariance: number = 0;
// ── 颜色 ────────────────────────────────────
@property({ tooltip: '起始颜色' })
startColor: Color = new Color(255, 255, 255, 255);
@property({ tooltip: '起始颜色浮动' })
startColorVariance: Color = new Color(0, 0, 0, 0);
@property({ tooltip: '结束颜色' })
endColor: Color = new Color(255, 255, 255, 0);
@property({ tooltip: '结束颜色浮动' })
endColorVariance: Color = new Color(0, 0, 0, 0);
// ── 重力模式参数 ────────────────────────────
@property({ tooltip: '重力 X' })
gravityX: number = 0;
@property({ tooltip: '重力 Y' })
gravityY: number = -200;
@property({ tooltip: '粒子速度' })
speed: number = 150;
@property({ tooltip: '速度浮动' })
speedVariance: number = 40;
@property({ tooltip: '发射角度(度)' })
angle: number = 90;
@property({ tooltip: '角度浮动(度)' })
angleVariance: number = 20;
@property({ tooltip: '径向加速度' })
radialAcceleration: number = 0;
@property({ tooltip: '径向加速度浮动' })
radialAccelVariance: number = 0;
@property({ tooltip: '切向加速度' })
tangentialAcceleration: number = 0;
@property({ tooltip: '切向加速度浮动' })
tangentialAccelVariance: number = 0;
// ── 半径模式参数 ────────────────────────────
@property({ tooltip: '起始半径' })
startRadius: number = 100;
@property({ tooltip: '起始半径浮动' })
startRadiusVariance: number = 0;
@property({ tooltip: '结束半径' })
endRadius: number = 50;
@property({ tooltip: '结束半径浮动' })
endRadiusVariance: number = 0;
@property({ tooltip: '每秒旋转角度' })
rotatePerSecond: number = 180;
@property({ tooltip: '每秒旋转角度浮动' })
rotatePerSecondVariance: number = 0;
// ── 混合模式 ────────────────────────────────
@property({ tooltip: 'Blend Src Factor (1=ONE, 770=SRC_ALPHA)' })
blendSrc: number = 1;
@property({ tooltip: 'Blend Dst Factor (1=ONE, 771=ONE_MINUS_SRC_ALPHA)' })
blendDst: number = 771;
// ── 位置相关 ────────────────────────────────
@property({ tooltip: '发射器相对于节点的X偏移' })
sourcePositonX: number = 0;
@property({ tooltip: '发射器相对于节点的Y偏移' })
sourcePositonY: number = 0;
@property({ tooltip: '发射器位置浮动X' })
sourcePositionVarX: number = 0;
@property({ tooltip: '发射器位置浮动Y' })
sourcePositionVarY: number = 0;
// ── 内部状态 ────────────────────────────────
private _particles: Particle[] = [];
private _pool: Particle[] = [];
private _emitCounter: number = 0;
private _elapsed: number = 0;
private _isActive: boolean = false;
private _particleIndex: number = 0;
// ── 生命周期 ────────────────────────────────
onLoad() {
// 预创建粒子池
for (let i = 0; i < this.maxParticles; i++) {
this._pool.push(this._createParticleNode());
}
}
onEnable() {
if (this.autoPlay) {
this.play();
}
}
onDisable() {
this.stop();
}
onDestroy() {
// 清理所有节点
for (const p of this._pool) {
p.node.destroy();
}
for (const p of this._particles) {
p.node.destroy();
}
this._pool.length = 0;
this._particles.length = 0;
}
update(dt: number) {
if (!this._isActive) return;
// 发射新粒子
this._updateEmission(dt);
// 更新现有粒子
this._updateParticles(dt);
// 清理过期粒子
this._removeDeadParticles();
}
// ── 公共 API ────────────────────────────────
/** 开始播放 */
play() {
this._isActive = true;
this._elapsed = 0;
this._emitCounter = 0;
this.active = true;
}
/** 停止播放 */
stop() {
this._isActive = false;
this.active = false;
}
/** 停止发射,让现有粒子自然消亡 */
stopEmitting() {
this._isActive = false;
}
/** 重置并重新播放 */
resetSystem() {
this.removeAllParticles();
this.play();
}
/** 移除所有粒子 */
removeAllParticles() {
for (const p of this._particles) {
p.node.active = false;
this._pool.push(p);
}
this._particles.length = 0;
}
/** 获取当前活跃粒子数 */
get particleCount(): number {
return this._particles.length;
}
/** 是否正在播放 */
get isPlaying(): boolean {
return this._isActive;
}
// ── 粒子节点创建 ────────────────────────────
private _createParticleNode(): Particle {
const node = new Node('_particle');
node.layer = Layers.Enum.UI_2D;
node.parent = this.node;
const sprite = node.addComponent(Sprite);
const transform = node.getComponent(UITransform) || node.addComponent(UITransform);
if (this.spriteFrame) {
sprite.spriteFrame = this.spriteFrame;
}
// 设置 blend 模式
sprite.srcBlendFactor = this.blendSrc as any;
sprite.dstBlendFactor = this.blendDst as any;
transform.setAnchorPoint(0.5, 0.5);
node.active = false;
return {
node,
sprite,
transform,
pos: new Vec2(),
startPos: new Vec2(),
dir: new Vec2(),
radialAccel: 0,
tangentialAccel: 0,
angle: 0,
degreesPerSecond: 0,
radius: 0,
deltaRadius: 0,
size: 1,
deltaSize: 0,
rotation: 0,
deltaRotation: 0,
color: new Color(),
deltaColor: new Color(),
timeToLive: 0,
totalTime: 0,
};
}
// ── 发射逻辑 ────────────────────────────────
private _updateEmission(dt: number) {
this._elapsed += dt;
// 检查持续时间
if (this.duration >= 0 && this._elapsed > this.duration) {
if (this.loop) {
this.resetSystem();
return;
} else if (this._particles.length === 0) {
this.stop();
return;
} else {
this._isActive = false;
return;
}
}
if (this.emissionRate <= 0) return;
const rate = 1.0 / this.emissionRate;
this._emitCounter += dt;
while (this._emitCounter >= rate && this._particles.length < this.maxParticles) {
this._emitParticle();
this._emitCounter -= rate;
}
}
private _emitParticle() {
const p = this._acquireParticle();
if (!p) return;
// 生命周期
p.totalTime = this._random(this.particleLifespan, this.particleLifespanVariance);
p.timeToLive = Math.max(0.01, p.totalTime);
// 发射器位置 + 浮动
const sourceX = this._random(this.sourcePositonX, this.sourcePositionVarX);
const sourceY = this._random(this.sourcePositonY, this.sourcePositionVarY);
// 获取节点世界位置
const worldPos = this.node.getWorldPosition();
p.pos.set(worldPos.x + sourceX, worldPos.y + sourceY);
p.startPos.set(p.pos.x, p.pos.y);
// 起始大小
p.size = Math.max(0, this._random(this.startSize, this.startSizeVariance));
const endSizeVal = this.endSize < 0 ? this.startSize : this.endSize;
p.deltaSize = (endSizeVal - p.size) / p.totalTime;
// 起始旋转
p.rotation = this._random(this.startSpin, this.startSpinVariance);
const endRotation = this._random(this.endSpin, this.endSpinVariance);
p.deltaRotation = (endRotation - p.rotation) / p.totalTime;
// 颜色
p.color.set(
this._clampColor(this._random(this.startColor.r, this.startColorVariance.r)),
this._clampColor(this._random(this.startColor.g, this.startColorVariance.g)),
this._clampColor(this._random(this.startColor.b, this.startColorVariance.b)),
this._clampColor(this._random(this.startColor.a, this.startColorVariance.a)),
);
p.deltaColor.set(
(this._clampColor(this._random(this.endColor.r, this.endColorVariance.r)) - p.color.r) / p.totalTime,
(this._clampColor(this._random(this.endColor.g, this.endColorVariance.g)) - p.color.g) / p.totalTime,
(this._clampColor(this._random(this.endColor.b, this.endColorVariance.b)) - p.color.b) / p.totalTime,
(this._clampColor(this._random(this.endColor.a, this.endColorVariance.a)) - p.color.a) / p.totalTime,
);
if (this.emitterMode === EmitterMode.GRAVITY) {
// 重力模式
const a = this._degToRad(this._random(this.angle, this.angleVariance));
const s = this._random(this.speed, this.speedVariance);
p.dir.set(Math.cos(a) * s, Math.sin(a) * s);
p.radialAccel = this._random(this.radialAcceleration, this.radialAccelVariance);
p.tangentialAccel = this._random(this.tangentialAcceleration, this.tangentialAccelVariance);
} else {
// 半径模式
p.angle = this._degToRad(this._random(this.angle, this.angleVariance));
p.degreesPerSecond = this._degToRad(this._random(this.rotatePerSecond, this.rotatePerSecondVariance));
p.radius = this._random(this.startRadius, this.startRadiusVariance);
const endR = this._random(this.endRadius, this.endRadiusVariance);
p.deltaRadius = (endR - p.radius) / p.totalTime;
}
// 应用初始视觉状态
this._applyVisuals(p);
p.node.active = true;
}
// ── 更新逻辑 ────────────────────────────────
private _updateParticles(dt: number) {
const gravity = new Vec2(this.gravityX, this.gravityY);
for (let i = 0, len = this._particles.length; i < len; i++) {
const p = this._particles[i];
// 生命周期衰减
p.timeToLive -= dt;
if (p.timeToLive <= 0) continue;
const t = 1 - (p.timeToLive / p.totalTime); // 0 → 1
if (this.emitterMode === EmitterMode.GRAVITY) {
// 重力模式更新
// 径向力(沿粒子→原点方向)
let radialX = 0, radialY = 0;
if (p.radialAccel !== 0) {
const dx = p.pos.x - p.startPos.x;
const dy = p.pos.y - p.startPos.y;
const len2 = Math.sqrt(dx * dx + dy * dy) || 1;
radialX = (dx / len2) * p.radialAccel;
radialY = (dy / len2) * p.radialAccel;
}
// 切向力(垂直于径向)
let tangentialX = 0, tangentialY = 0;
if (p.tangentialAccel !== 0) {
const dx = p.pos.x - p.startPos.x;
const dy = p.pos.y - p.startPos.y;
const len2 = Math.sqrt(dx * dx + dy * dy) || 1;
tangentialX = (-dy / len2) * p.tangentialAccel;
tangentialY = (dx / len2) * p.tangentialAccel;
}
// 合力
const ax = gravity.x + radialX + tangentialX;
const ay = gravity.y + radialY + tangentialY;
p.dir.x += ax * dt;
p.dir.y += ay * dt;
p.pos.x += p.dir.x * dt;
p.pos.y += p.dir.y * dt;
} else {
// 半径模式更新
p.angle += p.degreesPerSecond * dt;
p.radius += p.deltaRadius * dt;
p.pos.x = p.startPos.x + Math.cos(p.angle) * p.radius;
p.pos.y = p.startPos.y + Math.sin(p.angle) * p.radius;
}
// 更新大小
p.size = Math.max(0, p.size + p.deltaSize * dt);
// 更新旋转
p.rotation += p.deltaRotation * dt;
// 更新颜色
p.color.r = this._clampColor(p.color.r + p.deltaColor.r * dt);
p.color.g = this._clampColor(p.color.g + p.deltaColor.g * dt);
p.color.b = this._clampColor(p.color.b + p.deltaColor.b * dt);
p.color.a = this._clampColor(p.color.a + p.deltaColor.a * dt);
// 应用视觉
this._applyVisuals(p);
}
}
private _applyVisuals(p: Particle) {
// 位置(从世界坐标转为本地坐标)
const localPos = this.node.getComponent(UITransform)!
.convertToNodeSpaceAR(new Vec3(p.pos.x, p.pos.y, 0));
p.node.setPosition(localPos.x, localPos.y, 0);
// 大小
p.transform.setContentSize(p.size, p.size);
// 旋转
p.node.setRotationFromEuler(0, 0, p.rotation);
// 颜色
p.sprite.color = p.color.clone();
}
// ── 粒子池 ──────────────────────────────────
private _acquireParticle(): Particle | null {
let p: Particle | undefined;
// 优先从池中取
if (this._pool.length > 0) {
p = this._pool.pop()!;
} else if (this._particles.length < this.maxParticles) {
p = this._createParticleNode();
} else {
// 回收最老的粒子
const oldest = this._particles.shift();
if (oldest) {
oldest.node.active = false;
p = oldest;
}
}
if (p) {
this._particles.push(p);
}
return p || null;
}
private _removeDeadParticles() {
for (let i = this._particles.length - 1; i >= 0; i--) {
if (this._particles[i].timeToLive <= 0) {
const p = this._particles[i];
p.node.active = false;
this._particles.splice(i, 1);
this._pool.push(p);
}
}
}
// ── 工具函数 ────────────────────────────────
private _random(base: number, variance: number): number {
return base + variance * (Math.random() * 2 - 1);
}
private _clampColor(v: number): number {
return Math.max(0, Math.min(255, Math.round(v)));
}
private _degToRad(deg: number): number {
return deg * Math.PI / 180;
}
}
/**
*
* 使:
*
* 1. Node Node
* 2. spriteFrame 64x64 PNG
* 3. Inspector :
* - maxParticles: 最大粒子数
* - emissionRate: 每秒发射数量
* - particleLifespan: 粒子存活时间
* - startSize / endSize: 大小变化
* - startColor / endColor: 颜色变化
* - gravityX / gravityY: 重力
* - speed / angle: 发射速度和角度
*
* 4. :
* const emitter = node.getComponent(ParticleEmitter2D);
* emitter.play();
* emitter.stop();
* emitter.stopEmitting(); // 停止发射,现有粒子自然消亡
* emitter.resetSystem(); // 重置系统
*
* 5. :
* - 烟雾: gravityY=30, startSize=20, endSize=80, speed=20, startColor=(200,200,200,180), endColor=(200,200,200,0)
* - 火焰: gravityY=100, startSize=15, endSize=5, speed=80, startColor=(255,200,50,255), endColor=(255,50,0,0)
* - 星星: emitterMode=RADIUS, startRadius=50, endRadius=50, rotatePerSecond=90, startColor=(255,255,200,255)
* - 雪花: gravityY=-50, gravityX=20, speed=10, angle=270, startSize=8, endSize=8, angleVariance=30
* - 爆炸: speed=300, speedVariance=200, angleVariance=360, particleLifespan=0.8, startSize=20, endSize=2
*
*/