ソースを参照

Modified particles sample to support loading and saving of particle files on Windows.
Non-Windows platforms now simply display a single particle effect (fire).
Future updates will enable editing (loading/saving) on Mac/Linux.

Steve Grenier 12 年 前
コミット
78861ad62f

+ 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