1 /******************************************************************************* 2 * Copyright 2011 See AUTHORS file. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 ******************************************************************************/ 16 17 package com.badlogic.gdx.graphics.g2d; 18 19 import java.io.BufferedReader; 20 import java.io.IOException; 21 import java.io.Writer; 22 23 import com.badlogic.gdx.graphics.GL20; 24 import com.badlogic.gdx.graphics.Texture; 25 import com.badlogic.gdx.math.MathUtils; 26 import com.badlogic.gdx.math.Rectangle; 27 import com.badlogic.gdx.math.collision.BoundingBox; 28 29 public class ParticleEmitter { 30 static private final int UPDATE_SCALE = 1 << 0; 31 static private final int UPDATE_ANGLE = 1 << 1; 32 static private final int UPDATE_ROTATION = 1 << 2; 33 static private final int UPDATE_VELOCITY = 1 << 3; 34 static private final int UPDATE_WIND = 1 << 4; 35 static private final int UPDATE_GRAVITY = 1 << 5; 36 static private final int UPDATE_TINT = 1 << 6; 37 38 private RangedNumericValue delayValue = new RangedNumericValue(); 39 private ScaledNumericValue lifeOffsetValue = new ScaledNumericValue(); 40 private RangedNumericValue durationValue = new RangedNumericValue(); 41 private ScaledNumericValue lifeValue = new ScaledNumericValue(); 42 private ScaledNumericValue emissionValue = new ScaledNumericValue(); 43 private ScaledNumericValue scaleValue = new ScaledNumericValue(); 44 private ScaledNumericValue rotationValue = new ScaledNumericValue(); 45 private ScaledNumericValue velocityValue = new ScaledNumericValue(); 46 private ScaledNumericValue angleValue = new ScaledNumericValue(); 47 private ScaledNumericValue windValue = new ScaledNumericValue(); 48 private ScaledNumericValue gravityValue = new ScaledNumericValue(); 49 private ScaledNumericValue transparencyValue = new ScaledNumericValue(); 50 private GradientColorValue tintValue = new GradientColorValue(); 51 private RangedNumericValue xOffsetValue = new ScaledNumericValue(); 52 private RangedNumericValue yOffsetValue = new ScaledNumericValue(); 53 private ScaledNumericValue spawnWidthValue = new ScaledNumericValue(); 54 private ScaledNumericValue spawnHeightValue = new ScaledNumericValue(); 55 private SpawnShapeValue spawnShapeValue = new SpawnShapeValue(); 56 57 private float accumulator; 58 private Sprite sprite; 59 private Particle[] particles; 60 private int minParticleCount, maxParticleCount = 4; 61 private float x, y; 62 private String name; 63 private String imagePath; 64 private int activeCount; 65 private boolean[] active; 66 private boolean firstUpdate; 67 private boolean flipX, flipY; 68 private int updateFlags; 69 private boolean allowCompletion; 70 private BoundingBox bounds; 71 72 private int emission, emissionDiff, emissionDelta; 73 private int lifeOffset, lifeOffsetDiff; 74 private int life, lifeDiff; 75 private float spawnWidth, spawnWidthDiff; 76 private float spawnHeight, spawnHeightDiff; 77 public float duration = 1, durationTimer; 78 private float delay, delayTimer; 79 80 private boolean attached; 81 private boolean continuous; 82 private boolean aligned; 83 private boolean behind; 84 private boolean additive = true; 85 private boolean premultipliedAlpha = false; 86 boolean cleansUpBlendFunction = true; 87 88 public ParticleEmitter () { 89 initialize(); 90 } 91 92 public ParticleEmitter (BufferedReader reader) throws IOException { 93 initialize(); 94 load(reader); 95 } 96 97 public ParticleEmitter (ParticleEmitter emitter) { 98 sprite = emitter.sprite; 99 name = emitter.name; 100 imagePath = emitter.imagePath; 101 setMaxParticleCount(emitter.maxParticleCount); 102 minParticleCount = emitter.minParticleCount; 103 delayValue.load(emitter.delayValue); 104 durationValue.load(emitter.durationValue); 105 emissionValue.load(emitter.emissionValue); 106 lifeValue.load(emitter.lifeValue); 107 lifeOffsetValue.load(emitter.lifeOffsetValue); 108 scaleValue.load(emitter.scaleValue); 109 rotationValue.load(emitter.rotationValue); 110 velocityValue.load(emitter.velocityValue); 111 angleValue.load(emitter.angleValue); 112 windValue.load(emitter.windValue); 113 gravityValue.load(emitter.gravityValue); 114 transparencyValue.load(emitter.transparencyValue); 115 tintValue.load(emitter.tintValue); 116 xOffsetValue.load(emitter.xOffsetValue); 117 yOffsetValue.load(emitter.yOffsetValue); 118 spawnWidthValue.load(emitter.spawnWidthValue); 119 spawnHeightValue.load(emitter.spawnHeightValue); 120 spawnShapeValue.load(emitter.spawnShapeValue); 121 attached = emitter.attached; 122 continuous = emitter.continuous; 123 aligned = emitter.aligned; 124 behind = emitter.behind; 125 additive = emitter.additive; 126 premultipliedAlpha = emitter.premultipliedAlpha; 127 cleansUpBlendFunction = emitter.cleansUpBlendFunction; 128 } 129 130 private void initialize () { 131 durationValue.setAlwaysActive(true); 132 emissionValue.setAlwaysActive(true); 133 lifeValue.setAlwaysActive(true); 134 scaleValue.setAlwaysActive(true); 135 transparencyValue.setAlwaysActive(true); 136 spawnShapeValue.setAlwaysActive(true); 137 spawnWidthValue.setAlwaysActive(true); 138 spawnHeightValue.setAlwaysActive(true); 139 } 140 141 public void setMaxParticleCount (int maxParticleCount) { 142 this.maxParticleCount = maxParticleCount; 143 active = new boolean[maxParticleCount]; 144 activeCount = 0; 145 particles = new Particle[maxParticleCount]; 146 } 147 148 public void addParticle () { 149 int activeCount = this.activeCount; 150 if (activeCount == maxParticleCount) return; 151 boolean[] active = this.active; 152 for (int i = 0, n = active.length; i < n; i++) { 153 if (!active[i]) { 154 activateParticle(i); 155 active[i] = true; 156 this.activeCount = activeCount + 1; 157 break; 158 } 159 } 160 } 161 162 public void addParticles (int count) { 163 count = Math.min(count, maxParticleCount - activeCount); 164 if (count == 0) return; 165 boolean[] active = this.active; 166 int index = 0, n = active.length; 167 outer: 168 for (int i = 0; i < count; i++) { 169 for (; index < n; index++) { 170 if (!active[index]) { 171 activateParticle(index); 172 active[index++] = true; 173 continue outer; 174 } 175 } 176 break; 177 } 178 this.activeCount += count; 179 } 180 181 public void update (float delta) { 182 accumulator += delta * 1000; 183 if (accumulator < 1) return; 184 int deltaMillis = (int)accumulator; 185 accumulator -= deltaMillis; 186 187 if (delayTimer < delay) { 188 delayTimer += deltaMillis; 189 } else { 190 boolean done = false; 191 if (firstUpdate) { 192 firstUpdate = false; 193 addParticle(); 194 } 195 196 if (durationTimer < duration) 197 durationTimer += deltaMillis; 198 else { 199 if (!continuous || allowCompletion) 200 done = true; 201 else 202 restart(); 203 } 204 205 if (!done) { 206 emissionDelta += deltaMillis; 207 float emissionTime = emission + emissionDiff * emissionValue.getScale(durationTimer / (float)duration); 208 if (emissionTime > 0) { 209 emissionTime = 1000 / emissionTime; 210 if (emissionDelta >= emissionTime) { 211 int emitCount = (int)(emissionDelta / emissionTime); 212 emitCount = Math.min(emitCount, maxParticleCount - activeCount); 213 emissionDelta -= emitCount * emissionTime; 214 emissionDelta %= emissionTime; 215 addParticles(emitCount); 216 } 217 } 218 if (activeCount < minParticleCount) addParticles(minParticleCount - activeCount); 219 } 220 } 221 222 boolean[] active = this.active; 223 int activeCount = this.activeCount; 224 Particle[] particles = this.particles; 225 for (int i = 0, n = active.length; i < n; i++) { 226 if (active[i] && !updateParticle(particles[i], delta, deltaMillis)) { 227 active[i] = false; 228 activeCount--; 229 } 230 } 231 this.activeCount = activeCount; 232 } 233 234 public void draw (Batch batch) { 235 if (premultipliedAlpha) { 236 batch.setBlendFunction(GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA); 237 } else if (additive) { 238 batch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE); 239 } else { 240 batch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA); 241 } 242 Particle[] particles = this.particles; 243 boolean[] active = this.active; 244 245 for (int i = 0, n = active.length; i < n; i++) { 246 if (active[i]) particles[i].draw(batch); 247 } 248 249 if (cleansUpBlendFunction && (additive || premultipliedAlpha)) 250 batch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA); 251 252 } 253 254 /** Updates and draws the particles. This is slightly more efficient than calling {@link #update(float)} and 255 * {@link #draw(Batch)} separately. */ 256 public void draw (Batch batch, float delta) { 257 accumulator += delta * 1000; 258 if (accumulator < 1) { 259 draw(batch); 260 return; 261 } 262 int deltaMillis = (int)accumulator; 263 accumulator -= deltaMillis; 264 265 if (premultipliedAlpha) { 266 batch.setBlendFunction(GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA); 267 } else if (additive) { 268 batch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE); 269 } else { 270 batch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA); 271 } 272 273 Particle[] particles = this.particles; 274 boolean[] active = this.active; 275 int activeCount = this.activeCount; 276 for (int i = 0, n = active.length; i < n; i++) { 277 if (active[i]) { 278 Particle particle = particles[i]; 279 if (updateParticle(particle, delta, deltaMillis)) 280 particle.draw(batch); 281 else { 282 active[i] = false; 283 activeCount--; 284 } 285 } 286 } 287 this.activeCount = activeCount; 288 289 if (cleansUpBlendFunction && (additive || premultipliedAlpha)) 290 batch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA); 291 292 if (delayTimer < delay) { 293 delayTimer += deltaMillis; 294 return; 295 } 296 297 if (firstUpdate) { 298 firstUpdate = false; 299 addParticle(); 300 } 301 302 if (durationTimer < duration) 303 durationTimer += deltaMillis; 304 else { 305 if (!continuous || allowCompletion) return; 306 restart(); 307 } 308 309 emissionDelta += deltaMillis; 310 float emissionTime = emission + emissionDiff * emissionValue.getScale(durationTimer / (float)duration); 311 if (emissionTime > 0) { 312 emissionTime = 1000 / emissionTime; 313 if (emissionDelta >= emissionTime) { 314 int emitCount = (int)(emissionDelta / emissionTime); 315 emitCount = Math.min(emitCount, maxParticleCount - activeCount); 316 emissionDelta -= emitCount * emissionTime; 317 emissionDelta %= emissionTime; 318 addParticles(emitCount); 319 } 320 } 321 if (activeCount < minParticleCount) addParticles(minParticleCount - activeCount); 322 } 323 324 public void start () { 325 firstUpdate = true; 326 allowCompletion = false; 327 restart(); 328 } 329 330 public void reset () { 331 emissionDelta = 0; 332 durationTimer = duration; 333 boolean[] active = this.active; 334 for (int i = 0, n = active.length; i < n; i++) 335 active[i] = false; 336 activeCount = 0; 337 start(); 338 } 339 340 private void restart () { 341 delay = delayValue.active ? delayValue.newLowValue() : 0; 342 delayTimer = 0; 343 344 durationTimer -= duration; 345 duration = durationValue.newLowValue(); 346 347 emission = (int)emissionValue.newLowValue(); 348 emissionDiff = (int)emissionValue.newHighValue(); 349 if (!emissionValue.isRelative()) emissionDiff -= emission; 350 351 life = (int)lifeValue.newLowValue(); 352 lifeDiff = (int)lifeValue.newHighValue(); 353 if (!lifeValue.isRelative()) lifeDiff -= life; 354 355 lifeOffset = lifeOffsetValue.active ? (int)lifeOffsetValue.newLowValue() : 0; 356 lifeOffsetDiff = (int)lifeOffsetValue.newHighValue(); 357 if (!lifeOffsetValue.isRelative()) lifeOffsetDiff -= lifeOffset; 358 359 spawnWidth = spawnWidthValue.newLowValue(); 360 spawnWidthDiff = spawnWidthValue.newHighValue(); 361 if (!spawnWidthValue.isRelative()) spawnWidthDiff -= spawnWidth; 362 363 spawnHeight = spawnHeightValue.newLowValue(); 364 spawnHeightDiff = spawnHeightValue.newHighValue(); 365 if (!spawnHeightValue.isRelative()) spawnHeightDiff -= spawnHeight; 366 367 updateFlags = 0; 368 if (angleValue.active && angleValue.timeline.length > 1) updateFlags |= UPDATE_ANGLE; 369 if (velocityValue.active) updateFlags |= UPDATE_VELOCITY; 370 if (scaleValue.timeline.length > 1) updateFlags |= UPDATE_SCALE; 371 if (rotationValue.active && rotationValue.timeline.length > 1) updateFlags |= UPDATE_ROTATION; 372 if (windValue.active) updateFlags |= UPDATE_WIND; 373 if (gravityValue.active) updateFlags |= UPDATE_GRAVITY; 374 if (tintValue.timeline.length > 1) updateFlags |= UPDATE_TINT; 375 } 376 377 protected Particle newParticle (Sprite sprite) { 378 return new Particle(sprite); 379 } 380 381 private void activateParticle (int index) { 382 Particle particle = particles[index]; 383 if (particle == null) { 384 particles[index] = particle = newParticle(sprite); 385 particle.flip(flipX, flipY); 386 } 387 388 float percent = durationTimer / (float)duration; 389 int updateFlags = this.updateFlags; 390 391 particle.currentLife = particle.life = life + (int)(lifeDiff * lifeValue.getScale(percent)); 392 393 if (velocityValue.active) { 394 particle.velocity = velocityValue.newLowValue(); 395 particle.velocityDiff = velocityValue.newHighValue(); 396 if (!velocityValue.isRelative()) particle.velocityDiff -= particle.velocity; 397 } 398 399 particle.angle = angleValue.newLowValue(); 400 particle.angleDiff = angleValue.newHighValue(); 401 if (!angleValue.isRelative()) particle.angleDiff -= particle.angle; 402 float angle = 0; 403 if ((updateFlags & UPDATE_ANGLE) == 0) { 404 angle = particle.angle + particle.angleDiff * angleValue.getScale(0); 405 particle.angle = angle; 406 particle.angleCos = MathUtils.cosDeg(angle); 407 particle.angleSin = MathUtils.sinDeg(angle); 408 } 409 410 float spriteWidth = sprite.getWidth(); 411 particle.scale = scaleValue.newLowValue() / spriteWidth; 412 particle.scaleDiff = scaleValue.newHighValue() / spriteWidth; 413 if (!scaleValue.isRelative()) particle.scaleDiff -= particle.scale; 414 particle.setScale(particle.scale + particle.scaleDiff * scaleValue.getScale(0)); 415 416 if (rotationValue.active) { 417 particle.rotation = rotationValue.newLowValue(); 418 particle.rotationDiff = rotationValue.newHighValue(); 419 if (!rotationValue.isRelative()) particle.rotationDiff -= particle.rotation; 420 float rotation = particle.rotation + particle.rotationDiff * rotationValue.getScale(0); 421 if (aligned) rotation += angle; 422 particle.setRotation(rotation); 423 } 424 425 if (windValue.active) { 426 particle.wind = windValue.newLowValue(); 427 particle.windDiff = windValue.newHighValue(); 428 if (!windValue.isRelative()) particle.windDiff -= particle.wind; 429 } 430 431 if (gravityValue.active) { 432 particle.gravity = gravityValue.newLowValue(); 433 particle.gravityDiff = gravityValue.newHighValue(); 434 if (!gravityValue.isRelative()) particle.gravityDiff -= particle.gravity; 435 } 436 437 float[] color = particle.tint; 438 if (color == null) particle.tint = color = new float[3]; 439 float[] temp = tintValue.getColor(0); 440 color[0] = temp[0]; 441 color[1] = temp[1]; 442 color[2] = temp[2]; 443 444 particle.transparency = transparencyValue.newLowValue(); 445 particle.transparencyDiff = transparencyValue.newHighValue() - particle.transparency; 446 447 // Spawn. 448 float x = this.x; 449 if (xOffsetValue.active) x += xOffsetValue.newLowValue(); 450 float y = this.y; 451 if (yOffsetValue.active) y += yOffsetValue.newLowValue(); 452 switch (spawnShapeValue.shape) { 453 case square: { 454 float width = spawnWidth + (spawnWidthDiff * spawnWidthValue.getScale(percent)); 455 float height = spawnHeight + (spawnHeightDiff * spawnHeightValue.getScale(percent)); 456 x += MathUtils.random(width) - width / 2; 457 y += MathUtils.random(height) - height / 2; 458 break; 459 } 460 case ellipse: { 461 float width = spawnWidth + (spawnWidthDiff * spawnWidthValue.getScale(percent)); 462 float height = spawnHeight + (spawnHeightDiff * spawnHeightValue.getScale(percent)); 463 float radiusX = width / 2; 464 float radiusY = height / 2; 465 if (radiusX == 0 || radiusY == 0) break; 466 float scaleY = radiusX / (float)radiusY; 467 if (spawnShapeValue.edges) { 468 float spawnAngle; 469 switch (spawnShapeValue.side) { 470 case top: 471 spawnAngle = -MathUtils.random(179f); 472 break; 473 case bottom: 474 spawnAngle = MathUtils.random(179f); 475 break; 476 default: 477 spawnAngle = MathUtils.random(360f); 478 break; 479 } 480 float cosDeg = MathUtils.cosDeg(spawnAngle); 481 float sinDeg = MathUtils.sinDeg(spawnAngle); 482 x += cosDeg * radiusX; 483 y += sinDeg * radiusX / scaleY; 484 if ((updateFlags & UPDATE_ANGLE) == 0) { 485 particle.angle = spawnAngle; 486 particle.angleCos = cosDeg; 487 particle.angleSin = sinDeg; 488 } 489 } else { 490 float radius2 = radiusX * radiusX; 491 while (true) { 492 float px = MathUtils.random(width) - radiusX; 493 float py = MathUtils.random(height) - radiusY; 494 if (px * px + py * py <= radius2) { 495 x += px; 496 y += py / scaleY; 497 break; 498 } 499 } 500 } 501 break; 502 } 503 case line: { 504 float width = spawnWidth + (spawnWidthDiff * spawnWidthValue.getScale(percent)); 505 float height = spawnHeight + (spawnHeightDiff * spawnHeightValue.getScale(percent)); 506 if (width != 0) { 507 float lineX = width * MathUtils.random(); 508 x += lineX; 509 y += lineX * (height / (float)width); 510 } else 511 y += height * MathUtils.random(); 512 break; 513 } 514 } 515 516 float spriteHeight = sprite.getHeight(); 517 particle.setBounds(x - spriteWidth / 2, y - spriteHeight / 2, spriteWidth, spriteHeight); 518 519 int offsetTime = (int)(lifeOffset + lifeOffsetDiff * lifeOffsetValue.getScale(percent)); 520 if (offsetTime > 0) { 521 if (offsetTime >= particle.currentLife) offsetTime = particle.currentLife - 1; 522 updateParticle(particle, offsetTime / 1000f, offsetTime); 523 } 524 } 525 526 private boolean updateParticle (Particle particle, float delta, int deltaMillis) { 527 int life = particle.currentLife - deltaMillis; 528 if (life <= 0) return false; 529 particle.currentLife = life; 530 531 float percent = 1 - particle.currentLife / (float)particle.life; 532 int updateFlags = this.updateFlags; 533 534 if ((updateFlags & UPDATE_SCALE) != 0) 535 particle.setScale(particle.scale + particle.scaleDiff * scaleValue.getScale(percent)); 536 537 if ((updateFlags & UPDATE_VELOCITY) != 0) { 538 float velocity = (particle.velocity + particle.velocityDiff * velocityValue.getScale(percent)) * delta; 539 540 float velocityX, velocityY; 541 if ((updateFlags & UPDATE_ANGLE) != 0) { 542 float angle = particle.angle + particle.angleDiff * angleValue.getScale(percent); 543 velocityX = velocity * MathUtils.cosDeg(angle); 544 velocityY = velocity * MathUtils.sinDeg(angle); 545 if ((updateFlags & UPDATE_ROTATION) != 0) { 546 float rotation = particle.rotation + particle.rotationDiff * rotationValue.getScale(percent); 547 if (aligned) rotation += angle; 548 particle.setRotation(rotation); 549 } 550 } else { 551 velocityX = velocity * particle.angleCos; 552 velocityY = velocity * particle.angleSin; 553 if (aligned || (updateFlags & UPDATE_ROTATION) != 0) { 554 float rotation = particle.rotation + particle.rotationDiff * rotationValue.getScale(percent); 555 if (aligned) rotation += particle.angle; 556 particle.setRotation(rotation); 557 } 558 } 559 560 if ((updateFlags & UPDATE_WIND) != 0) 561 velocityX += (particle.wind + particle.windDiff * windValue.getScale(percent)) * delta; 562 563 if ((updateFlags & UPDATE_GRAVITY) != 0) 564 velocityY += (particle.gravity + particle.gravityDiff * gravityValue.getScale(percent)) * delta; 565 566 particle.translate(velocityX, velocityY); 567 } else { 568 if ((updateFlags & UPDATE_ROTATION) != 0) 569 particle.setRotation(particle.rotation + particle.rotationDiff * rotationValue.getScale(percent)); 570 } 571 572 float[] color; 573 if ((updateFlags & UPDATE_TINT) != 0) 574 color = tintValue.getColor(percent); 575 else 576 color = particle.tint; 577 578 if (premultipliedAlpha) { 579 float alphaMultiplier = additive ? 0 : 1; 580 float a = particle.transparency + particle.transparencyDiff * transparencyValue.getScale(percent); 581 particle.setColor(color[0] * a, color[1] * a, color[2] * a, a * alphaMultiplier); 582 } else { 583 particle.setColor(color[0], color[1], color[2], 584 particle.transparency + particle.transparencyDiff * transparencyValue.getScale(percent)); 585 } 586 return true; 587 } 588 589 public void setPosition (float x, float y) { 590 if (attached) { 591 float xAmount = x - this.x; 592 float yAmount = y - this.y; 593 boolean[] active = this.active; 594 for (int i = 0, n = active.length; i < n; i++) 595 if (active[i]) particles[i].translate(xAmount, yAmount); 596 } 597 this.x = x; 598 this.y = y; 599 } 600 601 public void setSprite (Sprite sprite) { 602 this.sprite = sprite; 603 if (sprite == null) return; 604 float originX = sprite.getOriginX(); 605 float originY = sprite.getOriginY(); 606 Texture texture = sprite.getTexture(); 607 for (int i = 0, n = particles.length; i < n; i++) { 608 Particle particle = particles[i]; 609 if (particle == null) break; 610 particle.setTexture(texture); 611 particle.setOrigin(originX, originY); 612 } 613 } 614 615 /** Ignores the {@link #setContinuous(boolean) continuous} setting until the emitter is started again. This allows the emitter 616 * to stop smoothly. */ 617 public void allowCompletion () { 618 allowCompletion = true; 619 durationTimer = duration; 620 } 621 622 public Sprite getSprite () { 623 return sprite; 624 } 625 626 public String getName () { 627 return name; 628 } 629 630 public void setName (String name) { 631 this.name = name; 632 } 633 634 public ScaledNumericValue getLife () { 635 return lifeValue; 636 } 637 638 public ScaledNumericValue getScale () { 639 return scaleValue; 640 } 641 642 public ScaledNumericValue getRotation () { 643 return rotationValue; 644 } 645 646 public GradientColorValue getTint () { 647 return tintValue; 648 } 649 650 public ScaledNumericValue getVelocity () { 651 return velocityValue; 652 } 653 654 public ScaledNumericValue getWind () { 655 return windValue; 656 } 657 658 public ScaledNumericValue getGravity () { 659 return gravityValue; 660 } 661 662 public ScaledNumericValue getAngle () { 663 return angleValue; 664 } 665 666 public ScaledNumericValue getEmission () { 667 return emissionValue; 668 } 669 670 public ScaledNumericValue getTransparency () { 671 return transparencyValue; 672 } 673 674 public RangedNumericValue getDuration () { 675 return durationValue; 676 } 677 678 public RangedNumericValue getDelay () { 679 return delayValue; 680 } 681 682 public ScaledNumericValue getLifeOffset () { 683 return lifeOffsetValue; 684 } 685 686 public RangedNumericValue getXOffsetValue () { 687 return xOffsetValue; 688 } 689 690 public RangedNumericValue getYOffsetValue () { 691 return yOffsetValue; 692 } 693 694 public ScaledNumericValue getSpawnWidth () { 695 return spawnWidthValue; 696 } 697 698 public ScaledNumericValue getSpawnHeight () { 699 return spawnHeightValue; 700 } 701 702 public SpawnShapeValue getSpawnShape () { 703 return spawnShapeValue; 704 } 705 706 public boolean isAttached () { 707 return attached; 708 } 709 710 public void setAttached (boolean attached) { 711 this.attached = attached; 712 } 713 714 public boolean isContinuous () { 715 return continuous; 716 } 717 718 public void setContinuous (boolean continuous) { 719 this.continuous = continuous; 720 } 721 722 public boolean isAligned () { 723 return aligned; 724 } 725 726 public void setAligned (boolean aligned) { 727 this.aligned = aligned; 728 } 729 730 public boolean isAdditive () { 731 return additive; 732 } 733 734 public void setAdditive (boolean additive) { 735 this.additive = additive; 736 } 737 738 /** @return Whether this ParticleEmitter automatically returns the {@link com.badlogic.gdx.graphics.g2d.Batch Batch}'s blend 739 * function to the alpha-blending default (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) when done drawing. */ 740 public boolean cleansUpBlendFunction () { 741 return cleansUpBlendFunction; 742 } 743 744 /** Set whether to automatically return the {@link com.badlogic.gdx.graphics.g2d.Batch Batch}'s blend function to the 745 * alpha-blending default (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) when done drawing. Is true by default. If set to false, the 746 * Batch's blend function is left as it was for drawing this ParticleEmitter, which prevents the Batch from being flushed 747 * repeatedly if consecutive ParticleEmitters with the same additive or pre-multiplied alpha state are drawn in a row. 748 * <p> 749 * IMPORTANT: If set to false and if the next object to use this Batch expects alpha blending, you are responsible for setting 750 * the Batch's blend function to (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) before that next object is drawn. 751 * @param cleansUpBlendFunction */ 752 public void setCleansUpBlendFunction (boolean cleansUpBlendFunction) { 753 this.cleansUpBlendFunction = cleansUpBlendFunction; 754 } 755 756 public boolean isBehind () { 757 return behind; 758 } 759 760 public void setBehind (boolean behind) { 761 this.behind = behind; 762 } 763 764 public boolean isPremultipliedAlpha () { 765 return premultipliedAlpha; 766 } 767 768 public void setPremultipliedAlpha (boolean premultipliedAlpha) { 769 this.premultipliedAlpha = premultipliedAlpha; 770 } 771 772 public int getMinParticleCount () { 773 return minParticleCount; 774 } 775 776 public void setMinParticleCount (int minParticleCount) { 777 this.minParticleCount = minParticleCount; 778 } 779 780 public int getMaxParticleCount () { 781 return maxParticleCount; 782 } 783 784 public boolean isComplete () { 785 if (continuous) return false; 786 if (delayTimer < delay) return false; 787 return durationTimer >= duration && activeCount == 0; 788 } 789 790 public float getPercentComplete () { 791 if (delayTimer < delay) return 0; 792 return Math.min(1, durationTimer / (float)duration); 793 } 794 795 public float getX () { 796 return x; 797 } 798 799 public float getY () { 800 return y; 801 } 802 803 public int getActiveCount () { 804 return activeCount; 805 } 806 807 public String getImagePath () { 808 return imagePath; 809 } 810 811 public void setImagePath (String imagePath) { 812 this.imagePath = imagePath; 813 } 814 815 public void setFlip (boolean flipX, boolean flipY) { 816 this.flipX = flipX; 817 this.flipY = flipY; 818 if (particles == null) return; 819 for (int i = 0, n = particles.length; i < n; i++) { 820 Particle particle = particles[i]; 821 if (particle != null) particle.flip(flipX, flipY); 822 } 823 } 824 825 public void flipY () { 826 angleValue.setHigh(-angleValue.getHighMin(), -angleValue.getHighMax()); 827 angleValue.setLow(-angleValue.getLowMin(), -angleValue.getLowMax()); 828 829 gravityValue.setHigh(-gravityValue.getHighMin(), -gravityValue.getHighMax()); 830 gravityValue.setLow(-gravityValue.getLowMin(), -gravityValue.getLowMax()); 831 832 windValue.setHigh(-windValue.getHighMin(), -windValue.getHighMax()); 833 windValue.setLow(-windValue.getLowMin(), -windValue.getLowMax()); 834 835 rotationValue.setHigh(-rotationValue.getHighMin(), -rotationValue.getHighMax()); 836 rotationValue.setLow(-rotationValue.getLowMin(), -rotationValue.getLowMax()); 837 838 yOffsetValue.setLow(-yOffsetValue.getLowMin(), -yOffsetValue.getLowMax()); 839 } 840 841 /** Returns the bounding box for all active particles. z axis will always be zero. */ 842 public BoundingBox getBoundingBox () { 843 if (bounds == null) bounds = new BoundingBox(); 844 845 Particle[] particles = this.particles; 846 boolean[] active = this.active; 847 BoundingBox bounds = this.bounds; 848 849 bounds.inf(); 850 for (int i = 0, n = active.length; i < n; i++) 851 if (active[i]) { 852 Rectangle r = particles[i].getBoundingRectangle(); 853 bounds.ext(r.x, r.y, 0); 854 bounds.ext(r.x + r.width, r.y + r.height, 0); 855 } 856 857 return bounds; 858 } 859 860 public void save (Writer output) throws IOException { 861 output.write(name + "\n"); 862 output.write("- Delay -\n"); 863 delayValue.save(output); 864 output.write("- Duration - \n"); 865 durationValue.save(output); 866 output.write("- Count - \n"); 867 output.write("min: " + minParticleCount + "\n"); 868 output.write("max: " + maxParticleCount + "\n"); 869 output.write("- Emission - \n"); 870 emissionValue.save(output); 871 output.write("- Life - \n"); 872 lifeValue.save(output); 873 output.write("- Life Offset - \n"); 874 lifeOffsetValue.save(output); 875 output.write("- X Offset - \n"); 876 xOffsetValue.save(output); 877 output.write("- Y Offset - \n"); 878 yOffsetValue.save(output); 879 output.write("- Spawn Shape - \n"); 880 spawnShapeValue.save(output); 881 output.write("- Spawn Width - \n"); 882 spawnWidthValue.save(output); 883 output.write("- Spawn Height - \n"); 884 spawnHeightValue.save(output); 885 output.write("- Scale - \n"); 886 scaleValue.save(output); 887 output.write("- Velocity - \n"); 888 velocityValue.save(output); 889 output.write("- Angle - \n"); 890 angleValue.save(output); 891 output.write("- Rotation - \n"); 892 rotationValue.save(output); 893 output.write("- Wind - \n"); 894 windValue.save(output); 895 output.write("- Gravity - \n"); 896 gravityValue.save(output); 897 output.write("- Tint - \n"); 898 tintValue.save(output); 899 output.write("- Transparency - \n"); 900 transparencyValue.save(output); 901 output.write("- Options - \n"); 902 output.write("attached: " + attached + "\n"); 903 output.write("continuous: " + continuous + "\n"); 904 output.write("aligned: " + aligned + "\n"); 905 output.write("additive: " + additive + "\n"); 906 output.write("behind: " + behind + "\n"); 907 output.write("premultipliedAlpha: " + premultipliedAlpha + "\n"); 908 output.write("- Image Path -\n"); 909 output.write(imagePath + "\n"); 910 } 911 912 public void load (BufferedReader reader) throws IOException { 913 try { 914 name = readString(reader, "name"); 915 reader.readLine(); 916 delayValue.load(reader); 917 reader.readLine(); 918 durationValue.load(reader); 919 reader.readLine(); 920 setMinParticleCount(readInt(reader, "minParticleCount")); 921 setMaxParticleCount(readInt(reader, "maxParticleCount")); 922 reader.readLine(); 923 emissionValue.load(reader); 924 reader.readLine(); 925 lifeValue.load(reader); 926 reader.readLine(); 927 lifeOffsetValue.load(reader); 928 reader.readLine(); 929 xOffsetValue.load(reader); 930 reader.readLine(); 931 yOffsetValue.load(reader); 932 reader.readLine(); 933 spawnShapeValue.load(reader); 934 reader.readLine(); 935 spawnWidthValue.load(reader); 936 reader.readLine(); 937 spawnHeightValue.load(reader); 938 reader.readLine(); 939 scaleValue.load(reader); 940 reader.readLine(); 941 velocityValue.load(reader); 942 reader.readLine(); 943 angleValue.load(reader); 944 reader.readLine(); 945 rotationValue.load(reader); 946 reader.readLine(); 947 windValue.load(reader); 948 reader.readLine(); 949 gravityValue.load(reader); 950 reader.readLine(); 951 tintValue.load(reader); 952 reader.readLine(); 953 transparencyValue.load(reader); 954 reader.readLine(); 955 attached = readBoolean(reader, "attached"); 956 continuous = readBoolean(reader, "continuous"); 957 aligned = readBoolean(reader, "aligned"); 958 additive = readBoolean(reader, "additive"); 959 behind = readBoolean(reader, "behind"); 960 961 // Backwards compatibility 962 String line = reader.readLine(); 963 if (line.startsWith("premultipliedAlpha")) { 964 premultipliedAlpha = readBoolean(line); 965 reader.readLine(); 966 } 967 setImagePath(reader.readLine()); 968 } catch (RuntimeException ex) { 969 if (name == null) throw ex; 970 throw new RuntimeException("Error parsing emitter: " + name, ex); 971 } 972 } 973 974 static String readString (String line) throws IOException { 975 return line.substring(line.indexOf(":") + 1).trim(); 976 } 977 978 static String readString (BufferedReader reader, String name) throws IOException { 979 String line = reader.readLine(); 980 if (line == null) throw new IOException("Missing value: " + name); 981 return readString(line); 982 } 983 984 static boolean readBoolean (String line) throws IOException { 985 return Boolean.parseBoolean(readString(line)); 986 } 987 988 static boolean readBoolean (BufferedReader reader, String name) throws IOException { 989 return Boolean.parseBoolean(readString(reader, name)); 990 } 991 992 static int readInt (BufferedReader reader, String name) throws IOException { 993 return Integer.parseInt(readString(reader, name)); 994 } 995 996 static float readFloat (BufferedReader reader, String name) throws IOException { 997 return Float.parseFloat(readString(reader, name)); 998 } 999 1000 public static class Particle extends Sprite { 1001 protected int life, currentLife; 1002 protected float scale, scaleDiff; 1003 protected float rotation, rotationDiff; 1004 protected float velocity, velocityDiff; 1005 protected float angle, angleDiff; 1006 protected float angleCos, angleSin; 1007 protected float transparency, transparencyDiff; 1008 protected float wind, windDiff; 1009 protected float gravity, gravityDiff; 1010 protected float[] tint; 1011 1012 public Particle (Sprite sprite) { 1013 super(sprite); 1014 } 1015 } 1016 1017 static public class ParticleValue { 1018 boolean active; 1019 boolean alwaysActive; 1020 1021 public void setAlwaysActive (boolean alwaysActive) { 1022 this.alwaysActive = alwaysActive; 1023 } 1024 1025 public boolean isAlwaysActive () { 1026 return alwaysActive; 1027 } 1028 1029 public boolean isActive () { 1030 return alwaysActive || active; 1031 } 1032 1033 public void setActive (boolean active) { 1034 this.active = active; 1035 } 1036 1037 public void save (Writer output) throws IOException { 1038 if (!alwaysActive) 1039 output.write("active: " + active + "\n"); 1040 else 1041 active = true; 1042 } 1043 1044 public void load (BufferedReader reader) throws IOException { 1045 if (!alwaysActive) 1046 active = readBoolean(reader, "active"); 1047 else 1048 active = true; 1049 } 1050 1051 public void load (ParticleValue value) { 1052 active = value.active; 1053 alwaysActive = value.alwaysActive; 1054 } 1055 } 1056 1057 static public class NumericValue extends ParticleValue { 1058 private float value; 1059 1060 public float getValue () { 1061 return value; 1062 } 1063 1064 public void setValue (float value) { 1065 this.value = value; 1066 } 1067 1068 public void save (Writer output) throws IOException { 1069 super.save(output); 1070 if (!active) return; 1071 output.write("value: " + value + "\n"); 1072 } 1073 1074 public void load (BufferedReader reader) throws IOException { 1075 super.load(reader); 1076 if (!active) return; 1077 value = readFloat(reader, "value"); 1078 } 1079 1080 public void load (NumericValue value) { 1081 super.load(value); 1082 this.value = value.value; 1083 } 1084 } 1085 1086 static public class RangedNumericValue extends ParticleValue { 1087 private float lowMin, lowMax; 1088 1089 public float newLowValue () { 1090 return lowMin + (lowMax - lowMin) * MathUtils.random(); 1091 } 1092 1093 public void setLow (float value) { 1094 lowMin = value; 1095 lowMax = value; 1096 } 1097 1098 public void setLow (float min, float max) { 1099 lowMin = min; 1100 lowMax = max; 1101 } 1102 1103 public float getLowMin () { 1104 return lowMin; 1105 } 1106 1107 public void setLowMin (float lowMin) { 1108 this.lowMin = lowMin; 1109 } 1110 1111 public float getLowMax () { 1112 return lowMax; 1113 } 1114 1115 public void setLowMax (float lowMax) { 1116 this.lowMax = lowMax; 1117 } 1118 1119 public void save (Writer output) throws IOException { 1120 super.save(output); 1121 if (!active) return; 1122 output.write("lowMin: " + lowMin + "\n"); 1123 output.write("lowMax: " + lowMax + "\n"); 1124 } 1125 1126 public void load (BufferedReader reader) throws IOException { 1127 super.load(reader); 1128 if (!active) return; 1129 lowMin = readFloat(reader, "lowMin"); 1130 lowMax = readFloat(reader, "lowMax"); 1131 } 1132 1133 public void load (RangedNumericValue value) { 1134 super.load(value); 1135 lowMax = value.lowMax; 1136 lowMin = value.lowMin; 1137 } 1138 } 1139 1140 static public class ScaledNumericValue extends RangedNumericValue { 1141 private float[] scaling = {1}; 1142 float[] timeline = {0}; 1143 private float highMin, highMax; 1144 private boolean relative; 1145 1146 public float newHighValue () { 1147 return highMin + (highMax - highMin) * MathUtils.random(); 1148 } 1149 1150 public void setHigh (float value) { 1151 highMin = value; 1152 highMax = value; 1153 } 1154 1155 public void setHigh (float min, float max) { 1156 highMin = min; 1157 highMax = max; 1158 } 1159 1160 public float getHighMin () { 1161 return highMin; 1162 } 1163 1164 public void setHighMin (float highMin) { 1165 this.highMin = highMin; 1166 } 1167 1168 public float getHighMax () { 1169 return highMax; 1170 } 1171 1172 public void setHighMax (float highMax) { 1173 this.highMax = highMax; 1174 } 1175 1176 public float[] getScaling () { 1177 return scaling; 1178 } 1179 1180 public void setScaling (float[] values) { 1181 this.scaling = values; 1182 } 1183 1184 public float[] getTimeline () { 1185 return timeline; 1186 } 1187 1188 public void setTimeline (float[] timeline) { 1189 this.timeline = timeline; 1190 } 1191 1192 public boolean isRelative () { 1193 return relative; 1194 } 1195 1196 public void setRelative (boolean relative) { 1197 this.relative = relative; 1198 } 1199 1200 public float getScale (float percent) { 1201 int endIndex = -1; 1202 float[] timeline = this.timeline; 1203 int n = timeline.length; 1204 for (int i = 1; i < n; i++) { 1205 float t = timeline[i]; 1206 if (t > percent) { 1207 endIndex = i; 1208 break; 1209 } 1210 } 1211 if (endIndex == -1) return scaling[n - 1]; 1212 float[] scaling = this.scaling; 1213 int startIndex = endIndex - 1; 1214 float startValue = scaling[startIndex]; 1215 float startTime = timeline[startIndex]; 1216 return startValue + (scaling[endIndex] - startValue) * ((percent - startTime) / (timeline[endIndex] - startTime)); 1217 } 1218 1219 public void save (Writer output) throws IOException { 1220 super.save(output); 1221 if (!active) return; 1222 output.write("highMin: " + highMin + "\n"); 1223 output.write("highMax: " + highMax + "\n"); 1224 output.write("relative: " + relative + "\n"); 1225 output.write("scalingCount: " + scaling.length + "\n"); 1226 for (int i = 0; i < scaling.length; i++) 1227 output.write("scaling" + i + ": " + scaling[i] + "\n"); 1228 output.write("timelineCount: " + timeline.length + "\n"); 1229 for (int i = 0; i < timeline.length; i++) 1230 output.write("timeline" + i + ": " + timeline[i] + "\n"); 1231 } 1232 1233 public void load (BufferedReader reader) throws IOException { 1234 super.load(reader); 1235 if (!active) return; 1236 highMin = readFloat(reader, "highMin"); 1237 highMax = readFloat(reader, "highMax"); 1238 relative = readBoolean(reader, "relative"); 1239 scaling = new float[readInt(reader, "scalingCount")]; 1240 for (int i = 0; i < scaling.length; i++) 1241 scaling[i] = readFloat(reader, "scaling" + i); 1242 timeline = new float[readInt(reader, "timelineCount")]; 1243 for (int i = 0; i < timeline.length; i++) 1244 timeline[i] = readFloat(reader, "timeline" + i); 1245 } 1246 1247 public void load (ScaledNumericValue value) { 1248 super.load(value); 1249 highMax = value.highMax; 1250 highMin = value.highMin; 1251 scaling = new float[value.scaling.length]; 1252 System.arraycopy(value.scaling, 0, scaling, 0, scaling.length); 1253 timeline = new float[value.timeline.length]; 1254 System.arraycopy(value.timeline, 0, timeline, 0, timeline.length); 1255 relative = value.relative; 1256 } 1257 } 1258 1259 static public class GradientColorValue extends ParticleValue { 1260 static private float[] temp = new float[4]; 1261 1262 private float[] colors = {1, 1, 1}; 1263 float[] timeline = {0}; 1264 1265 public GradientColorValue () { 1266 alwaysActive = true; 1267 } 1268 1269 public float[] getTimeline () { 1270 return timeline; 1271 } 1272 1273 public void setTimeline (float[] timeline) { 1274 this.timeline = timeline; 1275 } 1276 1277 /** @return the r, g and b values for every timeline position */ 1278 public float[] getColors () { 1279 return colors; 1280 } 1281 1282 /** @param colors the r, g and b values for every timeline position */ 1283 public void setColors (float[] colors) { 1284 this.colors = colors; 1285 } 1286 1287 public float[] getColor (float percent) { 1288 int startIndex = 0, endIndex = -1; 1289 float[] timeline = this.timeline; 1290 int n = timeline.length; 1291 for (int i = 1; i < n; i++) { 1292 float t = timeline[i]; 1293 if (t > percent) { 1294 endIndex = i; 1295 break; 1296 } 1297 startIndex = i; 1298 } 1299 float startTime = timeline[startIndex]; 1300 startIndex *= 3; 1301 float r1 = colors[startIndex]; 1302 float g1 = colors[startIndex + 1]; 1303 float b1 = colors[startIndex + 2]; 1304 if (endIndex == -1) { 1305 temp[0] = r1; 1306 temp[1] = g1; 1307 temp[2] = b1; 1308 return temp; 1309 } 1310 float factor = (percent - startTime) / (timeline[endIndex] - startTime); 1311 endIndex *= 3; 1312 temp[0] = r1 + (colors[endIndex] - r1) * factor; 1313 temp[1] = g1 + (colors[endIndex + 1] - g1) * factor; 1314 temp[2] = b1 + (colors[endIndex + 2] - b1) * factor; 1315 return temp; 1316 } 1317 1318 public void save (Writer output) throws IOException { 1319 super.save(output); 1320 if (!active) return; 1321 output.write("colorsCount: " + colors.length + "\n"); 1322 for (int i = 0; i < colors.length; i++) 1323 output.write("colors" + i + ": " + colors[i] + "\n"); 1324 output.write("timelineCount: " + timeline.length + "\n"); 1325 for (int i = 0; i < timeline.length; i++) 1326 output.write("timeline" + i + ": " + timeline[i] + "\n"); 1327 } 1328 1329 public void load (BufferedReader reader) throws IOException { 1330 super.load(reader); 1331 if (!active) return; 1332 colors = new float[readInt(reader, "colorsCount")]; 1333 for (int i = 0; i < colors.length; i++) 1334 colors[i] = readFloat(reader, "colors" + i); 1335 timeline = new float[readInt(reader, "timelineCount")]; 1336 for (int i = 0; i < timeline.length; i++) 1337 timeline[i] = readFloat(reader, "timeline" + i); 1338 } 1339 1340 public void load (GradientColorValue value) { 1341 super.load(value); 1342 colors = new float[value.colors.length]; 1343 System.arraycopy(value.colors, 0, colors, 0, colors.length); 1344 timeline = new float[value.timeline.length]; 1345 System.arraycopy(value.timeline, 0, timeline, 0, timeline.length); 1346 } 1347 } 1348 1349 static public class SpawnShapeValue extends ParticleValue { 1350 SpawnShape shape = SpawnShape.point; 1351 boolean edges; 1352 SpawnEllipseSide side = SpawnEllipseSide.both; 1353 1354 public SpawnShape getShape () { 1355 return shape; 1356 } 1357 1358 public void setShape (SpawnShape shape) { 1359 this.shape = shape; 1360 } 1361 1362 public boolean isEdges () { 1363 return edges; 1364 } 1365 1366 public void setEdges (boolean edges) { 1367 this.edges = edges; 1368 } 1369 1370 public SpawnEllipseSide getSide () { 1371 return side; 1372 } 1373 1374 public void setSide (SpawnEllipseSide side) { 1375 this.side = side; 1376 } 1377 1378 public void save (Writer output) throws IOException { 1379 super.save(output); 1380 if (!active) return; 1381 output.write("shape: " + shape + "\n"); 1382 if (shape == SpawnShape.ellipse) { 1383 output.write("edges: " + edges + "\n"); 1384 output.write("side: " + side + "\n"); 1385 } 1386 } 1387 1388 public void load (BufferedReader reader) throws IOException { 1389 super.load(reader); 1390 if (!active) return; 1391 shape = SpawnShape.valueOf(readString(reader, "shape")); 1392 if (shape == SpawnShape.ellipse) { 1393 edges = readBoolean(reader, "edges"); 1394 side = SpawnEllipseSide.valueOf(readString(reader, "side")); 1395 } 1396 } 1397 1398 public void load (SpawnShapeValue value) { 1399 super.load(value); 1400 shape = value.shape; 1401 edges = value.edges; 1402 side = value.side; 1403 } 1404 } 1405 1406 static public enum SpawnShape { 1407 point, line, square, ellipse 1408 } 1409 1410 static public enum SpawnEllipseSide { 1411 both, top, bottom 1412 } 1413 } 1414