Browse Source

Merge pull request #1152 from sgrenier/next

Next-sgrenier
Steve Grenier 12 years ago
parent
commit
e164e4121e

+ 5 - 2
gameplay/src/AbsoluteLayout.cpp

@@ -47,8 +47,11 @@ void AbsoluteLayout::update(const Container* container, const Vector2& offset)
         Control* control = controls[i];
         GP_ASSERT(control);
 
-        align(control, container);
-        control->update(container, offset);
+        if (control->isVisible())
+        {
+            align(control, container);
+            control->update(container, offset);
+        }
     }
 }
 

+ 3 - 0
gameplay/src/FlowLayout.cpp

@@ -57,6 +57,9 @@ void FlowLayout::update(const Container* container, const Vector2& offset)
         Control* control = controls.at(i);
         GP_ASSERT(control);
 
+        if (!control->isVisible())
+            continue;
+
         //align(control, container);
 
         const Rectangle& bounds = control->getBounds();

+ 8 - 0
gameplay/src/Font.cpp

@@ -154,6 +154,14 @@ unsigned int Font::getSize()
 void Font::start()
 {
     GP_ASSERT(_batch);
+
+    // Update the projection matrix for our batch to match the current viewport
+    const Rectangle& vp = Game::getInstance()->getViewport();
+    Game* game = Game::getInstance();
+    Matrix projectionMatrix;
+    Matrix::createOrthographicOffCenter(vp.x, vp.width, vp.height, vp.y, 0, 1, &projectionMatrix);
+    _batch->setProjectionMatrix(projectionMatrix);
+
     _batch->start();
 }
 

+ 4 - 0
gameplay/src/Form.cpp

@@ -685,6 +685,10 @@ static bool shouldPropagateMouseEvent(Control::State state, Mouse::MouseEvent ev
 
 bool Form::mouseEventInternal(Mouse::MouseEvent evt, int x, int y, int wheelDelta)
 {
+    // Do not process mouse input when mouse is captured
+    if (Game::getInstance()->isMouseCaptured())
+        return false;
+
     for (size_t i = 0; i < __forms.size(); ++i)
     {
         Form* form = __forms[i];

+ 6 - 0
gameplay/src/Node.cpp

@@ -1037,6 +1037,12 @@ void Node::cloneInto(Node* node, NodeCloneContext &context) const
         node->setModel(modelClone);
         modelClone->release();
     }
+    if (ParticleEmitter* emitter = getParticleEmitter())
+    {
+        ParticleEmitter* emitterClone = emitter->clone();
+        node->setParticleEmitter(emitterClone);
+        emitterClone->release();
+    }
     node->_world = _world;
     node->_bounds = _bounds;
 

+ 166 - 59
gameplay/src/ParticleEmitter.cpp

@@ -9,11 +9,12 @@
 #define PARTICLE_COUNT_MAX                       100
 #define PARTICLE_EMISSION_RATE                   10
 #define PARTICLE_EMISSION_RATE_TIME_INTERVAL     1000.0f / (float)PARTICLE_EMISSION_RATE
+#define PARTICLE_UPDATE_RATE_MAX                 16
 
 namespace gameplay
 {
 
-ParticleEmitter::ParticleEmitter(SpriteBatch* batch, unsigned int particleCountMax) :
+ParticleEmitter::ParticleEmitter(unsigned int particleCountMax) :
     _particleCountMax(particleCountMax), _particleCount(0), _particles(NULL),
     _emissionRate(PARTICLE_EMISSION_RATE), _started(false), _ellipsoid(false),
     _sizeStartMin(1.0f), _sizeStartMax(1.0f), _sizeEndMin(1.0f), _sizeEndMax(1.0f),
@@ -25,18 +26,13 @@ ParticleEmitter::ParticleEmitter(SpriteBatch* batch, unsigned int particleCountM
     _rotationPerParticleSpeedMin(0.0f), _rotationPerParticleSpeedMax(0.0f),
     _rotationSpeedMin(0.0f), _rotationSpeedMax(0.0f),
     _rotationAxis(Vector3::zero()), _rotation(Matrix::identity()),
-    _spriteBatch(batch), _spriteTextureBlending(BLEND_TRANSPARENT),  _spriteTextureWidth(0), _spriteTextureHeight(0), _spriteTextureWidthRatio(0), _spriteTextureHeightRatio(0), _spriteTextureCoords(NULL),
+    _spriteBatch(NULL), _spriteTextureBlending(BLEND_TRANSPARENT),  _spriteTextureWidth(0), _spriteTextureHeight(0), _spriteTextureWidthRatio(0), _spriteTextureHeightRatio(0), _spriteTextureCoords(NULL),
     _spriteAnimated(false),  _spriteLooped(false), _spriteFrameCount(1), _spriteFrameRandomOffset(0),_spriteFrameDuration(0L), _spriteFrameDurationSecs(0.0f), _spritePercentPerFrame(0.0f),
     _node(NULL), _orbitPosition(false), _orbitVelocity(false), _orbitAcceleration(false),
-    _timePerEmission(PARTICLE_EMISSION_RATE_TIME_INTERVAL), _timeRunning(0)
+    _timePerEmission(PARTICLE_EMISSION_RATE_TIME_INTERVAL), _emitTime(0), _lastUpdated(0)
 {
     GP_ASSERT(particleCountMax);
     _particles = new Particle[particleCountMax];
-
-    GP_ASSERT(_spriteBatch);
-    GP_ASSERT(_spriteBatch->getStateBlock());
-    _spriteBatch->getStateBlock()->setDepthWrite(false);
-    _spriteBatch->getStateBlock()->setDepthTest(true);
 }
 
 ParticleEmitter::~ParticleEmitter()
@@ -48,7 +44,7 @@ ParticleEmitter::~ParticleEmitter()
 
 ParticleEmitter* ParticleEmitter::create(const char* textureFile, TextureBlending textureBlending, unsigned int particleCountMax)
 {
-    Texture* texture = Texture::create(textureFile, false);
+    Texture* texture = Texture::create(textureFile, true);
 
     if (!texture)
     {
@@ -58,23 +54,15 @@ ParticleEmitter* ParticleEmitter::create(const char* textureFile, TextureBlendin
     GP_ASSERT(texture->getWidth());
     GP_ASSERT(texture->getHeight());
 
-    // Use default SpriteBatch material.
-    SpriteBatch* batch =  SpriteBatch::create(texture, NULL, particleCountMax);
-    texture->release(); // batch owns the texture.
-    GP_ASSERT(batch);
+    return ParticleEmitter::create(texture, textureBlending, particleCountMax);
+}
 
-    ParticleEmitter* emitter = new ParticleEmitter(batch, particleCountMax);
+ParticleEmitter* ParticleEmitter::create(Texture* texture, TextureBlending textureBlending,  unsigned int particleCountMax)
+{
+    ParticleEmitter* emitter = new ParticleEmitter(particleCountMax);
     GP_ASSERT(emitter);
 
-    // By default assume only one frame which uses the entire texture.
-    emitter->setTextureBlending(textureBlending);
-    emitter->_spriteTextureWidth = texture->getWidth();
-    emitter->_spriteTextureHeight = texture->getHeight();
-    emitter->_spriteTextureWidthRatio = 1.0f / (float)texture->getWidth();
-    emitter->_spriteTextureHeightRatio = 1.0f / (float)texture->getHeight();
-
-    Rectangle texCoord((float)texture->getWidth(), (float)texture->getHeight());
-    emitter->setSpriteFrameCoords(1, &texCoord);
+    emitter->setTexture(texture, textureBlending);
 
     return emitter;
 }
@@ -212,6 +200,61 @@ ParticleEmitter* ParticleEmitter::create(Properties* properties)
     return emitter;
 }
 
+void ParticleEmitter::setTexture(const char* texturePath, TextureBlending textureBlending)
+{
+    Texture* texture = Texture::create(texturePath, true);
+    if (texture)
+    {
+        setTexture(texture, textureBlending);
+        texture->release();
+    }
+    else
+    {
+        GP_WARN("Failed set new texture on particle emitter: %s", texturePath);
+    }
+}
+
+void ParticleEmitter::setTexture(Texture* texture, TextureBlending textureBlending)
+{
+    // Create new batch before releasing old one, in case the same texture
+    // is used for both (so it's not released before passing to the new batch).
+    SpriteBatch* batch =  SpriteBatch::create(texture, NULL, _particleCountMax);
+    batch->getSampler()->setFilterMode(Texture::LINEAR_MIPMAP_LINEAR, Texture::LINEAR);
+
+    // Free existing batch
+    SAFE_DELETE(_spriteBatch);
+
+    _spriteBatch = batch;
+    _spriteBatch->getStateBlock()->setDepthWrite(false);
+    _spriteBatch->getStateBlock()->setDepthTest(true);
+
+    setTextureBlending(textureBlending);
+    _spriteTextureWidth = texture->getWidth();
+    _spriteTextureHeight = texture->getHeight();
+    _spriteTextureWidthRatio = 1.0f / (float)texture->getWidth();
+    _spriteTextureHeightRatio = 1.0f / (float)texture->getHeight();
+
+    // By default assume only one frame which uses the entire texture.
+    Rectangle texCoord((float)texture->getWidth(), (float)texture->getHeight());
+    setSpriteFrameCoords(1, &texCoord);
+}
+
+Texture* ParticleEmitter::getTexture() const
+{
+    Texture::Sampler* sampler = _spriteBatch ? _spriteBatch->getSampler() : NULL;
+    return sampler? sampler->getTexture() : NULL;
+}
+
+void ParticleEmitter::setParticleCountMax(unsigned int max)
+{
+    _particleCountMax = max;
+}
+
+unsigned int ParticleEmitter::getParticleCountMax() const
+{
+    return _particleCountMax;
+}
+
 unsigned int ParticleEmitter::getEmissionRate() const
 {
     return _emissionRate;
@@ -227,6 +270,7 @@ void ParticleEmitter::setEmissionRate(unsigned int rate)
 void ParticleEmitter::start()
 {
     _started = true;
+    _lastUpdated = 0;
 }
 
 void ParticleEmitter::stop()
@@ -247,18 +291,7 @@ bool ParticleEmitter::isActive() const
     if (!_node)
         return false;
 
-    GP_ASSERT(_particles);
-    bool active = false;
-    for (unsigned int i = 0; i < _particleCount; i++)
-    {
-        if (_particles[i]._energy > 0)
-        {
-            active = true;
-            break;
-        }
-    }
-
-    return active;
+    return (_particleCount > 0);
 }
 
 void ParticleEmitter::emitOnce(unsigned int particleCount)
@@ -285,7 +318,6 @@ void ParticleEmitter::emitOnce(unsigned int particleCount)
     for (unsigned int i = 0; i < particleCount; i++)
     {
         Particle* p = &_particles[_particleCount];
-        p->_visible = true;
 
         generateColor(_colorStart, _colorStartVar, &p->_colorStart);
         generateColor(_colorEnd, _colorEndVar, &p->_colorEnd);
@@ -553,6 +585,13 @@ void ParticleEmitter::setTextureBlending(TextureBlending textureBlending)
             GP_ERROR("Unsupported texture blending mode (%d).", textureBlending);
             break;
     }
+
+    _spriteTextureBlending = textureBlending;
+}
+
+ParticleEmitter::TextureBlending ParticleEmitter::getTextureBlending() const
+{
+    return _spriteTextureBlending;
 }
 
 void ParticleEmitter::setSpriteAnimated(bool animated)
@@ -597,6 +636,16 @@ long ParticleEmitter::getSpriteFrameDuration() const
     return _spriteFrameDuration;
 }
 
+unsigned int ParticleEmitter::getSpriteWidth() const
+{
+    return (unsigned int)fabs(_spriteTextureWidth * (_spriteTextureCoords[2] - _spriteTextureCoords[0]));
+}
+
+unsigned int ParticleEmitter::getSpriteHeight() const
+{
+    return (unsigned int)fabs(_spriteTextureHeight * (_spriteTextureCoords[3] - _spriteTextureCoords[1]));
+}
+
 void ParticleEmitter::setSpriteTexCoords(unsigned int frameCount, float* texCoords)
 {
     GP_ASSERT(frameCount);
@@ -667,6 +716,11 @@ void ParticleEmitter::setSpriteFrameCoords(unsigned int frameCount, int width, i
     SAFE_DELETE_ARRAY(frameCoords);
 }
 
+unsigned int ParticleEmitter::getSpriteFrameCount() const
+{
+    return _spriteFrameCount;
+}
+
 Node* ParticleEmitter::getNode() const
 {
     return _node;
@@ -685,6 +739,21 @@ void ParticleEmitter::setOrbit(bool orbitPosition, bool orbitVelocity, bool orbi
     _orbitAcceleration = orbitAcceleration;
 }
 
+bool ParticleEmitter::getOrbitPosition() const
+{
+    return _orbitPosition;
+}
+
+bool ParticleEmitter::getOrbitVelocity() const
+{
+    return _orbitVelocity;
+}
+
+bool ParticleEmitter::getOrbitAcceleration() const
+{
+    return _orbitAcceleration;
+}
+
 long ParticleEmitter::generateScalar(long min, long max)
 {
     // Note: this is not a very good RNG, but it should be suitable for our purposes.
@@ -792,41 +861,46 @@ ParticleEmitter::TextureBlending ParticleEmitter::getTextureBlendingFromString(c
 void ParticleEmitter::update(float elapsedTime)
 {
     if (!isActive())
-    {
         return;
-    }
 
-    // Calculate the time passed since last update.
-    float elapsedSecs = elapsedTime * 0.001f;
+    // Cap particle updates at a maximum rate. This saves processing
+    // and also improves precision since updating with very small
+    // time increments is more lossy.
+    static double runningTime = 0;
+    runningTime += elapsedTime;
+    if (runningTime < PARTICLE_UPDATE_RATE_MAX)
+        return;    
+
+    float elapsedMs = runningTime;
+    runningTime = 0;
+
+    float elapsedSecs = elapsedMs * 0.001f;
 
     if (_started && _emissionRate)
     {
         // Calculate how much time has passed since we last emitted particles.
-        _timeRunning += elapsedTime;
+        _emitTime += elapsedMs; //+= elapsedTime;
 
         // How many particles should we emit this frame?
         GP_ASSERT(_timePerEmission);
-        unsigned int emitCount = (unsigned int)(_timeRunning / _timePerEmission);
+        unsigned int emitCount = (unsigned int)(_emitTime / _timePerEmission);
 
         if (emitCount)
         {
             if ((int)_timePerEmission > 0)
             {
-                _timeRunning = fmod(_timeRunning, (double)_timePerEmission);
+                _emitTime = fmod(_emitTime, (double)_timePerEmission);
             }
             emitOnce(emitCount);
         }
     }
 
-    GP_ASSERT(_node && _node->getScene() && _node->getScene()->getActiveCamera());
-    const Frustum& frustum = _node->getScene()->getActiveCamera()->getFrustum();
-
     // Now update all currently living particles.
     GP_ASSERT(_particles);
     for (unsigned int particlesIndex = 0; particlesIndex < _particleCount; ++particlesIndex)
     {
         Particle* p = &_particles[particlesIndex];
-        p->_energy -= elapsedTime;
+        p->_energy -= elapsedMs;
 
         if (p->_energy > 0L)
         {
@@ -847,12 +921,6 @@ void ParticleEmitter::update(float elapsedTime)
             p->_position.y += p->_velocity.y * elapsedSecs;
             p->_position.z += p->_velocity.z * elapsedSecs;
 
-            if (!frustum.intersects(p->_position))
-            {
-                p->_visible = false;
-                continue;
-            }
-
             p->_angle += p->_rotationPerParticleSpeed * elapsedSecs;
 
             // Simple linear interpolation of color and size.
@@ -951,12 +1019,9 @@ void ParticleEmitter::draw()
         {
             Particle* p = &_particles[i];
 
-            if (p->_visible)
-            {
-                _spriteBatch->draw(p->_position, right, up, p->_size, p->_size,
-                                   _spriteTextureCoords[p->_frame * 4], _spriteTextureCoords[p->_frame * 4 + 1], _spriteTextureCoords[p->_frame * 4 + 2], _spriteTextureCoords[p->_frame * 4 + 3],
-                                   p->_color, pivot, p->_angle);
-            }
+            _spriteBatch->draw(p->_position, right, up, p->_size, p->_size,
+                                _spriteTextureCoords[p->_frame * 4], _spriteTextureCoords[p->_frame * 4 + 1], _spriteTextureCoords[p->_frame * 4 + 2], _spriteTextureCoords[p->_frame * 4 + 3],
+                                p->_color, pivot, p->_angle);
         }
 
         // Render.
@@ -964,4 +1029,46 @@ void ParticleEmitter::draw()
     }
 }
 
+ParticleEmitter* ParticleEmitter::clone()
+{
+    // Create a clone of this emitter
+    ParticleEmitter* emitter = ParticleEmitter::create(_spriteBatch->getSampler()->getTexture(), _spriteTextureBlending, _particleCountMax);
+
+    // Copy all properties to the clone
+    emitter->setEmissionRate(_emissionRate);
+    emitter->_ellipsoid = _ellipsoid;
+    emitter->_sizeStartMin = _sizeStartMin;
+    emitter->_sizeStartMax = _sizeStartMax;
+    emitter->_sizeEndMin = _sizeEndMin;
+    emitter->_sizeEndMax = _sizeEndMax;
+    emitter->_energyMin = _energyMin;
+    emitter->_energyMax = _energyMax;
+    emitter->_colorStart = _colorStart;
+    emitter->_colorStartVar = _colorStartVar;
+    emitter->_colorEnd = _colorEnd;
+    emitter->_colorEndVar = _colorEndVar;
+    emitter->_position = _position;
+    emitter->_positionVar = _positionVar;
+    emitter->_velocity = _velocity;
+    emitter->_velocityVar = _velocityVar;
+    emitter->_acceleration = _acceleration;
+    emitter->_accelerationVar = _accelerationVar;
+    emitter->_rotationPerParticleSpeedMin = _rotationPerParticleSpeedMin;
+    emitter->_rotationPerParticleSpeedMax = _rotationPerParticleSpeedMax;
+    emitter->_rotationSpeedMin = _rotationSpeedMin;
+    emitter->_rotationSpeedMax = _rotationSpeedMax;
+    emitter->_rotationAxis = _rotationAxis;
+    emitter->_rotationAxisVar = _rotationAxisVar;
+    emitter->setSpriteTexCoords(_spriteFrameCount, _spriteTextureCoords);
+    emitter->_spriteAnimated = _spriteAnimated;
+    emitter->_spriteLooped = _spriteLooped;
+    emitter->_spriteFrameRandomOffset = _spriteFrameRandomOffset;
+    emitter->setSpriteFrameDuration(_spriteFrameDuration);
+    emitter->_orbitPosition = _orbitPosition;
+    emitter->_orbitVelocity = _orbitVelocity;
+    emitter->_orbitAcceleration = _orbitAcceleration;
+
+    return emitter;
+}
+
 }

+ 114 - 4
gameplay/src/ParticleEmitter.h

@@ -185,6 +185,48 @@ public:
      */
     static ParticleEmitter* create(const char* texturePath, TextureBlending textureBlending,  unsigned int particleCountMax);
 
+    /**
+     * Sets a new texture for this particle emitter.
+     *
+     * The current texture's reference count is decreased.
+     *
+     * @param texturePath Path to the new texture to set.
+     * @param textureBlending Blending mode for the new texture.
+     */
+    void setTexture(const char* texturePath, TextureBlending textureBlending);
+
+    /**
+     * Sets a new texture for this particle emitter.
+     *
+     * The reference count of the specified texture is increased, and the 
+     * current texture's reference count is decreased.
+     *
+     * @param The new texture to set.
+     * @param textureBlending Blending mode for the new texture.
+     */
+    void setTexture(Texture* texture, TextureBlending textureBlending);
+
+    /**
+     * Returns the texture currently set for this particle emitter.
+     *
+     * @return The current texture.
+     */
+    Texture* getTexture() const;
+
+    /**
+     * Sets the maximum number of particles that can be emitted.
+     *
+     * @param max The maximum number of particles that can be emitted.
+     */
+    void setParticleCountMax(unsigned int max);
+
+    /**
+     * Returns the maximum number of particles that can be emitted.
+     *
+     * @return The maximum number of particles that can be emitted.
+     */
+    unsigned int getParticleCountMax() const;
+
     /**
      * Sets the emission rate, measured in particles per second.
      *
@@ -559,6 +601,20 @@ public:
      */
     long getSpriteFrameDuration() const;
 
+    /**
+     * Returns the width of the first frame this particle emitter's sprite.
+     *
+     * @return The width of the first frame of the sprite.
+     */
+    unsigned int getSpriteWidth() const;
+
+    /**
+     * Returns the height of the first frame this particle emitter's sprite.
+     *
+     * @return The height of the first frame of the sprite.
+     */
+    unsigned int getSpriteHeight() const;
+
     /**
      * Sets the sprite's texture coordinates in texture space.
      *
@@ -589,6 +645,13 @@ public:
      */
     void setSpriteFrameCoords(unsigned int frameCount, int width, int height);
 
+    /**
+     * Returns the current number of frames for the particle emitter's sprite.
+     *
+     * @return The current frame count.
+     */
+    unsigned int getSpriteFrameCount() const;
+
     /**
      * Gets the node that this emitter is attached to.
      *
@@ -606,6 +669,27 @@ public:
      */
     void setOrbit(bool orbitPosition, bool orbitVelocity, bool orbitAcceleration);
 
+    /**
+     * Whether new particle positions are rotated by the node's rotation matrix.
+     *
+     * @return True if orbiting positions, false otherwise.
+     */
+    bool getOrbitPosition() const;
+
+    /**
+     * Whether new particle velocities are rotated by the node's rotation matrix.
+     *
+     * @return True if orbiting velocities, false otherwise.
+     */
+    bool getOrbitVelocity() const;
+
+    /**
+     * Whether new particle accelerations are rotated by the node's rotation matrix.
+     *
+     * @return True if orbiting accelerations, false otherwise.
+     */
+    bool getOrbitAcceleration() const;
+
     /**
      * Updates the particles currently being emitted.
      *
@@ -624,22 +708,48 @@ public:
     static TextureBlending getTextureBlendingFromString(const char* src);
 
     /**
-     * Sets a TextureBlending enum from a corresponding string.
+     * Sets the texture blend mode for this particle emitter.
+     *
+     * @param blending The new blend mode.
      */
     void setTextureBlending(TextureBlending blending);
 
+    /**
+     * Gets the current texture blend mode for this particle emitter.
+     *
+     * @return The current blend mode.
+     */
+    TextureBlending getTextureBlending() const;
+
+    /**
+     * Clones the particle emitter and returns a new emitter.
+     * 
+     * @return The new cloned particle emitter.
+     */
+    ParticleEmitter* clone();
+
 private:
 
     /**
      * Constructor.
      */
-    ParticleEmitter(SpriteBatch* batch, unsigned int particlesCount);
+    ParticleEmitter(unsigned int particlesCount);
 
     /**
      * Destructor.
      */
     ~ParticleEmitter();
 
+    /**
+     * Creates an uninitialized ParticleEmitter.
+     *
+     * @param texture the texture to use.
+     * @param textureBlending The type of texture blending to be used for the particles emitted.
+     * @param particleCountMax The maximum number of particles that can be alive at one time in this ParticleEmitter's system.
+     * @script{create}
+     */
+    static ParticleEmitter* create(Texture* texture, TextureBlending textureBlending,  unsigned int particleCountMax);
+
     /**
      * Hidden copy assignment operator.
      */
@@ -690,7 +800,6 @@ private:
         float _size;
         unsigned int _frame;
         float _timeOnCurrentFrame;
-        bool _visible;
     };
 
     unsigned int _particleCountMax;
@@ -741,7 +850,8 @@ private:
     bool _orbitVelocity;
     bool _orbitAcceleration;
     float _timePerEmission;
-    double _timeRunning;
+    float _emitTime;
+    double _lastUpdated;
 };
 
 }

+ 5 - 1
gameplay/src/RadioButton.cpp

@@ -66,11 +66,15 @@ bool RadioButton::isSelected() const
 
 void RadioButton::setSelected(bool selected)
 {
+    if (selected)
+        RadioButton::clearSelected(_groupId);
+
     if (selected != _selected)
     {
+        _selected = selected;
         _dirty = true;
+        notifyListeners(Control::Listener::VALUE_CHANGED);
     }
-    _selected = selected;
 }
 
 void RadioButton::setImageSize(float width, float height)

+ 0 - 10
gameplay/src/Theme.cpp

@@ -488,16 +488,6 @@ void Theme::setProjectionMatrix(const Matrix& matrix)
 {
     GP_ASSERT(_spriteBatch);
     _spriteBatch->setProjectionMatrix(matrix);
-
-    // Set the matrix on each Font used by the style.
-    std::set<Font*>::const_iterator it;
-    for (it = _fonts.begin(); it != _fonts.end(); ++it)
-    {
-        Font* font = *it;
-        GP_ASSERT(font);
-        GP_ASSERT(font->getSpriteBatch());
-        font->getSpriteBatch()->setProjectionMatrix(matrix);
-    }
 }
 
 SpriteBatch* Theme::getSpriteBatch() const

+ 10 - 7
gameplay/src/VerticalLayout.cpp

@@ -63,17 +63,20 @@ void VerticalLayout::update(const Container* container, const Vector2& offset)
         Control* control = controls.at(i);
         GP_ASSERT(control);
 
-        align(control, container);
+        if (control->isVisible())
+        {
+            align(control, container);
 
-        const Rectangle& bounds = control->getBounds();
-        const Theme::Margin& margin = control->getMargin();
+            const Rectangle& bounds = control->getBounds();
+            const Theme::Margin& margin = control->getMargin();
 
-        yPosition += margin.top;
+            yPosition += margin.top;
 
-        control->setPosition(margin.left, yPosition);
-        control->update(container, offset);
+            control->setPosition(margin.left, yPosition);
+            control->update(container, offset);
 
-        yPosition += bounds.height + margin.bottom;
+            yPosition += bounds.height + margin.bottom;
+        }
 
         i += iter;
     }

+ 1 - 0
samples/particles/game.config

@@ -4,4 +4,5 @@ window
     width = 1280
     height = 720
     fullscreen = false
+    resizable = true
 }

+ 263 - 120
samples/particles/res/editor.form

@@ -4,127 +4,270 @@ form particleEditor
     autoWidth = true
     autoHeight = true
 
-    container presets
-    {
-        style = basic
-        layout = LAYOUT_VERTICAL
-        position = 0, 0
-        size = 160, 220
-        consumeInputEvents = true
-
-        label title
-        {
-            style = title
-            size = 160, 30
-            text = Presets
-        }
-
-        radioButton spiralFlame
-        {
-           style = iconNoBorder
-           text = Fire
-           group = presets
-           size = 160, 40
-           imageSize = 35, 35
-           selected = true
-        }
-
-        radioButton smoke : spiralFlame
-        {
-            text = Smoke
-            selected = false
-        }
-
-        radioButton explosion : smoke
-        {
-            text = Explosion
-        }
-
-        button reset
-        {
-            style = buttonStyle
-            alignment = ALIGN_BOTTOM
-            autoWidth = true
-            height = 50
-            text = Reset
-        }
-    }
-
-    // Emission settings
-    container emission
-    {
-        style = basic
-        position = 0, 220
-        layout = LAYOUT_VERTICAL
-        size = 160, 210
-        consumeInputEvents = true
-
-        // Burst emission
-        button emit
-        {
-            style = buttonStyle
-            position = 0, 50
-            size = 140, 50
-            text = Emit
-        }
-
-        // Emission rate
-        slider emissionRate
-        {
-            style = noBorder
-            size = 140, 50
-            orientation = HORIZONTAL
-            min = 1
-            max = 500
-            value = 100
-            step = 0
-            text = Emission Rate
-            textAlignment = ALIGN_TOP_HCENTER
-            valueTextVisible = true
-            valueTextAlignment = ALIGN_BOTTOM_HCENTER
-            valueTextPrecision = 2            
-        }
-
-        slider burstSize : emissionRate
-        {
-            text = Burst Size
-            value = 20
-            max = 50
-            step = 1
-        }
-
-        // Start / Stop Emitter
-        checkBox started
-        {
-            style = iconNoBorder
-            size = 140, 40
-            imageSize = 35, 35
-            text = Running
-            checked = true
-        }
-    }
-
-    // Camera Zoom
-    container zoom
-    {
+	// Vsync checkbox
+	checkBox vsync
+	{
+		style = iconNoBorder
+		size = 100, 35
+		position = 165, 5
+		imageSize = 30, 30
+		text = VSYNC
+		checked = true
+		consumeInputEvents = true
+	}
+
+	// Saving/loading
+	container buttons
+	{
+		style = noBorder
+		size = 200, 50
+        position = 163, 100
+		consumeInputEvents = false
+        alignment = ALIGN_TOP_HCENTER
+
+		button save
+		{
+            consumeInputEvents = true
+			style = buttonStyle
+            width = 100
+			height = 50
+			text = Save
+		}
+
+        button load : save
+		{
+            x = 100
+			text = Load
+		}
+	}
+
+	container leftSide
+	{
         style = noBorder
-        size = 160, 50
-        position = 0, 430
-        consumeInputEvents = true
-
-        button zoomIn
-        {
-            style = buttonStyle
-            size = 80, 50
-            text = Zoom  In
-        }
-
-        button zoomOut : zoomIn
-        {
-            position = 80, 0
-            text = Zoom Out
-        }
-    }
+		width = 160
+		autoHeight = true
+		layout = LAYOUT_VERTICAL
+
+		button reset
+		{
+            consumeInputEvents = true
+			style = buttonStyle
+            width = 160
+			height = 50
+			text = Reset
+		}
+
+		// Emission settings
+		container emission
+		{
+			style = basic
+			layout = LAYOUT_VERTICAL
+			size = 160, 210
+			consumeInputEvents = true
+
+			// Burst emission
+			button emit
+			{
+				style = buttonStyle
+				position = 0, 50
+				size = 140, 50
+				text = Emit
+			}
+
+			// Emission rate
+			slider emissionRate
+			{
+				style = noBorder
+				size = 140, 50
+				orientation = HORIZONTAL
+				min = 1
+				max = 500
+				value = 100
+				step = 0
+				text = Emission Rate
+				textAlignment = ALIGN_TOP_HCENTER
+				valueTextVisible = true
+				valueTextAlignment = ALIGN_BOTTOM_HCENTER
+				valueTextPrecision = 2            
+			}
+
+			slider burstSize : emissionRate
+			{
+				text = Burst Size
+				value = 20
+				max = 50
+				step = 1
+			}
+
+			// Start / Stop Emitter
+			checkBox started
+			{
+				style = iconNoBorder
+				size = 140, 40
+				imageSize = 35, 35
+				text = Running
+				checked = true
+			}
+		}
+        
+        // Image settings
+		container image
+		{
+			style = basic
+			size = 160, 200
+			consumeInputEvents = true
+			layout = LAYOUT_VERTICAL
+
+            label
+            {
+                style = title
+                size = 150, 30
+                text = Image
+            }
+
+			image sprite
+			{
+				style = image
+				path = res/fire.png
+				size = 140, 140
+			}
+
+            container imageSettings
+            {
+                style = noBorder
+                size = 160, 220
+                consumeInputEvents = true
+			    layout = LAYOUT_VERTICAL
+
+                radioButton additive
+                {
+                    style = radio
+                    text = Additive
+                    group = blendMode
+                    size = 140, 25
+                    imageSize = 25, 25
+                    selected = true
+                }
+
+                radioButton transparent : additive
+                {
+                    text = Transparent
+                    selected = false
+                }
+
+                radioButton multiply : transparent
+                {
+                    text = Multiply
+                    selected = false
+                }
+
+                radioButton opaque : transparent
+                {
+                    text = Opaque
+                    selected = false
+                }
+
+                container
+                {
+                    style = noBorder
+                    autoWidth = true
+                    height = 30
+
+                    label
+                    {
+                        style = noBorder
+                        text = Frame Count:
+                        size = 100, 25
+                    }
+
+                    textBox frameCount
+                    {
+                        consumeInputEvents = true
+                        style = textBox
+                        text = 1
+                        size = 45, 22
+                        position = 100, 0
+                    }
+                }
+
+                container
+                {
+                    style = noBorder
+                    autoWidth = true
+                    height = 30
+
+                    label
+                    {
+                        style = noBorder
+                        text = Frame Width:
+                        size = 100, 25
+                    }
+
+                    textBox frameWidth
+                    {
+                        consumeInputEvents = true
+                        style = textBox
+                        text = 1
+                        size = 45, 22
+                        position = 100, 0
+                    }
+                }
+
+                container
+                {
+                    style = noBorder
+                    autoWidth = true
+                    height = 30
+
+                    label
+                    {
+                        style = noBorder
+                        text = Frame Height:
+                        size = 100, 25
+                    }
+
+                    textBox frameHeight
+                    {
+                        consumeInputEvents = true
+                        style = textBox
+                        text = 1
+                        size = 45, 22
+                        position = 100, 0
+                    }
+                }
+
+                button updateFrames
+                {
+				    style = buttonStyle
+				    width = 140
+				    height = 30
+                    fontSize = 16
+				    text = Update
+                }
+            }
+		}
+
+		// Camera Zoom
+		container zoom
+		{
+			style = noBorder
+			size = 160, 50
+			consumeInputEvents = true
+
+			button zoomIn
+			{
+				style = buttonStyle
+				size = 80, 50
+				text = Zoom  In
+			}
+
+			button zoomOut : zoomIn
+			{
+				position = 80, 0
+				text = Zoom Out
+			}
+		}
+	}
 
     container particleProperties
     {

+ 61 - 0
samples/particles/res/editor.theme

@@ -112,6 +112,24 @@ theme particleEditor
         color = #C3D9BFff
     }
 
+    skin formEntry
+    {
+        border
+        {
+            left = 6
+            right = 6
+            top = 2
+            bottom = 2
+        }
+        region = 20, 20, 10, 10
+        color = #4A8799ff
+    }
+
+	skin formFocus : formEntry
+	{
+        color = #C3D9BFff
+	}
+
     style basic
     {
         stateNormal
@@ -204,6 +222,49 @@ theme particleEditor
         }
     }
 
+    style image : noBorder
+    {
+        padding
+        {
+            bottom = 4
+        }
+    }
+
+    style radio : noBorder
+    {
+        stateNormal
+        {
+            font = res/arial.gpb
+            fontSize = 20
+            textAlignment = ALIGN_VCENTER_LEFT
+        }
+
+        stateActive
+        {
+            font = res/arial.gpb
+            fontSize = 20
+            textAlignment = ALIGN_VCENTER_LEFT
+        }
+    }
+
+    style textBox : basic
+    {
+        stateNormal
+        {
+            skin = formEntry
+            font = res/arial.gpb
+            fontSize = 16
+            textAlignment = ALIGN_TOP_LEFT
+        }
+
+        stateFocus
+        {
+            skin = formFocus
+            font = res/arial.gpb
+            fontSize = 16
+        }
+    }
+
     style title
     {
         padding

+ 1 - 1
samples/particles/res/explosion.particle

@@ -3,7 +3,7 @@ particle explosion
     // Sprite properties.
     sprite
     {
-        path = res/explosion.png
+        path = explosion.png
         width = 64
         height = 64
         frameCount = 16

+ 4 - 4
samples/particles/res/fire.particle

@@ -2,7 +2,7 @@ particle fire
 {
     sprite
     {
-		path = res/fire.png
+		path = fire.png
         width = 256
         height = 256
 	    blending = ADDITIVE
@@ -16,6 +16,9 @@ particle fire
     particleCountMax = 5000
     emissionRate = 300
     ellipsoid = true
+    orbitPosition = true
+    orbitVelocity = true
+    orbitAcceleration = false
     sizeStartMin = 1.5
     sizeStartMax = 2
     sizeEndMin = 0.5
@@ -33,7 +36,4 @@ particle fire
     accelerationVar = 3, 0.5, 3
     rotationPerParticleSpeedMin = -1.5
     rotationPerParticleSpeedMax = 1.5
-    orbitPosition = true
-    orbitVelocity = true
-    orbitAcceleration = false
 }

BIN
samples/particles/res/fire.png


+ 1 - 1
samples/particles/res/smoke.particle

@@ -2,7 +2,7 @@ particle chimney-smoke
 {
     sprite 
     {
-		path = res/smoke.png
+		path = smoke.png
         width = 64
         height = 64
         blending = ADDITIVE

+ 423 - 79
samples/particles/src/ParticlesGame.cpp

@@ -1,29 +1,29 @@
 #include "ParticlesGame.h"
+#ifdef WIN32
+#include <Windows.h>
+#include <Commdlg.h>
+#endif
 
 // Declare our game instance.
 ParticlesGame game;
 
-static const std::string _particleFiles[] = 
-{
-    "res/fire.particle",
-    "res/smoke.particle",
-    "res/explosion.particle",
-};
-
-const static unsigned int _particleFilesCount = 3;
-const static float PARTICLE_SIZE_MAX[] = { 5.0f, 30.0f, 30.0f };
-const static float EMIT_RATE_MAX[] = { 500, 100, 100 };
+#define DEFAULT_PARTICLE_EMITTER "res/fire.particle"
+
+const static float PARTICLE_SIZE_MAX = 30.0f; //5.0f, 30.0f, 30.0f;
+const static float EMIT_RATE_MAX = 500.0f; //500, 100, 100;;
 const float INPUT_SENSITIVITY = 0.05f;
+const float PANNING_SENSITIVITY = 0.05f;
+const float ROTATE_SENSITIVITY = 0.25f;
 const Vector4 BACKGROUND_COLOR = Vector4::zero();
 
-ParticlesGame::ParticlesGame() : _scene(NULL)
+ParticlesGame::ParticlesGame() : _scene(NULL), _panning(false), _rotating(false), _zooming(false)
 {
 }
 
 void ParticlesGame::initialize()
 {
-    // Display the gameplay splash screen for at least 1 second.
-    displayScreen(this, &ParticlesGame::drawSplash, NULL, 1000L);
+    // Display the gameplay splash screen
+    displayScreen(this, &ParticlesGame::drawSplash, NULL, 250L);
 
     setMultiTouch(true);
 
@@ -77,11 +77,10 @@ void ParticlesGame::initialize()
     _started = (CheckBox*)_form->getControl("started");
     _reset = (Button*)_form->getControl("reset");
     _emit = (Button*)_form->getControl("emit");
-    _spiralFlame = (RadioButton*)_form->getControl("spiralFlame");
-    _smoke = (RadioButton*)_form->getControl("smoke");
-    _explosion = (RadioButton*)_form->getControl("explosion");
     _zoomIn = (Button*)_form->getControl("zoomIn");
     _zoomOut = (Button*)_form->getControl("zoomOut");
+    _save = (Button*)_form->getControl("save");
+    _load = (Button*)_form->getControl("load");
     _burstSize = (Slider*)_form->getControl("burstSize");
     _posVarX = (Slider*)_form->getControl("posVarX");
     _posVarY = (Slider*)_form->getControl("posVarY");
@@ -108,6 +107,7 @@ void ParticlesGame::initialize()
     _axisVarZ = (Slider*)_form->getControl("axisVarZ");
     _rotationSpeedMin = (Slider*)_form->getControl("rotationSpeedMin");
     _rotationSpeedMax = (Slider*)_form->getControl("rotationSpeedMax");
+    _vsync = (CheckBox*)_form->getControl("vsync");
 
     // Listen for UI events.
     _startRed->addListener(this, Listener::VALUE_CHANGED);
@@ -128,13 +128,12 @@ void ParticlesGame::initialize()
     _started->addListener(this, Listener::VALUE_CHANGED);
     _reset->addListener(this, Listener::CLICK);
     _emit->addListener(this, Listener::CLICK);
-    _spiralFlame->addListener(this, Listener::VALUE_CHANGED);
-    _smoke->addListener(this, Listener::VALUE_CHANGED);
-    _explosion->addListener(this, Listener::VALUE_CHANGED);
     _zoomIn->addListener(this, Listener::PRESS);
     _zoomIn->addListener(this, Listener::RELEASE);
     _zoomOut->addListener(this, Listener::PRESS);
     _zoomOut->addListener(this, Listener::RELEASE);
+    _save->addListener(this, Listener::RELEASE);
+    _load->addListener(this, Listener::RELEASE);
     _burstSize->addListener(this, Listener::VALUE_CHANGED);
     _posVarX->addListener(this, Listener::VALUE_CHANGED);
     _posVarY->addListener(this, Listener::VALUE_CHANGED);
@@ -161,9 +160,205 @@ void ParticlesGame::initialize()
     _axisVarZ->addListener(this, Listener::VALUE_CHANGED);
     _rotationSpeedMin->addListener(this, Listener::VALUE_CHANGED);
     _rotationSpeedMax->addListener(this, Listener::VALUE_CHANGED);
+    _vsync->addListener(this, Listener::VALUE_CHANGED);
+    _form->getControl("sprite")->addListener(this, Listener::CLICK);
+    _form->getControl("additive")->addListener(this, Listener::VALUE_CHANGED);
+    _form->getControl("transparent")->addListener(this, Listener::VALUE_CHANGED);
+    _form->getControl("multiply")->addListener(this, Listener::VALUE_CHANGED);
+    _form->getControl("opaque")->addListener(this, Listener::VALUE_CHANGED);
+    _form->getControl("updateFrames")->addListener(this, Listener::CLICK);
     
+    // Hide save/load buttons for non-windows platforms until we implement picking dialogs for others
+#ifndef WIN32
+    _form->getControl("save")->setVisible(false);
+    _form->getControl("load")->setVisible(false);
+    _form->getControl("image")->setVisible(false);
+#endif
+
     // Apply default emitter values to the UI.
     emitterChanged();
+
+    updateImageControl();
+}
+
+std::string ParticlesGame::openFile(const char* title, const char* filterDescription, const char* filterExtension)
+{
+#ifdef WIN32
+    OPENFILENAMEA ofn;
+    memset(&ofn, 0, sizeof(ofn));
+
+    std::string desc = filterDescription;
+    desc += " (*.";
+    desc += filterExtension;
+    desc += ")";
+    std::string ext = "*.";
+    ext += filterExtension;
+    char filter[1024];
+    memset(filter, 0, 1024);
+    strcpy(filter, desc.c_str());
+    strcpy(filter + desc.length() + 1, ext.c_str());
+
+    char szCurrentDir[256];
+    GetCurrentDirectoryA(256, szCurrentDir);
+    std::string initialDir = szCurrentDir;
+    initialDir += "\\res";
+
+    char szFileName[256] = "";
+
+    ofn.lStructSize = sizeof(ofn); // SEE NOTE BELOW
+    ofn.hwndOwner = GetForegroundWindow();
+    ofn.lpstrTitle = title;
+    ofn.lpstrFilter = filter;//"Particle Files (*.particle)\0*.particle\0";
+    ofn.lpstrFile = szFileName;
+    ofn.lpstrInitialDir = initialDir.c_str();
+    ofn.nMaxFile = 256;
+    ofn.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY;
+    ofn.lpstrDefExt = "filterExtension";
+
+    GetOpenFileNameA(&ofn);
+
+    // Restore current dir
+    SetCurrentDirectoryA(szCurrentDir);
+
+    return szFileName;
+#endif
+
+    return "";
+}
+
+std::string toString(ParticleEmitter::TextureBlending blending)
+{
+    switch (blending)
+    {
+    case ParticleEmitter::BLEND_OPAQUE:
+        return "OPAQUE";
+    case ParticleEmitter::BLEND_TRANSPARENT:
+        return "TRANSPARENT";
+    case ParticleEmitter::BLEND_ADDITIVE:
+        return "ADDITIVE";
+    case ParticleEmitter::BLEND_MULTIPLIED:
+        return "MULTIPLIED";
+    default:
+        return "TRANSPARENT";
+    }
+}
+
+std::string toString(const Vector4& v)
+{
+    std::ostringstream s;
+    s << v.x << ", " << v.y << ", " << v.z << ", " << v.w;
+    return s.str();
+}
+
+std::string toString(const Vector3& v)
+{
+    std::ostringstream s;
+    s << v.x << ", " << v.y << ", " << v.z;
+    return s.str();
+}
+
+std::string toString(bool b)
+{
+    return b ? "true" : "false";
+}
+
+void ParticlesGame::saveFile()
+{
+    std::string filename;
+
+#ifdef WIN32
+    OPENFILENAMEA ofn;
+    memset(&ofn, 0, sizeof(ofn));
+
+    char szCurrentDir[256];
+    GetCurrentDirectoryA(256, szCurrentDir);
+    std::string initialDir = szCurrentDir;
+    initialDir += "\\res";
+
+    char szFileName[256] = "";
+
+    ofn.lStructSize = sizeof(ofn); // SEE NOTE BELOW
+    ofn.hwndOwner = GetForegroundWindow();
+    ofn.lpstrFilter = "Particle Files (*.particle)\0*.particle\0";
+    ofn.lpstrFile = szFileName;
+    ofn.lpstrInitialDir = initialDir.c_str();
+    ofn.nMaxFile = 256;
+    ofn.Flags = OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT;
+    ofn.lpstrDefExt = "particle";
+
+    GetSaveFileNameA(&ofn);
+
+    filename = szFileName;
+
+    // Restore current dir
+    SetCurrentDirectoryA(szCurrentDir);
+
+#endif
+
+    if (filename.length() == 0)
+        return;
+
+    ParticleEmitter* e = _particleEmitter;
+
+    // Extract just the particle name from the filename
+    std::string dir = FileSystem::getDirectoryName(filename.c_str());
+    std::string ext = FileSystem::getExtension(filename.c_str());
+    std::string name = filename.substr(dir.length(), filename.length() - dir.length() - ext.length());
+
+    Texture* texture = e->getTexture();
+    std::string texturePath = texture->getPath();
+    std::string textureDir = FileSystem::getDirectoryName(texturePath.c_str());
+    texturePath = texturePath.substr(textureDir.length());
+
+    // Write out a properties file
+    std::ostringstream s;
+    s << 
+        "particle " << name << "\n" <<
+        "{\n" <<
+        "    sprite\n" <<
+        "    {\n" <<
+        "        path = " << texturePath << "\n" <<
+        "        width = " << e->getSpriteWidth() << "\n" <<
+        "        height = " << e->getSpriteHeight() << "\n" <<
+        "        blending = " << toString(e->getTextureBlending()) << "\n" <<
+        "        animated = " << toString(e->isSpriteAnimated()) << "\n" <<
+        "        looped = " << toString(e->isSpriteLooped()) << "\n" <<
+        "        frameCount = " << e->getSpriteFrameCount() << "\n" <<
+        "        frameRandomOffset = " << e->getSpriteFrameRandomOffset() << "\n" <<
+        "        frameDuration = " << e->getSpriteFrameDuration() << "\n" <<
+        "    }\n" <<
+        "\n" <<
+        "    particleCountMax = " << e->getParticleCountMax() << "\n" <<
+        "    emissionRate = " << e->getEmissionRate() << "\n" <<
+        "    ellipsoid = " << toString(e->isEllipsoid()) << "\n" <<
+        "    orbitPosition = " << toString(e->getOrbitPosition()) << "\n" <<
+        "    orbitVelocity = " << toString(e->getOrbitVelocity()) << "\n" <<
+        "    orbitAcceleration = " << toString(e->getOrbitAcceleration()) << "\n" <<
+        "    sizeStartMin = " << e->getSizeStartMin() << "\n" <<
+        "    sizeStartMax = " << e->getSizeStartMax() << "\n" <<
+        "    sizeEndMin = " << e->getSizeEndMin() << "\n" <<
+        "    sizeEndMax = " << e->getSizeEndMax() << "\n" <<
+        "    energyMin = " << e->getEnergyMin() << "\n" <<
+        "    energyMax = " << e->getEnergyMax() << "\n" <<
+        "    colorStart = " << toString(e->getColorStart()) << "\n" <<
+        "    colorStartVar = " << toString(e->getColorStartVariance()) << "\n" <<
+        "    colorEnd = " << toString(e->getColorEnd()) << "\n" <<
+        "    colorEndVar = " << toString(e->getColorEndVariance()) << "\n" <<
+        "    position = " << toString(e->getPosition()) << "\n" <<
+        "    positionVar = " << toString(e->getPositionVariance()) << "\n" <<
+        "    velocity = " << toString(e->getVelocity()) << "\n" <<
+        "    velocityVar = " << toString(e->getVelocityVariance()) << "\n" <<
+        "    acceleration = " << toString(e->getAcceleration()) << "\n" <<
+        "    accelerationVar = " << toString(e->getAccelerationVariance()) << "\n" <<
+        "    rotationPerParticleSpeedMin = " << e->getRotationPerParticleSpeedMin() << "\n" <<
+        "    rotationPerParticleSpeedMax = " << e->getRotationPerParticleSpeedMax() << "\n" <<
+        "}\n";
+
+    std::string text = s.str();
+    Stream* stream = FileSystem::open(filename.c_str(), FileSystem::WRITE);
+    stream->write(text.c_str(), 1, text.length());
+    stream->close();
+    SAFE_DELETE(stream);
 }
 
 void ParticlesGame::controlEvent(Control* control, EventType evt)
@@ -408,20 +603,29 @@ void ParticlesGame::controlEvent(Control* control, EventType evt)
                 emitter->stop();
             }
         }
-        else if (control == _spiralFlame && _spiralFlame->isSelected())
+        else if (control == _vsync)
         {
-            _particleEmitterIndex = 0;
-            emitterChanged();
+            Game::getInstance()->setVsync(_vsync->isChecked());
         }
-        else if (control == _smoke && _smoke->isSelected())
+        else if (strcmp(control->getId(), "additive") == 0)
         {
-            _particleEmitterIndex = 1;
-            emitterChanged();
+            if (((RadioButton*)control)->isSelected())
+                emitter->setTextureBlending(ParticleEmitter::BLEND_ADDITIVE);
         }
-        else if (control == _explosion && _explosion->isSelected())
+        else if (strcmp(control->getId(), "transparent") == 0)
         {
-            _particleEmitterIndex = 2;
-            emitterChanged();
+            if (((RadioButton*)control)->isSelected())
+                emitter->setTextureBlending(ParticleEmitter::BLEND_TRANSPARENT);
+        }
+        else if (strcmp(control->getId(), "multiply") == 0)
+        {
+            if (((RadioButton*)control)->isSelected())
+                emitter->setTextureBlending(ParticleEmitter::BLEND_MULTIPLIED);
+        }
+        else if (strcmp(control->getId(), "opaque") == 0)
+        {
+            if (((RadioButton*)control)->isSelected())
+                emitter->setTextureBlending(ParticleEmitter::BLEND_OPAQUE);
         }
         break;
     case Listener::CLICK:
@@ -429,8 +633,7 @@ void ParticlesGame::controlEvent(Control* control, EventType evt)
         {
             // Re-load the current emitter.
             _particleEmitterNode->setParticleEmitter(NULL);
-            SAFE_RELEASE(emitter);
-            emitter = _particleEmitters[_particleEmitterIndex] = ParticleEmitter::create(_particleFiles[_particleEmitterIndex].c_str());
+            emitter = _particleEmitter = ParticleEmitter::create(_url.c_str());
             emitterChanged();
         }
         else if (control == _emit)
@@ -439,6 +642,14 @@ void ParticlesGame::controlEvent(Control* control, EventType evt)
             unsigned int burstSize = (unsigned int)_burstSize->getValue();
             emitter->emitOnce(burstSize);
         }
+        else if (strcmp(control->getId(), "sprite") == 0)
+        {
+            updateTexture();
+        }
+        else if (strcmp(control->getId(), "updateFrames") == 0)
+        {
+            updateFrames();
+        }
         break;
     case Listener::PRESS:
         if (control == _zoomIn)
@@ -459,20 +670,62 @@ void ParticlesGame::controlEvent(Control* control, EventType evt)
         {
             _sDown = false;
         }
+        else if (control == _save)
+        {
+            Game::getInstance()->pause();
+            saveFile();
+            Game::getInstance()->resume();
+        }
+        else if (control == _load)
+        {
+            Game::getInstance()->pause();
+            std::string filename = openFile("Select Particle File", "Particle Files", "particle");
+            if (filename.length() > 0)
+            {
+                _particleEmitter = ParticleEmitter::create(filename.c_str());
+                _url = filename;
+                emitterChanged();
+            }
+            Game::getInstance()->resume();
+        }
         break;
     }
 }
 
+void ParticlesGame::updateFrames()
+{
+    Texture* texture = _particleEmitter->getTexture();
+    TextBox* cBox = (TextBox*)_form->getControl("frameCount");
+    TextBox* wBox = (TextBox*)_form->getControl("frameWidth");
+    TextBox* hBox = (TextBox*)_form->getControl("frameHeight");
+    unsigned int fc = (unsigned int)atoi(cBox->getText());
+    unsigned int w = (unsigned int)atoi(wBox->getText());
+    unsigned int h = (unsigned int)atoi(hBox->getText());
+    if (fc > 0 && fc < 256 && fc < 1000 && w > 0 && h > 0 && w < 4096 && h < 4096)
+    {
+        if (w > _particleEmitter->getTexture()->getWidth())
+        {
+            w = texture->getWidth();
+            char buf[1024];
+            wBox->setText(itoa(w, buf, 10));
+        }
+        if (h > texture->getHeight())
+        {
+            h = texture->getHeight();
+            char buf[1024];
+            hBox->setText(itoa(h, buf, 10));
+        }
+
+        _particleEmitter->setSpriteFrameCoords(fc, w, h);
+    }
+}
+
 void ParticlesGame::finalize()
 {
     SAFE_RELEASE(_scene);
     SAFE_RELEASE(_form);
     SAFE_RELEASE(_font);
-
-    for (unsigned int i = 0; i < _particleEmitters.size(); i++)
-    {
-        SAFE_RELEASE(_particleEmitters[i]);
-    }    
+    SAFE_RELEASE(_particleEmitter);
 }
 
 void ParticlesGame::update(float elapsedTime)
@@ -516,14 +769,14 @@ void ParticlesGame::render(float elapsedTime)
     // Clear the color and depth buffers.
     clear(CLEAR_COLOR_DEPTH, BACKGROUND_COLOR, 1.0f, 0);
 
-    // Draw the UI.
-    _form->draw();
-
     // Visit all the nodes in the scene for drawing.
     _scene->visit(this, &ParticlesGame::drawScene, (void*)0);
 
+    // Draw the UI.
+    _form->draw();
+
     // Draw the framerate and number of live particles.
-    drawFrameRate(_font, Vector4(0, 0.5f, 1, 1), 170, 10, getFrameRate());
+    drawFrameRate(_font, Vector4(1, 1, 1, 1), 170, 40, getFrameRate());
 }
 
 bool ParticlesGame::drawScene(Node* node, void* cookie)
@@ -536,6 +789,62 @@ bool ParticlesGame::drawScene(Node* node, void* cookie)
     return true;
 }
 
+bool ParticlesGame::mouseEvent(Mouse::MouseEvent evt, int x, int y, int wheelDelta)
+{
+    switch (evt)
+    {
+    case Mouse::MOUSE_PRESS_MIDDLE_BUTTON:
+        Game::getInstance()->setMouseCaptured(true);
+        _panning = true;
+        return true;
+    case Mouse::MOUSE_RELEASE_MIDDLE_BUTTON:
+        Game::getInstance()->setMouseCaptured(false);
+        _panning = false;
+        return true;
+    case Mouse::MOUSE_PRESS_LEFT_BUTTON:
+        Game::getInstance()->setMouseCaptured(true);
+        _rotating = true;
+        return true;
+    case Mouse::MOUSE_RELEASE_LEFT_BUTTON:
+        Game::getInstance()->setMouseCaptured(false);
+        _rotating = false;
+        return true;
+    case Mouse::MOUSE_PRESS_RIGHT_BUTTON:
+        Game::getInstance()->setMouseCaptured(true);
+        _zooming = true;
+        return true;
+    case Mouse::MOUSE_RELEASE_RIGHT_BUTTON:
+        Game::getInstance()->setMouseCaptured(false);
+        _zooming = false;
+        return true;
+    case Mouse::MOUSE_MOVE:
+        if (_panning)
+        {
+            Vector3 n(-(float)x * PANNING_SENSITIVITY, (float)y * PANNING_SENSITIVITY, 0);
+            _cameraParent->getMatrix().transformVector(&n);
+            _cameraParent->translate(n);
+            return true;
+        }
+        else if (_rotating)
+        {
+            _cameraParent->rotateY(-MATH_DEG_TO_RAD((float)x * ROTATE_SENSITIVITY));
+            _cameraParent->rotateX(-MATH_DEG_TO_RAD((float)y * ROTATE_SENSITIVITY));
+            return true;
+        }
+        else if (_zooming)
+        {
+            Vector3 v = _scene->getActiveCamera()->getNode()->getForwardVector();
+            v.normalize();
+            v.scale((float)(x-y) * INPUT_SENSITIVITY);
+            _scene->getActiveCamera()->getNode()->translate(v);
+            return true;
+        }
+        break;
+    }
+
+    return true;
+}
+
 void ParticlesGame::touchEvent(Touch::TouchEvent evt, int x, int y, unsigned int contactIndex)
 {
     // Touch events that don't hit the UI
@@ -597,14 +906,6 @@ void ParticlesGame::keyEvent(Keyboard::KeyEvent evt, int key)
         case Keyboard::KEY_D:
             _dDown = true;
             break;
-        case Keyboard::KEY_P:
-            _particleEmitterIndex++;
-            if (_particleEmitterIndex >= _particleFilesCount)
-            {
-                _particleEmitterIndex = 0;
-            }
-            emitterChanged();
-            break;
         }
         break;
 
@@ -630,12 +931,9 @@ void ParticlesGame::keyEvent(Keyboard::KeyEvent evt, int key)
 
 void ParticlesGame::loadEmitters()
 {
-    for (unsigned int i = 0; i < _particleFilesCount; i++)
-    {
-        ParticleEmitter* emitter = ParticleEmitter::create(_particleFiles[i].c_str());
-        _particleEmitters.push_back(emitter);
-    }
-    _particleEmitterIndex = 0;
+    // Load the default particle emitter
+    _url = DEFAULT_PARTICLE_EMITTER;
+    _particleEmitter = ParticleEmitter::create(_url.c_str());
 
     _particleEmitterNode = _scene->addNode("Particle Emitter");
     _particleEmitterNode->setTranslation(0.0f, 0.0f, 0.0f);
@@ -643,32 +941,16 @@ void ParticlesGame::loadEmitters()
 
 void ParticlesGame::emitterChanged()
 {
-    // Stop the current emitter.
-    ParticleEmitter* prevEmitter = _particleEmitterNode->getParticleEmitter();
-    if (prevEmitter)
-    {
-        prevEmitter->stop();
-    }
+    ParticleEmitter* emitter = _particleEmitter;
 
     // Set the new emitter on the node.
-    ParticleEmitter* emitter = _particleEmitters[_particleEmitterIndex];
-    _particleEmitterNode->setParticleEmitter(emitter);
-
-    // The 'explosion' emitter is meant to emit in bursts.
-    if (_particleEmitterIndex == 2)
-    {
-        _started->setChecked(false);
-        emitter->emitOnce(20);
-    }
-    else
-    {
-        _started->setChecked(true);
-        emitter->start();
-    }
+    _particleEmitterNode->setParticleEmitter(_particleEmitter);
+    _particleEmitter->release();
 
     // Reset camera view and zoom.
     _scene->getActiveCamera()->getNode()->setTranslation(0.0f, 0.0f, 40.0f);
     _cameraParent->setIdentity();
+    _particleEmitterNode->setIdentity();
 
     // Set the values of UI controls to display the new emitter's settings.
     _startRed->setValue(emitter->getColorStart().x);
@@ -681,21 +963,21 @@ void ParticlesGame::emitterChanged()
     _endBlue->setValue(emitter->getColorEnd().z);
     _endAlpha->setValue(emitter->getColorEnd().w);
 
-    _startMin->setMax(PARTICLE_SIZE_MAX[_particleEmitterIndex]);
+    _startMin->setMax(PARTICLE_SIZE_MAX);
     _startMin->setValue(emitter->getSizeStartMin());
 
-    _startMax->setMax(PARTICLE_SIZE_MAX[_particleEmitterIndex]);
+    _startMax->setMax(PARTICLE_SIZE_MAX);
     _startMax->setValue(emitter->getSizeStartMax());
     
-    _endMin->setMax(PARTICLE_SIZE_MAX[_particleEmitterIndex]);
+    _endMin->setMax(PARTICLE_SIZE_MAX);
     _endMin->setValue(emitter->getSizeEndMin());
-    _endMax->setMax(PARTICLE_SIZE_MAX[_particleEmitterIndex]);
+    _endMax->setMax(PARTICLE_SIZE_MAX);
     _endMax->setValue(emitter->getSizeEndMax());
 
     _energyMin->setValue(emitter->getEnergyMin());
     _energyMax->setValue(emitter->getEnergyMax());
 
-    _emissionRate->setMax(EMIT_RATE_MAX[_particleEmitterIndex]);
+    _emissionRate->setMax(EMIT_RATE_MAX);
     _emissionRate->setValue(emitter->getEmissionRate());
 
     char txt[25];
@@ -743,6 +1025,10 @@ void ParticlesGame::emitterChanged()
 
     _rotationSpeedMin->setValue(emitter->getRotationSpeedMin());
     _rotationSpeedMax->setValue(emitter->getRotationSpeedMax());
+
+    emitter->start();
+
+    updateImageControl();
 }
 
 void ParticlesGame::drawSplash(void* param)
@@ -760,6 +1046,64 @@ void ParticlesGame::drawFrameRate(Font* font, const Vector4& color, unsigned int
     char buffer[30];
     sprintf(buffer, "FPS: %u\nParticles: %u", fps, _particleEmitterNode->getParticleEmitter()->getParticlesCount());
     font->start();
-    font->drawText(buffer, x, y, color, 28);
+    font->drawText(buffer, x, y, color, 22);
     font->finish();
 }
+
+void ParticlesGame::resizeEvent(unsigned int width, unsigned int height)
+{
+    setViewport(gameplay::Rectangle(width, height));
+    _form->setSize(width, height);
+}
+
+void ParticlesGame::updateTexture()
+{
+    std::string file = openFile("Select Texture", "PNG Files", "png");
+    if (file.length() > 0)
+    {
+        // Set new sprite on our emitter
+        _particleEmitter->setTexture(file.c_str(), _particleEmitter->getTextureBlending());
+
+        // Update the UI to display the new sprite
+        updateImageControl();
+    }
+}
+
+void ParticlesGame::updateImageControl()
+{
+    ((ImageControl*)_form->getControl("sprite"))->setImage(_particleEmitter->getTexture()->getPath());
+
+    // Resize the image control so keep it to scale
+    int w = _particleEmitter->getTexture()->getWidth();
+    int h = _particleEmitter->getTexture()->getHeight();
+    int max = w > h ? w : h;
+    if (max > 140)
+    {
+        float ratio = 140.0f / max;
+        w *= ratio;
+        h *= ratio;
+    }
+    ((ImageControl*)_form->getControl("sprite"))->setSize(w, h);
+    _form->getControl("image")->setHeight(h + _form->getControl("imageSettings")->getHeight() + 50);
+
+    char buf[1024];
+    ((TextBox*)_form->getControl("frameCount"))->setText(itoa((int)_particleEmitter->getSpriteFrameCount(), buf, 10));
+    ((TextBox*)_form->getControl("frameWidth"))->setText(itoa((int)_particleEmitter->getSpriteWidth(), buf, 10));
+    ((TextBox*)_form->getControl("frameHeight"))->setText(itoa((int)_particleEmitter->getSpriteHeight(), buf, 10));
+
+    switch (_particleEmitter->getTextureBlending())
+    {
+    case ParticleEmitter::BLEND_ADDITIVE:
+        ((RadioButton*)_form->getControl("additive"))->setSelected(true);
+        break;
+    case ParticleEmitter::BLEND_MULTIPLIED:
+        ((RadioButton*)_form->getControl("multiply"))->setSelected(true);
+        break;
+    case ParticleEmitter::BLEND_OPAQUE:
+        ((RadioButton*)_form->getControl("opaque"))->setSelected(true);
+        break;
+    case ParticleEmitter::BLEND_TRANSPARENT:
+        ((RadioButton*)_form->getControl("transparent"))->setSelected(true);
+        break;
+    }
+}

+ 28 - 5
samples/particles/src/ParticlesGame.h

@@ -22,11 +22,21 @@ public:
      */
     void touchEvent(Touch::TouchEvent evt, int x, int y, unsigned int contactIndex);
 
+    /**
+     * @see Game::mouseEvent
+     */
+    bool mouseEvent(Mouse::MouseEvent evt, int x, int y, int wheelDelta);
+
     /**
      * @see Game::keyEvent
      */
     void keyEvent(Keyboard::KeyEvent evt, int key);
 
+    /**
+     * @see Game::resizeEvent
+     */
+    void resizeEvent(unsigned int width, unsigned int height);
+
     /**
      * @see Control::controlEvent
      */
@@ -66,6 +76,16 @@ private:
 
     void drawFrameRate(Font* font, const Vector4& color, unsigned int x, unsigned int y, unsigned int fps);
 
+    std::string openFile(const char* title, const char* filterDescription, const char* filterExtension);
+
+    void saveFile();
+
+    void updateTexture();
+
+    void updateImageControl();
+
+    void updateFrames();
+
     Scene* _scene;
     Node* _particleEmitterNode;
     Node* _cameraParent;
@@ -73,8 +93,8 @@ private:
     bool _wDown, _sDown, _aDown, _dDown;
     bool _touched;
     int _prevX, _prevY;
-    std::vector<ParticleEmitter*> _particleEmitters;
-    unsigned int _particleEmitterIndex;
+    ParticleEmitter* _particleEmitter;
+    std::string _url;
     Font* _font;
     
     Slider* _startRed;
@@ -122,12 +142,15 @@ private:
     Button* _emit;
     Button* _zoomIn;
     Button* _zoomOut;
-    RadioButton* _spiralFlame;
-    RadioButton* _smoke;
-    RadioButton* _explosion;
+    Button* _save;
+    Button* _load;
     Slider* _burstSize;
     Container* _position;
     Container* _particleProperties;
+    CheckBox* _vsync;
+    bool _panning;
+    bool _rotating;
+    bool _zooming;
 };
 
 #endif