particle_system.dart 14.6 KB
Newer Older
1 2 3 4 5 6
part of sprites;

class _Particle {
  Vector2 pos;
  Vector2 startPos;

7 8
  double colorPos = 0.0;
  double deltaColorPos = 0.0;
9

10 11
  double size = 0.0;
  double deltaSize = 0.0;
12

13 14
  double rotation = 0.0;
  double deltaRotation = 0.0;
15

16
  double timeToLive = 0.0;
17 18

  Vector2 dir;
19 20 21 22

  _ParticleAccelerations accelerations;

  Float64List simpleColorSequence;
23 24 25 26

  ColorSequence colorSequence;
}

27
class _ParticleAccelerations {
28 29
  double radialAccel = 0.0;
  double tangentialAccel = 0.0;
30
}
31

32 33 34 35 36 37 38 39 40 41
/// A particle system uses a large number of sprites to draw complex effects
/// such as explosions, smoke, rain, or fire. There are a number of properties
/// that can be set to control the look of the particle system. Most of the
/// properties have a base value and a variance, these values are used when
/// creating each individual particle. For instance, by setting the [life] to
/// 1.0 and the [lifeVar] to 0.5, each particle will get a life time in the
/// range of 0.5 to 1.5.
///
/// Particles are created and added to the system at [emissionRate], but the
/// number of particles can never exceed the [maxParticles] limit.
42 43
class ParticleSystem extends Node {

44
  /// The texture used to draw each individual sprite.
45 46
  Texture texture;

47
  /// The time in seconds each particle will be alive.
48
  double life;
49 50

  /// Variance of the [life] property.
51 52
  double lifeVar;

53
  /// The variance of a particles initial position.
54 55
  Point posVar;

56
  /// The start scale of each individual particle.
57
  double startSize;
58 59

  /// Variance of the [startSize] property.
60 61
  double startSizeVar;

62
  /// The end scale of each individual particle.
63
  double endSize;
64 65

  /// Variance of the [endSize] property.
66 67
  double endSizeVar;

68
  /// The start rotation of each individual particle.
69
  double startRotation;
70 71

  /// Variance of the [startRotation] property.
72 73
  double startRotationVar;

74
  /// The end rotation of each individual particle.
75
  double endRotation;
76 77

  /// Variance of the [endRotation] property.
78 79
  double endRotationVar;

80 81 82 83
  /// If true, each particle will be rotated to the direction of the movement
  /// of the particle. The calculated rotation will be added to the current
  /// rotation as calculated by the [startRotation] and [endRotation]
  /// properties.
84 85
  bool rotateToMovement;

86
  /// The direction in which each particle will be emitted in degrees.
87
  double direction;
88 89

  /// Variance of the [direction] property.
90 91
  double directionVar;

92
  /// The speed at which each particle will be emitted.
93
  double speed;
94 95

  /// Variance of the [direction] property.
96 97
  double speedVar;

98
  /// The radial acceleration of each induvidual particle.
99
  double radialAcceleration;
100 101

  /// Variance of the [radialAcceleration] property.
102 103
  double radialAccelerationVar;

104
  /// The tangential acceleration of each individual particle.
105
  double tangentialAcceleration;
106 107

  /// Variance of the [tangentialAcceleration] property.
108 109
  double tangentialAccelerationVar;

110
  /// The gravity vector of the particle system.
111 112
  Vector2 gravity;

113
  /// The maximum number of particles the system can display at a single time.
114
  int maxParticles;
115 116 117

  /// Total number of particles to emit, if the value is set to 0 the system
  /// will continue to emit particles for an indifinte period of time.
118
  int numParticlesToEmit;
119 120

  /// The rate at which particles are emitted, defined in particles per second.
121
  double emissionRate;
122 123 124

  /// If set to true, the particle system will be automatically removed as soon
  /// as there are no more particles left to draw.
125 126
  bool autoRemoveOnFinish;

127 128 129 130
  /// The [ColorSequence] used to animate the color of each individual particle
  /// over the duration of its [life]. When applied to a particle the sequence's
  /// color stops modified in accordance with the [alphaVar], [redVar],
  /// [greenVar], and [blueVar] properties.
131
  ColorSequence colorSequence;
132 133

  /// Alpha varience of the [colorSequence] property.
134
  int alphaVar;
135 136

  /// Red varience of the [colorSequence] property.
137
  int redVar;
138 139

  /// Green varience of the [colorSequence] property.
140
  int greenVar;
141 142

  /// Blue varience of the [colorSequence] property.
143
  int blueVar;
144 145 146

  /// The transfer mode used to draw the particle system. Default is
  /// [TransferMode.plus].
147 148 149 150 151 152 153
  TransferMode transferMode;

  List<_Particle> _particles;

  double _emitCounter;
  int _numEmittedParticles = 0;

154 155 156 157
  static Paint _paint = new Paint()
    ..setFilterQuality(FilterQuality.low)
    ..isAntiAlias = false;

158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
  ParticleSystem(this.texture,
                 {this.life: 1.5,
                  this.lifeVar: 1.0,
                  this.posVar: Point.origin,
                  this.startSize: 2.5,
                  this.startSizeVar: 0.5,
                  this.endSize: 0.0,
                  this.endSizeVar: 0.0,
                  this.startRotation: 0.0,
                  this.startRotationVar: 0.0,
                  this.endRotation: 0.0,
                  this.endRotationVar: 0.0,
                  this.rotateToMovement : false,
                  this.direction: 0.0,
                  this.directionVar: 360.0,
                  this.speed: 100.0,
                  this.speedVar: 50.0,
                  this.radialAcceleration: 0.0,
                  this.radialAccelerationVar: 0.0,
                  this.tangentialAcceleration: 0.0,
                  this.tangentialAccelerationVar: 0.0,
                  this.gravity,
                  this.maxParticles: 100,
                  this.emissionRate: 50.0,
                  this.colorSequence,
                  this.alphaVar: 0,
                  this.redVar: 0,
                  this.greenVar: 0,
                  this.blueVar: 0,
                  this.transferMode: TransferMode.plus,
                  this.numParticlesToEmit: 0,
                  this.autoRemoveOnFinish: true}) {
    _particles = new List<_Particle>();
    _emitCounter = 0.0;
    // _elapsedTime = 0.0;
    if (gravity == null) gravity = new Vector2.zero();
    if (colorSequence == null) colorSequence = new ColorSequence.fromStartAndEndColor(new Color(0xffffffff), new Color(0x00ffffff));
  }

  void update(double dt) {
198 199
    // TODO: Fix this (it's a temp fix for low framerates)
    if (dt > 0.1) dt = 0.1;
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230

    // Create new particles
    double rate = 1.0 / emissionRate;

    if (_particles.length < maxParticles) {
      _emitCounter += dt;
    }

    while(_particles.length < maxParticles
       && _emitCounter > rate
       && (numParticlesToEmit == 0 || _numEmittedParticles < numParticlesToEmit)) {
      // Add a new particle
      _addParticle();
      _emitCounter -= rate;
    }

    // _elapsedTime += dt;

    // Iterate over all particles
    for (int i = _particles.length -1; i >= 0; i--) {
      _Particle particle = _particles[i];

      // Manage life time
      particle.timeToLive -= dt;
      if (particle.timeToLive <= 0) {
        _particles.removeAt(i);
        continue;
      }

      // Update the particle

231
      if (particle.accelerations != null) {
232 233
      // Radial acceleration
      Vector2 radial;
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
        if (particle.pos[0] != 0 || particle.pos[1] != 0) {
          radial = new Vector2.copy(particle.pos).normalize();
        } else {
          radial = new Vector2.zero();
        }
        Vector2 tangential = new Vector2.copy(radial);
        radial.scale(particle.accelerations.radialAccel);

        // Tangential acceleration
        double newY = tangential.x;
        tangential.x = -tangential.y;
        tangential.y = newY;
        tangential.scale(particle.accelerations.tangentialAccel);

        // (gravity + radial + tangential) * dt
        Vector2 accel = (gravity + radial + tangential).scale(dt);
        particle.dir += accel;
      } else if (gravity[0] != 0.0 || gravity[1] != 0) {
        // gravity
        Vector2 accel = gravity.scale(dt);
        particle.dir += accel;
255 256
      }

257 258 259
      // Update particle position
      particle.pos[0] += particle.dir[0] * dt;
      particle.pos[1] += particle.dir[1] * dt;
260 261 262 263 264 265 266 267

      // Size
      particle.size = math.max(particle.size + particle.deltaSize * dt, 0.0);

      // Angle
      particle.rotation += particle.deltaRotation * dt;

      // Color
268 269 270 271 272 273 274
      if (particle.simpleColorSequence != null) {
        for (int i = 0; i < 4; i++) {
          particle.simpleColorSequence[i] += particle.simpleColorSequence[i + 4] * dt;
        }
      } else {
        particle.colorPos = math.min(particle.colorPos + particle.deltaColorPos * dt, 1.0);
      }
275 276 277 278 279 280 281 282 283 284 285 286
    }

    if (autoRemoveOnFinish && _particles.length == 0 && _numEmittedParticles > 0) {
      if (parent != null) removeFromParent();
    }
  }

  void _addParticle() {

    _Particle particle = new _Particle();

    // Time to live
287
    particle.timeToLive = math.max(life + lifeVar * randomSignedDouble(), 0.0);
288 289 290

    // Position
    Point srcPos = Point.origin;
291 292
    particle.pos = new Vector2(srcPos.x + posVar.x * randomSignedDouble(),
                               srcPos.y + posVar.y * randomSignedDouble());
293 294

    // Size
295 296
    particle.size = math.max(startSize + startSizeVar * randomSignedDouble(), 0.0);
    double endSizeFinal = math.max(endSize + endSizeVar * randomSignedDouble(), 0.0);
297 298 299
    particle.deltaSize = (endSizeFinal - particle.size) / particle.timeToLive;

    // Rotation
300 301
    particle.rotation = startRotation + startRotationVar * randomSignedDouble();
    double endRotationFinal = endRotation + endRotationVar * randomSignedDouble();
302 303 304
    particle.deltaRotation = (endRotationFinal - particle.rotation) / particle.timeToLive;

    // Direction
305
    double dirRadians = convertDegrees2Radians(direction + directionVar * randomSignedDouble());
306
    Vector2 dirVector = new Vector2(math.cos(dirRadians), math.sin(dirRadians));
307
    double speedFinal = speed + speedVar * randomSignedDouble();
308 309
    particle.dir = dirVector.scale(speedFinal);

310 311 312 313
    // Accelerations
    if (radialAcceleration != 0.0 || radialAccelerationVar != 0.0 ||
        tangentialAcceleration != 0.0 || tangentialAccelerationVar != 0.0) {
      particle.accelerations = new _ParticleAccelerations();
314

315 316 317 318 319 320
      // Radial acceleration
      particle.accelerations.radialAccel = radialAcceleration + radialAccelerationVar * randomSignedDouble();

      // Tangential acceleration
      particle.accelerations.tangentialAccel = tangentialAcceleration + tangentialAccelerationVar * randomSignedDouble();
    }
321 322 323 324 325 326

    // Color
    particle.colorPos = 0.0;
    particle.deltaColorPos = 1.0 / particle.timeToLive;

    if (alphaVar != 0 || redVar != 0 || greenVar != 0 || blueVar != 0) {
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
      particle.colorSequence = _ColorSequenceUtil.copyWithVariance(colorSequence, alphaVar, redVar, greenVar, blueVar);
    }

    // Optimizes the case where there are only two colors in the sequence
    if (colorSequence.colors.length == 2) {
      Color startColor;
      Color endColor;

      if (particle.colorSequence != null) {
        startColor = particle.colorSequence.colors[0];
        endColor = particle.colorSequence.colors[1];
      } else {
        startColor = colorSequence.colors[0];
        endColor = colorSequence.colors[1];
      }

      // First 4 elements are start ARGB, last 4 are delta ARGB
      particle.simpleColorSequence = new Float64List(8);
      particle.simpleColorSequence[0] = startColor.alpha.toDouble();
      particle.simpleColorSequence[1] = startColor.red.toDouble();
      particle.simpleColorSequence[2] = startColor.green.toDouble();
      particle.simpleColorSequence[3] = startColor.blue.toDouble();

      particle.simpleColorSequence[4] = (endColor.alpha.toDouble() - startColor.alpha.toDouble()) / particle.timeToLive;
      particle.simpleColorSequence[5] = (endColor.red.toDouble() - startColor.red.toDouble()) / particle.timeToLive;
      particle.simpleColorSequence[6] = (endColor.green.toDouble() - startColor.green.toDouble()) / particle.timeToLive;
      particle.simpleColorSequence[7] = (endColor.blue.toDouble() - startColor.blue.toDouble()) / particle.timeToLive;
354 355 356 357 358 359 360 361 362 363 364 365
    }

    _particles.add(particle);
    _numEmittedParticles++;
  }

  void paint(PaintingCanvas canvas) {

    List<RSTransform> transforms = [];
    List<Rect> rects = [];
    List<Color> colors = [];

366 367
    _paint.setTransferMode(transferMode);

368
    for (_Particle particle in _particles) {
369 370 371 372
      // Rect
      Rect rect = texture.frame;
      rects.add(rect);

373 374 375 376
      // Transform
      double scos;
      double ssin;
      if (rotateToMovement) {
377
        double extraRotation = GameMath.atan2(particle.dir[1], particle.dir[0]);
378 379
        scos = math.cos(convertDegrees2Radians(particle.rotation) + extraRotation) * particle.size;
        ssin = math.sin(convertDegrees2Radians(particle.rotation) + extraRotation) * particle.size;
380
      } else if (particle.rotation != 0.0) {
381 382
        scos = math.cos(convertDegrees2Radians(particle.rotation)) * particle.size;
        ssin = math.sin(convertDegrees2Radians(particle.rotation)) * particle.size;
383 384 385
      } else {
        scos = particle.size;
        ssin = 0.0;
386
      }
387 388 389 390 391
      double ax = rect.width / 2;
      double ay = rect.height / 2;
      double tx = particle.pos[0] + -scos * ax + ssin * ay;
      double ty = particle.pos[1] + -ssin * ax - scos * ay;
      RSTransform transform = new RSTransform(scos, ssin, tx, ty);
392 393 394
      transforms.add(transform);

      // Color
395 396 397 398 399 400 401
      if (particle.simpleColorSequence != null) {
        Color particleColor = new Color.fromARGB(
          particle.simpleColorSequence[0].toInt().clamp(0, 255),
          particle.simpleColorSequence[1].toInt().clamp(0, 255),
          particle.simpleColorSequence[2].toInt().clamp(0, 255),
          particle.simpleColorSequence[3].toInt().clamp(0, 255));
        colors.add(particleColor);
402
      } else {
403 404 405 406 407 408 409
        Color particleColor;
        if (particle.colorSequence != null) {
          particleColor = particle.colorSequence.colorAtPosition(particle.colorPos);
        } else {
          particleColor = colorSequence.colorAtPosition(particle.colorPos);
        }
        colors.add(particleColor);
410 411 412
      }
    }

413
    canvas.drawAtlas(texture.image, transforms, rects, colors,
414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444
      TransferMode.modulate, null, _paint);
  }
}

class _ColorSequenceUtil {
  static ColorSequence copyWithVariance(
    ColorSequence sequence,
    int alphaVar,
    int redVar,
    int greenVar,
    int blueVar
  ) {
    ColorSequence copy = new ColorSequence.copy(sequence);

    int i = 0;
    for (Color color in sequence.colors) {
      int aDelta = ((randomDouble() * 2.0 - 1.0) * alphaVar).toInt();
      int rDelta = ((randomDouble() * 2.0 - 1.0) * redVar).toInt();
      int gDelta = ((randomDouble() * 2.0 - 1.0) * greenVar).toInt();
      int bDelta = ((randomDouble() * 2.0 - 1.0) * blueVar).toInt();

      int aNew = (color.alpha + aDelta).clamp(0, 255);
      int rNew = (color.red + rDelta).clamp(0, 255);
      int gNew = (color.green + gDelta).clamp(0, 255);
      int bNew = (color.blue + bDelta).clamp(0, 255);

      copy.colors[i] = new Color.fromARGB(aNew, rNew, gNew, bNew);
      i++;
    }

    return copy;
445 446
  }
}