Browse Source

Merge branch 'next' of https://github.com/blackberry/GamePlay into next

seanpaultaylor 12 years ago
parent
commit
d87774a346

+ 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);
+        }
     }
 }
 

+ 1 - 1
gameplay/src/AnimationTarget.cpp

@@ -130,7 +130,7 @@ Animation* AnimationTarget::createAnimation(const char* id, Properties* animatio
     }
 
     // Get animation target property id
-    int propertyId = AnimationTarget::getPropertyId(_targetType, propertyIdStr);
+	int propertyId = getPropertyId(_targetType, propertyIdStr);
     if (propertyId == -1)
     {
         GP_ERROR("Property ID is invalid.");

+ 10 - 10
gameplay/src/AnimationTarget.h

@@ -166,6 +166,16 @@ protected:
      */
     virtual ~AnimationTarget();
 
+	/**
+     * Gets the TargetType's property ID value for the specified property ID string.
+     *
+     * @param type The TargetType of the AnimationTarget.
+     * @param propertyIdStr The property ID string.
+     * @return The property ID value for the property ID string; -1 if the propertyIdStr does not exist
+     *    for the TargetType.
+     */
+    virtual int getPropertyId(TargetType type, const char* propertyIdStr);
+
     /**
      * Adds the given animation channel to this animation target.
      *
@@ -210,16 +220,6 @@ private:
      */
     AnimationTarget(const AnimationTarget& copy);
 
-    /**
-     * Gets the TargetType's property ID value for the specified property ID string.
-     *
-     * @param type The TargetType of the AnimationTarget.
-     * @param propertyIdStr The property ID string.
-     * @return The property ID value for the property ID string; -1 if the propertyIdStr does not exist
-     *    for the TargetType.
-     */
-    static int getPropertyId(TargetType type, const char* propertyIdStr);
-
     /**
      * Converts by-value animations to to-value animations.
      */

+ 1 - 0
gameplay/src/Base.h

@@ -45,6 +45,7 @@ using std::size_t;
 using std::min;
 using std::max;
 using std::modf;
+using std::atoi;
 
 // Common
 #ifndef NULL

+ 16 - 4
gameplay/src/Container.cpp

@@ -295,8 +295,8 @@ void Container::removeControl(unsigned int index)
     GP_ASSERT(index < _controls.size());
 
     std::vector<Control*>::iterator it = _controls.begin() + index;
-    _controls.erase(it);
     Control* control = *it;
+    _controls.erase(it);
     control->_parent = NULL;
     SAFE_RELEASE(control);
 }
@@ -407,6 +407,16 @@ bool Container::isScrolling() const
              abs(_scrollingLastY - _scrollingVeryFirstY) > SCROLL_THRESHOLD));
 }
 
+const Vector2& Container::getScrollPosition() const
+{
+    return _scrollPosition;
+}
+
+void Container::setScrollPosition(const Vector2& scrollPosition)
+{
+    _scrollPosition = scrollPosition;
+}
+
 Animation* Container::getAnimation(const char* id) const
 {
     std::vector<Control*>::const_iterator itr = _controls.begin();
@@ -916,7 +926,8 @@ bool Container::moveFocus(Direction direction, Control* outsideControl)
         for (itt = _controls.begin(); itt < _controls.end(); itt++)
         {
             Control* nextControl = *itt;
-            if (nextControl->getFocusIndex() == focusIndex)
+            if (nextControl->getFocusIndex() == focusIndex &&
+                nextControl->isEnabled() && nextControl->isVisible())
             {
                 next = nextControl;
                 break;
@@ -1302,6 +1313,7 @@ void Container::updateScroll()
     const Theme::Padding& containerPadding = getPadding();
 
     // Calculate total width and height.
+    _totalWidth = _totalHeight = 0.0f;
     std::vector<Control*> controls = getControls();
     for (size_t i = 0, controlsCount = controls.size(); i < controlsCount; i++)
     {
@@ -1310,13 +1322,13 @@ void Container::updateScroll()
         const Rectangle& bounds = control->getBounds();
         const Theme::Margin& margin = control->getMargin();
 
-        float newWidth = bounds.x + bounds.width;
+        float newWidth = bounds.x + bounds.width + margin.right;
         if (newWidth > _totalWidth)
         {
             _totalWidth = newWidth;
         }
 
-        float newHeight = bounds.y + bounds.height;
+        float newHeight = bounds.y + bounds.height + margin.bottom;
         if (newHeight > _totalHeight)
         {
             _totalHeight = newHeight;

+ 10 - 0
gameplay/src/Container.h

@@ -207,6 +207,16 @@ public:
      */
     void setScrollWheelSpeed(float speed);
 
+    /**
+     * Get an offset of how far this layout has been scrolled in each direction.
+     */
+    const Vector2& getScrollPosition() const;
+
+    /**
+     * Set an offset of how far this layout has been scrolled in each direction.
+     */
+    void setScrollPosition(const Vector2& scrollPosition);
+
     /**
      * @see AnimationTarget::getAnimation
      */

+ 4 - 1
gameplay/src/Control.cpp

@@ -37,7 +37,10 @@ void Control::initialize(Theme::Style* style, Properties* properties)
     _style = style;
 
     // Properties not defined by the style.
-    _alignment = getAlignment(properties->getString("alignment"));
+    const char * alignmentString = properties->getString("alignment");
+
+    _isAlignmentSet = alignmentString != NULL;
+    _alignment = getAlignment(alignmentString);
     _autoWidth = properties->getBool("autoWidth");
     _autoHeight = properties->getBool("autoHeight");
 

+ 26 - 6
gameplay/src/Effect.cpp

@@ -1,8 +1,9 @@
 #include "Base.h"
 #include "Effect.h"
 #include "FileSystem.h"
+#include "Game.h"
 
-#define OPENGL_ES_DEFINE  "#define OPENGL_ES\n"
+#define OPENGL_ES_DEFINE  "OPENGL_ES"
 
 namespace gameplay
 {
@@ -104,9 +105,31 @@ Effect* Effect::createFromSource(const char* vshSource, const char* fshSource, c
 
 static void replaceDefines(const char* defines, std::string& out)
 {
-    if (defines && strlen(defines) != 0)
+    Properties* graphicsConfig = Game::getInstance()->getConfig()->getNamespace("graphics", true);
+    const char* globalDefines = graphicsConfig ? graphicsConfig->getString("shaderDefines") : NULL;
+
+    // Build full semicolon delimited list of defines
+#ifdef OPENGL_ES
+    out = OPENGL_ES_DEFINE;
+#else
+    out = "";
+#endif
+    if (globalDefines && strlen(globalDefines) > 0)
+    {
+        if (out.length() > 0)
+            out += ';';
+        out += globalDefines;
+    }
+    if (defines && strlen(defines) > 0)
+    {
+        if (out.length() > 0)
+            out += ';';
+        out += defines;
+    }
+
+    // Replace semicolons
+    if (out.length() > 0)
     {
-        out = defines;
         size_t pos;
         out.insert(0, "#define ");
         while ((pos = out.find(';')) != std::string::npos)
@@ -115,9 +138,6 @@ static void replaceDefines(const char* defines, std::string& out)
         }
         out += "\n";
     }
-#ifdef OPENGL_ES
-    out.insert(0, OPENGL_ES_DEFINE);
-#endif
 }
 
 static void replaceIncludes(const char* filepath, const char* source, std::string& out)

+ 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();

+ 11 - 0
gameplay/src/Font.cpp

@@ -154,6 +154,17 @@ 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();
+    if (!vp.isEmpty())
+    {
+        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];

+ 26 - 20
gameplay/src/Layout.cpp

@@ -47,31 +47,37 @@ void Layout::align(Control* control, const Container* container)
         }
 
         // Vertical alignment
-        if ((control->_alignment & Control::ALIGN_BOTTOM) == Control::ALIGN_BOTTOM)
+        if(control->_isAlignmentSet || control->_autoHeight)
         {
-            controlBounds.y = clipHeight - controlBounds.height - controlMargin.bottom;
-        }
-        else if ((control->_alignment & Control::ALIGN_VCENTER) == Control::ALIGN_VCENTER)
-        {
-            controlBounds.y = clipHeight * 0.5f - controlBounds.height * 0.5f;
-        }
-        else if ((control->_alignment & Control::ALIGN_TOP) == Control::ALIGN_TOP)
-        {
-            controlBounds.y = controlMargin.top;
+            if ((control->_alignment & Control::ALIGN_BOTTOM) == Control::ALIGN_BOTTOM)
+            {
+                controlBounds.y = clipHeight - controlBounds.height - controlMargin.bottom;
+            }
+            else if ((control->_alignment & Control::ALIGN_VCENTER) == Control::ALIGN_VCENTER)
+            {
+                controlBounds.y = clipHeight * 0.5f - controlBounds.height * 0.5f;
+            }
+            else if ((control->_alignment & Control::ALIGN_TOP) == Control::ALIGN_TOP)
+            {
+                controlBounds.y = controlMargin.top;
+            }
         }
 
         // Horizontal alignment
-        if ((control->_alignment & Control::ALIGN_RIGHT) == Control::ALIGN_RIGHT)
-        {
-            controlBounds.x = clipWidth - controlBounds.width - controlMargin.right;
-        }
-        else if ((control->_alignment & Control::ALIGN_HCENTER) == Control::ALIGN_HCENTER)
-        {
-            controlBounds.x = clipWidth * 0.5f - controlBounds.width * 0.5f;
-        }
-        else if ((control->_alignment & Control::ALIGN_LEFT) == Control::ALIGN_LEFT)
+        if(control->_isAlignmentSet || control->_autoWidth)
         {
-            controlBounds.x = controlMargin.left;
+            if ((control->_alignment & Control::ALIGN_RIGHT) == Control::ALIGN_RIGHT)
+            {
+                controlBounds.x = clipWidth - controlBounds.width - controlMargin.right;
+            }
+            else if ((control->_alignment & Control::ALIGN_HCENTER) == Control::ALIGN_HCENTER)
+            {
+                controlBounds.x = clipWidth * 0.5f - controlBounds.width * 0.5f;
+            }
+            else if ((control->_alignment & Control::ALIGN_LEFT) == Control::ALIGN_LEFT)
+            {
+                controlBounds.x = controlMargin.left;
+            }
         }
 
         control->setBounds(controlBounds);

+ 2 - 1
gameplay/src/Logger.cpp

@@ -1,3 +1,4 @@
+
 #include "Base.h"
 #include "Game.h"
 #include "ScriptController.h"
@@ -66,7 +67,7 @@ void Logger::log(Level level, const char* message, ...)
     else
     {
         // Log to the default output
-        gameplay::print(str);
+        gameplay::print("%s", str);
     }
 
     va_end(args);

+ 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;
 };
 
 }

+ 22 - 2
gameplay/src/Properties.cpp

@@ -124,9 +124,11 @@ void Properties::readProperties(Stream* stream)
     char* rc;
     char* rcc;
     char* rccc;
+    bool comment = false;
 
     while (true)
     {
+        // Skip whitespace at the start of lines
         skipWhiteSpace(stream);
 
         // Stop when we have reached the end of the file.
@@ -141,8 +143,26 @@ void Properties::readProperties(Stream* stream)
             return;
         }
 
-        // Ignore comment, skip line.
-        if (strncmp(line, "//", 2) != 0)
+        // Ignore comments
+        if (comment)
+        {
+            // Check for end of multi-line comment at either start or end of line
+            if (strncmp(line, "*/", 2) == 0)
+                comment = false;
+            else
+            {
+                trimWhiteSpace(line);
+                const int len = strlen(line);
+                if (len >= 2 && strncmp(line + (len - 2), "*/", 2) == 0)
+                    comment = false;
+            }
+        }
+        else if (strncmp(line, "/*", 2) == 0)
+        {
+            // Start of multi-line comment (must be at start of line)
+            comment = true;
+        }
+        else if (strncmp(line, "//", 2) != 0)
         {
             // If an '=' appears on this line, parse it as a name/value pair.
             // Note: strchr() has to be called before strtok(), or a backup of line has to be kept.

+ 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;
     }

+ 88 - 0
gameplay/src/lua/lua_Container.cpp

@@ -76,6 +76,7 @@ void luaRegister_Container()
         {"getPadding", lua_Container_getPadding},
         {"getRefCount", lua_Container_getRefCount},
         {"getScroll", lua_Container_getScroll},
+        {"getScrollPosition", lua_Container_getScrollPosition},
         {"getScrollWheelRequiresFocus", lua_Container_getScrollWheelRequiresFocus},
         {"getScrollWheelSpeed", lua_Container_getScrollWheelSpeed},
         {"getScrollingFriction", lua_Container_getScrollingFriction},
@@ -123,6 +124,7 @@ void luaRegister_Container()
         {"setPosition", lua_Container_setPosition},
         {"setScroll", lua_Container_setScroll},
         {"setScrollBarsAutoHide", lua_Container_setScrollBarsAutoHide},
+        {"setScrollPosition", lua_Container_setScrollPosition},
         {"setScrollWheelRequiresFocus", lua_Container_setScrollWheelRequiresFocus},
         {"setScrollWheelSpeed", lua_Container_setScrollWheelSpeed},
         {"setScrollingFriction", lua_Container_setScrollingFriction},
@@ -2174,6 +2176,50 @@ int lua_Container_getScroll(lua_State* state)
     return 0;
 }
 
+int lua_Container_getScrollPosition(lua_State* state)
+{
+    // Get the number of parameters.
+    int paramCount = lua_gettop(state);
+
+    // Attempt to match the parameters to a valid binding.
+    switch (paramCount)
+    {
+        case 1:
+        {
+            if ((lua_type(state, 1) == LUA_TUSERDATA))
+            {
+                Container* instance = getInstance(state);
+                void* returnPtr = (void*)&(instance->getScrollPosition());
+                if (returnPtr)
+                {
+                    ScriptUtil::LuaObject* object = (ScriptUtil::LuaObject*)lua_newuserdata(state, sizeof(ScriptUtil::LuaObject));
+                    object->instance = returnPtr;
+                    object->owns = false;
+                    luaL_getmetatable(state, "Vector2");
+                    lua_setmetatable(state, -2);
+                }
+                else
+                {
+                    lua_pushnil(state);
+                }
+
+                return 1;
+            }
+
+            lua_pushstring(state, "lua_Container_getScrollPosition - Failed to match the given parameters to a valid function signature.");
+            lua_error(state);
+            break;
+        }
+        default:
+        {
+            lua_pushstring(state, "Invalid number of parameters (expected 1).");
+            lua_error(state);
+            break;
+        }
+    }
+    return 0;
+}
+
 int lua_Container_getScrollWheelRequiresFocus(lua_State* state)
 {
     // Get the number of parameters.
@@ -4370,6 +4416,48 @@ int lua_Container_setScrollBarsAutoHide(lua_State* state)
     return 0;
 }
 
+int lua_Container_setScrollPosition(lua_State* state)
+{
+    // Get the number of parameters.
+    int paramCount = lua_gettop(state);
+
+    // Attempt to match the parameters to a valid binding.
+    switch (paramCount)
+    {
+        case 2:
+        {
+            if ((lua_type(state, 1) == LUA_TUSERDATA) &&
+                (lua_type(state, 2) == LUA_TUSERDATA || lua_type(state, 2) == LUA_TNIL))
+            {
+                // Get parameter 1 off the stack.
+                bool param1Valid;
+                ScriptUtil::LuaArray<Vector2> param1 = ScriptUtil::getObjectPointer<Vector2>(2, "Vector2", true, &param1Valid);
+                if (!param1Valid)
+                {
+                    lua_pushstring(state, "Failed to convert parameter 1 to type 'Vector2'.");
+                    lua_error(state);
+                }
+
+                Container* instance = getInstance(state);
+                instance->setScrollPosition(*param1);
+                
+                return 0;
+            }
+
+            lua_pushstring(state, "lua_Container_setScrollPosition - Failed to match the given parameters to a valid function signature.");
+            lua_error(state);
+            break;
+        }
+        default:
+        {
+            lua_pushstring(state, "Invalid number of parameters (expected 2).");
+            lua_error(state);
+            break;
+        }
+    }
+    return 0;
+}
+
 int lua_Container_setScrollWheelRequiresFocus(lua_State* state)
 {
     // Get the number of parameters.

+ 2 - 0
gameplay/src/lua/lua_Container.h

@@ -44,6 +44,7 @@ int lua_Container_getOpacity(lua_State* state);
 int lua_Container_getPadding(lua_State* state);
 int lua_Container_getRefCount(lua_State* state);
 int lua_Container_getScroll(lua_State* state);
+int lua_Container_getScrollPosition(lua_State* state);
 int lua_Container_getScrollWheelRequiresFocus(lua_State* state);
 int lua_Container_getScrollWheelSpeed(lua_State* state);
 int lua_Container_getScrollingFriction(lua_State* state);
@@ -91,6 +92,7 @@ int lua_Container_setPadding(lua_State* state);
 int lua_Container_setPosition(lua_State* state);
 int lua_Container_setScroll(lua_State* state);
 int lua_Container_setScrollBarsAutoHide(lua_State* state);
+int lua_Container_setScrollPosition(lua_State* state);
 int lua_Container_setScrollWheelRequiresFocus(lua_State* state);
 int lua_Container_setScrollWheelSpeed(lua_State* state);
 int lua_Container_setScrollingFriction(lua_State* state);

+ 88 - 0
gameplay/src/lua/lua_Form.cpp

@@ -80,6 +80,7 @@ void luaRegister_Form()
         {"getPadding", lua_Form_getPadding},
         {"getRefCount", lua_Form_getRefCount},
         {"getScroll", lua_Form_getScroll},
+        {"getScrollPosition", lua_Form_getScrollPosition},
         {"getScrollWheelRequiresFocus", lua_Form_getScrollWheelRequiresFocus},
         {"getScrollWheelSpeed", lua_Form_getScrollWheelSpeed},
         {"getScrollingFriction", lua_Form_getScrollingFriction},
@@ -129,6 +130,7 @@ void luaRegister_Form()
         {"setPosition", lua_Form_setPosition},
         {"setScroll", lua_Form_setScroll},
         {"setScrollBarsAutoHide", lua_Form_setScrollBarsAutoHide},
+        {"setScrollPosition", lua_Form_setScrollPosition},
         {"setScrollWheelRequiresFocus", lua_Form_setScrollWheelRequiresFocus},
         {"setScrollWheelSpeed", lua_Form_setScrollWheelSpeed},
         {"setScrollingFriction", lua_Form_setScrollingFriction},
@@ -2214,6 +2216,50 @@ int lua_Form_getScroll(lua_State* state)
     return 0;
 }
 
+int lua_Form_getScrollPosition(lua_State* state)
+{
+    // Get the number of parameters.
+    int paramCount = lua_gettop(state);
+
+    // Attempt to match the parameters to a valid binding.
+    switch (paramCount)
+    {
+        case 1:
+        {
+            if ((lua_type(state, 1) == LUA_TUSERDATA))
+            {
+                Form* instance = getInstance(state);
+                void* returnPtr = (void*)&(instance->getScrollPosition());
+                if (returnPtr)
+                {
+                    ScriptUtil::LuaObject* object = (ScriptUtil::LuaObject*)lua_newuserdata(state, sizeof(ScriptUtil::LuaObject));
+                    object->instance = returnPtr;
+                    object->owns = false;
+                    luaL_getmetatable(state, "Vector2");
+                    lua_setmetatable(state, -2);
+                }
+                else
+                {
+                    lua_pushnil(state);
+                }
+
+                return 1;
+            }
+
+            lua_pushstring(state, "lua_Form_getScrollPosition - Failed to match the given parameters to a valid function signature.");
+            lua_error(state);
+            break;
+        }
+        default:
+        {
+            lua_pushstring(state, "Invalid number of parameters (expected 1).");
+            lua_error(state);
+            break;
+        }
+    }
+    return 0;
+}
+
 int lua_Form_getScrollWheelRequiresFocus(lua_State* state)
 {
     // Get the number of parameters.
@@ -4496,6 +4542,48 @@ int lua_Form_setScrollBarsAutoHide(lua_State* state)
     return 0;
 }
 
+int lua_Form_setScrollPosition(lua_State* state)
+{
+    // Get the number of parameters.
+    int paramCount = lua_gettop(state);
+
+    // Attempt to match the parameters to a valid binding.
+    switch (paramCount)
+    {
+        case 2:
+        {
+            if ((lua_type(state, 1) == LUA_TUSERDATA) &&
+                (lua_type(state, 2) == LUA_TUSERDATA || lua_type(state, 2) == LUA_TNIL))
+            {
+                // Get parameter 1 off the stack.
+                bool param1Valid;
+                ScriptUtil::LuaArray<Vector2> param1 = ScriptUtil::getObjectPointer<Vector2>(2, "Vector2", true, &param1Valid);
+                if (!param1Valid)
+                {
+                    lua_pushstring(state, "Failed to convert parameter 1 to type 'Vector2'.");
+                    lua_error(state);
+                }
+
+                Form* instance = getInstance(state);
+                instance->setScrollPosition(*param1);
+                
+                return 0;
+            }
+
+            lua_pushstring(state, "lua_Form_setScrollPosition - Failed to match the given parameters to a valid function signature.");
+            lua_error(state);
+            break;
+        }
+        default:
+        {
+            lua_pushstring(state, "Invalid number of parameters (expected 2).");
+            lua_error(state);
+            break;
+        }
+    }
+    return 0;
+}
+
 int lua_Form_setScrollWheelRequiresFocus(lua_State* state)
 {
     // Get the number of parameters.

+ 2 - 0
gameplay/src/lua/lua_Form.h

@@ -45,6 +45,7 @@ int lua_Form_getOpacity(lua_State* state);
 int lua_Form_getPadding(lua_State* state);
 int lua_Form_getRefCount(lua_State* state);
 int lua_Form_getScroll(lua_State* state);
+int lua_Form_getScrollPosition(lua_State* state);
 int lua_Form_getScrollWheelRequiresFocus(lua_State* state);
 int lua_Form_getScrollWheelSpeed(lua_State* state);
 int lua_Form_getScrollingFriction(lua_State* state);
@@ -94,6 +95,7 @@ int lua_Form_setPadding(lua_State* state);
 int lua_Form_setPosition(lua_State* state);
 int lua_Form_setScroll(lua_State* state);
 int lua_Form_setScrollBarsAutoHide(lua_State* state);
+int lua_Form_setScrollPosition(lua_State* state);
 int lua_Form_setScrollWheelRequiresFocus(lua_State* state);
 int lua_Form_setScrollWheelSpeed(lua_State* state);
 int lua_Form_setScrollingFriction(lua_State* state);

+ 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

+ 432 - 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,219 @@ 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";
+}
+
+std::string toString(int i)
+{
+    char buf[1024];
+    sprintf(buf, "%d", i);
+    return buf;
+}
+
+std::string toString(unsigned int i)
+{
+    char buf[1024];
+    sprintf(buf, "%d", i);
+    return buf;
+}
+
+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 +617,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 +647,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 +656,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 +684,58 @@ 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())
+        {
+            wBox->setText(toString(texture->getWidth()).c_str());
+        }
+        if (h > texture->getHeight())
+        {
+            hBox->setText(toString(texture->getHeight()).c_str());
+        }
+
+        _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 +779,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 +799,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 +916,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 +941,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 +951,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 +973,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 +1035,10 @@ void ParticlesGame::emitterChanged()
 
     _rotationSpeedMin->setValue(emitter->getRotationSpeedMin());
     _rotationSpeedMax->setValue(emitter->getRotationSpeedMax());
+
+    emitter->start();
+
+    updateImageControl();
 }
 
 void ParticlesGame::drawSplash(void* param)
@@ -760,6 +1056,63 @@ 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);
+
+    ((TextBox*)_form->getControl("frameCount"))->setText(toString(_particleEmitter->getSpriteFrameCount()).c_str());
+    ((TextBox*)_form->getControl("frameWidth"))->setText(toString(_particleEmitter->getSpriteWidth()).c_str());
+    ((TextBox*)_form->getControl("frameHeight"))->setText(toString(_particleEmitter->getSpriteHeight()).c_str());
+
+    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