Tutorial

In this tutorial we want to create a custom parameter that utilizes Curvy to move particles along a curved tube. Though this is somewhat special and bases on another package, most techniques apply to any custom parameter.

Objective

We want particles to form a virtual tube around a spline. At birth particles get a random angle and radius within the tube. Particles then should travel the whole spline during their lifetime. If movement direction changes, all particles should die and start over beginning from the new direction.

Getting started

First we need a custom parameter skeleton. Fortunately, we don't have to code it from scratch. Instead, we create it using the “Create custom parameter” wizard that ships with Magical Box (Main Menu:Window→Magical Box→Create Custom Parameter).

Our particles don't need birth animation, so disable it. We want to set particle's position each frame, so set Animated Lifetime to mandatory. By hitting Create the wizard generates two files:

  • MBParticleCurvyTubeSimple.cs ⇒ the custom parameter
  • MBEditorParticleCurvyTubeSimpleHandler.cs ⇒ the parameter UI

Congratulations! you just created your first custom parameter! Let's test it!

Create a MBParticleSystem and an emitter (name it “TubeEmitter”) and add both a LifeTime and our new parameter (Custom→Curvy Simple Tube) to the emitter. Obviously, it doesn't do anything useful yet, but we have a running parameter and it's UI integrated into Magical Box, so all we need is some functionality. On to the fun part:

Creating Properties

We want our parameter to have some properties (and yes, actually they're fields!), so open Assets/Magical Box/User Parameters/MBParticleCurvyTubeSimple.cs and add them:

public CurvySpline Spline; // Spline reference
public bool Backward; // Movement direction
int mAngleSlotID; // UserData: slot ID for angle
int mRadiusSlotID; // UserData: slot ID for radius
bool mLastBackwardStatus; // to detect direction changes

Mostly self-explanatory. Ignore the two UserData variables for now.

Setting default values

We use the Reset() method to set default values for our parameter:

public override void Reset()
{
        base.Reset();
        Backward = false;
        Spline = null;
        mAngleSlotID = -1;
        mRadiusSlotID = -1;
        Order=500; 
}

A parameter's order determines it's default processing order, from low to high. E.g. the Lifetime parameter has an order of 10 while the Velocity parameter has an order of 50. So, if you add Lifetime and Velocity to an emitter, particle's Lifetime parameter will be applied before Velocity. By giving an order of 500 our new parameter is called after all builtin parameters by default. Read more about it here.

Before adding further functionality, we should create an UI for our parameter:

Creating UI

Open Assets/Magical Box/Editor/User Handlers/MBEditorParticleCurvyTubeSimpleHandler.cs and add UI fields for our two public fields (most lines were added by the wizard):

public override void OnLifetimeGUI()
{
        base.OnLifetimeGUI();
        MBParticleCurvyTubeSimple P = Target as MBParticleCurvyTubeSimple;
        EditorGUILayout.BeginHorizontal();
        P.Spline = (CurvySpline)MBGUI.DoObjectField("Spline", "Spline to use", P.Spline, typeof(CurvySpline));
        P.Backward = MBGUI.DoToggle("Backwards", "Move backwards?", P.Backward);
        EditorGUILayout.EndHorizontal();
}

Also, uncomment the class' constructor to prevent drawing of a birth animation UI box (we don't have any birth animation!). Save and compile and see the resulting UI:

Nice! We now have a custom parameter added to an emitter and a Curvy Spline assigned, so we're ready to actually add parameter functionality:

Adding Custom Data

As particles should form a tube around the spline, we randomly choose a radius and angle at a particle's birth and store them in the particle's UserData - a simple object array within the particle class that can be used for anything you want. You work with this array by registering a slot. The returned ID can be used to access the array then. (Switch over to MBParticleCurvyTubeSimple.cs):

public override void OnPlay()
{
        mAngleSlotID = ParentEmitter.RegisterParticleUserData("CurvySimpleTubeAngle" + GetInstanceID());
        mRadiusSlotID = ParentEmitter.RegisterParticleUserData("CurvySimpleTubeRadius" + GetInstanceID());
        mLastBackwardStatus = Backward;
}

OnPlay() is called when the emitter starts playing, so this is the best place to register data slots. After registration, we can store our values in OnBirth():

public override void OnBirth(MBParticle PT)
{
        // Set a random tube angle
        if (PT.HasUserData(mAngleSlotID))
            PT.UserData[mAngleSlotID] = Random.Range(0f, 360f);
        // Set a random tube radius
        if (PT.HasUserData(mRadiusSlotID))
            PT.UserData[mRadiusSlotID] = Random.value;
}

Now every particle will get a random angle and radius assigned at it's birth. Note that we check the existence of the UserData slot before accessing it! This is neccessary because the slot doesn't exist unless the parameter is added to the emitter, so adding the parameter while particles exist would result in a runtime error. We're almost done, now it's time to let the rubber hit the road:

Modifying particles

Basically, OnLifetime() can be used to modify any particle property during a particle's lifetime.

public override bool OnLifetime(MBParticle PT) 
{
        if (!Spline || !Spline.IsInitialized || !PT.HasUserDataValue(mAngleSlotID) || !PT.HasUserDataValue(mRadiusSlotID))
            return false; // missing data or not ready yet, so kill particle
...

Before calculating anything, we want to make sure that everyhing is in place. If there's no spline assigned or a UserData slot doesn't have a value, we let the particle die. Alternatively we could have returned true here to leave the particle untouched!

Next, we want to clear all existing particles of the current emitter, if the movement direction changes:

if (mLastBackwardStatus != Backward) {
            PT.Parent.Clear();
            mLastBackwardStatus = Backward;
        }

Now we retrieve the radius and angle previously stored in our particle by OnBirth():

float radius = (float)PT.UserData[mRadiusSlotID];
float angle = (float)PT.UserData[mAngleSlotID];

Now for some Curvy specific code: We use the age of the particle to interpolate a tube position around our spline:

// Get TF based on Age and Direction
float tf = (Backward) ? 1-PT.AgePercent: PT.AgePercent;
// NOTE: We retrieve the affected spline segment and use it directly. 
// Instead of this, you can use the corresponding Spline methods using TF, but then Curvy
// needs to find the affected segment each time which is slightly slower!

// Get the segment and local F for current TF
float localF;
CurvySplineSegment seg=Spline.TFToSegment(tf,out localF); 
// Calculate Point in tube
// Radius can be altered by changing Control Point's scale x-axis
PT.Position = Spline.GetExtrusionPoint(seg.InterpolateFast(localF), // Position
                                       seg.GetTangentFast(localF), // Tangent
                                       seg.GetOrientationUpFast(localF), // Up-Vector
                                       seg.InterpolateScale(localF).x * radius, // Radius
                                       angle); // Angle
return true; // Particle stays alive!

If you don't own Curvy, ignore most of this code and focus on the interesting line where we set the position (PT.Position=…).

That's it!

Easy, ain't it?

Last modified: 2015/07/29 20:33 by Jake