返回 Skill 列表
extension
分类: 开发与工程无需 API Key

ta-phaser-particle-design

设计粒子系统以实现视觉效果。在创建粒子效果、发射器或视觉优化时积极使用。

person作者: jakexiaohubgithub

Phaser Particle Design

"Create memorable visual moments with thoughtfully designed particle effects."

When to Use This Skill

Use when:

  • Designing signature visual effects
  • Creating atmosphere and mood
  • Building feedback systems
  • Adding visual polish
  • Implementing weather and ambient effects

Design Principles

  1. Purposeful - Every particle effect should communicate something
  2. Performant - Limit particle count for target platform
  3. Readable - Effects shouldn't obscure gameplay
  4. Satisfying - Good feel through tuning and iteration

Effect Types

| Category | Examples | Key Properties | | -------- | ----------------- | -------------------------- | | Impact | Hit, explosion | Speed variance, size decay | | Ambient | Dust, fog, rain | Slow, subtle, continuous | | Magic | Spells, aura | Additive blend, glow | | UI | Confetti, shine | Quick, responsive | | Weather | Rain, snow, storm | Direction, density |

Progressive Guide

Level 1: Impact Effects

// Hit spark effect
const createHitSpark = (
  scene: Phaser.Scene,
  x: number,
  y: number,
  color = 0xffff00,
) => {
  const particles = scene.add.particles(x, y, "spark", {
    speed: { min: 50, max: 200 },
    angle: { min: 0, max: 360 },
    scale: { start: 0.8, end: 0 },
    alpha: { start: 1, end: 0 },
    lifespan: 300,
    quantity: 8,
    blendMode: "ADD",
    emitting: false,
    onEmit: (particle: any) => {
      particle.tint = color;
    },
  });

  particles.explode(8, x, y);

  // Auto-cleanup
  scene.time.delayedCall(500, () => particles.destroy());
};

// Blood/spark effect on damage
scene.events.on("enemy-hit", (enemy) => {
  createHitSpark(scene, enemy.x, enemy.y, 0xff0000);
});

Level 2: Ambient Effects

// Atmospheric dust motes
const createDustMotes = (scene: Phaser.Scene) => {
  const dust = scene.add.particles(0, 0, "dust", {
    x: { min: 0, max: scene.scale.width },
    y: { min: 0, max: scene.scale.height },
    speed: { min: 2, max: 10 },
    angle: { min: 200, max: 340 },
    scale: { start: 0.1, end: 0.3 },
    alpha: { start: 0, end: 0.3, ease: "Quad.easeOut" },
    lifespan: { min: 3000, max: 6000 },
    frequency: 200,
    blendMode: "ADD",
  });

  dust.setDepth(-1); // Behind everything
  return dust;
};

// Fire flicker effect
const createFireGlow = (scene: Phaser.Scene, x: number, y: number) => {
  const glow = scene.add.particles(x, y, "glow", {
    speed: 20,
    scale: { start: 0.5, end: 1.5, ease: "Sine.easeInOut" },
    alpha: { start: 0.4, end: 0, ease: "Linear" },
    lifespan: 500,
    frequency: 100,
    blendMode: "ADD",
    emitZone: {
      type: "random",
      source: new Phaser.Geom.Circle(0, 0, 10),
    },
  });

  return glow;
};

Level 3: Magic and Ability Effects

// Magic circle cast effect
const createMagicCircle = (
  scene: Phaser.Scene,
  x: number,
  y: number,
  color = 0x8800ff,
) => {
  const circle = scene.add.particles(x, y, "spark", {
    speed: 100,
    angle: { min: 0, max: 360 },
    scale: { start: 0.2, end: 0.8, ease: "Back.easeOut" },
    alpha: { start: 1, end: 0 },
    lifespan: 600,
    quantity: 30,
    blendMode: "ADD",
    emitting: false,
    onEmit: (particle: any) => {
      particle.tint = color;
      // Spiral motion
      const angle = particle.angle;
      particle.velocityX = Math.cos(angle) * 100;
      particle.velocityY = Math.sin(angle) * 100;
    },
  });

  circle.explode(30, x, y);

  // Lingering aura
  const aura = scene.add.particles(x, y, "glow", {
    speed: 30,
    scale: { start: 0.5, end: 1.5 },
    alpha: { start: 0.3, end: 0 },
    lifespan: 800,
    frequency: 50,
    blendMode: "ADD",
    emitZone: {
      type: "edge",
      source: new Phaser.Geom.Circle(0, 0, 40),
      quantity: 20,
    },
    onEmit: (particle: any) => {
      particle.tint = color;
    },
  });

  scene.time.delayedCall(2000, () => aura.destroy());

  return { circle, aura };
};

// Projectile trail
const createTrailEmitter = (
  scene: Phaser.Scene,
  projectile: Phaser.GameObjects.Image,
) => {
  const trail = scene.add.particles(0, 0, "trail", {
    speed: 0,
    scale: { start: 0.5, end: 0 },
    alpha: { start: 0.5, end: 0 },
    lifespan: 200,
    frequency: 30,
    blendMode: "ADD",
  });

  trail.startFollow(projectile);

  return trail;
};

Level 4: Weather Systems

class WeatherSystem {
  private rain?: Phaser.GameObjects.Particles.ParticleEmitter;
  private lightning?: Phaser.Time.TimerEvent;
  private clouds?: Phaser.GameObjects.TileSprite;

  constructor(private scene: Phaser.Scene) {}

  setRain(intensity: "light" | "medium" | "heavy" = "medium") {
    this.clearWeather();

    const config = {
      light: { frequency: 100, speedY: 300 },
      medium: { frequency: 50, speedY: 400 },
      heavy: { frequency: 20, speedY: 500 },
    };

    const cfg = config[intensity];

    this.rain = this.scene.add.particles(0, 0, "rain", {
      x: { min: 0, max: this.scene.scale.width },
      y: -20,
      speedY: cfg.speedY,
      speedX: -20,
      scale: { start: 0.3, end: 0.3 },
      alpha: { start: 0.3, end: 0.6 },
      lifespan: 2000,
      frequency: cfg.frequency,
      emitZone: {
        type: "random",
        source: new Phaser.Geom.Rectangle(0, 0, this.scene.scale.width, 50),
      },
    });

    // Darken sky
    this.scene.cameras.main.setTint(0x8888aa);
  }

  setStorm() {
    this.setRain("heavy");

    // Lightning flashes
    this.lightning = this.scene.time.addEvent({
      delay: Phaser.Math.Between(2000, 5000),
      callback: this.flashLightning,
      callbackScope: this,
      loop: true,
    });

    // Storm clouds
    this.clouds = this.scene.add.tileSprite(
      this.scene.scale.width / 2,
      this.scene.scale.height / 2,
      this.scene.scale.width,
      this.scene.scale.height,
      "clouds",
    );
    this.clouds.setAlpha(0.3);
    this.clouds.setScrollFactor(0);
  }

  private flashLightning() {
    // Flash screen
    this.scene.cameras.main.flash(100, 255, 255, 255);

    // Play thunder sound delay based on "distance"
    const delay = Phaser.Math.Between(500, 2000);
    this.scene.time.delayedCall(delay, () => {
      // this.sound.play('thunder');
    });
  }

  setSnow(intensity: "light" | "medium" | "heavy" = "medium") {
    this.clearWeather();

    const config = {
      light: { frequency: 200, speedY: 50 },
      medium: { frequency: 100, speedY: 80 },
      heavy: { frequency: 50, speedY: 120 },
    };

    const cfg = config[intensity];

    this.rain = this.scene.add.particles(0, 0, "snow", {
      x: { min: 0, max: this.scene.scale.width },
      y: -10,
      speedY: cfg.speedY,
      speedX: { min: -20, max: 20 },
      scale: { start: 0.2, end: 0.3 },
      alpha: { start: 0.6, end: 0.8 },
      lifespan: 4000,
      frequency: cfg.frequency,
      emitZone: {
        type: "random",
        source: new Phaser.Geom.Rectangle(0, 0, this.scene.scale.width, 100),
      },
    });

    // Slight blue tint
    this.scene.cameras.main.setTint(0xddddff);
  }

  clearWeather() {
    if (this.rain) {
      this.rain.destroy();
      this.rain = undefined;
    }
    if (this.lightning) {
      this.lightning.destroy();
      this.lightning = undefined;
    }
    if (this.clouds) {
      this.clouds.destroy();
      this.clouds = undefined;
    }
    this.scene.cameras.main.clearTint();
  }
}

Level 5: Particle Effect Library

class ParticleLibrary {
  private effects: Map<
    string,
    (
      scene: Phaser.Scene,
      x: number,
      y: number,
      options?: any,
    ) => Phaser.GameObjects.Particles.ParticleEmitter
  >;

  constructor() {
    this.registerDefaults();
  }

  private registerDefaults() {
    // Explosion
    this.register("explosion", (scene, x, y, options = {}) => {
      const emitter = scene.add.particles(x, y, "explosion", {
        speed: { min: 100, max: 300 * (options.intensity || 1) },
        angle: { min: 0, max: 360 },
        scale: { start: 0.2, end: 1 },
        alpha: { start: 1, end: 0 },
        lifespan: 600,
        quantity: 20 * (options.quantity || 1),
        blendMode: "ADD",
        emitting: false,
      });

      emitter.explode(emitter.config.quantity, x, y);
      return emitter;
    });

    // Dust cloud
    this.register("dust", (scene, x, y, options = {}) => {
      return scene.add.particles(x, y, "dust", {
        speed: { min: 20, max: 60 },
        angle: { min: 0, max: 360 },
        scale: { start: 0.1, end: 0.5 },
        alpha: { start: 0.5, end: 0 },
        lifespan: 800,
        quantity: 5,
        blendMode: "NORMAL",
      });
    });

    // Healing aura
    this.register("heal", (scene, x, y, options = {}) => {
      return scene.add.particles(x, y, "sparkle", {
        speed: 30,
        scale: { start: 0, end: 0.3, ease: "Back.easeOut" },
        alpha: { start: 1, end: 0 },
        lifespan: 1000,
        frequency: 50,
        blendMode: "ADD",
        emitZone: {
          type: "edge",
          source: new Phaser.Geom.Circle(0, 0, 30),
          quantity: 10,
        },
        onEmit: (particle: any) => {
          particle.tint = 0x00ff88;
        },
      });
    });

    // Level up
    this.register("levelup", (scene, x, y) => {
      const burst = scene.add.particles(x, y, "star", {
        speed: { min: 100, max: 300 },
        angle: { min: 0, max: 360 },
        scale: { start: 0.2, end: 0.8 },
        alpha: { start: 1, end: 0 },
        lifespan: 1500,
        quantity: 50,
        blendMode: "ADD",
        emitting: false,
        onEmit: (particle: any) => {
          const colors = [0xffff00, 0xff8800, 0xffffff, 0x88ffff];
          particle.tint = Phaser.Utils.Array.GetRandom(colors);
        },
      });

      burst.explode(50, x, y);

      // Rising column of light
      const light = scene.add.rectangle(x, y, 40, 200, 0xffff00);
      light.setAlpha(0.3);
      light.setBlendMode(Phaser.BlendModes.ADD);

      scene.tweens.add({
        targets: light,
        scaleY: 3,
        alpha: 0,
        duration: 1000,
        ease: "Cubic.easeOut",
        onComplete: () => light.destroy(),
      });

      return burst;
    });
  }

  register(name: string, effect: Function) {
    this.effects.set(name, effect);
  }

  play(name: string, scene: Phaser.Scene, x: number, y: number, options?: any) {
    const effect = this.effects.get(name);
    if (effect) {
      return effect(scene, x, y, options);
    }
    return null;
  }
}

// Usage
const particles = new ParticleLibrary();
particles.play("explosion", this, this.enemy.x, this.enemy.y, {
  intensity: 1.5,
  quantity: 2,
});

Tuning Guidelines

Performance Targets

| Platform | Max Particles | Blend Modes | | ------------- | ------------- | ---------------- | | Desktop | 500-1000 | Full support | | Mobile (high) | 200-400 | Careful with ADD | | Mobile (low) | 50-150 | Avoid ADD |

Feel Tuning

| Effect Property | Quick Effect | Slow Effect | | --------------- | ------------ | ----------- | | Lifespan | 200-400ms | 800-1500ms | | Speed | 200-400 | 20-80 | | Scale Decay | Fast | Slow | | Frequency | 20-50ms | 100-300ms |

Anti-Patterns

DON'T:

  • Overuse additive blending on mobile
  • Create too many simultaneous emitters
  • Use large textures for small particles
  • Forget to destroy temporary emitters
  • Make particles too bright (washes out)
  • Use effects that obscure gameplay

DO:

  • Profile on target device
  • Pool emitters when possible
  • Use small particle textures
  • Clean up emitters properly
  • Balance brightness and readability | Keep gameplay visible

Checklist

  • [ ] Effect purpose is clear
  • [ ] Particle count appropriate for platform
  • [ ] Lifespan tuned for feel
  • [ ] Blend mode chosen correctly
  • [ ] Emitters cleaned up
  • [ ] Effects don't obscure gameplay
  • [ ] Performance tested on target device

Reference